C++14 智慧指標unique_ptr、shared_ptr、weak_ptr

Doc Tsing發表於2017-07-09

記憶體控制這一大毒瘤,幾乎一直伴隨著C/C++工程師。隨著計算機技術的發展,在boost準標準庫的推動下,C++11終於將unique_ptr、shared_ptr、weak_ptr這幾類智慧指標納入C++中。當然,在這之前還有一種auto_ptr智慧指標,不過由於它的設計存在較大問題(比如:auto_ptr與STL不相容),現階段已經很少能看到它的出現了。

感謝@毛毛是我的小可愛 在評論中指出“ 標題說的還是C++14,文章開頭就跑出C++11。請嚴謹一點!”。
這裡說明一下,(1)上文中提到C++11出現了智慧指標unique_ptr、shared_ptr、weak_ptr,(2)但是文中的程式碼示例使用了make_unique()std::move(),這些為C++14中的內容。


智慧指標實質是一個物件,行為表現的卻像一個指標。


shared_ptr和unique_ptr之間的區別在於:shared_ptr是引用計數的智慧指標,而unique_ptr不是。這意味著,可以有多個shared_ptr例項指向同一塊動態分配的記憶體,當最後一個shared_ptr離開作用域時,才會釋放這塊記憶體。shared_ptr也是執行緒安全的。 另一方面,unique_ptr意味著所有權。單個unique_ptr離開作用域時,會立即釋放底層記憶體。

2018-2-28更新 :感謝@奔跑的哇牛 在留言中說到shared_ptr本身不是執行緒安全的。是的,shared_ptr本身不是執行緒安全的。陳碩的書中也明確提到了,shared_ptr的計數功能是原子的,但物件的讀寫不是原子的。c++標準也只是保證的是weak_ptrlock()指標提升是執行緒安全的。所以,要實現執行緒安全,可能需要weak_ptrshared_ptr配合使用,詳見陳碩的多執行緒書籍。

也可以參考:http://www.pandademo.com/2017/08/thread-safety-of-shared_ptr-and-weak_ptr/

預設的智慧指標應該是unique_ptr。只有需要共享資源時,才使用shared_ptr。

這兩個智慧指標都需要包含<memory>標頭檔案。

在開始本文之前,首先給出一個類。因為下文中,為了演示智慧指標的使用方式,在較多時候都有用到這個類demo。

#include <memory>
#include <utility> //std::move()

class demo {
public:
	demo() : uptr(std::make_unique<int[]>(10)){
		printf("demo\n");
		for (int i = 0; i < 10; ++i){
			uptr[i] = i;
		}
	}
	~demo(){
		printf("~demo\n");
	}
	void show(){
		printf("%d\n", uptr[9]);
	}
private:
	std::unique_ptr<int[]> uptr;
};

unique_ptr

unique_ptr是唯一的,適用於儲存動態分配的舊C風格的陣列。auto關鍵字會自動識別指標型別,當與make_unique配合使用時,即表示unique_ptr智慧指標。

void my_unique_ptr(){
	auto uptr = std::make_unique<int[]>(10);
	uptr[5] = 17;
	printf("%d\n", uptr[5]);
}

這裡應該總是使用auto/make_unique寫法。除非編譯器不支援的情況下,可以這樣寫,

std::unique_ptr<int[]> uptr(new int[10]);
//std::unique_ptr<int[]> uptr = new int[10];//error

注意,上文中,被註釋的那種寫法是不被接受的。再整理一下,unique_ptr可以有以下使用方式,

void my_unique_ptr2(){
	auto uptr = std::make_unique<demo>();
	uptr->show();
	std::unique_ptr<demo> uptr2(new demo());///等效寫法
	uptr2->show();
}

另外,需要說明的是,unique_ptr無法使用拷貝建構函式的,上文已經提到過了,這裡再給出一個示例,

void my_unique_ptr3(){
	auto uptr = std::make_unique<int>(42);
	printf("%d\n", *uptr);//42
	///std::unique_ptr<int> uptr1 = uptr; ///unique_ptr無拷貝建構函式
	///std::unique_ptr<int> uptr1(uptr);  ///等效寫法
	std::unique_ptr<int> uptr2 = std::move(uptr);
	///printf("%d\n",*uptr); ///error 所有權已交給uptr2
	printf("%d\n", *uptr2);
}

既然,無法使用拷貝建構函式,那麼就無法直接使用賦值“=”來轉移指標所有權。但是C++14設計者給開了另外一扇門:std::move。它被包含在<utility>標頭檔案中。

shared_ptr

shared_ptr用法與unique_ptr類似。如果編譯器支援,你應該總是使用auto/make_shared的寫法,它比直接建立shared_ptr更高效。

void my_shared_ptr(){
	///auto sptr = std::make_shared<int[]>(10); //error
	///sptr[6] = 20;
	auto sptr = std::make_shared<demo>();
	sptr->show();
}

上文中已經提到了,智慧指標家族中,unique_ptr是唯一可以適用於舊C風格陣列的指標,shared_ptr等其他智慧指標不能。

shared_ptr除了用於管理純粹的記憶體之外還可以用於其他的目的,比如管理FILE、SOCKET等,極大的增加了程式設計的方便性。

void auto_run_fun(FILE* f){
	printf("auto running.\n");
	fclose(f);
}

void my_shared_ptr2(){
	FILE* f = fopen("data.txt","w");
	std::shared_ptr<FILE> file_ptr(f, auto_run_fun);
}

由於shared_ptr是引用計數的,這裡需要極為注意的一點是:糟糕!只呼叫一次建構函式,卻呼叫了兩次解構函式。

正確的使用方式應該是使用make_shared拷貝建構函式建立副本。範例如下,

void my_shared_ptr3(){
	/*demo* d = new demo();
	std::shared_ptr<demo> sptr1(d);
	std::shared_ptr<demo> sptr2(d);//~demo() error
	*/
	auto sptr3 = std::make_shared<demo>();
	std::shared_ptr<demo> sptr4(sptr3); ///shared_ptr拷貝建構函式
}

shared_ptr引用計數,完全可以返回一個子函式的指標。 在以往的認知中,子函式中的棧空間上的記憶體是無法返回的,而子函式中堆空間上的記憶體是可以返回的(同時,還必須注意手動釋放它,否則必然記憶體洩漏)。

std::shared_ptr<demo> my_shared_ptr4(){
	auto sptr = std::make_shared<demo>();
	return sptr;
}

那麼,對於以上程式碼的返回值,下文中這樣子使用它,也是非常正確的。

my_shared_ptr4()->show();

智慧指標,完全繼承了JAVA和C#中記憶體託管的風格,而且智慧指標在很多情況下完全可以具體推算出:它會在何時被釋放。

關於shared_ptr更多使用方式,推薦《C++14 N叉樹使用shared_ptr智慧指標》 一文。

weak_ptr

weak_ptr是shared_ptr的黃金夥伴。從上文知道shared_ptr與shared_ptr之間,每拷貝一次,引用計數就會+1,而如果使用weak_ptr則不會出現這個現象。

如果將一個shared_ptr指標賦值給weak_ptr指標,對shared_ptr指標本身不會造成任何影響。對於weak_ptr指標來說,卻可以通過一些方法來探測被賦值過來的shared_ptr指標的有效性,同時weak_ptr指標也可以間接操縱shared_ptr指標。以下主要介紹兩個方法:

  • lock() ,weak_ptr指標呼叫lock()方法會獲得一個返回值:shared_ptr。而這個返回值就是被賦值過來的shared_ptr指標,那麼指標都獲得了,當然可以操縱它。
  • expired() ,該方法主要用來探測shared_ptr指標的有效性。shared_ptr一旦被釋放,指標就會被置為nullptr。
///weak_ptr -> shared_ptr
void my_weak_ptr(){
	std::weak_ptr<demo> wptr;
	{
		auto sptr = std::make_shared<demo>();
		wptr = sptr;
		auto sptr2 = wptr.lock();
		if (!wptr.expired()){///等價於sptr2 != nullptr
			printf("shared_ptr ok\n");
			sptr2->show();
		}
	}
	if (wptr.expired()){
		printf("shared_ptr deleted\n");
	}
}

以上程式碼中,之所以要加個大括號{}在文中,主要是:為了利用變數的作用域原理。 讓shared_ptr指標離開{}作用域後,立即被釋放。



©為徑
2018-12-27(修改) 北京 海淀



參考文獻:

Marc, Gregoire. C++高階程式設計(第3版)[M]. 北京:清華大學出版社, 2015. 630-636

相關文章