1. 程式人生 > >C++智慧指標與類模板

C++智慧指標與類模板

開發十年,就只剩下這套架構體系了! >>>   

智慧指標是行為類似於指標的類物件,所有的智慧指標都會過載 -> 和 * 操作符。智慧指標還有許多其他功能,比較有用的是自動銷燬。這主要是利用棧物件的有限作用域以及臨時物件(有限作用域實現)解構函式釋放記憶體。當然,智慧指標還不止這些,還包括複製時可以修改源物件等。智慧指標根據需求不同,設計也不同(寫時複製,賦值即釋放物件擁有許可權、引用計數等,控制權轉移等)。智慧指標是儲存指向動態分配(堆)物件指標的類,用於生存期控制,能夠確保自動正確的銷燬動態分配的物件,防止記憶體洩露。 智慧指標類將一個計數器與類指向的物件相關聯,引用計數跟蹤該類有多少個物件共享同一指標。每次建立類的新物件時,初始化指標並將引用計數置為1;當物件作為另一物件的副本而建立時,拷貝建構函式拷貝指標並增加與之相應的引用計數;對一個物件進行賦值時,賦值操作符減少左運算元所指物件的引用計數(如果引用計數為減至0,則刪除物件),並增加右運算元所指物件的引用計數;呼叫解構函式時,建構函式減少引用計數(如果引用計數減至0,則刪除基礎物件)。 本文主要介紹3個可幫助管理動態記憶體分配的智慧指標模板(auto_ptr、unique_ptr和shared_ptr)。

目錄

1、智慧指標設計思想

2、智慧指標簡單介紹

3、為何摒棄auto_ptr

4、unique_ptr為何優於auto_ptr

5、如何選擇智慧指標

正文

1、智慧指標設計思想

先來看一下需要哪些功能以及這些功能是如何實現的。請看下面的函式:

?

1

2

3

4

5

6

7

void remodel(std::string & str)

{

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

    

...

    str = ps;

    return;

}

 每當呼叫時,該函式都分配堆中的記憶體,但從不收回,從而導致記憶體洩漏。此時只要不忘記在return前新增下面語句釋放記憶體即可:

?

1

delete ps ;

 然而,請大家再看一個例子:

?

1

2

3

4

5

6

7

8

9

10

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)背後的思想

按以下3個步驟用智慧指標模板auto_ptr進行轉換remodel()函式:

  • 包含頭義件memory(智慧指標所在的標頭檔案);
  • 將指向string的指標替換為指向string的智慧指標物件;
  • 刪除delete語句。

?

1

2

3

4

5

6

7

8

9

10

11

12

#include <memory>

using namespace std;

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、智慧指標簡單介紹

在介紹之前先看一個簡單程式,再對智慧指標作一個簡單的介紹。該程式演示瞭如何使用3種智慧指標。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

//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{

private:

    string str;

public:

    Report(const string s):str(s){

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

    }

    ~Report(){

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

    }

    void comment() const{

        cout << str << endl;

    }

};

 

int main(){

    {

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

        ps->comment();

    }

    {

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

        ps->comment();

    }

    {

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

        ps->comment();

    }

    return 0;

}

執行結果:

?

1

2

3

4

5

6

7

8

9

Object created !

using auto_ptr

Object deleted !

Object created !

using unique_ptr

Object deleted !

Object created !

using shared_ptr

Object deleted !

現在來分析一下這三個智慧指標模板(auto_ptr、unique_ptr和shared_ptr)。

我們可以將new獲得(直接或者間接)的地址賦給這種物件,當智慧指標過期時,其解構函式將使用delete來釋放記憶體。

要建立智慧指標物件,必須包含檔案memory,該檔案含有智慧指標模板的定義,然後使用通常的模板語法來例項化所需型別的指標。所有的智慧指標類都有一個explicit建構函式,例如,其中智慧指標auto_ptr包含如下建構函式:

?

1

2

3

4

5

templet<class T>class auto_ptr {

public:

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

        ...

};

因此,請求X型別的auto_ptr將獲得一個指向X型別的auto_ptr:

?

1

2

3

4

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

// use in place of double *pd

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

// use in place of double *string

new double 是new 返回的指標,指向新分配的記憶體塊,它是auto_ptr<double>的引數,即對應於原型中形參p的實參。同樣,new string也是建構函式的實參。另外兩種智慧指標使用同樣的語法:

?

1

2

unique_ptr<double> pd(new double);

shared_ptr<double> pd(new double);

但是要注意的是,三種智慧指標都應該避免一點:

?

1

2

string vacation("I wandered lonely as a cloud.");

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

pvac過期時,程式將把delete運算子用於非堆記憶體,這是錯誤的。

3、為何摒棄auto_ptr

先來看下面的賦值語句:

?

1

2

3

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

auto_ptr<string>  vocation;

vocaticn = ps;

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

  • 定義陚值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採用此方案。

  • 建立所有權(ownership)概念。對於特定的物件,只能有一個智慧指標可擁有,這樣只有擁有物件的智慧指標的建構函式會刪除該物件。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。

  • 建立智慧更高的指標,跟蹤引用特定物件的智慧指標數。這稱為引用計數。例如,賦值時,計數將加1,而指標過期時,計數將減1,。當減為0時才呼叫delete。這是shared_ptr採用的策略。

當然,同樣的策略也適用於複製建構函式。

每種方法都有其用途,但為何說要摒棄auto_ptr呢?

先看一個例子:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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

}

該程式執行結果是崩潰了,但是,如果把auto_ptr<string> pwin;改成shared_ptr<string> pwin;編譯後執行結果卻正常了。如果把auto_ptr<string> pwin;改成unique_ptr<string> pwin;編譯語句pwin = films[2];時不能通過。

原因如下:

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

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

4、unique_ptr為何優於auto_ptr

可能大家認為前面的例子已經說明了unique_ptr為何優於auto_ptr,也就是安全問題,下面再敘述的清晰一點。請看下面的語句:

?

1

2

3

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的情況:

?

1

2

3

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

unique_ptr<string> p4;                       //#5

p4 = p3;                                      //#6

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

有時候,會將一個智慧指標賦給另一個並不會留下危險的懸掛指標。假設有如下函式定義:

?

1

2

3

4

5

unique_ptr<string> demo(const char * s)

{

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

    return temp;

}

並假設編寫了如下程式碼:

?

1

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

demo() 返回一個臨時unique_ptr,然後ps接管了原本歸返回的unique_ptr所有的物件,而返回時臨時的 unique_ptr 被銷燬,也就是說沒有機會使用 unique_ptr 來訪問無效的資料,換句話來說,這種賦值是不會出現任何問題的,即沒有理由禁止這種賦值。實際上,編譯器確實允許這種賦值。

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

?

1

2

3

4

unique_ptr<string> pu1(new string ("hello world"));

unique_ptr<string> pu2;_x000D_pu2 = pu1;          // #1 not allowed

unique_ptr<string> pu3;

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

其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它呼叫 unique_ptr 的建構函式,該建構函式建立的臨時物件在其所有權讓給 pu3 後就會被銷燬。這種隨情況而已的行為表明,unique_ptr 優於允許兩種賦值的auto_ptr 。

當然,您可能確實想執行類似於#1的操作,僅當以非智慧的方式使用摒棄的智慧指標時(如解除引用時),這種賦值才不安全。要安全的重用這種指標,可給它賦新值。C++ 有一個標準庫函式std::move(),讓你能夠將一個unique_ptr賦給另一個。原來的指標仍轉讓所有權變成空指標,可以對其重新賦值。下面是一個使用前述demo()函式的例子,該函式返回一個 unique_ptr<string> 物件:

?

1

2

3

4

5

unique_ptr<string> ps1, ps2;

ps1 = demo("hello");

ps2 = move(ps1);

ps1 = demo("alexia");

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

相比於auto_ptr,unique_ptr還有另一個優點,它有一個可用於陣列的變體,這裡就不作細節討論了。

5、如何選擇智慧指標

在現實中我們該如何選擇智慧指標呢?下面給出幾個選擇方法:

  • 如果程式要使用多個指向同一個物件的指標,應選擇shared_ptr。
  1. 有一個指標陣列,並使用一些輔助指標來標示特定的元素,如最大的元素和最小的元素;
  2. 兩個物件包含都指向第三個物件的指標;
  3. STL容器包含指標。很多STL演算法都支援複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
  • 如果程式不需要多個指向同一個物件的指標,則可使用unique_ptr。
  • 如果函式使用new分配記憶體,並返還指向該記憶體的指標,將其返回型別宣告為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智慧指標將負責呼叫delete。可將unique_ptr儲存到STL容器在那個,只要不呼叫將一個unique_ptr複製或賦給另一個演算法(如sort())。例如,可在程式中使用類似於下面的程式碼段:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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>:

?

1

2

3

unique_ptr<int> pup(make_int(rand() % 1000));   // ok

shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue

hared_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類似。

 

https://blog.csdn.net/crusierliu/art