1. 程式人生 > >C++相關:動態內存和智能指針

C++相關:動態內存和智能指針

數量 網絡 隱式 執行 動態分配 int 弱引用 支持 相關操作

前言

在C++中,動態內存的管理是通過運算符newdelete來完成的。但使用動態內存很容易出現問題,因為確保在正確的時間釋放內存是及其困難的。有時候我們會忘記內存的的釋放,這種情況下就會產生內存泄露;有時候又會在尚有指針引用的情況下就用delete釋放了內存,這樣又會產生引用非法內存的指針(野指針)。因此,為了更容易地使用動態內存,C++標準庫提供了兩種智能指針,shared_ptrunique_ptr。shared_ptr允許多個指針指向同一個對象,unique_ptr則獨占指向的對象。另外,還有一種叫weak_ptr的伴隨類,他是一種弱引用,指向shared_ptr所管理的對象。三者定義於memory

頭文件中。

shared_ptr類

聲明方式類似vector,屬於模板類,如下

shared_ptr<string> p1;     //聲明了一個指向string的智能指針,默認空

解引用等使用的方式類似普通指針

if( p1 && p1->empty())
  *p1 = "hi!"; //如果p1指向一個空string,解引用並賦一個新值

兩種智能指針公用的操作

shared_ptr<T> sp;
unique_ptr<T> up;

//假設聲明了兩個名為p、q的智能指針
p->mem;  //等價於(*p).mem
p.get();     //返回p中存放的指針

//交換二者保存的指針
swap(p,q);
p.swap(q);

shared_ptr獨有的操作

p.unique();  //是否獨有
p.use_count; //返回p共享對象的智能指針數量
p = q;  //該操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數變為0,則將其管理的原內存釋放
make_shared<T>(args);//該方法返回一個shared_ptr,指向一個T類型的對象,並使用args初始化該對象,具體見下文 

make_shared函數

最安全的分配和使用動態內存的方法。

shared_ptr<int> p3 = make_shared<int>(42);//指向值為42的int的智能指針。
//或者也可以
auto p3 = make_shared<int>(42);

每一個shared_ptr都有關聯的計數器,稱為引用計數。當用一個shared_ptr ——p去初始化另個一個q時,或者將p作為參數傳遞給函數,或者作為函數返回值時,它關聯的對象的引用計數就會遞增;而如果給它賦一個新值或者是shared_ptr被銷毀時,之前關聯的計數器就減1,當一個shared_ptr的計數器為0時,他就會自動釋放管理的對象的內存。

動態分配的const對象

const int *pci = new int(1024);
const string *pcs = new string;
/*const修飾的對象必須初始化
雖然對象的值不能被修改,但是本身可以銷毀的*/
delete pcs;//這是可以的

PS:用delete釋放一個空指針總是沒有錯誤的。

內存耗盡

如果內存耗盡的情況下,使用new會分配失敗並拋出std::alloc異常。

可以使用

int *p2 = new (nothrow) int;

的形式來向new傳遞額外的參數,從而告訴它不能拋出異常,這種稱為定位new。

動態對象的生存周期直到被釋放為止

Foo* factory(T arg)
{
   return new Foo(arg);
}

void use_factory(T arg)
{
   Foo *p = factory(arg);
}

上述的代碼,雖然p在離開作用域以後被銷毀了,但他所指向的動態內存並沒有被釋放,不註意的話很可能內存泄漏!

所以,正確的做法如下:

void use_factory(T arg)
{
   Foo *p = factory(arg);
   //這塊內存不再使用了就釋放
   delete p;
}

概括來說,由內置指針(不是智能指針)管理的動態內存在被顯式地釋放前會一直存在,直到手動釋放或者程序結束時才會被回收。因此,智能指針的使用能夠避免很多忘記釋放內存等失誤帶來的麻煩。

另外,delete之後,雖然指針已經無效,但是它依然保存著釋放的內存的地址,因此為了避免誤操作最好將指針置空。

int *p(new int(42));
auto q = p;
delete p;
p = nullptr;

但是這樣提供的保護還是有限的,如上述代碼雖然將p置空,但是q仍然指向那塊內存,仍然存在隱患。

shared_ptr和new結合使用

//錯誤的方式,智能指針的構造函數由explicit修飾,不支持將內置指針隱式轉換為智能指針
shared_ptr<int> p1= new int(1024);
//正確方式
shared_ptr<int> p2(new int(1024));

p2.reset(); //若p2是唯一指向,則釋放其指向的內存並置空
p2.reset(q) //令p2指向q,否則置空

//同樣的,返回值如果時內置指針也會報錯
shared_ptr<int> clone(int p)
{
    return new int(p);  //錯誤,無法隱式轉換為智能指針
}

  

智能指針和普通指針最好不要混合使用

void process(shared_ptr<int> ptr)
{

}
/*ptr離開作用域被銷毀
-----------------
如果使用普通指針*/
int *x(new int(1024));
process(x);//出錯,無法轉換
process(shared_ptr<int>(x));//合法,但是x指向的內存在內部會被釋放掉!!
int j = *x; //錯誤,未定義,x是一個空懸指針

上述代碼中,shared_ptr通過x拷貝構造了一個智能指針ptr傳遞進process,這個時候的引用計數為1,而離開作用域後ptr被銷毀,其指向的對象不再被引用,因此內存被回收,指針x因此無家可指變為野指針。

另外,也盡量避免使用get初始化另一個智能指針,也不要delete get()返回的內置指針。

使用自定義的釋放操作

struct destination;       //連接目的地
struct connection;       //連接信息
connection connect(destination *);  //打開連接
void disconnect(connection);          //關閉指定連接
void end_connection(connection *p)
{
     disconnect(*p);
}

void f(destination &d /*其他參數*/)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c,end_connection);
    //使用連接
   //當f退出時(即使為異常退出),connection也會被正確關閉
}

上述代碼模擬的一個網絡庫的代碼使用。

當p被銷毀時,她不會對保存的的指針delete,而是調用end_connection,接下來end_connection會調用disconnect,從而確保連接被關閉。如果f正常退出,那麽p的銷毀會作為結束處理的一部分,如果發生了異常,p同樣被銷毀,連接從而被關閉。

unique_ptr類

顧名思義,獨一無二的指針,與shared_ptr不同,某個時刻只能由一個unique_ptr指向一個給定對象。聲明以及初始化如下

unique_ptr<int> p2(new int(42));

由於unique_ptr獨享其對象,所以它不支持普通的拷貝和賦值操作

unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  //錯誤:不支持拷貝
unique_ptr<string> p3‘
p3 = p2;                             //錯誤,不支持賦值

unique_ptr的相關操作

unique_ptr<T> u1; //空unique_ptr指針
unique_ptr<T,D> u2; //使用D對象來代替delete釋放
unique_ptr<T,D> u(new class());

u = nullptr; //釋放u指向的對象並置空
u.release(); //u會放棄對該對象的控制權(內存不會釋放),返回一個指向對象的指針,並置空自己
u.reset();   //釋放u所指對象
u,reset(q);//如果提供了內置指針q,則指向q所指對象;否則u置空

當unique_ptr將要被銷毀時,可以“特殊地”被拷貝或者賦值,比如下面這種情況

unique_ptr<int> clone(int p)
{
    return unique_ptr<int>(new int(p));  //正確
}

//或者
unique_ptr<int> clone(int p)
{
   unique_ptr<int> ret(new int(p));
   //......
   return ret; //正確
}

weak_ptr類

weak_ptr是一種不控制所指向對像生命周期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數,當最後一個指向對象的shared_ptr被銷毀時,對象會被釋放(即使有weak_ptr指向)。

weak_ptr的操作

weak_ptr<T>w;
weak_ptr<T>w(sp);//使用一個shared_ptr初始化

w = p;               //p可以是一個sp也可是一個wp。賦值後w,p共享對象

w.reset();//置空
w.use_count();       //同w共享對象的shared_ptr的數量
w.expired();        //w.use_count()為0返回true,否則返回false
w.lock();            //expired為true,返回一個空的shared_ptr;否則返回一個指向w的對象的shared_ptr

allocator類

定義在memory中,它提供了一種類型感知的內存分配方式,將內存分配和對象構造分離開來。它分配的內存是原始的、未構造的。基本用法如下:

allocator<string> alloc;  //分配string的allocator對象
auto const p = alloc,allocate(n); //分配n個未初始化的string

allocator的操作

allocator<T> a;
a.allocate(n);

a.deallocate(p,n); /*釋放從T*指針p中地址開始的內存,這塊內存保存了n個類型為T的對象;p必須是
一個先前由allocate返回的指針,且n必須是p創建時所要求的大小。在調用deallocate之後,用戶必須對
每個在這塊內存中創建的對象調用destroy*/

a.construct(p,args);/*p必須是一個類型為T*的指針,指向一塊原始內存;args被傳遞給類型為T的構造函數,用來在p指向的內存中構造一個對象*/

a.destroy(p); //p為T*類型的指針,此算法對p所指向的對象執行析構函數

allocator分配的內存是未構造的,所以我們必須用construct構造對象,並且只能對構造了的對象執行destroy操作。

銷毀的參考代碼如下:

while(q != p)
   alloc.destroy(--q);

 一旦所有元素被銷毀後,就可以重新使用這部分內存來保存其他的string,也可以使用 alloc.deallocate(p,n)來釋放內存。

參考資料

《C++ Primer 第5版》 電子工業出版社 作者:【美】 Stanley B. Lippman && Josee Lajoie && Barbara E.Moo

C++相關:動態內存和智能指針