C++11中智能指針的使用(shared_ptr、unique_ptr、weak_ptr)
一、shared_ptr原理
shared_ptr 是另一種智能指針,用于實現多個 shared_ptr 實例共享同一個對象的所有權。它通過內部的控制塊(通常是一個包含計數器和指向對象的指針的結構)來管理對象的生命周期。每當一個新的 shared_ptr 被創建并指向對象時,控制塊中的計數器就會遞增;每當一個 shared_ptr 被銷毀或重置時,計數器就會遞減。當計數器減至零時,對象被刪除。
1.std::shared_ptr的原理
std::shared_ptr
的核心原理是引用計數。它通過一個**控制塊(Control Block)**來管理對象的生命周期。控制塊記錄了以下信息:
1.1 引用計數(Strong Count):
表示當前有多少個 std::shared_ptr
正在管理同一個對象。
當一個 std::shared_ptr
被復制時,引用計數加1。
當一個 std::shared_ptr
被銷毀時,引用計數減1。
當引用計數變為0時,對象會被自動釋放。
1.2 弱引用計數(Weak Count):
用于支持 std::weak_ptr
,表示有多少個 std::weak_ptr
正在觀察這個對象。
當弱引用計數變為0時,控制塊本身也會被銷毀。
1.3 刪除器(Deleter):
一個函數對象,用于在對象被銷毀時執行清理操作(如釋放內存)。
1.4 分配器(Allocator):
用于分配和釋放內存。
1.1 控制塊的結構
控制塊通常是一個獨立的結構體,與 std::shared_ptr
和 std::weak_ptr
共享。它的結構大致如下:
struct ControlBlock {int* ptr; // 指向被管理的對象unsigned* strong; // 引用計數(Strong Count)unsigned* weak; // 弱引用計數(Weak Count)Deleter deleter; // 刪除器
};
1.2 生命周期管理
-
當創建一個新的
std::shared_ptr
時,控制塊會被初始化,引用計數設置為1。 -
當一個
std::shared_ptr
被復制時,引用計數加1。 -
當一個
std::shared_ptr
被銷毀時,引用計數減1。如果引用計數變為0,控制塊會調用刪除器來釋放對象。 -
當最后一個
std::weak_ptr
被銷毀時,控制塊本身也會被銷毀。
1.3 引用計數的線程安全性
std::shared_ptr
的引用計數操作是線程安全的。它使用原子操作來保證在多線程環境下引用計數的正確性。
2、std::shared_ptr的用法
2.1 創建 std::shared_ptr
(1)使用 std::make_shared
推薦使用 std::make_shared
來創建 std::shared_ptr
,因為它更高效且安全。
#include <iostream>
#include<memory>
using namespace std;int main()
{// 創建一個shared_ptrshared_ptr<int> ptr = make_shared<int>(20);cout << "Value = "<< *ptr << endl;
}
std::make_shared
會同時分配對象和控制塊的內存,減少了內存分配的次數。
(2)直接構造
也可以通過構造函數直接創建,但需要小心避免懸掛指針。
int* ptr2 = new int(10);shared_ptr<int> ptr3(ptr2);
(3)從其他智能指針轉換
std::shared_ptr
可以從 std::unique_ptr
或其他 std::shared_ptr
轉換而來。
std::shared_ptr<int> ptr3 = std::make_shared<int>(42);
std::shared_ptr<int> ptr4 = ptr3; // 復制構造,引用計數增加
2.2 引用計數機制
std::shared_ptr
的核心是引用計數。當一個 std::shared_ptr
被復制時,引用計數會增加;當一個 std::shared_ptr
被銷毀時,引用計數會減少。當引用計數為零時,它所管理的對象會被自動釋放。
std::shared_ptr<int> ptr5 = std::make_shared<int>(100);
{std::shared_ptr<int> ptr6 = ptr5; // 引用計數 +1std::cout << "Value: " << *ptr6 << std::endl;std::cout << "ptr5 Use_Count: " << ptr5.use_count() << std::endl;std::cout << "ptr6 Use_Count: " << ptr6.use_count() << std::endl;} // ptr6 超出作用域,引用計數 -1
std::cout << "Value: " << *ptr5 << std::endl; // 仍然可以訪問
std::cout << "Use_Count: " << ptr5.use_count() << std::endl;
代碼運行結果:
在C++中通過 std::shared_ptr
的成員函數 use_count()
來獲取當前 shared_ptr
的引用計數。這個函數返回一個 std::size_t
類型的值,表示當前有多少個 std::shared_ptr
共享同一個控制塊。
2.3 使用自定義刪除器
如果需要對對象進行特殊處理(如釋放資源或調用特定函數),可以為 std::shared_ptr
提供自定義刪除器。
std::shared_ptr<int> ptr7(new int(30), [](int* p) {cout << "Custom Deleter Call" << endl;delete p;});
程序結束,調用客戶自定義的刪除器。
2.4 使用 std::weak_ptr
避免循環引用
std::shared_ptr
可能會導致循環引用問題,從而無法正確釋放資源。通過 std::weak_ptr
可以解決這個問題。
class B;class A
{
public:shared_ptr<B> m_ptrB;~A() { cout << "A 析構" << endl; };
};class B
{
public:shared_ptr<A> m_ptrA;~B() { cout << "B 析構" << endl; };
};int main()
{shared_ptr<A> ptrA = make_shared<A>();shared_ptr<B> ptrB = make_shared<B>();ptrA->m_ptrB = ptrB;ptrB->m_ptrA = ptrA;
}
上述代碼運行時不會調用析構函數。
解決辦法:將B類中的shared_ptr改為weak_ptr后,程序運行結果:
2.5 其他操作
-
reset()
:釋放當前管理的對象,并可選地綁定到新的對象。 -
use_count()
:返回當前對象的引用計數。 -
get()
:返回底層裸指針(不推薦直接使用,僅在必要時)。
std::shared_ptr<int> ptr8 = std::make_shared<int>(200);
std::cout << "Use count: " << ptr8.use_count() << std::endl; // 輸出引用計數
ptr8.reset(); // 釋放對象
3、注意事項
-
循環引用問題
-
如果兩個或多個
std::shared_ptr
相互引用,會導致引用計數永遠不會變為0,從而無法釋放對象。 -
使用
std::weak_ptr
可以解決循環引用問題。
-
-
不要直接操作底層指針
-
盡量避免使用
get()
獲取底層指針,因為這可能導致懸掛指針或內存泄漏。 -
如果需要操作底層指針,建議使用
std::weak_ptr
來確保對象仍然有效。
-
-
性能開銷
-
std::shared_ptr
的引用計數操作是線程安全的,但會帶來一定的性能開銷。 -
如果不需要線程安全,可以考慮使用
std::unique_ptr
。
-
-
優先使用
std::make_shared
std::make_shared
會同時分配對象和控制塊的內存,減少了內存分配的次數,性能更好。
4、注意事項
std::shared_ptr
是一種非常強大的智能指針工具,適用于需要共享所有權的場景。它通過引用計數自動管理內存,減少了內存泄漏和懸掛指針的風險。但在使用時需要注意以下幾點:
-
盡量避免循環引用,必要時使用
std::weak_ptr
。 -
不要直接操作底層指針,除非絕對必要。
-
優先使用
std::make_shared
創建std::shared_ptr
,因為它更高效。
二、unique_ptr原理
unique_ptr是獨占式的智能指針,每一次只會有一個指針指向其給定的對象。當unique_ptr離開其作用域時,其所指的對象會被自動刪除,并且該對象擁有的任何資源都會被釋放。
std::unique_ptr
是 C++11 引入的一種智能指針,用于管理動態分配的資源(如通過 new
分配的內存)。它的核心原理是通過獨占所有權語義來自動管理資源的生命周期,確保資源在合適的時機被釋放,從而避免內存泄漏和野指針問題。
以下是 std::unique_ptr
的工作原理和關鍵特性:
1. 獨占所有權
std::unique_ptr
采用獨占所有權機制,即同一時間只能有一個 unique_ptr
指向某個資源。這意味著:
-
不能復制:
unique_ptr
不能被復制(即沒有拷貝構造函數和拷貝賦值運算符),因為復制會導致多個指針指向同一資源,從而破壞所有權的獨占性。 -
可以移動:
unique_ptr
支持移動語義(通過移動構造函數和移動賦值運算符),允許將資源的所有權從一個unique_ptr
轉移到另一個unique_ptr
。移動操作會將原指針置為nullptr
,確保資源的唯一所有權。
2. 自動釋放資源
std::unique_ptr
在以下情況下會自動釋放其管理的資源:
-
析構函數:當
unique_ptr
被銷毀(如超出作用域、對象析構)時,它會調用資源的析構函數(如delete
)來釋放資源。 -
重置:通過調用
reset()
方法,可以手動釋放當前資源,并可選擇分配新的資源。
3. 定制刪除器
std::unique_ptr
允許用戶自定義刪除器(deleter),這使得它不僅可以管理動態分配的內存,還可以管理其他類型的資源(如文件句柄、網絡連接等)。刪除器是一個可調用對象,用于定義資源的釋放方式。默認情況下,unique_ptr
使用 delete
作為刪除器,但用戶可以通過模板參數或構造函數傳遞自定義刪除器。
struct FileDeleter
{void operator()(FILE* file){fclose(file);cout << "Custom Deleter called" << endl;}
};unique_ptr<FILE,FileDeleter> file(fopen("1.txt","r"));
file.reset(); // 調用自定義刪除器關閉文件
4. 實現原理
std::unique_ptr
的實現基于模板和智能指針的底層機制:
模板參數:std::unique_ptr
是一個模板類,通常有兩個模板參數:
T
:指針指向的類型。
Deleter
:刪除器類型(默認為 std::default_delete<T>
)。
內部存儲:unique_ptr
內部存儲一個裸指針(T*
)和一個刪除器對象。它通過操作這個裸指針來管理資源,并在需要時調用刪除器釋放資源。
移動語義:unique_ptr
的移動構造函數和移動賦值運算符通過交換內部指針和刪除器來實現資源的轉移,確保所有權的唯一性。
5. 使用示例
以下是一個簡單的 std::unique_ptr
使用示例:
#include <iostream>
#include <memory>struct Foo // object to manage
{Foo() { std::cout << "Foo...\n"; }~Foo() { std::cout << "~Foo...\n"; }
};struct D // deleter
{void operator() (Foo* p){std::cout << "Calling delete for Foo object... \n";delete p;}
};int main()
{std::cout << "Creating new Foo...\n";std::unique_ptr<Foo, D> up(new Foo(), D()); // up owns the Foo pointer (deleter D)std::cout << "Replace owned Foo with a new Foo...\n";up.reset(new Foo()); // calls deleter for the old onestd::cout << "Release and delete the owned Foo...\n";up.reset(nullptr);
}
代碼運行結果:
6. 優點
自動管理資源:避免手動調用 delete
,減少內存泄漏風險。
獨占所有權:確保同一時間只有一個指針管理資源,避免野指針問題。
支持自定義刪除器:可以管理除動態內存之外的其他資源。
輕量級:std::unique_ptr
通常只有裸指針大小,性能開銷極小。
7.缺點
- 不能復制:由于獨占所有權,
unique_ptr
不能被復制,這在某些場景下可能需要額外的邏輯來處理。 - 依賴移動語義:需要 C++11 或更高版本支持,因為其依賴于移動構造函數和移動賦值運算符。
總之,std::unique_ptr
是一種非常強大且輕量級的智能指針,適用于大多數需要管理動態資源的場景。
三、weak_ptr原理
在C++中,weak_ptr
是一種智能指針,用于解決shared_ptr
的循環引用問題。它不擁有對象的所有權,而是觀察shared_ptr
管理的對象,避免增加引用計數。以下是weak_ptr
的原理和用法詳解:
1. weak_ptr
的核心原理
不增加引用計數:weak_ptr
指向由shared_ptr
管理的對象,但不會增加其引用計數。當最后一個shared_ptr
被銷毀時,對象仍會被釋放,即使有weak_ptr
存在。
觀察者模式:weak_ptr
只是"觀察"資源,不控制生命周期。若要訪問資源,需臨時轉換為shared_ptr
(通過lock()
方法)。
解決循環引用:在相互持有shared_ptr
的場景中(如雙向鏈表、父-子對象),使用weak_ptr
打破循環,防止內存泄漏.
1.1 與 std::shared_ptr
的關系
-
std::weak_ptr
是基于std::shared_ptr
的。它不會增加對象的引用計數,但會與std::shared_ptr
共享對象的控制塊(control block)。控制塊中包含了對象的引用計數和弱引用計數。 -
引用計數(
use_count
):記錄有多少個std::shared_ptr
指向對象。當引用計數為 0 時,對象會被銷毀。 -
弱引用計數(
weak_count
):記錄有多少個std::weak_ptr
指向對象。弱引用計數不會影響對象的生命周期,但當對象被銷毀時,所有指向該對象的std::weak_ptr
會失效。
1.2 實現原理
-
當創建一個
std::weak_ptr
時,它會從一個std::shared_ptr
或另一個std::weak_ptr
中獲取控制塊的指針,并增加弱引用計數。 -
當使用
std::weak_ptr
的lock()
方法時,它會檢查控制塊中的對象是否仍然存在。如果對象存在,它會返回一個指向該對象的std::shared_ptr
,并增加引用計數;如果對象已經被銷毀,則返回一個空的std::shared_ptr
。 -
當一個
std::weak_ptr
被銷毀時,它會減少弱引用計數。當弱引用計數和引用計數都為 0 時,控制塊也會被銷毀。
2. 基本用法
2.1** 創建weak_ptr
**
必須從shared_ptr
或另一個weak_ptr
構造.
從 std::shared_ptr
創建:從 std::shared_ptr
創建:
std::shared_ptr<int> ptr = std::make_shared<int>(10);
std::weak_ptr<int> w_ptr(ptr); // 從shared_ptr中構造
從另一個 std::weak_ptr
創建:
std::weak_ptr<int> w_ptr2(w_ptr);
2.2** 檢查對象是否仍然存在**
- 使用
expired()
方法檢查對象是否已經被銷毀:
if (w_ptr2.expired()){std::cout << "Object has been destroyed." << std::endl; }else{std::cout << "Object still exists." << std::endl;}
2.3獲取對象的 std::shared_ptr
- 使用
lock()
方法獲取對象的std::shared_ptr
:
std::shared_ptr<int> shared_from_weak = w_ptr2.lock();if (shared_from_weak) {std::cout << "Accessing object: " << *shared_from_weak << std::endl;}else {std::cout << "Object has been destroyed." << std::endl;}
如果對象仍然存在,lock()
方法會返回一個指向該對象的 std::shared_ptr
;如果對象已經被銷毀,則返回一個空的 std::shared_ptr
。
3、std::weak_ptr
的使用場景
3.1解決循環引用問題
-
在使用
std::shared_ptr
時,可能會出現循環引用的情況,導致對象無法被正確銷毀。例如,兩個對象互相持有對方的std::shared_ptr
,它們的引用計數永遠不會為 0。 -
通過將其中一個引用改為
std::weak_ptr
,可以打破循環引用。例如:
class B;class A {
public:std::shared_ptr<B> b_ptr;
};class B {
public:std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循環引用
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;return 0;
}
在這個例子中,B
持有 A
的 std::weak_ptr
,避免了循環引用。
3.2觀察但不擁有對象
當一個對象需要被多個組件觀察,但這些組件不需要擁有對象時,可以使用 std::weak_ptr
。例如,一個觀察者模式中,觀察者可以持有被觀察對象的 std::weak_ptr
,這樣即使觀察者仍然存在,也不會阻止被觀察對象的銷毀。
3.3緩存場景
在某些緩存場景中,可以使用 std::weak_ptr
來存儲對對象的弱引用。當對象被銷毀時,緩存中的 std::weak_ptr
會自動失效,避免了對已銷毀對象的訪問。
4、注意事項
-
線程安全
std::weak_ptr
的操作(如lock()
、expired()
等)是線程安全的,因為它們都是通過控制塊中的原子操作來實現的。
-
生命周期管理
- 使用
std::weak_ptr
時,需要注意對象的生命周期。雖然std::weak_ptr
不會阻止對象的銷毀,但在訪問對象時,必須確保對象仍然存在。
- 使用
-
性能開銷
std::weak_ptr
的使用會帶來一定的性能開銷,因為它需要維護弱引用計數。在性能敏感的場景中,需要權衡使用std::weak_ptr
的利弊。
總之,std::weak_ptr
是一種非常有用的智能指針,它通過與 std::shared_ptr
共享控制塊,提供了對對象的弱引用。它可以用于解決循環引用問題、觀察對象以及緩存場景等。在使用時,需要注意對象的生命周期和性能開銷。