[CPP] 智能指针

介绍 C++ 的智能指针 (Smart Pointers) 相关 API。

C++ 中的智能指针是为了解决内存泄漏、重复释放等问题而提出的,它基于 RAII (Resource Acquisition Is Initialization),也称为“资源获取即初始化” 的思想实现。智能指针实质上是一个类,但经过封装之后,在行为语义上的表现像指针。

参考资料:

shared_ptr

shared_ptr 能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式调用 delete,当引用计数变为零的时候就会将对象自动删除。

注意,这里使用 shared_ptr 能够实现自动 delete ,但是如果使用之前仍需要 new 的话,代码风格就会变得很「奇怪」,因为 new/delete 总是需要成对出现的,所以尽可能使用封装后的 make_shared 来「代替」new

shared_ptr 基于引用计数实现,每一个 shared_ptr 的拷贝均指向相同的内存。如果某个 shared_ptr 被析构(生命周期结束),那么引用计数减 1 ,当引用计数为 0 时,自动释放指向的内存。

shared 的所有成员函数,包括拷贝构造函数 (Copy Constructor) 和拷贝赋值运算 (Copy Assignment Operator),都是线程安全的,即使这些 shared_ptr 指向同一对象。但如果是多线程访问同一个 non-const 的 shared_ptr ,那有可能发生资源竞争 (Data Race) 的情况,比如改变这个 shared_ptr 的指向,因此这种情况需要实现多线程同步机制。当然,可以使用 shared_ptr overloads of atomic functions 来防止 Data Race 的发生。

内部实现

如下图所示,shared_ptr 内部仅包括 2 个指针,一个指针指向共享对象,另外一个指针指向 Control block .

初始化

  1. 通过构造函数初始化(废话

下面是正确的方式。

void func1()
{
    int *a = new int[10];
    shared_ptr<int[]> p(a);
    // a is same as p.get()
    cout << a << endl;
    cout << p.get() << endl;
    for (int i = 0; i < 10; i++) p[i] = i;
    for (int i = 0; i < 10; i++) cout << a[i] << ' ';
}
// Output: 1-9

下面是错误的方式,因为 ptr 析构时会释放 &a 这个地址,但这个地址在栈上(而不是堆),因此会发生运行时错误。

int main()
{
    int a = 10;
    shared_ptr<int> ptr(&a);
    // a is same as p.get(), but runs fail
    cout << &a << endl;
    cout << ptr.get() << endl;
}
  1. 如果通过 nullptr 初始化,那么引用计数的初始值为 0 而不是 1 。
shared_ptr<void *> p(nullptr);
cout << p.use_count() << endl;
  1. 不允许通过一个原始指针初始化多个 shared_ptr
int main()
{
    int *p = new int[10];
    shared_ptr<int> ptr1(p);
    shared_ptr<int> ptr2(p);
    cout << p << endl;
    cout << ptr1.get() << endl;
    cout << ptr2.get() << endl;
}

上述方式是错误的。可以通过编译,三行 cout 也能正常输出,但会发生运行时错误,因为 ptr2 会先执行析构函数,释放 p ,然后 ptr1 进行析构的时候,就会对无效指针 p 进行重复释放。

0x7feefd405a10
0x7feefd405a10
0x7feefd405a10
a.out(6286,0x113edde00) malloc: *** error for object 0x7feefd405a10: pointer being freed was not allocated
a.out(6286,0x113edde00) malloc: *** set a breakpoint in malloc_error_break to debug
  1. 通过 make_shared 初始化

make_shared 的参数可以时一个对象,也可以是跟该类的构造函数匹配的参数列表。

auto ptr1 = make_shared<vector<int>>(10, -1);
auto ptr2 = make_shared<vector<int>>(vector<int>(10, -1));

与通过构造函数初始化不同的是,make_shared 允许传入一个临时对象,如以下代码:

int main()
{
    vector<int> v = {1, 2, 3};
    auto ptr = make_shared<vector<int>>(v);
    // &v = 0x7ffeef698690
    // ptr.get() = 0x7fc03ec05a18
    cout << &v << endl;
    cout << ptr.get() << endl;
    // v[0] is still 1
    ptr.get()->resize(3, -1);
    cout << v[0] << endl;
}

通过 ptr.get() 获取指针并修改指向的内存,并不会影响局部变量 v 的内容。

自定义 deleter

在初始化时传入一个函数指针,shared_ptr 在释放指向的对象时,会调用自定义的 deleter 处理释放行为。

int main()
{
    int *p = new int[10];
    auto func = [](int *p) {
        delete[] p;
        cout << "Delete memory at " << p << endl;
    };
    shared_ptr<int> ptr(p, func);
}

那么 deleter 有什么用呢?假如我们有这么一段代码:

class Basic
{
public:
    Basic() { cout << "Basic" << endl; }
    ~Basic() { cout << "~Basic" << endl; }
};
int main()
{
    Basic *p = new Basic[3];
    shared_ptr<Basic> ptr(p);
}

这段代码会发生运行时错误。因为 shared_ptr 默认是使用 delete 去释放指向的对象,但定义了析构函数的对象数组,必须要通过 delete[] 析构,否则产生内存错误。

因此,为了使上述代码正常工作,需要自定义 delete 函数:

shared_ptr<Basic> ptr(p, [](Basic *p){ delete[] p; });

或者(C++17 及其之后的标准支持):

shared_ptr<Base[]> ptr(p);

指向一个函数

根据参考资料 [1] ,shared_ptr 指向一个函数,有时用于保持动态库或插件加载,只要其任何函数被 shared_ptr 引用:

void func() { cout << "hello" << endl; }
int main()
{
    shared_ptr<void()> ptr(func, [](void (*)()) {});
    (*ptr)();
}

注意,这里自定义的 deleter 是必不可少的,否则不能通过编译。

例子

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;
class Base
{
public:
    Base() { cout << "Base" << endl; }
    ~Base() { cout << "~Base" << endl; }
};
class Derived : public Base
{
public:
    Derived() { cout << "  Derived" << endl; }
    ~Derived() { cout << "  ~Derived" << endl; }
};
void worker(shared_ptr<Base> ptr)
{
    this_thread::sleep_for(std::chrono::seconds(1));
    shared_ptr<Base> lp = ptr;
    {
        static std::mutex io_mutex;
        lock_guard<mutex> lock(io_mutex);
        cout << "local pointer in a thread:\n"
             << "  lp.get() = " << lp.get() << ", "
             << "  lp.use_count() = " << lp.use_count() << "\n";
    }
}

int main()
{
    shared_ptr<Base> ptr = make_shared<Derived>();

    cout << "Created a shared Derived (as a pointer to Base)\n"
         << "ptr.get() = " << ptr.get() << ", "
         << "ptr.use_count() = " << ptr.use_count() << '\n';

    thread t1(worker, ptr), t2(worker, ptr), t3(worker, ptr);
    this_thread::sleep_for(std::chrono::seconds(2));
    ptr.reset();
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << ptr.get()
              << ", p.use_count() = " << ptr.use_count() << '\n';
    t1.join(), t2.join(), t3.join();
}

输出:

Base
  Derived
Created a shared Derived (as a pointer to Base)
ptr.get() = 0x7fcabc405a08, ptr.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = 0x0, p.use_count() = 0
local pointer in a thread:
  lp.get() = 0x7fcabc405a08,   lp.use_count() = 6
local pointer in a thread:
  lp.get() = 0x7fcabc405a08,   lp.use_count() = 4
local pointer in a thread:
  lp.get() = 0x7fcabc405a08,   lp.use_count() = 2
  ~Derived
~Base

lp.use_count 也可能是 {5,3,2} 这样的序列。在 worker 传入参数过程中,ptr 被拷贝了 3 次,并且在进入 worker 后,三个线程的局部变量 lp 又把 ptr 拷贝了 3 次,因此 user_count 的最大值是 7 。

unique_ptr

unique_ptr 保证同一时刻只能有一个 unique_ptr 指向给定对象。发生下列情况之一时,指定对象就会被释放:

  • unique_ptr 被销毁(生命周期消亡,被 delete 等情况)
  • unique_ptr 调用 reset 或者进行 ptr1 = move(ptr2) 操作

基于这 2 个特点,non-const 的 unique_ptr 可以把管理对象的所有权转移给另外一个 unique_ptr

示例代码:

class Base
{
public:
    Base() { cout << "Base" << endl; }
    ~Base() { cout << "~Base" << endl; }
};
int main()
{
    auto p = new Base();
    cout << p << endl;
    unique_ptr<Base> ptr(p);
    unique_ptr<Base> ptr2 = std::move(ptr);
    cout << ptr.get() << endl;
    cout << ptr2.get() << endl;
}
/* Output is :
Base
0x7fd81fc059f0
0x0
0x7fd81fc059f0
~Base
 */

在上述代码中,存在 U = move(V) ,当执行该语句时,会发生两件事情。首先,当前 U 所拥有的任何对象都将被删除;其次,指针 V 放弃了原有的对象所有权,被置为空,而 U 则获得转移的所有权,继续控制之前由 V 所拥有的对象。

如果是 const unique_ptr ,那么其指向的对象的作用域 (Scope) 只能局限在这个 const unique_ptr 的作用域当中。

此外,unique_ptr 不能通过 pass by value 的方式传递参数,只能通过 pass by reference 或者 std::move

初始化

shared_ptr 类似。但由于 unique_ptr 的特点,它没有拷贝构造函数,因此不允许 unique_ptr<int> ptr2 = ptr 这样的操作。

下面是 unique_ptr 正确初始化的例子。

  • 指向对象
class Base
{
public:
    Base() { cout << "Base" << endl; }
    ~Base() { cout << "~Base" << endl; }
    void printThis() { cout << this << endl; }
};
int main()
{
    auto p = new Base();
    unique_ptr<Base> ptr(p);
    ptr->printThis();
}
/* Output is:
 Base
 0x7fbe0a4059f0
 ~Base
 */
  • 指向数组
int main()
{
    auto p = new Base[3];
    unique_ptr<Base[]> ptr(p);
    for (int i = 0; i < 3; i++)
        ptr[i].printThis();
}
/* Output is:
Base * 3
0xc18c28 0xc18c29 0xc18c2a
~Base * 3
 */
  • make_unique

make_shared 类似,允许向 make_unique 传入一个临时变量。

void func3()
{
    auto ptr = make_unique<vector<int>>(5, 0);
    for (int i = 0; i < 5;i++) (*ptr)[i] = i;
    for (int x : *ptr) cout << x << ' ';
}
// Output: 0 1 2 3 4

自定义 deleter

unique_ptrdeletershared_ptr 不同,它是基于模版参数实现的。

使用仿函数

struct MyDeleter
{
    void operator()(Base *p)
    {
        cout << "Delete memory[] at " << p << endl;
        delete[] p;
    }
};
unique_ptr<Base[], MyDeleter> ptr(new Base[3]);
// unique_ptr<Base, MyDeleter> ptr(new Base[3]);
// both of them is okay

使用普通函数

unique_ptr<Base[], void (*)(Base * p)> ptr(new Base[3], [](Base *p) {
    cout << "Delete memory[] at " << p << endl;
    delete[] p;
});

使用 std::function

unique_ptr<Base[], function<void(Base *)>> ptr(new Base[3], [](Base *p) { delete[] p; });

注意到,使用普通函数时,模版参数为 void (*)(Base *p) ,这是一种数据类型,该类型是一个指针,指向一个返回值为 void , 参数列表为 (Base *p) 的函数,而 void *(Base *p) 则是在声明一个函数(看不懂可以忽略)。

作为函数参数或返回值

unique_ptr 作为函数参数,只能通过引用,或者 move 操作实现。

下列操作无法通过编译:

void func5(unique_ptr<Base> ptr) {}
int main()
{
    unique_ptr<Base> ptr(new Base());
    func5(ptr);
}

需要改成:

void func5(unique_ptr<Base> &ptr) {}
func(ptr);

或者通过 move 转换为右值引用:

void func5(unique_ptr<Base> ptr)
{
    cout << "ptr in function: " << ptr.get() << endl;
}
int main()
{
    auto p = new Base();
    cout << "p = " << p << endl;
    unique_ptr<Base> ptr(p);
    func5(move(ptr));
    cout << "ptr in main: " << ptr.get() << endl;
}
/* Output is:
   Base
   p = 0xa66c20
   ptr in function: 0xa66c20
   ~Base
   ptr in main: 0
 */

unique_ptr 作为函数返回值,会自动发生 U = move(V) 的操作(转换为右值引用):

unique_ptr<Base> func6()
{
    auto p = new Base();
    unique_ptr<Base> ptr(p);
    cout << "In function: " << ptr.get() << endl;
    return ptr;
}
int main()
{
    auto ptr = func6();
    cout << "In main: " << ptr.get() << endl;
}

成员函数

函数 作用
release returns a pointer to the managed object and releases the ownership (will not delete the object)
reset replaces the managed object (it will delete the object)
swap swaps the managed objects
get returns a pointer to the managed object
get_deleter returns the deleter that is used for destruction of the managed object
operator bool checks if there is an associated managed object (more details)
operator = assigns the unique_ptr, support U = move(V) , U will delete its own object

例子

#include <vector>
#include <memory>
#include <iostream>
#include <fstream>
#include <functional>
#include <cassert>
#include <cstdio>

using namespace std;

// helper class for runtime polymorphism demo
class B
{
public:
    virtual void bar() { cout << "B::bar\n"; }
    virtual ~B() = default;
};
class D : public B
{
public:
    D() { cout << "D::D\n"; }
    ~D() { cout << "D::~D\n"; }
    void bar() override { cout << "D::bar\n"; }
};

// a function consuming a unique_ptr can take it by value or by rvalue reference
unique_ptr<D> passThrough(unique_ptr<D> p)
{
    p->bar();
    return p;
}
// helper function for the custom deleter demo below
void close_file(FILE *fp) { std::fclose(fp); }

// unique_ptr-base linked list demo
class List
{
public:
    struct Node
    {
        int data;
        unique_ptr<Node> next;
        Node(int val) : data(val), next(nullptr) {}
    };
    List() : head(nullptr) {}
    ~List() { while (head) head = move(head->next); }
    void push(int x)
    {
        auto t = make_unique<Node>(x);
        if (head) t->next = move(head);
        head = move(t);
    }

private:
    unique_ptr<Node> head;
};

int main()
{
    cout << "unique ownership semantics demo\n";
    {
        auto p = make_unique<D>();
        auto q = passThrough(move(p));
        assert(!p), assert(q);
    }

    cout << "Runtime polymorphism demo\n";
    {
        unique_ptr<B> p = make_unique<D>();
        p->bar();
        cout << "----\n";

        vector<unique_ptr<B>> v;
        v.push_back(make_unique<D>());
        v.push_back(move(p));
        v.emplace_back(new D());
        for (auto &p : v) p->bar();
    }

    cout << "Custom deleter demo\n";
    ofstream("demo.txt") << "x";
    {
        unique_ptr<FILE, decltype(&close_file)> fp(fopen("demo.txt", "r"), &close_file);
        if (fp) cout << (char)fgetc(fp.get()) << '\n';
    }

    cout << "Linked list demo\n";
    {
        List list;
        for (long n = 0; n != 1000000; ++n) list.push(n);
        cout << "Pass!\n";
    }
}

weak_ptr

weak_ptr 指针通常不单独使用(因为没有实际用处),只能和 shared_ptr 类型指针搭配使用。

weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。

此外,weak_ptr 没有重载 *-> 运算符,因此 weak_ptr 只能访问所指的堆内存,而无法修改它。

weak_ptr 作为一个 Observer 的角色存在,可以获取 shared_ptr 的引用计数,可以读取 shared_ptr 指向的对象。

成员函数:

函数 作用
operator = weak_ptr 可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值
swap 与另外一个 weak_ptr 交换 own objetc
reset 置为 nullptr
use_count 查看与 weak_ptr 指向相同对象的 shared_ptr 的数量
expired 判断当前 weak_ptr 是否失效(指针为空,或者指向的堆内存已经被释放)
lock 如果 weak_ptr 失效,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

例子:

#include <memory>
#include <iostream>
using namespace std;
// global weak ptr
weak_ptr<int> gw;
void observe()
{
    cout << "use count = " << gw.use_count() << ": ";
    if (auto spt = gw.lock()) cout << *spt << "\n";
    else cout << "gw is expired\n";
}
int main()
{
    {
        auto sp = make_shared<int>(233);
        gw = sp;
        observe();
    }
    observe();
}
// Output:
// use count = 1: 233
// use count = 0: gw is expired

总结

使用智能指针的几个重要原则是:

  • 永远不要试图去动态分配一个智能指针,相反,应该像声明函数的局部变量那样去声明智能指针。
  • 使用 shared_ptr 要注意避免循环引用
Tags: