窺見C++11智能指針

導語: C++指針的內存管理相信是大部分C++入門程序員的夢魘,受到Boost的啟發(fā),C++11標準推出了智能指針,讓我們從指針的內存管理中釋放出來,幾乎消滅所有new和delete。既然智能指針如此強大,今天我們來一窺智能指針的原理以及在多線程操作中需要注意的細節(jié)。

智能指針的由來

在遠古時代,C++發(fā)明了指針這把雙刃劍,既可以讓程序員精確地控制堆上每一塊內存,也讓程序更容易發(fā)生crash,大大增加了使用指針的技術門檻。因此,從C++98開始便推出了auto_ptr,對裸指針進行封裝,讓程序員無需手動釋放指針指向的內存區(qū)域,在auto_ptr生命周期結束時自動釋放,然而,由于auto_ptr在轉移指針所有權后會產生野指針,導致程序運行時crash,如下面示例代碼所示:

auto_ptr<int> p1(new int(10));

auto_ptr<int> p2 = p1; //轉移控制權

*p1 += 10; //crash,p1為空指針,可以用p1->get判空做保護

因此在C++11又推出了unique_ptr、shared_ptr、weak_ptr三種智能指針,慢慢取代auto_ptr。

unique_ptr的使用

unique_ptr是auto_ptr的繼承者,對于同一塊內存只能有一個持有者,而unique_ptr和auto_ptr唯一區(qū)別就是unique_ptr不允許賦值操作,也就是不能放在等號的右邊(函數(shù)的參數(shù)和返回值例外),這一定程度避免了一些誤操作導致指針所有權轉移,然而,unique_str依然有提供所有權轉移的方法move,調用move后,原unique_ptr就會失效,再用其訪問裸指針也會發(fā)生和auto_ptr相似的crash,如下面示例代碼,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指針所有權被轉移。

unique_ptr<int> up(new int(5));

//auto up2 = up; // 編譯錯誤

auto up2 = move(up);

cout << *up << endl; //crash,up已經失效,無法訪問其裸指針

除了上述用法,unique_ptr還支持創(chuàng)建動態(tài)數(shù)組。在C++中,創(chuàng)建數(shù)組有很多方法,如下所示:

// 靜態(tài)數(shù)組,在編譯時決定了數(shù)組大小

int arr[10];

// 通過指針創(chuàng)建在堆上的數(shù)組,可在運行時動態(tài)指定數(shù)組大小,但需要手動釋放內存

int *arr = new int[10];

// 通過std::vector容器創(chuàng)建動態(tài)數(shù)組,無需手動釋放數(shù)組內存

vector<int> arr(10);

// 通過unique_ptr創(chuàng)建動態(tài)數(shù)組,也無需手動釋放數(shù)組內存,比vector更輕量化

unique_ptr<int[]> arr(new int[10]);

這里需要注意的是,不管vector還是unique_ptr,雖然可以幫我們自動釋放數(shù)組內存,但如果數(shù)組的元素是復雜數(shù)據(jù)類型時,我們還需要在其析構函數(shù)中正確釋放內存。

真正的智能指針:shared_ptr

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11還推出了shared_ptr,這也是目前工程內使用最多最廣泛的智能指針,他使用引用計數(shù)(感覺有參考Objective-C的嫌疑),實現(xiàn)對同一塊內存可以有多個引用,在最后一個引用被釋放時,指向的內存才釋放,這也是和unique_ptr最大的區(qū)別。

另外,使用shared_ptr過程中有幾點需要注意:

構造shared_ptr的方法,如下示例代碼所示,我們盡量使用shared_ptr構造函數(shù)或者make_shared的方式創(chuàng)建shared_ptr,禁止使用裸指針賦值的方式,這樣會shared_ptr難于管理指針的生命周期。

// 使用裸指針賦值構造,不推薦,裸指針被釋放后,shared_ptr就野了,不能完全控制裸指針的生命周期,失去了智能指針價值

int *p = new int(10);

shared_ptr<int>sp = p;

delete p; // sp將成為野指針,使用sp將crash

// 將裸指針作為匿名指針傳入構造函數(shù),一般做法,讓shared_ptr接管裸指針的生命周期,更安全

shared_ptr<int>sp1(new int(10));

// 使用make_shared,推薦做法,更符合工廠模式,可以連代碼中的所有new,更高效;方法的參數(shù)是用來初始化模板類

shared_ptr<int>sp2 = make_shared<int>(10);

禁止使用指向shared_ptr的裸指針,也就是智能指針的指針,這聽起來就很奇怪,但開發(fā)中我們還需要注意,使用shared_ptr的指針指向一個shared_ptr時,引用計數(shù)并不會加一,操作shared_ptr的指針很容易就發(fā)生野指針異常。

shared_ptr<int>sp = make_shared<int>(10);

cout << sp.use_count() << endl; //輸出1

shared_ptr<int> *sp1 = &sp;

cout << (*sp1).use_count() << endl; //輸出依然是1

(*sp1).reset(); //sp成為野指針

cout << *sp << endl; //crash

使用shared_ptr創(chuàng)建動態(tài)數(shù)組,在介紹unique_ptr時我們就講過創(chuàng)建動態(tài)數(shù)組,而shared_ptr同樣可以做到,不過稍微復雜一點,如下代碼所示,除了要顯示指定析構方法外(因為默認是T的析構函數(shù),不是T[]),另外對外的數(shù)據(jù)類型依然是shared_ptr<T>,非常有迷惑性,看不出來是數(shù)組,最后不能直接使用下標讀寫數(shù)組,要先get()獲取裸指針才可以使用下標。所以,不推薦使用shared_ptr來創(chuàng)建動態(tài)數(shù)組,盡量使用unique_ptr,這可是unique_ptr為數(shù)不多的優(yōu)勢了。

template <typename T>

shared_ptr<T> make_shared_array(size_t size) {

return shared_ptr<T>(new T[size], default_delete<T[]>());

}

shared_ptr<int>sp = make_shared_array(10); //看上去是shared<int>類型,實際上是數(shù)組

sp.get()[0] = 100; //不能直接使用下標讀寫數(shù)組元素,需要通過get()方法獲取裸指針后再操作

用shared_ptr實現(xiàn)多態(tài),在我們使用裸指針時,實現(xiàn)多態(tài)就免不了定義虛函數(shù),那么用shared_ptr時也不例外,不過有一處是可以省下的,就是析構函數(shù)我們不需要定義為虛函數(shù)了,如下面代碼所示:

class A {

public:

~A() {

cout << "dealloc A" << endl;

}

};

class B : public A {

public:

~B() {

cout << "dealloc B" << endl;

}

};

int main(int argc, const char * argv[]) {

A *a = new B();

delete a; //只打印dealloc A

shared_ptr<A>spa = make_shared<B>(); //析構spa是會先打印dealloc B,再打印dealloc A

return 0;

}

循環(huán)引用,筆者最先接觸引用計數(shù)的語言就是Objective-C,而OC中最常出現(xiàn)的內存問題就是循環(huán)引用,如下面代碼所示,A中引用B,B中引用A,spa和spb的強引用計數(shù)永遠大于等于1,所以直到程序退出前都不會被退出,這種情況有時候在正常的業(yè)務邏輯中是不可避免的,而解決循環(huán)引用的方法最有效就是改用weak_ptr,具體可見下一章。

class A {

public:

shared_ptr<B> b;

};

class B {

public:

shared_ptr<A> a;

};

int main(int argc, const char * argv[]) {

shared_ptr<A> spa = make_shared<A>();

shared_ptr<B> spb = make_shared<B>();

spa->b = spb;

spb->a = spa;

return 0;

} //main函數(shù)退出后,spa和spb強引用計數(shù)依然為1,無法釋放

剛柔并濟:weak_ptr

正如上一章提到,使用shared_ptr過程中有可能會出現(xiàn)循環(huán)引用,關鍵原因是使用shared_ptr引用一個指針時會導致強引用計數(shù)+1,從此該指針的生命周期就會取決于該shared_ptr的生命周期,然而,有些情況我們一個類A里面只是想引用一下另外一個類B的對象,類B對象的創(chuàng)建不在類A,因此類A也無需管理類B對象的釋放,這個時候weak_ptr就應運而生了,使用shared_ptr賦值給一個weak_ptr不會增加強引用計數(shù)(strong_count),取而代之的是增加一個弱引用計數(shù)(weak_count),而弱引用計數(shù)不會影響到指針的生命周期,這就解開了循環(huán)引用,上一章最后的代碼使用weak_ptr可改造為如下代碼。

class A {

public:

shared_ptr<B> b;

};

class B {

public:

weak_ptr<A> a;

};

int main(int argc, const char * argv[]) {

shared_ptr<A> spa = make_shared<A>();

shared_ptr<B> spb = make_shared<B>();

spa->b = spb; //spb強引用計數(shù)為2,弱引用計數(shù)為1

spb->a = spa; //spa強引用計數(shù)為1,弱引用計數(shù)為2

return 0;

} //main函數(shù)退出后,spa先釋放,spb再釋放,循環(huán)解開了

使用weak_ptr也有需要注意的點,因為既然weak_ptr不負責裸指針的生命周期,那么weak_ptr也無法直接操作裸指針,我們需要先轉化為shared_ptr,這就和OC的Strong-Weak Dance有點像了,具體操作如下:

shared_ptr<int> spa = make_shared<int>(10);

weak_ptr<int> spb = spa; //weak_ptr無法直接使用裸指針創(chuàng)建

if (!spb.expired()) { //weak_ptr最好判斷是否過期,使用expired或use_count方法,前者更快

*spb.lock() += 10; //調用weak_ptr轉化為shared_ptr后再操作裸指針

}

cout << *spa << endl; //20

智能指針原理

看到這里,智能指針的用法基本介紹完了,后面筆者來粗淺地分析一下為什么智能指針可以有效幫我們管理裸指針的生命周期。

使用棧對象管理堆對象

在C++中,內存會分為三部分,堆、棧和靜態(tài)存儲區(qū),靜態(tài)存儲區(qū)會存放全局變量和靜態(tài)變量,在程序加載時就初始化,而堆是由程序員自行分配,自行釋放的,例如我們使用裸指針分配的內存;而最后棧是系統(tǒng)幫我們分配的,所以也會幫我們自動回收。因此,智能指針就是利用這一性質,通過一個棧上的對象(shared_ptr或unique_ptr)來管理一個堆上的對象(裸指針),在shared_ptr或unique_ptr的析構函數(shù)中判斷當前裸指針的引用計數(shù)情況來決定是否釋放裸指針。

shared_ptr引用計數(shù)的原理

一開始筆者以為引用計數(shù)是放在shared_ptr這個模板類中,但是細想了一下,如果這樣將shared_ptr賦值給另一個shared_ptr時,是怎么做到兩個shared_ptr的引用計數(shù)同時加1呢,讓等號兩邊的shared_ptr中的引用計數(shù)同時加1?不對,如果還有第二個shared_ptr再賦值給第三個shared_ptr那怎么辦呢?或許通過下面的類圖便清楚個中奧秘。

[ boost中shared_ptr與weak_ptr類圖 ]

我們重點關注shared_ptr<T>的類圖,它就是我們可以直接操作的類,這里面包含裸指針T*,還有一個shared_count的對象,而shared_count對象還不是最終的引用計數(shù),它只是包含了一個指向sp_counted_base的指針,這應該就是真正存放引用計數(shù)的地方,包括強應用計數(shù)和弱引用計數(shù),而且shared_count中包含的是sp_counted_base的指針,不是對象,這也就意味著假如shared_ptr<T> a = b,那么a和b底層pi_指針指向的是同一個sp_counted_base對象,這就很容易做到多個shared_ptr的引用計數(shù)永遠保持一致了。

多線程安全

本章所說的線程安全有兩種情況:

多個線程操作多個不同的shared_ptr對象

C++11中聲明了shared_ptr的計數(shù)操作具有原子性,不管是賦值導致計數(shù)增加還是釋放導致計數(shù)減少,都是原子性的,這個可以參考sp_counted_base的源碼,因此,基于這個特性,假如有多個shared_ptr共同管理一個裸指針,那么多個線程分別通過不同的shared_ptr進行操作是線程安全的。

多個線程操作同一個shared_ptr對象

同樣的道理,既然C++11只負責sp_counted_base的原子性,那么shared_ptr本身就沒有保證線程安全了,加入兩個線程同時訪問同一個shared_ptr對象,一個進行釋放(reset),另一個讀取裸指針的值,那么最后的結果就不確定了,很有可能發(fā)生野指針訪問crash。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • C++裸指針的內存問題有:1、空懸指針/野指針2、重復釋放3、內存泄漏4、不配對的申請與釋放 使用智能指針可以有效...
    WalkeR_ZG閱讀 3,114評論 0 5
  • 原作者:Babu_Abdulsalam 本文翻譯自CodeProject,轉載請注明出處。 引入### Ooops...
    卡巴拉的樹閱讀 30,129評論 13 74
  • C#、Java、python和go等語言中都有垃圾自動回收機制,在對象失去引用的時候自動回收,而且基本上沒有指針的...
    StormZhu閱讀 3,751評論 1 15
  • 一人之下里面人物都是很有背景的,連寶寶都是有人罩著的。寶寶還是史詩級怪物、一開始還不知道她的年齡、現(xiàn)在為她超強的年...
    90后人脈圈閱讀 1,361評論 0 0
  • 序 天和年間,平復王朝內亂之時,將士一敗涂地,后回鶻可汗應朝廷之請,派王子帶兵,三次助王朝太子平亂,收復失地。后太...
    天心啊閱讀 1,233評論 3 6