介紹 C++ 的智慧指標 (Smart Pointers) 相關 API。
C++ 中的智慧指標是為了解決記憶體洩漏、重複釋放等問題而提出的,它基於 RAII (Resource Acquisition Is Initialization),也稱為“資源獲取即初始化” 的思想實現。智慧指標實質上是一個類,但經過封裝之後,在行為語義上的表現像指標。
參考資料:
- [1] https://en.cppreference.com/w/cpp/memory
- [2] https://docs.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-160
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 .
初始化
- 通過建構函式初始化(
廢話)
下面是正確的方式。
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;
}
- 如果通過
nullptr
初始化,那麼引用計數的初始值為 0 而不是 1 。
shared_ptr<void *> p(nullptr);
cout << p.use_count() << endl;
- 不允許通過一個原始指標初始化多個
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
- 通過
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_ptr
的 deleter
與 shared_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
要注意避免迴圈引用