C++智能指针详解

目录

一.   智能指针初识

1.1   什么是智能指针

1.2   智能指针历史历程

 1.3   为什么需要智能指针

1.3.1   内存泄漏

1.3.2   防止内存泄漏

1.3.3   异常的重新捕获

二.   智能指针的原理与使用

2.1   智能指针的原理

2.2   智能指针的使用

2.3   智能指针的拷贝问题

三.   智能指针的众多版本

3.1   auto_ptr

3.2   unique_ptr

3.3   shared_ptr

3.3.1   基础实现 

3.3.2   shared_ptr的循环引用

四.   定制删除器

4.1   定制删除器的使用

 4.2   定制删除器的模拟实现

4.2.1   按照库的实现 

 4.2.2   不按照库的实现


 一.   智能指针初识

1.1   什么是智能指针

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏和空悬指针等等问题。

动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。

1.2   智能指针历史历程

  • C++ 98 中产生了第一个智能指针auto_ptr。
  • C++boost给出了更加实用的scoped_ptr(防止拷贝)shared_ptr(引进引用计数)weak_ptr
  • C++ 11 引入了unquie_ptr shared_ptrweak_ptr .需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中的实现的。

 

 1.3   为什么需要智能指针

1.3.1   内存泄漏

我们在讲为什么之前先来了解一下什么是内存泄漏。

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。

1.3.2   防止内存泄漏

我们来看看这一个代码:

void fxx()
{
	int* p1 = new int[10];
	int* p2 = new int[20];
    int* p3 = new int[30];

	//...

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

如果指针p2或者p3开辟空间new错误,这里就会导致后面的delete不会被执行,这就导致了指针p1的内存泄漏。这里我们可以用异常来解决,但是很难看:

void fxx()
{
	int* p1 = new int[10];
	int* p2, *p3;
	try
	{
		p2 = new int[20];
		try {
			p3 = new int[30];
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
		delete[] p1;
		throw;
	}

	//...

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

1.3.3   异常的重新捕获

我们之前一个博客(http://t.csdnimg.cn/6jA1U)在异常的描述中说了,在异常的重新抛出与捕获中,可以用智能指针解决。我们来看看:

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
 
void fyy() noexcept
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
 
void func()
{
	//这里可以看到如果发生除0错误抛出异常,下面的array数组就没有得到释放
	//所以这里捕获异常但是不处理异常,异常还是交给外面处理,这里捕获了再抛出去
	//就能delete array了
	int* array = new int[10];
	try
	{
		fyy();
	}
	catch (...)
	{
		//捕获异常不是为了处理异常
		//是为了释放内存,然后异常再重新抛出
		cout << "delete[]" << array << endl;
		delete[] array;
		throw;//捕到什么抛什么
	}
	cout << "delete[]" << array << endl;
	delete[] array;
}

但是当有很多个变量要new和delete呢?就跟上面一样,会导致代码的繁琐嵌套,所以我们要用智能指针来解决。

二.   智能指针的原理与使用

2.1   智能指针的原理

智能指针的基本原理是利用RAII

RAII:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class Smartptr
{
public:
	//RAII
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}
	~Smartptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

int main()
{
    Smartptr<int> sp1(new int(1));
	Smartptr<int> sp2(new int(2));
	*sp1 += 10;
	Smartptr<pair<string, int>> sp3(new pair<string, int>);
	sp3->first = "apple";
	sp3->second = 1;
	return 0;
}

2.2   智能指针的使用

template<class T>
class Smartptr
{
public:
	//RAII
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}
	~Smartptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}
	return a / b;
}
void func()
{
	Smartptr<int> sp1(new int(1));
	Smartptr<int> sp2(new int(2));
	*sp1 += 10;
	cout << div() << endl;
}
int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

通过SmartPtr对象,无论程序是正常执行结束,还是因为某些中途原因进行返回,或者抛出异常等开始所面临的困境,只要SmartPtr对象的生命周期结束就会自动调用对应的析构函数,不会造成内存泄漏,完成资源释放。

2.3   智能指针的拷贝问题

如果我们用一个智能指针拷贝构造一个智能指针,或者用一个智能指针赋值给另一个智能指针。这样的操作都会导致程序崩溃。

void test()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1);//拷贝构造
	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4 = sp3;//赋值
}

因为对于我们的智能指针来说,将sp1拷贝给sp2操作是浅拷贝,是将两个指针的指向统一到一块空间。当sp1和sp2释放时,会导致这块空间释放两次。同样的道理,将sp3赋值给sp4的时候,也只是单纯的将指针的指向指到同一块空间,这样在析构的时候也会导致析构两次。

所以对于如何解决这个问题,智能指针分为了很多版本。

三.   智能指针的众多版本

C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。

3.1   auto_ptr

auto_ptr :管理权转移,被拷贝对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空。

auto_ptr是C++98的,通过管理权转移的方式解决智能指针拷贝问题,保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放:

int main()
{
	yjy::auto_ptr<int> ap1(new int(1));
	yjy::auto_ptr<int> ap2(ap1);

	*ap2 += 10;

	//ap1悬空
	//*ap1 += 10;

	return 0;
}

 auto_ptr的模拟实现为:

namespace yjy
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		//ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值

			if (this != &ap)
			{
				// 释放当前对象中资源

				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中

				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}


		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

构造对象获取资源,析构对象释放资源。对*和->运算符进行重载,使其像指针一样。拷贝构造函数,用传入的对象的资源来构造当前对象,并将传入对象管理资源指针悬空。

3.2   unique_ptr

需要引用memory库来使用。

unique_ptr是C++11中的智能指针,unique_ptr来的更直接:直接防止拷贝的方式解决智能指针的拷贝问题,简单而又粗暴,防止智能指针对象拷贝,保证资源不会被多次释放,但是防止拷贝也不是解决问题的好办法,因为在很多场景下是需要拷贝的。

int main()
{
	yjy::unique_ptr<int> sp1(new int(1));

	yjy::unique_ptr<int> sp2(new int(10));
	sp1 = sp2;

	return 0;
}

模拟实现如下:

namespace yjy
{
    template<class T>
	class unique_ptr
	{
	public:
		//RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		//up2(up1)
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

 为了禁止拷贝,所以我们C++98的方式是将拷贝构造函数和拷贝赋值函数声明为私有;C++11的方式就直接在这两个函数后面加上=delete

3.3   shared_ptr

3.3.1   基础实现 

shared_ptr是C++11的智能指针,通过引用计数的方式解决智能指针的拷贝问题。

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。

 引用计数的方式能够支持多个对象一起管理一个资源,也就支持智能指针的拷贝,只有当资源的引用计数减为0时才会释放,保证了同一个资源不会被多次释放:

int main()
{
	yjy::shared_ptr<int> sp1(new int(1));
	cout << sp1.use_count() << endl;

	yjy::shared_ptr<int> sp2(sp1);
	cout << sp2.use_count() << endl;

	*sp2 += 10;
	*sp1 += 10;

	yjy::shared_ptr<int> sp3(new int(2));
	yjy::shared_ptr<int> sp4(sp2);

	cout << sp3.use_count() << endl;
	return 0;
}

模拟实现如下:

namespace yjy
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		// 保存资源
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		// 释放资源
		~shared_ptr()
		{
			Release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		void Release()
		{
			if (--(*_pcount) == 0)
			{
				delete _pcount;
				delete _ptr;
			}
		}
        
        //sp1 = sp1;
        //sp1 = sp2;//sp2如果是sp1的拷贝呢?
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//资源地址不一样
			{
				Release();
				_pcount = sp._pcount;
				_ptr = sp._ptr;
				++(*_pcount);
			}

			return *this;
		}
        
        int use_count()
		{
			return *_pcount;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}
  • 构造函数获取资源时,同时将对应于的引用计数设为1,表示当前一个对象在管理这块资源。
  • 析构函数,将管理资源对应的引用计数--,如果为0就需要进行释放。
  • 拷贝构造函数中,与传入对象一起管理资源,将该资源的引用计数++。
  • 对于拷贝赋值:先将当前对象管理的资源对应的引用计数–,为0时需要释放,然后在传入对象一起管理资源。将该资源对应的引用计数++。

为什么引用计数要用指针?

  • 首先引用计数可不能用int整型来表示,这样的话,每个对象都有一个单独的引用计数。而我们要求当多个对象管理一个资源的时候,应该是引用的同一个引用计数。
  • 其次引用计数也不能设置为静态的,这样的话,结果是同一个类创建的对象都是同一个引用计数,即管理不同资源的对象引用了同一个引用计数。而我们要求的是一个资源对应一个引用计数。

3.3.2   shared_ptr的循环引用

我们来讲一下shared_ptr的美中不足的地方:循环引用

struct ListNode
{
	int _val;
	yjy::shared_ptr<ListNode> _next;
	yjy::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

	~ListNode()
	{
		cout << "ListNode" << endl;
	}
};

int main()
{
	yjy::shared_ptr<ListNode> n1(new ListNode(10));
	yjy::shared_ptr<ListNode> n2(new ListNode(20));

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_next = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

我们可以看到定义了两个对象,对象里面的prev和next对应指向另一个对象,这时候我们的shared_ptr就会存在缺陷。

在我们出作用域销毁的时候,会发生下面的情况:

n2对象销毁时-》_prev指针释放-》n1对象销毁-》_next指针释放-》n2对象销毁

可以看看运行情况:

 

可以看见销毁时是出错了的。

可以看到这个销毁的过程是一个互相影响的过程,是一个死循环。这样的结构就是我们的循环引用。该怎么办呢?

这里就要用到我们的weak_ptr:

//不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
	//RAII
	weak_ptr()
		:_ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& wp)
	{
		_ptr = wp.get();
	}

	weak_ptr<T>& operator=(const shared_ptr<T>& wp)
	{
		_ptr = wp.get();
		return *this;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

 这里的weak_ptr就不涉及RAII,不参与资源管理,从根源上杜绝了这个问题

struct ListNode
{
	int _val;

	yjy::weak_ptr<ListNode> _next;
	yjy::weak_ptr<ListNode> _prev;
	ListNode(int val = 0)
		:_val(val)
	{}

	~ListNode()
	{
		cout << "ListNode" << endl;
	}
};

int main()
{
	yjy::shared_ptr<ListNode> n1(new ListNode(10));
	yjy::shared_ptr<ListNode> n2(new ListNode(20));

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_next = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

 

这样就好了。

四.   定制删除器

4.1   定制删除器的使用

智能指针该如何辨别我们的资源是用new int开辟的还是new int[]开辟的呢,要知道[]必须与delete[]匹配否则会有未知错误的,这个问题我们就交给定制删除器来解决:

这个del参数就是定制删除器,是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

所以当资源不是以new的形式开辟的时候,就要传特定的定制删除器。

比如:

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

struct ListNode
{
	int _val;

	yjy::weak_ptr<ListNode> _next;
	yjy::weak_ptr<ListNode> _prev;
	ListNode(int val = 0)
		:_val(val)
	{}

	~ListNode()
	{
		cout << "ListNode" << endl;
	}
};

int main()
{
	yjy::shared_ptr<ListNode, DeleteArray<ListNode>> n2(new ListNode[10]);
	return 0;
}

 4.2   定制删除器的模拟实现

4.2.1   按照库的实现 

这是按照库的实现方法来的。可以看到模板参数定制删除器是在构造函数的。 

template<class T>
class shared_ptr
{
public:
	template<class D>//为了跟库保持一致,我们在此处定义模板
	shared_ptr(T* ptr, D del)//定制删除器
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

	//RAII
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	//sp2(sp1)
	shared_ptr(const shared_ptr<int>& sp)
	{
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
	}

	void release()
	{
		//说明最后一个管理对象析构了,可以释放资源了
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			//delete _ptr
			_del(_ptr);
			delete _pcount;
		}
	}
	//sp1=sp4
	//sp4=sp4
	//sp1=sp2
	shared_ptr<T>& operator=(const shared_ptr& sp)
	{
		//if(this!=&sp)
		if (_ptr != sp._ptr)
		{
			release();
			_ptr = sp._ptr;
			--(*_pcount);
			_pcount = sp._pcount;
			//拷贝时++计数
			++(*_pcount);
		}
		return *this;
	}

	~shared_ptr()
	{
		//析构时,--计数,计数减到0
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

int main()
{
	yjy::shared_ptr<ListNode> p1(new ListNode(10));
	yjy::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
	yjy::shared_ptr<FILE> p3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
	return 0;
}

nwe[]申请内存空间必须以delete[]方式进行释放,文件指针要fclost进行释放。

 4.2.2   不按照库的实现

下面是不按照库的实现方法:

template <class T>
    struct Delete
    {
        void operator()(T* ptr)
        {
            delete ptr;
        }
    };
    template<class T,class D=Delete<T>>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr)
            :_ptr(ptr)
            ,_pcount(new int(1))
        {}

        shared_ptr(const shared_ptr<T>& sp)
        {
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            ++(*_pcount);
        }

        void release()
        {
            //说明最后一个管理对象析构了,可以释放资源了
            if (--(*_pcount) == 0)
            {
                cout << "delete:" << _ptr << endl;
                //delete _ptr
                _del(_ptr);
                delete _pcount;
            }
        }
        //sp1=sp4
        //sp4=sp4
        //sp1=sp2
        shared_ptr<T>& operator=(const shared_ptr& sp)
        {
            //if(this!=&sp)
            if(_ptr!=sp._ptr)
            {
                release();
                _ptr = sp._ptr;
                --(*_pcount);
                _pcount = sp._pcount;
                //拷贝时++计数
                ++(*_pcount);
            }
            return *this;
        }

        ~shared_ptr()
        {
            //析构时,--计数,计数减到0
            release();
        }

        int use_count()
        {
            return *_pcount;
        }

        // 像指针一样
        T& operator*()
        {
            return *_ptr;
        }

        T* operator->()
        {
            return _ptr;
        }

        T* get() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pcount;
        D _del;
    };

那我们main函数传参的时候就要变换一下:

int main()
{
    yjy::shared_ptr<ListNode, DeleteArray<ListNode>> n2(new ListNode[10]);
    return 0;
}

总结:

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/581090.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

视频抽帧转图片,opencv和ffmpeg效果测评

最近在做一个项目&#xff0c;需要从视频中抽帧转图片&#xff0c;于是对opencv和ffmpeg效果进行了测评。 文章目录 1. open cv2. ffmpeg3.抽帧效果对比 1. open cv open cv 视频抽图片的教程&#xff0c;推荐以下链接&#xff0c;抽的帧数可以自行调节&#xff01; 用pythono…

CSS伪类大全!4大类伪类详解

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端的程序媛。 云桃桃-大专生&#xff0c;一枚程序媛&#xff0c;感谢关注。回复 “前端基础题”&#xff0c;可免费获得前端基础 100 题汇总&#xff0c;回复 “前端工具”&#xff0c;可获取 Web 开发工具合…

[C++基础学习]----01-C++数据类型详解

前言 C是一种静态类型的编程语言&#xff0c;它提供了丰富的数据类型来存储和操作数据。这些数据类型为C程序员提供了丰富的选择&#xff0c;可以根据具体需求来选择最合适的类型来存储和操作数据。下面详细解释一些常见的C数据类型&#xff0c;包括其原理和使用方法&#xff1…

VBA技术资料MF146:发出多次Beep提示声

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

Day20.一刷数据结构算法(C语言版) 669修剪二叉搜索树;108将有序数组转换为二叉搜索树;538把二叉搜索树转换为累加树

一、669修剪二叉搜索树 这道题目比较难&#xff0c;比添加增加和删除节点难的多&#xff0c;建议先看视频理解。 题目链接&#xff1a;修剪二叉搜索树 文章讲解&#xff1a; 代码随想录 视频讲解&#xff1a; 你修剪的方式不对&#xff0c;我来给你纠正一下&#xff01;| 修剪二…

AI预测体彩排列3第2套算法实战化测试第6弹2024年4月28日第6次测试

今天继续进行新算法的测试&#xff0c;今天是第6次测试。好了&#xff0c;废话不多说了&#xff0c;直接上图上结果。 2024年4月28日体彩排3预测结果 6码定位方案如下&#xff1a; 百位&#xff1a;3、2、1、0、5、6、7 十位&#xff1a;3、5、6、2、1、0 个位&#xff1a;3、4…

iOS - 多线程-atomic

文章目录 iOS - 多线程-atomic1. 源码分析1.1 get方法1.2 set方法 2. 一般不使用atomic的原因 iOS - 多线程-atomic atomic用于保证属性setter、getter的原子性操作&#xff0c;相当于在getter和setter内部加了线程同步的锁可以参考源码objc4的objc-accessors.mm它并不能保证使…

【无监督+自然语言】 GPT,BERT, GPT-2,GPT-3 生成式预训练模型方法概述 (Generative Pre-Traning)

主要参考 【GPT&#xff0c;GPT-2&#xff0c;GPT-3 论文精读【李沐论文精读】-2022.03.04】 https://www.bilibili.com/video/BV1AF411b7xQ/ 大语言模型综述&#xff1a; https://blog.csdn.net/imwaters/article/details/137019747 GPT与chatgpt的关系 图源&#xff1a;L…

【Verilog-语法】 条件编译 `ifdef/`ifndef

一、前言 在Verilog项目开发过程中某功能是&#xff0c;一部分代码可能有时候用&#xff0c;有时候不用&#xff0c;为了避免全部编译占用资源&#xff0c;可以使用条件编译语句&#xff1b;尤其在大型项目中还可以节约大量的时间。 二、语法 语法书写格式&#xff1a; &am…

嵌入式Linux学习——Linux常用命令(下)

压缩/解压缩命令 gzip gzip 的常用选项&#xff1a; -l(list) 列出压缩文件的内容。-k(keep) 在压缩或解压时&#xff0c;保留输入文件。-d(decompress) 将压缩文件进行解压缩。 注意&#xff1a; 如果 gzip 不加任何选项&#xff0c;此时为压缩。 压缩完该文件会生成后缀…

web自动化系列-selenium的基本方法介绍

web自动化 &#xff0c;一个老生常谈的话题 &#xff0c;很多人的自动化之路就是从它开始 。它学起来简单 &#xff0c;但做起来又比较难以驾驭 &#xff1b;它的执行效率慢 、但又是最接近于用户的操作场景 &#xff1b; 1.web自动化中的三大亮点技术 我们先聊聊 &#xff0…

hive安装

文章目录 1、下载hive2、安装hadoop&#xff08;略&#xff09;3、安装mysql&#xff08;略&#xff09;4、安装 1、下载hive https://dlcdn.apache.org/hive/hive-3.1.3/ 2、安装hadoop&#xff08;略&#xff09; 3、安装mysql&#xff08;略&#xff09; 4、安装 解压 …

(十一)Servlet教程——Request请求转发

1.Web应用在处理客户端的请求的时候&#xff0c;一般的时候都需要多个资源协同处理&#xff0c;比如先经过一个Servlet的处理&#xff0c;然后再经过另外一个Servlet的处理。但是在一个Servlet中又不能直接调用另外一个Servlet的service方法&#xff0c;所以Servlet就提供了请求…

【嵌入式软件】LWIP 以太网通信

1.互联网模型 1.1 OSI&#xff08;Open System Interconnection&#xff09;七层模型 1&#xff09;应用层&#xff1a; 为上层用户提供应用的接口。常用的应用层的网络协议有&#xff1a;HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP 2&#xff09;表示层&…

【C++】C\C++内存管理

下面是围绕C\C内存管理这一块知识谈论相关的内存管理机制&#xff0c;有需要借鉴即可。 同时&#xff0c;我在下面也放了快速建立链表的模板&#xff0c;方便oj题目拿到vs上进行调试。 内存管理目录 1.CPP内存管理1.1new、delete关键字概念1.2特性1.3总结 2.new、delete的底层…

【C语言】贪吃蛇详解(附源码)

一、贪吃蛇实现效果 【C语言】贪吃蛇&#xff08;控制台&#xff09; 二、源码 &#x1f388;&#x1f388;&#x1f388;Snake 残风也想永存/C语言项目 - 码云 - 开源中国 (gitee.com)&#x1f388;&#x1f388;&#x1f388; 三、如何使用C语言去实现一个贪吃蛇&#xff1f…

每日一题:地下城游戏

恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里&#xff0c;他必须穿过地下城并通过对抗恶魔来拯救公主。 骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0…

ThreeJs 环境配置及遇到问题的解决方法

一、环境搭建 ThreeJs在实际在实际使用中更多的是结合框架开发例如&#xff1a;vue框架、react框架&#xff0c;在使用时需要配置开发环境&#xff0c;本文使用的是vscode ThreeJs NodeJs vue 1、ThreeJs安装 下载路径&#xff1a;GitHub - mrdoob/three.js: JavaScript…

CentOS命令大全:掌握关键命令及其精妙用法!

CentOS是一种流行的开源企业级Linux发行版&#xff0c;它基于Red Hat Enterprise Linux (RHEL)的源代码构建。对于系统管理员和运维工程师来说&#xff0c;掌握CentOS的常用命令至关重要。 这些命令不仅可以帮助管理服务器&#xff0c;还可以进行故障排查、性能监控和安全加固等…

【idea】idea 中 git 分支多个提交合并一个提交到新的分支

一、方法原理讲解 我们在 dev 分支对不同的代码文件做了多次提交。现在我们想要把这些提交都合并到 test 分支。首先我们要明白四个 git 操作&#xff0c; commit&#xff1a;命令用于将你的代码变更保存到本地代码仓库中&#xff0c;它创建了一个新的提交&#xff08;commit…
最新文章