天天看點

記憶體管理-智能指針1: auto_ptr2: shared_ptr3: unique_ptr4: weak_ptr補充資料:weak_ptr用于解決循環引用和自引用對象問題 【review at 2015-10-03】

問題引出:c++繼承了c那高效而又靈活的指針,使用起來稍微不小心就會導緻記憶體洩漏,懸挂指針,越界通路等問題,比如以下代碼:

int *p = new int;
... 
if(xxx)
    goto loop;
delete p;
           

以上代碼,很明顯很容易導緻記憶體洩漏問題,比如當滿足條件進行跳轉,或者當delete p;前的操作出現異常導緻程式退出,這些都會導緻動态記憶體沒有正确釋放掉,另外的當一個複雜程式,如果由于不小心delete/free了多次同一指針的話,這種錯誤編譯能夠通過,但在運作時會出現程式崩潰錯誤,另外還有可能忘了對指針進行delete導緻記憶體洩漏,而智能指針可以在哪些變量或對象在退出作用域時–不管是正常退出或異常退出–也能夠進行相應的資源釋放工作;

注:delete/free一個空指針總是沒有錯誤的;

以下就總結一下标準庫中的幾種智能指針包括:auto_ptr , shared_ptr , unique_ptr , weak_ptr ;都包含在頭檔案memory中;

1: auto_ptr

存在多種的智能指針,其中最有名的應該是c++98标準中改的自動指針“auto_ptr”,【注:c++11标準中聲明廢棄該智能指針,而應選用新的智能指針實std::unique_ptr,目前c++11中新增的智能指針有unique_ptr, shared_ptr, weak_ptr】,auto_ptr其實是一種類模闆,但其重載了operator*和operator->,auto_ptr對象的行為類似指針。其構造函數接受new操作符或者對象工廠建立出的對象指針作為參數,進而代理了原始指針,建立auto_ptr對象的示例方法如下代碼。c++保證當那些動态配置設定記憶體的指針變量離開作用域時,會調用auto_ptr的析構函數,進而使用delete操作符釋放動态申請的指針所指向的記憶體資源;

/* 不能使用指派的方法進行初始化一個auto_ptr,因為根據一般指針建立auto_ptr的構造函數被聲明為explict,
必須進行顯式的調用構造函數進行對象的建立,注意不是不可以指派 */
auto_ptr<int> p(new int());  
auto_ptr<ClassA> pA(factory.create()); // 本質上還是傳入一個普通指針
           

auto_ptr的注意事項與常見操作:

1: auto_ptr所有權的轉移

即auto_ptr的對象所有權是獨占的,不可能有兩個auto_ptr對象同時擁有同一個動态對象的所有權【這個要求也是在提醒編碼時要注意防範以同一個對象為初值,将兩個auto_ptr初始化】;因為auto_ptr所有權獨占這個條件,導緻auto_ptr的copy構造函數和指派運算符函數的工作也有所不同;示例代碼:

auto_ptr<ClassA> ptr1(new ClassA);
auto_ptr<ClassA> ptr2(ptr1); // 調用拷貝構造函數進行對象的建立
auto_ptr<ClassA> ptr3(new ClassA);
auto_ptr<ClassA> ptr4;
...
ptr4 = ptr3; // 調用指派運算符
           

解釋:第一個語句中ptr1擁有new出來的那個對象的所有權,在第二個語句中,擁有權由ptr1轉交給ptr2。此後,ptr2就擁有了那個new出來的對象的所有權,ptr1不再擁有它,隻剩下一個null指針在手即“是去了所有權,隻剩一個null指針”;上面的指派操作也是相同的道理,ptr4擁有了原先ptr3擁有的對象,ptr3後來隻生一個null指針;

注:如果ptr3被指派之前正擁有另一個對象,指派動作發生時會調用delete,将該對象删除;

可能發生擁有權的轉移有函數轉交到另一個函數的情況:

1:某函數是資料的終點: auto_ptr以傳值得方式被當作一個參數傳遞給某函數,然後調用端獲得了這個auto_ptr的擁有權,如果函數不再将它傳遞出去,它所指的對象就會在函數退出時被删除;

2:某函數是資料的起點,當一個auto_ptr被傳回,其擁有權便被轉交給調用端了;

auto_ptr<ClassA> f()
{
    auto_ptr<ClassA> ptr(new ClassA);
    ...
    return ptr; // transfet ownership to calling function
}

void g()
{
    auto_ptr<ClassA> p;
    for(int i=; i<; i++)
    {
        p = f(); // p gets ownership of the returned object
            // previous rerurned object of f() gets deleted
        ...
    }
} // lasr-owned object of p gets deleted
           

由于函數調用導緻的所有權轉交問題,是以函數中的用法一定要注意:當五一轉交所有權時,不要再參數列中使用auto_ptr,也不要以它作為傳回值;

可以建立const型auto_ptr對象來終結所有權的轉交;

如:

const auto_ptr<int> p(new int());
auto_ptr<int> q(new int);
*p = ; // OK,關鍵詞const并非意味不能更改對象,而是意味不能更改auto_ptr的擁有權而已;
// 這個時候const型的auto_ptr更像是T* const p,而不是const T* P,盡管文法上看上去比較像後者;
p = q; // Error,想要轉交所有權,出現compile-time error
           

當auto_ptr作為類的成員變量之一時,别忘了要重寫拷貝構造函數和指派運算符函數,這兩個如果是預設的都會有所有權轉交的發生;

auto_ptr使用注意小結:

1: auto_ptr之間不能共享所有權;

2:auto_ptr不能指向數組;

3:auto_ptr不要作為容器的成員;

4:根據一般指針生成一個auto_ptr的構造函數被聲明為explict,是以不要一下方式進行對象的建立;

auto_ptr<int> p = new int(); // ERROR
auto_ptr<int> p(new int()); // OK
           

注:auto_ptr的接口與一般指針非常相似:operator*用來提領其所指對象,operator->用來指向對象中的成員,然而,*所有的指針算術(包括++)都沒有定義;

2: shared_ptr

是一種引用計數型的智能指針,允許多個指針指向同一個對象,包裝了new操作符在對上配置設定的動态對象,與auto_ptr或unique_ptr不同的是,shared_ptr可以被自由的拷貝和指派,在任意的地方共享它,當沒有代碼使用(引用計數為0)它時,才删除被包裝的動态配置設定的對象;shared_ptr可以被安全的放在标準容器中,是STL容器中存儲指針的最标準解法;

shared_ptr代碼示例

shared_ptr<string> p1; // 可以指向string的shared_ptr,預設初始化時儲存着一個空指針nullptr
shared_ptr<list<int> > p2; // 可以隻想list<int>的shared_ptr
           

shared_ptr和unique_ptr都支援的操作:

shared_ptr<T> sp / unique_ptr<T> up; // 空智能指針,可以指向類型T的對象
p // 将p用作一個條件進行判斷,若p指向一個對象,則為true
*p // 解引用,獲得它所指向的對象
p->mem // 等價于(*p).mem
p.get() // 傳回p中儲存的指針,要小心使用,若智能指針釋放了其對象,傳回的指針所指向的對象也就消失了
swap(p,q) / p.swap(q) // 交換p和q的指針
           

shares_ptr獨有的操作:

make_shared<T>(args); // 傳回一個shared_ptr,指向一個動态配置設定的類型為T的對象,是用args初始化此對象;
shared_ptr<T>p(q); // p是qshared_ptr q的拷貝,此操作會遞增q中的計數器,q中國的指針必須能轉換為T*
p = q; // p和q都是shared_ptr,所儲存的指針必須能互相轉換,此操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數變為,則将其管理的原記憶體釋放
p.unique(); // 若p.use_count();為,傳回true,否則傳回false
p.use_count(); // 傳回與p共享對象的智能指針數量,主要用于調試作用
           

示例代碼:

if(p) // p為shared_ptr,相當于if(p==nullptr)
shared_ptr<int> p1 = make_shared<int>(); 
// 零初始化
shared_ptr<int> p2 = make_shared<int>(); 

/* 
    p6指向一個動态配置設定的空vector<string>通常用auto定義一個對象來
    儲存make_shared的結果,方式簡單,有編譯器去識别;
*/
auto p3 = make_shared<vector<string> >(); 

// 錯誤,對于接受指針參數的智能指針構造函數是explict,必須直接初始化形式,即等号改為括号
shared_ptr<int> p4 = new int(); 

shared_ptr<int> clone(int p)
{
    // 錯誤,這裡要求的是普通指針隐式的轉換為shared_ptr<int>,但該構造函數是explict的
    return new int(p); 
    // 正确的是 return shared_ptr<int>(new int(p));
}
           

每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數,拷貝一個shared_ptr,将它作為參數傳遞給一個函數,作為函數的傳回值,用一個shared_ptr去初始化另一個shared_ptr,這幾種情況都會遞增它所關聯的計數器;

當給shared_ptr賦予一個新值,或shared_ptr被銷毀,例如一個局部的shared_ptr離開其作用域是,計數器就會遞減;

一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的對象;

void use_factory(T args)
{
    shared_ptr<Foo> p = factory(arg);
    // 使用p
    return p; // 傳回p時,引用計數進行了遞增操作
} 
/*
 p離開了作用域,但它指向的記憶體不會被釋放掉,因為上面return的時候計數加1,不為0,
    不會進行相應的資源釋放回收
*/
           

預設情況下,一個用來初始化智能指針的普通指針必須指向動态記憶體,因為隻能指針預設使用delete釋放它所關聯的對象;

定義和改變shared_ptr的其它方法

shared_ptr<T> p(q); // p管理内置指針q所指向的對象,q必須指向new配置設定的記憶體,且能夠轉換為T*類型
shared_ptr<T> p(u); // p從unique_ptr u那裡接管了對象的所有權,将u置為空
shared_ptr<T> p(q, d); // p接管了内置指針q所指向的對象的所有權,q必須能轉換為T*類型,p将使用可調用對象d來代替delete
shared_ptr<T> p(p2, d); // p是shared_ptr p2的拷貝唯一的差別是p将用可調用兌現gd來代替delete
p.reset(); // 若p是唯一指向其對象的shared_ptr,reset會釋放此對象,若傳遞了可選的參數内置指針q,會令p指向q,否則會将p置為空,若還傳遞了參數d,将會調用d而不是delete來釋放q
p.reset(q);
p.reset(q, d);
           

使用我們自己的釋放操作

預設情況下,shared_ptr假定它們直線改的是動态記憶體,是以,當一個shared_ptr被銷毀時,它預設的對它管理的指針進行delete操作,對于一些資料庫或網絡的連接配接釋放,我們需要自己定義函數代替預設的delete操作,比如:

void end_connection(connection *p)
{
    disconnect( *p );
}

void f(destination &d)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
}
           

智能指針使用建議:

1:不使用相同的内置指針值初始化(或reset)多個智能指針;

2:不delete get()傳回的指針;

3:不使用get()初始化或reset另一個智能指針;

4:如果使用get()傳回的指針,記住當最後一個對應的之恩那個指針銷毀後,對應的指針就變為無效了;

5:如果使用智能指針管理的資源不是new配置設定的記憶體,記住傳遞給它一個删除器;

3: unique_ptr

unique_str是在c++11标準中定義的用來取代auto_ptr的新的智能指針,不僅能夠代理new建立的單個對象,也能夠代理new[ ]建立出來的數組對象,這點是auto_ptr不能實作的;

一個unique_ptr“擁有”它所指向的對象,某個時刻隻能有一個unique_ptr指向一個給定對象,當unique_ptr被銷毀時,它所指向的對象也被銷毀;

unique_ptr<string> pStr(new string("Hello"));
unique_ptr<int> p1;
unique_ptr<int> p2(new int());
p1 = p2; // 錯誤,unique_ptr不支援普通指派
unique_ptr<int> p3(p2); // 錯誤,unique_ptr不支援普通拷貝

**// 由于一個unique_ptr擁有它所指向的對象,是以unique_ptr不支援普通的拷貝或指派操作,
// 但是有一個例外就是,unique_ptr可以拷貝或指派一個将要被銷毀的unique_ptr,最常見的就是從函數傳回一個unique_ptr**
unique_ptr<int> clone(int p)
{
    return unique_ptr<int>(new int(p)); // 建立一個匿名對象用于傳回
}
           

unique_ptr的獨有操作:

unique_ptr<T> u1; // 空unique_ptr,可以指向類型為T的對象,u1會使用delete來釋放它的指針,u2會使用一個類型為D的可調用對象釋放它的指針
unique_ptr<T, D> u2;
unique_ptr<P, D> u(d); // 空unique_ptr,指向類型T的對象,用類型為D的對象d代替delete
u = nullptr; // 釋放u指向的對象,将u置為空
u.release(); // u放棄對指針的控制權,傳回指針,并将u置為空
u.reset(); // 釋放u指向的對象
u.reset(q); // 如果提供了内置指針q,令u指向這個對象,否則将u置為空
u.reset(nullptr);
           
// 以上方法,release或reset可以将指針的所有權從一個(非const)unique_ptr轉移到另一個unique; 比如:
unique_ptr<string> p2(p.release()); // 所有權從p1轉移給p2
unique_ptr<string> p3(new string("Hello Test"));
p2.reset(p3.release()); // reset釋放了p2原來指向的記憶體,将所有權從p3轉移到p2

*// 注意:如果不用另一個智能指針來儲存release傳回的指針,那我們的程式就要負責資源的釋放,比如:*
auto p = p2.release();
// ...
delete p; *// 對于傳回給普通的指針類型或auto,需要手工的添加delete操作*
           

類似shared_ptr,unique_ptr預設情況下用delete釋放它指向的對象,與shared_ptr一樣,我們可以重載一個unique_ptr中預設的删除器;

4: weak_ptr

weak_ptr是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象,将一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數,一旦最後一個指向對象的shared_ptr被銷毀,對象就會被釋放,即使用weak_ptr指向對象,對象也還是會被釋放,weak_ptr是一種“弱共享”對象;

注:weak_ptr是為配合shared_ptr而引入的一種智能指針,它更像是shared_ptr的助手,因為它不具有普通指針的行為,沒有重載operator*和operator->【注:這是故意不重載的,因為它不共享指針,不能操作資源,這就是稱之為weak_ptr的原因】,它的最大作用在于協助shared_ptr工作,weak_ptr的構造和析構都不會影響shared_ptr的引用計數,隻是一個靜靜的觀察者;

weak_ptr的操作:

weak_ptr<T> w; // 空weak_ptr可以指向類型為T的對象
weak_ptr<T> w(sp); // 與shared_ptr sp指向相同對象的weak_ptr,sp指向的類型必須能轉換為類型T
w = p; // p可以是一個shared_ptr或一個weak_ptr,指派後w與p共享對象
w.reset(); // 将w置為空
w.use_count(); // 與w共享對象的shared_ptr的數量
w.expired(); // 若w.use_count()為,傳回true,否則傳回false
w.lock(); // 如果expired為true,傳回一個空shared_ptr,否則傳回一個指向w的對象的shared_ptr 
           

建立一個weak_ptr時,要用一個shared_ptr來初始化它:

auto p = make_shared<int>();
weak_ptr<int> wp(p);
           

建立wp不會改變p的引用計數,wp指向的對象可能被釋放掉,由于對象可能不存在,我們不能使用weak_ptr直接通路對象,而必須調用lock,該函數檢查weak_ptr指向的對象是否仍存在,如果存在,lock傳回一個指向共享對象的shared_ptr;

if(shared_ptr<int> np = wp.lock())
{
    // 在if中,np與p共享對象
    // ...
}
           

補充資料:weak_ptr用于解決循環引用和自引用對象問題 【review at 2015-10-03】

weak_ptr

1 、weak_ptr是用來解決循環引用和自引用對象

從前面的例子我們可以看出,引用計數是一種很便利的記憶體管理機制,但是有一個很大的缺點,那就是不能管理循環引用或自引用對象(例如連結清單或樹節點),為了解決這個限制,是以weak_ptr被引入到boost的智能指針庫中。

2、weak_ptr并不能單獨存在

它是與shared_ptr同時使用的,它更像是shared_ptr的助手而不是智能指針,因為它不具備普通指針的行為,沒有重載operator *和->操作符,這是特意的。這樣它就不能共享指針,不能操作資源,這正是它“弱”的原因。它最大的作用是協助shared_ptr工作,像旁觀者那樣觀察資源的使用情況。

3、 weak_ptr獲得資源的觀察權

weak_ptr可以從一個shared_ptr或另外一個weak_ptr構造,進而獲得資源的觀察權,但weak_ptr并沒有共享資源,它的構造并不會引起引用計數的增加,同時它的析構也不會引起引用計數的減少,它僅僅是觀察者。

4、 weak_ptr可以被用于标準容器庫中的元素

weak_ptr實作了拷貝構造函數和重載了指派操作符,是以weak_ptr可以被用于标準容器庫中的元素,例如:在一個樹節點中聲明子樹節點std::vector

weak_ptr常見用途:

結合

shared_ptr

使用的特例智能指針。

weak_ptr

提供對一個或多個

shared_ptr

執行個體所屬對象的通路,但是,不參與引用計數。 如果您想要觀察對象但不需要其保持活動狀态,請使用該執行個體。 在某些情況下需要斷開

shared_ptr

執行個體間的循環引用。 頭檔案:

<memory>

weak_ptr

的用法如下:

weak_ptr

用于配合

shared_ptr

使用,并不影響動态對象的生命周期,即其存在與否并不影響對象的引用計數器。

weak_ptr

并沒有重載

operator->

operator *

操作符,是以不可直接通過

weak_ptr

使用對象。提供了

expired()

lock()

成員函數,前者用于判斷

weak_ptr

指向的對象是否已被銷毀,後者傳回其所指對象的

shared_ptr

智能指針(對象銷毀時傳回”空“

shared_ptr

)。循環引用的場景:如二叉樹中父節點與子節點的循環引用,容器與元素之間的循環引用等。

智能指針的循環引用

循環引用:

“循環引用”簡單來說就是:兩個對象互相使用一個

shared_ptr

成員變量指向對方的會造成循環引用。導緻引用計數失效。下面給段代碼來說明循環引用:

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

class B;
class A
{
public:// 為了省去一些步驟這裡 資料成員也聲明為public
  //weak_ptr<B> pb;
  shared_ptr<B> pb;
  void doSomthing()
  {
    //      if(pb.lock())
    //      {
    //
    //      }
  }

  ~A()
  {
    cout << "kill A\n";
  }
};

class B
{
public:
  //weak_ptr<A> pa;
  shared_ptr<A> pa;
  ~B()
  {
    cout <<"kill B\n";
  }
};

int main(int argc, char** argv)
{
  shared_ptr<A> sa(new A());
  shared_ptr<B> sb(new B());
  if(sa && sb)
  {
    sa->pb=sb;
    sb->pa=sa;
  }
  cout<<"sa use count:"<<sa.use_count()<<endl;
  return ;
}
           

上面的代碼運作結果為:

sa use count:2

, 注意此時sa,sb都沒有釋放,産生了記憶體洩露問題!!!

即A内部有指向B,B内部有指向A,這樣對于A,B必定是在A析構後B才析構,對于B,A必定是在B析構後才析構A,這就是循環引用問題,違反正常,導緻記憶體洩露。

解決循環引用有下面有三種可行的方法:

一般來講,解除這種循環引用有下面有三種可行的方法( 參考 ):

1 . 當隻剩下最後一個引用的時候需要手動打破循環引用釋放對象。

2 . 當A的生存期超過B的生存期的時候,B改為使用一個普通指針指向A。

3 . 使用弱引用的智能指針打破這種循環引用。

雖然這三種方法都可行,但方法1和方法2都需要程式員手動控制,麻煩且容易出錯。我們一般使用第三種方法:弱引用的智能指針

weak_ptr

強引用和弱引用

一個強引用當被引用的對象活着的話,這個引用也存在(就是說,當至少有一個強引用,那麼這個對象就不能被釋放)。

share_ptr

就是強引用。相對而言,弱引用當引用的對象活着的時候不一定存在。僅僅是當它存在的時候的一個引用。弱引用并不修改該對象的引用計數,這意味這弱引用它并不對對象的記憶體進行管理,在功能上類似于普通指針,然而一個比較大的差別是,弱引用能檢測到所管理的對象是否已經被釋放,進而避免通路非法記憶體。

使用

weak_ptr

來打破循環引用

代碼如下:

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

class B;
class A
{
public:// 為了省去一些步驟這裡 資料成員也聲明為public
  weak_ptr<B> pb;
  //shared_ptr<B> pb;
  void doSomthing()
  {
    if(pb.lock())
    {

    }
  }

  ~A()
  {
    cout << "kill A\n";
  }
};

class B
{
public:
  //weak_ptr<A> pa;
  shared_ptr<A> pa;
  ~B()
  {
    cout <<"kill B\n";
  }
};

int main(int argc, char** argv)
{
  shared_ptr<A> sa(new A());
  shared_ptr<B> sb(new B());
  if(sa && sb)
  {
    sa->pb=sb;
    sb->pa=sa;
  }
  cout<<"sb use count:"<<sb.use_count()<<endl;
  return ;
}
           

參考資料:

http://www.tuicool.com/articles/6j2yy2z

《C++ 标準程式庫》 侯捷 孟岩 譯

《C++ Primer》(第5版) 王剛 楊巨峰 譯

《Boost程式庫完全開發指南》(第3版) 羅劍鋒 著

繼續閱讀