C++11 智能指針


什么是智能指針?

智能指針是存儲指向動態分配(位於堆)對象指針的類,用於生存期控制,能確保在離開指針所在作用域時,自動正確地銷毀動態分配的對象,以防止內存泄漏。
智能指針通常通過引用計數技術實現:每使用一次,內部引用計數+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 輔助函數、reset方法 來初始化shared_ptr,如:

// 智能指針的初始化方式
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 默認刪除器是刪除delete T對象的,並不是針對數組。如果要刪除數組,就需要為數組指定delete[]刪除器。或者,可以通過封裝一個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[]>());
}
// 使用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的陷阱

  1. 不要將原始指針賦值給shared_ptr
shared_ptr<int> p = new int; // 編譯錯誤
  1. 不要將一個原始指針初始化多個shared_ptr
int* rawp = new int;
shared_ptr<int> p1(rawp);
shared_ptr<int> p2(rawp); // 邏輯錯誤,可能導致程序崩潰
  1. 不要在函數實參中創建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 尚未創建,那么int內存就泄漏了。
正確寫法是先創建智能指針,然后調用函數:

// 函數參數是shared_ptr時,正確寫法
shared_ptr<int> p(new int());
f(p, g());
  1. 通過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 類,然后使用base class的成員函數shared_from_this來返回this的shared_ptr。

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,注意這里會引起指向A的raw pointer對應的shared_ptr的引用計數+1
  1. 避免循環引用。循環引用會導致內存泄漏。
    一個典型的循環引用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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM