[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 要注意避免循環引用
相關文章
相關標籤/搜索