C++智能指针的头文件:
智能指针从本质上来说是一个模板类,用类实现对指针对象的管理。
shared_ptr能解决的问题:
先来看一个普通指针可能出现的悬垂问题:
当有多个指针指向同一个基础对象时,如果某个指针delete了该基础对象,对于其他指针来说,它们是无法感知的,此时则出现了悬垂指针,如果再对其他指针进行操作,则可能会导致core dump。
(core dump的原因:因为已经调用了delete,相当于已经将内存资源归还给了系统,如果有其他地方向系统申请资源时,系统则重新分配这块内存。此时有两种情况:① 原始的悬垂指针调用delete,系统检测到二次释放,直接core dump;② 原始的悬垂指针对指针地址上的内存进行读、写操作,可能意外的改写了其他程序的内容,即“踩内存”,导致发生意想不到的情况。)
普通指针出现悬垂的根本原因在于:当多个指针同时指向同一个内存资源时,如果通过其中的某一个指针delete释放了资源,其他指针无法感知到。
解决方法自然想到了“引用计数” ---- 通过一块额外的内存,实现对原始内存的管理。
在这块 “控制块” 内存中,保存当前对原始内存资源的引用计数。
普通指针多指针场景下出现悬垂指针的原因:
引入“控制块”,保存对于基础对象的“引用计数”,示例中有ptr1、ptr2、ptr3三个指针同时指向同一基础对象,因此对应这个基础对象的引用计数为 3:
当有某个指针退出作用域,或调用了delete释放资源时,系统并非真正的释放基础对象,而是对引用计数减一。
那么何时才可以删除基础对象呢?当只有一个指针指向基础对象的时候,就可以大大方方的通过该指针将基础对象删除(真正的调用delete释放基础对象的资源)。
对于“控制块”的实现方式:
对“控制块”中“引用计数”的管理:
1、构造函数:
当创建类的新对象时,初始化指针,并将引用计数设置为 1;
2、拷贝构造函数:
当对象作为另一个对象的副本时(即发生“拷贝构造”时),拷贝构造函数拷贝副本指针,并对引用计数 加1;
3、拷贝赋值运算符:
当使用“拷贝赋值运算符”(=)时,处理复杂一点:
a. 先使“左操作数”的指针的引用计数减1 (为何减一:因为该指针已经指向别的地方,则指向原基础对象的指针个数减1), 如果减1后引用计数降为0,则释放指针所指对象的内存资源;
b. 然后增加“右操作数”所指对象的引用计数(为何加一:因为此时左操作数转而指向此基础对象,则指向此基础对象的指针个数加1);
4、析构函数:
调用析构函数时,析构函数先使引用计数减1,如果减至0则delete释放对象。
shared_ptr类的“构造函数”使得基础对象的引用计数递增,shared_ptr类的“析构函数”使得基础对象的引用计数递减。
当最后一个指向基础对象的shared_ptr被析构时,会调用delete释放基础对象的内存资源。
1.2 make_shared:
注意 make_shared 是 函数模板,不是类模板,make_shared函数模板的返回值类型是 shared_ptr。
make_shared的优点:
make_shared的缺点:
假设原始对象类型为 widget,shared_ptr的“控制块”中需要维护的关于“引用计数”的信息包括:
如果通过原始的new表达式分配对象,然后传递给shared_ptr(即使用shared_ptr内部的构造函数),则“控制块内存”与“基础对象内存”是分离开的,如图所示:
此时是两个分配内存的动作,所以控制块与基础对象的内存是分离的(可能造成内存碎片)。控制块的内存是在shared_ptr的构造函数中分配的。
如果使用 make_shared 的方式,则只需要一次分配内存,分配出的内存结构如图所示:
1.2.2 优点:异常安全:
可能会出现异常的情况:
C++是不保证参数求值顺序,以及内部表达式的求值顺序的,所以可能的执行顺序如下:
此时,如果程序在第2步时抛出一个异常(比如out of memory等,Rhs的构造函数异常的),那么在第1步中new分配的Lhs对象内存将无法释放,导致内存泄漏。
这个问题的核心在于 shared_ptr 没有立即获得new分配出来的裸指针,shared_ptr与new结合使用时是要分成两步。
修复这个问题的方式有两种:
(1)不要将new操作放到函数形参初始化中,这样将无法保证求值顺序:
(2)更推荐的方法,是使用make_shared,一步到位 :
当我们想要创建的对象没有公有的构造函数时,make_shared就无法使用了。
make_shared的优点是只需申请一次内存,带来了性能上的提升。但这一性能同样也给make_shared带来了缺点:
智能指针的“控制块”中保存着两类关于“引用计数”的信息:
“弱引用计数”用来保存当前正在指向此基础对象的weak_ptr指针的个数,weak_ptr会保持控制块的生命周期,因此有一种特殊情况是:强引用的引用计数已经降为0,没有shared_ptr再持有基础对象,然而由于仍有weak_ptr指向基础对象,弱引用的引用计数非0,原本因为强引用计数已经归0就可以释放的基础对象内存,现在变成了“强引用、弱引用都减为0时才能释放”, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。
(一般情况下,程序中无需考虑这种微小的差别。)
摘自cppreference:
在典型的实现中,shared_ptr 只保有两个指针:
控制块是一个动态分配的对象,其中包含:
多线程环境下,调用不同shared_ptr实例的 成员函数是不需要额外的同步手段的(例如use_count()等成员函数),即使这些shared_ptr拥有的是同样的对象。
但是,如果多线程访问(有写操作)同一个shared_ptr,则需要线程同步,否则就会有race condition发生。
shared_ptr的引用计数本身是安全且无锁的,但shared_ptr中封装的基础对象的读写则不是。
出现这种情况的原因是:shared_ptr有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。
在使用shared_ptr管理指针时,有一个原则就是要尽量避免“先new、后用裸指针初始化shared_ptr” 的方式,这是因为当有两个或多个shared_ptr同时管理一个指针时,多个shared_ptr之间无法共享彼此的引用计数,导致可能造成double free。
异常场景示例:(两个shared_ptr共同管理同一个裸指针)
由此引出一个使用shared_ptr的原则:
当我们使用智能指针管理资源时,必须统一使用智能指针,而不能在某些地方使用智能指针,某些地方使用raw pointer,否则不能保持智能指针管理这个类对象的语义,从而产生各种错误。
给shared_ptr管理的资源必须在分配时立即交给shared_ptr,即:shared_ptr sp(new T());,而不是先new出ptr,再在后面的某个地方将ptr赋给shared_ptr。
上述的情况同样可能会发生在 this指针 上面。
当一个类被shared_ptr管理(当使用shared_ptr管理类对象时,实际上是管理的类对象的 *this指针),且在类的成员函数中需要把当前类对象作为参数传递给其他函数时,就需要返回当前对象的this指针,但是,直接传递this指针(相当于裸指针)到类外,有可能会被多个shared_ptr所管理,造成与上面一样的二次释放的异常错误。
错误示例:
出现上述异常的原因很简单,类的成员函数将对象的this指针返回出去,this是一个普通指针,交给智能指针sp2管理,而sp2根本感知不到这个裸指针已经被其他智能指针sp1给管理起来了。
使用shared_ptr直接管理this指针导致“重复释放”的原因在于:
C++11 引入shared_from_this,使用方式如下:
使用shared_from_this 改写上面的错误示例:
shared_from_this的使用公式为:
要实现上述的shared_from_this 的功能,首先要考虑两个设计原则:
在类对象本身当中不能存储类对象本身的shared_ptr,否则类对象shared_ptry永远也不会为0,从而这些资源永远不会释放,除非程序结束。
基于以上两点要求,boost中使用的是 weak_ptr 的方式来实现的。
1、首先生成类A:会依次调用 enable_shared_from_this 的构造函数 以及 类A的构造函数。
enable_shared_from_this 类中有一个 weak_ptr 成员,在enable_shared_from_this构造函数中对其初始化,此时weak_ptr无效的,不指向任何对象。
2、接着,外部程序会把指向类A 对象的this指针作为初始化参数来初始化一个shared_ptr,就是下面的过程:
关键点在于这个shared_ptr如何初始化, shared_ptr模板类中定义了如下的构造函数:
C++11中共有四种智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr。
所有这些智能指针都是为了管理动态分配对象的生命周期而设计的,换言之,通过保证这样的对象在适当的时机以适当的方式析构(包括发生异常的场合),来防止资源泄漏。
auto_ptr是个从c++98中残留下来的弃用特性,它是一种对智能指针进行标准化的尝试,这种尝试后来成为了c++11中的unique_ptr。
要正确的完成这个特性就需要移动语义,但在c++98中却并没有这样的语义。作为一种变通手段,auto_ptr使用了 拷贝复制操作 来完成移动任务,这就导致:
unique_ptr必须直接初始化,且不能通过隐式转换来构造,因为unique_ptr的构造函数被声明为explicit。
全部0条评论
快来发表一下你的评论吧 !