C++智能指針簡(jiǎn)單剖析

導(dǎo)讀##

最近在補(bǔ)看《C++ Primer Plus》第六版,這的確是本好書,其中關(guān)于智能指針的章節(jié)解析的非常清晰,一解我以前的多處困惑。C++面試過(guò)程中,很多面試官都喜歡問(wèn)智能指針相關(guān)的問(wèn)題,比如你知道哪些智能指針?shared_ptr的設(shè)計(jì)原理是什么?如果讓你自己設(shè)計(jì)一個(gè)智能指針,你如何完成?等等……。而且在看開源的C++項(xiàng)目時(shí),也能隨處看到智能指針的影子。這說(shuō)明智能指針不僅是面試官愛問(wèn)的題材,更是非常有實(shí)用價(jià)值。

下面是我在看智能指針時(shí)所做的筆記,希望能夠解決你對(duì)智能指針的一些困擾。

目錄##

  1. 智能指針背后的設(shè)計(jì)思想
  2. C++智能指針簡(jiǎn)單介紹
  3. 為什么摒棄auto_ptr?
  4. unique_ptr為何優(yōu)于auto_ptr?
  5. 如何選擇智能指針?

正文##

1. 智能指針背后的設(shè)計(jì)思想####

我們先來(lái)看一個(gè)簡(jiǎn)單的例子:

void remodel(std::string & str)
{
    std::string * ps = new std::string(str);
    ...
    if (weird_thing())
        throw exception();
    str = *ps; 
    delete ps;
    return;
}

當(dāng)出現(xiàn)異常時(shí)(weird_thing()返回true),delete將不被執(zhí)行,因此將導(dǎo)致內(nèi)存泄露。
如何避免這種問(wèn)題?有人會(huì)說(shuō),這還不簡(jiǎn)單,直接在<code>throw exception();</code>之前加上<code>delete ps;</code>不就行了。是的,你本應(yīng)如此,問(wèn)題是很多人都會(huì)忘記在適當(dāng)?shù)牡胤郊由蟙elete語(yǔ)句(連上述代碼中最后的那句delete語(yǔ)句也會(huì)有很多人忘記吧),如果你要對(duì)一個(gè)龐大的工程進(jìn)行review,看是否有這種潛在的內(nèi)存泄露問(wèn)題,那就是一場(chǎng)災(zāi)難!
這時(shí)我們會(huì)想:當(dāng)remodel這樣的函數(shù)終止(不管是正常終止,還是由于出現(xiàn)了異常而終止),本地變量都將自動(dòng)從棧內(nèi)存中刪除—因此指針ps占據(jù)的內(nèi)存將被釋放,如果ps指向的內(nèi)存也被自動(dòng)釋放,那該有多好啊。
我們知道析構(gòu)函數(shù)有這個(gè)功能。如果ps有一個(gè)析構(gòu)函數(shù),該析構(gòu)函數(shù)將在ps過(guò)期時(shí)自動(dòng)釋放它指向的內(nèi)存。但ps的問(wèn)題在于,它只是一個(gè)常規(guī)指針,不是有析構(gòu)凼數(shù)的類對(duì)象指針。如果它指向的是對(duì)象,則可以在對(duì)象過(guò)期時(shí),讓它的析構(gòu)函數(shù)刪除指向的內(nèi)存。

這正是 auto_ptr、unique_ptr和shared_ptr這幾個(gè)智能指針背后的設(shè)計(jì)思想。我簡(jiǎn)單的總結(jié)下就是:將基本類型指針封裝為類對(duì)象指針(這個(gè)類肯定是個(gè)模板,以適應(yīng)不同基本類型的需求),并在析構(gòu)函數(shù)里編寫delete語(yǔ)句刪除指針指向的內(nèi)存空間。

因此,要轉(zhuǎn)換remodel()函數(shù),應(yīng)按下面3個(gè)步驟進(jìn)行:

  • 包含頭義件memory(智能指針?biāo)诘念^文件);
  • 將指向string的指針替換為指向string的智能指針對(duì)象;
  • 刪除delete語(yǔ)句。

下面是使用auto_ptr修改該函數(shù)的結(jié)果:

# include <memory>
void remodel (std::string & str)
{
    std::auto_ptr<std::string> ps (new std::string(str));
    ...
    if (weird_thing ())
        throw exception(); 
    str = *ps; 
    // delete ps; NO LONGER NEEDED
    return;
}

2. C++智能指針簡(jiǎn)單介紹####

STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。
模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,并提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時(shí),如果您的編譯器不支持其他兩種解決力案,auto_ptr將是唯一的選擇。

使用注意點(diǎn)

  • 所有的智能指針類都有一個(gè)explicit構(gòu)造函數(shù),以指針作為參數(shù)。比如auto_ptr的類模板原型為:
templet<class T>
class auto_ptr {
    explicit auto_ptr(X* p = 0) ; 
    ...
};

因此不能自動(dòng)將指針轉(zhuǎn)換為智能指針對(duì)象,必須顯式調(diào)用:

shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)
  • 對(duì)全部三種智能指針都應(yīng)避免的一點(diǎn):
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No

pvac過(guò)期時(shí),程序?qū)裠elete運(yùn)算符用于非堆內(nèi)存,這是錯(cuò)誤的。

使用舉例

#include <iostream>
#include <string>
#include <memory>

class report
{
private:
    std::string str;
public:
 report(const std::string s) : str(s) {
  std::cout << "Object created.\n";
 }
 ~report() {
  std::cout << "Object deleted.\n";
 }
 void comment() const {
  std::cout << str << "\n";
 }
};

int main() {
 {
  std::auto_ptr<report> ps(new report("using auto ptr"));
  ps->comment();
 }

 {
  std::shared_ptr<report> ps(new report("using shared ptr"));
  ps->comment();
 }

 {
  std::unique_ptr<report> ps(new report("using unique ptr"));
  ps->comment();
 }
 return 0;
}

3. 為什么摒棄auto_ptr?####

先來(lái)看下面的賦值語(yǔ)句:

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

上述賦值語(yǔ)句將完成什么工作呢?如果ps和vocation是常規(guī)指針,則兩個(gè)指針將指向同一個(gè)string對(duì)象。這是不能接受的,因?yàn)槌绦驅(qū)⒃噲D刪除同一個(gè)對(duì)象兩次——一次是ps過(guò)期時(shí),另一次是vocation過(guò)期時(shí)。要避免這種問(wèn)題,方法有多種:

  • 定義陚值運(yùn)算符,使之執(zhí)行深復(fù)制。這樣兩個(gè)指針將指向不同的對(duì)象,其中的一個(gè)對(duì)象是另一個(gè)對(duì)象的副本,缺點(diǎn)是浪費(fèi)空間,所以智能指針都未采用此方案。
  • 建立所有權(quán)(ownership)概念。對(duì)于特定的對(duì)象,只能有一個(gè)智能指針可擁有,這樣只有擁有對(duì)象的智能指針的構(gòu)造函數(shù)會(huì)刪除該對(duì)象。然后讓賦值操作轉(zhuǎn)讓所有權(quán)。這就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴(yán)格。
  • 創(chuàng)建智能更高的指針,跟蹤引用特定對(duì)象的智能指針數(shù)。這稱為引用計(jì)數(shù)。例如,賦值時(shí),計(jì)數(shù)將加1,而指針過(guò)期時(shí),計(jì)數(shù)將減1,。當(dāng)減為0時(shí)才調(diào)用delete。這是shared_ptr采用的策略。

當(dāng)然,同樣的策略也適用于復(fù)制構(gòu)造函數(shù)。
每種方法都有其用途,但為何說(shuō)要摒棄auto_ptr呢?
下面舉個(gè)例子來(lái)說(shuō)明。

#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. 將所有權(quán)從films[2]轉(zhuǎn)讓給pwin,此時(shí)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;
}

運(yùn)行下發(fā)現(xiàn)程序崩潰了,原因在上面注釋已經(jīng)說(shuō)的很清楚,films[2]已經(jīng)是空指針了,下面輸出訪問(wèn)空指針當(dāng)然會(huì)崩潰了。但這里如果把a(bǔ)uto_ptr換成shared_ptr或unique_ptr后,程序就不會(huì)崩潰,原因如下:

  • 使用shared_ptr時(shí)運(yùn)行正常,因?yàn)閟hared_ptr采用引用計(jì)數(shù),pwin和films[2]都指向同一塊內(nèi)存,在釋放空間時(shí)因?yàn)槭孪纫袛嘁糜?jì)數(shù)值的大小因此不會(huì)出現(xiàn)多次刪除一個(gè)對(duì)象的錯(cuò)誤。
  • 使用unique_ptr時(shí)編譯出錯(cuò),與auto_ptr一樣,unique_ptr也采用所有權(quán)模型,但在使用unique_ptr時(shí),程序不會(huì)等到運(yùn)行階段崩潰,而在編譯器因下述代碼行出現(xiàn)錯(cuò)誤:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.

指導(dǎo)你發(fā)現(xiàn)潛在的內(nèi)存錯(cuò)誤。

這就是為何要摒棄auto_ptr的原因,一句話總結(jié)就是:避免潛在的內(nèi)存崩潰問(wèn)題。

4. unique_ptr為何優(yōu)于auto_ptr?####

可能大家認(rèn)為前面的例子已經(jīng)說(shuō)明了unique_ptr為何優(yōu)于auto_ptr,也就是安全問(wèn)題,下面再敘述的清晰一點(diǎn)。
請(qǐng)看下面的語(yǔ)句:

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                       //#2
p2 = p1;                                   //#3

在語(yǔ)句#3中,p2接管string對(duì)象的所有權(quán)后,p1的所有權(quán)將被剝奪。前面說(shuō)過(guò),這是好事,可防止p1和p2的析構(gòu)函數(shù)試圖刪同—個(gè)對(duì)象;
但如果程序隨后試圖使用p1,這將是件壞事,因?yàn)閜1不再指向有效的數(shù)據(jù)。

下面來(lái)看使用unique_ptr的情況:

unique_ptr<string> p3 (new string ("auto");   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;                                      //#6

編譯器認(rèn)為語(yǔ)句#6非法,避免了p3不再指向有效數(shù)據(jù)的問(wèn)題。因此,unique_ptr比auto_ptr更安全。

但unique_ptr還有更聰明的地方。
有時(shí)候,會(huì)將一個(gè)智能指針賦給另一個(gè)并不會(huì)留下危險(xiǎn)的懸掛指針。假設(shè)有如下函數(shù)定義:

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s)); 
    return temp;
}

并假設(shè)編寫了如下代碼:

unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一個(gè)臨時(shí)unique_ptr,然后ps接管了原本歸返回的unique_ptr所有的對(duì)象,而返回時(shí)臨時(shí)的 unique_ptr 被銷毀,也就是說(shuō)沒有機(jī)會(huì)使用 unique_ptr 來(lái)訪問(wèn)無(wú)效的數(shù)據(jù),換句話來(lái)說(shuō),這種賦值是不會(huì)出現(xiàn)任何問(wèn)題的,即沒有理由禁止這種賦值。實(shí)際上,編譯器確實(shí)允許這種賦值,這正是unique_ptr更聰明的地方。

總之,黨程序試圖將一個(gè) unique_ptr 賦值給另一個(gè)時(shí),如果源 unique_ptr 是個(gè)臨時(shí)右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時(shí)間,編譯器將禁止這么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下懸掛的unique_ptr(pu1),這可能導(dǎo)致危害。而#2不會(huì)留下懸掛的unique_ptr,因?yàn)樗{(diào)用 unique_ptr 的構(gòu)造函數(shù),該構(gòu)造函數(shù)創(chuàng)建的臨時(shí)對(duì)象在其所有權(quán)讓給 pu3 后就會(huì)被銷毀。這種隨情況而已的行為表明,unique_ptr 優(yōu)于允許兩種賦值的auto_ptr 。

當(dāng)然,您可能確實(shí)想執(zhí)行類似于#1的操作,僅當(dāng)以非智能的方式使用摒棄的智能指針時(shí)(如解除引用時(shí)),這種賦值才不安全。要安全的重用這種指針,可給它賦新值。C++有一個(gè)標(biāo)準(zhǔn)庫(kù)函數(shù)std::move(),讓你能夠?qū)⒁粋€(gè)unique_ptr賦給另一個(gè)。下面是一個(gè)使用前述demo()函數(shù)的例子,該函數(shù)返回一個(gè)unique_ptr<string>對(duì)象:
使用move后,原來(lái)的指針仍轉(zhuǎn)讓所有權(quán)變成空指針,可以對(duì)其重新賦值。

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

5. 如何選擇智能指針?####

在掌握了這幾種智能指針后,大家可能會(huì)想另一個(gè)問(wèn)題:在實(shí)際應(yīng)用中,應(yīng)使用哪種智能指針呢?
下面給出幾個(gè)使用指南。

(1)如果程序要使用多個(gè)指向同一個(gè)對(duì)象的指針,應(yīng)選擇shared_ptr。這樣的情況包括:

  • 有一個(gè)指針數(shù)組,并使用一些輔助指針來(lái)標(biāo)示特定的元素,如最大的元素和最小的元素;
  • 兩個(gè)對(duì)象包含都指向第三個(gè)對(duì)象的指針;
  • STL容器包含指針。很多STL算法都支持復(fù)制和賦值操作,這些操作可用于shared_ptr,但不能用于unique_ptr(編譯器發(fā)出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫(kù)提供的shared_ptr。

(2)如果程序不需要多個(gè)指向同一個(gè)對(duì)象的指針,則可使用unique_ptr。如果函數(shù)使用new分配內(nèi)存,并返還指向該內(nèi)存的指針,將其返回類型聲明為unique_ptr是不錯(cuò)的選擇。這樣,所有權(quán)轉(zhuǎn)讓給接受返回值的unique_ptr,而該智能指針將負(fù)責(zé)調(diào)用delete。可將unique_ptr存儲(chǔ)到STL容器在那個(gè),只要不調(diào)用將一個(gè)unique_ptr復(fù)制或賦給另一個(gè)算法(如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調(diào)用沒有問(wèn)題,因?yàn)樗祷匾粋€(gè)臨時(shí)unique_ptr,該unique_ptr被賦給vp中的一個(gè)unique_ptr。另外,如果按值而不是按引用給show()傳遞對(duì)象,for_each()將非法,因?yàn)檫@將導(dǎo)致使用一個(gè)來(lái)自vp的非臨時(shí)unique_ptr初始化pi,而這是不允許的。前面說(shuō)過(guò),編譯器將發(fā)現(xiàn)錯(cuò)誤使用unique_ptr的企圖。
在unique_ptr為右值時(shí),可將其賦給shared_ptr,這與將一個(gè)unique_ptr賦給一個(gè)需要滿足的條件相同。與前面一樣,在下面的代碼中,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包含一個(gè)顯式構(gòu)造函數(shù),可用于將右值unique_ptr轉(zhuǎn)換為shared_ptr。shared_ptr將接管原來(lái)歸unique_ptr所有的對(duì)象。
在滿足unique_ptr要求的條件時(shí),也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫(kù)提供的scoped_ptr,它與unique_ptr類似。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. 什么是智能指針? 智能指針是行為類似于指針的類對(duì)象,但這種對(duì)象還有其他功能。 2. 為什么設(shè)計(jì)智能指針? 引...
    MinoyJet閱讀 647評(píng)論 0 1
  • 原作者:Babu_Abdulsalam 本文翻譯自CodeProject,轉(zhuǎn)載請(qǐng)注明出處。 引入### Ooops...
    卡巴拉的樹閱讀 30,182評(píng)論 13 74
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,541評(píng)論 1 51
  • C++智能指針 原文鏈接:http://blog.csdn.net/xiaohu2022/article/deta...
    小白將閱讀 6,901評(píng)論 2 21
  • 12.1 智能指針 智能指針行為類似普通指針,但它負(fù)責(zé)自動(dòng)釋放所知的對(duì)象。 #include <memory> s...
    龍遁流閱讀 373評(píng)論 0 1