iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
3
Software Development

山姆大叔談 C++:從歷史談起,再給個定義—Modern C++ 解惑系列 第 4

DAY 3:只能死一次,不能鞭屍,談 std::unique_ptr<T>,卷一

眾所周知,C++ 的特色之一是難搞的「指標(Pointer)」。指標常被用做「記憶體管理」,也被用來實作「繼承體系」,許多 C++ 的設計手法必須透過操作指標方可完成。

許多初學者入門時,因沒摸清指標的抽象性,被搞得昏頭轉向,對 C++ 恨得牙癢癢。為減輕痛苦,降低撰寫高效且穩定程式的門檻,C++11 引入了「Smart Pointer」。

早在 C++11 推出前,Boost 便實作了 Smart Pointer,該些組件也是 C++11 標準規格的基石。

C++11 的 Smart Pointer 有三個:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

本篇只談 std::unique_ptr

麻煩的事,交給輔具

其實早在 C++11 推出前,C++ 就有 std::auto_ptr 可用。但是,該組件有潛在的問題,因此不建議使用。至於什麼問題,擇日再說。現下,只要記住不要再用 std::auto_ptr,若有需要,一律改用 std::unique_ptr 或之後會介紹的 std::shared_ptr

本篇示範如何利用 Smart Pointer 來解決前一篇提到幾個常見的記憶體管理問題。

第一個常見的問題是「記憶體洩漏(Memory Leak)」,即使細心如山姆大叔(?)也會犯這種錯。時常大白天「鬼遮眼」,明明臭蟲就在眼前,卻怎麼也看不見。

聰明人要懂得利用工具,物件的生與死交給適當的工具處理,減少低級錯誤出現的機率。std::unique_ptr<T> 就是一個用來管理物件以及記憶體的好用工具。

C++11 以前,我們通常這麼使用物件。(TeaShopOwner 為使用者自定型別,其實作細節與本文無關。就像賣國商人如何致富,不是評價他們的重點):

bool WorthBuying()
{
    TeaShopOwner* the_owner = new TeaShopOwner();
    
    if (the_owner->SupportCCP()) {
        delete the_owner;
        return false;
    }
    
    if (!the_owner->AdmitTaiwanAsACountry()) {
        delete the_owner;
        return false;
    }
    
    delete the_owner;
    return true;
}

利用 new 建出 TeaShopOwner 物件,用完後,呼叫 delete 刪除物件。上述程式碼中,惱人的地方是有人為了賣茶出賣靈魂在每個函數回返的地方都要記得刪除物件,不僅容易出錯,而且不美觀。有了 C++11⁄14 ,可以使用 std::unique_ptr 改寫:

bool WorthBuying()
{
    std::unique_ptr<TeaShopOwner> the_owner = std::make_unique<TeaShopOwner>();
    if (the_owner->SupportCCP())
        return false;
    if (!the_owner->AdmitTaiwanAsACountry())
        return false;
    return true;
}

上述程式碼還有優化空間,但為了說明,請讀者先別計較。

改寫後的版本利用 std::unique_ptr 管理物件生命週期,一旦離開 scope(此例為 WorthBuying 函數),則自動刪除所管理的物件。這樣的行為,即使函數內部發生 Exception(例外?異常?),也能保證該物件被正確刪除,並釋放佔用的記憶體。

因為 std::unique_ptr 提供上述「保證」,而且能讓程式碼更精簡,因此,「Modern C++」的精神之一便是:

不再使用 RAW Pointer,不再使用 new/delete,全面改用 Smart Pointer 及輔具來管理物件。(不知道怎麼挑,就用 std::unique_ptr 吧)

請注意,改寫後的版本使用了 std::make_unique 這個在 C++14 才出現的輔具,如果編譯器尚未支援 C++14,會出現編譯錯誤。

明白表示意圖

「設計模式(Design Pattern)」是好東西,負責任的工程師都應該學一學。有個範式叫「Factory Function/Method」,即利用專門的函數來生成物件,該函數的用法如下:

TeaShopOwnder* CreateOwner();

{
    TeaShopOwner* the_owner = CreateOwner();
    // Do something with the_owner
    delete the_owner;
}

上述程式碼會衍生至少兩個疑問:

  1. CreateOwner 回傳的物件應由呼叫端刪除嗎?
  2. 使用 delete 能正確刪除 CreateOwner 回傳的物件嗎?

除非 CreateOwner 有附註解或查看其實作,否則沒人有把握回答上述兩個疑問。來看看用 std::unique_ptr 重新設計後的版本:

std::unique_ptr<TeaShopOwner> CreateOwner();

{
    std::unique_ptr<TeaShopOwner> the_owner = CreateOwner();
    // Do something with the_owner
}

改寫後,除了更為精簡外,對呼叫端來說,兩個疑問的答案是:

  1. 不用管
  2. 不用管

要不要刪物件,要怎麼刪物件都「不用管」,而且很安全,有沒有給他很開心!為什麼 C++ 人這麼愛「Modern C++」,這就是了。

我誤判情勢,以為一篇文章就能搞定 std::unique_ptr。下一篇,我們繼續談談 std::unique_ptr 的其他面向。

延伸閱讀


上一篇
DAY 2:指標是功能還是臭蟲?兼談 Smart Pointer(拜託不要翻成「聰明指標」)的必要性
下一篇
DAY 4:只能死一次,不能鞭屍,談 std::unique_ptr<T>,卷二
系列文
山姆大叔談 C++:從歷史談起,再給個定義—Modern C++ 解惑26
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言