c++++智能指針通過raii機制自動管理內存,避免內存泄漏。1. unique_ptr實現獨占式所有權,確保同一時間只有一個指針指向對象,支持顯式轉移所有權,適用于資源管理和工廠函數返回值;2. shared_ptr采用引用計數實現共享所有權,最后一個shared_ptr銷毀時釋放對象,適合多指針共享資源的場景,并可結合weak_ptr解決循環引用;3. weak_ptr提供弱引用,不增加引用計數,用于觀察對象狀態或打破shared_ptr間的循環引用;4. 推薦使用make_shared創建shared_ptr,提升異常安全性和性能;5. 多線程環境下shared_ptr線程安全但對象訪問需同步,unique_ptr非線程安全;6. 自定義刪除器可用于釋放非內存資源或執行特定清理操作;7. 選擇智能指針時需權衡所有權模型、性能及線程安全等因素,慎用與原始指針的交互以避免重復釋放或懸掛指針問題。
c++智能指針本質上是為了更好地管理動態分配的內存,避免內存泄漏。它們通過RaiI(Resource Acquisition Is Initialization)機制,在對象生命周期結束時自動釋放資源。
unique_ptr、shared_ptr、weak_ptr
unique_ptr
立即學習“C++免費學習筆記(深入)”;
unique_ptr是獨占式指針,意味著同一時間只能有一個unique_ptr指向特定的對象。它非常輕量級,開銷幾乎與原始指針相同,而且保證了對象所有權的唯一性。
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "MyClass constructedn"; } ~MyClass() { std::cout << "MyClass destructedn"; } void doSomething() { std::cout << "Doing something...n"; } }; int main() { std::unique_ptr<MyClass> ptr(new MyClass()); // 創建 unique_ptr ptr->doSomething(); // 使用 -> 訪問對象成員 // std::unique_ptr<MyClass> ptr2 = ptr; // 錯誤!不能復制 unique_ptr std::unique_ptr<MyClass> ptr2 = std::move(ptr); // 正確,轉移所有權 if (ptr) { ptr->doSomething(); // 不會執行,因為 ptr 已經為空 } if (ptr2) { ptr2->doSomething(); // ptr2 現在擁有對象 } return 0; // ptr2 析構,MyClass 對象被銷毀 }
unique_ptr的主要用途:
- 資源管理: 確保動態分配的對象在不再需要時被自動釋放。
- 所有權轉移: 通過std::move顯式地轉移所有權。
- 工廠函數: 用于從工廠函數返回動態分配的對象。
shared_ptr
shared_ptr允許多個指針指向同一個對象,它使用引用計數來跟蹤有多少個shared_ptr指向該對象。當最后一個shared_ptr被銷毀時,對象才會被釋放。
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "MyClass constructedn"; } ~MyClass() { std::cout << "MyClass destructedn"; } void doSomething() { std::cout << "Doing something...n"; } }; int main() { std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); // 推薦使用 make_shared std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有權 std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 輸出 2 std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 輸出 2 ptr1->doSomething(); ptr2->doSomething(); ptr1.reset(); // 釋放 ptr1 的所有權 std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 輸出 0 std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 輸出 1 return 0; // ptr2 析構,MyClass 對象被銷毀 }
shared_ptr的主要用途:
- 共享所有權: 當多個對象需要共享對一個資源的所有權時。
- 循環引用處理: 與weak_ptr結合使用,打破循環引用。
- 并發編程: 在多線程環境中安全地共享資源。
weak_ptr
weak_ptr是一種弱引用,它指向由shared_ptr管理的對象,但不增加引用計數。weak_ptr不能直接訪問對象,必須先轉換為shared_ptr才能使用。當shared_ptr管理的對象被銷毀時,weak_ptr會自動失效。
#include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "MyClass constructedn"; } ~MyClass() { std::cout << "MyClass destructedn"; } void doSomething() { std::cout << "Doing something...n"; } }; int main() { std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>(); std::weak_ptr<MyClass> weakPtr = sharedPtr; if (auto ptr = weakPtr.lock()) { // 嘗試獲取 shared_ptr ptr->doSomething(); // 安全地訪問對象 std::cout << "Object is still aliven"; } else { std::cout << "Object has been destroyedn"; } sharedPtr.reset(); // 釋放 shared_ptr 的所有權 if (auto ptr = weakPtr.lock()) { ptr->doSomething(); // 不會執行 std::cout << "Object is still aliven"; } else { std::cout << "Object has been destroyedn"; // 輸出此行 } return 0; }
weak_ptr的主要用途:
- 觀察者模式: 允許觀察對象的狀態,而不會影響對象的生命周期。
- 緩存: 緩存對象,當對象不再被使用時自動釋放。
- 打破循環引用: 解決shared_ptr可能導致的循環引用問題。
如何選擇合適的智能指針?
在選擇智能指針時,需要考慮以下因素:
- 所有權模型: 是否需要獨占所有權還是共享所有權?
- 性能: unique_ptr開銷最小,shared_ptr開銷較大。
- 線程安全: shared_ptr是線程安全的,unique_ptr不是。
- 循環引用: 是否需要處理循環引用問題?
為什么推薦使用make_shared?
std::make_shared是一個模板函數,用于創建一個shared_ptr并初始化其指向的對象。與先new再用shared_ptr包裝相比,make_shared的主要優勢在于:
- 異常安全: 如果new操作成功,但在shared_ptr構造函數中拋出異常,會導致內存泄漏。make_shared將對象的內存分配和shared_ptr的構造放在一起,避免了這個問題。
- 性能優化: make_shared通常會一次性分配對象和shared_ptr控制塊的內存,減少內存分配次數,提高性能。
智能指針與原始指針的混合使用問題
雖然智能指針旨在替代原始指針,但在某些情況下,仍然需要與原始指針交互,例如:
- 與舊代碼庫集成: 舊代碼庫可能使用原始指針,需要將智能指針轉換為原始指針進行交互。
- 底層操作: 某些底層操作可能需要直接訪問內存地址。
在與原始指針交互時,需要格外小心,避免出現以下問題:
- 重復釋放: 不要使用原始指針delete由智能指針管理的對象。
- 懸掛指針: 當智能指針釋放對象后,原始指針可能變成懸掛指針。
可以使用get()方法從智能指針獲取原始指針,但必須謹慎使用,并確保在使用原始指針時,智能指針仍然有效。
如何避免循環引用?
循環引用是指兩個或多個對象相互持有shared_ptr,導致引用計數永遠不為零,對象無法被釋放。
例如:
#include <iostream> #include <memory> class A; // 前向聲明 class B { public: std::shared_ptr<A> a; ~B() { std::cout << "B destructedn"; } }; class A { public: std::shared_ptr<B> b; ~A() { std::cout << "A destructedn"; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; b->a = a; // 循環引用 // a 和 b 都不會被銷毀,導致內存泄漏 return 0; }
解決循環引用的常用方法是使用weak_ptr。將其中一個shared_ptr改為weak_ptr,打破循環引用。
#include <iostream> #include <memory> class A; // 前向聲明 class B { public: std::weak_ptr<A> a; // 使用 weak_ptr ~B() { std::cout << "B destructedn"; } }; class A { public: std::shared_ptr<B> b; ~A() { std::cout << "A destructedn"; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; b->a = a; // 循環引用被打破 // a 和 b 都會被銷毀 return 0; }
智能指針在多線程環境下的使用
shared_ptr本身是線程安全的,多個線程可以同時訪問和修改同一個shared_ptr對象,而不會導致數據競爭。但是,shared_ptr指向的對象本身并不一定是線程安全的,需要額外的同步機制來保護對象的訪問。
unique_ptr不是線程安全的,不應該在多個線程之間共享。如果需要在多線程之間傳遞所有權,可以使用std::move顯式地轉移所有權。
weak_ptr本身也是線程安全的,但需要注意,在將weak_ptr轉換為shared_ptr時,可能會出現競爭條件。
自定義刪除器
智能指針允許使用自定義刪除器,用于在對象被銷毀時執行特定的操作。自定義刪除器可以是一個函數、函數對象或Lambda表達式。
#include <iostream> #include <memory> void customDeleter(int* ptr) { std::cout << "Custom deleter calledn"; delete ptr; } int main() { std::unique_ptr<int, void(*)(int*)> ptr(new int(10), customDeleter); // 或者使用 lambda 表達式 std::shared_ptr<int> sharedPtr(new int(20), [](int* ptr) { std::cout << "Lambda deleter calledn"; delete ptr; }); return 0; }
自定義刪除器的主要用途:
- 釋放非內存資源: 例如文件句柄、網絡連接等。
- 執行特定的清理操作: 例如關閉數據庫連接、釋放鎖等。
- 與C風格API集成: 當需要使用C風格的釋放函數時。
智能指針的性能考量
雖然智能指針提供了很多便利,但也帶來了一些性能開銷:
- 引用計數: shared_ptr需要維護引用計數,增加了一些額外的開銷。
- 虛函數表: 如果使用自定義刪除器,可能會增加虛函數表的開銷。
- 內存分配: shared_ptr的控制塊和對象可能需要分別分配內存。
在性能敏感的場景下,需要仔細評估智能指針的性能影響,并選擇合適的智能指針類型。在不需要共享所有權的情況下,優先使用unique_ptr。
總結
C++智能指針是管理動態內存的強大工具,可以有效地避免內存泄漏和懸掛指針。理解不同類型的智能指針的特點和適用場景,可以編寫更安全、更可靠的C++代碼。記住,選擇合適的智能指針是關鍵,并且需要謹慎處理與原始指針的交互,以避免潛在的問題。