C++11——智能指針


1. 介紹

  一般一個程序在內存中可以大體划分為三部分——靜態內存(局部的static對象、類static數據成員以及所有定義在函數或者類之外的變量)、棧內存(保存和定義在函數或者類內部的變量)和動態內存(實質上這塊內存池就是堆,通常通過new/malloc操作申請的內存)。對於靜態內存和棧內存來說,編譯器可以根據它們的定義去自動創建和銷毀的相應的內存空間。而對於動態內存,由於程序只有在運行時才知道需要分配多少內存空間,所以只能由程序員去動態的去創建和回收這塊內存。

  而對於動態內存的回收是一個很復雜的問題,經常會因為一些難以觀察的細節遺忘對一些對象的釋放造成內存泄露,比如下面的代碼:

#include <iostream>
#include <exception>
using namespace std;
class myException : public exception
{
public:
    const char* what_happened() const throw(){
        return "error: what you have down is error.";
    }
};

void check(int x){
    if(x == 0){
        throw myException();
    }
}

int main(){
    string* str = new string("testing....");
    try {
       check(0);
       //do something I really want to do
       // ....
    } catch (myException &e) {
        cout << e.what_happened() << endl;
        return -1;
    }
    delete str;
    return 0;
}

  一旦項目的代碼量非常龐大時,此時像這樣的內存泄露即便可以通過一些內存檢測工具(比如valgrind),但去定位並改正這些錯誤還是很繁瑣的。

  為了更方便且更安全的使用動態內存C++提供了四種智能指針來動態管理這些對象——auto_ptr(C++98,現在基本被淘汰),unique_ptr,shared_ptr,weak_ptr(后三種是C++11的新標准)。上面的程序改成如下形式,同時去掉delete str;就可以了。

std::auto_ptr<std::string> ps(new string("testing....")); 

2.智能指針

  使用智能指針,需要引入頭文件#include <memory>,接受參數的智能指針的構造函數是explict,如下

template<typename _Tp>
    class auto_ptr
    {
    private:
      _Tp* _M_ptr;
      
    public:
		explicit
     		 auto_ptr(element_type* __p = 0) throw() : _M_ptr(__p) { }
        //....
    }

  因此不能自動將指針轉換為智能指針對象,而是采用直接初始化的方式來初始化一個指針,顯示的創建對象。如下:

shared_ptr<std::string> ps(new string("testing...."));	  //正確
shared_ptr<std::string> ps = new string("testing....");   //錯誤

同時,應該避免把一個局部變量的指針傳給智能指針:

//error —— double free or corruption (out): 0x00007fffffffd910 ***
string s("testing.....");
shared_ptr<string> pvac(&s);

//correct
string* str = new string("testing....");
shared_ptr<string> pvac(str);

局部變量s是在棧上分配的內存,且其作用域范圍僅限於當前函數,一旦執行完,該變量將被自動釋放,而智能指針shared_ptr又會自動再次調用s的析構函數,導致一個變量double free。而new方式申請的內存在堆上,該部分的內存不會隨着作用域范圍的結束而被釋放,只能等到智能指針調用析構函數再去釋放。

題外話——隱式類型轉換

  隱式類型轉換可:能夠用一個實參來調用的構造函數定義了從形參類型到該類類型的一個隱式轉換。如下面程序:

#include <string>
#include <iostream>
using namespace std ;
class BOOK
{
private:
    string _bookISBN ;
    float _price ;

public:
    //這個函數用於比較兩本書的ISBN號是否相同
    bool isSameISBN(const BOOK& other){
        return other._bookISBN==_bookISBN;
    }

    //類的構造函數,即那個“能夠用一個參數進行調用的構造函數”(雖然它有兩個形參,但其中一個有默認實參,只用一個參數也能進行調用)
    BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}
};

int main()
{
    BOOK A("A-A-A");
    BOOK B("B-B-B");
    cout<<A.isSameISBN(B)<<endl;   //正常地進行比較,無需發生轉換
    cout<<A.isSameISBN(string("A-A-A"))<<endl; //此處即發生一個隱式轉換:string類型-->BOOK類型,借助BOOK的構造函數進行轉換,以滿足isSameISBN函數的參數期待。
    cout<<A.isSameISBN(BOOK("A-A-A"))<<endl;    //顯式創建臨時對象,也即是編譯器干的事情。

    return 0;
}

  此處發生了一個隱式類型轉換,將一個string類型轉化成了BOOK類,如果要阻止該類型的轉換,可以將構造函數定義成如下形式:

explicit BOOK(string ISBN,float price=0.0f):_bookISBN(ISBN),_price(price){}

現在,我們只能顯示的類型轉換和顯示的去創建BOOK對象。

2.1 auto_ptr

  auto_ptr是舊版gcc的智能指針,現在新版本的已經將其摒棄,如下程序:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
int main(){
    auto_ptr<string> day[7] = {
        auto_ptr<string>(new string("Monday")),
        auto_ptr<string>(new string("Tudsday")),
        auto_ptr<string>(new string("Wednesday")),
        auto_ptr<string>(new string("Thursday")),
        auto_ptr<string>(new string("Friday")),
        auto_ptr<string>(new string("Saturday")),
        auto_ptr<string>(new string("Sunday"))
    };
    //將Saturday的值賦給today
    auto_ptr<string> today = day[5];
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}

對於上面程序,會發現,編譯的時候,沒有什么問題,可以當運行的時候就會發生段錯誤。上面有兩個變量day[5]和today都指向同一內存地址,當這兩個變量的在這個作用域范圍失效時,就會調用各自的析構函數,造成同一塊內存被釋放兩次的情況。為了避免這種情況,在auto_ptr中有一種所有權的概念,一旦它指向一個對象后,這個對象的所有權都歸這個指針控制,但是如果此時又有一個新的auto_ptr指針指向了這個對象,舊的auto_ptr指針就需要將所有權轉讓給新的auto_ptr指針,此時舊的auto_ptr指針就是一個空指針了,上面的程序通過調試可以看出這些變量值的變化過程。

程序可以編譯通過,但運行時會出錯,這種錯誤在項目中去查找是一件很痛苦的事情,C++新標准避免潛在的內存崩潰問題而摒棄了auto_ptr。

2.2 unique_ptr

  unique_ptr和auto_ptr類似,也是采用所有權模型,但是如果同樣的程序,只是把指針的名字換了一下:

int main(){
    unique_ptr<string> day[7] = {
        unique_ptr<string>(new string("Monday")),
        unique_ptr<string>(new string("Tudsday")),
        unique_ptr<string>(new string("Wednesday")),
        unique_ptr<string>(new string("Thursday")),
        unique_ptr<string>(new string("Friday")),
        unique_ptr<string>(new string("Saturday")),
        unique_ptr<string>(new string("Sunday"))
    };
    //將Saturday的值賦給today
    unique_ptr<string> today = day[5];
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}
/* 編譯階段就會報錯
smart_ptr.cpp:17:37: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]’
     unique_ptr<string> today = day[5];
*/

可以看出unique比auto_ptr更加安全,在編譯階段就可以提前告知錯誤,而且unique_ptr還有一個很智能的地方,就是雖然不允許兩個unique_ptr的賦值操作,但是允許在函數返回值處去接受這個類型的指針,如下: 

unique_ptr<string> test(const char* c){
    unique_ptr<string> temp(new string(c));
    return temp;
}
int main(){
    unique_ptr<string> ptr;
    ptr = test("haha");
    return 0;
}

  如果確實想讓兩個unique_ptr進行賦值操作,可以調用標准庫函數std::move()函數,它可以實現對象資源的安全轉移,如下:  

unique_ptr<string> today = std::move(day[5]);
cout << "today is " << *today << endl;
for(int i = 0; i < 7; i++){
    cout << *day[i] << " ";
}
cout << endl;

  上面的代碼雖然可以安全編譯過,day[5]將資源所有權轉移到today上,會造成像auto_ptr一樣出現訪問day[5]這個空指針異常的錯誤。

2.3 shared_ptr

  現在將上面的代碼換成shared_ptr:

#include <iostream>
#include <exception>
#include <memory>
using namespace std;
shared_ptr<string> test(const char* c){
    shared_ptr<string> temp(new string(c));
    return temp;
}
int main(){
    shared_ptr<string> day[7] = {
        shared_ptr<string>(new string("Monday")),
        shared_ptr<string>(new string("Tudsday")),
        shared_ptr<string>(new string("Wednesday")),
        shared_ptr<string>(new string("Thursday")),
        shared_ptr<string>(new string("Friday")),
        shared_ptr<string>(new string("Saturday")),		//指向new string("Saturday")計數器為1
        shared_ptr<string>(new string("Sunday"))
    };
    //將Saturday的值賦給today
    shared_ptr<string> today = day[5];  //指向new string("Saturday")計數器為2
    cout << "today is " << *today << endl;
    for(int i = 0; i < 7; i++){
        cout << *day[i] << " ";
    }
    cout << endl;
    return 0;
}
/* output
today is Saturday
Monday Tudsday Wednesday Thursday Friday Saturday Sunday
*/

我們會驚訝的發現這個程序是可以正常的跑過的,而且day[5]也是可以正常打印出來的,原因在於share_ptr並不是采用所有權機制,當有多個share_ptr指向同一對象時,它就會向java的垃圾回收機制一樣采用引用計數器,賦值的時候,計數器加1,而指針對象過期的時候,計數器減1,直到計數器的值為0的時候,才會調用析構函數將對象的內存清空。

shared_ptr內存也可以這樣申請:

std::shared_ptr<ClassA> p1 = std::shared_ptr<ClassA>();
std::shared_ptr<ClassA> p2 = std::make_shared<ClassA>();

  

第一種方式會先申請A類對象所需的空間,然后再去申請針對對該空間控制的內存控制塊。而第二種方式是數據塊和控制塊會一塊申請,所以它的效率會更高一點。

2.4 wek_ptr

先來看一個例子,假設有兩個對象,他們之間重存在這相互引用的關系:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    shared_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    shared_ptr<ClassA> pa;  // 在B中引用A
};

int main02() {
    //也可以通過make_shared來返回一個shared_ptr對象,它的效率會更高
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函數結束:spa和spb會釋放資源么?
    return 0;
}

/** valgrind 一部分報告
==812== LEAK SUMMARY:
==812==    definitely lost: 32 bytes in 1 blocks
==812==    indirectly lost: 32 bytes in 1 blocks
==812==      possibly lost: 0 bytes in 0 blocks
==812==    still reachable: 72,704 bytes in 1 blocks
*/

  

使用valgrind可以看出確實造成了內存泄露,因為ClassA和ClassB相互循環的引用對方,造成各自的引用計數器都會加1,使得最終析構函數調用無法將其置為0。

這個時候可以用到wek_ptr,weak_ptr是一種“弱”共享對象的智能指針,它指向一個由share_ptr管理的對象,講一個weak_ptr綁定到shared_ptr指向的對象去,並不會增加對象的引用計數器的大小,即使weak_ptr還指向某一個對象,也不會阻止該對象的析構函數的調用。這個時候需要判斷一個對象是否存在,然后才可以去訪問對象,如下代碼:

class C
{
public:
    C() : a(8) { cout << "C Constructor..." << endl; }
    ~C() { cout << "C Destructor..." << endl; }
    int a;
};
int main() {
    shared_ptr<C> sp(new C());
    weak_ptr<C> wp(sp);
    if (shared_ptr<C> pa = wp.lock())
    {
        cout << pa->a << endl;
    }
    else
    {
        cout << "wp指向對象為空" << endl;
    }
    sp.reset(); //reset--釋放sp關聯內存塊的所有權,如果是最后一個指向該資源的(引用計數為0),就釋放這塊內存
    //wp.lock()檢查和shared_ptr綁定的對象是否還存在
    if (shared_ptr<C> pa = wp.lock())
    {
        cout << pa->a << endl;
    }
    else
    {
        cout << "wp指向對象為空" << endl;
    }
}
/* output
C Constructor...
8
C Destructor...
wp指向對象為空
*/

  然后將最開始的程序改成如下形式,則可以避免循環引用而造成的內存泄漏問題。

class ClassB;

class ClassA
{
public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb;  // 在A中引用B
};

class ClassB
{
public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa;  // 在B中引用A
};

int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函數結束,思考一下:spa和spb會釋放資源么?
    return 0;
}

/* valgrind報告
==5401== LEAK SUMMARY:
==5401==    definitely lost: 0 bytes in 0 blocks
==5401==    indirectly lost: 0 bytes in 0 blocks
==5401==      possibly lost: 0 bytes in 0 blocks
==5401==    still reachable: 72,704 bytes in 1 blocks
==5401==         suppressed: 0 bytes in 0 blocks
*/

參考資料

  1. C++ Primer(第五版)

  1. C++智能指針簡單剖析

  2. C++ 隱式類類型轉換

  3. 【C++11新特性】 C++11智能指針之weak_ptr

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM