智慧指標是 C++ 的一項現代功能,旨在提供自動記憶體管理,協助避免原始指標常見的記憶體洩漏與懸空指標問題。 它們就像原始指標的包裝器,額外提供當指標不再需要時自動釋放記憶體之類的功能。
智慧指標在 C++ 標準函式庫中通常以類別模板的形式實作。
最常用的兩種智慧指標是std::unique_ptr和std::shared_ptr。
std::unique_ptr是一種智慧指標,獨佔擁有該物件。
它確保在任何時刻,只有一個std::unique_ptr物件擁有該資源。
當持有資源的std::unique_ptr被銷毀或重設時,它會自動解構物件並釋放記憶體。
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique()的優點建立std::unique_ptr時,建議使用std::make_unique(),而不是直接用new配置記憶體。
std::make_unique()提供幾項優點:
std::make_unique()保證例外安全。
如果在物件建構期間拋出例外,記憶體會自動被釋放,避免記憶體洩漏。std::make_unique()能讓程式碼更清楚、更簡潔。
它不需要明確指定要配置的型別,因為模板引數會自動推導。new手動配置記憶體,編譯器更有機會有效最佳化std::make_unique(),可能帶來更好的效能。std::unique_ptr時,可以刪除其底層資源。
這會在std::unique_ptr於作用域結束時嘗試刪除該資源時,導致未定義行為。std::shared_ptr是一種智慧指標,允許有多個std::shared_ptr物件共享同一個資源的所有權。
它會追蹤有多少個共享指標參考該資源,只有在最後一個擁有該資源的共享指標離開作用域或被重設時,才會釋放記憶體。
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
在 C++17 及更早的版本中,不直接支援透過std::make_shared<T[]>將std::shared_ptr用於陣列。
雖然可以用std::make_shared<T[]>配置陣列,但直接從它們建立共享指標可能導致未定義行為,因為單一物件與陣列的記憶體管理方式不同。
建議改用std::vector或自訂刪除函式,來有效管理搭配共享指標的陣列。
在 C++17 中處理陣列配置與共享指標時,請務必確認與你的編譯器及標準函式庫實作相容。
std::make_shared()的優點與std::make_unique()類似,std::make_shared()帶來更高的記憶體效率、例外安全與可讀性等好處。
它把控制區塊與受管理物件的記憶體配置合併成單一操作,提升效率並降低記憶體洩漏的風險。
此外,模板引數的自動推導讓程式碼更簡單、可讀性更好。
在 C++ 中處理std::shared_ptr物件時,使用std::make_shared()能帶來更乾淨、更安全且更有效率的程式碼。
std::weak_ptr是std::shared_ptr的夥伴類別,對共享指標所管理的物件提供不具所有權的「弱」參考。
它讓你能存取資源,卻不影響資源的生命週期。
在需要打破循環參考以避免記憶體洩漏的情境中,弱指標很實用。
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr與循環所有權std::weak_ptr是為了處理循環所有權(也稱為循環參考)的問題而設計,這種情況在使用std::shared_ptr時可能發生。
在循環所有權的情境中,兩個或多個std::shared_ptr物件彼此參考,形成一個循環,使任何物件都無法被刪除,因為它們彼此持有強參考,最終導致記憶體洩漏。
std::weak_ptr提供了解決這個問題的方法:它允許對共享物件建立弱參考,而不增加其參考計數。
這表示它可以觀察並存取共享物件,卻不會阻止該物件被刪除。
如果共享物件的所有強參考都被釋放,物件就會被銷毀,而觀察它的弱指標會自動重設為nullptr。
以下是一個簡短的範例,示範如何使用std::weak_ptr打破循環所有權:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
"Node constructed"會印出兩次,接著是兩行 "Node destructed"。
當next不是弱指標而是std::shared_ptr時,就會形成循環所有權的情況,導致記憶體洩漏,因為兩個節點會互相持有強參考,使其無法被銷毀。
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
在這個情境中,node1和node2都透過std::shared_ptr彼此持有強參考,形成一個循環。
當程式結束並嘗試用reset()釋放強參考時,兩個節點的參考計數都不為零,因為每個節點都持有對方的強參考。
因此,這些節點不會被解構,導致記憶體洩漏。
"Node constructed"會印出兩次,但後面不會有任何一行 "Node destructed"。
std::weak_ptr
當指標參考的物件已被刪除時,就會產生懸空指標,對該指標解參考時會導致未定義行為。
std::weak_ptr透過對std::shared_ptr所管理的物件提供不具所有權的弱參考,協助避免懸空指標。
以下是使用std::weak_ptr避免懸空指標的方法:
建立std::weak_ptr:不要直接持有std::shared_ptr,而是對共享物件建立一個std::weak_ptr。
檢查有效性:使用std::weak_ptr之前,先用expired()函式檢查它是否有效。
如果關聯的共享物件已被刪除,這個函式會回傳true。
鎖定std::weak_ptr:若要安全存取共享物件,請使用lock()函式;如果物件仍然有效,它會回傳指向同一個物件的std::shared_ptr。
如果物件已被刪除,lock()會回傳一個空的std::shared_ptr。
以下是一個程式碼範例,示範如何使用std::weak_ptr避免懸空指標:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
在這個範例中,node1->next和node2->next都是std::weak_ptr物件。
存取共享物件之前,會先用expired()函式檢查有效性。
接著用lock()安全地存取共享物件。
這種做法能確保使用std::weak_ptr時不會產生懸空指標。
預設使用智慧指標:獨佔所有權用std::unique_ptr,共享所有權用std::shared_ptr。
原始指標則保留給不具所有權的參考,或與舊有程式碼介接時使用。
大多數情況下,std::unique_ptr就足以處理獨佔所有權,因為它提供輕量的記憶體管理,沒有參考計數的額外負擔。
std::shared_ptr應謹慎使用,因為除非真的需要共享所有權,否則它會帶來額外負擔與複雜度。
std::weak_ptr專門用來打破循環相依或觀察共享物件,但並不常用。