深入學習C++智能指針之shared_ptr與右值引用的方法

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

到此這篇關於C++智能指針之shared_ptr與右值引用的文章就介紹到這瞭,更多相關C++智能指針內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: