本文將從這幾方面講解智慧指標:
- 智慧指標的應用場景分析
- 智慧指標的效能分析: 為什麼 shared_ptr 效能比 unique_ptr 差
- 指標作為函式引數時應該傳,傳值、傳引用,還是裸指標?
對於智慧指標的使用,實際上是對所有權和生命週期的思考
1.unique_ptr:專屬所有權
1.1 unique_ptr介紹
我們大多數場景下用到的應該都是 unique_ptr 。
unique_ptr 代表的是專屬所有權,即由 unique_ptr 管理的記憶體,只能被一個物件持有。
所以, unique_ptr 不支援複製和賦值 ,如下:
auto w = std::make_unique<Widget>();
auto w2 = w; // 編譯錯誤
如果想要把 w 複製給 w2, 是不可以的。因為複製從語義上來說,兩個物件將共享同一塊記憶體。
因此, unique_ptr 只支援移動 , 即如下:
auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2獲得記憶體所有權,w此時等於nullptr
unique_ptr 代表的是專屬所有權,如果想要把一個 unique_ptr 的記憶體交給另外一個 unique_ptr 物件管理。 只能使用 std::move 轉移當前物件的所有權 。轉移之後,當前物件不再持有此記憶體,新的物件將獲得專屬所有權。
如上程式碼中,將 w 物件的所有權轉移給 w2 後,w 此時等於 nullptr,而 w2 獲得了專屬所有權。
1.2 unique_ptr的效能
因為 C++ 的 zero cost abstraction 的特點,unique_ptr 在預設情況下和裸指標的大小是一樣的。
所以 記憶體上沒有任何的額外消耗,效能是最優的。
1.3 unique_ptr的使用場景
1.3.1 忘記delete
unique_ptr 一個最簡單的使用場景是用於類屬性。程式碼如下:
class Box
{
public:
Box() : w(new Widget())
{}
~Box()
{}
private:
Widget* w;
};
如果因為一些原因,w 必須建立在堆上。如果用裸指標管理 w,那麼需要在解構函式中 delete w;
這種寫法雖然沒什麼問題,但是容易漏寫 delete 語句,造成記憶體洩漏。
如果按照 unique_ptr 的寫法,不用在解構函式手動 delete 屬性,當物件析構時,屬性 w 將會自動釋放記憶體。
1.3.2 異常安全
假如我們在一段程式碼中,需要建立一個物件,處理一些事情後返回,返回之前將物件銷燬,如下所示:
void process()
{
Widget *w = new Widget();
w->do_something(); // 可能會發生異常
delete w;
}
在正常流程下,我們會在函式末尾 delete 建立的物件 w,正常呼叫解構函式,釋放記憶體。
但是如果 w->do_something() 發生了異常,那麼 delete w
將不會被執行。此時就會發生 記憶體洩漏 。
我們當然可以使用 try…catch 捕捉異常,在 catch 裡面執行 delete,但是這樣程式碼上並不美觀,也容易漏寫。
如果我們用 std::unique_ptr,那麼這個問題就迎刃而解了。無論程式碼怎麼拋異常,在 unique_ptr 離開函式作用域的時候,記憶體就將會自動釋放。
2.shared_ptr:共享所有權
2.1 shared_ptr簡介
在使用 shared_ptr 之前應該考慮,是否真的需要使用 shared_ptr, 而非 unique_ptr。
shared_ptr 代表的是共享所有權,即多個 shared_ptr 可以共享同一塊記憶體。
因此,從語義上來看, shared_ptr 是支援複製的 。如下:
auto w = std::make_shared<Widget>();
{
auto w2 = w;
cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1
shared_ptr 內部是利用引用計數來實現記憶體的自動管理,每當複製一個 shared_ptr,引用計數會 + 1。當一個 shared_ptr 離開作用域時,引用計數會 - 1。當引用計數為 0 的時候,則 delete 記憶體。
同時, shared_ptr 也支援移動 。從語義上來看,移動指的是所有權的傳遞。如下:
auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此時w等於nullptr,w2.use_count()等於1
我們將 w 物件 move 給 w2,意味著 w 放棄了對記憶體的所有權和管理,此時 w 物件等於 nullptr。
而 w2 獲得了物件所有權,但因為此時 w 已不再持有物件,因此 w2 的引用計數為 1。
用法:
std::shared_ptr<型別> 變數名稱{};
std::shared_ptr<int> ptrA{};
std::shared_ptr<int> ptrB{std::make_shared<int>(5)};
注意:std::make_shared不支援陣列。
std::shared_ptr<int[]> ptrC{ new int[5]{1, 2, 3, 4, 5} };
2.2 shared_ptr效能
1.記憶體佔用高
shared_ptr 的記憶體佔用是裸指標的兩倍。因為除了要管理一個裸指標外,還要維護一個引用計數。
因此相比於 unique_ptr, shared_ptr 的記憶體佔用更高
2.原子操作效能低
考慮到執行緒安全問題,引用計數的增減必須是原子操作。而原子操作一般情況下都比非原子操作慢。
3.使能移動最佳化效能
shared_ptr 在效能上固然是低於 unique_ptr。而通常情況,我們也可以儘量避免 shared_ptr 複製。
如果,一個 shared_ptr 需要將所有權共享給另外一個新的 shared_ptr,而我們確定在之後的程式碼中都不再使用這個 shared_ptr,那麼這是一個非常鮮明的移動語義。
對於此種場景,我們儘量使用 std::move,將 shared_ptr 轉移給新的物件。因為移動不用增加引用計數,效能比複製更好。
2.3 shared_ptr的使用場景
1.shared_ptr 通常使用在共享權不明的場景。有可能多個物件同時管理同一個記憶體時。
2.物件的延遲銷燬。陳碩在《Linux 多執行緒伺服器端程式設計》中提到,當一個物件的析構非常耗時,甚至影響到了關鍵執行緒的速度。可以使用 BlockingQueue<std::shared_ptr<void>>
將物件轉移到另外一個執行緒中釋放,從而解放關鍵執行緒。
3.選擇哪種指標作為函式的引數?
很多時候,函式的引數是個指標。這個時候就會面臨選擇困難症,這個引數應該怎麼傳,應該是 shared_ptr,還是 const shared_ptr&,還是直接 raw pointer 更合適。
1.只在函式使用指標,但並不儲存物件內容
假如我們只需要在函式中,用這個物件處理一些事情,但不打算涉及其生命週期的管理,也不打算透過函式傳參延長 shared_ptr 的生命週期。
對於這種情況,可以使用 raw pointer 或者 const shared_ptr&。
void func(Widget *);
void func(const std::shared_ptr<Widget>&);
實際上第一種裸指標的方式可能更好,從語義上更加清楚,函式也不用關心智慧指標的型別。
2.在函式中儲存智慧指標
假如我們需要在函式中把這個智慧指標儲存起來,這個時候建議直接傳值。
void func(std::shared_ptr<Widget> ptr);
這樣的話,外部傳過來值的時候,可以選擇 move 或者賦值。函式內部直接把這個物件透過 move 的方式儲存起來。
這樣效能更好,而且外部呼叫也有多種選擇。
參考文章:
C++ 智慧指標的正確使用方式:unique_ptr VS shared_ptr_unique shared區別-CSDN部落格