Smart Pointers in C++

在C++中,动态内存的管理依赖于 new 和 delete 关键字。当程序员想要在堆(heap)上为对象分配空间并返回一个指向该对象的指针时,可以使用 new 关键字进行,同时也可以利用该类的构造函数(constructor)对其进行初始化(initialization)。当动态对象不再需要被使用时,我们可以使用 delete 关键字销毁该对象,并释放与之关联的内存。使用动态内存容易造成错误,当我们忘记释放内存或者不小心丢失了指向该内存的指针时,内存泄漏(memory leak)便会发生。为了更加方便地使用和管理动态内存,基于类模版的智能指针便应运而生。顾名思义,智能指针的行为类似于常规指针,但其可以智能地释放所指向的对象,避免造成内存泄漏。

智能指针有哪些

  1. auto_ptr (C++11 已抛弃)
  2. shared_ptr (共享型,强引用类型指针)
  3. unique_ptr (独占型)
  4. weak_ptr (观察型,弱引用类型指针)

auto_ptr

auto_ptr在C++03标准库中被首次引入,但是该智能指针存在设计缺陷,其“剥夺”所有权的特性容易造成内存崩溃,因此在C++11版本的标准库中auto_ptr已被抛弃,新的标准库提供了其他更加安全方便的智能指针。

以该智能指针的重载 “=” 以实现拷贝构造的源码为例:

    _LIBCPP_INLINE_VISIBILITY auto_ptr& operator=(auto_ptr& __p) _NOEXCEPT
        {reset(__p.release()); return *this;}
    template<class _Up> _LIBCPP_INLINE_VISIBILITY auto_ptr& operator=(auto_ptr<_Up>& __p) _NOEXCEPT
        {reset(__p.release()); return *this;}

可以看出,使用 “=” 运算符对auto_ptr进行赋值时,__p的值不仅会被传递给左值并且__p会被置空。由此可知,一块内存只可以允许一个auto_ptr进行管理,因为在赋值操作时,右值上的auto_ptr被无情地release()掉了。这样的特性设计其实是make sense的,由于auto_ptr的实现是基于RAII(保证在任何情况下,使用对象时先构造对象,最后析构对象)的变性(所有权可以被接管)外部初始化(RAII实例的构造函数接管了在外部被创建资源的所有权)类型,若存在多个auto_ptr指向同一块内存,在析构这些auto_ptr时,同一块内存会因被多次释放而造成错误。
基于auto_ptr“剥夺”所有权的特性,需要注意以下几点以防止内存泄漏及崩溃:

  1. 不要访问被用来赋值过的右值(auto_ptr)
  2. 不要采用auto_ptr接收数组指针
  3. 不要将auto_ptr作为参数按照值传递方式传入函数
  4. 不要将auto_ptr作为容器对象

1.不要访问被用来赋值过的右值(auto_ptr)

    auto_ptr<Person> p1(new Person("Jason"));
    auto_ptr<Person> p2;
    p2 = p1;
    p1->getName();

在该程序中,p2“剥夺”了p1对于对象的所有权,与此同时,p1作为右值被auto_ptr的“=”运算符重载操作变为nullptr。因此调用p1的getName()方法时会出现错误,因为本质上的操作是 nullptr->getName()。

2. 不要采用auto_ptr接收数组指针

    _LIBCPP_INLINE_VISIBILITY ~auto_ptr() _NOEXCEPT {delete __ptr_;}

从以上源码可知,当auto_ptr执行其析构函数时,调用的是delete方法而不是delete[]方法,这样做会造成内存泄漏,因为我们仅仅只是释放了数组第一个元素的空间。因此以下程序会造成内存泄漏:

    int* array = new int[10];
    auto_ptr<int> ptr1(array);

3. 不要将auto_ptr作为参数按照值传递方式传入函数

    void FireEmployee(auto_ptr<Person> ptr) {
        // Fire!
        return;
    }

    int main(void) {
        auto_ptr<Person> ptr(new Person("Jason"));
        ptr->getName();

        FireEmployee(ptr);
        ptr->getName();
        return 0;
    }

在该程序中,line11造成了错误,这是因为使用了auto_ptr按照值传递方式传参。在函数调用的过程中,FireEmployee()函数会在其作用域中复制传入的ptr,这会导致原本传入的ptr(实参)失去了对Person对象的所有权,因此函数调用结束后,无法再次访问ptr。一种更加安全的方式便是进行引用传参,函数可以修改为:

    void FireEmployee(auto_ptr<Person>& ptr) {
        // Fire!
        return;
    }

4. 不要将auto_ptr作为容器对象

使用容器时,通常会伴随着对STL算法或者容器成员函数的使用,若某算法或者成员函数对auto_ptr进行第一次拷贝,则auto_ptr会被置为nullptr,再次对同一个auto_ptr进行拷贝抑或是使用该auto_ptr则会造成内存崩溃,引发不良后果。因此不大推荐使用auto_ptr作为容器的对象。

shared_ptr

shared_ptr是一种共享型的智能指针,这是因为多个shared_ptr可以被允许指向相同的对象,这一特性与我们之前浅析过的auto_ptr以及下文即将剖析的unique_ptr大不相同。此外,shared_ptr还属于强引用类型智能指针,那么何为“引用”?每个shared_ptr身上都具备一个关联的计数器,其计数结果便称之为“引用计数”(reference count),引用计数的存在可以让shared_ptr记录程序中总共有多少个shared_ptr指向相同的对象以便智能指针可以在恰当的时机自动释放对象,这一行为依赖于shared_ptr类的析构函数(destructor)所实现。

    smart_ptr::~smart_ptr() {
        if (ptr_ && !shared_count_->reduce_count()) {
            delete ptr_;
            delete shared_count_;
        }
    }

为了更好地理解shared_ptr析构的全过程,笔者截取了简易版本shared_ptr实现的析构代码做粗略分析。可以看出,当shared_ptr调用析构函数时,其引用计数(shared_count_)会递减,而一旦这个shared_ptr的引用计数变为0,它就会自动释放指向的对象及其内置的引用计数类。换言之,对于一块内存,shared_ptr类可以保证只要有任何其他的shared_ptr对象引用它,它就不会被释放。
在使用shared_ptr时,需要关注以下几个方面:

  1. 条件判断中的智能指针
  2. 利用 make_shared (args) 函数初始化 shared_ptr
  3. p.use_count() 与 p.unique() 的使用
  4. 销毁shared_ptr的两种方法

1. 条件判断中的智能指针

如果在一个条件判断中使用智能指针,效果就是检测它是否为空。

    shared_ptr<string> p1;
    if (p1) {
        cout << "该指针不为空" << endl;
    } else {
        cout << "该指针是一个空指针!" << endl;
    }

    -----------------------------
    该指针是一个空指针!
    -----------------------------

注意:默认初始化的智能指针中保存着一个空指针!

2. 利用 make_shared (args) 函数初始化 shared_ptr

在分配和使用动态内存时,调用 make_shared (args) 标准库函数是一种较为推荐的方法。make_shared (args) 函数可以在在动态内存中分配一个对象并用 args 初始化它,最终返回指向此对象的 shared_ptr。使用此方法时要包含头文件“memory”,编译时建议在命令语句后添加 “-std=c++11”。

    // 指向一个值为42的整型 shared_ptr
    shared_ptr<int> p2 = make_shared<int>(42);
    cout << "p2 指向的值是:" << *p2 << endl;

    // 指向一个值为 "999999999" (10个'9') 的 shared_ptr
    shared_ptr<string> p3 = make_shared<string>(10, '9');
    cout << "p3 指向的值是:" << *p3 << endl;

    // 指向一个值初始化的 int (0) 的 shared_ptr
    auto p4 = make_shared<int> ();
    cout << "p4 指向的值是:" << *p4 << endl;

    // (利用 auto 定义对象,较简单) p5 指向一个动态分配的空 vector<string>
    auto p5 = make_shared<vector<string> > ();
    cout << "p5 指向对象的地址(该对象的shared_ptr)为:" << p5 << endl;

    // 指向一个新创建的 Test 类 (利用构造函数初始化其 data 属性为 10,s 属性为“Hello World!”)
    shared_ptr<Test> p6 = make_shared<Test> (10, "Hello World!");
    cout << "p6 指向的对象的 data 为:" << p6->data << endl;
    cout << "p6 指向的对象的 s 为:" << p6->s << endl;

    -----------------------------
    p2 指向的值是:42
    p3 指向的值是:9999999999
    p4 指向的值是:0
    p5 指向对象的地址(该对象的shared_ptr)为:0x6000020802b8
    p6 指向的对象的 data 为:10
    p6 指向的对象的 s 为:Hello World!
    -----------------------------

3. p.use_count() 与 p.unique() 的使用

当进行拷贝或赋值操作时,每个 shared_ptr 都会记录总共有多少个 shared_ptr 指向相同的对象。

    auto p = make_shared<int> (42);
    cout << "与 p 共享对象(指向int 42对象)的智能指针数量为:" << p.use_count() << endl;

    // 检测 p 是否唯一
    if (p.unique()){
        cout << "p 是唯一一个指向其对象的 shared_ptr" << endl;
    } else {
        cout << "p 不是唯一一个指向其对象的 shared_ptr" << endl;
    }

    auto q(p); // 此时 q 是 shared_ptr p 的拷贝,此操作会递增 p 中的计数器
    cout << "与 p 共享对象(指向int 42对象)的智能指针数量为:" << p.use_count() << endl;

    // 检测 p 是否唯一
    if (p.unique()){
        cout << "p 是唯一一个指向其对象的 shared_ptr" << endl;
    } else {
        cout << "p 不是唯一一个指向其对象的 shared_ptr" << endl;
    }

    // shared_ptr q 同样在记录总共有多少个 shared_ptr 指向相同的对象
    cout << "与 q 共享对象(指向int 42对象)的智能指针数量为:" << q.use_count() << endl;

    auto r = make_shared<int> (100);
    cout << "与 r 共享对象(指向int 100对象)的智能指针数量为:" << r.use_count() << endl;

    r = q; // 此操作让 r 指向 q 指向的对象,会递增 q 指向对象的引用次数,递减 r 指向对象的引用次数,最终导致 r 原来指向的对象没有引用者而被自动释放
    cout << "与 r 共享对象(指向int 42对象)的智能指针数量为:" << r.use_count() << endl;
    cout << "与 p 共享对象(指向int 42对象)的智能指针数量为:" << p.use_count() << endl;
    cout << "与 q 共享对象(指向int 42对象)的智能指针数量为:" << q.use_count() << endl;

    -----------------------------
     p 共享对象(指向int 42对象)的智能指针数量为:1
    p 是唯一一个指向其对象的 shared_ptr
     p 共享对象(指向int 42对象)的智能指针数量为:2
    p 不是唯一一个指向其对象的 shared_ptr
     q 共享对象(指向int 42对象)的智能指针数量为:2
     r 共享对象(指向int 100对象)的智能指针数量为:1
     r 共享对象(指向int 42对象)的智能指针数量为:3
     p 共享对象(指向int 42对象)的智能指针数量为:3
     q 共享对象(指向int 42对象)的智能指针数量为:3
    -----------------------------

4. 销毁shared_ptr的两种方法

由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。

    // 已知 r,p,q 指向相同的内存空间(均指向int 42对象)

    // 利用 nullptr 显式销毁
    r = nullptr;
    cout << "与 r 共享对象(之前指向int 42对象)的智能指针数量为:" << r.use_count() << endl;
    cout << "与 p 共享对象(指向int 42对象)的智能指针数量为:" << p.use_count() << endl;

    // 利用 reset() 方法
    q.reset();
    cout << "与 q 共享对象(之前指向int 42对象)的智能指针数量为:" << q.use_count() << endl;
    cout << "与 p 共享对象(指向int 42对象)的智能指针数量为:" << p.use_count() << endl;

    -----------------------------
     r 共享对象(指向int 42对象)的智能指针数量为:0
     p 共享对象(指向int 42对象)的智能指针数量为:2
     q 共享对象(指向int 42对象)的智能指针数量为:0
     p 共享对象(指向int 42对象)的智能指针数量为:1
    -----------------------------

unique_ptr

敬请期待

weak_ptr

敬请期待

Reference

  1. Lippman, S, Lajoie, J, Moo, B 2013, C++ Primer, Fifth Edition, Pearson Education, Inc., Permissions Department, One Lake Street, Upper Saddle River, New Jersey, US.