女性走夜路不安全?英國奇葩新招:無人機護航,關鍵時刻用光嚇退張三
什麼是智慧指標?
智慧指標是儲存指向動態分配(位於堆)物件指標的類,用於生存期控制,能確保在離開指標所在作用域時,自動正確地銷燬動態分配的物件,以防止記憶體洩漏。
智慧指標通常通過引用計數技術實現:每使用一次,內部引用計數+1;每析構一次,內部引用計數-1,當減為0時,刪除所指堆記憶體。
C++11提供3種智慧指標:std::shared_ptr, std::unique_ptr, std::weak_ptr
標頭檔案:
下面圍繞這這種智慧指標進行探討。
shared_ptr
shared_ptr是共享的智慧指標,使用引用計數,允許多個shared_ptr指標指向同一個物件。只有在最後一個shared_ptr析構時,引用計數為0,記憶體才會被釋放。
shared_ptr基本用法
1. 初始化
可以通過 建構函式、make_shared
// 智慧指標的初始化方式 shared_ptr<int> p(new int(1)); // 傳參構造 shared_ptr<int> p2 = p; // copy構造 shared_ptr<int> p3; // 建立空的shared_ptr,不指向任何記憶體 p3.reset(new int(2)); // reset方法 替換p3管理物件指標為傳入指標引數 auto p4 = make_shared<int>(3); // 利用make_shared輔助函式,建立shared_ptr if (p3) cout << "p3 is not null" << endl; else cout << "p3 is null" << endl;
應該優先使用make_shared建立智慧指標,因為更高效。
TIPS:
1)如果智慧指標中引用計數 > 0,reset將導致引用計數-1;
2)除了通過引用計數,還可以通過智慧指標的operator bool型別操作符,來判斷指標所指內容是否為空(未初始化);
錯誤做法:將一個原始指標直接賦值給一個智慧指標。
// 錯誤的智慧指標建立方法
shared_ptr<int> p = new int(1); // 編譯錯誤,不允許直將原始指標賦值給智慧指標
2. 獲取原始指標
通過shared_ptr的get方法來獲取原始指標。如:
shared_ptr<int> p(new int(1)); int* rawp = p.get(); cout << *rawp << endl; // 列印1
注意:get獲取原始指標並不會引起引用計數變化。
3. 指定刪除器
智慧指標的預設刪除器是operator delete,初始化的時候可以指定自定義刪除器。如:
// 自定義刪除器DeleteIntPtr
void DeleteIntPtr(int* p) {
delete p;
}
shared_ptr<int> p(new int, DeleteIntPtr); // 為shared_ptr指定自定義刪除器
刪除器何時呼叫?
當p的引用計數為0時,自動呼叫刪除器DeleteIntPtr釋放物件的記憶體。刪除器可以是函式,也可以是lambda表示式,甚至任意可呼叫物件。
如:
// 為shared_ptr指定刪除器示例
shared_ptr<int> p(new int, DeleteIntPtr); // 為指向int的shared_ptr指定刪除器,刪除器是自定義函式
shared_ptr<int> p1(new int, [](int* p) { delete p; }); // 刪除器是lambda表示式
function<void(int *)> f = DeleteIntPtr;
shared_ptr<int> p2(new int, f); // 刪除器是函式物件
shared_ptr<int> p3(new int[10], [](int* p) { delete[] p; }); // 為指向陣列的shared_ptr指定刪除器
shared_ptr<int> p4(new int[10], std::default_delete<int[]>()); // 刪除器是default_delete
shared_ptr
template<typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
// 使用make_shared_array,建立指向陣列的shared_ptr
shared_ptr<int> p = make_shared_array<int>(10);
shared_ptr<char> p1 = make_shared_array<char>(10);
使用shared_ptr的陷阱
- 不要將原始指標賦值給shared_ptr
shared_ptr<int> p = new int; // 編譯錯誤
- 不要將一個原始指標初始化多個shared_ptr
int* rawp = new int;
shared_ptr<int> p1(rawp);
shared_ptr<int> p2(rawp); // 邏輯錯誤,可能導致程式崩潰
- 不要在函式實參中建立shared_ptr
void func(shared_ptr<int> p, int a);
int g();
func(shared_ptr<int>(new int), g()); // 有缺陷
由於C++的函式引數計算順序在不同的編譯器(不同的預設呼叫慣例)下,可能不一樣,一般從右到左,也可能從左到右,因而可能的過程是先new int,然後呼叫g()。如果恰好g()傳送異常,而shared_ptr
正確寫法是先建立智慧指標,然後呼叫函式:
// 函式引數是shared_ptr時,正確寫法
shared_ptr<int> p(new int());
f(p, g());
- 通過shared_from_this()返回this指標。不要將this指標作為shared_ptr返回出來,因為this本質是一個裸指標。因此,直接傳this指標可能導致重複析構。
例如,
// 將this作為shared_ptr返回,從而導致重複析構的錯誤示例
struct A {
shared_ptr<A> GetSelf() {
return shared_ptr<A>(this); // 不要這樣做,可能導致重複析構
}
~A() {
cout << "~A()" << endl;
}
};
shared_ptr<A> p1(new A);
shared_ptr<A> p2 = p1->GetSelf(); // A的物件將被析構2次,從而導致程式崩潰
本例中,用同一個指標(this)構造了2個智慧指標p1, p2(兩者無任何關聯),離開作用域後,this會被構造的2個智慧指標各自析構1次,從而導致重複析構的錯誤。
正確返回this的shared_ptr做法:讓目標類通過派生std::enable_shared_from_this
struct A : public enable_shared_from_this<A> {
shared_ptr<A> GetSelf() {
return shared_from_this();
}
~A() {
cout << "~A()" << endl;
}
};
shared_ptr<A> p1(new A);
shared_ptr<A> p2 = p1->GetSelf(); // OK
cout << p2.use_count() << endl; // 列印2
- 避免迴圈引用。迴圈引用會導致記憶體洩漏。
一個典型的迴圈引用case:
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() { cout << "A is delete!" << endl; }
};
struct B {
std::shared_ptr<A> aptr;
~B() { cout << "B is delete!" << endl; }
};
void test() {
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
// A和B物件應該都被刪除,然而實際情況是都不會被刪除:沒有呼叫解構函式
}
解決迴圈引用的有效方法是使用weak_ptr。例子中,可以將A和B中任意一個成員變數,由shared_ptr修改為weak_ptr。
unique_ptr
unique_ptr基本用法
獨佔型智慧指標,不允許與其他智慧指標共享內部指標,不允許將一個unique_ptr賦值給另外一個unique_ptr。
錯誤用法:
// 將一個unique_ptr賦值給另外一個unique_ptr是錯誤的
unique_ptr<int> p(new int);
unique_ptr<int> p1 = p; // 錯誤,unique_ptr不允許複製
正確用法:可以移動(std::move)。移動後,原來的unique_ptr不再擁有原來指標的所有權了,所有權移動給了新unique_ptr。
unique_ptr<int> p(new int);
unique_ptr<int> p1 = p; // 錯誤,unique_ptr不允許複製
unique_ptr<int> p2 = std::move(p); // OK
自定義make_unique建立unique_ptr
shared_ptr有輔助方法make_shared可以建立智慧指標,但C++11中沒有類似的make_unique(C++14才提供)。
自定義make_unique方法(需要在C++11環境下執行):
// 支援普通指標
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;
// 使用自定義make_unique建立unique_ptr
unique_ptr<int> p = make_unique<int>(10);
cout << *p << endl;
unique_ptr與shared_ptr的區別
如果希望同一時刻,只有一個智慧指標管理資源,就用unique_ptr;如果希望多個智慧指標管理同一個資源,就用shared_ptr。
除了獨佔性外,unique_ptr與shared_ptr的區別:
1)unique_ptr可以指向一個數組,shared_ptr不能
unique_ptr<int []> ptr(new int[10]); // OK:智慧指標ptr指向元素個數為10的int陣列
ptr[9] = 9; // 設定最後一個元素為9
shared_ptr<int []> ptr(new int[10]); // 錯誤:C++11中,shared_ptr不能通過讓模板引數為陣列,從而讓智慧指標直接指向陣列,因為預設刪除器是delete,而陣列需要delete[](C++17中已經可用支援)
2)指定刪除器時,不能像shared_ptr那樣直接傳入lambda表示式
shared_ptr<int> p(new int(1), [](int* p) { delete p; }); // OK
unique_ptr<int> p2(new int(1), [](int* p) { delete p; }); // 錯誤:為unique_ptr指定刪除器時,需要確定刪除器的型別
因為為unique_ptr指定刪除器時,不能像shared_ptr那樣,需要確定刪除器的型別。像這樣:
unique_ptr<int, void(*)(int *)> p2(new int(1), [](int* p) { delete p; }); // OK
如果lambda表示式捕獲了變數,這種寫法就是錯誤的:
unique_ptr<int, void(*)(int *)> p2(new int(1), [&](int* p) { delete p; }); // 錯誤:lambda無法轉換為函式指標
因為lambda沒有捕獲變數時,可以直接轉換為函式指標,而不會變數後,無法轉換。
如果希望unique_ptr刪除器支援已不會變數的lambda,可以將模板實參由函式型別修改為std::function型別(可呼叫物件):
unique_ptr<int, function<void(int *)>> p2(new int(1), [&](int* p) { delete p; });
自定義unique_ptr刪除器
#include <memory>
#include <iostream>
#include <functional>
using namespace std;
struct MyDeleter{
void operator()(int* p) {
cout << "delete" << endl;
delete p;
}
};
unique_ptr<int, MyDeleter> p(new int(1));
cout << *p << endl;
weak_ptr
弱引用智慧指標weak_ptr用來監視shared_ptr,不會引起引用計數變化,也不管理shared_ptr內部指標,主要為了監視shared_ptr生命週期。weak_ptr沒有過載操作符*和->,因為不共享指標,不能操作資源。
weak_ptr主要作用:
1)監視shared_ptr管理的資源是否存在;
2)用來返回this指標;
3)解決迴圈引用問題;
weak_ptr基本用法
1)通過use_count() 獲得當前觀測資源的引用計數。
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; // 列印1
2)通過expired() 判斷所觀測的資源釋放已經被釋放。
{
shared_ptr<int> sp(new int(10)); // sp所指向內容非空
weak_ptr<int> wp(sp);
if (wp.expired()) {
cout << "weak_ptr 無效,所監視的智慧指標已經被釋放" << endl;
}
else
cout << "weak_ptr 有效" << endl; // 輸出 "weak_ptr 有效"
}
{
shared_ptr<int> sp; // sp所指向的內容為空
weak_ptr<int> wp(sp);
if (wp.expired()) {
cout << "weak_ptr 無效,所監視的智慧指標已經被釋放" << endl; // 輸出 "weak_ptr 無效..."
}
else
cout << "weak_ptr 有效" << endl;
}
3)通過lock() 獲取所監視的shared_ptr。
weak_ptr<int> wp;
void f() {
shared_ptr<int> sp(new int(10));
wp = sp;
if (wp.expired()) {
cout << "weak_ptr 無效,所監視的智慧指標已經被釋放" << endl;
}
else {
cout << "weak_ptr 有效" << endl; // 列印 "weak_ptr 有效"
auto spt = wp.lock();
cout << *spt << endl; // 列印10
}
}
weak_ptr返回this指標
上文提到不能直接將this指標返回為shared_ptr,需要通過繼承enable_shared_from_this類,然後通過繼承的shared_from_this()訪問來返回智慧指標。這是因為enable_shared_from_this類中有個weak_ptr,用於監測this智慧指標,呼叫shared_from_this()方法時,會呼叫內部weak_ptr的lock()方法,將監測的shared_ptr返回。
之前提到的那個例子:
struct A : public std::enable_shared_from_this<A> {
std::shared_ptr<A> GetSelf() {
return shared_from_this();
}
~A() {
cout << "A is deleted" << endl;
}
};
shared_ptr<A> spy(new A);
shared_ptr<A> p = spy->GetSelf(); // OK. 如果A不用繼承自enable_shared_from_this類的方法,直接傳this指標給shared_ptr,會導致重複析構的問題
// 只會輸出一次 "A is deleted"
weak_ptr解決迴圈引用問題
前面提到shared_ptr存在的迴圈引用的經典問題:A類持有指向B物件的shared_ptr,B類持有指向A物件的shared_ptr,導致2個shared_ptr引用計數無法歸0,從而導致shared_ptr所指向物件無法正常釋放。
struct A;
struct B;
struct A {
shared_ptr<B> bptr;
~A() { cout << "A is deleted" << endl; }
};
struct B {
shared_ptr<A> bptr;
~B() { cout << "B is deleted" << endl; }
};
void test() {
shared_ptr<A> pa(new A);
shared_ptr<B> pb(new B);
pa->bptr = pb;
pb->aptr = pa;
} // 離開函式作用域後,A、B物件應該銷燬,但事實沒有被銷燬,從而導致記憶體洩漏
用weak_ptr解決迴圈引用問題,具體方法是將A或B類中,shared_ptr型別修改為weak_ptr。
struct A;
struct B;
struct A {
shared_ptr<B> bptr;
~A() { cout << "A is deleted" << endl; }
};
struct B {
weak_ptr<A> aptr; // 將B中指向A物件的智慧指標,由shared_ptr修改為weak_ptr
~B() { cout << "B is deleted" << endl; }
};
void test() {
shared_ptr<A> pa(new A);
shared_ptr<B> pb(new B);
pa->bptr = pb;
pb->aptr = pa;
} // OK
通過智慧指標管理第三方庫分配的記憶體
第三方庫提供的介面,通常是用的原始指標,而非智慧指標。那麼,我們在用完第三方庫之後,如何通過智慧指標管理第三方庫分配的記憶體呢?
我們先看第三方庫一般的用法:
// GetHandler()獲取第三方庫控制代碼
// Create, Release是第三方庫提供的資源建立、釋放介面
void* p = GetHandler()->Create();
// do something...
GetHandler()->Release();
實際上,上面這段程式碼是不安全的,原因在於:1)使用第三方庫分配時,可能忘記呼叫Release介面;2)發生了異常,或者提前返回,實際並沒有呼叫Release介面。從而導致資源無法正常釋放。
使用智慧指標管理第三方庫,不要顯式呼叫釋放介面,即使發生異常或者忘記呼叫,也能正常釋放資源。
如上面一般用法,可以改寫成用智慧指標的方式:
// OK
void* p = GetHandler()->Create();
shared_ptr<void> sp(p, [this](void* p) { GetHandle()->Release(p); };
上面程式碼可以保證任何時候,都能正確釋放第三方庫分配的記憶體。雖然能解決問題,但還很繁瑣,因為每個第三方庫分配記憶體的地方,就要呼叫這段程式碼。可將這段程式碼提煉出來作為一個公共函式,以簡化呼叫:
// 存在安全隱患
// 將建立智慧指標,用於管理第三方庫的程式碼段封裝到一個函式
shared_ptr<void> Guard(void* p) {
return shared_ptr<void> sp(p, [this](void* p) { GetHandler()->Release(p); });
}
void* p = GetHandler()->Create();
auto sp = Guard(p);
// do something with sp...
上面這段程式碼存在安全隱患:客戶可能並不會利用Guard返回的臨時shared_ptr,構造一個新的shared_ptr,這樣p所建立的資源會立即釋放。
// 安全隱患演示
void* p = GetHandler()->Create();
Guard(p); // 該句結束後,p就會被釋放
// do something with p...
這種呼叫方式中,Guard(p)是一個右值,語句結束後建立的資源會立即釋放,從而導致p提前釋放,p成為野指標,而後面繼續訪問p可能導致程式異常。雖然用auto sp = Guard(p);
賦值不存在問題,但是客戶可能會忘記,也就是說這種寫法不夠安全。
如何解決由於忘記賦值導致指標提前釋放的問題?
答:可以用一個巨集來解決這個問題,通過巨集來強制建立一個臨時智慧指標。程式碼如:
// OK
#define GUARD(p) std::shared_ptr<void> p##p(p, [](void* p) { GetHandler()->Release(p); })
void* p = GetHandler()->Create();
GUARD(p); // 安全:會在當前作用域下,建立名為pp的shared_ptr<void>
當然,如果只希望用獨佔性的管理第三方庫的資源,可以用unique_ptr。
// OK
#define GUARD(p) std::unique_ptr<void, void(*)(int*)> p##p(p, [](void* p) { GetHandler()->Release(p); })
小結:
1)使用巨集定義方式的優勢:即使忘記對智慧指標賦值,也能正常執行,安全又方便。
2)使用GUARD這種智慧指標管理第三方庫的方式,其本質是智慧指標,能在各種場景下正確釋放記憶體。
參考
[1]祁宇. 深入應用C++11 : 程式碼優化與工程級應用[M]. 機械工業出版社, 2015.
[2]斯坦利·李普曼, 約瑟·拉喬伊, 芭芭拉·默,等. C++ Primer中文版(第5版)[J]. 中國科技資訊, 2013.