本文將介紹智能指針用法的一些平時可能沒注意的細節(關於智能指針的基本用法可以參考前面的博文)。
1.unique_ptr和shared_ptr在構造上的一點差異
unique_ptr支持動態數組,而shared_ptr不能直接支持動態數組。std::unique_ptr<int []> ptr(new int[10]);合法,而std::shared_ptr<int []> ptr(new int[10]);是不合法的。
如果通過std::shared_ptr來構造動態數組,則需要顯式指定刪除器,比如下面的代碼:
std::shared_ptr<int> p(new int[10], [](int* p){delete[] p;}); //指定delete[]
也可以用std::default_delete作為刪除器:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>);
我們可以封裝一個make_shared_array方法讓shared_ptr支持數組:
template<typename T> shared_ptr<T> make_shared_array(size_t size) { return shared_ptr<T>(new T[size], default_delete<T[]>()); }
測試代碼:
std::shared_ptr<int> p = make_shared_array<int>(10); std::shared_ptr<char> p = make_shared_array<char>(10);
unique_ptr缺少一個類似於make_shared的make_unique方法,不過在c++14中會增加make_unique方法。其實要實現一個make_unique方法是比較簡單的:
//支持普通指針 template<class T, class... Args> inline typename enable_if<!is_array<T>::value, unique_ptr<T> >::type make_unique(Args&&... args) { return unique_ptr<T>(new T(std::forward<Args>(args)...)); } //支持動態數組 template<class T> inline typename enable_if<is_array<T>::value && extent<T>::value==0, unique_ptr<T> >::type make_unique(size_t size) { typedef typename remove_extent<T>::type U; return unique_ptr<T>(new U[size]()); } //過濾掉定長數組的情況 template<class T, class... Args> typename enable_if<extent<T>::value != 0, void>::type make_unique(Args&&...) = delete;
實現思路很簡單,如果不是數組則直接創建unique_ptr,如果是數組的話,先判斷是否為定長數組,如果為定長數組則編譯不通過;非定長數組時,獲取數組中的元素類型,再根據入參size創建動態數組的unique_ptr。extent<T>::value用來獲取數組的長度,如果獲取值為0,則不到說明不是定長數組。
2.shared_ptr和unique_ptr指定刪除器方式的一點差異
unique_ptr指定刪除器和std:: shared_ptr是有差別的,比如下面的寫法:
std:: shared_ptr<int> ptr(new int(1), [](int*p){delete p;}); //正確
std::unique_ptr<int> ptr(new int(1), [](int*p){delete p;}); //錯誤
std::unique_ptr指定刪除器的時候需要確定刪除器的類型,所以不能直接像shared_ptr指定刪除器,可以這樣寫:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [](int*p){delete p;});
上面這種寫法在lambda沒有捕獲變量的情況下是正確的,如果捕獲了變量則會編譯報錯:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [&](int*p){delete p;}); //錯誤,因為捕獲了變量
為什么lambda捕獲了變量作為unique_ptr就會報錯呢,因為lambda在沒有捕獲變量的情況下是可以直接轉換為函數指針的,捕獲了就不能轉換為函數指針。
如果希望unique_ptr的刪除器支持lambda,可以這樣寫:
std::unique_ptr<int, std::function<void(int*)>> ptr(new int(1), [&](int*p){delete p;});
我們還可以自定義unique_ptr的刪除器,比如下面的代碼:
#include <memory> #include <functional> using namespace std; struct MyDeleter { void operator()(int*p) { cout<<"delete"<<endl; delete p; } }; int main() { std::unique_ptr<int, MyDeleter> p(new int(1)); return 0; }
3.通過智能指針管理第三方庫分配的內存
智能指針可以很方便的管理當前程序庫動態分配的內存,還可以用來管理第三方庫分配的內存。第三方庫分配的內存一般需要通過第三方庫提供的釋放接口才能釋放,由於第三方庫返回出來的指針一般都是原始指針,如果用完之后沒有調用第三方庫的釋放接口,就很容易造成內存泄露。比如下面的代碼:
void* p = GetHandle()->Create(); //do something… GetHandle()->Release(p);
這段代碼實際上是不安全的,在使用第三方庫分配的內存過程中,可能忘記調用Release接口,還有可能中間不小心返回了,還有可能中間發生了異常,導致無法調用Release接口,這時用智能指針去管理第三方庫的內存就很合適了,只要出了作用域內存就會自動釋放,不用顯式去調用釋放接口了,不用擔心中途返回或者發生異常導致無法調用釋放接口的問題。
void* p = GetHandle()->Create(); std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);});
上面這段代碼就可以保證任何時候都能正確釋放第三方庫分配的內存。雖然能解決問題,但還是有些繁瑣,因為每個第三方庫分配內存的地方都要調用這段代碼,比較繁瑣,我們可以將這段代碼提煉出來作為一個公共函數,簡化調用。
std::shared_ptr<void> Guard(void* p) { return std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);}); } void* p = GetHandle()->Create(); auto sp = Guard(p); //do something…
上面的代碼通過Guard函數做了簡化,用起來比較方便,但仍然不夠安全,因為有可能使用者可能會這樣寫:
void* p = GetHandle()->Create(); Guard(p); //危險,這句結束之后p就被釋放了 //do something…
這樣寫是有問題的,會導致訪問野指針,因為Guard(p);是一個右值,如果不賦值給一個指針的話,Guard(p);這句結束之后,就會釋放,導致p提前釋放了,后面就會訪問野指針的內容。auto sp = Guard(p);需要一個賦值操作,忘記賦值就會導致指針提前釋放,這種寫法仍然不夠安全。我們可以定義一個宏來解決這個問題:
#define GUARD(P) std::shared_ptr<void> p##p(p, [](void*p){ GetHandle()->Release(p);}); void* p = GetHandle()->Create(); GUARD(p); //安全
也可以用unique_ptr來管理第三方的內存:
#define GUARD(P) std::unique_ptr<void, void(*)(int*)> p##p(p, [](void*p){ GetHandle()->Release(p);});
通過宏定義方式我們可以避免智能指針忘記賦值,即方便又安全。
c++11 boost技術交流群:296561497,歡迎大家來交流技術。
