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 =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); } 其它代码 }; Observer { : shared_ptr<Subject> object; : Observer(shared_ptr<Object> obj) : (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了,不存在内存泄漏的问题了 :
shared_ptr< weak_ptr<Subject> > 其它代码
};
5.错误用法1:多个无关的shared_ptr管理同一个裸指针,有可能导致二次析构 int *a = );
shared_ptr<int> p1(a);
shared_ptr< p2(a);
}
p1和p2管理同一个裸指针a,此时的p1和p2有着完全独立的两个引用计数器,所以p1析构的时候会将a析构一次,p2析构的时候也会将a析构一次,C++中不允许同一个东西被析构两次,这样会导致程序爆炸 为了避免这种情况,我们永远不要将new用在shared_ptr构造函数列表以外的地方,或者干脆不用new,改用make_shared 另外,即使这样,也有可能导致二次析构,比如我们采用shared_ptr的get函数获得原始裸指针来构造另一个shared_ptr A
{
:
std::shared_ptr<A> getShared()
{
return std::shared_ptr<A>(this);
}
};
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,那么就不会产生内存泄漏 使用
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> DelTest { : DelTest(){ j= 0; cout<< DelTest()"<<:"<<i++<<endl; } ~DelTest(){ i = ~ DelTest()endl; } static i,j; }; int DelTest::i = int DelTest::j = ; void noDefine() { cout<<no_define start running!"<<endl; shared_ptr<DelTest> p(new DelTest[]); } slefDefine() { cout<<slefDefine start running!10],[](DelTest *p){delete[] p;});!传入lambada表达式代替delete操作。 } 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获得一个原始内置裸指针,然后用它来访问数组元素 样例如下: ;
x=i;
cout<<int x;
};
!传入lambada表达式代替delete操作。
cout<<p.get()[4].x<<endl;
}
运行结果:
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 <stdio.h>
#include <memory>
void closePf(FILE * pf)即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose
{
cout<<----close pf after works!----endl;
fclose(pf);
}
main()
{
shared_ptr<FILE> pf(fopen(bin2.txt",w),closePf);
cout<<*****start working****endl;
if(!pf)
return -1;
char *buf = abcdefg;
fwrite(buf,8,128);">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来保证不会产生内存泄漏 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |