[CPP] 智慧指標

sinkinben發表於2021-01-18

介紹 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 .

[CPP] 智慧指標

初始化

  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

[CPP] 智慧指標

示例程式碼:

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 要注意避免迴圈引用

相關文章