從異常安全說起
使用 raw pointer 管理動態記憶體時,經常會遇到這樣的問題:
- 忘記
delete
記憶體,造成記憶體洩露。 - 出現異常時,不會執行
delete
,造成記憶體洩露。
下面的程式碼解釋了,當一個操作發生異常時,會導致delete
不會被執行:
1 2 3 4 5 6 7 8 9 | void func() { auto ptr = new Widget; // 執行一個會丟擲異常的操作 func_throw_exception(); delete ptr; } |
在 C++98 中我們需要用一種笨拙的方式,寫出異常安全的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void func() { auto ptr = new Widget; try { func_throw_exception(); } catch(...) { delete ptr; throw; } delete ptr; } |
使用智慧指標能輕易寫出異常安全的程式碼,因為當物件退出作用域時,智慧指標將自動呼叫物件的解構函式,避免記憶體洩露:
1 2 3 4 5 6 | void func() { std::unique_ptr<Widget> ptr{ new Widget }; func_throw_exception(); } |
unique_ptr 的原理
讓我們瞭解一下unique_ptr
的實現細節:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | namespace std { template <typename T, typename D = default_delete<T>> class unique_ptr { public: explicit unique_ptr(pointer p) noexcept; ~unique_ptr() noexcept; T& operator*() const; T* operator->() const noexcept; unique_ptr(const unique_ptr &) = delete; unique_ptr& operator=(const unique_ptr &) = delete; unique_ptr(unique_ptr &&) noexcept; unique_ptr& operator=(unique_ptr &&) noexcept; // ... private: pointer __ptr; }; } |
從上面的程式碼中,我們可以瞭解到:
unique_ptr
內部儲存一個 raw pointer,當unique_ptr
析構時,它的解構函式將會負責析構它持有的物件。unique_ptr
提供了operator*()
和operator->()
成員函式,像 raw pointer 一樣,我們可以使用*
解引用unique_ptr
,使用->
來訪問unique_ptr
所持有物件的成員。unique_ptr
並不提供 copy 操作,這是為了防止多個unique_ptr
指向同一物件。- 但
unique_ptr
提供了 move 操作,因此我們可以用std::move()
來轉移unique_ptr
。
很顯然,預設情況下,unique_ptr
會使用delete
析構物件,不過我們可以使用自定義的 deleter。
1 2 3 4 5 6 7 | struct Widget{ }; // ... auto deleter = []( Widget *p ) { cout << "delete Widget!" << endl; delete p; }; unique_ptr<Widget, decltype(deleter)> ptr{ new Widget, deleter }; |
當然,我們可以使用 C++11 的 alias template 特性,這樣就可以避免指定 deleter 的型別:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct Widget{ }; template <typename T> using uniquePtr = unique_ptr<T, void(*)(T*)>; void func() { uniquePtr<Widget> ptr( new Widget, []( Widget *p ) { cout << "delete Widget!" << endl; delete p; }); } |
unique_ptr
為陣列提供了模板偏特化,因此unique_ptr
也可以指向陣列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | namespace std { template <typename T, typename D> class unique_ptr<T[], D> { public: // ... T& operator[]( size_t i ) const; }; template <typename T> class default_delete<T[]> { public: // ... void operator()( T *p ) const; // call delete[] p }; } |
當unique_ptr
指向陣列時,可以使用[]
來訪問陣列元素。default_delete
也為陣列提供模板偏特化,因此當unique_ptr
被銷燬時,會呼叫delete []
釋放陣列記憶體。
1 2 3 | unique_ptr<string[]> ptr{ new string[100] }; ptr[0] = "hello"; ptr[1] = "world"; |
一些陷阱
unique_ptr
是用來獨佔地持有物件的,所以通過同一原生指標來初始化多個unique_ptr
,下面是一種錯誤的使用方式:
1 2 3 4 | struct Widget{ }; Widget *ptr = new Widget; unique_ptr<Widget> p1{ ptr }; unique_ptr<Widget> p2{ ptr }; // ERROR: multiple ownership |
當p1
和p2
各自被銷燬的時候,它們指向的Widget
將被delete
兩次。
再談異常安全
C++14 提供了std::make_unique<T>()
函式用來直接建立unique_ptr
,但 C++11 並沒有提供,不過其實現並不複雜:
1 2 3 4 5 6 | template <typename T, typename... Ts> std::unique_ptr<T> make_unique( Ts&&... params ) { return std::unique_ptr<T>( new T( std::forward<Ts>(params)... ) ); } // ... auto ptr = make_unique<std::string>("senlin"); |
思考一下使用make_unique
的好處?
使用unique_ptr
並不能絕對地保證異常安全,你可能很驚訝於這個結論。讓我們看看一個例子:
1 | func(unique_ptr<T>{ new T }, func_throw_exception()); |
C++ 標準並沒有規定編譯器對函式引數的求值次序,所以有可能出現這樣的次序:
- 呼叫
new T
分配動態記憶體。 - 呼叫
func_throw_exception()
函式。 - 呼叫
unique_ptr
的建構函式。
呼叫func_throw_exception()
函式會丟擲異常,所以無法構造unique_ptr
,導致new T
所分配的記憶體不能回收,造成了記憶體洩露。解決這個問題,需要使用make_unique
函式:
1 | func(make_unique<T>(), func_throw_exception()); |
這種情況下,成功解決了記憶體洩露的問題。
make_unique
在初始化物件的時候使用的()
而不是{}
,所以下面的程式碼顯然是初始化10
個元素:
1 2 3 | auto up = make_unique<vector<int>>( 10, 100 ); cout << "size: " << up->size() << endl; // size: 10 |
但是如果使用std::initializer_list
來初始化物件時,要怎樣做呢?嗯嗯,看看下面的程式碼:
1 2 3 4 | auto initList = { 1, 2, 3, 4, 5 }; auto up = make_unique<vector<int>>( initList ); cout << "size: " << up->size() << endl; // size: 5 |