【C++】auto_ptr为何被唾弃?以及其他智能指针的学习

搭配异常可以让异常的代码更简洁

文章目录

  • 智能指针
  •     内存泄漏的危害
  •     1.auto_ptr(非常不建议使用)
  •     2.unique_ptr
  •     3.shared_ptr
  •     4.weak_ptr
  • 总结


智能指针

C++中为什么会需要智能指针呢?下面我们看一下样例:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

 在上面的代码中,一旦出现异常那就会造成内存泄漏,什么是内存泄漏呢:

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏分类:
堆内存泄漏 (Heap leak)
堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一
块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如果func函数里的p1new的时候抛异常该怎么办呢?对于第一个new如果抛异常会直接跳到main函数中的catch被捕获,那么p2new失败了会怎么办呢?div函数抛异常我们可以捕获一下:

 那么如果是p2失败了我们还要再catch一下:

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
		try
		{
			cout << div() << endl;
		}
		catch (...)
		{
			delete p1;
			delete p2;
			throw;
		}
	}
	catch (...)
	{
		delete p1;
		delete p2;
	}
}

 那么这样的代码看起来会不会有些冗余呢?为了处理这样的问题,智能指针就能起到很好的作用:

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{

	}
	~SmartPtr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}
private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	SmartPtr<int> sp1(p1);
	int* p2 = new int;
	SmartPtr<int> sp2(p2);
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

 可以看到有了智能指针即使抛异常了我们没有释放的空间也会被自动释放,因为抛异常后自定义类型出了作用域我们智能指针的析构函数会将这个空间释放。当然,我们的智能指针也可以直接创建资源,比如下面这样:

 但是我们这样写不就不可以对指针解引用访问指针的资源了吗?其实我们只需要再给智能指针多加加个功能让它变得像指针一样就解决了这个问题:

template <class T>
class SmartPtr
{
public:
	//保存资源
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{

	}
	//释放资源
	~SmartPtr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

 这个时候我们来使用一下这个指针:

 可以看到现在我们的这个智能指针也可以正常使用了,再次说明一下:智能指针的构造函数是为了保存资源,析构函数是为了释放资源,其他功能是为了和指针一样。我们上面将资源管理的责任托管给对象的做法就叫做RAII(资源获得即初始化),这就是避免内存泄漏的一种方法。

避免内存泄漏:
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。 ps
这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智
能指针来管理才有保证。
2. 采用 RAII 思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。 ps :不过很多工具都不够靠谱,或者收费昂贵。
总结一下 :
内存泄漏非常常见,解决方案分为两种: 1 、事前预防型。如智能指针等。 2 、事后查错型。如泄
漏检测工具。

那么C++库里面有没有智能指针呢?答案是有的,并且有好几种。

auto_ptr:

下面我们看一下C++中被骂了很多年的一款智能指针auto_ptr.

 上图中出错的原因是重复析构了两次sp1,这是因为我们用的编译器自动生成的拷贝构造,是个浅拷贝。auto_ptr的最大问题在于就像我们的SmartPtr一样支持拷贝但是又会让另一个指针悬空,什么意思呢,我们先来调试看一下然后把代码写出来:

 上图是拷贝之前sp1的资源

 上图是拷贝之后sp2的资源,我们可以看到auto_ptr的拷贝就是将资源管理权转移,原本sp1指向的内容被sp2指向了,但是问题就在于auto_ptr竟然让原先的sp1指针悬空了也就是说什么也没指向,这就导致不知道的人对原先sp1这个指针解引用等操作,这样就对空指针进行解引用了,这就是auto_ptr被吐槽的根源所在。下面我们看看auto_ptr是如何实现的:

namespace sxy
{
	template <class T>
	class auto_ptr
	{
	public:
		//保存资源
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{

		}
		//拷贝构造
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		//释放资源
		~auto_ptr()
		{
			delete _ptr;
			cout << _ptr << endl;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
	};
}

 上面代码中大家重点看auto_ptr的构造函数就可以了:

 一旦我们对之前的空指针sp1进行解引用操作程序立马就挂掉了。注意:auto_ptr这款指针指针,很多公司都明确规定不能使用它,如果有面试官让你写一款智能指针,一定不要写auto_ptr!!!

下面我们讲解三种算是经常被使用的智能指针:unique_ptr,   shared_ptr,    weak_ptr.我们可以先看看unique_ptr是如何实现的:

unique_ptr:

 其实unique_ptr的实现很简单,就是直接禁掉了拷贝构造函数和赋值重载。

template <class T>
	class unique_ptr
	{
	public:
		//保存资源
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{

		}
		//拷贝构造
		unique_ptr(const unique_ptr<T>& up) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
		//释放资源
		~unique_ptr()
		{
			delete _ptr;
			cout << _ptr << endl;
		}
		//像指针一样
		//.........
	private:
		T* _ptr;
	};

 后面像指针一样的功能都是相同的我们就直接删掉了,可以看到unique_ptr的实现还是非常简单粗暴的。

当然也不能都不能拷贝吧,所以又出现了一个可以进行拷贝的指针指针shared_ptr,这个智能指针可算是在这之中最优秀的了,下面我们来讲讲:

shared_ptr:

int main()
{
	shared_ptr<int> sp1(new int(0));
	shared_ptr<int> sp2(sp1);
	(*sp1)++;
	(*sp2)++;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	return 0;
}

 我们可以看到shared_ptr不仅可以拷贝还没有之前那么多的问题,那么shared_ptr是如何实现的呢?实际上shared_ptr的是借助引用计数实现的,我们可以调试看一下:

 经过拷贝后引用计数由1变成了2,如下图:

 那么如何实现引用计数比较好呢?是在类中加一个私有成员变量count吗?首先直接加私有变量肯定是不可以的,因为我们的引用计数是要所有的类对象都能看到并且只有一份,如果是私有变量count那么多个对象每个对象都有一个count就不叫引用计数了。那么能否用static静态成员变量呢?静态成员变量不就是所有对象都有的吗?注意:静态的是不可以的,静态变量是属于整个类的,前三个指针指向的都是同一块资源计数为3,然后第四个指针指向不同的资源,这个不同的指针的计数器应该是1才对,但是如果将静态计数器改为1那么前三个指针的计数右不对了,所以不能使用静态变量。这里我们可以使用静态的map来做计数器,让每个不同的资源与计数器做一个KV映射,拷贝哪个资源就映射到map让V值++即可,这里提供一个最好的方式:多开一个指针,这个指针里保存的就是一个计数器,相同拷贝的资源里的计数器指针直接指向这个计数器即可。

了解了这个后我们就来实现一下,不理解的也没关系看着下面的代码就理解了:

template <class 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);
		}
		//释放资源
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
		}
		//像指针一样
		//.......
	private:
		T* _ptr;
		int* _pcount;
	};

 首先类中私有成员多了一个_pcount的引用计数,注意:这是在堆上开的空间。我们在构造函数初始化的时候,每当有新的对象被创建我们就给这个引用计数初始化为1,释放资源的时候我们不能直接释放,因为有可能其他拷贝的对象和我们指向同一块资源,所以这个时候我们只需要将引用计数--即可,注意:我们用的前置--只要进入判断语句就会先解引用拿到计数器的值然后--之后才会判断,即使判断条件不满足还是会--计数器,只有当计数器为0说明没有对象在指向这个资源了,那么这个时候就可以将资源释放了,释放的时候记得将引用计数也释放了防止内存泄漏。我们的拷贝构造就非常简单了,直接让ptr和pcount指向被拷贝的那个对象的资源,然后让计数器++就行了。

当然即使支持了拷贝构造那么赋值重载也是能支持的,因为已经不惧怕拷贝了嘛。对于赋值重载我们一定要铭记:防止相同资源进行赋值,防止直接释放资源导致其他对象不能使用其资源,下面我们给出代码:

//赋值重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}

 首先我们判断了相同资源赋值的情况,因为我们本身是被sp赋值的,所以我们本身的计数一定会少一个,一旦计数少了那么就要判断是否需要释放资源,所以我们还是减自身的计数器,如果减到0了我们就将自身的资源释放掉,如果没有到0就不释放,然后获取sp的资源和引用计数,因为sp赋值给我们我们本身少了一个sp多了一个,所以获取sp的计数器资源后我们还要加加一下计数器。下面我们验证一下是否正确:

int main()
{
	sxy::shared_ptr<int> sp1(new int(0));
	sxy::shared_ptr<int> sp2(sp1);
	(*sp1)++;
	(*sp2)++;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	sxy::shared_ptr<int> sp3 = sp1;
	sxy::shared_ptr<int> sp4(new int(10));
	sxy::shared_ptr<int> sp5 = sp4;
	return 0;
}

 可以看到程序是没有问题的,不管是赋值还是拷贝都可以完成任务。下面我们提出一个新问题:如果我们目前的场景是多线程并发的,那么引用计数还能正确的计数吗我们来看看:

#include <thread>

int main()
{
	sxy::shared_ptr<int> sp(new int(1));
	int n = 10000;
	thread t1([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<int> cp1(sp);
			}
		});
	thread t2([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<int> cp2(sp);
			}
		});
	t1.join();
	t2.join();
	cout << sp.use_count() << endl;
	return 0;
}

首先我们创造了一个场景,这个场景是两个线程多次对sp这个智能指针进行拷贝,最后我们输出这个智能指针的引用计数,注意:shared_ptr中通常会有use_count这个接口返回当前资源的引用计数,实现如下:

        int use_count() const
		{
			return *_pcount;
		}

注意:如果use_count返回1是正确的,因为我们是在返回前打印的,所以这个时候还有sp这个指针指向这个资源,所以是1.下面我们运行起来:

 如果我们多运行几次就会发现程序有时候是正常的有时候是不正常的,那么这种情况一定是有问题的,那么该如何解决这个问题呢?其实很简单加锁就可以了。

因为我们的shared指针可以支持拷贝和赋值,所以我们定义锁的时候还是像引用计数一样不能让每个对象都有一个锁,并且这个锁还要支持赋值等操作,要知道库里的锁是不支持赋值的直接禁掉了,所以我们只需要定义一个锁的指针,赋值的时候把锁资源让另一个对象指向即可。注意:我们的锁一定是要保护每个资源对应的引用计数器的,所以相当于每个对象有三个资源:数据资源,计数器资源,锁资源。

当然加锁之前我们还可以优化一下:

template <class 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)
			{
				delete _ptr;
				delete _pcount;
			}
		}
		//赋值重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}
		//释放资源
		~shared_ptr()
		{
			Release();
		}
		int use_count() const
		{
			return *_pcount;
		}
		//像指针一样
		//.........
	private:
		T* _ptr;
		int* _pcount;
	};

我们直接用一个Release()函数代替释放资源的函数,这样代码看起来会更简单,下面我们开始加锁:

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

		}
		//拷贝构造
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_pmtx(sp._pmtx)
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		void Release()
		{
			_pmtx->lock();
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_pmtx->unlock();
		}
		//赋值重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;
				_pmtx->lock();
				++(*_pcount);
				_pmtx->unlock();
			}
			return *this;
		}
		//释放资源
		~shared_ptr()
		{
			Release();
		}
		int use_count() const
		{
			return *_pcount;
		}
		//像指针一样
		// ...............
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};

我们构造初始化的时候先给锁的指针开一个锁,拷贝构造如果谁和我们指向同一块资源那么就让他的锁的指针指向我们开好的锁,然后遇到计数器++或者--的地方我们就加锁保护起来,这样在计数器++或--的过程中即使是多线程也依旧是串行的而不是并行的,下面我们运行起来看看能否解决刚刚的问题:

 经过多次的运行后我们发现是没问题的,但是如果有细心的同学应该会发现我们的指针new了锁但是没有释放啊,所以下面我们赶紧加上:

        void Release()
		{
			bool flag = false;
			_pmtx->lock();
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
				flag = true;
			}
			_pmtx->unlock();
			if (flag)
			{
				delete _pmtx;
			}
		}

我们再保护计数器的时候用了锁,所以不能在if语句中释放ptr和pcount资源的时候将锁释放,而是需要一个标志只要计数器为0需要释放资源了那么就将标志标记,最后解锁后将锁释放就好了。

那么我们前面已经说过,锁是保护计数器的,那么指针指向的资源该如何被保护呢?下面我们写个例子:

struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main()
{
	sxy::shared_ptr<Date> sp(new Date);
	int n = 100000;
	thread t1([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<Date> cp1(sp);
				cp1->_day++;
				cp1->_month++;
				cp1->_year++;
			}
		});
	thread t2([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<Date> cp2(sp);
				cp2->_day++;
				cp2->_month++;
				cp2->_year++;
			}
		});
	t1.join();
	t2.join();
	cout << sp.use_count() << endl;
	cout << sp->_year << " : " << sp->_month << " : " << sp->_day << endl;
	return 0;
}

 我们可以看到正常的结果应该是200000才对,对于这种情况我们只需要对资源进行加锁即可:

int main()
{
	sxy::shared_ptr<Date> sp(new Date);
	int n = 100000;
	mutex mtx;
	thread t1([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<Date> cp1(sp);
				mtx.lock();
				cp1->_day++;
				cp1->_month++;
				cp1->_year++;
				mtx.unlock();
			}
		});
	thread t2([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				sxy::shared_ptr<Date> cp2(sp);
				mtx.lock();
				cp2->_day++;
				cp2->_month++;
				cp2->_year++;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	cout << sp.use_count() << endl;
	cout << sp->_year << " : " << sp->_month << " : " << sp->_day << endl;
	return 0;
}

 加锁后我们运行多次答案依旧是正确的,所以一定要注意:shared_ptr的锁只保护引用计数,不保护指针所指向的资源。

总结:shared_ptr本身是线程安全的(拷贝和析构时,引用计数++ --是线程安全的)

shared_ptr管理资源的访问不是线程安全的,需要用的地方自行保护。

下面我们讲一下shared_ptr的循环引用问题:

struct ListNode
{
	int val;
	/*ListNode* _next;
	ListNode* _prev;*/
	sxy::shared_ptr<ListNode> _next;
	sxy::shared_ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "distory" << endl;
	}
};

int main()
{
	sxy::shared_ptr<ListNode> n1(new ListNode);
	sxy::shared_ptr<ListNode> n2(new ListNode);
	//n1->_next = n2;
	//n2->_prev = n1;
	return 0;
}

现在有两个链表,我们用指针指针让他们在析构的时候可以自己释放节点的空间,当我们不让n1和n2前后连接时,运行可以正常释放:

 注意:我们为了能在ListNode中将next和prev设为智能指在构造函数中加了缺省参数,否则编译不过去:

 然后我们让n1和n2两个节点前后链接再看看是否可以可以成功释放:

 可以看到释放不了,这就是循环引用,循环引用会导致内存泄漏。下面我们讲讲原理:

 刚开始这两个节点引用计数为1是因为n1和n2都是智能指针,初始化引用计数为1,一点n1的next链接到n2,这个时候相当于n1的next管理了n2(因为我们链表节点中prev和next也是智能指针),这样的话n2就由n1的next和n2本身这个智能指针一起管理,所以引用计数变成2,n1也同理变成2.然后出了n1和n2作用域要析构的时候他们引用计数--变成了1,如下图:

这个时候n1这个资源由n2的prev管理,n2这个资源由n1的next管理,析构的时候没有办法析构了呀,n1要析构那么next管理的资源的引用计数必须减为0但是n2的引用计数不为0,n2要析构那么prev管理的资源的引用计数必须减为0但是n1的引用计数不为0,谁都退出不了就导致了死循环,所以最后就无法成功释放,造成了内存泄漏。为了解决这个问题,weak_ptr就应运而生了。我们刚刚最主要的问题在于next和prev这两个指针参与资源的管理了导致引用计数变了,如果我们让这两个指针不参与资源的管理不就解决了吗,实际上weak_ptr就是一个不参与资源管理的指针,并且weak_ptr是配合shared_ptr使用的。

整体分析:

循环引用分析:
1. node1 node2 两个智能指针对象指向两个节点,引用计数变成 1 ,我们不需要手动
delete
2. node1 _next 指向 node2 node2 _prev 指向 node1 ,引用计数变成 2
3. node1 node2 析构,引用计数减到 1 ,但是 _next 还指向下一个节点。但是 _prev 还指向上
一个节点。
4. 也就是说 _next 析构了, node2 就释放了。
5. 也就是说 _prev 析构了, node1 就释放了。
6. 但是 _next 属于 node 的成员, node1 释放了, _next 才会析构,而 node1 _prev 管理, _prev
属于 node2 成员,所以这就叫循环引用,谁也不会释放。

weak_ptr:

下面是没有用weak_ptr的n1和n2的退出前的引用计数:

 下面是用weak_ptr的n1和n2的退出前的引用计数:

struct ListNode
{
	int val;
	//weak_ptr可以指向资源,访问资源,不参与资源管理,不增加引用计数
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "distory" << endl;
	}
};

 下面我们就实现一下weak_ptr:

template <class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{

		}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{

		}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//由于_ptr是私有成员weak_ptr无法直接访问,所以shared_ptr有一个get()接口是返回_ptr的
			_ptr = sp.get();
			return *this;
		}
        //像指针一样.................
	private:
		T* _ptr;
	};

 首先weak_ptr不接受RAII操作,也就是说单独使用weak_ptr起不到释放的作用,是需要配合shared_Ptr解决循环引用问题的。我们前面说过,weak_ptr不管理资源,引用计数也不会++,所以这个指针只会指向shared_ptr指向的资源。

下面我们就用自己的weak_ptr解决一下循环引用问题:

 通过运行结果可以看到没有任何问题。


总结

1. C++ 98 中产生了第一个智能指针 auto_ptr.
2. C++ boost 给出了更实用的 scoped_ptr shared_ptr weak_ptr.
3. C++ TR1 ,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版。
4. C++ 11 ,引入了 unique_ptr shared_ptr weak_ptr 。需要注意的是 unique_ptr 对应 boost
scoped_ptr 。并且这些智能指针的实现原理是参考 boost 中的实现的。
下一篇文章中我们会讲到和智能指针相关的定制删除器,定制删除器也有很多不一样的玩法。

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

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

相关文章

ThreadPoolExecutor源码剖析

ThreadPoolExecutor源码涉及到的内容比较多&#xff0c;需要一点点的去啃和查看… ThreadPoolExecutor的核心属性 ThreadPoolExecutor的核心属性主要就是CTL。基于CTL获取到线程池的状态以及工作线程个数。 ctl是一个int类型的整数&#xff0c;內部基于AtomicInteger&#xff0…

STM32开发——ADC(烟雾传感器)

目录 1.ADC简介 2.项目简介 3.CubeMX设置 4.函数代码 1.ADC简介 作用&#xff1a;用于读取电压值&#xff0c;然后转换为数字量传给单片机&#xff0c;单片机再通过计算&#xff0c;可以得到电压值。 ADC的性能指标 量程&#xff1a;能测量的电压范围分辨率&#xff1a;A…

详解:阿里邮箱_阿里企业邮箱_阿里邮箱企业版

阿里邮箱是阿里云自主研发的&#xff0c;基于飞天平台自主研发的云原生分布式邮箱系统&#xff0c;阿里邮箱提供免费版、标准版、尊享版和集团版&#xff0c;企业邮箱版本不同支持的账号数也不同&#xff0c;共享网盘容量和个人网盘容量均不同&#xff0c;阿里云百科来详细介绍…

python:并发编程(二十四)

前言 本文将和大家一起探讨python并发编程的实际项目&#xff1a;win图形界面应用&#xff08;篇六&#xff0c;共八篇&#xff09;&#xff0c;系列文章将会从零开始构建项目&#xff0c;并逐渐完善项目&#xff0c;最终将项目打造成适用于高并发场景的应用。 本文为python并…

NCI Architecture

2.1 组成部分 NCI 可分为以下逻辑组件&#xff1a;  NCI 核心 NCI 核心定义了设备主机 (DH) 和 NFC 控制器 (NFCC) 之间通信的基本功能。 这使得 NFCC 和 DH 之间能够进行控制消息&#xff08;命令、响应和通知&#xff09;和数据消息交换。  传输映射 传输映射定义 N…

【C++】哈希unordered系列容器的模拟实现

文章目录 一、哈希表的模拟实现&#xff08;开散列&#xff09;1. 开散列的概念2. 开散列的节点结构3. 开散列的插入删除与查找4. 开散列整体代码实现 二、unordered系列容器的封装实现(开散列)1. 迭代器2. unordered_set和unordered_map的封装实现3. 哈希表整体源码 一、哈希表…

如何让ChatGPT制作XMind思维导图

一、使用ChatGPT辅助生成内容 给大家一个思路&#xff0c;比如我想制作《股神巴菲特给儿女的一生忠告》相关的思维导图&#xff0c;那我们可以在ChatGPT上提问“请使用markdown格式写出股神巴菲特给儿女的一生忠告的思维导图&#xff0c;以代码格式输出”。 生成后&#xff0…

【小沐学Python】Python实现Web服务器(Flask,gevent )

文章目录 1、简介1.1 功能列表1.2 支持平台1.3 安装 2、gevent入门示例2.1 文件IO2.2 MySQL2.3 redis2.4 time2.5 requests2.6 socket2.7 并发抓取文字2.8 并发抓取图片2.9 生产者 - 消费者 3、gevent其他示例3.1 StreamServer3.2 WSGI server3.3 flask3.4 websocket3.5 udp 结…

压测工具Jmeter学习

压测工具Jmeter Jmeter介绍 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 它可以用于测试静态和动态资源&#xff0c;例如静态文件、Java 小服务程序、CGI …

三层交换机与路由互联配置(华为设备)

#三层交换机与路由器配置配置 #三层交换机与路由器配置配置 路由器配置 #进入系统视图 <Huawei>system-view #关闭系统提示信息 [Huawei]undo info-center enable #配置一个环回口 [Huawei]int LoopBack 0 #配置IP地址 与 掩码 [Huawei-LoopBack0]ip address 1.1.…

源码编译LAMP与论坛安装

目录 前言 LAMP工作过程 Apache 1&#xff09;Apache主要特点 如何创建论坛 第一步 关闭防火墙和安全机制 第二、安装相关的Apache服务 1&#xff09;解压压缩包 2&#xff09;安装依赖环境 3&#xff09;配置安装路经等 4&#xff09;编译并安装 5&#xff09;优化…

C++【STL】之反向迭代器

反向迭代器 前面在vector和list的模拟实现中都有讲到正向迭代器&#xff0c;今天我们就来讲解一下反向迭代器的思想和模拟实现&#xff0c;在某些场景下还是很实用的&#xff0c;下面正文直接开始。 文章目录&#xff1a; 反向迭代器1. 反向迭代器结构2. 反向迭代器实现2.1 多…

基于Springboot+vue的汽车租赁系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

5.4.2 网络地址转换NAT

5.4.2 网络地址转换NAT 我们知道为了缓解IPv4地址紧缺的问题&#xff0c;相继出现了一系列缓解地址耗尽的解决方案&#xff0c;比如通过子网划分&#xff08;5.2.8 子网编址&#xff09;实现网络地址在多个物理网络之间的复用&#xff0c;通过无分类编址&#xff08;5.2.9 无分…

Android PagerSnapHelper改造RecyclerView为ViewPage,kotlin

Android PagerSnapHelper改造RecyclerView为ViewPage&#xff0c;kotlin <?xml version"1.0" encoding"utf-8"?> <androidx.recyclerview.widget.RecyclerView xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tool…

端午节粽子(python)

目录 前言 正文 粽叶绘制 粽叶绳绘制 祝福语绘制 源代码 总结 前言 今天端午节&#xff0c;然后昨天也学习一下绘图的相关知识&#xff0c;然后就想看一下能不能画一下&#xff0c;结果还是有点困难的&#xff0c;用CharAI生成简直一言难尽了。后面是找了一个改了一下。 …

【剑指offer刷题记录 java版】数组双指针 之 其它题目

本系列文章记录labuladong的算法小抄中剑指offer题目 【剑指offer刷题记录 java版】数组双指针 之 其它题目 剑指 Offer II 018. 有效的回⽂剑指 Offer 58 - I. 翻转单词顺序剑指 Offer 21. 调整数组顺序使奇数位于偶数前⾯剑指 Offer 57. 和为s的两个数字剑指 Offer II 007. 数…

STM32单片机LED显示屏驱动原理与实现

STM32单片机驱动LED显示屏的原理与实现方法与Arduino类似&#xff0c;但涉及到的具体硬件资源和库函数可能会有所不同。下面是一个详细的介绍&#xff1a; 原理&#xff1a; STM32单片机驱动LED显示屏的原理是通过控制GPIO引脚的电平状态来控制LED的亮灭。通过设置引脚的输出电…

Mybatis源码分析_Mapper接口是如何实例化的 (2)

我们在使用Springmybatis的时候&#xff0c;经常都是直接写一个接口和一个对应的 ***Mapper.xml文件&#xff0c;然后业务代码就可以直接注入这个接口了。它是如何做到的呢&#xff1f; 接口&#xff1a; xml 想搞清楚这个问题&#xff0c;那还是要从Mybatis底层源码进行分析的…

轻量级的深度学习框架Tinygrad

Tinygrad是一个轻量级的深度学习库&#xff0c;它提供了一种简化和直观的方法来理解和实现神经网络。在本文中&#xff0c;我们将探讨Tinygrad及其主要功能&#xff0c;以及它如何成为那些开始深度学习之旅的人的有价值的工具。 什么是Tinygrad? Tinygrad是一个开源的深度学习…