1. 程式人生 > >C++_智能指針模板類

C++_智能指針模板類

另一個 temp 運算符 func 避免 執行 它的 移動構造函數 stl算法

智能指針是行為類似於指針的類對象,但這種對象還有其他功能。

本節介紹三個可幫助管理動態內存分配的智能指針模板auto_ptrunique_ptrshared_ptr)。

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 是C++98提供的解決方案,但被C++11所摒棄;

C++提供了另外兩種解決方案unique_ptr和shared_ptr;

=========================================

一、使用智能指針

三個智能指針模板(auto_ptr、unique_ptr和shared_ptr)都定義了類似指針的對象,可以將new獲得的地址(直接或間接)賦給這種對象。

當智能指針過期時,其析構函數將使用delete來釋放內存。

因此如果將new返回的地址賦給這些對象,將無需記住稍後釋放這些內存:在智能指針過期時,這些內存將自動釋放。

要創建智能指針對象,必須包含頭文件memory,該文件模板定義。

然後使用通常的模板語法來實例化所需的類型指針。

例如:模板auto_ptr包含如下構造函數:

template <class X> class auto_ptr {

public:

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

...

};

auto_ptr<double> pd (new double); //pd是auto_ptr to double,代替了double * pd

auto_ptr<string> ps (new string); //ps是auto_ptr to string,代替了 string * ps

其中的new double是new返回的指針,指向新分配的內存塊。它是構造函數auto_ptr<double>的參數;

因此,要轉換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名稱空間中,接下來的程序演示了如何使用全部三種指針。

 1 //smrtptrs.cpp -- using three kinds of smart pointers
 2 //requires support of C++11 shared_ptr and unique_ptr
 3 
 4 #include <iostream>
 5 #include <string>
 6 #include <memory>
 7 
 8 class Report
 9 {
10 private:
11     std::string str;    
12 public:
13     Report (const std::string s):str(s)
14                {std::cout<<"Object created!\n";}
15     ~Report() {std::cout<<"Object deleted!\n";}
16     void comment() const {std::cout<<str<<"\n";}  
17 };
18 
19 int main()
20 {
21 {
22     std::auto_ptr<Report> ps (new Report("using auto_ptr"));
23     ps -> comment();  //use -> to invoke a member function
24 }
25 {
26     std::shared_ptr<Report> ps (new Report("using shared_ptr"));
27     ps -> comment();
28 }
29 {
30     std::unique_ptr<Report> ps (new Report("using unique_ptr"));
31     ps->comment();
32 }
33     return 0;
34 }

所有智能指針類都有一個explicit構造函數,該構造函數將指針作為參數。因此不需要自動將指針轉換為智能指針對象://explicit直言的、不隱瞞的;//implicit不言明的、含蓄的

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呢?首先看下下列語句:

auto_ptr<string> ps (new string("I reigned lonely as cloud."));

auto_ptr<string> vocation;

vocation = ps;

這兩個指針將指向同一個string對象,這是不能接受的。因為程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。

要避免這種問題,有很多方法:

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

2、建立所有權(ownership)概念,對特定的對象,只能有一個智能指針可擁有它,這樣只有擁有對象的智能指針的析構函數會刪除該對象。然後,讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr的策略,但unique_ptr的策略更嚴格。

3、創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱之為引用計數(reference counting)。例如,賦值時,計數將加1,指針過期時,計數將減1。僅當最後一個指針過期時,才調用delete。這是shared_ptr采用的策略。

接下來是一個不適合使用auto_ptr的例子:

 1 //fowl.cpp  -- auto_ptr a poor choice
 2 #include <iostream>
 3 #include <string>
 4 #include <memory>
 5 
 6 int main()
 7 {
 8     using namespace std;
 9     auto_ptr<string> films[5] =
10     {
11         auto_ptr<string> (new string("Fowl Balls")),
12         auto_ptr<string> (new string("Duck Walks")),
13         auto_ptr<string> (new string("Chicken Runs")),
14         auto_ptr<string> (new string("Goose Eggs")),
15         auto_ptr<string> (new string("Turkey Errors"))
16     };
17     auto_ptr<string> pwin;
18     pwin = films[2];
19 
20     cout << "The nominees for best avian basketball film are\n";
21     for (int i = 0; i < 5; i++)
22         cout<<*films[i]<<endl;
23     cout << "The winner is "<< * pwin<<"!\n";
24     cin.get();
25     return 0;  
26 }

下面是程序的輸出:

The nominees for best avian baseball film are

Fowl Balls

Duck Walks

Segmentation fault(core dumped)

消息core dumped 表明,錯誤地使用auto_ptr可能導致問題。問題在於,下面的語句將所有權從film[2]轉讓給pwin:

pwn = films[2];

當film[2]不再引用該字符串,在auto_ptr放棄對象的所有權後,但有可能會使用它來訪問該對象。

當程序打印film[2]指向的字符串時,卻發現這是一個空指針,這顯然是討厭的意外。

如果程序使用shared_ptr代替auto_ptr,則程序將正常運行。這次pwinhefilms[2]指向同一個對象,而引用計數從1增加到2。

在程序末尾,後聲明的pwin將首先調用其析構函數,該析構函數將引用計數降低到1。

然後,shared_ptr數組的成員被釋放,對film[2]調用析構函數,將引用計數降低到0,並釋放以前分配的空間。

因此使用shared_ptr,可以保證程序正常運行;使用auto_ptr會導致程序在運行階段崩潰。

如果使用unique_ptr,結果將如何呢?與auto_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

p4 = p3; //#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比auto_ptr好用

using namespace std;

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指針,這將導致危害。

語句2不會留下懸掛的unique_ptr指針,因為它調用unique_ptr的構造函數,該構造函數創建的臨時對象在其所有權轉讓給pu3後,會被銷毀。

這種隨情況而異的行為表明,unique_ptr優於auto_ptr。

//下面這段更加晦澀

如果我們確實想執行類型#1的語句那樣的操作的話。

僅當以非智能的方式使用遺棄的智能指針,這種賦值才不安全。

要安全地重用這種指針,可給它賦新值。

C++有一個標準庫函數std::move(),讓您能夠將一個unique_ptr賦給另一個。

using namespace std;

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[]的版本。

=========================================

四、選擇智能指針

應該如何選用只能指針?

如果程序要使用多個指向同一個對象的指針,應選擇share_ptr。

這樣的情況包括:有一個數組,並使用一些輔助指針來標識特定的元素,如最大的元素和最小的元素;

兩個對象都包含指向第三個對象的指針;STL容器包含指針。

很多STL算法都支持賦值和復制操作。這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出警告)和auto_ptr(行為不確定)。

如果您的編譯器沒有支持shared_ptr,可使用Boost庫提供的shared_ptr。

如果程序不需要多個指向同一個對象的指針,則可以使用unique_ptr。

如果函數使用new分配內存,並返回指向該內存的指針,將其返回類型聲明為unique_ptr是不錯的選擇。

這樣所有權將轉讓給接受返回值的unique_ptr,而該指針將負責調用delete。

如果編譯器沒有提供unique_ptr,可考慮使用BOOST庫提供的scoped_ptr,它與unique_ptr類似。

C++_智能指針模板類