C++智慧指標之shared_ptr與右值引用(詳細)

進擊的汪sir發表於2021-07-12

1. 介紹

在 C++ 中沒有垃圾回收機制,必須自己釋放分配的記憶體,否則就會造成記憶體洩露。解決這個問題最有效的方法是使用智慧指標(smart pointer)。智慧指標是儲存指向動態分配(堆)物件指標的類,用於生存期的控制,能夠確保在離開指標所在作用域時,自動地銷燬動態分配的物件,防止記憶體洩露。智慧指標的核心實現技術是引用計數,每使用它一次,內部引用計數加1,每析構一次內部的引用計數減1,減為0時,刪除所指向的堆記憶體。

C++11 中提供了三種智慧指標,使用這些智慧指標時需要引用標頭檔案

  • std::shared_ptr:共享的智慧指標
  • std::unique_ptr:獨佔的智慧指標
  • std::weak_ptr:弱引用的智慧指標,它不共享指標,不能操作資源,是用來監視 shared_ptr 的。

共享智慧指標(shared_ptr)是指多個智慧指標可以同時管理同一塊有效的記憶體,共享智慧指標 shared_ptr 是一個模板類,如果要進行初始化有三種方式:通過建構函式、std::make_shared 輔助函式以及 reset 方法。共享智慧指標物件初始化完畢之後就指向了要管理的那塊堆記憶體,如果想要檢視當前有多少個智慧指標同時管理著這塊記憶體可以使用共享智慧指標提供的一個成員函式 use_count

2. 初始化方法

2.1 通過建構函式初始化

例項

// 使用智慧指標管理一塊 int 型的堆記憶體
shared_ptr<int> ptr1(new int(520));

2.2 通過拷貝和移動建構函式初始化

呼叫拷貝建構函式

shared_ptr<int> ptr2(ptr1);

呼叫移動建構函式

std::shared_ptr<int> ptr5 = std::move(ptr2);

如果使用拷貝的方式初始化共享智慧指標物件,這兩個物件會同時管理同一塊堆記憶體,堆記憶體對應的引用計數也會增加;
如果使用移動的方式初始智慧指標物件,只是轉讓了記憶體的所有權,管理記憶體的物件並不會增加,因此記憶體的引用計數不會變化。

2.2.1 移動構造

關於移動構造,可能有些讀者不太明白

移動構造是C++11標準中提供的一種新的構造方法。

在現實中有很多這樣的例子,我們將錢從一個賬號轉移到另一個賬號,將手機SIM卡轉移到另一臺手機,將檔案從一個位置剪下到另一個位置……移動構造可以減少不必要的複製,帶來效能上的提升。

我們首先來看看move函式
首先看這樣一段程式碼

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
	string st = "I love 進擊的汪sir";
	vector<string> vc;
	vc.push_back(move(st));
	cout << vc[0] << endl;
	if (!st.empty())
		cout << st << endl;

	return 0;
}

輸出的結果為
在這裡插入圖片描述

再看這樣一段程式碼

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

int main()
{
	string st = "I love xing";
	vector<string> vc;
	vc.push_back(st);
	cout << vc[0] << endl;
	if (!st.empty())
		cout << st << endl;

	return 0;
}

其結果為
在這裡插入圖片描述
這兩段程式碼唯一的不同是呼叫vc.push_back()將字串插入到容器中去時,第一段程式碼使用了move語句,而第二段程式碼沒有使用move語句。輸出的結果差異也很明顯,第一段程式碼中,原來的字串st已經為空,而第二段程式碼中,原來的字串st的內容沒有變化。

先暫時記住這兩端程式碼的輸出結果之間的差異。
我們回到移動建構函式上

有時候我們會遇到這樣一種情況,我們用物件a初始化物件b,後物件a我們就不在使用了,但是物件a的空間還在呀(在析構之前),既然拷貝建構函式,實際上就是把a物件的內容複製一份到b中,那麼為什麼我們不能直接使用a的空間呢?這樣就避免了新的空間的分配,大大降低了構造的成本。這就是移動建構函式設計的初衷。

通俗一點的解釋就是,拷貝建構函式中,對於指標,我們一定要採用深層複製,而移動建構函式中,對於指標,我們採用淺層複製。

所以在上面的例子中,如果呼叫移動建構函式來初始化智慧指標,引用計數是不會增加的,而move函式實際上是返回的右值引用

2.2.2 右值引用

上面我們講到了右值引用,這裡就來擴充套件一下右值引用是啥
首先得分清楚,什麼是右值,什麼是左值

  • lvalue 是 loactor value 的縮寫,rvalue 是 read value 的縮寫

  • 左值是指儲存在記憶體中、有明確儲存地址(可取地址)的資料;

  • 右值是指可以提供資料值的資料(不可取地址);

通過描述可以看出,區分左值與右值的便捷方法是:可以對錶達式取地址(&)就是左值,否則為右值 。所有有名字的變數或物件都是左值,而右值是匿名的。

C++11 中右值可以分為兩種:一個是將亡值( xvalue, expiring value),另一個則是純右值( prvalue, PureRvalue):

  • 純右值:非引用返回的臨時變數、運算表示式產生的臨時變數、原始字面量和 lambda 表示式等
  • 將亡值:與右值引用相關的表示式,比如,T&& 型別函式的返回值、 std::move 的返回值等。

右值引用就是對一個右值進行引用的型別。因為右值是匿名的,所以我們只能通過引用的方式找到它。無論宣告左值引用還是右值引用都必須立即進行初始化,因為引用型別本身並不擁有所繫結物件的記憶體,只是該物件的一個別名。通過右值引用的宣告,該右值又“重獲新生”,其生命週期與右值引用型別變數的生命週期一樣,只要該變數還活著,該右值臨時量將會一直存活下去。

右值通過&&來引用

例如:

  • int&& value = 520; 裡面 520 是純右值,value 是對字面量 520 這個右值的引用。
  • int &&a2 = a1; 中 a1 雖然寫在了 = 右邊,但是它仍然是一個左值,使用左值初始化一個右值引用型別是不合法的。
  • const Test& t = getObj() 這句程式碼的語法是正確的,常量左值引用是一個萬能引用型別,它可以接受左值、右值、常量左值和常量右值。

2.3 通過 std::make_shared 初始化

通過 C++ 提供的 std::make_shared() 就可以完成記憶體物件的建立並將其初始化給智慧指標,函式原型如下:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );

例項
使用智慧指標管理一塊 int 型的堆記憶體, 內部引用計數為 1

shared_ptr<int> ptr1 = make_shared<int>(520);

注意
使用 std::make_shared() 模板函式可以完成記憶體地址的建立,並將最終得到的記憶體地址傳遞給共享智慧指標物件管理。如果申請的記憶體是普通型別,通過函式的()可完成地址的初始化,如果要建立一個類物件,函式的()內部需要指定構造物件需要的引數,也就是類建構函式的引數。

2.4 通過 reset 方法初始化

共享智慧指標類提供的 std::shared_ptr::reset 方法函式原型如下:

void reset() noexcept;

template< class Y >
void reset( Y* ptr );

template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );

template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );
  • ptr:指向要取得所有權的物件的指標
  • d:指向要取得所有權的物件的指標
  • aloc:內部儲存所用的分配器

例項

shared_ptr<int> ptr5;
ptr5.reset(new int(250));

3. 獲取原始指標

對應基礎資料型別來說,通過操作智慧指標和操作智慧指標管理的記憶體效果是一樣的,可以直接完成資料的讀寫。但是如果共享智慧指標管理的是一個物件,那麼就需要取出原始記憶體的地址再操作,可以呼叫共享智慧指標類提供的 get () 方法得到原始地址,其函式原型如下:

T* get() const noexcept;

例項

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // 得到指標的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "部落格:進擊的汪sir");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

4. 指定刪除器

當智慧指標管理的記憶體對應的引用計數變為 0 的時候,這塊記憶體就會被智慧指標析構掉了。另外,我們在初始化智慧指標的時候也可以自己指定刪除動作,這個刪除操作對應的函式被稱之為刪除器,這個刪除器函式本質是一個回撥函式,我們只需要進行實現,其呼叫是由智慧指標完成的。

例項

#include <iostream>
#include <memory>
using namespace std;

// 自定義刪除器函式,釋放int型記憶體
void deleteIntPtr(int* p)
{
    delete p;
    cout << "int 型記憶體被釋放了...";
}

int main()
{
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}

刪除器函式也可以是 lambda 表示式!

5. 參考連結

https://subingwen.cn/cpp/shared_ptr/
https://www.cnblogs.com/qingergege/p/7607089.html

相關文章