从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(上):https://developer.aliyun.com/article/1522495
3.1 auto_ptr模拟代码
(上面SmartPtr再加一个赋值重载改下名字就差不多是auto_ptr的模拟了,再用命名空间封一下)
赋值重载细节还挺多的,前面学的赋值重载都类似拷贝构造,可以不看先写写,这里直接放代码:
#include <iostream> #include <memory> using namespace std; //1、RAII //2、像指针一样 //3、解决拷贝问题(不同的智能指针的解决方式不一样) namespace rtx { template<class T> class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} ~auto_ptr() { cout << "~auto_ptr -> delete: " << _ptr << endl; delete _ptr; } auto_ptr(auto_ptr<T>& ptr) :_ptr(ptr._ptr) { ptr._ptr = nullptr; } auto_ptr<T>& operator=(auto_ptr<T>& ap) { if (this != &ap) // 防止自己赋值给自己 { if (_ptr) // 防止释放空,delete空也行 { cout << "operator= -> Delete:" << _ptr << endl; delete _ptr; } _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } protected: T* _ptr; }; } class A { public: ~A() { cout << "~A()" << endl; } //protected: int _a1 = 0; int _a2 = 0; }; int main() { //SmartPtr<A> sp1(new A); //SmartPtr<A> sp2(sp1); rtx::auto_ptr<A> sp1(new A); rtx::auto_ptr<A> sp2(sp1); rtx::auto_ptr<A> sp3 = sp2; return 0; }
可以把命名空间切换到std比较一下,auto_ptr使用的是管理权转移的办法,会导致被拷贝对象悬空,是不负责的拷贝,对于不清楚auto_ptr这个特点的人来说,拷贝后再次使用ap1就会出问题。auto_ptr是C++98一个失败的设计,被挂在了耻辱柱上,很多公司明确要求不能使用auto_ptr。
C++98至C++11期间人们被迫用C++更新探索的库:boost库里的一些智能指针,到了C++11,终于更新了三个智能指针:unique_prt,shared_ptr,wead_ptr,相当于抄boost库的作业了。下面我们介绍以及模拟实现这几个智能指针,当然,还有很多接口在模拟代码里没有实现。
4. unique_ptr
在C++11中更加靠谱的unique_ptr智能指针:
unique_ptr直接禁止使用拷贝构造函数,即使编译器也不能生成默认的拷贝构造函数,因为使用了delete关键字。
unique_ptr采用的策略就是,既然拷贝有问题,那么就直接禁止拷贝,这确实解决了悬空等问题,使得unique_ptr是一个独一无二的智能指针。
(写到这发现忘记创建新项目了,这里创建一个Test.cpp和SmartPtr.hpp(.h+.cpp,直接.h也行,都可以把函数的实现在里面实现。声明和定义分离只是为了保护源码)
4.1 unique_ptr模拟代码
直接复制一份auto_ptr代码过来,用delete关键字禁言拷贝构造和赋值重载就行了:
template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { cout << "~unique_ptr -> delete: " << _ptr << endl; delete _ptr; } unique_ptr(unique_ptr<T>& ptr) = delete; unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete; T& operator*() { return *_ptr; } T* operator->() { return _ptr; } protected: T* _ptr; };
关于delete关键字的复习链接:(在5.2)从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值_GR_C的博客-CSDN博客
5. shared_ptr
unique_ptr禁掉了拷贝,但是如果我就想拷贝智能指针呢?这就要用到shared_ptr了:
shared_ptr采用了引用计数的方法来解决拷贝问题:(引用计数直接在成员变量加一个int Count可以吗?每一个对象都有一个自己的Count显然是不对的,我们应该让拷贝和被拷贝对象管理同一个Count。那么使用静态成员变量可以吗?这也不可以,因为这样所有的对象都管理的是同一个Count了,包括没有拷贝的对象)
shared_ptr原理:
通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
例如:老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
① shared_ptr内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
② 在对象被销毁时(也就是析构函数调用),说明自己不使用该资源了,对象引用计数减一。
③ 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
④ 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr增加了一个成员类似int* _pCount解决这个问题:
这样构造,拷贝构造和析构函数就是这样的:
构造先给 _pCount指向1,析构无论什么时候都减减,如果减减0就释放资源,拷贝构造就是把指针也给它,然后指针指向的内容加加。
到这可以自己尝试写一个赋值重载出来,手写或者敲都行OK,这里直接放代码了:
5.1 shared_ptr模拟代码
template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) : _ptr(ptr) , _pCount(new int(1)) {} void Release() { if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数 { delete _ptr; delete _pCount; } } ~shared_ptr() { Release(); } shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { //if (this != &sp) if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2; { // 比较_pCount也行 //if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数 //{ // delete _ptr; // delete _pCount; //} Release(); _ptr = sp._ptr; _pCount = sp._pCount; (*_pCount)++; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } protected: T* _ptr; int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员 };
赋值重载需要注意细节的都在注释写了,可以自己画一个图看看。
5.2 循环引用
shared_ptr 完美了吗?并不是,它有一个死穴:循环引用。创建一个链表节点,在该节点的析构函数中打印提示信息:
struct Node { ~Node() { cout << "~Node" << endl; } int _val; std::shared_ptr<Node> _next; std::shared_ptr<Node> _prev; };
将n1和n2互相指向,形成循环引用:
(因为要给_next和_prev赋值,所以Node里也要用智能指针)
int main() { std::shared_ptr<Node> n1(new Node); std::shared_ptr<Node> n2(new Node); n1->_next = n2; n2->_prev = n1; return 0; }
执行该程序后,节点析构函数中的打印信息并没有打印,说明析构出了问题。
如果不形成循环引用就会打印提示信息:
可以调用shared_ptr里的use_count接口打印引用计数值:
n1和n2刚创建的时候,它两的引用计数值都是1。当两个节点循环引用后,它们的引用计数值都变成了2。
n2先析构,右边的引用计数变为1,n1再析构,左边的引用计数变为1,然后就没了。
左边结点的_next什么时候释放?-> 取决于左边的结点什么时候delete。
左边的结点什么时候delete?-> 取决于右边结点的_prev。
右边结点的_prev什么时候释放?-> 取决于右边的结点什么时候delete。
右边的结点什么时候delete?-> 取决于左边结点的_next。
左边结点的_next什么时候释放? -> 回到一开始的问题,进入死循环。
在循环引用中,节点得不到真正的释放,就会造成内存泄漏。
循环引用的根本原因在于,next和prev也参与了资源的管理。
这个漏洞shared_ptr本身也解决不了,所以就增加了weak_ptr来解决这个问题。解决办法就是让节点中的_next和_prev仅指向对方,而不参与资源管理,也就是计数值不增加。
这里为了配合上面和给下面模拟weak_ptr演示给我们的shared_ptr加两个接口函数:
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(下):https://developer.aliyun.com/article/1522498?spm=a2c6h.13148508.setting.23.50c04f0ef94tTt