智能指针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_ptr
(sp
)构造新对象,共享同一块内存和引用计数。 -
引用计数:递增计数器(表示多了一个
shared_ptr
管理该资源)。
Q:(*_pcount)++; 对吗?不是应该加对象sp的成员变量pcount吗?
A:两种写法完全等价
在拷贝构造函数中:
-
_pcount
已经被初始化为sp._pcount
(通过成员初始化列表:_pcount(sp._pcount)
)。 -
因此,
(*_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
的资源。 -
关键步骤:
-
释放原资源:
-
减少原引用计数,若归零则释放内存和计数器。
-
-
共享新资源:
-
指向
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 → 2(n1->_next
也指向n2
) -
n1
的引用计数 不变(n2
没有指向n1
)
c: main
函数结束
-
n2
析构:-
n2
的引用计数 -1 → 1(n1->_next
仍然持有n2
)
-
-
n1
析构:-
n1
的引用计数 -1 → 0(n1
被释放) -
n1->_next
析构 →n2
的引用计数 -1 → 0(n2
被释放)
-
总结:
“
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 → 2(node1->_next
持有node2
)。 -
node2->_prev = node1
:
node1
的引用计数 +1 → 2(node2->_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库成为标准库的候选方案。
本文还有智能指针和线程安全的问题没讲,后面会增加在此篇博客中~❀
相关文章:
智能指针RAII
引入:智能指针的意义是什么? RAll是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效&#…...
AI护航化工:《山西省危化品视频智能分析指南》下的视频分析重构安全体系
化工和危化品行业的AI智能视频分析应用:构建安全与效率新范式 一、行业背景与挑战 化工和危化品行业是国民经济的重要支柱,但生产过程涉及高温、高压、易燃易爆等高风险场景。传统安全监管依赖人工巡检和固定监控设备,存在效率低、盲区多、…...
GitHub SSH Key 配置详细教程(适合初学者,Windows版)-学习记录4
GitHub SSH Key 配置详细教程(适合初学者,Windows版) 本教程适用于在 Windows 系统下,将本地 Git 仓库通过 SSH 方式推送到 GitHub,适合没有配置过 SSH key 的初学者。 1. 检查是否已有 SSH key 打开 Git Bash 或 Po…...
初识Linux · NAT 内网穿透 内网打洞 代理
目录 前言: 内网穿透和打洞 NAPT表 内网穿透 内网打洞 正向/反向代理 前言: 本文算是网络原理的最后一点补充,为什么说是补充呢,因为我们在前面第一次介绍NAT的时候详细介绍的是报文从子网到公网,却没有介绍报文…...
docker-compose使用详解
Docker-Compose 是 Docker 官方提供的容器编排工具,用于简化多容器应用的定义、部署和管理。其核心功能是通过 YAML 配置文件(docker-compose.yml)定义服务、网络和存储卷,并通过单一命令实现全生命周期的管理。以下从核心原理、安…...
使用计算机视觉实现目标分类和计数!!超详细入门教程
什么是物体计数和分类 在当今自动化和技术进步的时代,计算机视觉作为一项关键工具脱颖而出,在物体计数和分类任务中提供了卓越的功能。 无论是在制造、仓储、零售,还是在交通监控等日常应用中,计算机视觉系统都彻底改变了我们感知…...
并发编程中的对象组合的哲学
文章目录 引言对象组合与安全委托实例封闭技术基于监视器模式的对象访问对象不可变性简化委托原子维度的访问现有容器的并发安全的封装哲学使用继承使用组合小结参考引言 本文将介绍通过封装技术,保证开发者不对整个程序进行分析的情况下,就可以明确一个类是否是线程安全的,…...
03-Web后端基础(Maven基础)
1. 初始Maven 1.1 介绍 Maven 是一款用于管理和构建Java项目的工具,是Apache旗下的一个开源项目 。 Apache 软件基金会,成立于1999年7月,是目前世界上最大的最受欢迎的开源软件基金会,也是一个专门为支持开源项目而生的非盈利性…...
禁忌搜索算法:从原理到实战的全解析
禁忌搜索算法:从原理到实战的全解析 一、算法起源与核心思想 禁忌搜索(Tabu Search, TS)由美国工程院院士Fred Glover于1986年正式提出,其灵感源于人类的记忆机制——通过记录近期的搜索历史(禁忌表)&…...
从加密到信任|密码重塑车路云一体化安全生态
目录 一、密码技术的核心支撑 二、典型应用案例 三、未来发展方向 总结 车路云系统涉及海量实时数据交互,包括车辆位置、传感器信息、用户身份等敏感数据。其安全风险呈现三大特征: 开放环境威胁:V2X(车与万物互联࿰…...
【ffmpeg】SPS与PPS的概念
PPS(Picture Parameter Set)详解 PPS(图像参数集)是H.264/H.265视频编码标准中的关键数据结构,与SPS(序列参数集)共同组成视频的解码配置信息,直接影响视频的正确解码和播放。以下是…...
Java垃圾回收与JIT编译优化
1. Java中的垃圾回收 垃圾回收是Java内存管理的核心,负责自动回收不再被应用程序引用的对象内存,从而防止内存泄漏并优化资源使用。以下详细介绍垃圾回收的机制、算法及优化实践。 1.1 垃圾回收的必要性 垃圾回收解决了手动内存管理中的常见问题,如内存泄漏和悬空指针。它…...
mmaction2——tools文件夹下
build_rawframes.py 用法示例 python tools/data/build_rawframes.py data/videos data/frames --task rgb --level 2 --ext mp4 --use-opencv --num-worker 8总结: 只需要 RGB 帧,推荐 --use-opencv,简单高效,无需额外依赖。 …...
论文阅读:Next-Generation Database Interfaces:A Survey of LLM-based Text-to-SQL
地址:Next-Generation Database Interfaces: A Survey of LLM-based Text-to-SQL 摘要 由于用户问题理解、数据库模式解析和 SQL 生成的复杂性,从用户自然语言问题生成准确 SQL(Text-to-SQL)仍是一项长期挑战。传统的 Text-to-SQ…...
Devicenet主转Profinet网关助力改造焊接机器人系统智能升级
某汽车零部件焊接车间原有6台焊接机器人(采用Devicenet协议)需与新增的西门子S7-1200 PLC(Profinet协议)组网。若更换所有机器人控制器或上位机系统,成本过高且停产周期长。 《解决方案》 工程师选择稳联技术转换网关…...
【HTML-5】HTML 实体:完整指南与最佳实践
1. 什么是 HTML 实体? HTML 实体是一种在 HTML 文档中表示特殊字符的方法,这些字符如果直接使用可能会与 HTML 标记混淆,或者无法通过键盘直接输入。实体由 & 符号开始,以 ; 分号结束。 <p>这是一个小于符号的实体&am…...
MySQL 索引详解与原理分析
MySQL 索引详解与原理分析 一、什么是索引? 索引(Index)是数据库表中一列或多列的值进行排序的一种数据结构,可以加快数据的检索速度。索引类似于书本的目录,通过目录可以快速定位到想要的内容,而不用全书…...
游戏引擎学习第303天:尝试分开对Y轴和Z轴进行排序
成为我们自己的代码精灵α 所以现在应该可以正常使用了。不过,这两周我们没办法继续处理代码里的问题,而之前留在代码里的那个问题依然存在,没有人神奇地帮我们修复,这让人挺无奈的。其实我们都希望有个神奇的“代码仙子”&#…...
javaweb-html
1.交互流程: 浏览器向服务器发送http请求,服务器对浏览器进行回应,并发送字符串,浏览器能对这些字符串(html代码)进行解释; 三大web语言:(1)html:…...
3.2.3
# 导入必要的库 import onnx import numpy as np from PIL import Image import onnxruntime as ort # 定义预处理函数,用于将图片转换为模型所需的输入格式 def preprocess(image_path): input_shape (1, 1, 64, 64) # 模型输入期望的形状,这里…...
Redis 8.0 GA,重回开源
在数字化浪潮的推动下,实时数据处理已成为现代应用的核心需求。作为全球广泛使用的 NoSQL 数据库,Redis 8.0 不仅通过 30 余项性能改进重新定义了实时数据处理的速度极限,更通过整合社区资源与开放授权模式,进一步巩固其在开源生态…...
心联网(社群经济)视角下开源AI智能名片、链动2+1模式与S2B2C商城小程序源码的协同创新研究
摘要:在心联网(社群经济)理论框架下,本文构建了开源AI智能名片、链动21模式与S2B2C商城小程序源码的技术协同体系,提出"情感连接-利益驱动-生态裂变"三维创新模型。通过实证分析与案例研究,验证该…...
【图像大模型】Hunyuan-DiT:腾讯多模态扩散Transformer的架构创新与工程实践
Hunyuan-DiT:腾讯多模态扩散Transformer的架构创新与工程实践 一、架构设计与技术创新1.1 核心架构解析1.2 关键技术突破1.2.1 多粒度训练策略1.2.2 动态路由MoE 二、系统架构解析2.1 完整生成流程2.2 性能对比 三、实战部署指南3.1 环境配置3.2 基础推理代码3.3 高…...
TASK04【Datawhale 组队学习】构建RAG应用
目录 将LLM接入LangChain构建检索问答链运行成功图遇到的问题 langchain可以便捷地调用大模型,并将其结合在以langchain为基础框架搭建的个人应用中。 将LLM接入LangChain from langchain_openai import ChatOpenAI实例化一个 ChatOpenAI 类,实例化时传入超参数来…...
YOLOv11旋转目标检测Hrsc2016
from ultralytics import YOLOmodel YOLO(/kaggle/input/model-v11-obb/yolo11n-obb.pt) model.train(data/kaggle/input/hrscobb4/HRSC-YOLO/data.yaml, epochs30) 1使用的训练平台为Kaggle 数据集:HRSC的三种形式 一级分类:船 有水平框版本&…...
Debian重装系统后
安装配置java环境 手动安装 下载openJDK:openJDK 设置替代项 sudo update-alternatives --install /usr/bin/java java /opt/jdk-21.0.2/bin/java 1 sudo update-alternatives --install /usr/bin/javac javac /opt/jdk-21.0.2/bin/javac 1 sudo update-alternat…...
野火鲁班猫(arrch64架构debian)从零实现用MobileFaceNet算法进行实时人脸识别(四)安装RKNN Toolkit Lite2
RKNN Toolkit Lite2 是瑞芯微专为RK系列芯片开发的NPU加速推理API。若不使用该工具,计算任务将仅依赖CPU处理,无法充分发挥芯片高达6TOPS的NPU算力优势。 按照官方文档先拉一下官方代码库,然后通过whl文件安装,因为我是python3.1…...
ElasticSearch导读
ElasticSearch 简介:ElasticSearch简称ES是一个开源的分布式搜素和数据分析引擎。是使用Java开发并且是当前最流行的开源的企业级搜索引擎,能够达到近实时搜索,它专门设计用于处理大规模的文本数据和实现高性能的全文搜索。它基于 Apache Luc…...
【STM32】自定义打印函数
STM32 学习笔记:理解 my_printf 与 va_start 在嵌入式开发中,我们常常需要实现类似标准 C 中 printf 的调试输出功能。为了支持“任意数量参数”的传递,C 语言提供了对 可变参数(variable arguments) 的支持。其中&am…...
基于 STM32 的 PC ARGB 风扇控制器设计与实现
一、项目背景 最近购入的 X99 系列主板,没有风扇的 ARGB 彩灯接口,并且在 Ubuntu 系统上 4pin 的风扇接口调速也是非常的难用,sensor 扫描不到传感器,于是决定手搓一个风扇控制器,来实现转速自定义和彩灯控制。 我控制…...
【软件设计师】计算机网络考点整理
以下是软件设计师考试中 计算机网络 的核心考点总结,帮助您高效备考: 一、网络体系结构与协议 OSI七层模型 & TCP/IP四层模型 各层功能(物理层-数据链路层-网络层-传输层-会话层-表示层-应用层)对应协…...
在 Qt 中实现动态切换主题(明亮和暗黑)
目录 步骤 1:准备主题文件步骤 2:将 QSS 文件加入资源系统步骤 3:创建主题管理类步骤 4:在应用程序中切换主题步骤 5:处理自定义控件和动态资源步骤 6:保存用户主题偏好步骤 7:处理图片资源切换…...
JavaEE 初阶文件操作与 IO 详解
一、文件操作基础:File 类 作用:操作文件或目录(创建、删除、获取信息)。 核心方法: exists():文件是否存在createNewFile():创建新文件mkdir():创建目录delete():删除…...
基于Qt的app开发第十天
写在前面 笔者昨天刚刚收到课设的截止时间要求,距离写这篇博客的时间还有一个月,我从申请自命题课设到今天已经27天了,先用两周时间学Qt,然后就开始做这个项目,现在已经快把基础功能全部实现了。 目前的打算是完成基础…...
QT中信号和事件的区别
好的,简单来说,Qt 的信号(Signal)和事件(Event)虽然都用于组件间通信和交互,但它们的机制和用途是不同的: 1. 信号(Signal) 概念:信号是对象发出的…...
AUTOSAR图解==>AUTOSAR_SRS_PWMDriver
AUTOSAR PWM驱动模块详解 基于AUTOSAR 4.4.0 SRS 规范文档 目录 1. PWM驱动概述2. PWM驱动架构3. PWM驱动配置4. PWM驱动API接口5. PWM驱动状态管理6. PWM驱动典型应用场景7. 总结1. PWM驱动概述 AUTOSAR PWM驱动是AUTOSAR基础软件中的一个重要组件,属于微控制器抽象层(MCAL)…...
SQL数据处理流程
一、数据处理 1、数据清洗 对空值处理:删除/填充为0 -- 用 0 填充 NULL SELECT COALESCE(sales, 0) AS sales FROM orders;-- 删除含 NULL 的记录 DELETE FROM users WHERE email IS NULL; COALESCE(bonus, 0) 相当于IF(bonus IS NULL, 0, bonus),当…...
Mysql差异备份与恢复
1.练习差异备份 差异备份:备份完全备份后,新产生的数据。 在192.168.88.50主机完成差异备份 步骤一:练习差异备份//周一完全备份 mysql> select * from test.one; --------------------- | name | age | sex | ------------------…...
目标检测 Lite-DETR(2023)详细解读
文章目录 迭代高级特征跨尺度融合高效的低层次特征跨尺度融合KDA:Key-aware Deformable Attention 论文翻译: CVPR 2023 | Lite DETR:计算量减少60%!高效交错多尺度编码器-CSDN博客 DINO团队的 (Lightweight Transfo…...
【Java学习方法】类变量
类变量 引出关键字:static 又名:静态变量,静态字段,类字段(字段又名属性,成员方法),类属性 是什么? 供该(同一个类)的所有对象共享的变量 &am…...
智能手表为什么需要做 EN 18031 认证?
EN 18031 是欧盟针对电磁兼容性(EMC)中人体暴露于电磁场的安全要求制定的标准,全称为 《Electromagnetic compatibility (EMC) - Standards for protective measures against electromagnetic fields with regard to human exposure》&#x…...
什么是 Agent 的 Message
Messages 2.4.1 概述 什么是 Agent 的 Message? 当你和朋友聊天、在网上搜索信息或是对手机语音助手说“帮我查一下天气”时,其实你都在向某个“代理者(Agent)”发送一条“信息(Message)”。这里的“代理者”既可以是一个人,也可以是一个能执…...
如何用JAVA手写一个Tomcat
一、初步理解Tomcat Tomcat是什么? Tomcat 是一个开源的 轻量级 Java Web 应用服务器,核心功能是 运行 Servlet/JSP。 Tomcat的核心功能? Servlet 容器:负责加载、实例化、调用和销毁 Servlet。 HTTP 服务器:监听端口…...
WebRTC与RTSP|RTMP的技术对比:低延迟与稳定性如何决定音视频直播的未来
引言 音视频直播技术已经深刻影响了我们的生活方式,尤其是在教育、医疗、安防、娱乐等行业中,音视频技术成为了行业发展的重要推动力。近年来,WebRTC作为一种开源的实时通信技术,成为了音视频领域的重要选择,它使得浏览…...
COMPUTEX 2025 | 广和通创新解决方案共筑AI交互新纪元
5月20日至23日,广和通携多领域创新解决方案亮相2025年台北国际电脑展(COMPUTEX 2025),台北南港展览馆#K0727a展位。此次展会,广和通围绕“Advancing Connectivity Intelligent Future”为主题,设置四大核心…...
COMPUTEX 2025 | 广和通率先发布基于MediaTek T930 平台的5G模组FG390
5月19日,全球领先的无线通信模组和AI解决方案提供商广和通率先发布基于MediaTek T930平台的5G模组FG390系列。FG390系列模组为以5G固定无线接入(Fixed Wireless Access,FWA)为代表的MBB终端产品而设计,将在CPE…...
Power Integrations 汽车电源管理方案:为汽车应用增加系统价值
在新能源汽车产业蓬勃发展的当下,高效的电源管理方案成为提升汽车性能与可靠性的关键。近期,Power Integrations 举办线上交流会,介绍了基于其 1700V InnoSwitch3-AQ 反激式开关 IC 的五款全新参考设计,旨在为 800V 纯电动汽车提供…...
汽车转向系统行业2025数据分析报告
汽车转向系统市场概况 2024年全球汽车转向系统市场规模约为2769.4亿元,预计到2031年将增长至3296.3亿元,年均复合增长率(CAGR)为2.5%。这一增长主要得益于汽车行业的持续发展以及转向系统技术的不断进步。 市场驱动因素 汽车转…...
Tiny C 编译器中,如何实现宏展开和头文件包含的预处理逻辑?
首先,预处理的主要功能包括宏展开、头文件包含、条件编译等。用户的问题主要集中在宏展开和头文件包含,所以需要分别考虑这两个部分。 关于宏展开,首先需要解析#define指令。编译器在预处理阶段需要维护一个符号表,用来存储宏的名…...
谈谈 Kotlin 中的构造方法,有哪些注意事项?
在 Kotlin 中,构造方法分为主构造方法(Primary Constructor)和次构造方法(Secondary Constructor)。 1 主构造方法 主构造方法是类的核心构造方法,直接在类头声明,位于类名之后。 1.1 基本语…...