智能指针RAII

引入:智能指针的意义是什么?

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在

对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做

法有两大好处:

·不需要显式地释放资源。
· 采用这种方式,对象所需的资源在其生命期内始终保持有效。

说白了就是为了解决异常引起的内存泄漏!我们知道,如果一个内存在申请和释放这二者之间被抛出异常了,那么有可能就会出现内存泄漏,而智能指针的本质就是将指向这块内存的指针封装成一个类,该指针作为类的对象以后,就会在出作用域是自动的进行析构,所以我们再也不用担心异常引出的内存泄漏问题了!

一:智能指针的使用场景

1:异常导致的内存泄漏

cpp 复制代码
#include<exception>
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int(1); //赋值1 方便监视窗口观察
	//...
	cout << div() << endl;
	//...
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果:

通过监视窗口体现内存泄漏:

2:异常的重新抛出

根据上篇博客,可知,其实这种简单的用异常的重新抛出也可以解决代码:

cpp 复制代码
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;
}

运行结果及监视窗口:

**解释:**即使是抛出异常,但仍然没有内存泄漏!

3:智能指针的实现

对于上面这个内存泄漏的问题,我们还可以采取智能指针来解决

cpp 复制代码
// SmartPtr类(译为智能指针类)
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "析构被调用" << endl;
	}

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);
	cout << div() << endl;
}

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

运行结果:

**解释:**和引入中说的一样,将指向申请的内存的指针封装成一个类,该类的析构函数会被自动的调用,再也不用担心异常引发的内存泄漏了!至于析构函数怎么写,就根据内存怎么申请的来写,这里若是申请的数组,则delete的时候加上[ ]即可!

但是智能指针,是能像指针一样去使用的,也就是可以* -> 等操作,所以我们还要加上*和->的重载,完整版如下:

cpp 复制代码
// SmartPtr类(译为智能指针类)
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "析构被调用" << endl;
	}

    T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};

此时我们func函数变成这样,方便体现*的作用:

cpp 复制代码
void Func()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(new int(2));
	cout << "sp1值为->" << *sp1 << endl;
	cout << "sp2值为->" << *sp2 << endl;

	cout << div() << endl;

}

运行结果:

看过上篇博客的人都知道,有这么一种场景,连异常的重新抛出也解决不了:

cpp 复制代码
void riskyOperation() {
    int* ptr1 = new int(100);  // 内存1
    int* ptr2 = new int(200);  // 内存2
    int* ptr3 = new int(300);  // 内存3
 
    // 模拟后续操作抛出异常
    throw runtime_error("操作失败");
 
    // 正常释放(永远不会执行)
    delete ptr1;
    delete ptr2;
    delete ptr3;
}

我们现在有了智能指针,简直小case啦:

cpp 复制代码
// SmartPtr类(译为智能指针类)
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "析构被调用" << endl;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};

void riskyOperation() {
	SmartPtr<int> ptr1(new int(100));  // 内存1
	SmartPtr<int> ptr2(new int(200));  // 内存2
	SmartPtr<int> ptr3(new int(300));  // 内存3

	// 模拟后续操作抛出异常
	throw runtime_error("操作失败");

	// 正常释放(永远不会执行)

}

int main() {
	try {
		riskyOperation();
	}
	catch (const exception& e) {
		cerr << "捕获异常: " << e.what() << endl;
		// 问题:ptr1/ptr2/ptr3 内存泄漏!
	}
}

运行结果:

完美❀~!

智能指针乍一看,很简单啊,真的如此吗,实则不然~

二:智能指针的问题

1:问题场景

先把智能指针类放这:

cpp 复制代码
// SmartPtr类(译为智能指针类)
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "析构被调用" << endl;
	}

	T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};

智能指针的难点在于两个智能指针间的拷贝或赋值会出问题:

当在main中如下的时候:

cpp 复制代码
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;
}

报错:

2:拷贝赋值的问题本质

**①:**编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。

**②:**编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4现在管理的都是sp4管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp3原来管理的空间没人管理了,所以没有得到释放。

Q:那去手动写拷贝和赋值的深拷贝类型就能解决问题了,对吗?

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

体现指针间的赋值和拷贝本身就是让两个指针指向同一块内存空间的例子:

cpp 复制代码
int main()
{
	int a = 1;
	int b = 2;
	int* p1 = &a;
	int* p2 = &b;

	cout << *p1 << *p2 << endl;

	//拷贝
	int* p3(p1);
	cout << *p1 << *p3 << endl;

	//赋值
	p1 = p2;
	cout << *p1 << *p2 << endl;


	return 0;
}

运行结果:

Q:那岂不是,没办法了,咱们虽然白嫖了类的析构自动调用去成功解决异常引发的内存泄漏,但是呢在拷贝和赋值的时候,却又想避开类带来的影响->类的两次析构,那怎么办呢o(╥﹏╥)o?

**A:**C++官方对于这个问题的解决过程中,过程是曲折的,也走过弯路~,C++的库中的常用的智能指针类我会按照产生时间(也正好是从不优秀 到 优秀)来介绍:

**①:**auto_ptr -> 极其大坑,大多数公司也都明确规定了禁止使用auto_ptr

**②:**unique_ptr -> 略有不妥

**③:**shared_ptr ->完美

**④:**weak_ptr ->某些场景,会和shared_ptr一起使用

三:智能指针的种类

1:auto_ptr

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

cpp 复制代码
int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;
	//*ap1 = 10; //error

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	//*ap4 = 10; //error

	return 0;
}

但你解开任意一个注释的时候,就会报错:

**解释:**被拷贝/赋值对象把资源管理权转移给拷贝/赋值对象,导致被拷贝/赋值对象悬空!

这是一个极其不好的设计,进了公司,用这个就GG,而且面试的时候,问到你了解哪种智能指针,你说你了解这个,你也GG~

2:unique_ptr

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝/赋值的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝/赋值,这样也能保证资源不会被多次释放。比如:

cpp 复制代码
int main()
{

	std::unique_ptr<int> up1(new int(0));
	//std::unique_ptr<int> up2(up1); //error


	std::unique_ptr<int> up3(new int(0));
	//up3 = up1; //error


	return 0;
}

解释: 当你解开注释的时候,会报错尝试引用已经删除的函数!但防拷贝/赋值,其实也不是一个很好的办法,因为总有一些场景需要进行拷贝或者赋值。

3:shared_ptr

最好用最优秀的智能指针就是shared_ptr!
C++11中引入的智能指针shared_ptr,通过引用计数的方式解决智能指针的拷贝问题。

· 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块dain当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再

· 管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。

· 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

引用计数例子:老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。所以只要不是最后一个学生离开教室,都不会锁门,直到最后一个学生离开,才会锁门

同理,只有该资源的引用计数到了0,才会释放资源,反之不会

注意:博主会说资源 也会说空间 知道是一个意思就行啦

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝和赋值,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:

须知: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。

直接用库中的share_ptr ,可以进行随意的赋值拷贝!:

cpp 复制代码
int main()
{
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);

	//体现二者的地址一致
	cout << sp1 << endl;
	cout << sp2 << endl;
	//内容一致
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	//打印内存的引用计数
	cout << sp1.use_count() << endl; //2


	shared_ptr<int> sp3(new int(1));
	shared_ptr<int> sp4(new int(2));
	sp3 = sp4;

	//体现二者的地址一致
	cout << sp3 << endl;
	cout << sp4 << endl;
	//内容一致
	cout << *sp3 << endl;
	cout << *sp4 << endl;
	//打印内存的引用计数
	cout << sp3.use_count() << endl; //2

	return 0;
}

运行结果:

用谁都会用,重点是自己模拟实现shared_ptr,智能指针面试问到,跑不了的模拟实现

①:典型错误实现shared_ptr

实现shared_ptr有一种非常经典的错误实现 ,理解错误在哪,会提升自己,代码:

cpp 复制代码
class wtt
{
public:
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr)
			:_ptr(ptr)
		{
			_count = 1;
		}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			++_count;
		}

		~shared_ptr()
		{
			if (--_count == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		int use_count()
		{
			return _count;
		}

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

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

		static int _count;
	};

};

template<class T>
int wtt::shared_ptr<T>::_count = 0;  // 正确:加上 wtt::

解释:

首先,先不看为什么错误,咱们先看好处

细节1:

类的静态成员必须在类外初始化,而模板类的静态成员,不仅要在类外初始化,还要带上模版

cpp 复制代码
template<class T>
int shared_ptr<T>::_count = 0;  // 正确:加上 wtt::

细节2:

当你想不和库中的shared_ptr冲突的时候,你选择在自己实现的shared_ptr外面套一层域的时候,要记住嵌套类是外层类的 private 成员!所以此时你必须在两个类之间加上public,用来将 嵌套类声明为 public

cpp 复制代码
class wtt
{
public://一定要加
	template<class T>
	class shared_ptr
	{
	    //.....
	};

};

细节3:

当你采取细节2的方法的时候,内层类的静态变量的初始化是在类外,此时的类外指的是,嵌套类的类外(也就是两个类的类外),而不是两个类之间进行初始化,而且你还要在原先的基础上,再加上外层类wtt域名:

cpp 复制代码
template<class T>
int wtt::shared_ptr<T>::_count = 0;  // 正确:加上 wtt::

现在再来看为什么错?

首先如果shared_ptr类只有一个对象的时候,也就是只开辟了一块空间A的时候,那么此时的对象无论是拷贝还是赋值去生成新的对象的时候,这些新生成的对象,都是指向的空间 A,所以引用计数都可以正确++,例子如下:

cpp 复制代码
int main() {
	wtt::shared_ptr<int> sp1(new int(42));
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 1(正确)

	wtt::shared_ptr<int> sp2(sp1);
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 2(正确)
	cout << "sp2.use_count(): " << sp2.use_count() << endl;  // 输出 2(正确)


	return 0;
}

运行结果:

正确!

但是问题是,如果你现在通过构造创建一个新的对象的时候,那么引用计数将会出错,例子:

cpp 复制代码
int main() {
	wtt::shared_ptr<int> sp1(new int(42));
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 1(但实际是 1)

	wtt::shared_ptr<int> sp2(sp1);
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 2(但实际是 2)
	cout << "sp2.use_count(): " << sp2.use_count() << endl;  // 输出 2(但实际是 2)

	wtt::shared_ptr<int> sp3(new int(100));  // 错误:sp3 的计数会干扰 sp1/sp2
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  //应该输出 3(但是是1)
	return 0;
}

运行结果:

错误!

解释:这不符合我们的预期,我们的预期是sp3对象对应的空间的引用计数是1,而不是将sp1和sp2共同的空间对应的引用计数2影响到了1!!

Q:为什么会发生这种情况?

**A:**因为这写法就是错的, 所有对象共享 static _count,当我们sp1、sp2指向同一块空间的时候,此时还看不出错,但是当一个sp3指向新的空间的时候,此时所有对象的引用计数都会被置为1!因为我们的构造函数里面,将引用计数初始化为了1,本意是让每一块空间在第一次开辟的时候让其自己的引用计数为1,但是却变成了每次有新空间都会影响所有空间的引用计数为1

②:正确实现shared_ptr

cpp 复制代码
namespace wtt
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}
		//拷贝
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}
		//赋值
		shared_ptr& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
			{
				if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				(*_pcount)++;         //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}
		//析构
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
			}
		}
		//获取引用计数
		int use_count(){ return *_pcount;}
		//*重载
		T& operator*(){ return *_ptr;}
		//->重载
		T* operator->(){ return _ptr;}

	private:
		T* _ptr;      //管理的资源
		int* _pcount; //管理的资源对应的引用计数
	};
}


int main() {
	wtt::shared_ptr<int> sp1(new int(42));
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 1(但实际是 1)

	wtt::shared_ptr<int> sp2(sp1);
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 2(但实际是 2)
	cout << "sp2.use_count(): " << sp2.use_count() << endl;  // 输出 2(但实际是 2)

	wtt::shared_ptr<int> sp3(new int(100));  // 错误:sp3 的计数会干扰 sp1/sp2
	cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 3(错误!)
	return 0;
}

运行结果:

正确!每个空间的引用计数互不干扰!

解释:先知道是对的就行了,下面慢慢解释

①:成员变量的改变

shared_ptr类增加一个int*变量,int*指向一个整形,该整形表示引用计数的值, 因为你只有构造的时候,就会新增一份新的引用计数,新的对象意味着新的空间,所以需要新的引用计数,而不是像错误方法:只有第一次实例化对象的时候,才会有引用计数 而每次构造的时候,不会出现新的引用计数,而是在原有的上面++

说白了,现在就变成了每个空间对应的引用计数在独立的空间之中,因为引用计数是new出来的一个整形的空间

②:构造

cpp 复制代码
        //构造
    	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		{}

正如①所言,构造意味着新的对象,则意味着新的空间要产生了,所以需要一个新的独立的引用计数来跟随这块空间,所以每次构造进来就是给成员变量引用计数new上一个空间,初始化为1

③:拷贝函数

cpp 复制代码
shared_ptr(shared_ptr<T>& sp)
    : _ptr(sp._ptr), _pcount(sp._pcount) 
{
    (*_pcount)++; // 引用计数 +1
}
  • 功能 :用另一个 shared_ptrsp)构造新对象,共享同一块内存和引用计数

  • 引用计数 :递增计数器(表示多了一个 shared_ptr 管理该资源)。

Q:(*_pcount)++; 对吗?不是应该加对象sp的成员变量pcount吗?

A:两种写法完全等价

在拷贝构造函数中:

  1. _pcount 已经被初始化为 sp._pcount (通过成员初始化列表 :_pcount(sp._pcount))。

  2. 因此,(*_pcount)++(*sp._pcount)++ 访问的是同一个内存地址,效果完全相同。

④:赋值函数

cpp 复制代码
shared_ptr& operator=(shared_ptr<T>& sp) {
    if (_ptr != sp._ptr) { // 避免自赋值
        // 1. 减少原资源的引用计数
        if (--(*_pcount) == 0) {
            cout << "delete: " << _ptr << endl;
            delete _ptr;
            delete _pcount;
        }
        // 2. 共享新资源
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        (*_pcount)++; // 引用计数 +1
    }
    return *this;
}

赋值是难点,假设B要赋值A,所以A对象会指向和B相同的空间,所以A原先的空间的引用计数就需要事先被--,然后再++B指向的空间的对应的引用计数

  • 功能 :将当前 shared_ptr 改为管理 sp 的资源。

  • 关键步骤

    1. 释放原资源

      • 减少原引用计数,若归零则释放内存和计数器。
    2. 共享新资源

      • 指向 sp 的资源,并递增其引用计数。
  • 自赋值检查if (_ptr != sp._ptr) 避免无意义操作。

⑤:析构函数

cpp 复制代码
~shared_ptr() {
    if (--(*_pcount) == 0)        // 1. 引用计数减1
    {    
        if (_ptr != nullptr)      // 2. 检查资源是否有效
        {    
         
            delete _ptr;          // 3. 释放管理的资源
            _ptr = nullptr;       // 4. 置空指针(避免悬空指针)
        }
        delete _pcount;           // 5. 释放引用计数器
        _pcount = nullptr;        // 6. 置空计数器指针
    }
}
  • 功能:递减引用计数,若归零则释放资源。

  • 细节

    • 只有最后一个 shared_ptr 析构时(计数为 0),才会释放内存。

    • 安全处理 nullptr 情况。

cpp 复制代码
if (_ptr != nullptr)
  • 确保 _ptr 不是空指针(避免对 nullptr 调用 delete,这是安全的编程习惯)。

至此 才是正确的实现shared_ptr!

4:weak_ptr

但是智能指针在某些场景(循环引用)下,还需要weak_ptr的使用,才能完美的应对所有的场景,所以shared_ptr也不例外

①:循环引用的定义

循环引用(Circular Reference)指 两个或多个对象通过智能指针互相持有对方的引用,导致它们的引用计数始终无法归零,从而无法释放内存。

场景:循环引用

shared_ptr的循环引用问题在一些特定的场景下才会产生。比如如下的结点类,

cpp 复制代码
struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

现在以new的方式构建两个结点,并将这两个结点连接起来,在程序的最后以delete的方式释放这两个结点。比如:

cpp 复制代码
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类型如下:

cpp 复制代码
struct ListNode
{
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

此时我们在main中进行:

cpp 复制代码
int main()
{
    std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
    


  return 0;
}

运行结果:

正确!

解释: 没有引发循环引用的本质是**n1n2 的引用关系是单向的**

Q:为什么没有触发循环引用?

A:分析如下

a:创建 n1n2

  • n1 的引用计数 = 1(main 中的 n1

  • n2 的引用计数 = 1(main 中的 n2

b: n1->_next = n2

  • n2 的引用计数 +1 → 2n1->_next 也指向 n2

  • n1 的引用计数 不变n2 没有指向 n1

c: main 函数结束

  • n2 析构

    • n2 的引用计数 -1 → 1n1->_next 仍然持有 n2
  • n1 析构

    • n1 的引用计数 -1 → 0n1 被释放)

    • n1->_next 析构 → n2 的引用计数 -1 → 0n2 被释放)

总结:

"n1->_next = n2 本质是复制 n2shared_ptr,使 n2 的引用计数变为 2。
n2 离开作用域时,其引用计数减为 1(因 n1->_next 仍持有它)。
接着 n1 离开作用域,引用计数减为 0,触发 n1 的析构。
n1 的析构过程中,其成员 _next(类型为 shared_ptr)也会析构,导致 n2 的引用计数归零,从而释放 n2。由于 n2 从未持有 n1shared_ptr,因此没有形成循环引用。"

所以单独的node2->_prev = node1;也不会引发循环引用!

②:循环引用的场景

而下面这个场景则会形成引用循环:

cpp 复制代码
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

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


	return 0;
}

解释:

Q:为什么会引发循环引用?

A:分析如下

(1) 初始化阶段

  • node1 的引用计数 = 1(由 std::shared_ptr<ListNode> node1 管理)。

  • node2 的引用计数 = 1(由 std::shared_ptr<ListNode> node2 管理)。

(2) 建立双向链接

  • node1->_next = node2
    node2 的引用计数 +1 → 2node1->_next 持有 node2)。

  • node2->_prev = node1
    node1 的引用计数 +1 → 2node2->_prev 持有 node1)。

(3) main 函数结束时

  • node1node2 离开作用域,触发析构:

    • node1 的引用计数 -1 → 1 (因 node2->_prev 仍持有 node1)。

    • node2 的引用计数 -1 → 1 (因 node1->_next 仍持有 node2)。

内存泄漏的根源

  • 循环依赖
    node1node2 的成员变量 _next_prev 互相持有对方的 shared_ptr

  • 引用计数无法归零

    即使外部的 node1node2 被销毁,它们的成员变量仍然保持对方的引用计数为 1

  • 结果

    两个 ListNode 对象永远不会被释放(内存泄漏),它们的析构函数也不会被调用。

验证现象

  • 输出结果

    运行代码后,不会输出 ~ListNode(),说明析构函数未被调用。

错误!永远不会析构

所以此时就需要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,进而释放这两个结点的资源。比如:

cpp 复制代码
struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~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;
}

运行结果:

通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。

weak_ptr的模拟实现:

cpp 复制代码
namespace cl
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_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 的引用计数(与 shared_ptr 的拷贝赋值不同)。

四:C++11和boost库中智能指针的关系

C++98中产生了第一个智能指针auto_ptr。

C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。

C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。

C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。

说明一下: boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。

本文还有智能指针和线程安全的问题没讲,后面会增加在此篇博客中~❀

相关推荐
?!7145 分钟前
算法打卡第三天
c++·算法·leetcode
向哆哆30 分钟前
Java 项目管理工具:Maven 与 Gradle 的深度对比与选择
java·开发语言·maven
怪侠沈剑心34 分钟前
海康NVR录像回放SDK原始流转FLV视频流:基于Java的流媒体转码(无需安装第三方插件ffmpeg)
java·开发语言·前端
钟烁卓38 分钟前
C++日志
开发语言·c++
浪浪山小白兔42 分钟前
CMake跨平台编译生成:从理论到实战
c++
Pafey1 小时前
在 Qt 中实现动态切换主题(明亮和暗黑)
开发语言·qt
啊吧怪不啊吧1 小时前
C++之初识模版
开发语言·数据结构·c++
YKPG1 小时前
C++学习-入门到精通-【7】类的深入剖析
c++·学习·算法
whoarethenext1 小时前
c/c++的opencv模糊
c语言·c++·opencv