軌道
/
C++
C++
/
課程大綱
/
智慧指標
智慧

智慧指標 在 C++

1 個練習

關於 智慧指標

智慧指標是 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()提供幾項優點:

  1. 例外安全:std::make_unique()保證例外安全。 如果在物件建構期間拋出例外,記憶體會自動被釋放,避免記憶體洩漏。
  2. 清晰度:使用std::make_unique()能讓程式碼更清楚、更簡潔。 它不需要明確指定要配置的型別,因為模板引數會自動推導。
  3. 最佳化機會:比起用new手動配置記憶體,編譯器更有機會有效最佳化std::make_unique(),可能帶來更好的效能。
  4. 避免誤用:手動建構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;
Caution

在 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避免懸空指標的方法:

  1. 建立std::weak_ptr:不要直接持有std::shared_ptr,而是對共享物件建立一個std::weak_ptr。

  2. 檢查有效性:使用std::weak_ptr之前,先用expired()函式檢查它是否有效。 如果關聯的共享物件已被刪除,這個函式會回傳true。

  3. 鎖定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專門用來打破循環相依或觀察共享物件,但並不常用。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 智慧指標