[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_ptr 的所有成員函數,包括拷貝構造函數 (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 的使用,要注意避免循環引用的問題,否則智能指針同樣能造成內存泄漏的問題。

考慮如下場景:A parent has a child, a child knows his/her parent .

class Parent;
class Child;
class Parent
{
public:
    shared_ptr<Child> childptr;
    Parent() { cout << "Parent" << endl; }
    ~Parent() { cout << "~Parent" << endl; }
};
class Child
{
public:
    shared_ptr<Parent> parentptr;
    Child() { cout << "Child" << endl; }
    ~Child() { cout << "~Child" << endl; }
};
int main()
{
    shared_ptr<Parent> parent(new Parent());
    shared_ptr<Child> child(new Child());
    parent->childptr = child;
    child->parentptr = parent;
}

在上述代碼中,只輸出了 "Parent""Child",並沒有輸出 "~Parent""~Child",說明析構函數沒有執行,shared_ptr 離開作用域後沒有釋放 2 個對象。這是爲什麼呢?

    parent                   child
      |                        |
      V                        V
+---Parent---+           +---Child---+
|  childptr  |---------->|           |
|            |<----------| parentptr |
+------------+           +-----------+

parent, child 這 2 個指針離開作用域後,在堆上的 2 個對象依然在相互引用,導致它們的 use_count 依然爲 1 ,因此無法釋放。

可以通過下面的代碼驗證:

int main()
{
    weak_ptr<Parent> wp1;
    weak_ptr<Child> wp2;
    {
        shared_ptr<Parent> parent(new Parent());
        shared_ptr<Child> child(new Child());
        parent->childptr = child, child->parentptr = parent;
        wp1 = parent, wp2 = child;
        cout << "Parent: " << wp1.use_count() << endl; // 2
        cout << "Child: " << wp2.use_count() << endl;  // 2
    }
    cout << "Parent: " << wp1.use_count() << endl;     // 1
    cout << "Child: " << wp2.use_count() << endl;      // 1
}

解決辦法:把 2 個類中的成員變量改爲 weak_ptr<>

總結

使用智能指針的幾個重要原則是:

  • 永遠不要試圖去動態分配一個智能指針,相反,應該像聲明函數的局部變量那樣去聲明智能指針。
  • 使用 shared_ptr 要注意避免循環引用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章