1. 程式人生 > 實用技巧 >C++的智慧指標你瞭解嗎?

C++的智慧指標你瞭解嗎?

前言

C中我們會進行malloc一塊記憶體,然後free掉。但是經常會遇到我們忘記寫free,導致記憶體溢位,C++也有類似的情況,為了解決掉我們忘記釋放記憶體的習慣,C++引入了幾種智慧指標,為的就是讓函式可以在正常終止或者異常終止的情況下,改指標的指向的記憶體都可以處於正確的狀態。shared_ptr、unique_ptr、weak_ptr、auto_ptr。其中shared_ptr允許多個指標指向同一個物件;unique_ptr則"獨佔"所指向的物件, 保證同一時間只有一個 指標指向某個記憶體,對於避免記憶體洩漏很有用;weak_ptr則是一種弱引用,指向shared_ptr所管理的物件;而auto_ptr

則是類似於unique_ptr的一種指標,並且已經在C++11摒棄了它


作者:良知猶存

轉載授權以及圍觀:歡迎新增微信公眾號:羽林君


shared_ptr

類似vector,智慧指標也是模板。因此當我們建立一個指標指標的時,必須提供額外的資訊——指標可以指向的型別。預設初始化的智慧指標中儲存著一個空指標。

智慧指標使用和普通指標類似。解引用一個智慧指標返回它指向的物件。如果在一個條件判斷中使用智慧指標,效果就是檢測它是否為空。

shared_ptr<T> p; //空智慧指標 可以指向型別為T的物件
p.get(); //返回p中儲存的指標
swap(p,q);//交換p和q的指標
p.swap(q);//交換p和q的指標
shared_ptr<T> p(q); //p是share_ptr q的拷貝;此操作會遞增q中的計數器。此外q中的指標必須能轉為T*;
p.use_count(); //返回與p共享物件的智慧指標的數量,可能很慢,主要用於除錯

程式使用動態記憶體出於以下三種原因之一 1、程式不知道自己需要多少物件; 2、程式不知道所需物件的準確型別;

3、程式需要在多個物件間共享資料

shared_ptr可以指向特定型別的物件,用於自動釋放所指的物件,所以我們會經常多的用,此外還有一個最安全的分配和使用動態記憶體的方法就是呼叫一個名為make_shared的標準庫函式;和make_pair、make_unique一樣,都是在原有基礎進行的泛化。

make_shared的用法

make_shared 在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr,與智慧指標一樣,make_shared也定義在標頭檔案memory中;當要用make_shared時,必須指定想要建立的物件型別,定義方式與模板類相同,在函式名之後跟一個尖括號,在其中給出型別;

unique_ptr

unique_ptr<>是c ++ 11提供的智慧指標實現之一,用於防止記憶體洩漏。unique_ptr物件包含一個原始指標,並負責其生命週期。當這個物件被銷燬的時候,它的解構函式會刪除關聯的原始指標。unique_ptr有過載的- >和*運算子,所以它可以被用於類似於普通的指標。

unique_ptr<T> u1;//空unique_ptr,可以指向型別為T的物件。
unique_ptr<T ,D> u2;


//u1會使用delet來釋放它的指標,u2會使用一個型別為D的可呼叫物件來釋放它的指標
unique_ptr<T ,D> u(d);///空unique_ptr,指向型別為T的物件,用型別為D的物件d代替delete
u1 = nullptr; //釋放u指向的物件,將u置為空


u.release(); //u放棄對指標的控制權,返回指標,並將u置為空
u.reset(); //釋放u指向的物件
u.reset(q); //如果提供了內建指標q,令u指向這個物件;否則將u置空
u.reset(nullptr);

雖然我們不能拷貝或賦值unique_ptr,但是可以通過release或reset將指標的所有權從一個(非const)unique_ptr轉移到另一個unique_ptr;

C++11加入了make_shared,C++14加入了make_unique,如果你處於C++11環境也不必擔心,因為make_unique很容易實現,make_unique只是將其引數完美轉發給待建立物件的建構函式,並返回一個由原始指標初始化得到的unique_ptr。這種實現形式並不支援陣列或自定義刪除器,但它至少表明實現make_unique並不困難。

C++ 14引入了std::make_unique,因為由於未指定引數評估順序,因此這是不安全的:

f(std::unique_ptr<MyClass>(new MyClass(param)), g()); // Syntax A

(說明:如果評估首先為原始指標分配記憶體,然後呼叫g(),並且在std::unique_ptr構造之前引發了異常,則記憶體被洩漏。)

呼叫std::make_unique是一種限制呼叫順序的方法,從而使事情變得安全:

f(std::make_unique<MyClass>(param), g());       // Syntax B

至於更加詳細的解釋大家可以網上搜索兩者的對比。

weak_ptr

weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指標的引用計數永遠不可能下降為0,資源永遠不會釋放。它是對物件的一種弱引用,不會增加物件的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過呼叫lock函式來獲得shared_ptr。

class A;
class B;
class A {
public:
A() {cout<< "A Created" << endl;}
~A() { cout<< "A Destroyed" << endl;
}
shared_ptr<B>ptr;
};
class B {
public:
B() {cout<< "B Created" << endl; }
~B() {cout<< "B Destroyed" << endl;}
shared_ptr<A>ptr;
};

int main() {
shared_ptr<A>pt1(new A());
shared_ptr<B>pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "useof pt1: " << pt1.use_count() << endl;
cout << "useof pt2: " << pt2.use_count() << endl;
return 0;
}

weak_ptr是一種不控制物件生命週期的智慧指標,它與一個 shared_ptr 繫結,卻不參與引用計數。一旦最後一個shared_ptr 銷燬,物件就會釋放。

weak_ptr 作用是在需要的時候變出一個 shared_ptr,其他時候不干擾 shared_ptr 的引用計數。weak_ptr沒有 * 和 -> 符號,需要用lock() 獲得shared_ptr ,進而使用物件。

利用 weak_ptr可以消除上面的迴圈引用,例子如下。

class A;
class B;
class A {
public:
A() {cout<< "A Created" << endl;}
~A() { cout<< "A Destroyed" << endl;
}
weak_ptr<B>ptr;
};
class B {
public:
B() {cout<< "B Created" << endl; }
~B() {cout<< "B Destroyed" << endl;}
weak_ptr<A>ptr;
};

int main() {
shared_ptr<A>pt1(new A());
shared_ptr<B>pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "useof pt1: " << pt1.use_count() << endl;
cout << "useof pt2: " << pt2.use_count() << endl;
return 0;
}

auto_ptr

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocaticn = ps;

   上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指標,則兩個指標將指向同一個string物件。這是不能接受的,因為程式將試圖刪除同一個物件兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:

1.定義陚值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採用此方案。
2.建立所有權(ownership)概念。對於特定的物件,只能有一個智慧指標可擁有,這樣只有擁有物件的智慧指標的建構函式會刪除該物件。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
3.建立智慧更高的指標,跟蹤引用特定物件的智慧指標數。這稱為引用計數。例如,賦值時,計數將加1,而指標過期時,計數將減1,。當減為0時才呼叫delete。這是shared_ptr採用的策略。
當然,同樣的策略也適用於複製建構函式。
每種方法都有其用途,但為何說要摒棄auto_ptr呢?
下面舉個例子來說明。

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


int main() {
auto_ptr<string> films[5] =
{
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership. 將所有權從films[2]轉讓給pwin,此時films[2]不再引用該字串從而變成空指標


cout << "The nominees for best avian baseballl film are\n";
for(int i = 0; i < 5; ++i)
cout << *films[i] << endl;
cout << "The winner is " << *pwin << endl;
cin.get();


return 0;
}

執行下發現程式崩潰了,原因在上面註釋已經說的很清楚,films[2]已經是空指標了,下面輸出訪問空指標當然會崩潰了。但這裡如果把auto_ptr換成shared_ptr或unique_ptr後,程式就不會崩潰,原因如下:

2.使用shared_ptr時執行正常,因為shared_ptr採用引用計數,pwin和films[2]都指向同一塊記憶體,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個物件的錯誤。

1.使用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程式不會等到執行階段崩潰,而在編譯器因下述程式碼行出現錯誤:

unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.

這就是為何要摒棄auto_ptr的原因,一句話總結就是:避免潛在的記憶體崩潰問題。

如何選擇智慧指標呢?

在掌握了這幾種智慧指標後,大家可能會想另一個問題:在實際應用中,應使用哪種智慧指標呢?

下面給出幾個使用指南。

(1)如果程式要使用多個指向同一個物件的指標,應選擇shared_ptr。這樣的情況包括:

  • 有一個指標陣列,並使用一些輔助指標來標示特定的元素,如最大的元素和最小的元素;

  • 兩個物件包含都指向第三個物件的指標;

  • STL容器包含指標。很多STL演算法都支援複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。

(2)如果程式不需要多個指向同一個物件的指標,則可使用unique_ptr。如果函式使用new分配記憶體,並返還指向該記憶體的指標,將其返回型別宣告為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智慧指標將負責呼叫delete。可將unique_ptr儲存到STL容器在那個,只要不呼叫將一個unique_ptr複製或賦給另一個演算法(如sort())。例如,可在程式中使用類似於下面的程式碼段。

unique_ptr<int> make_int(int n)
{
return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
cout << *a << ' ';
}
int main()
{
...
vector<unique_ptr<int> > vp(size);
for(int i = 0; i < vp.size(); i++)
vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr
vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary
for_each(vp.begin(), vp.end(), show); // use for_each()
...
}

其中push_back呼叫沒有問題,因為它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞物件,for_each()將非法,因為這將導致使用一個來自vp的非臨時unique_ptr初始化pi,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。

在unique_ptr為右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的程式碼中,make_int()的返回型別為unique_ptr<int>

unique_ptr<int> pup(make_int(rand() % 1000));  // ok
shared_ptr<int> spp(pup); // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000)); // ok

模板shared_ptr包含一個顯式建構函式,可用於將右值unique_ptr轉換為shared_ptr。shared_ptr將接管原來歸unique_ptr所有的物件。

在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。

這就是我分享的c++的智慧指標,其中參考了好多人的文字,此外如果大家有什麼更好的思路,也歡迎分享交流哈。

***END*

推薦閱讀

【1】c++nullptr(空指標常量)、constexpr(常量表達式)

【2】嵌入式底層開發的軟體框架簡述

【3】CPU中的程式是怎麼執行起來的 必讀

【4】C++的匿名函式(lambda表示式)

【5】階段性文章總結分析

本公眾號全部原創乾貨已整理成一個目錄,回覆[ 資源 ]即可獲得。

更多分享,掃碼關注我