[CPP] 智能指针

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

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

参考资料:安全

shared_ptr

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

注意,这里使用 shared_ptr 可以实现自动 delete ,可是若是使用以前仍须要 new 的话,代码风格就会变得很「奇怪」,由于 new/delete 老是须要成对出现的,因此尽量使用封装后的 make_shared 来「代替」newide

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 的发生。ui

内部实现

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

初始化

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

下面是正确的方式。atom

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 这个地址,但这个地址在栈上(而不是堆),所以会发生运行时错误。spa

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 要注意避免循环引用
相关文章
相关标签/搜索