1. 程式人生 > >c++智慧指標--所有的型別的解析

c++智慧指標--所有的型別的解析

參考文獻:

1. 智慧指標背後的設計思想

1.1 無智慧指標造成記憶體洩漏的例子

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

當出現異常時(weird_thing()返回true),delete將不被執行,因此將導致記憶體洩露 。

常規解決方案:

  1. throw exception()
    之前新增delete ps;
  2. 不要忘了最後一個delete ps;

出現問題:在一個大型的工程中,並不能保證所有的開發人員都能在合適的地方新增delete語句。

1.2 智慧指標的設計思想

仿照本地變數能夠自動從棧記憶體中刪除的思想,對指標設計一個解構函式,該解構函式將在指標過期時自動釋放它指向的記憶體,總結來說就是:將基本型別指標封裝為類物件指標(這個類肯定是個模板,以適應不同基本型別的需求),並在解構函式中編寫delete語句以用來刪除指標指向的記憶體空間

轉換remodel()函式的步驟:

  • 包含頭義件memory(智慧指標所在的標頭檔案);
  • 將指向string的指標替換為指向string的智慧指標物件;
  • 刪除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;
}

2. C++智慧指標簡單介紹

STL一共給我們提供了四種智慧指標:auto_ptr

unique_ptrshared_ptrweak_ptr

其中:auto_ptr在C++11中已將其摒棄。

使用注意點

  • 所有的智慧指標類都有一個explicit建構函式,以指標作為引數。比如auto_ptr的類模板原型為:
templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
};

因此不能自動將指標轉換為智慧指標物件,必須顯示呼叫:

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)
  • 對全部三種智慧指標都應避免的一點:
string vacation("I wandered lonely as a child."); //heap param
shared_ptr<string> pvac(&vacation);//NO!!

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

  • 使用例項
#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();
    }//auto_ptr 作用域結束
    {
        std::shared_ptr<report> ps(new report("using shared_ptr"));
        ps->comment();
	}//shared_ptr 作用域結束
    {
        std::unique_ptr<report> ps(new report("using unique ptr"));
        ps->comment();
    }//unique_ptr 作用域結束
    return 0;
}

3. 為什麼摒棄auto_ptr?

問題來源:

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

如果psvocation是常規指標,則兩個指標指向同一個string物件,當指標過期時,則程式會試圖刪除同一個物件,要避免這種問題,解決辦法:

  • 定義賦值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採取此方案。
  • 建立所有權(ownership)概念。對於特定的物件,智慧有一個智慧物件可擁有,這樣只能擁有物件的智慧指標的解構函式會刪除該物件。然後讓賦值操作轉讓所有權。這就是用於auto_ptrunique_ptr的策略,但unique_ptr的策略更嚴格
  • 建立智慧更高的指標,跟蹤引用特定物件的智慧指標數。這稱為引用計數。例如,賦值時,計數將加1,而指標過期時,計數將減1,當減為0時才呼叫delete這是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 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 owership,將所有權從films[2]轉讓給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;
}

執行下發現程式崩潰了,原因是films[2]已經是空指標了,輸出空指標就會崩潰。如果把auto_ptr換成shared_ptrunique_ptr後,程式就不會崩潰,原因如下:

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

  • 適用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在適用unique_ptr時,程式不會等到執行階段崩潰,在編譯階段下屬程式碼就會出現錯誤:

  unique_ptr<string> pwin;
  pwin = films[2];//films[2] loses ownership

這就是為何摒棄auto_ptr的原因:避免潛在的記憶體洩漏問題。

4.unique_ptr為何優於auto_ptr

4.1 使用規則更嚴格

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

在語句#3中,p2接管string物件的所有權後,p1的所有權將被剝奪。–>可防止p1和p2的解構函式試圖刪除同一個物件。但如果隨後試圖使用p1,則會出現錯誤。

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

編譯器會認為#6語句為非法,可以避免上述問題。

4.2 對懸掛指標的操作更智慧

總體來說:允許臨時懸掛指標的賦值,禁止其他情況的出現

示例:函式定義如下:

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

在程式中呼叫函式:

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

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

  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的操作,僅當以非智慧的方式使用摒棄的智慧指標時(如解除引用時),這種賦值才不安全。要安全的重用這種指標,可給它賦新值。C++有一個標準庫函式std::move(),可以將原來的指標轉讓所有權變成空指標,可以對其重新賦值。

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

5. 如何選擇智慧指標

使用指南:

  1. 如果程式要使用多個指向同一個物件的指標,應選用shared_ptr。這樣的情況包括:
    • 有一個指標陣列,並使用一些輔助指標來標示特定的元素,如最大的元素和最小的元素;
    • 連個物件包含指向第三個物件的指標;
    • STL容器包含指標。很多STL演算法都支援複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr
  2. 如果程式不需要多個指向同一個物件的指標,則可使用unique_ptr。如果函式使用new分配記憶體,並返還指向該記憶體的指標,將其返回型別宣告為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智慧指標將負責呼叫delete。可將unique_ptr儲存到STL容器中,只要不呼叫將unique_ptr複製或賦值給另一個演算法(如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呼叫沒有問題,因為它返回一個臨時unique_ptr,該unique_ptr被賦值給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞物件,for_each()將非法,因為這將導致使用一個來自vp的非臨時unique_ptr初始化p1,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。

unique_ptr為右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的程式碼中,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包含一個顯式建構函式,可用於將右值unique_ptr轉換為shared_ptrshared_ptr將接管原來歸unique_ptr所有的物件。

在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。

6. 弱引用智慧指標 weak_ptr

設計weak_ptr的原因:解決使用shared_ptr因迴圈引用而不能釋放資源的問題。

6.1 空懸指標問題

這裡寫圖片描述

有兩個指標p1p2,指向堆上的同一個物件Object,p1p2位於不同的執行緒中。假設執行緒A通過p1指標將物件銷燬了(儘管把p1置為NULL),那p2就成了空懸指標。這是一種典型的C/C++記憶體錯誤。

使用weak_ptr能夠幫助我們輕鬆解決上述的空懸指標問題(直接使用shared_ptr也是可以的)。

weak_ptr不控制物件的生命期,但是它知道物件是否還活著,如果物件還活著,那麼它可以提升為有效的shared_ptr(提升操作通過lock()函式獲取所管理物件的強引用指標);如果物件已經死了,提升會失敗,返回一個空的shared_ptr

舉個栗子 :

#include <iostream>
#include <memory>

int main()
{
    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid
    // empty definition
    std::shared_ptr<int> sptr;
    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;
    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;
    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;
    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;
    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";
    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

6.2 迴圈引用問題

栗子 大法:

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

class BB;
class AA
{
public:
    AA() { cout << "AA::AA() called" << endl; }
    ~AA() { cout << "AA::~AA() called" << endl; }
    shared_ptr<BB> m_bb_ptr;  //!
};

class BB
{
public:
    BB() { cout << "BB::BB() called" << endl; }
    ~BB() { cout << "BB::~BB() called" << endl; }
    shared_ptr<AA> m_aa_ptr; //!
};

int main()
{
    shared_ptr<AA> ptr_a (new AA);
    shared_ptr<BB> ptr_b ( new BB);
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
    //下面兩句導致了AA與BB的迴圈引用,結果就是AA和BB物件都不會析構
    ptr_a->m_bb_ptr = ptr_b;
    ptr_b->m_aa_ptr = ptr_a;
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}

執行結果:

這裡寫圖片描述

可以看到由於AA和BB內部的shared_ptr各自儲存了對方的一次引用,所以導致了ptr_aptr_b銷燬的時候都認為內部儲存的指標計數沒有變成0,所以AA和BB的解構函式不會被呼叫。解決方法就是把一個shared_ptr替換成weak_ptr

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

class BB;
class AA
{
public:
    AA() { cout << "AA::AA() called" << endl; }
    ~AA() { cout << "AA::~AA() called" << endl; }
    weak_ptr<BB> m_bb_ptr;  //!
};

class BB
{
public:
    BB() { cout << "BB::BB() called" << endl; }
    ~BB() { cout << "BB::~BB() called" << endl; }
    shared_ptr<AA> m_aa_ptr; //!
};

int main()
{
    shared_ptr<AA> ptr_a (new AA);
    shared_ptr<BB> ptr_b ( new BB);
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
    //下面兩句導致了AA與BB的迴圈引用,結果就是AA和BB物件都不會析構
    ptr_a->m_bb_ptr = ptr_b;
    ptr_b->m_aa_ptr = ptr_a;
    cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
    cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}

執行結果:

這裡寫圖片描述

最後值得一提的是,雖然通過弱引用指標可以有效的解除迴圈引用,但這種方式必須在能預見會出現迴圈引用的情況下才能使用,即這個僅僅是一種編譯期的解決方案,如果程式在執行過程中出現了迴圈引用,還是會造成記憶體洩漏的。因此,不要認為只要使用了智慧指標便能杜絕記憶體洩漏。