智能指针剖析
- auto_ptr
已经废弃。原因是它行为上是"排它性"指针,但又允许编译器实现拷贝操作,拷贝后的右值会被赋空。即将“传递”语义掩盖在“拷贝”动作之下。
即a=b时,作为右值的b的物理指针会是NULL。
会造成使用它的容器混乱。
这是典型的设计缺陷。既然是“传递”语义,就不应以“拷贝"形式出现。
另一方面,它对于数组的指针也支持不好,无法完成new []和delete []的配对。
- unique_ptr
这是对auto_ptr的“传递”语义的正确实现。从语言层面来保证不出现“拷贝”动作。
1 typedef std::unique_ptr<int> unique_t; 2 typedef std::vector< unique_t > vector_t; 3 4 vector_t vec1; // fine 5 vector_t vec2(5, unique_t(new Foo)); // Error (Copy) 6 vector_t vec3(vec1.begin(), vec1.end()); // Error (Copy) 7 vector_t vec3(make_move_iterator(vec1.begin()), make_move_iterator(vec1.end())); 8 // Courtesy of sehe 9 10 std::sort(vec1.begin(), vec1.end()); // fine, because using Move Assignment Operator 11 12 std::copy(vec1.begin(), vec1.end(), std::back_inserter(vec2)); // Error (copy)
- shared_ptr
不像之前的unique_ptr,shared_ptr重点在于shared,即为了多个指针指向同一对象而生。同时,又在原生指针基础之上,加了引用计数。在如a=b的场景下,会自动增减引用的计数。如a如果原来指向一个对象,则由于此赋值动作,原先被指向的对象计数要减1,同时,b指向的对象计数要加1。
引用计数通过如下图的control block表示,总的的内存分配:
可见:
- shared_ptr占用空间是正常指针的两倍;
- control block记录了引用计数,同时还有其他的一些自定义的deleter等。
control block本身也是动态分配的,它在如make_shared,或由unique_ptr、raw ptr初始化时创建。
为此,要避免对于raw指针,多次重复创建shared_ptr,导致同一个object多个control block的情况。这会引起重复析构!
分以下情况避免:
如果由于需要定制化的deleter而不得不使用new时,要将其写在一行语句中。
1 std::shared_ptr<Widget> spw1(new Widget, xxxDel);
这样。
对于有的需要this指针的场合,有专门的语言层面解决方法:enable_shared_from_this,但需要调用前本对象已经有control block,即已经有shared_ptr指向本身。所以一般用在工厂模式中。
衡量下control block的具体开销:
- 大小,如上所述指针的大小外,本身的大小不会太大,一般也就3个字长;
- 解引用开销:可见,解引用是无额外开销的;
- 引用计数:原子操作的开销,与具体机器有关,但往往也是一条指令完成;
- 它内部还存在虚函数的调用。
如果对开销敏感,同时又需要“execlusive"语义,unique_ptr才是选择。unique还支持对[]T的指针,虽然没啥用。unique_ptr它可以转成shared_ptr,但反过来不行。
- weak_ptr
weak_ptr纯粹是作为shared_ptr的补充而存在。它不能解引用,不能判空,生命周期开始之初就需要与shared_ptr共存(只有通过shared_ptr才能初始化)。
它的作用是,在不占用shared_ptr引用计数的同时,判断出一个对象是否销毁。即判断shared_ptr中的引用计数是否为0了。(dangle指针)。这种状态对于weak_ptr来说有个专有名词,叫做expired。
这种检查有两种形式:
a. 通过weak_ptr的lock(),“原子性”的判断是否expired,如果是,返回shared_ptr,否则返回空;
b. 直接用来初始化一个shared_ptr,如果expired,则异常。
应用场景:
- 是可用于像观察者模式这种,需要持有某对象的指针,但这个“持有”动作并不应影响对象的生命周期。(观察者是否销毁不应由持有指针的subject影响);另一方面,subject又需要知道observer是否存活,这种情况,需要shared_ptr加weak_ptr的配合;
- 有一种场景,A与B需要互相持有对方指针,如果是shared_ptr,则会出现java式“内存泄漏”。需要用weak_ptr破除这种环形引用。
- 初始化与control block
使用new与使用make_shared初始化的区别:
- 需要明确的是,control block也是需要动态申请的内存。 make_shared时,可能会对shared_ptr的对象的内存与control block的内存一起申请。会在内存对齐,申请速度上有优势;
- 另一方面,new与shared_ptr两者分开进行时,如果编译器在new与shared_ptr初始化过程中,优化插入了一些可能抛出异常的代码,则会有内存泄漏。
- 但,在需要定制化deleter时,也需要new,这种代码要小心,写成异常安全的;
- 在构造函数初始化时,有传入{}和()的区别,特别对于如vector这种object,如果使用make_shared,要注意默认传的是()。即:
auto p = std::make_shared<std::vector<int> >(10, 20);
这种代码,到底是初始化了一个vecotr包含10, 20两个数字,还是10个元素的vector每一个都是20?
- 最需要理解的一点:如果是make_shared出来的内存,由于control block与object内存是绑定一起申请的,那么也要一起释放。而control block中,引用计数的值不仅shared_ptr会看,weak_ptr也会看。这样即使对象没有引用,引用计数为0了,但如果有weak_ptr存在,这control block就不能释放(要给weak_ptr判断用)。control block中的weak_counter就是记录weak_ptr的引用计数的。这在极端情况下,可能会造成一些影响。