首页 > 代码库 > 智能指针剖析

智能指针剖析

  1. auto_ptr
已经废弃。原因是它行为上是"排它性"指针,但又允许编译器实现拷贝操作,拷贝后的右值会被赋空。即将“传递”语义掩盖在“拷贝”动作之下。
即a=b时,作为右值的b的物理指针会是NULL。
会造成使用它的容器混乱。
这是典型的设计缺陷。既然是“传递”语义,就不应以“拷贝"形式出现。
另一方面,它对于数组的指针也支持不好,无法完成new []和delete []的配对。
 
  1. 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)

 

  1. 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的具体开销:
  1. 大小,如上所述指针的大小外,本身的大小不会太大,一般也就3个字长;
  2. 解引用开销:可见,解引用是无额外开销的;
  3. 引用计数:原子操作的开销,与具体机器有关,但往往也是一条指令完成;
  4. 它内部还存在虚函数的调用。
 
如果对开销敏感,同时又需要“execlusive"语义,unique_ptr才是选择。unique还支持对[]T的指针,虽然没啥用。unique_ptr它可以转成shared_ptr,但反过来不行。
 
  1.   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,则异常。
 
应用场景:
  1. 是可用于像观察者模式这种,需要持有某对象的指针,但这个“持有”动作并不应影响对象的生命周期。(观察者是否销毁不应由持有指针的subject影响);另一方面,subject又需要知道observer是否存活,这种情况,需要shared_ptr加weak_ptr的配合;
  2. 有一种场景,A与B需要互相持有对方指针,如果是shared_ptr,则会出现java式“内存泄漏”。需要用weak_ptr破除这种环形引用。
 
 
  1. 初始化与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的引用计数的。这在极端情况下,可能会造成一些影响。
 

 

智能指针剖析