前往
大廳
主題

C++雜記-std::unique_ptr介紹

最後的疼愛是腿張開 | 2023-03-30 19:02:32 | 巴幣 4232 | 人氣 1280

先附上Hackmd好讀版

0. 閱讀前須了解的知識

I. 知道C/C++的指標且對於指標的操作熟悉
II. 對於C++的class有基礎的認知

1. 前言

其實這篇應該前幾個月就可以生出來了
但最近一直沉迷於交界地
來談談最近蠻常使用的工具
這是C++11發表的新語法
相信C/C++的開發者多少聽過Memory Leakage
以前學校老師總告訴我們
malloc / free, new / delete記得要成對出現
(雖然我以前也都沒這樣做…)
但不管怎樣
Memory Leakage對於程式有著非常大的危害
嚴重的話甚至會引起程式崩潰
但對於以物件導向開發的程式來說
指標可能會在各個function中傳遞
所以在撰寫時
你也可能不確定應該在什麼時機做釋放
更甚者你今天傳遞的是個singleton
你也不能亂釋放
因此C++委員會的成員
利用C++的物件在離開其作用域會自動析構的性質
std::unique_ptr就作為一個封裝raw pointer的物件
在離開作用域後
std::unique_ptr就會自動析構裡面託管的raw pointer
如此一來便不用再去維護何時應該釋放指標
達成類似GC(Garbage Collection垃圾回收)的機制
且因為不像正統的GC
僅藉著封裝raw pointer的物件
所耗的效能幾乎可以忽略不計
另外 它藉由重載*以及->兩個運算子
std::unique_ptr也可以做到跟原生指標一樣的操作

2. 操作介紹

1. 建構
std::unique_ptr這些smart pointer就是來取代舊有的pointer的
也因此之後若使用指標
盡量不要去使用new來創建
而是使用make_unique
至於原因的話
我應該會留到把Effective Modern C++這本書的筆記做完再寫
使用std::make_unique / std::make_shared取代new
是其中的Item 21
有興趣的人可以去拜讀一下
I. 一般指標
假如要創建一個字元指標且指標指向’a’
使用new的寫法:
使用make_unique的寫法:
II. 動態一維陣列
假如要創建長度10的整數動態陣列
使用new的寫法:
使用make_unique的寫法:
這邊可以稍微來看一下這兩種宣告有什麼不同
第一種是一般的指標
宣告的樣板是
std::unique_ptr<T>
這種宣告它內部預設的Deleter(後面會說這是甚麼)
在unique_ptr析構要釋放託管的raw_ptr時
就是單純的執行delete raw_ptr
而一維的動態陣列宣告的樣板如下
std::unique_ptr<T[]>
這種宣告它內部預設的Deleter
在unique_ptr析構時要釋放託管的raw_ptr
則是執行delete [] raw_ptr
這也就對應了如果使用new去申請連續配置的記憶體
釋放時需寫delete [] ptr
且如果是使用std::unique_ptr<T[]>這種樣板的話
他內部還有重載[]這個運算子
所以可以像一般陣列一樣去操作這個指標
III. 動態二維陣列
假設想創建row=2, col=3的二維整數陣列
使用new的寫法:
使用make_unique的寫法:
這樣make_unique造出來的二維陣列也可以用一般的二維陣列操作
IV. 自定義的class
假設我有一個Image的class如下
假設想創建一張w = 100, h = 200的影像
使用new的寫法:
使用make_unique的寫法:

2. get()
有時候你可能會想拿到unique_ptr託管的raw_ptr
這時候可以使用get()
它會回傳託管的指標
3. reset()
這個是在你想改變託管的指標
他會先釋放先前託管的指標
再接管新的指標
比如說一開始是長度10的整數陣列
改為長度100的整數陣列
如果reset()中不傳遞新的指標
則只會釋放託管的raw_ptr
4. release()
這個不代表釋放unique_ptr託管指標的意思
這是告訴unique_ptr不用負責管理託管指標的釋放了
之後指標的釋放由開發者自己決定
5. swap()
交換兩個unique_ptr

3. 自定義的Deleter

這個是前面有提到的關鍵字
其作用是在unique_ptr析構時會去調用Deleter刪除raw_ptr
預設的Deleter分別是delete / delete []
但有時候你不一定都是這樣釋放
當然也是可以把東西都包成class
在class內的析構函數裡寫如何釋放
就如同上面Image的例子那樣
如果你只是想讀個檔案
寫個class也太麻煩了
不如寫個釋放的函數來的方便
直接上代碼
這邊會發現我怎麼沒用make_unique
而是使用像一般class的初始化
目前來說
如果想寫自定義的Deleter只能這樣初始化
至於Deleter我這邊是使用lambda
也是可以用std::function(void(FILE*))封裝
但std::function會使unique_ptr占用太多內存
所以用lambda比較好

4. 其他注意事項

unique_ptr看它的命名就大概知道
它保證同一時間raw_ptr只有一個unique_ptr託管
所以不允許=的操作
如果你還是很想assign給另一個unique_ptr的話
就得使用std::move()
function中要傳遞unique_ptr的話
必須使用call by reference
因為call by value會使物件產生複製
所以請不要寫出這種東西
這會造成double free的問題
最好能在建構式或.reset()中寫new
像這樣
當然 比較好的做法是使用make_unique

5. 後記

最後來個小問題
幫大家統整一下unique_ptr的用法
假設一開始我想創建row = 2, col = 3的動態二維陣列
後面想改變成row = 5, col = 10該怎麼寫?
代碼如下
這邊要用release()而不是get()
原因在於如果使用get()
得到的是unique_ptr託管的raw pointer
將這個raw pointer再交由另一個unique_ptr託管
這會發生像上面提到的問題
會造成double free的問題
因此使用release()會讓原先的unique_ptr讓出他的託管權
此時移交給另一個unique_ptr就不會有問題了
希望大家都能好好利用這個方便的工具
在撰寫時能大大減輕對程式碼的管理
完整的範例我放在github上了
有興趣的可以去查看一下

Reference

創作回應

Haatonもち
老大知識好淵博...追不上...
2023-03-31 02:31:44
最後的疼愛是腿張開
您才是...
2023-03-31 08:14:15
Caster
剩我不會C++了……
2023-03-31 03:21:09
最後的疼愛是腿張開
我也不會西加加
2023-03-31 08:14:35
邊緣人
看不懂,但知道老大很厲害,佬......
2023-03-31 09:41:35
最後的疼愛是腿張開
沒有啦 我也是研究了一陣子
2023-03-31 20:11:18
のんびり猫
老大好強好帥...跪了麻糬...
2023-03-31 12:44:38
最後的疼愛是腿張開
Ok 再ban一個
2023-03-31 20:11:30
麻糬農夫
老大…
2023-03-31 15:10:56
最後的疼愛是腿張開
老師好
2023-03-31 20:11:40

更多創作