shared_ptr 的实现

智能指针

shared_ptr 是C++11后引入的智能指针。智能指针的出现大大简化了C++程序员进行内存管理的逻辑,同时也避免了低级C++程序员可能写出的各种bug。陈硕老师在《Linux多线程服务器端编程》中总结道:

C++里可能出现的内存问题大致有这么几个:
1. 缓冲区溢出。
2. 空悬指针/悬挂指针
3. 重复释放
4. 内存泄漏
5. 不配对的new[]/delete
6. 内存碎片

正确使用智能指针能很轻易地解决前面5个问题。

shared_ptr 是智能指针中比较典型的一个。下面我们将从源代码的角度来进行分析,看看它是怎么实现的。源代码可以从GNU 处进行下载。

__shared_ptr 的定义

shared_ptr是智能指针,有别于int *、char *等原生指针,它是用类来实现的。

从类的定义上来看,shared_ptr 继承自 __shared_ptr。由类的命名规则,我们可以推测出后者是前者的内部实现,实际上也正是如此。shared_ptr的所有函数内部都调用了__shared_ptr的同名函数,因此在下文中,将不加区分地使用这两个词。类的定义有200来行,直接贴出来不太容易看,因此我们把这个类的实现按照数据成员、构造函数、操作符重载、成员函数这四个部分来进行划分。

首先给出__shared_ptr的类定义:

__shared_ptr是一个模板类,第一个模板参数是一个类型名,而第二个参数则代表一种锁策略,定义在 gnu_cxx的命名空间中。

数据成员

__shared_ptr内部只有两个私有成员变量,一个是原生指针变量_M_ptr,一个是用来表示它所指向的对象的引用计数的 _M_refcount_M_ptr的类型与shared_ptr指向的类型有关,比如

那么p_int中的_M_ptr就是一个指向int的指针,而

p_long中的_M_ptr就是一个指向long型的指针。

至于 _M_refcount所属的类型 __shared_count,我们暂时不去管它,现在只要知道 _M_refcount表示的是当前这个对象被多少个其他对象引用了,是一个计数器。下文我们再介绍这个类。

构造函数

__shared_ptr 有13个构造函数。

可以看出上述构造函数大致分为三类:一类是对__shared_ptr的某些行为进行配置的构造函数,一类是拷贝和移动构造函数,一类是将其他指针转换到本类的构造函数。

操作符重载

__shared_ptr 重载了=操作符,对它有四种不同的实现。第一种是普通的拷贝赋值,第二和第三种都是 shared_ptr的移动赋值,第四种是对unique_ptr类型的移动赋值。后三者都使用了std::moveswap 函数。这两个函数都是为了避免拷贝而设计的:std::move表示在拷贝时使用移动拷贝,swap则是直接交换两个指针的位置。

对*和->的重载则是为了让_shared_ptr也能像原生指针一样有解引用和箭头操作。

成员函数

reset函数的作用是重置shared_ptr,让它指向NULL,引用计数也变回到0。

owner_before函数的作用则是:当该shared_ptr__rhs的类型同属一个继承层次时,不管他们类型是否相同,他们两都被决议为“相等”。当他们的类型不属于同一继承层次时,比较的为他们所管理指针的地址值的大小。为什么要提供这样的函数呢?因为一个智能指针有可能指向了另一个智能指针指向对象中的某一部分,但又要保证这两个智能指针销毁时,只对那个被指的对象完整地析构一次,而不是两个指针分别析构一次。在这种情况下,指针就可以分为两种,一种是 stored pointer 它是指针本身的类型所表示的对象(可能是一个大对象中的一部分);另一种是 owned pointer 指向内存中的实际完整对象(这一个对象可能被许多智能指针指向了它里面的不同部分,但最终只析构一次)。owner-based order 就是指后一种情况,如果内存中只有一个对象,然后被许多 shared pointer 指向了其中不同的部分,那么这些指针本身的地址肯定是不同的,也就是operator<()可以比较它们,并且它们都不是对象的 owner,它们销毁时不会析构对象。但它们都指向了一个对象,在owner-based order 意义下它们是相等的。

get()的设计则是考虑到兼容性,在使用指针传参时,有的函数只能接受原生指针,而不能接受智能指针,这是我们就可以用get()来获得一个原生指针。

unique()函数返回一个bool型,表示当前这个share_ptr是不是唯一一个管理着所指对象的指针。

use_count()函数返回当前所指对象的引用计数。

__shared_count的实现

正如这个类的名字所暗示的那样,__shared_count 是一个用于计数的类。shared_ptr之所以可以实现对所指向对象的自动管理,与这个计数器类有着非常紧密的联系。

数据成员

__shared_count类只有一个成员变量,它是一个指向 _Sp_counted_base类型的指针。 _Sp_counted_base 是一个抽象基类,所以在构造函数里, _M_pi会被初始化为一个指向 _Sp_counted_base子类的指针 。在通常情况下,它都会初始化为一个__Sp_counted_ptr类型的指针。该类是_Sp_counted_base类的子类。

构造函数

构造器在这里不再详细列出,只需要知道构造器里完成对 _M_pi的初始化即可。

操作符重载

_shared_count 只重载了=这个操作符。在这个函数里,首先排除自赋值的情况,然后获得参数__r 的 _M_pi_M_add_ref_copy方法,并调用 _M_pi_M_release方法。

成员函数

从重载的=操作符和成员函数中可以看出, __shared_count内的函数内部都调用了 _M_pi的方法,因此我们转而去看一下这些调用是怎么回事。

_Sp_counted_base的实现

成员变量

成员变量的类型是 _Atomic_word,这个类型定义在 atomic_word.h中,实际上就是 int。在 _Sp_counted_base初始构造时,将这两个变量的初值都设置为1。

构造函数

成员函数

上述几个成员函数都是原子性的,保证了线程安全。

_M_add_ref_copy_M_use_count 进行+1,_M_weak_add_ref_M_weak_count进行+1。

_M_release中的__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1),先取出 _M_use_count的值,然后再对 _M_use_count进行-1操作。因此当 _M_use_count变为0时,就会调用_M_dispose()来释放资源,然后检测_M_weak_count的值并进行-1操作,如果也为0,那么调用 _M_destroy()来释放本身这个实例对象。

_M_weak_release 中所做的事情和_M_release 是类似的:先取出 _M_weak_count的值,然后对 _M_weak_count进行-1操作。当 _M_weak_count 变为0时,就会调用 _M_destroy()来释放本身这个实例对象。

__shared_ptr的使用

至此,我们已经把 __shared_ptr的内部结构给分析清楚了。不过只介绍这几个内部类,相当于只介绍了人有两个胳膊两条腿,对于人是怎么控制躯体做出各种操作的,还是没有说清楚。因此这一节我们将结合例子来分析一下上述各个类是怎么协调作用的。

__shared_ptr的拷贝

当我们拷贝一个shared_ptr 时,计数器会递增。

拷贝构造

make_shared是标准库中的一个函数,它在动态内存中分配一个对象并且初始化该对象,然后返回指向此对象的shared_ptr。因此我们可以像下面这样使用它:

此时p的引用计数为1。

该过程会导致__shared_ptr的拷贝构造函数被调用

p的 _M_refcount会用来构造q的 _M_refcount,因此会调用 __shared_count的拷贝构造函数

在拷贝构造函数中,先进行一次普通的指针赋值,让q 的_M_pi指向p 的_M_pi,再调用 q 的_M_pi_M_add_ref_copy()方法,让 q 的引用计数+1。

拷贝赋值操作符

当我们用=号来进行拷贝赋值时,会发生什么?还是上面的例子:

当把 p 赋值给 q 时,会调用__shared_ptr 的拷贝赋值函数,因此调用

第一行让q 的_M_ptr 指向p 的_M_ptr,而第二行对_M_refcount 则会调用__shared_count的拷贝赋值运算符:

拷贝赋值运算符和拷贝构造函数不尽相同。首先获得 p 的_M_pi,如果p 和 q 的_M_pi 不同,那么就先将 p 指向的对象的引用计数+1,然后调用 q 的_M_pi 的_M_release方法。在 《C++ Primer 》中,是这样描述这一过程的:

将一个shared_ptr 赋予另一个shared_ptr 会递增赋值号右边 shared_ptr 的引用计数,而递减左侧shared_ptr 的引用计数。

shared_ptr 自动销毁所管理的对象

__shared_ptr 并没有实现析构函数,因此__shared_ptr 的析构函数是编译器自动为我们合成的。它会调用每个类成员变量的析构函数,因此在释放_M_refcount 时,会调用__shared_count 的析构函数:

_M_release在前文多次出现,因此不再赘述。

小结

本文简要介绍了shared_ptr的内部实现,分析了其引用计数的原理。C++中的智能指针还有其他几种,如weak_ptrunique_ptr等,其特性稍有不同,不过基本思想是类似的。希望本文能对各位有所启发。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据