轉載來自:https://subingwen.cn/cpp/shared_ptr/#2-%E6%8C%87%E5%AE%9A%E5%88%A0%E9%99%A4%E5%99%A8
在 C++ 中沒有垃圾回收機制,必須自己釋放分配的內存,否則就會造成內存泄露。解決這個問題最有效的方法是使用智能指針(smart pointer)。智能指針是存儲指向動態分配(堆)對象指針的類,用於生存期的控制,能夠確保在離開指針所在作用域時,自動地銷毀動態分配的對象,防止內存泄露。
智能指針的核心實現技術是引用計數,每使用它一次,內部引用計數加1,每析構一次內部的引用計數減1,減為0時,刪除所指向的堆內存。
C++11 中提供了三種智能指針,使用這些智能指針時需要引用頭文件 <memory>:
std::shared_ptr:共享的智能指針
std::unique_ptr:獨占的智能指針
std::weak_ptr:弱引用的智能指針,它不共享指針,不能操作資源,是用來監視 shared_ptr 的。
1. shared_ptr 的初始化
共享智能指針是指多個智能指針可以同時管理同一塊有效的內存,共享智能指針 shared_ptr 是一個模板類,如果要進行初始化有三種方式:
通過構造函數、std::make_shared 輔助函數以及 reset 方法。共享智能指針對象初始化完畢之后就指向了要管理的那塊堆內存,如果想要查看當前有多少個智能指針同時管理着這塊內存可以使用共享智能指針提供的一個成員函數 use_count,函數原型如下:
// 管理當前對象的 shared_ptr 實例數量,或若無被管理對象則為 0。 long use_count() const noexcept;
1.1 通過構造函數初始化
// shared_ptr<T> 類模板中,提供了多種實用的構造函數, 語法格式如下: std::shared_ptr<T> 智能指針名字(創建堆內存);
測試代碼如下:
#include <iostream> #include <memory> using namespace std; int main() { // 使用智能指針管理一塊 int 型的堆內存 shared_ptr<int> ptr1(new int(520)); cout << "ptr1管理的內存引用計數: " << ptr1.use_count() << endl; // 使用智能指針管理一塊字符數組對應的堆內存 shared_ptr<char> ptr2(new char[12]); cout << "ptr2管理的內存引用計數: " << ptr2.use_count() << endl; // 創建智能指針對象, 不管理任何內存 shared_ptr<int> ptr3; cout << "ptr3管理的內存引用計數: " << ptr3.use_count() << endl; // 創建智能指針對象, 初始化為空 shared_ptr<int> ptr4(nullptr); cout << "ptr4管理的內存引用計數: " << ptr4.use_count() << endl; return 0; }
21測試代碼輸出的結果如下:
ptr1管理的內存引用計數: 1 ptr2管理的內存引用計數: 1 ptr3管理的內存引用計數: 0 ptr4管理的內存引用計數: 0
如果智能指針被初始化了一塊有效內存,那么這塊內存的引用計數 + 1,如果智能指針沒有被初始化或者被初始化為 nullptr 空指針,引用計數不會 + 1。另外,不要使用一個原始指針初始化多個 shared_ptr。
int *p = new int; shared_ptr<int> p1(p); shared_ptr<int> p2(p); // error, 編譯不會報錯, 運行會出錯
1.2 通過拷貝和移動構造函數初始化
當一個智能指針被初始化之后,就可以通過這個智能指針初始化其他新對象。在創建新對象的時候,對應的拷貝構造函數或者移動構造函數就被自動調用了。
#include <iostream> #include <memory> using namespace std; int main() { // 使用智能指針管理一塊 int 型的堆內存, 內部引用計數為 1 shared_ptr<int> ptr1(new int(520)); cout << "ptr1管理的內存引用計數: " << ptr1.use_count() << endl; //調用拷貝構造函數 shared_ptr<int> ptr2(ptr1); cout << "ptr2管理的內存引用計數: " << ptr2.use_count() << endl; shared_ptr<int> ptr3 = ptr1; cout << "ptr3管理的內存引用計數: " << ptr3.use_count() << endl; //調用移動構造函數 shared_ptr<int> ptr4(std::move(ptr1)); cout << "ptr4管理的內存引用計數: " << ptr4.use_count() << endl; std::shared_ptr<int> ptr5 = std::move(ptr2); cout << "ptr5管理的內存引用計數: " << ptr5.use_count() << endl; return 0; }
測試程序輸入的結果:
C++
ptr1管理的內存引用計數: 1 ptr2管理的內存引用計數: 2 ptr3管理的內存引用計數: 3 ptr4管理的內存引用計數: 3 ptr5管理的內存引用計數: 3
如果使用拷貝的方式初始化共享智能指針對象,這兩個對象會同時管理同一塊堆內存,堆內存對應的引用計數也會增加;如果使用移動的方式初始智能指針對象,只是轉讓了內存的所有權,管理內存的對象並不會增加,因此內存的引用計數不會變化。
1.3 通過 std::make_shared 初始化
通過 C++ 提供的 std::make_shared() 就可以完成內存對象的創建並將其初始化給智能指針,函數原型如下:
template< class T, class... Args > shared_ptr<T> make_shared( Args&&... args );
T:模板參數的數據類型
Args&&... args :要初始化的數據,如果是通過 make_shared 創建對象,需按照構造函數的參數列表指定
測試代碼如下:
#include <iostream> #include <string> #include <memory> using namespace std; class Test { public: Test() { cout << "construct Test..." << endl; } Test(int x) { cout << "construct Test, x = " << x << endl; } Test(string str) { cout << "construct Test, str = " << str << endl; } ~Test() { cout << "destruct Test ..." << endl; } }; int main() { // 使用智能指針管理一塊 int 型的堆內存, 內部引用計數為 1 shared_ptr<int> ptr1 = make_shared<int>(520); cout << "ptr1管理的內存引用計數: " << ptr1.use_count() << endl; shared_ptr<Test> ptr2 = make_shared<Test>(); cout << "ptr2管理的內存引用計數: " << ptr2.use_count() << endl; shared_ptr<Test> ptr3 = make_shared<Test>(520); cout << "ptr3管理的內存引用計數: " << ptr3.use_count() << endl; shared_ptr<Test> ptr4 = make_shared<Test>("我是要成為海賊王的男人!!!"); cout << "ptr4管理的內存引用計數: " << ptr4.use_count() << endl; return 0; }
ptr1管理的內存引用計數: 1 construct Test... ptr2管理的內存引用計數: 1 construct Test, x = 520 ptr3管理的內存引用計數: 1 construct Test, str = 我是要成為海賊王的男人!!! ptr4管理的內存引用計數: 1 destruct Test ... destruct Test ... destruct Test ...
使用 std::make_shared() 模板函數可以完成內存地址的創建,並將最終得到的內存地址傳遞給共享智能指針對象管理。如果申請的內存是普通類型,通過函數的()可完成地址的初始化,如果要創建一個類對象,函數的()內部需要指定構造對象需要的參數,也就是類構造函數的參數。
1.4 通過 reset 方法初始化
共享智能指針類提供的 std::shared_ptr::reset 方法函數原型如下:
void reset() noexcept; template< class Y > void reset( Y* ptr ); template< class Y, class Deleter > void reset( Y* ptr, Deleter d ); template< class Y, class Deleter, class Alloc > void reset( Y* ptr, Deleter d, Alloc alloc ); ptr:指向要取得所有權的對象的指針 d:指向要取得所有權的對象的指針 aloc:內部存儲所用的分配器
測試代碼如下:
#include <iostream> #include <string> #include <memory> using namespace std; int main() { // 使用智能指針管理一塊 int 型的堆內存, 內部引用計數為 1 shared_ptr<int> ptr1 = make_shared<int>(520); shared_ptr<int> ptr2 = ptr1; shared_ptr<int> ptr3 = ptr1; shared_ptr<int> ptr4 = ptr1; cout << "ptr1管理的內存引用計數: " << ptr1.use_count() << endl; cout << "ptr2管理的內存引用計數: " << ptr2.use_count() << endl; cout << "ptr3管理的內存引用計數: " << ptr3.use_count() << endl; cout << "ptr4管理的內存引用計數: " << ptr4.use_count() << endl; ptr4.reset(); cout << "ptr1管理的內存引用計數: " << ptr1.use_count() << endl; cout << "ptr2管理的內存引用計數: " << ptr2.use_count() << endl; cout << "ptr3管理的內存引用計數: " << ptr3.use_count() << endl; cout << "ptr4管理的內存引用計數: " << ptr4.use_count() << endl; shared_ptr<int> ptr5; ptr5.reset(new int(250)); cout << "ptr5管理的內存引用計數: " << ptr5.use_count() << endl; return 0; }
測試代碼輸入的結果:
ptr1管理的內存引用計數: 4 ptr2管理的內存引用計數: 4 ptr3管理的內存引用計數: 4 ptr4管理的內存引用計數: 4 ptr1管理的內存引用計數: 3 ptr2管理的內存引用計數: 3 ptr3管理的內存引用計數: 3 ptr4管理的內存引用計數: 0 ptr5管理的內存引用計數: 1
對於一個未初始化的共享智能指針,可以通過 reset 方法來初始化,當智能指針中有值的時候,調用 reset 會使引用計數減 1。
1.5 獲取原始指針
對應基礎數據類型來說,通過操作智能指針和操作智能指針管理的內存效果是一樣的,可以直接完成數據的讀寫。但是如果共享智能指針管理的是一個對象,那么就需要取出原始內存的地址再操作,可以調用共享智能指針類提供的 get () 方法得到原始地址,其函數原型如下:
T* get() const noexcept;
測試代碼如下:
#include <iostream> #include <string> #include <memory> using namespace std; int main() { int len = 128; shared_ptr<char> ptr(new char[len]); // 得到指針的原始地址 char* add = ptr.get(); memset(add, 0, len); strcpy(add, "我是要成為海賊王的男人!!!"); cout << "string: " << add << endl; shared_ptr<int> p(new int); *p = 100; cout << *p.get() << " " << *p << endl; return 0; }
2. 指定刪除器
當智能指針管理的內存對應的引用計數變為 0 的時候,這塊內存就會被智能指針析構掉了。另外,我們在初始化智能指針的時候也可以自己指定刪除動作,這個刪除操作對應的函數被稱之為刪除器,這個刪除器函數本質是一個回調函數,我們只需要進行實現,其調用是由智能指針完成的。
#include <iostream> #include <memory> using namespace std; // 自定義刪除器函數,釋放int型內存 void deleteIntPtr(int* p) { delete p; cout << "int 型內存被釋放了..."; } int main() { shared_ptr<int> ptr(new int(250), deleteIntPtr); return 0; }
刪除器函數也可以是 lambda 表達式,因此代碼也可以寫成下面這樣:
int main() { shared_ptr<int> ptr(new int(250), [](int* p) {delete p; }); return 0; }
在上面的代碼中,lambda表達式的參數就是智能指針管理的內存的地址,有了這個地址之后函數體內部就可以完成刪除操作了。
在 C++11 中使用 shared_ptr 管理動態數組時,需要指定刪除器,因為 std::shared_ptr的默認刪除器不支持數組對象,具體的處理代碼如下:
int main() { shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; }); return 0; }
在刪除數組內存時,除了自己編寫刪除器,也可以使用 C++ 提供的 std::default_delete<T>() 函數作為刪除器,這個函數內部的刪除功能也是通過調用 delete 來實現的,要釋放什么類型的內存就將模板類型 T 指定為什么類型即可。具體處理代碼如下:
int main() { shared_ptr<int> ptr(new int[10], default_delete<int[]>()); return 0; }
另外,我們還可以自己封裝一個 make_shared_array 方法來讓 shared_ptr 支持數組,代碼如下:
#include <iostream> #include <memory> using namespace std; template <typename T> shared_ptr<T> make_share_array(size_t size) { // 返回匿名對象 return shared_ptr<T>(new T[size], default_delete<T[]>()); } int main() { shared_ptr<int> ptr1 = make_share_array<int>(10); cout << ptr1.use_count() << endl; shared_ptr<char> ptr2 = make_share_array<char>(128); cout << ptr2.use_count() << endl; return 0; }
