C++11智能指针

目录

一、智能指针的初步认识

1.1 使用场景

1.2 原理

二、std::auto_ptr

2.1 管理权转移

2.2 auto_ptr的模拟实现

三、std::unique_ptr

四、std::shared_ptr

4.1 基础设计

4.2 线程安全问题

4.3 定制删除器

五、std::weak_ptr

六、C++11与Boost中智能指针的关系


一、智能指针的初步认识

1.1 使用场景

使用智能指针是解决内存泄露问题的良好手段

int Div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	//...
	cout << Div() << endl;
	//...
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

执行上述代码时,若用户输入的除数为0,那么Div()函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func()函数中申请的内存资源没有得到释放

利用异常的重新捕获解决

对于这种情况,可以在func()函数中先对Div()函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出

int Div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	try
	{
		cout << Div() << endl;
	}
	catch (...)
	{
		delete ptr;
		throw;
	}
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

但这种方式并完全不可靠,有时可能会疏忽一些异常情况

利用智能指针解决

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
	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);//是否抛异常都会释放
	SmartPtr<int> sp2(new int);

	*sp1 = 0;
	*sp2 = 2;

	cout << Div() << endl;
}

int main()
{
	try {
		Func();
	}
	catch (exception& e) {
		cout << e.what() << endl;
	}
	return 0;
}

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间的地址保存起来
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放
  • 为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载

无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放

1.2 原理

实现智能指针时需要考虑以下三个方面的问题:

  • 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性
  • 对*和->运算符进行重载,使得该对象具有像指针一样的行为
  • 智能指针对象的拷贝问题

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

智能指针对象的拷贝问题

对于当前实现的SmartPtr类,若用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷贝构造

	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷贝赋值
	
	return 0;
}

原因如下:

  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次
  • 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放

智能指针就是要模拟原生指针的行为,当将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里就应该进行浅拷贝。但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针

二、std::auto_ptr

2.1 管理权转移

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

#include <iostream>
using namespace std;
int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;
	//*ap1 = 20; //error

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	cout << *ap3 << endl;//2
	//cout << *ap4 << endl;//error
	return 0;
}

但使用管理权转移的方式来解决问题并不优秀。对象的管理权转移后也就意味着,不能再用该对象对原来管理的资源进行访问了,否则程序就会崩溃

使用auto_ptr之前必须先了解其机制,否则程序极易出问题,很多公司也规定禁止使用auto_ptr

2.2 auto_ptr的模拟实现

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
  • 对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为
  • 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,将传入对象管理资源的指针置空
  • 在拷贝赋值函数中,将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空
namespace bjy
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr):_ptr(ptr) {}
		~auto_ptr() {
			if (_ptr != nullptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
		auto_ptr(auto_ptr<T>& ap) {
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		auto_ptr& operator=(auto_ptr<T>& ap) {
			if (this != &ap) {
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
	private:
		T* _ptr;//指向所管理的资源
	};
}

三、std::unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,ji即简单粗暴的防止对智能指针对象进行拷贝,保证资源不会被多次释放

int main()
{
	std::unique_ptr<int> up1(new int(10));
	//std::unique_ptr<int> up2(up1); //error
	return 0;
}

但防拷贝其实也不是一个很好的办法,总有一些场景需要进行拷贝

unique_ptr的模拟实现

  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
  2. 对 * 和 -> 运算符进行重载,使unique_ptr对象具有指针一样的行为
  3. 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式=delete,防止外部调用
namespace bjy
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr) :_ptr(ptr) {}
		~unique_ptr() {
			if (_ptr != nullptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
		//防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr& operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;//指向所管理的资源
	};
}

四、std::shared_ptr

4.1 基础设计

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

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源
  • 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--
  • 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放

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

#include <iostream>
int main()
{
	std::shared_ptr<int> sp1(new int(1));
	std::shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	std::cout << sp1.use_count() << std::endl; //2

	std::shared_ptr<int> sp3(new int(1));
	std::shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	std::cout << *sp3 << std::endl;//2
	std::cout << sp3.use_count() << std::endl; //2
	return 0;
}

注意: use_count()成员函数,用于获取当前对象管理的资源对应的引用计数

shared_ptr的模拟实现

  • 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前有一个对象在管理该资源
  • 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++
  • 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(若减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
  • 在析构函数中,将管理资源对应的引用计数--,若减为0则需要将该资源释放
  • 对 * 和 -> 运算符进行重载,使shared_ptr对象具有指针一样的行为
namespace bjy
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new size_t(1)) {}
		~shared_ptr() 
		{
			if (--(*_pCount) == 0) 
			{
				if (_ptr != nullptr) {//shared_ptr可能管理的是0地址处的空间
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pCount;
				_pCount = nullptr;
			}
		}
		shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount) {
			++(*_pCount);
		}
		shared_ptr<T>& operator=(shared_ptr<T>& sp) {
			if (_ptr != sp._ptr)
			{
				if (--(*_pCount) == 0) {//若引用计数为0,则释放该对象
					delete _ptr;
					delete _pCount;
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}
			return *this;
		}

		size_t GetCount() { return *_pCount; }
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; };
	private:
		T* _ptr;
		size_t* _pCount;
	};
}

为什么引用计数需要存放在堆区?

shared_ptr中的引用计数不能单纯的定义成一个整型类型的成员变量,否则每个shared_ptr对象都有各自的引用计数,而当多个对象要管理同一个资源时,这些对象应该用的是同一个引用计数

shared_ptr中的引用计数也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数

若将shared_ptr中的引用计数定义成一个指针,当资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,若有其他对象也想要管理这个资源,那么除了需要这个资源的地址之外,还需要引用计数的地址 。此时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数,相当于将各个资源与其对应的引用计数进行了绑定

注意:由于引用计数的内存空间也是在堆上开辟的,因此当资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放

4.2 线程安全问题

存在问题

当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题

如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁

在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作

加锁解决问题

引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数的地址交给当前对象管理之外,还需要将对应的互斥锁的地址也交给当前对象
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,还需要将对应的互斥锁进行释放
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef()函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成ReleaseRef()函数,只需对AddRef()和ReleaseRef()函数进行加锁保护即可
#include <mutex>
using std::mutex;
using std::unique_lock;
namespace bjy
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pCount(new size_t(1)), _pMtx(new mutex) {}
		~shared_ptr() { ReleaseRef(); }
		shared_ptr(shared_ptr<T>& sp) :_ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx) {
			AddRef();
		}
		shared_ptr<T>& operator=(shared_ptr<T>& sp) {
			if (_ptr != sp._ptr) {//管理同一块空间的对象之间不需进行赋值操作
				ReleaseRef();
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMtx = sp._pMtx;
				AddRef();
			}
			return *this;
		}

		size_t GetCount() { return *_pCount; }
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; };

	private:
		void AddRef() {
			unique_lock<mutex> lock(*_pMtx);
			++(*_pCount);
		}
		void ReleaseRef()//flag为true表示引用计数已为0,需要删除锁
		{
			bool flag = false;
			{
				unique_lock<mutex> lock(*_pMtx);
				if (--(*_pCount) == 0) //引用计数完成--
				{
					if (nullptr != _ptr) {
						delete _ptr;
						_ptr = nullptr;
					}
					delete _pCount;
					_pCount = nullptr;
					flag = true;
				}
			}
			if (flag == true) delete _pMtx;
		}
	private:
		T* _ptr;
		size_t* _pCount;
		mutex* _pMtx;
	};
}

  • 在ReleaseRef()函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后是否需要释放互斥锁资源
  • shared_ptr只需要保证引用计数的线程安全问题,而不需要保证资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证

4.3 定制删除器

定制删除器的用法

当智能指针对象的生命周期结束时,所有的智能指针默认都是以 delete 的方式将资源释放,但是智能指针并不是只管理以 new 方式申请到的内存空间,智能指针管理的也可能是以 new[] 的方式申请到的空间,或管理的是一个文件指针

#include <iostream>
struct ListNode 
{
	ListNode* _prev;
	ListNode* _next;
	size_t _value;
	~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);//err
	std::shared_ptr<FILE> sp2(std::fopen("test.cpp","w"));//err
	return 0;
}

以 new[] 的方式申请到的内存空间必须以 delete[] 的方式进行释放,而文件指针必须通过调用 fclose 函数进行释放

这时需要定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数

template <class U, class D>
shared_ptr (U* p, D del);
  • 参数p:需要让智能指针管理的资源的地址
  • 参数del:删除器,为一个可调用对象,如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象
#include <iostream>
struct ListNode 
{
	ListNode* _prev;
	ListNode* _next;
	size_t _value;
	~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
template<class T>
struct DelArrary
{
	void operator()(const T* ptr) {
		std::cout << "delete[]: " << ptr << std::endl;
		delete[] ptr;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArrary<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "w"), [](FILE* ptr) {
		std::cout << "fclose: " << ptr << std::endl;
		fclose(ptr);
	});

	return 0;
}

定制删除器的模拟实现

  • C++标准库中实现shared_ptr时是分成了很多类的,因此C++标准库中可以将删除器的类型设置为构造函数的模板参数,然后将删除器的类型在各个类之间进行传递。
  • 但本次模拟实现是直接用一个类来模拟实现shared_ptr的,因此不能将删除器的类型设置为构造函数的模板参数。因为删除器不是在构造函数中调用的,而是需要在ReleaseRef()函数中进行调用,因此势必需要用一个成员变量将删除器保存下来,而在定义这个成员变量时就需要指定删除器的类型,因此模拟实现的时候不能将删除器的类型设置为构造函数的模板参数
  • 要在当前模拟实现的shared_ptr的基础上支持定制删除器,就只能给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,若用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源
namespace bjy
{
	//默认的删除器
	template<class T>
	struct Delete
	{
		void operator()(const T* ptr) {
			delete ptr;
		}
	};
	template<class T, class D = Delete<T>>
	class shared_ptr
	{
	private:
		void ReleaseRef()
		{
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					_del(_ptr); //使用定制删除器释放资源
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmutex->unlock();
			if (flag == true)
			{
				delete _pmutex;
			}
		}
		//...
	public:
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
			, _del(del)
		{}
		//...
	private:
		T* _ptr;        //管理的资源
		int* _pcount;   //管理的资源对应的引用计数
		mutex* _pmutex; //管理的资源对应的互斥锁
		D _del;         //管理的资源对应的删除器
	};
}
  • 若传入的删除器是仿函数,那么需要在构造shared_ptr对象时指明仿函数的类型
  • 若传入的删除器是一个lambda表达式更为麻烦,因为lambda表达式的类型不太容易获取。可以将lambda表达式的类型指明为一个包装器类型,让编译器传参时自行进行推演,也可以先用auto接收lambda表达式,然后再用decltype来声明删除器的类型
template<class T>
struct DelArr
{
	void operator()(const T* ptr) {
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	//仿函数示例
	cl::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());

	//lambda示例1
	cl::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	});

	//lambda示例2
	auto f = [](FILE* ptr) {
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	};
	cl::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "w"), f);

	return 0;
}

五、std::weak_ptr

  • 该类型指针通常不单独使用(没有实际用处),只能和shared_ptr搭配使用。可以将weak_ptr视为shared_ptr指针的一种辅助工具
  • 借助weak_ptr类型指针,可以获取shared_ptr指针的一些状态信息,如有多少指向相同的shared_ptr指针,shared_ptr指针指向的堆内存是否已经被释放等
  • 当weak_ptr类型指针的指向和shared_ptr指针相同时,weak_ptr并不会使资源的引用计数加1
  • 当weak_ptr指针被释放时,之前所指堆内存的引用计数也不会因此而减1,即weak_ptr并不会影响所指堆内存空间的引用计数
  • weak_ptr<T>模板类中没有重载 * 和 -> 运算符,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它

shared_ptr的循环引用问题

shared_ptr的循环引用问题在一些特定的场景下才会产生。如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。以 new 的方式在堆上构建两个结点,并将这两个结点连接起来,在程序的最后以 delete 的方式释放这两个结点

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}

上述程序没有问题,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点的赋值操作能够执行,就需要把ListNode类中的_next和_prev成员变量的类型也改为shared_ptr类型

struct ListNode
{
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	size_t _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...

	return 0;
}

这时程序运行结束后两个结点都没有被释放,但若是去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用

当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1

将这两个结点连接起来后,资源1当中的_next成员与node2一同管理资源2,资源2中的_prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2

当出了main()函数的作用域后,node1和node2的生命周期都结束了,因此这两个资源对应的引用计数都减到了1

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2中的_prev成员,而资源2的释放取决于资源1中的_next成员
  • 而资源1当中的_next成员的释放又取决于资源1,资源2当中的_prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放

而若连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因

weak_ptr解决循环引用问题

weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,主要是用来解决shared_ptr的循环引用问题的。weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数

将ListNode中的_next和_prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	size_t _val = 10;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	//...
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

weak_ptr的模拟实现

namespace bjy
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr(const weak_ptr<T>& wp)noexcept :_ptr(ptr) {}
		weak_ptr(const shared_ptr<T>& sp)noexcept :_ptr(sp.get()) {}
		weak_ptr& operator=(const shared_ptr<T>& sp) {
			_ptr = sp.get();
			return *this;
		}
		T& operator*() { return *_ptr; };
		T* operator->() { return _ptr; };
	private:
		T* _ptr;
	};
}

利用shared_ptr的成员函数get()获取裸指针

六、C++11与Boost中智能指针的关系

  1. C++98中产生了第一个智能指针auto_ptr
  2. C++ boost库给出了更实用的scoped_ptr、shared_ptr和weak_ptr
  3. C++TR1,引入了boost中的shared_ptr等,不过TR1并不是标准版
  4. C++11,引入了boost库中的unique_ptr、shared_ptr和weak_ptr。unique_ptr对应的是boost库中的scoped_ptr,并且C++11中的智能指针的实现原理是参考boost中实现的

注意:

Boost是为C++标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称

Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库

Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎

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

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

相关文章

脱不下孔乙己的长衫,现代的年轻人该怎么办?

“如果我没读过书&#xff0c;我还可以做别的工作&#xff0c;可我偏偏读过书” “学历本该是我的敲门砖&#xff0c;却成了我脱不下的长衫。” 最近&#xff0c;“脱下孔乙己的长衫”在网上火了。在鲁迅的原著小说中&#xff0c;孔乙己属于知识阶级&#xff08;长衫客&#xf…

网络安全工具大合集

还是一句话&#xff0c;功夫再高&#xff0c;也怕菜刀首先&#xff0c;恭喜你发现了宝藏。本文章集成了全网优秀的开源攻防武器项目&#xff0c;包含&#xff1a;信息收集工具&#xff08;自动化利用工具、资产发现工具、目录扫描工具、子域名收集工具、指纹识别工具、端口扫描…

Json数据传递参数

文章目录Json数据传递参数集合参数&#xff1a;Json格式POJO参数&#xff1a;json格式集合参数&#xff1a;json格式RequestBody与RequestParam的区别时间参数的转换Json数据传递参数 第一步 在pom文件中添加相关配置第二步 作用时开启json数据转换成对象postman发送json数据…

CSS 实现六边形柱状图

前言 &#x1f44f;CSS 实现六边形柱状图 速速来Get吧~ &#x1f947;文末分享源代码。记得点赞关注收藏&#xff01; 1.实现效果 2.实现步骤 定义全局css变量&#xff0c;柱状宽度为–w&#xff0c;最大高度为–h&#xff0c;柱形整体为渐变色&#xff0c;定义上部分颜色为…

【STL三】序列容器——array容器

【STL三】序列容器——array一、array简介二、头文件三、模板类四、成员函数1、迭代器2、元素访问3、容量4、操作五、demo1、容量&#xff08;不使用迭代器&#xff09;2、使用迭代器3、元素访问 at()、front()、back()、data()一、array简介 array 容器是 C 11 标准中新增的序…

ChatGPT能否取代程序员?

目录ChatGPT能否取代程序员&#xff1f;ChatGPT和程序员的工作内容和工作方式ChatGPT和程序员的共同点程序员的优势程序员的实力ChatGPT和程序员的关系结论惊喜ChatGPT能否取代程序员&#xff1f; ChatGPT是一种非常普遍的人工智能&#xff08;AI&#xff09;系统&#xff0c;…

基于springboot家政服务管理系统(程序+数据库+文档)

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

基于java+SpringBoot+Vue的论坛管理系统设计与实现【源码(完整源码请私聊)+论文+演示视频+包运行成功】

博主介绍&#xff1a;专注于Java技术领域和毕业项目实战 &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案例&#xff08;200套&#xff09; 目录 一、效果演示 二、…

MyBatis --- 缓存、逆向工程、分页插件

一、MyBatis的缓存 1.1、MyBatis的一级缓存 一级缓存是SqlSession级别的&#xff0c;通过同一个SqlSession查询的数据会被缓存&#xff0c;下次查询相同的数据&#xff0c;就会从缓存中直接获取&#xff0c;不会从数据库重新访问 使一级缓存失效的四种情况&#xff1a; 1、…

Python生日蛋糕

目录 前言 底盘 蛋糕 蜡烛 祝福 前言 Hello&#xff0c;小伙伴们晚上好吖&#xff01;前两天博主满20岁啦&#xff08;要开始奔三辽呜呜呜&#xff09;&#xff0c;这几天收到了不少小伙伴们的祝福&#xff0c;浪漫的小博主想送给大家一份不一样的生日蛋糕&#xff0c…

【Linux】学会这些基本指令来上手Linux吧

前言上篇文章介绍了一些常用的指令&#xff0c;这篇文章再来介绍一下Linux必须学会的指令。一.时间相关的指令ate显示date 指定格式显示时间&#xff1a; date %Y:%m:%d date 用法&#xff1a;date [OPTION]... [FORMAT]1.在显示方面&#xff0c;使用者可以设定欲显示的格式&am…

2023最全最牛的Jmeter接口测试教程及接口测试详情,你不知道的东西太多了!

下边是详细的jmeter接口测试入门到精通的详细教程&#xff0c;还有视频版本教您实战操作&#xff01; 2023年B站最新Jmeter接口测试实战教程&#xff0c;精通接口自动化测试只需要这一套视频_哔哩哔哩_bilibili2023年B站最新Jmeter接口测试实战教程&#xff0c;精通接口自动化…

算法套路四——反转链表

算法套路四——反转链表 算法示例一&#xff1a;LeetCode206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 初始化pre为空&#xff0c;cur为头指针 pre指针&#xff1a;记录当前结点的前一个结点 cur指针&#xff1a;记录当…

SpringBoot整合MongoDB

参考链接 https://www.mongodb.org.cn/ 文章目录一、前言1.1 NoSQL介绍1.1.1 NoSQL 数据库分类1.1.2 NoSQL的优点/缺点1.1.3 BASE1.2 MongoDB介绍1.2.1 MongoDB和SQL对比1.2.2 数据库1.2.3 元数据1.2.4 MongoDB 数据类型二、SpringBoot整合MongDB2.1 环境配置2.2 MongoTemplate…

XCPC第十一站,带你学会图论基本算法

我们约定&#xff1a;以下n表示点的数目&#xff0c;m表示边的数目。 引子1——邻接表存储图的方法&#xff08;&#xff09;&#xff08;暂时不考虑重边和自环&#xff09; 现在我们有n个点&#xff08;编号为1~n&#xff09;和m条边&#xff0c;要用数组存储它们&#xff0c…

大数据模型、离线架构、实时架构

一.大数据模型 8种常见的大数据分析模型&#xff1a;1、留存分析模型&#xff1b;2、漏斗分析模型&#xff1b;3、全行为路径分析&#xff1b;4、热图分析模型&#xff1b;5、事件分析模型&#xff1b;6、用户分群模型&#xff1b;7、用户分析模型&#xff1b;8、黏性分析模型…

10 个超赞的 C 语言开源项目

今天给大家分享10个超赞的C语言开源项目&#xff0c;希望这些内容能对大家有所帮助&#xff01;01.WebbenchWebbench是一个在 Linux 下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL&#xff0c;测试网站在压力下工作的性能。最多可以模拟 3…

Mysql 时区差8小时的多种问题 统统解决

笑小枫专属目录背景知识点代码中常见的三种时间差错问题【我遇到的】本地获取的时间没有错&#xff0c;存入数据库的时候时间相差8小时java下使用 new date()获取的时间会和真实的本地时间相差8小时数据库时间没有错&#xff0c;获取到了后端&#xff0c;之后返回给前端相差8小…

Android 不申请权限储存、删除相册图片

Android 不申请权限储存、删除相册图片 前言 最近重新看了下安卓的储存适配&#xff0c;并结合之前做的拍照、裁切demo&#xff0c;小小实验了一下。Android 6.0增加了动态文件权限申请; Android 7.0需要使用FileProvider来获取Uri&#xff0c;不能直接使用file获得; Android…

FPGA基于RIFFA实现PCIE采集HDMI传输,提供工程源码和QT上位机

目录1、前言2、RIFFA理论基础3、设计思路和架构4、vivado工程详解5、上板调试验证并演示6、福利&#xff1a;工程代码的获取1、前言 PCIE是目前速率很高的外部板卡与CPU通信的方案之一&#xff0c;广泛应用于电脑主板与外部板卡的通讯&#xff0c;PCIE协议极其复杂&#xff0c…