C++ 智能指针 shared_ptr 分析
引文:
C++对指针的管理提供了两种解决问题的思路:
1.不允许多个对象管理一个指针
2.允许多个对象管理一个指针,但仅当管理这个指针的最后一个对象析构时才调用delete
ps:这两种思路的共同点就是只允许delete一次,下面将讨论的shared_ptr就是采用思路1实现的
ps:智能指针不是指针,而是类,可以实例化为一个对象,来管理裸指针
1.shared_ptr的实现原理:
shared_ptr最本质的功能:“当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete”,该功能是通过引用计数法实现的
引用计数法的规则:
1)所有管理同一个裸指针的shared_ptr,都共享一个引用计数器
2)每当一个shared_ptr被赋值给其他shared_ptr时,这个共享的引用计数器就加1
3)每当一个shared_ptr析构或被用于管理其他裸指针时,这个引用计数器就减1
4)如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源
引用计数法的内部实现:
1)这个引用计数器保存在某个内部类型中,而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中
2)shared_ptr重载了赋值运算符,在赋值和拷贝另一个shared_ptr时,这个指针被另一个shared_ptr共享
3)在引用计数归0时,这个内部类型指针与shared_ptr管理的资源一起释放
4)此外,为了保证线程安全,引用计数器的加1和减1都是原子操作,它保证了shared_ptr由多个线程共享时不会爆掉
2.shared_ptr的使用
#include<iostream> #include<stdio.h> #include<string> #include<memory> using namespace std; int main() { //初始化 方法1: shared_ptr<string> sptr1(new string("name")); //初始化 方法2: shared_ptr<string> sptr2=make_shared<string>("sex"); //初始化 方法3: int *p =new int(10); shared_ptr<int> sptr3(p); //这种初始化的方式很危险,delete p之后,strp3也不再有效 }
相关成员函数:
1)use_count:返回引用计数的个数
2)unique:返回是否独占所有权(use_count=1)
3)swap:交换两个share_ptr对象(即交换所拥有的对象)
4)reset:放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少
5)get:返回内部对象指针
3.引用计数最大的缺点:循环引用
下面是事故现场:
class Observer; // 前向声明 class Subject { private: std::vector<shared_ptr<Observer>> observers; public: Subject() {} addObserver(shared_ptr<Observer> ob) { observers.push_back(ob); } // 其它代码 }; class Observer { private: shared_ptr<Subject> object; public: Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代码 };
目标类subject连接这多个观察者类,当某个事件发生时,目标类可以遍历观察者数组observers,对观察者进行通知,而观察者类中也保留着目标类的shared_ptr,这样多个观察者之间可以以目标类为桥梁进行沟通,除了会发生内存泄漏外,这还是一种很不错的设计模式嘛……
这里产生内存泄漏的原因就是循环引用,循环引用指的是一个引用通过一系列的引用链,竟然引回到自身,在上面的例子中,subject->observer->subject就是这么一条环形引用链,假设我们程序中只有一个变量shared_ptr<sbuject> p,此时p指向的对象不仅通过shared_ptr引向自己,还通过它包含的observer中的object成员变量引回自己,于是它的引用计数是2,每个observer的引用计数都是1,当p析构时,它的引用计数2-1=1,大于0,其析构函数不会被调用,于是p和它包含的每个observer对象在程序结束时依然驻留在内存中,没有被delete,从而造成了内存泄漏
4.采用weak_ptr(弱引用)解决循环引用的问题:
标准库提供了std::weak_ptr,weak_ptr是shared_ptr的观察者,它与一个shared_ptr绑定,但是却不参与引用计数的计算,在需要时,它还能生成一个与它所观察的shared_ptr共享引用计数器的新的shared_ptr,总而言之,weak_ptr的作用就是:在需要时生成一个与绑定的shared_ptr共享引用计数器的新shared_ptr,在其他时候不干扰绑定的shared_ptr的引用计数
weak_ptr相关成员函数:
1)lock:获得一个和绑定的shared_ptr共享引用计数器的新的shared_ptr
2)expired:功能等价于判断use_count是否等于0,但是速度更快
继续引用上面subject和observer的例子,来解决循环引用的问题:
将上述例子中,observer中object成员的类型换成weak_ptr<subject>即可解决内存泄漏的问题,因为之前的observer中object成员的subject参与了引用计数,替换成weak_ptr<subject>之后没有参与引用计数,这样以来,p指向对象的引用计数为1,所以在p析构时,subject指针将被delete,其中包含的observer数组在析构时,内部的observer对象的引用计数也为0,所以他们也被deleete了,不存在内存泄漏的问题了
class Observer; // 前向声明 class Subject { private: std::vector<shared_ptr<Observer>> observers; public: Subject() {} addObserver(shared_ptr<Observer> ob) { observers.push_back(ob); } // 其它代码 }; class Observer { private: shared_ptr< weak_ptr<Subject> > object; public: Observer(shared_ptr<Object> obj) : object(obj) {} // 其它代码 };
5.错误用法1:多个无关的shared_ptr管理同一个裸指针,有可能导致二次析构
int main() { int *a = new int(10); shared_ptr<int> p1(a); shared_ptr<int> p2(a); }
p1和p2管理同一个裸指针a,此时的p1和p2有着完全独立的两个引用计数器,所以p1析构的时候会将a析构一次,p2析构的时候也会将a析构一次,C++中不允许同一个东西被析构两次,这样会导致程序爆炸
为了避免这种情况,我们永远不要将new用在shared_ptr构造函数列表以外的地方,或者干脆不用new,改用make_shared
另外,即使这样,也有可能导致二次析构,比如我们采用shared_ptr的get函数获得原始裸指针来构造另一个shared_ptr
class A { public: std::shared_ptr<A> getShared() { return std::shared_ptr<A>(this); } }; int main() { std::shared_ptr<A> pa = std::make_shared<A>(); std::shared_ptr<A> pbad = pa->getShared(); }
上面的样例中,pa和pbad各自拥有一个独立的引用计数器,也有可能会导致二次析构
总而言之:管理同一个资源的sahred_ptr,只能由同一个初始shared_ptr通过一系列赋值和拷贝构造得到,要确保其共享的是同一个引用计数器
6.错误用法2:直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏
// 声明 void f(A *p1, B *p2); // 使用 f(new A, new B);
上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏
如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏
//声明 void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用 f(shared_ptr<A> (new A),shared_ptr<B>(new B));
因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:
1)new A发生在shared_ptr<A>构造发生之前
2)new B发生在shared_ptr<B>构造发生之前
3)两个shared_ptr的构造发生在函数f的调用之前
在满足上面三条性质的前提下,各操作的顺序可以任意执行
若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏
//声明 void f(shared_ptr<A> p1,shared_ptr<B> p2); //使用 f(make_shared<A>(),make_shared<B>());
原因很简单,依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行,于是make_shared<A>的构造完成了,即使make_shared<B>的构造抛出了异常,那么A的资源也能够被正确的释放,和上面的情形相比较,make_shared保证了第二个new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,所以在异常发生时,能够正确的释放资源
总结:请总是使用make_shared来生成shared_ptr
7.如果希望使用shared_ptr来管理动态数组,那么需要提供一个自定义的删除器来代替delete
#include <iostream> #include<memory> using namespace std; class DelTest { public: DelTest(){ j= 0; cout<<" DelTest()"<<":"<<i++<<endl; } ~DelTest(){ i = 0; cout<<"~ DelTest()"<<":"<<i++<<endl; } static int i,j; }; int DelTest::i = 0; int DelTest::j = 0; void noDefine() { cout<<"no_define start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine() { cout<<"slefDefine start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。 } int main() { noDefine();//!构造10次,析构1次。内存泄漏。 cout<<"--------------------"<<endl; slefDefine();//!构造次数==析构次数 无内存泄漏 } /* 运行结果: no_define start running! DelTest():0 DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 ~ DelTest():0 -------------------- slefDefine start running! DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 DelTest():10 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 */
需要注意的是:虽然通过自定义删除器的方式shared_ptr可以管理动态数组,但是shared_ptr并不支持下标运算符的操作,而且只能指针类型不支持指针算术运算(不能取地址),因此为了访问数组中的元素,必须用get获得一个原始内置裸指针,然后用它来访问数组元素
样例如下:
#include <iostream> #include<memory> using namespace std; class DelTest { public: DelTest(){ j= 0; x=i; cout<<" DelTest()"<<":"<<i++<<endl; } ~DelTest(){ i = 0; cout<<"~ DelTest()"<<":"<<i++<<endl; } static int i,j; int x; }; int DelTest::i = 0; int DelTest::j = 0; void noDefine() { cout<<"no_define start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10]); } void slefDefine() { cout<<"slefDefine start running!"<<endl; shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。 cout<<p.get()[4].x<<endl; } int main() { noDefine();//!构造10次,析构1次。内存泄漏。 cout<<"--------------------"<<endl; slefDefine();//!构造次数==析构次数 无内存泄漏 } /* 运行结果: no_define start running! DelTest():0 DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 ~ DelTest():0 -------------------- slefDefine start running! DelTest():1 DelTest():2 DelTest():3 DelTest():4 DelTest():5 DelTest():6 DelTest():7 DelTest():8 DelTest():9 DelTest():10 5 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 ~ DelTest():0 */
8.使用shared_ptr管理非常规的动态对象的时候,记得自定义删除器
某些情况下,有些动态内存也不是我们new出来的,如果要使用shared_ptr管理这种动态内存,也要自定义删除器
#include <iostream> #include <stdio.h> #include <memory> using namespace std; void closePf(FILE * pf)//即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose { cout<<"----close pf after works!----"<<endl; fclose(pf); } int main() { shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf); cout<<"*****start working****"<<endl; if(!pf) return -1; char *buf = "abcdefg"; fwrite(buf,8,1,pf.get());//确保fwrite不会删除指针的情况下,可以将shared_ptr内置指针取出 cout<<"------write in file!-----"<<endl; } /* *****start working**** ------write in file!----- ----close pf after works!---- */
类比TCP/IP中连接打开和关闭的情况,同理都可以使用shared_ptr来管理
总结:
1)不用使用相同的内置/原始/裸指针初始化多个智能指针
2)不要delete get函数返回的指针
3)如果你使用了get返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
4)如果你使用的智能指针管理的资源不是new分配的内存,记得传递一个删除器
5)请勿使用new构造多个shared_ptr作为实参,应该使用make_shared
6)存在循环引用关系时,请使用weak_ptr来保证不会产生内存泄漏