1. 程式人生 > >C++智能指針

C++智能指針

ring 標準 [] image space 創建 配對 優點 object

智能指針模板類

智能指針是行為類似於指針的類對象,但這種對象還有其他功能。本文章介紹三個可幫助管理動態內存分配的只能指針模板。先來看需要哪些功能以及這些功能是如何實現的。請看下面的函數

void remodel(std::string & str)

{

std::string *ps = new std::string(str);

...

str = ps;

return ;

}

你可能發現了其中的缺陷。每當調用時,該函數都分配堆中的內存,,但從不回收,從而導致內存泄露。你可能也知道解決之道------只要別忘了在return語句前添加下面的語句,以釋放分配的內存即可:

delete ps;

然而,但凡涉及“別忘了”的解決方法,很少是最佳的。因為你有時可能忘了,有時可能記住了,但可能在不經意間刪除或註釋了這些代碼。即使確實沒有忘記,也可能有問題。請看下面的變體:

void remodel(std::string & str)

{

std::string *ps = new std::string(str);

...

if (weird_thing())

{

throw exception();

}

str = *ps;

delete ps;

return;

}

當出現異常時,delete將不被執行,因此也將導致內存泄露。

當remodel()這樣的函數終止(不管是正常終止,還是由於出現了異常而終止),本地變量都將從棧內存中刪除------因此指針ps占據的內存將被釋放。如果ps指向的內存也被釋放,那該有多好啊。如果ps有一個析構函數,該析構函數在ps過期時釋放他指向的內存。因此,ps的問題在於,它只是一個常規指針,不是有析構函數的類對象。如果它是對象,則可以在對象過期時,讓它的析構函數刪除指向的內存。這正是auto_ptr、unique_ptr、shared_ptr背後的思想。模板auto_ptr是C++98提供的解決方案,C++已將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了多年同時,如果您的編譯器不支持其他兩種解決方案,auto_ptr將是唯一的選擇。

使用智能指針

這三個智能指針模板(auto_ptr,unique_ptr,shared_ptr)都定義了類似指針的對象,可以將new獲得(直接或間接)的地址賦給這個對象。當智能指針過期時,其析構函數將使用delete來釋放內存。因此,如果將new返回的地址賦給這些對象,將無需記住稍後釋放這些內存;在智能指針過期時,這些內存將自動被釋放。下圖說明了auto_ptr和常規指針在行為方面的差別;shared_ptr和unique_ptr的行為與auto_ptr相同。

要創建智能指針對象,必須包含頭文件memory,該文件模板定義。然後使用通常的模板語法來實例化所需類型的指針。例如,模板auto_ptr包含如下構造函數:

template<class X>

class auto_ptr

{

explicit auto_ptr(X *p = 0)throw();

...

};

throw()意味這構造函數不會引發異常:與auto_ptr一樣,throw()也被摒棄。因此,請求X類型的auto_ptr將獲得一個指向X類型的auto_ptr:

auto_ptr<double>pd(new double);//pd an auto_ptr to double

//(use in place of double *pd)

auto_ptr<string>ps(new string);//ps an auto_ptr to string

//(use in place of string *ps)

new double是new返回的指針,指向新分配的內存塊。它是構造函數auto_ptr<double>的參數,即對應於原型中形參p的實參。同樣,new string也是構造函數的實參。其他兩種智能指針使用同樣的語法:

unique_ptr<double> pdu(new double);//pdu an unique_ptr to double

shared_ptr<string> pss(new string);//pss a shared_ptr to string

因此。要轉換remodel()函數,應按下面3個步驟進行:

  1. 包含頭文件memory;
  2. 將指向string的指針替換為指向string的智能指針對象;
  3. 刪除delete語句。

下面是使用auto_ptr修改該函數的結果:

#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 ;

}

註意到智能指針模板位於命名空間std中。下面是一個簡單的程序,演示了如何使用全部三種智能指針。每個智能指針都放在一個代碼塊內,這樣離開代碼塊時,指針將過期。Report類使用方法報告對象的創建和銷毀。

//smrtptrs.cpp -- using three kinds of smart pointers

//requires support of C++11 shared_ptr and unique_ptr

#include <iostream>

#include <string>

#include <memory>

using namespace std;

class Report

{

public:

Report(const string s) : str(s)

{

cout << "Object created!" << endl;

}

~Report()

{

cout << "Object deleted!" << endl;

}

void comment() const

{

cout << str << endl;

}

private:

string str;

};

int main()

{

{

auto_ptr<Report> ps(new Report("using auto_ptr"));

ps->comment();

}

{

shared_ptr<Report> ps(new Report("using shared_ptr"));

ps->comment();

}

{

unique_ptr<Report> ps(new Report("using unique_ptr"));

ps->comment();

}

system("pause");

return 0;

}

程序輸出結果:

技術分享

所有智能指針類都一個explicit構造函數,該構造函數將指針作為參數。因此不需要自動將指針轉換為智能指針對象:

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)

由於智能指針模板類的定義方式,智能指針對象的很多方面都類似於常規指針。例如,如果ps是一個智能指針對象,則可以對它執行解除引用操作(*ps)、用它來訪問結構成員(ps->puffIndex)、將他賦給指向相同類型的常規指針,還可以將智能指針對象賦給另一個同類型智能指針對象,但將引起一個問題,這將在下面進行討論。

但在此之前,先說說對全部三種智能指針都應避免的一點:

string vacation(“I wandered lonely as a cloud”);

shared_ptr<string> pvac(&vacation);//NO;

技術分享

pvac過期時,程序將把delete運算符用於非堆內存,這是錯誤的。

有關智能指針的註意事項

先來看下面的賦值語句:

auto_ptr<string> ps(new string(“I reigned lonely as a cloud”));

auto_ptr<string> vocation;

vocation = ps;

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

l 定義賦值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本。

l 建立所有權(ownership)概念,對於特定的對象,只能有一個智能指針可擁有它,這樣只有擁有和unique_ptr的策略,但unique_ptr的策略更嚴格。

l 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數(reference counting)。這是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 Run")),

auto_ptr<string> (new string("Turkey Errors")),

auto_ptr<string> (new string("Goose Eggs"))

};

auto_ptr<string> pwin;

pwin = films[2];

cout << "The nominees for best avian baseball film are \n";

for (int i = 0; i < 5; i++)

{

cout << *films[i] << endl;

}

cout << "The winner is " << *pwin << endl;

cin.get();

return 0;

}

該程序的輸出:

技術分享

這裏的問題在於,下面的語句所有權從film[2]轉讓給pwin:

pwin = films[2];//films[2] loses ownership

這導致films[2]不再引用該字符串。在auto_ptr放棄對象的所有權後,便可能使用它來訪問該對象。當程序打印films[2]指向的字符串時,卻發現這是一個空指針,這顯然討厭的意外。

如果在上面程序中使用shared_ptr代替auto_ptr,則程序將正常運行,其輸出如下:

技術分享

這次pwin和films[2]指向同一個對象,而引用計數從1增加到2。在程序末尾,後申明的pwin首先調用其析構函數,該析構函數將引用技術降低到1.然後,shared_ptr數組的成員被釋放,對filmsp[2]調用析構函數時,將引用計數降低到0,並釋放以前分配的空間。

如果使用unique_ptr結果會怎麽樣呢?unique_ptr也采用所有權模型,但使用unique_ptr時。程序不會等到運行階段崩潰,而是出現語法錯誤,如下

pwin = films[2];

技術分享

unique_ptr為何優於auto_ptr

請看下面的語句:

auto_ptr<string> p1(new string("auto"));//#1

auto_ptr<string> p2;//#2

p2 = p1;//#3

在語句#3中,p2接管string對象的所有權後,p1的所有權被剝奪。前面說過,這是件好事,可防止p1和p2的析構函數試圖刪除同一個對象;但如果程序隨後試圖使用p1,這將是件壞事,因為p1不再指向有效的數據。

下面來看使用unique_ptr的情況:

unique_ptr<string> p3(new string("auto"));//#4

unique_ptr<string> p4;//#5

p3 = p4;//#6

編譯器認為語句#6非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全(編譯階段錯誤比潛在的程序崩潰更安全)

但有時候,有一個智能指針賦給另一個並不會留下危險的懸掛指針。假設有如下函數定義:

unique_ptr<string> demo(const char *s)

{

unique_ptr<string> temp(new string(s));

return temp;

}

並假設編寫了如下代碼:

unique_ptr<string> ps;

ps = demo("Uniquely special");

demo()返回一個臨時unique_ptr,然後ps接管了原本歸返回的unique_ptr所有的對象,而返回的unique_ptr被銷毀。這沒有問題,因為ps擁有了string對象的所有權。但這裏的另一個好處是,demo()返回的臨時unique_ptr很快被銷毀,沒有機會使用它來訪問無效的數據。換句話說,沒有理由禁止這種賦值。神奇的是,編譯器確實允許這種賦值!

總之,程序試圖講一個unique_ptr賦給另一個時,如果源unique_ptr是一個臨時右值,編譯器允許這樣做;如果源unique_ptr將存在一段時間,編譯器將禁止這樣做

unique_ptr<string> pu1(new string("Hi ho!"));

unique_ptr<string> pu2;

pu2 = pu1; //#1 not allowed

unique_ptr<string> pu3;

pu3 = unique_ptr<string>(new string("Yo!")); //#2 allowed

語句#1將留下懸掛的unique_ptr(pu1),這可能導致危害。語句#2不會留下的unique_ptr,因為它調用unique_ptr的構造函數,該構造函數創建的臨時對象在其所有權轉讓給pu3後就會被銷毀。這種隋情況而異的行為表明,unique_ptr優於允許兩種賦值的auto_ptr。這也是禁止(只是一個建議,編譯器並不禁止)在容器對象中使用auto_ptr,但允許使用unique_ptr的原因如果容器算法試圖對包含unique_ptr的容器執行類似於語句#1的操作,將導致編譯錯誤;如果算法試圖執行類似於語句#2的操作,則不會有任何問題,而對於auto_ptr,類似於語句#1的操作可能導致不確定的行為和神秘的崩潰。

當然,你可能確實想執行類似於語句#1的操作。僅當以非智能的方式使用遺棄的智能指針(如解除引用時),這種賦值不安全。要安全重入這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:

unique_ptr<string> ps1, ps2;

ps1 = demo("Uniquely special");

ps2 = move(ps1);

ps1 = demo(" and more");

cout << *ps2 << *ps1 << endl;

相比於auto_ptr,unique_ptr還有一個另一個優點。它有一個可用於數組的變體。別忘了,必須將delete和new配對,將delete []和new []配對。模板auto_ptr使用delete而不是delete[],因此只能與new一起使用,而不能與new[]一起使用。但unique_ptr有使用new []和delete []的版本:

unique_ptr<double []>pda(new double(5));//will use delete []

警告:使用new分配內存時,才能使用auto_ptr和shared_ptr,使用new []分配內存時,不能使用它們,不使用new分配內存時,不能使用auto_ptr或shared_ptr;不適用new []分配內存時,不能使用unique_ptr。

C++智能指針