【C++】智慧指標的正確使用方式

Emma1111發表於2024-11-01

本文將從這幾方面講解智慧指標:

  • 智慧指標的應用場景分析
  • 智慧指標的效能分析: 為什麼 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部落格

相關文章