1. 程式人生 > 實用技巧 >C++ 11中的智慧指標

C++ 11中的智慧指標

C++ 11中的智慧指標

引言

普通指標使用時存在掛起引用以及記憶體洩漏的問題,C++ 11中引入了智慧指標來解決它

std::unique_ptr

std::unique_ptr是std::auto_ptr的替代品,解決了C++ 11之前std::auto_ptr的很多缺漏

簡單的看一下std::auto_ptr的複製建構函式

template<typename T>
class auto_ptr {
public:
    //Codes..
    auto_ptr(auto_ptr& atp) {
    	m_ptr = atp.m_ptr;
    	atp.m_ptr = nullptr;
	}
private:
    T* m_ptr;
};

可以很容易的看出,該函式將指標所有權從一個物件轉移到另外一個物件,且將原物件置空。該函式中std::auto_ptr實際上在運用引用去實現移動語義。但若是在轉移所有權後仍去訪問前一個物件(現在已經被置為空指標),程式可能會崩潰。

std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
//auto _data = atp.data;		//undefined behavior
//此時的atp已經為nullptr,“移動函式”將所有權轉接給了另外一個物件

慶幸的是C++ 11中引入了一種叫做右值引用的東西,可運用此實現出轉移建構函式。如今std::auto_ptr已經被std::unique_ptr所替代

unique_ptr(unique_ptr&& unip)
{
	m_ptr = unip.m_ptr;
	unip.m_ptr = nullptr;
}

雖然說這個函式與std::auto_ptr中的做法一樣,但它只能接受右值作為引數。傳遞右值,即為轉換指標所有權

std::unique_ptr<int> a(new int(10));	//okay
std::unique_ptr<int> b(a);	//error

與std::shared_ptr不同,該智慧指標用於不能被多個例項共享的記憶體管理,也就是說,僅有一個例項擁有記憶體所有權。

對於智慧指標而言,聲明後會預設置為空。建議使用std::make_unique來替代直接使用new建立例項(C++ 14中加入此方法)。若在new時期智慧指標指向的類(也就是模板接受到的引數)丟擲了異常,或是類的建構函式失效,這樣就會導致new出來的物件被漏掉,導致記憶體洩漏。

std::unique_ptr<int> uniq1;								   //預設置空
std::unique_ptr<int> uniq2(new int(5));				    	//舊方法
std::unique_ptr<int> uniq3 = std::make_unique<int>(10);		 //C++ 14新規範
std::unique_ptr<std::string> uniq4 = std::make_unique<std::string>("Pointer");
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> uniq5 = std::make_unique<std::array<int, 10>>(arr);

make函式的不足


需要注意的是,接受指標引數的智慧指標是explicit的,因此我們不能將一個內建指標隱式轉換為一個智慧指標

std::unique_ptr<int> uniq = new int(10); 	//錯誤

同時std::unique_ptr不允許左值賦值,即不允許拷貝。但是可以通過移動語義轉移所有權

  std::unique_ptr<int> unip = std::make_unique<int>(10);
//std::unique_ptr<int> copy_unip = unip;
//std::unique_ptr<int> copy_unip(unip);
  std::unique_ptr<int> move_unip = std::move(unip);	//unip被置空,所有權轉到move_unip上

但我們可以拷貝或是賦值一份即將銷燬的std::unique_ptr,最常見的例子是從函式返回一個std::unique_ptr

std::unique_ptr<int> clone() 
{
    std::unique_ptr<int> uniq = std::make_unique<int>(10);
    return uniq;		//編譯器知道返回的物件即將被銷燬,於是編譯器執行了一次“特殊的”拷貝
}

除了利用std::make_unique的方法,還可以運用變參模板,簡單實現make_unique

template<typename T, typename ... Ts>
std::unique_ptr<T> make_unique_Selfcode(Ts ... args)
{
    return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }};	//泛型工廠函式
}

C++ 11中包含了花括號初始的方法,叫做initializer_list

小括號呼叫到了型別的建構函式,包含了隱式轉換的規則,而花括號並沒有呼叫到建構函式,而是用到了列表初始化,這種方法當碰到精度降低,範圍變窄等情況(narrowing conversion)時,編譯器都會給出報錯

double num = 3.1415926;
int test1(num);		//編譯器給出警告,該轉換可能會丟失資料
int test2{num};		//編譯器給出報錯,從double轉換到int需要收縮轉換

同時,C++ 11也支援使用者在自定義類的初始化時使用列表初始化

class Test
{
public:
    Test(const std::initializer_list<int>& i_list)
    {
        for (auto i : i_list)
            vec.emplace_back(i);
    }
private:
    std::vector<int> vec;
};
int main()
{
    Test t{ 1, 2, 3 };
    Test t = { 1 ,2 ,3 , 4 };
}

回到std::unique_ptr上, std::unique_ptr物件可以傳給左值常量引用常數,因為這樣並不會改變記憶體所有權,也可以使用移動語義進行右值傳值

class Test {};
void Change_Right(std::unique_ptr<Test>&& t) {}
void Change_Ref(const std::unique_ptr<Test>& t) {}
int main()
{
    auto temp = std::make_unique<Test>();
    //Change_Right(temp);    //錯誤 無法將右值引用繫結到左值
      Change_Right(std::move(temp));
      Change_Ref(temp);
      Change_Ref(std::move(temp));
}

對於該智慧指標來說,應避免犯以下兩個錯誤

// 1.用同一份指標初始化多個std::unique_ptr
int *p = new int(10);
std::unique_ptr<int> uniq(p);
std::unique_ptr<int> uniq2(p);
// 2.混用普通指標與智慧指標
int *p = new int(10);
std::unique_ptr<int> uniq(p);
delete p;

std::unique_ptr中有get(), release(), reset()方法

get()可以獲得智慧指標中管理的資源。release()會返回該其所管理的資源,並釋放其所有權。reset()則直接析構其管理的記憶體,同時reset()還可以接受一個內建指標,使原物件指向新的記憶體

std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);
//unip.reset(unip2.get())		//絕對禁止的操作,會導致掛起引用
  unip.reset(unip2.release());	//unip析構其管理的記憶體後,接收unip2轉接過來的所有權
  unip = std::move(unip2);		//與上一行作用相同

說到了reset(),就應該順便扯一下智慧指標的自定義刪除器

首先std::shared_ptr的構造與std::unique_ptr存在一點差異。std::unique_ptr支援動態陣列,而std::share_ptr不支援動態陣列(C++ 17之前)。

std::unique_ptr<int[]> unip(new int[10]);	//合法
std::shared_ptr<int[]> shrp(new int [10];	//在C++ 17之前時非法操作,不能傳入陣列型別作為模板引數
std::shared_ptr<int> shrp2(new int[10]);	//可編譯,但存在未定義行為

所以,std::share_ptr會使用delete p來釋放資源,這對於new int[10] 而言肯定是非法的,對它應使用delete[] p

對此我們有兩種解決方法,一種是傳入std::default_delete,另外一種是自行構造刪除器

std::shared_ptr<int> p(new int[10], std::default_delete<int[]>());
std::shared_ptr<int> shrp2(new int[10], [](int* p) {delete[] p; });	//lambda表示式轉換為函式指標
void deleter(int *p)
{
    delete[] p;
}
std::shared_ptr<int> shrp2(new int[10], deleter);		
//還可以封裝一個make_shared_SelfCode的方法
template<typename T>
auto make_shared_SelfCode(size_t size)
{
    return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
//允許使用auto作為模板函式的返回值
std::shared_ptr<int> shrp3 = make_shared_SelfCode(10);

雖然說能用了,但存在著幾個缺點

1.我們想管理的是int[]型別,但傳入的引數模板卻為int

2.需要顯示提供刪除器

3.無法使用make_shared,無法保證異常安全

4.由於沒有過載operator[],故需要使用shrq.get().[Index]來獲取陣列中的元素

在C++ 17中,std::shared_ptr過載了[]運算子,並支援傳入int[]型別作為模板引數

std::shared_ptr<int[]> shrp(new int[10]);
shrp[0] = 5;

那麼還剩下一個問題,我們還不能使用std::make_shared去例項化指標。所以在C++2a中,新規定便解決了這個問題(雖然我的編譯器跑不了)

std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);	
//分配一個管理有10個int元素的動態陣列的std::shared_ptr
//同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);

對於std::shared_ptr來說,自定義一個刪除器是比較簡單的,而對於std::unique_ptr來說,情況有點不同

  std::shared_ptr<int> shrp(new int(10), [](int* p) {delete p; });	//正確
//std::unique_ptr<int> unip(new int(10), [](int* p) {delete p; });	//錯誤

std::unique_ptr在指定刪除器時需要在模板引數中傳入刪除器的型別

std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) {delete p; });

如果要在lambda表示式中捕獲變數,則編譯器將會報錯,原因是捕獲了變數的lambda表示式無法轉換成函式指標,所以我們可以使用std::function來進行包裝

//std::unique_ptr<int,void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });	//錯誤
  std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) {delete p; });

除了用lambda表示式,我們還可以這麼幹

template<typename T>
void deleter(T* p)
{
    delete[] p;
}
//使用decltype型別推斷
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取別名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//聯合typedef與decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);
//使用using
using deleter_int_U = void(*)(int*);
std::unique_ptr<int, deleter_int_U> unip(new int(10), deleter<int>);