智能指针RAII

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

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

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

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

一:智能指针的使用场景

1:异常导致的内存泄漏

#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:异常的重新抛出

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

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:智能指针的实现

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

// 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的时候加上[ ]即可!

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

// 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函数变成这样,方便体现*的作用:

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;}

运行结果:

 

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

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

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

// 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));  // 内存1SmartPtr<int> ptr2(new int(200));  // 内存2SmartPtr<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:问题场景

先把智能指针类放这:

// 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中如下的时候:

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

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


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通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:

int main()
{std::auto_ptr<int> ap1(new int(1));std::auto_ptr<int> ap2(ap1);*ap2 = 10;//*ap1 = 10; //errorstd::auto_ptr<int> ap3(new int(1));std::auto_ptr<int> ap4(new int(2));ap3 = ap4;//*ap4 = 10; //errorreturn 0;
}

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

 

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

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

2:unique_ptr

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

int main()
{std::unique_ptr<int> up1(new int(0));//std::unique_ptr<int> up2(up1); //errorstd::unique_ptr<int> up3(new int(0));//up3 = up1; //errorreturn 0;
}

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

3:shared_ptr

最好用最优秀的智能指针就是shared_ptr!

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

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

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

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

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

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

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

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

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

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

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; //2shared_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; //2return 0;
}

运行结果:

  

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

①:典型错误实现shared_ptr

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

class wtt
{
public:template<class T>class shared_ptr{public:// RAIIshared_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:

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

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

细节2:

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

class wtt
{
public://一定要加template<class T>class shared_ptr{//.....};};

细节3:

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

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

 

现在再来看为什么错?

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

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;
}

运行结果: 

 正确!

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

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/sp2cout << "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

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/sp2cout << "sp1.use_count(): " << sp1.use_count() << endl;  // 输出 3(错误!)return 0;
}

运行结果:

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

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

①:成员变量的改变

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

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

②:构造

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

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

③:拷贝函数

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)++ 访问的是同一个内存地址,效果完全相同。

④:赋值函数

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) 避免无意义操作。

⑤:析构函数

~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 情况。

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

 至此 才是正确的实现shared_ptr!

4:weak_ptr

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

①:循环引用的定义

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

场景:循环引用

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

struct ListNode
{ListNode* _next;ListNode* _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};

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

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;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};

此时我们在main中进行:

int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;return 0;
}

运行结果:

正确!

 解释:没有引发循环引用的本质是n1 和 n2 的引用关系是单向的

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

A:分析如下

a:创建 n1 和 n2

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

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

②:循环引用的场景

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

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 函数结束时

  • node1 和 node2 离开作用域,触发析构:

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

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

内存泄漏的根源

  • 循环依赖
    node1 和 node2 的成员变量 _next 和 _prev 互相持有对方的 shared_ptr

  • 引用计数无法归零
    即使外部的 node1 和 node2 被销毁,它们的成员变量仍然保持对方的引用计数为 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,进而释放这两个结点的资源。比如: 

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的模拟实现:

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库成为标准库的候选方案。
 

 

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nt!MiRemovePageByColor函数分析之脱链和刷新颜色表

第0部分&#xff1a;背景 PFN_NUMBER FASTCALL MiRemoveZeroPage ( IN ULONG Color ) { ASSERT (Color < MmSecondaryColors); Page FreePagesByColor[Color].Flink; if (Page ! MM_EMPTY_LIST) { // // Remove the first entry on the zeroe…

DEBUG:Lombok 失效

DEBUG&#xff1a;Lombok 失效 问题描述 基于 Spring Boot 的项目中&#xff0c;编译时显示找不到 log 属性。查看对应的 class 类&#xff0c;Lombok 正常在编译时生成 log 属性。 同时存在另一个问题&#xff0c;使用Getter注解&#xff0c;但实际使用中该注解并没有生效&…

3D几何建模引擎3D ACIS Modeler核心功能深度解读

3D ACIS Modeler是一款由Spatial Corporation&#xff08;现为Dassault Systmes旗下&#xff09;开发的工业级三维几何建模内核&#xff0c;为CAD/CAM/CAE、建筑、制造、测量及三维动画等领域提供底层建模能力。本文将从基本定位、核心功能及行业案例三方面&#xff0c;系统介绍…

Flutter - 集成三方库:数据库(sqflite)

数据库 $ flutter pub add sqlite $ flutter pub get$ flutter run运行失败&#xff0c;看是编译报错,打开Xcode工程 ⌘ B 编译 对比 GSYGithubAppFlutter 的Xcode工程Build Phases > [CP] Embed Pods Frameworks 有sqfite.framework。本地默认的Flutter工程默认未生成Pod…

Android 中 权限分类及申请方式

在 Android 中,权限被分为几个不同的类别,每个类别有不同的申请和管理方式。 一、 普通权限(Normal Permissions) 普通权限通常不会对用户隐私或设备安全造成太大风险。这些权限在应用安装时自动授予,无需用户在运行时手动授权。 android.permission.INTERNETandroid.pe…

目标检测指标计算

mAP&#xff08;mean Average Precision&#xff09; 概述 预备参数&#xff1a;类别数&#xff0c;IoU阈值&#xff0c;maxDets值&#xff08;每张测试图像最多保留maxDets个预测框&#xff0c;通常是根据置信度得分排序后取前maxDets个&#xff09;&#xff1b; Q: 假如某张…

联合索引失效情况分析

一.模拟表结构&#xff1a; 背景&#xff1a; MySQL版本——8.0.37 表结构DDL&#xff1a; CREATE TABLE unite_index_table (id bigint NOT NULL AUTO_INCREMENT COMMENT 主键,clomn_first varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMEN…

软件架构之-论分布式架构设计及其实现

论分布式架构设计及其实现 摘要正文摘要 2023年2月,本人所在集团公司承接了长三角地区某省渔船图纸电子化审查项目开发,该项目旨在为长三角地区渔船建造设计院、渔船审图机构提供一个便捷化的服务平台。在次项目中,我作为项目成员参与了整个项目的建设工作,全权负责项目需求…

Pydantic数据验证实战指南:让Python应用更健壮与智能

导读&#xff1a;在日益复杂的数据驱动开发环境中&#xff0c;如何高效、安全地处理和验证数据成为每位Python开发者面临的关键挑战。本文全面解析了Pydantic这一革命性数据验证库&#xff0c;展示了它如何通过声明式API和类型提示系统&#xff0c;彻底改变Python数据处理模式。…

3、ubantu系统 | 通过vscode远程安装并配置anaconda

1、vscode登录 登录后通过pwd可以发现目前位于wangqinag账号下&#xff0c;左侧为属于该账号的文件夹及文件。 通过cd ..可以回到上一级目录&#xff0c;通过ls可以查看当前目录下的文件夹及文件。 2、安装 2.1、下载anaconda 通过wget和curl下载未成功&#xff0c;使用手动…

Python 与 Java 在 Web 开发中的深度对比:从语言特性到生态选型

在 Web 开发领域&#xff0c;Python 和 Java 作为两大主流技术栈&#xff0c;始终是开发者技术选型时的核心考量。本文将从语言本质、框架生态、性能工程、工程实践等多个维度展开深度对比&#xff0c;结合具体技术场景解析两者的适用边界与融合方案&#xff0c;为开发者提供系…

【OpenGL学习】(一)创建窗口

文章目录 【OpenGL学习】&#xff08;一&#xff09;创建窗口 【OpenGL学习】&#xff08;一&#xff09;创建窗口 GLFW OpenGL 本身只是一套图形渲染 API&#xff0c;不提供窗口创建、上下文管理或输入处理的功能。 GLFW 是一个支持创建窗口、处理键盘鼠标输入和管理 OpenGL…

电脑闪屏可能的原因

1. 显示器 / 屏幕故障 屏幕排线接触不良&#xff1a;笔记本电脑屏幕排线&#xff08;屏线&#xff09;松动或磨损&#xff0c;导致信号传输不稳定&#xff0c;常见于频繁开合屏幕的设备。屏幕面板损坏&#xff1a;液晶屏内部灯管老化、背光模块故障或面板本身损坏&#xff0c;…

docker容器知识

一、docker与docker compose区别&#xff1a; 1、docker是创建和管理单个容器的工具&#xff0c;适合简单的应用或服务&#xff1b; 2、docker compose是管理多容器应用的工具&#xff0c;适合复杂的、多服务的应用程序&#xff1b; 3、docker与docker compose对比&#xff…

什么是Rootfs

Rootfs (Root Filesystem) 详解 buildroot工具构建了一个名为"rootfs.tar"的根文件系统压缩包。 什么是rootfs Rootfs&#xff08;Root Filesystem&#xff0c;根文件系统&#xff09;是操作系统启动后挂载的第一个文件系统&#xff0c;它包含系统正常运行所需的基…

关于NLP自然语言处理的简单总结

参考&#xff1a; 什么是自然语言处理&#xff1f;看这篇文章就够了&#xff01; - 知乎 (zhihu.com) 所谓自然语言理解&#xff0c;就是研究如何让机器能够理解我们人类的语言并给出一些回应。 自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff0…

Linux下载国外软件镜像的加速方法(以下载Python-3.8.0.tgz为例)

0 前言 使用linux经常会通过国外服务器下载软件镜像&#xff0c;有些软件的下载速度奇慢&#xff0c;本文介绍一种加速国外软件镜像下载速度的方法&#xff0c;需要准备下载工具&#xff1a;迅雷。 1 以下载Python-3.8.0.tgz为例 找到Python官网的Python-3.8.0.tgz镜像下载地…

没有公网ip怎么端口映射外网访问?使用内网穿透可以解决

无公网IP时本地搭建的网络端口服务怎么映射外网远程访问&#xff1f;较为简单通用的方案就是使用nat123内网穿透&#xff0c;下面详细内网映射外网实现教程。​ 一、了解内网公网区别&#xff0c;及无公网IP外网访问方案 内网IP默认只能在同局域网内连接互通&#xff0c;而公…

Word2Vec详解

目录 Word2Vec 一、Word2Vec 模型架构 &#xff08;一&#xff09;Word2Vec 的核心理念 &#xff08;二&#xff09;Word2Vec 的两种架构 &#xff08;三&#xff09;负采样与层次 Softmax &#xff08;四&#xff09;Word2Vec 的优势与局限 二、Word2Vec 预训练及数据集…

ShardingSphere:查询报错:Actual table `数据源名称.表名` is not in table rule configuration

目录 简介异常信息排查原因解决 简介 1、使用ShardingSphere框架&#xff0c;版本为5.2.1 <dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core</artifactId><version>5.2.1</version>…