内存管理一直是令C++程序员最头疼的工作。C++继承了C高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏、悬挂指针、访问越界等问题—曾几何时,C++程序员曾经无限地向往Java、C#等语言的垃圾回收机制……
阅读本章,你会了解到高效的内存管理方法,彻底忘记“栈”、“堆”等内存分配相关的术语,并且还会发现,这些解决方案可能要比 Java、C#等语言更好。

3.1 smart_ptr库概述

计算机系统中资源有很多种,内存是我们最常用到的,此外还有文件描述符、socket、操作系统、handle、数据库连接等,程序里申请这些资源后必须及时归还系统,否则就会产生难以预料的后果。

3.3.1 RAII机制

为了管理内存等资源,C++程序员通常采用 RAII 机制(资源获取即初始化,Resource Acquisition Is Initialization),在类的构造函数里申请资源,然后使用,最终在析构函数中释放资源。
如果对象是用声明的方式在栈上创建的(一个局部对象),那么 RAII 机制会工作正常,当离开作用域时对象会自动销毁从而调用析构函数释放资源。但如果对象是用new操作符在堆上创建的,那么它的析构函数不会自动调用,程序员必须明确地用对应的delete操作符销毁它才能释放资源。这就存在着资源泄漏的隐患,因为这时没有任何对象对已经获取的资源负责,如果因某些意外导致程序未能执行delete语句,那么内存等资源就永久地丢失了。例如:

  1. auto p = new class_need_resource; // 对象创建,获取资源
  2. ... // 可能发生异常导致资源泄露
  3. delete p; // 删除对象,调用析构函数释放资源

new、delete以及指针的不恰当运用是C++中造成资源获取/释放问题的根源,能否正确而明智地运用delete是区分 C++新手与熟手的关键所在。但很多人—即使是熟练的C++程序员,也经常会忘记调用delete。

3.1.2 智能指针

智能指针(smart pointer)是C++群体中热门的议题,围绕它有很多有价值的讨论和结论。它实践了推荐书目[1]中的代理模式,代理了原始“裸”指针的行为,为它添加了更多更有用的特性。
C++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的时机delete,必须到处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时—不管是正常流程离开或是因异常离开—总调用delete来析构在堆上动态分配的对象。
存在很多种智能指针,其中最早的一个的应该是C++98标准中的“自动指针”auto_ptr,它部分地解决了获取资源自动释放的问题,例如:

{
    auto_ptr<class_need_resource> p1(new class_need_resource);
    auto_ptr<demo_class>          p2(factory.create());
    ...
}                                                //  离开作用域,p1、p2自动析构从而释放内存等资源

auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了operator*opreator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开函数main或者发生异常),C++语言会保证auto_ptr对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。
auto_ptr很好用,被包含在C++标准库中令它在世界范围内被广泛使用,使智能指针的思想和用法深入人心。但auto_ptr存在一些缺陷,所以新的C++标准提供了更完善的unique_ptrshared_ptrweak_ptr,而它们正是基于我们接下来要介绍的boost.smart_ptr库。
boost.smart_ptr库提供了六种智能指针:scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr和intrusive_ptr。它们是轻量级的对象,速度与原始指针相差无几,都是异常安全的(exceptionsafe),而且对于所指向的类型T也仅有一个很小且很合理的要求:类型T的析构函数不能抛出异常。
由于scoped_array和shared_array应用的局限性较大,故本书只介绍scoped_ptr、shared_ptr、weak_ptr和intrusive_ptr这四种智能指针。

3.2 scoped_ptr

scoped_ptr是一个很类似auto_ptr/unique_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但scoped_ptr的所有权更加严格,不能转让,一旦scoped_ptr获取了对象的管理权,我们就无法再从它那里取回来。
scoped_ptr拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让

3.2.1 类摘要

scoped_ptr的类摘要如下:

template<class T>
class scoped_ptr{                                // noncopyable
private:
    T *px;                                       // 原始指针
    scoped_ptr(scoped_ptr const&);               // 拷贝构造函数私有化
    scoped_ptr &operator=(scoped_ptr const &);   // 赋值操作私有化

    void operator==(scoped_ptr const&) const;    // 相等操作私有化
    void operator!=(scoped_ptr const&) const;    // 不等操作私有化
public:
    explicit scoped_ptr(T *p=0);                  // 显示构造函数
    ~scoped_ptr();                                // 析构函数
    void reset(T *p = 0);                         // 重置智能指针
    T& operator*() const;                         // 操作符重载
    T* operator->() const;                        // 操作符重载
    T* get() const;                               // 获得原始指针

    explicit operator bool() const;               // 显式bool值转型
    void swap(scoped_ptr &b);                     // 交换指针
};

template<class T> inline                          // 与空指针比较
    bool operator==(scoped_ptr<T> const &p, boost::detail::sp_nullptr_t);   
// 为了兼容不同的编译器, Boost没有直接使用标准里的std::nullptr_t, 
// 而是在 boost::detail 名字空间里用typedef定义了等价的类型 sp_nullptr_t.

3.2.2 操作函数

scoped_ptr的构造函数接受一个类型为T的指针p,创建出一个scoped_ptr对象,并在内部保存指针参数p。p必须是一个new表达式动态分配的结果,或者是个空指针(nullptr)。当scoped_ptr对象的生命期结束时,析构函数会使用delete操作符自动销毁所保存的指针对象,从而正确地回收资源。①
scoped_ptr同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的拷贝操作(原理可参考4.1节noncopyable),保证了被它管理的指针不能被转让所有权。
成员函数reset()的功能是重置scoped_ptr:它删除原来保存的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr将不持有任何指针。一般情况下reset()不应该被调用,因为它违背了scoped_ptr的本意—资源应该一直由scoped_ptr自己自动管理。scoped_ptr用operator
()和operator->()重载了解引用操作符“*”和箭头操作符“->”,以模仿被代理的原始指针的行为,因此可以把scoped_ptr对象如同指针一样使用。如果scoped_ptr保存的是空指针,那么这两个操作的行为未定义。②
scoped_ptr提供了一个可以在bool语境(如if的条件表达式)中自动转换成bool值的功能,用来测试scoped_ptr是否持有一个有效的指针(非空)。它可以代替与空指针的比较操作,而且写法更简单。
成员函数get()返回scoped_ptr内部保存的原始指针,可以用在某些要求必须是原始指针的场景(如底层的C接口)。但使用时必须小心,这将使原始指针脱离scoped_ptr的控制!不能对这个指针做delete操作,否则scoped_ptr析构时会对已经删除的指针再进行删除操作,发生未定义行为(通常是程序崩溃,这可能是最好的结果,因为它说明你的程序存在bug)。
scoped_ptr支持有限的比较操作,不能在两个scoped_ptr之间进行相等或者不等测试,默认仅支持与nullptr进行比较(也可以是NULL或者0,因为这两者可以隐式转换为nullptr)。③

① 实际上调用的是boost::check_delete()函数。 ② scoped_ptr 内部使用了BOOST_ASSERT来断言指针非空,但它仅工作在debug模式下。 ③ 我们可以为它编写额外的比较函数,但这样做通常意义不大,因为使用成员函数get()就可以获得原始指针进行比较。

3.2.3 用法

scoped_ptr的用法很简单:在原本使用指针变量接受new表达式结果的地方改成用scoped_ptr对象,然后去掉哪些多余的try/catch和delete操作就可以了。像这样:

scoped_str<string> sp(new string("text"));        // 构造一个scoped_ptr对象
assert(sp);                                       // 使用显式bool转换
assert(sp != nullptr);                            // 空指针比较操作

scoped_ptr是一种“智能指针”,因此其行为与普通指针基本相同,可以使用非常熟悉的“*”和“->”操作符:

cout << *sp << endl;                              // operator*取字符串的内容
cout << sp->size() << endl;                       // operator->取字符串的长度

但记住:不再需要delete操作,scoped_ptr会自动地帮助我们释放资源。如果我们对scoped_ptr执行delete会得到一个编译错误:因为scoped_ptr是一个行为类似指针的对象实例,而不是指针,对一个对象应用delete是不允许的。
scoped_ptr把拷贝构造函数和赋值函数都声明为私有的,不允许拷贝或赋值,拒绝了指针所有权的转让,只能在scoped_ptr被声明的作用域内使用—除了scoped_ptr自己,其他任何人都无权访问被管理的指针,从而保证了指针的绝对安全。

scoped_ptr<string> sp2 = sp;                      // 错误,scoped_str不能拷贝构造

如果代码编写者企图从一个scoped_ptr构造或赋值另一个scoped_ptr,那么编译器会报出一个错误,阻止他这么做,从而保护了我们的代码,而且是早在运行之前。scoped_ptr明确地表明了代码原始编写者的意图:只能在定义的作用域内使用,不可转让,这在代码后续的维护生命周期中很重要。由此也引出了另外一个结论:如果一个类持有scoped_ptr成员变量,那么它也会是不可拷贝和赋值的。例如:

class ptr_owned final {                           // 一个持有scoped_ptr成员的类是不可拷贝和赋值的
    scoped_ptr<int> m_ptr;                        // scoped_ptr成员
}; 
ptr_owned p;                                      // 类的一个实例
ptr_owned p2(p);                                  // 编译错误,不能拷贝构造

在“*”和“->”之外scoped_ptr没有定义其他的操作符,所以不能对scoped_ptr进行“++”或者“—”等指针算术操作。与普通指针相比,它只有很小的接口,这一点使指针的使用更加安全,更容易使用同时更不容易被误用。下面的代码都是scoped_ptr的错误用法:

sp++;                                             // 错误,scoped_ptr 未定义递增操作符
std::prev(sp);                                    // 错误,scoped_ptr 未定义递减操作符

使用scoped_ptr会带来两个好处:一是使代码变得清晰简单,而简单意味着更少的错误;二是它并没有增加多余的操作,安全的同时保证了效率,可以获得与原始指针同样的速度。
示范 scoped_ptr 用法的另一段代码如下:

#include <boost/smart_ptr.hpp>

using namespace boost;

struct posix_file {                               // 一个示范性质的文件类

    posix_file(const char *file_name) {           // 构造函数打开文件
        cout << "open file:" << file_name << endl;
    }
    ~posix_file() {                               // 析构函数关闭文件
        cout << "close file" << endl;
    }
};

int main() {
    // 文件类的 scoped_ptr,将在离开作用域时自动析构,从而关闭文件释放资源
    scoped_ptr<posix_file> fp(new posix_file("/tmp/a.txt"));
    scoped_ptr<int> p(new int);                   // 一个int指针的scoped_ptr
    if (p) {                                      // 在bool语境中测试指针是否有效
        *p = 100;                                 // 可以像普通指针一样使用解引用操作符 *
        cout << *p << endl;
    }
    p.reset();                                    // 置空 scoped_ptr, 仅仅是演示
    assert(p == 0);                               // 与 0 比较,  p不持有任何指针
    if (!p) {                                     // 在bool语境中测试,可以用!操作符
        cout << "scoped_ptr == nullptr" << endl;
    }
}                                        // 在这里发生scoped_ptr的析构, p和fp管理的指针自动被删除
/*
    open file:/tmp/a.txt 
    100 
    scoped_ptr == nullptr 
    close file
*/

3.2.4 对比标准

unique_ptr是在 C++标准中定义的新的智能指针,用来取代曾经的auto_ptr。根据C++标准定义(C++11.20.7.1),unique_ptr不仅能够代理new创建的单个对象,也能够代理new[]创建的数组对象,在这里我们简单介绍它的单个对象用法。
unique_ptr
C++标准中对unique_ptr的定义如下(做了适当的简化,其中的部分新关键字可参考附录 B):

template<class T, class D = default_delete<T>>    // 使用删除器
class unique_ptr {
public:
    typedef some_define pointer;                  // 内部类型定义
    typedef T element_type;

    constexpr unique_ptr() noexcept;              // 构造函数
    explicit unique_ptr(pointer p) noexcept;

    ~unique_ptr();                                // 析构函数
    unique_ptr &operator=(unique_ptr &&u) noexcept;   // 转移语义赋值

    element_type &operator*() const;              // 操作符重载
    pointer operator->() const noexcept;          // 操作符重载
    pointer get() const noexcept;                 // 获得原始指针
    explicit operator bool() const noexcept;      // bool值转型

    pointer release() noexcept;                   // 释放指针的管理权
    void reset(pointer p) noexcept;               // 重置智能指针
    void swap(unique_ptr &u) noexcept;            // 交换指针
    unique_ptr(const unique_ptr &) = delete;      // 使用delete禁用拷贝
    unique_ptr &operator=(const unique_ptr &) = delete;
};

bool operator==(const unique_ptr &x, const unique_ptr &y); 
...

unique_ptr的基本能力与scoped_ptr相同,同样可以在作用域内管理指针,也不允许拷贝构造和拷贝赋值,例如:①

unique_ptr<int> up(new int);                      // 声明一个 unique_ptr,管理 int 指针
assert(up);                                       // bool 语境测试指针是否有效
*up = 10;                                         // 使用 operator*操作指针
cout << *up << endl; 
up.reset();                                       // 释放指针
assert(!up);                                      // 此时不管理任何指针

但unique_ptr要比scoped_ptr有更多的功能:可以像原始指针一样进行比较,可以像shared_ptr一样定制删除器,也可以安全地放入标准容器。因此,如果读者使用的编译器支持C++11标准,那么可以毫不犹豫地使用unique_ptr来代替scoped_ptr。
当然,scoped_ptr也有它的优点,“少就是多”永远是一句至理名言,它只专注于做好作用域内的指针管理工作,含义明确,而且不允许转让指针所有权。
make_unique
C++11 标准虽然定义了unique_ptr,但却“遗忘”了对应的工厂函数make_unique()(C++14 标准补上了这个“漏洞”),于是boost.smart_ptr库特意在头文件里实现了make_unique()函数,基本形式是:②

template<class T, class... Args>                  // 使用可变参数模板
inline typename boost::detail::up_if_not_array<T>::type  
make_unique(Args&&... args) {                     // 使用可变参数模板
     return std::unique_ptr<T>(new T(...));       // C++的完美转发
}

需要注意两点:其一,它不含在头文件里,必须单独包含;其二,它位于名字空间 boost 而不是 std,这是为了避免潜在的冲突。

① 但支持新的转移语义,如unique_ptr up = std::move(another_ptr),解决了auto_ptr的在拷贝构造时微妙的转移语义问题,本书不做过多介绍。 ② 实际上 make_unique.hpp 里有两个头文件,使用模板元编程技术分别创建单个对象和数组对象。

boost::make_unique()的用法与 C++14 标准是一样的,示范代码如下:

auto p = boost::make_unique<int>(10);             // 使用auto创建unique_ptr<int>对象
assert(p && *p == 10);                            // 访问指针内容

scoped_ptr不需要也不可能有make_scoped()函数,因为它不能拷贝不能转移。

3.3 shared_ptr

shared_ptr是一个最像指针的“智能指针”,是boost.smart_ptr库中最有价值、最重要的组成部分,也是最有用的,Boost库的许多组件—甚至还包括其他一些领域的智能指针都使用了shared_ptr,所以它被毫无悬念地收入了C++11标准。shared_ptr与scoped_ptr一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针①,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。
shared_ptr也可以安全地放到标准容器中,是在STL容器中存储指针的最标准解法.

① shared_ptr的早期名字就叫做counted_ptr。

3.3.1 类摘要

shared_ptr要比同为智能指针的scoped_ptr复杂许多,它的类摘要如下:

template<class T> 
class shared_ptr 
{ 
public: 
    typedef T element_type;                            // 内部类型定义
    shared_ptr();                                      // 构造函数
    template<class Y> explicit shared_ptr(Y * p); 
    template<class Y, class D> shared_ptr(Y * p, D d); 

    ~shared_ptr();                                     // 析构函数

    shared_ptr(shared_ptr const & r);                  // 拷贝构造

    shared_ptr & operator=(shared_ptr const & r);      // 赋值操作
    template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r);

    void reset();                                      // 重置智能指针
    template<class Y> void reset(Y * p); 
    template<class Y, class D> void reset(Y * p, D d); 

    T & operator*() const;                             // 操作符重载
    T * operator->() const;                            // 操作符重载
    T * get() const;                                   // 获得原始指针

    bool unique() const;                               // 是否唯一
    long use_count() const;                            // 引用计数

    explicit operator bool() const;                    // 显式bool值转型
    void swap(shared_ptr & b);                         // 交换指针
};

3.3.2 操作函数

shared_ptr与scoped_ptr同样是用于管理new动态分配对象的智能指针,因此功能上有很多相似之处:它们都重载了“*”和“->”操作符以模仿原始指针的行为,提供显式bool类型转换以判断指针的有效性,get()可以得到原始指针,并且没有提供指针算术操作,也不能管理new[]产生的动态数组指针。

shared_ptr<int> spi(new int);                         // 一个int的shared_ptr 
assert(spi);                                          // 在bool语境中转换为bool值
*spi = 253;                                           // 使用解引用操作符* 

shared_ptr<string> sps(new string("smart"));          // 一个string的shared_ptr 
assert(sps->size() == 5);                             // 使用箭头操作符-> 

shared_ptr<int> dont_do_this(new int[10]);            // 危险!不能正确释放内存

例如:但shared_ptr的名字表明了它与scoped_ptr的主要不同:它是可以被安全共享的。shared_ptr是一个“全功能”的类,有着正常的拷贝、赋值语义,也可以进行shared_ptr间的比较,是“最智能”的智能指针。
shared_ptr 有多种形式的构造函数,应用于各种可能的情形:

  • 无参的shared_ptr()创建一个持有空指针的shared_ptr;
  • shared_ptr(Y * p)获得指向类型T的指针p的管理权,同时引用计数置为 1。这个构造函数要求Y类型必须能够转换为T类型;
  • shared_ptr(shared_ptr const & r)从另外一个shared_ptr获得指针的管理权,同时引用计数加 1,结果是两个shared_ptr共享一个指针的管理权;
  • operator=赋值操作符可以从另外一个shared_ptr获得指针的管理权,其行为同拷贝构造函数;
  • shared_ptr(Y p, D d)行为类似shared_ptr(Y p),但使用参数d指定了析构时的定制删除器,而不是简单的delete。这部分将在 3.3.8 节详述。
  • 别名构造函数(aliasing)是不增加引用计数的特殊用法,在 3.3.9 节讲解。

shared_ptr的reset()函数的行为与scoped_ptr也不尽相同,它的作用是将引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作。带参数的reset()则类似相同形式的构造函数,原指针引用计数减1的同时改为管理另一个指针。
shared_ptr有两个专门的函数来检查引用计数。unique()在shared_ptr是指针的唯一所有者时返回true(这时shared_ptr的行为类似scoped_ptr或unique_ptr),use_count()返回当前指针的引用计数。要小心,use_count()应该仅仅用于测试或者调试,它不提供高效率的操作,而且有的时候可能是不可用的(极少数情形)。而unique()则是可靠的,任何时候都可用,而且比use_count()==1速度更快。
shared_ptr还支持比较运算,可以测试两个shared_ptr的相等或不相等,比较基于内部保存的指针,相当于a.get()==b.get()。shared_ptr还可以使用operator<比较大小,但不提供除operator<以外的比较操作符,这使得shared_ptr可以被用于标准关联容器(set和map):

typedef shared_ptr<string> sp_t;         // shared_ptr类型定义
map<sp_t, int> m;                        // 标准映射容器
sp_t sp(new string("one"));              // 一个shared_ptr对象
m[sp] = 111;                             // 关联数组用法

此外,shared_ptr还支持流输出操作符operator<<,输出内部的指针值,方便调试。

3.3.3 用法

shared_ptr的“高智能”使其行为最接近原始指针,因此它比scoped_ptr的应用范围更广。几乎是100%可以在任何new出现的地方接受new的动态分配结果,然后被任意使用,从而完全消灭delete的使用和内存泄漏,而它的用法与scoped_ptr同样的简单。
shared_ptr也提供基本的线程安全保证,一个shared_ptr可以被多个线程安全读取,但其他的访问形式结果是未定义的。
示范shared_ptr基本用法的例子如下:

shared_ptr<int> sp(new int(10));      // 一个指向整数的shared_ptr 
assert(sp.unique());                  // 现在shared_ptr是指针的唯一持有者

shared_ptr<int> sp2 = sp;             // 第二个shared_ptr,拷贝构造函数
assert(sp == sp2 &&                   // 两个shared_ptr相等
    sp.use_count() == 2);             // 指向同一个对象, 引用计数为 2 

*sp2 = 100;                           // 使用解引用操作符修改被指对象
assert(*sp == 100);                   // 另一个shared_ptr也同时被修改

sp.reset();                           // 停止shared_ptr的使用
assert(!sp);                          // sp不再持有任何指针(空指针)

第二个例子示范了shared_ptr较复杂的用法:

#include <iostream>
#include <boost/smart_ptr.hpp>

using namespace boost;

class shared {                                // 一个拥有shared_ptr的类
private:
    shared_ptr<int> p;                        // shared_ptr成员变量
public:
    shared(shared_ptr<int> p_) : p(p_) {}     // 构造函数初始化shared_ptr
    void print() {                            // 输出 shared_ptr 的引用计数和指向的值
        std::cout << "count:" << p.use_count() << " v=" << *p << std::endl;
    }
};

void print_func(shared_ptr<int> p) {         // 使用shared_ptr作为函数参数
    // 同样输出引用计数和指向的值
    std::cout << "count:" << p.use_count() << " v=" << *p << std::endl;
}

int main() {
    shared_ptr<int> p(new int(100));         // shared_ptr持有整数指针
    shared s1(p), s2(p);                     // 构造两个自定义类
    s1.print();
    s2.print();
    *p = 20;                                 // 修改shared_ptr所指的值
    print_func(p);
    s2.print();
    std::cout << &p << "; " << &s1 << "; " << &s2;
}
/*  count:3 v=100
    count:3 v=100
    count:4 v=20
    count:3 v=20
    0x7fff20dbbe30; 0x7fff20dbbe20; 0x7fff20dbbe10  */

这段代码定义了一个类和一个函数,两者都接受shared_ptr对象作为参数,特别注意的是我们没有使用引用的方式传递参数,而是直接拷贝,就像是在使用一个原始指针—shared_ptr支持这样的用法。
在声明了shared_ptr和两个shared类实例后,指针被它们所共享,因此引用计数为3。print_func()函数内部拷贝了一个shared_ptr对象,因此引用计数再增加1,但当退出函数时拷贝自动析构,引用计数又恢复为3。

3.4 工厂函数

shared_ptr很好地消除了显式的delete调用,如果读者掌握了它的用法,可以肯定delete将会在你的编程字典中彻底消失。
但这还不够,因为shared_ptr的构造还需要new调用,这导致了代码中的某种不对称性。虽然shared_ptr很好的包装了new表达式,但过多的显式new操作符也是个问题,显式new调用应该使用工厂模式来解决。
因此,smart_ptr库提供了一个工厂函数(位于 boost 名字空间)make_shared ()②,来消除显式的new调用,声明如下:

template<class T, class... Args>                 // C++可变参数模板
typename boost::detail::sp_if_not_array<T>::type // 模板元计算类型
make_shared( Args && ... args );                 // C++的右值引用用法

make_shared()函数可以接受若干个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr的对象并返回。通常make_shared()函数要比直接创建shared_ptr对象的方式快且高效,因为它内部仅分配一次内存,消除了shared_ptr构造时的开销。
下面的代码示范了make_shared()函数的用法:

auto sp = make_shared<string>("make_shared");    // 创建string的共享指针
auto spv = make_shared<vector<int> >(10, 2);     // 创建vector的共享指针
assert(spv->size() == 10);

如果C++编译器支持可变参数模板特性,那么make_shared()的参数数量没有限制,能够以任意多数量的参数构造对象,否则它只能接受最多10个参数,一般情况下这不会成为问题。实际上,很少有如此多的参数的函数接口,即使有,那也会是一个设计得不够好的接口,应该被重构。
除了make_shared(),smart_ptr库还提供一个allocate_shared(),它比make_shared()多接受一个定制的内存分配器类型参数,其他方面都相同。

① 作者已经在数年的实践开发工作中很久没有写过这个单词了,一直到写作本书。 ② 在 Boost 库的较早版本时make_shared()不含在中,而是在

中,必须单独写出。

3.3.5应用于标准容器

有两种方式可以将shared_ptr应用于标准容器(或者容器适配器等其他容器)。一种用法是将容器作为shared_ptr管理的对象,如shared_ptr>,使容器可以被安全地共享,用法与普通shared_ptr没有区别,我们不再讨论。
另一种用法是将shared_ptr作为容器的元素,如vector>,因为shared_ptr支持拷贝语义和比较操作,符合标准容器对元素的要求,所以可以在容器中安全地容纳元素的指针而不是拷贝。标准容器不能容纳scoped_ptr,因为scoped_ptr不能拷贝和赋值。
标准容器可以容纳原始指针,但这就丧失了容器的许多好处,因为标准容器无法自动管理类型为指针的元素,必须编写额外的大量代码来保证指针最终被正确删除,这通常很麻烦而且容易出错。
存储shared_ptr的容器与存储原始指针的容器功能几乎一样,但shared_ptr为程序员做了指针的管理工作,可以任意使用shared_ptr而不用担心资源泄漏。
下面的代码示范了将shared_ptr应用于标准容器的用法:

#include <iostream>
#include <boost/smart_ptr.hpp>

using namespace boost;

int main(){ 
    typedef std::vector<shared_ptr<int>> vs; // 一个持有 shared_ptr 的标准容器类型
    vs vv(10);                               // 声明一个拥有 10 个元素的容器
    // 元素被初始化为空指针
    int i = 0;
    for (auto pos = vv.begin(); pos != vv.end(); ++pos) {
        (*pos) = make_shared<int>(++i);      // 使用工厂函数赋值
        std::cout << *(*pos) << ", ";        // 输出值
    }
    std::cout << std::endl;
    shared_ptr<int> pp = vv[9];
    *pp = 100;
    std::cout << *vv[9] << std::endl;
}

这段代码里需要注意的是迭代器和operator[]的用法,因为容器内存储的是shared_ptr,我们必须对迭代器pos使用一次解引用操作符“”以获得shared_ptr,然后再对shared_ptr使用解引用操作符“”才能操作真正的值。(pos)也可以直接写成*pos,但前者更清晰,后者很容易让人迷惑。
vector的operator[]用法与迭代器类似,也需要使用“
”获取真正的值。使用boost.foreach库(8.1节)或者C++里的新式for循环可以避免迭代器到shared_ptr的两次解引用,直接取出容器里的shared_ptr,例如:

for (auto& ptr : v) {                       // range based for,注意是引用形式
    ptr = make_shared<int>(++i);            // 使用工厂函数赋值
    cout << *ptr << ", ";                   // 输出值
}

Boost 还另外在boost.iterators库里提供了迭代器适配器 indirect_iterator来简化容纳shared_ptr 容器的使用,读者可参考推荐书目[3]。

3.3.6 应用于桥接模式

桥接模式(Bridge)是一种结构型设计模式,它把类的具体实现细节对用户隐藏起来,以达到类之间的最小耦合关系。在具体编程实践中桥接模式也被称为pimpl或者handle/body惯用法,它可以将头文件的依赖关系降到最小,减少编译时间,而且可以不使用虚函数实现多态。
scoped_ptr和shared_ptr都可以用来实现桥接模式,但shared_ptr通常更合适,因为它支持拷贝和赋值,这在很多情况下都是有用的,比如可以配合容器工作。
本节不可能完整讲述桥接模式和pimpl惯用法的所有细节,仅通过一个小例子来说明shared_ptr如何用于pimpl。
首先我们声明一个类sample,它仅向外界暴露了最小的细节,真正的实现在内部类impl,sample用一个shared_ptr来保存它的指针:

#include <boost/smart_ptr.hpp>

using namespace std;

class sample {
private:
    class impl;                         // 不完整的内部类声明
    shared_ptr<impl> p;                 // shared_ptr 成员变量
public:
    sample();                           // 构造函数
    void print();
};

// 在sample的cpp中完整定义impl类和其他功能:
class sample::impl                      // 内部类的实现
{
public:
    void print() {
        std::cout << "impl print" << std::endl;
    }
};

sample::sample() : p(new impl) {}      // 构造函数初始化 shared_ptr

void sample::print() {                 // 调用 pimpl 实现 print()
    p->print();
}

int main(){
    // 桥接模式的使用
    sample s;
    s.print();
}

桥接模式非常有用,它可以任意改变具体的实现而外界对此一无所知,同时也减小了源文件之间的编译依赖,使程序获得了更多的灵活性。而shared_ptr是实现它的最佳工具之一,它解决了指针的共享和引用计数问题。(关于桥接模式更详细的讨论请见推荐书目[1])。

3.3.7 应用于工厂模式

工厂模式是一种创建型设计模式,这个模式包装了new操作符的使用,使对象的创建工作集中在工厂类或者工厂函数中,从而更容易适应变化,make_shared()就是工厂模式的一个很好的例子。
在程序中编写自己的工厂类或者工厂函数时通常需要在堆上使用new动态分配一个对象,然后返回对象的指针。这种做法很不安全,因为用户很容易忘记对指针调用delete,存在资源泄漏的隐患。
使用shared_ptr可以解决这个问题,只需要修改工厂方法的接口,不再返回一个原始指针,而是返回一个被shared_ptr包装的智能指针,这样可以很好地保护系统资源,而且会更好地控制对接口的使用。
接下来我们使用代码来解释shared_ptr应用于工厂模式的用法:

#include <iostream>
#include <boosta/smart_ptr.hpp>

using namespace boost;

// 首先实现一个纯抽象基类, 也就是接口类
class abstract                                 // 接口类定义
{
public:
    virtual void f() = 0;
    virtual void g() = 0;
protected:
    // 注意abstract的析构函数,被定义为保护的,意味着除了它自己和它的子类,
    // 其他任何对象都无权调用delete来删除它
    virtual ~abstract() = default;             // 注意这里
};

// 然后再定义abstract的实现子类
class impl : public abstract {
public:
    impl() = default;

    virtual ~impl() = default;

public:
    virtual void f() {
        std::cout << "class impl f" << std::endl;
    }
    virtual void g() {
        std::cout << "class impl g" << std::endl;
    }
};

// 随后的工厂函数返回基类的shared_ptr
shared_ptr<abstract> create() {
    return make_shared<impl>();
}

int main1() {
    // 这样就完成全部工厂模式的实现,可以把这些组合起来
    auto p = create();                          // 工厂函数创建对象
    p->f();                                     // 可以像普通指针一样使用
    p->g();                                     // 不必担心资源泄漏,shared_ptr会自动管理指针
}

由于基类abstract的析构函数是保护的,所以用户不能做出任何对指针的破坏行为,即使是用get()获得了原始指针:

abstract *q = p.get();                          // 正确
delete q;                                       // 错误

这段代码不能通过编译,因为无法访问abstract的保护析构函数。
但这不是绝对的,使用“粗鲁”的方法也可以在shared_ptr外删除对象,因为impl的析构函数是公开的,所以:

impl *q = (impl*)(p.get());                    // 强制转型
delete q;                                      // ok but dangerous

这样就可以任意操作原本处于shared_ptr控制之下的原始指针了,但永远也不要这样做,因为这会使shared_ptr在析构时删除可能已经不存在的指针,引发未定义行为。

3.3.8 定制删除器

在3.3.2节我们特意没有讨论shared_ptr一种形式的构造函数shared_ptr(Yp, Dd),它涉及shared_ptr的另一个重要概念:删除器。
shared_ptr(Y
p, Dd)的第一个参数是要被管理的指针,它的含义与其他构造函数的参数相同。而第二个删除器参数d则告诉shared_ptr在析构时不是使用delete来操作指针p,而要用d来操作,即把deletep换成d(p)。
在这里删除器d可以是一个函数对象,也可以是一个函数指针,只要它能够像函数那样被调用,使得d(p)成立即可。对删除器的要求是它必须可拷贝,行为必须也像delete那样,不能抛出异常。
为了配合删除器的工作,shared_ptr提供一个自由函数get_deleter(),它能够返回内部的删除器指针。
有了删除器的概念,我们就可以用shared_ptr实现管理任意资源。只要这种资源提供了它自己的释放操作,shared_ptr就能够保证自动释放。
假设我们有一组操作socket的函数,使用一个socket_t类:

class socket_t {...};                   // socket 类

socket_t* open_socket()  {              // 打开 socket 
    cout << "open_socket" << endl; 
    return new socket_t; 
}

void close_socket(socket_t *s) {        // 关闭 socket 
    cout << "close_socket" << endl; 
    ... //其他操作,释放资源
}

那么,socket资源对应的释放操作就是函数close_socket(),它符合shared_ptr对删除器的定义,可以用 shared_ptr这样管理socket 资源:

socket_t *s = open_socket(); 
shared_ptr<socket_t> p(s, close_socket); // 传入删除器

在这里删除器close_socket()是一个自由函数,因此只需要把函数名传递给 shared_ptr 就可以了。在函数名前也可以加上取地址操作符&,效果是等价的:

shared_ptr<socket_t> p(s, &close_socket); // 传入删除器

这样我们就使用shared_ptr配合定制的删除器管理了socket资源。当离开作用域时,shared_ptr会自动调用close_socket()函数关闭socket,再也不会有资源遗失的担心。
再例如,对于传统的使用struct FILE的C文件操作,也可以使用shared_ptr配合定制删除器自动管理,像这样:

shared_ptr<FILE> fp(fopen("./1.txt","r"), fclose);

离开作用域时shared_ptr就会自动调用fclose()函数关闭文件。
shared_ptr的删除器特性在处理某些特殊资源时非常有用,它使得用户可以定制、扩展shared_ptr的行为,使shared_ptr不仅仅能够管理内存资源,而是成为一个“万能”的资源管理工具。

3.3.9 高级议题

本节讨论关于shared_ptr的一些高级议题。
对比std::shared_ptr
C++标准(C++11.20.7.2)中定义了std::shared_ptr,功能与boost::shared_ptr基本相同,完全可以等价互换C++标准(C++11.20.7.2)中定义了std::shared_ptr,功能与boost::shared_ptr基本相同,完全可以等价互换
显示bool转型
早期版本的shared_ptr的bool转型函数是隐式转换,但后来为了与标准一致添加了explicit修饰,变成了显式转换。①
出于兼容性的考虑,C++标准规定在if/assert/for等逻辑判断语境下shared_ptr还是可以“隐式”转换的(比如之前的代码),但其他情形—如函数参数或者返回值—则必须显式转换,可以使用static_cast(p)、p!=nullptr或者!!p等形式。例如

bool bool_test() {                           // 测试shared_ptr的bool转型
    auto p = make_shared<int>(776);          // 创建一个shared_ptr 
    assert(p);                               // assert可以隐式转换
    if(p) {                                  // if判断可以隐式转换
        std::cout << "explicit cast" << std::endl; 
    } 
    return static_cast<bool>(p);             // 返回值必须显式转换
}

① 但如果编译器不支持 C++11,那么 shared_ptr 也不会使用显式 bool 转型。

指针转型函数
在编写基于虚函数的多态代码时指针的类型转换很有用,比如把一个基类指针转型为一个派生类指针或者反过来。但对于shared_ptr不能使用诸如static_cast(p.get())的形式,这将导致转型后的指针无法再被shared_ptr正确管理。
为了支持这样的用法,shared_ptr提供了类似的转型函数static_pointer_cast()、const_pointer_cast()和dynamic_pointer_cast(),它们与标准的转型操作符static_cast等类似,但返回的是转型后的shared_ptr。
例如,下面的代码使用dynamic_pointer_cast把一个shared_ptr向下转型为一个shared_ptr,然后又用static_pointer_cast重新转为shared_ptr

shared_ptr<std::exception> sp1(new bad_exception);  
auto sp2 = dynamic_pointer_cast<bad_exception>(sp1);  
auto sp3 = static_pointer_cast<std::exception>(sp2);
assert(sp3 == sp1);

shared_ptr
shared_ptr能够存储void型的指针,而void型指针可以指向任意类型,因此shared_ptr就像是一个泛型的指针容器,拥有容纳任意类型的能力。
但将指针存储为void同时也丧失了原来的类型信息,为了在需要的时候正确使用,可以用static_pointer_cast等转型函数重新转为原来的指针。但这涉及运行时动态类型转换,会使代码不够安全,建议最好不要这样使用。
*删除器的高级用法

基于shared_ptr和定制删除器,shared_ptr可以有更惊人的用法。由于空指针可以是任何指针类型,因此shared_ptr还可以实现退出作用域时调用任意函数。例如:

void any_func(void* p) {                     // 一个可执行任意功能的函数
    cout << "some operate" << endl;
}

int main()  { 
     shared_ptr<void> p(nullptr,any_func);    // 容纳空指针,定制删除器
}                                            // 退出作用域时将执行any_func()

shared_ptr存储了一个空指针,并指定了删除器是操作void的一个函数,因此当它析构时会自动调用函数any_func(),从而执行任意我们想做的工作。
*别名构造函数(aliasing)

在之前介绍的构造函数之外,shared_ptr还有一种比较特殊的构造函数,形式是:

template< class Y >  
shared_ptr( shared_ptr<Y> const & r, element_type * p );

它的作用是共享r的引用计数,但实际持有的却是另外一个可能毫无关系的指针p,而且并不负责p的自动销毁。
初看上去这种形式的构造函数非常的怪异,但它是有实际应用价值的,一个例子使用场景是指向已经被shared_ptr管理的对象的内部成员变量:

auto p1 = make_shared<std::pair<int, int>>(0,1);     // 一个 pair 智能指针

shared_ptr<int> p2(p1, &p1->second);                 // 别名构造

assert(p1.use_count() == 2 &&                        // 原引用计数增加
       p1.use_count() == p2.use_count());            // 两者引用计数相同

assert((void*)p1.get() != (void*)p2.get());          // 但指向的内容不同
assert(&p1->second == p2.get());                     // 指向的是另外的指针

owner_less
因为存在别名构造函数,所以某些情况下单纯地基于p.get()的指针值比较就不适用了,为此smart_ptr库提供了基于所有权的比较函数对象owner_less,定义了严格的弱序关系,可以用于关联容器。
下面的代码简单地示范了owner_less用于标准容器set:

#include <set>
#include <boost/smart_ptr/owner_less.hpp> //需单独包含头文件

using namespace boost;

int main() {
    typedef shared_ptr<int> int_ptr;           // 共享指针 typedef
    typedef owner_less<int_ptr> int_ptr_less;  // 函数对象 typedef

    int_ptr p1(new int(10));                   // 共享指针
    int n = 20;
    int_ptr p2(p1, &n);                        // 别名构造

    assert(!int_ptr_less()(p1, p2) &&          // 两者即不小于
           !int_ptr_less()(p2, p1));           // 也不大于,即等价

    typedef std::set<int_ptr> int_set;         // 关联容器 typedef
    int_set s;

    s.insert(p1);                              // 插入两个元素
    s.insert(p2);                              // 因为等价所以不会被插入
    assert(s.size() == 1);                     // 实际容器里只有一个元素
}

其他高级用法
shared_ptr的功能已经远远超出了智能指针的范围,除了以上的介绍外还有很多其他用途,如包装成员函数、延时释放等,限于篇幅本书不作详细介绍,读者可参考Boost说明文档。

3.4 weak_ptr

weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->。它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

template<class T> 
class weak_ptr 
{ 
public: 
    weak_ptr();                                           // 构造函数
    template<class Y> weak_ptr(shared_ptr<Y> const & r); 
    weak_ptr(weak_ptr const & r); 

    ~weak_ptr();                                          // 析构函数

    weak_ptr & operator=(weak_ptr const & r);             // 赋值

    long use_count() const;                               // 引用计数

    bool expired() const;                                 // 是否失效指针
    shared_ptr<T> lock() const;                           //获取 shared_ptr 

    void reset();                                         // 重置指针
    void swap(weak_ptr<T> & b);                           // 交换指针
};

weak_ptr的接口很小,正如它的名字,是一个“弱”指针,但它能够完成一些特殊的工作,足以证明它的存在价值。

3.4.2 用法

weak_ptr被设计为与shared_ptr协同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,weak_ptr析构时也不会导致引用计数减少,它只是一个静静的观察者。
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是被shared_ptr管理的资源)已经不复存在。
weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这正是它“弱”的原因。但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,把弱关系转换为强关系,从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。下面的代码示范了weak_ptr的用法

#include <boost/smart_ptr/owner_less.hpp> //需单独包含头文件

using namespace boost;

#include <set>

int main() {
    shared_ptr<int> sp(new int(10));           // 一个 shared_ptr
    assert(sp.use_count() == 1);

    weak_ptr<int> wp(sp);                      // 从 shared_ptr 创建 weak_ptr
    assert(wp.use_count() == 1);               // weak_ptr 不影响引用计数

    if (!wp.expired()) {                       // 判断 weak_ptr 观察的对象是否失效
        shared_ptr<int> sp2 = wp.lock();       // 获得一个 shared_ptr
        *sp2 = 100;
        assert(wp.use_count() == 2);
    }                                          // 退出作用域,sp2 自动析构,引用计数减 1

    assert(wp.use_count() == 1);
    sp.reset();                                // shared_ptr 失效
    assert(wp.expired());
    assert(!wp.lock());                        // weak_ptr 将获得一个空指针
}

3.4.3 对象自我管理

weak_ptr的一个重要用途是获得this指针的shared_ptr,使对象自己能够生产shared_ptr管理自己:对象使用weak_ptr观测this指针,这并不影响引用计数,在需要的时候就调用lock()函数,返回一个符合要求的shared_ptr供外界使用。
这个解决方案被实现为一个惯用法,在头文件定义了一个助手类enable_shared_from_this,它的声明摘要如下:

template<class T> 
class enable_shared_from_this            // 辅助类,需要继承使用
{ 
public: 
    shared_ptr<T> shared_from_this();    // 工厂函数,产生this的shared_ptr 
}

使用的时候只需要让想被sharedptr管理的类继承它即可,成员函数 shared_from this()会返回this的 shared_ptr。例如:

#include <boost/smart_ptr.hpp> 

using spacename std;

class self_shared :                        // 一个需要用shared_ptr自我管理的类
    public enable_shared_from_this<self_shared> {
public:
    self_shared(int n) : x(n) {}
    int x;
    void print() {
        cout << "self_shared:" << x << endl;
    }
};


int main() {
    auto sp = make_shared<self_shared>(313);
    sp->print();
    auto p = sp->shared_from_this();
    p->x = 1000;
    p->print();
}

需要注意的是千万不能对一个普通对象(非sharedptr管理的对象)使用 shared from_this()获取shared_ptr,例如:

self_shared ss;  
auto p = ss.shared_from_this();  // 错误!

这样虽然语法上正确,编译也无问题,但在运行时会导致shared_ptr析构时企图删除一个栈上分配的对象,发生未定义行为。

3.4.4 打破循环引用

有的时候代码中可能会出现“循环引用”,这时shared_ptr的引用计数机制就会失效,导致不能正确释放资源,例如:

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace boost;

class node                              // 一个用于链表节点的类
{
public:
    ~node() {                           // 析构函数输出信息
        std::cout << "deleted" << std::endl;
    }

    typedef shared_ptr<node> ptr_type;  // 指针类型使用 shared_ptr
    ptr_type next; //后继指针
};

int main() {
    auto p1 = make_shared<node>();      // 两个节点对象
    auto p2 = make_shared<node>();
    p1->next = p2;                      // 形成循环链表
    p2->next = p1;
    assert(p1.use_count() == 2);        // 每个 shared_ptr 的引用计数都是 2
    assert(p2.use_count() == 2);
}

上面的代码中两个节点对象互相持有对方的引用,每一个shared_ptr的引用计数都是2,因此在析构时引用计数没有减至0,不会调用删除操作,导致内存泄漏。
这个时候我们就可以使用weak_ptr,因为它不会增加智能指针的引用计数,这样就把原来的强引用改变为弱引用,在可能存在循环引用的地方打破了循环,而在真正需要shared_ptr的时候调用weak_ptr的lock()函数:

#include <iostream>
#include <boost/smart_ptr.hpp>

using namespace boost;

class node //链表节点的类
{
public:
    ~node() {                             // 析构函数输出信息
        std::cout << "deleted" << std::endl;
    }

    typedef weak_ptr<node> ptr_type;      // 指针类型使用 weak_ptr
    ptr_type next;                        // 后继指针
};

int main() {
    auto p1 = make_shared<node>();        // 两个节点对象
    auto p2 = make_shared<node>();
    p1->next = p2;                        // 形成循环链表
    p2->next = p1;                        // 引用使用了 weak_ptr 所以正常
    assert(p1.use_count() == 1);          // 每个 shared_ptr 的引用计数是 1
    assert(p2.use_count() == 1);          // 没有了循环引用
    if (!p1->next.expired()) {            // 检查弱引用是否有效
        auto p3 = p1->next.lock();        // 调用 lock()获得强引用
    }
}                                         // 退出作用域, shared_ptr 均正确析构

3.5 intrusive_ptr

intrusive_ptr也是一种引用计数型智能指针,但与之前介绍的scoped_ptr、shared_ptr不同,需要额外增加一些的代码才能 使用。它的名字可能会给人造成误解,实际上它并不一定要修改代理对象的内部数据。
如果现存代码已经有了引用计数机制管理的对象,那么intrusive_ptr是一个非常好的选择,可以包装已有对象从而得到与shared_ptr类似的智能指针。

3.5.1 类摘要

template<class T>
class intrusive_ptr {
public:
    typedef T element_type;                          // 被代理的对象

    intrusive_ptr();                                 // 构造函数
    intrusive_ptr(T *p, bool add_ref = true);
    intrusive_ptr(intrusive_ptr const &r);
    template<class Y> intrusive_ptr(intrusive_ptr<Y> const &r);

    ~intrusive_ptr();

    intrusive_ptr &operator=(intrusive_ptr const &r);
    template<class Y> intrusive_ptr &operator=(intrusive_ptr<Y> const &r);
    intrusive_ptr &operator=(T *r);

    void reset();                                    // 重置指针
    void reset(T *r);
    void reset(T *r, bool add_ref);

    T &operator*() const;                            // 操作符重载
    T *operator->() const;
    explicit operator bool() const;

    T *get() const;
    T *detach();
    void swap(intrusive_ptr &b);
};

因为intrusive_ptr也是引用计数型指针,所以它的接口与shared_ptr很像,也同样支持比较和static_pointer_cast()、dynamic_pointer_cast()等转型操作,但它自己不直接管理引用计数,而是调用下面两个函数来间接管理:

void intrusive_ptr_add_ref(T * p);                  // 增加引用计数
void intrusive_ptr_release(T * p);                  // 减少引用计数

intrusive_ptr的构造函数和reset()还多出一个add_ref参数,表示是否增加引用计数,如果add_ref==false,那么它就相当于weak_ptr,只是简单地观察对象。

3.5.2 用法

假设我们已经有了一个自己实现引用技术的类counted_data:

struct counted_data {                              // 自己实现引用计数
    int m_count  = 0;                              // 引用计数
    ...                                            // 其他数据成员
};

为了让intrusive_ptr能够正常工作,我们需要实现它要求的两个回调函数:①

void intrusive_ptr_add_ref(counted_data* p) {      // 增加引用计数
    ++p->m_count; 
} 
void intrusive_ptr_release(counted_data* p) {      // 减少引用计数
     if(--p->m_count == 0)  { 
         delete p;                                 // 引用计数为0则删除指针
     } 
}

注意:在intrusive_ptr_release()函数里必须检查引用计数,因为intrusive_ptr不负责实例的销毁,这个工作必须由我们自己完成。
实现intrusive_ptr_release()和intrusive_ptr_release()后intrusive_ptr就可以管理counted_data了,示范代码如下:

int main()  { 
     typedef intrusive_ptr<counted_data> counted_ptr; // 类型定义
     counted_ptr p(new counted_data);                 // 创建智能指针
     assert(p);                                       // bool 转型
     assert(p->m_count == 1);                         // operator-> 
     counted_ptr p2(p);                               // 指针拷贝构造
     assert(p->m_count == 2);                         // 引用计数增加
     counted_ptr weak_p(p.get(), false);              // 弱引用
     assert(weak_p->m_count == 2);                    // 引用计数不增加
     p2.reset();                                      // 复位指针
     assert(p->m_count == 1);                         // 引用计数减少
}                                                     // 对象被正确析构

可以看到,只需要编写少量的代码,我们就可以复用既存的数据结构,获得一个与shared_ptr用法几乎一样的智能指针,而且并没有增加多余的开销,这在某些对性能要求比较苛刻的场景里非常有用。
但大多数情况下shared_ptr完全不必增加新代码,而且提供了更多的灵活性,使用intrusive_ptr前必须要确定它能够带来足够多的好处。

3.5.3 引用计数器

为了进一步简化实现引用计数的工作,intrusive_ptr在头文件里定义了一个辅助类intrusive_ref_counter,声明如下:

template< typename DerivedT, typename CounterPolicyT = thread_safe_counter> 
class intrusive_ref_counter { 
private: 
     typedef typename CounterPolicyT::type counter_type; 
     mutable counter_type m_ref_counter; 
public: 
     intrusive_ref_counter(); 
     unsigned int use_count() const; 
protected: 
     ~intrusive_ref_counter() = default; 
     friend void intrusive_ptr_add_ref(const intrusive_ref_counter* p); 
     friend void intrusive_ptr_release(const intrusive_ref_counter* p);
};

intrusive_ref_counter内部定义了一个计数器变量m_ref_counter,使用模板参数CounterPolicyT配置策略类实现了计数的增减,默认的策略是线程安全的thread_safe_counter。
intrusive_ref_counter需要被继承使用,这样子类就会自动获得引用计数的能力,之前的counted_data可以简化如下:

struct counted_data : public intrusive_ref_counter<counted_data>
{ ... }; 

int main() { 
     typedef intrusive_ptr<counted_data> counted_ptr; // 类型定义
     counted_ptr p(new counted_data);                 // 创建智能指针
     assert(p);                                       // bool 转型
     assert(p->use_count() == 1);                     // operator-> 
}                                                     // 对象被正确析构

本书的12.1.4节使用atomic实现了一个更好的辅助类ref_count,读者可参考。

3.6 pool库概述

如果读者学习过操作系统的内存管理机制和内存分配算法等知识,那么就可能了解“内存池”的概念。简单来说,内存池预先分配了一块大的内存空间,然后就可以在其中使用某种算法实现高效快速的自定制内存分配。
boost.pool库基于简单分隔存储思想实现了一个快速、紧凑的内存池库,不仅能够管理大量的对象,还可以被用做STL的内存分配器。某种程度上讲,它近似于一个小型的垃圾回收机制,在需要大量地分配/释放小对象时很有效率,而且完全不需要考虑delete。
pool库包含四个组成部分:最简单的pool、分配类实例的object_pool、单件内存池singleton_pool和可用于标准库的pool_alloc。

3.7 pool

pool是最简单也最容易使用的内存池类,可以返回一个简单数据类型(POD)①的内存指针。它位于名字空间boost,需要包含头文件

① POD是C++标准中的技术术语,是“普通旧式数据”(Plain Old Data)的缩写。

3.7.1 类摘要

template <typename UserAllocator = default_user_allocator_new_delete > 
class pool { 
public: 
    explicit pool(size_type requested_size);    // 构造函数
    ~pool();                                    // 析构函数

    size_type get_requested_size() const;       // 分配内存块的大小

    void * malloc();                            // 分配内存
    void * ordered_malloc(); 
    void * ordered_malloc(size_type n); 
    bool is_from(void * chunk) const; 

    void free(void * chunk);                    // 归还内存
    void ordered_free(void * chunk); 
    void free(void * chunks, size_type n); 
    void ordered_free(void * chunks, size_type n); 

    bool release_memory();                      // 释放内存
    bool purge_memory(); 
};

3.7.2 操作函数

pool的模板类型参数UserAllocator是一个用户定义的内存分配器,它实现了特定的内存分配算法,通常可以直接用默认的default_user_allocator_new_delete,它内部使用new[]和delete[]分配内存。
pool的构造函数接受一个size_type类型的整数requested_size,指示每次分配内存块的大小(而不是内存池的大小),这个值可以用get_requested_size()获得。pool会根据需要自动地向系统申请或归还使用的内存,在析构时,pool将自动释放它所持有的所有内存。
成员函数malloc()和ordered_malloc()的行为很类似C中的全局函数malloc(),用void*指针返回从内存池中分配的内存块,大小为构造函数中指定的requested_size。如果内存分配失败,函数会返回0(即空指针),不会抛出异常。malloc()从内存池中任意分配一个内存块,而ordered_malloc()则在分配的同时合并空闲块链表。ordered_malloc()带参数的形式还可以连续分配n块的内存。分配后的内存块可以用is_from()函数测试是否是从这个内存池分配出去的。
与malloc()对应的一组函数是free(),用来手工释放之前分配的内存块,这些内存块必须是从这个内存池分配出去的(is_from(chunk)==true)。一般情况内存池会自动管理内存分配,不应该调用free(),除非你认为内存池的空间已经不足,必须释放已经分配的内存。
最后还有两个成员函数:release_memory()让内存池释放所有未被分配的内存,但已分配的内存块不受影响;purge_memory()则强制释放pool持有的所有内存,不管内存块是否被使用。实际上,pool的析构函数就是调用的purge_memory()。这两个函数一般情况下也不应该由程序员手工调用。

3.7.3 用法

pool很容易使用,可以像C中的malloc()一样分配内存,然后随意使用。除非有特殊要求,否则不必对分配的内存调用free()释放,pool会很好地管理内存。例如:

int main(){
    pool<> p1(sizeof(int));                     // 一个可分配int的内存池

    int *p = static_cast<int*>(p1.malloc());   // 必须把void*转换成需要的类型
    assert(p1.is_form(p));

    p1.free();
    for(int i=0;i<100;i++){                    // 释放内存池分配的内存卡
        p1.ordered_malloc(100);                // 连续分配大量的内存
    }
}                                              // 内存池对象析构,所以分配的内存在这里都被释放

因为pool在分配内存失败的时候不会抛出异常,所以实际编写的代码应该检查malloc()函数返回的指针,以防止空指针错误,不过通常这种情况极少出现:

int *p = static_cast<int *>(p1.malloc());
if (p != nullptr)
    ...

关于pool没有更多的解释,因为它真的很容易使用,只需要注意一点:它只能作为普通数据类型如int、double等的内存池,不能应用于复杂的类和对象,因为它只分配内存,不调用构造函数,这个时候我们需要用object_pool。

3.8 object_pool

object_pool是用于类实例(对象)的内存池,它的功能与pool类似,但会在析构时对所有已经分配的内存块调用析构函数,从而正确地释放资源。
object_pool位于名字空间boost,需要包含头文件

3.8.1 类摘要

template <typename T, typename UserAllocator> 
class object_pool :protected pool<UserAllocator> 
{ 
public: 
     typedef T element_type; 
public: 
     object_pool();                             // 构造函数
     ~object_pool();                            // 析构函数

     element_type * malloc();                   // 分配内存
     void free(element_type * p);               // 归还内存
     bool is_from(element_type * p) const;      // 测试内存块的归属

     element_type * construct(...);             // 创建对象
     void destroy(element_type * p);            // 销毁对象
};

3.8.2 操作函数

object_pool是pool的子类,但它使用的是保护继承,因此不能使用pool的接口,但基本操作还是很相似的。
object_pool的模板类型参数T指定了object_pool要分配的元素类型,要求其析构函数不能抛出异常。一旦在模板中指定了类型,object_pool实例就不能再用于分配其他类型的对象。
malloc()和free()函数分别分配和释放一块类型为T*的内存块,同样,可以用is_from()来测试内存块的归属,只有是本内存池分配的内存才能被free()释放。但它们被调用时并不调用类的构造函数和析构函数,也就是说,操作的是一块原始内存块,里面的值是未定义的,因此我们应当尽量少使用malloc()和free()。
object_pool的特殊之处是construct()和destroy()函数,这两个函数是object_pool的真正价值所在。construct()实际上是一组函数,有多个参数的重载形式(目前最多支持3个参数,但可以扩展),它先调用malloc()分配内存,然后再在内存块上使用传入的参数调用类的构造函数,返回的是一个已经初始化的对象指针。destory()则先调用对象的析构函数,然后再用free()释放内存块。
这些函数都不会抛出异常,如果内存分配失败,将返回0(即空指针)。

3.8.3 用法

object_pool的用法也很简单,我们既可以像pool那样分配原始内存块,也可以使用construct()来直接在内存池中创建对象。当然,后一种使用方法是最方便的,也是本书所推荐的。
下面的代码示范了object_pool的用法:

#include <iostream>
#include <string>
#include <boost/pool/object_pool.hpp>

using namespace std;
using namespace boost;

struct demo_class                                  // 一个示范用的类
{
public:
    int a, b, c;
    demo_class(int x = 1, int y = 2, int z = 3) :
            a(x), b(y), c(z) {}
};

int main() {
    object_pool<demo_class> pl;                   // 对象内存池
    auto p = pl.malloc();                         // 分配一个原始内存块
    assert(pl.is_from(p));

    // p指向的内存未经过初始化
    assert(p->a != 1 || p->b != 2 || p->c != 3);

    p = pl.construct(7, 8, 9);                    // 构造一个对象,可以传递参数
    assert(p->a == 7);

    object_pool<string> pls;                      // 定义一个分配 string 对象的内存池
    for (int i = 0; i < 10; ++i) {                // 连续分配大量 string 对象
        string *ps = pls.construct("hello object_pool");
        cout << *ps << endl;
    }
}                                                 // 所有创建的对象在这里都被正确析构、释放内存

3.8.4 更多的构造参数

默认情况下,在使用object_pool的construct()的时候我们只能最多使用3个参数来创建对象。大多数情况下这都是足够的,但有的时候我们可能会定义3个以上参数的构造函数,此时construct()的默认重载形式就不能用了。
很遗憾,construct()并没有及时跟进C++标准使用可变参数模板支持任意数量的参数构造。它基于宏预处理m4(通常UNIX系统自带,也有Windows的版本)实现了一个变通的扩展机制,可以生成接受任意数量参数的construct()函数代码。
pool库在目录/boost/pool/detail下提供了一个名为pool_construct.m4和pool_construct_simple.m4的脚本,并同时提供可在UNIX和Windows下运行的同名sh和bat可执行脚本文件。只需要简单地向批处理脚本传递一个整数的参数N,m4就会自动生成能够创建具有N个参数的construct()函数源代码。
例如,在Linux下,执行命令:

./pool_construct_simple.sh 5;./pool_construct.sh 5

将生成两个同名的.ipp文件,里面包含了新的construct()函数定义,能够支持最多传递5个参数创建对象。
m4的解决方案显得比较笨拙,使用可变参数模板特性,我们可以定义一个辅助模板函数,支持任意数量的参数,彻底解决这个问题:

template<typename P, typename ... Args>           // 可变参数模板
inline typename P::element_type* 
construct(P& p, Args&& ... args) { 
    typename P::element_type* mem = p.malloc(); 
    assert(mem != 0); 
    new (mem) typename P::element_type( 
        std::forward<Args>(args)...);             // 完美转发
    return mem; 
}

自由函数construct()接受任意多个参数,第一个是object_pool对象,其后是创建对象所需参数,要创建的对象类型可以使用object_pool的内部类型定义element_type来获得。函数中首先调用malloc()分配一块内存,然后调用不太常见的“定位new表达式”(placementnewexpression)创建对象。
假设我们有如下的一个4参数构造函数的类:

struct demo_class {
    demo_class(int, int, int, int)               // 构造函数接受4个参数
    {
        cout << "demo_class ctor" << endl;
    }

    ~demo_class() {
        cout << "demo_class dtor" << endl;
    }
};

那么使用自定义的 construct()创建对象的代码就是:

object_pool<demo_class> pl;
auto d = construct(pl, 1, 2, 3, 4);              // 使用自定义扩展

3.9 singleton_pool

singleton_pool与pool的接口完全一致,可以分配简单数据类型(POD)的内存指针,但它是一个单件。
singleton_pool位于名字空间boost,需要包含头文件
singleton_pool默认使用boost.thread库提供线程安全保证,所以需要链接boost_thread库,如果不使用多线程,那么可以在头文件前定义宏BOOST_POOL_NO_MT。

3.9.1 类摘要

singleton_pool 的类摘要如下:

class singleton_pool {
public:
    static bool is_from(void *ptr);

    static void *malloc();                      // 分配内存
    static void *ordered_malloc();
    static void *ordered_malloc(size_type n);

    static void free(void *ptr);               // 归还内存
    static void ordered_free(void *ptr);
    static void free(void *ptr, std::size_t n);
    static void ordered_free(void *ptr, size_type n);

    static bool release_memory();              // 释放内存
    static bool purge_memory();
};

3.9.2 用法

singleton_pool主要有两个模板类型参数(其余的可以使用默认值)。第一个Tag仅仅是用于标记不同的单件,可以是空类,甚至只是声明(这个形式还被用于4.7节的boost.exception)。第二个参数RequestedSize等同于pool构造函数中的整数requested_size,指示pool分配内存块的大小。
singleton_pool的接口与pool完全一致,但成员函数均是静态的,所以不需要声明singleton_pool的实例①,直接用域操作符“::”来调用静态成员函数。因为singleton_pool是单件,所以它的生命周期与整个程序同样长,除非手动调用release_memory()或purge_memory(),否则singleton_pool不会自动释放所占用的内存。除了这两点,singleton_pool的用法与pool完全相同。
下面的代码示范了singleton_pool的用法:

struct pool_tag{};                                  // 仅仅用于标记的空类
typedef singleton_pool<pool_tag, sizeof(int)> spl;  // 内存池定义
int main()  { 
    int *p = (int*)spl::malloc();                   // 分配一个整数内存块
    assert(spl::is_from(p)); 
    spl::release_memory();                          // 释放所有未被分配的内存
}                                          // spl的内存直到程序结束才完全释放,而不是退出作用域

singleton_pool在使用时最好使用typedef来简化名称,否则会使得类型名过于冗长而难以使用。如代码中所示:

typedef singleton_pool<pool_tag, sizeof(int)> spl;

用于标记的类pool_tag可以再进行简化,直接在模板参数列表中声明tag类,这样可以在一条语句中完成对singleton_pool的类型定义,例如:

typedef singleton_pool<struct pool_tag, sizeof(int)> spl;

① 因为使用了单件模式,用户也无法创建singleton_pool的实例。

3.10 pool_alloc

pool_alloc提供了两个可以用于标准容器模板参数的内存分配器,分别是pool_alloc和fast_pool_allocator,它们的行为与之前的内存池类有一点不同—当内存分配失败时会抛出异常std::bad_alloc。它们位于名字空间boost,需要包含头文件
除非有特别的需求,我们应该总使用标准库实现自带的内存分配器,使用pool_alloc需要经过仔细的测试,以保证它与容器可以共同工作。下面的代码示范了pool_alloc的用法:

vector<int, pool_allocator<int> > v;        // pool_allocator代替默认的内存分配器

v.push_back(10);                            // vector将使用新的分配器良好工作
cout << v.size()

3.11 总结

内存管理是C++程序开发中永恒的话题,因为没有垃圾回收机制,小心谨慎地管理内存等系统资源是每一个C++程序员都必须面对的问题。C++标准提供了unique_ptr、shared_ptr和weak_ptr,较好地减轻了程序员的内存管理负担,但没有解决所有问题。本章讨论了Boost里关于内存管理的两个库:smart_ptr和pool。
smart_ptr库
smart_ptr库提供了数种新型智能指针,非常接近C++标准的定义,可以有效地消除new和delete的显式使用,减少甚至杜绝代码资源泄漏。scoped_ptr是smart_ptr库中最容易学习和使用的一个,它的行为类似unique_ptr,但所有权更明确,清晰地表明了这种智能指针只能在声明的作用域中使用,不能转让,任何对它的复制企图都会失败。这个特点对代码的后期维护工作非常有用。
shared_ptr可能是最有用的智能指针,也是这些智能指针中最“智能”的一个,不仅可以管理内存,也可以管理其他系统资源,能够应用于许多场合。它可以自动地计算指针的引用计数,其行为最接近原始指针。几乎可以用在任何原始指针可以使用的地方,并且不用承担资源泄漏的风险。shared_ptr不仅可以保存指针,通过配置删除器也可以自动释放指针关联的资源。在基本的用法之外,我们还讨论了shared_ptr的很多其他用法,如实现pimpl惯用法、应用于工厂模式、别名构造、持有任意对象的指针等,这些用法进一步展示了它的强大功能。
为了方便智能指针的使用,smart_ptr库提供了工厂函数make_unique()和make_shared(),进一步消除了代码中的new操作符。我们还简要讨论了smart_ptr里的另两个组件:weak_ptr是一个弱引用,能够“静态”地观察shared_ptr而不影响引用计数,intrusive_ptr则为自行实现引用计数智能指针提供了通用技术方案。
pool库
pool库是Boost程序库在内存管理方面提供的另一个有用工具,它实现了高效的内存池,用于管理内存资源。pool库提供了pool、object_pool、singleton_pool和pool_alloc四种形式的内存池,适合于各种情形的应用。可以完全把它们当做是一个小型的垃圾回收机制,在内存池中随意地动态创建对象,而完全不用关心它的回收,也无需对原有类做任何形式的修改。
pool库的四个内存池类中前三个都很有用,尤其是object_pool,它可以统一地管理各种对象的创建与销毁,能够很好地应用在各种规模的面向对象软件系统中。至于pool_alloc,它是符合C++标准的一个内存分配器实现,快速且高效,但通常标准库自带的内存分配器会更好地与容器配合工作,使用pool_alloc时需要仔细地评估可能带来的影响。
pool库还提供一个底层的实现类simple_segregated_storage,它实现了简单分隔存储的管理机制,是pool库其他类的基础。它不适合大多数库用户,但可以作为自行实现内存池类的一个很好的起点。