C++相關:動態內存和智能指針
前言
在C++中,動態內存的管理是通過運算符new和delete來完成的。但使用動態內存很容易出現問題,因為確保在正確的時間釋放內存是及其困難的。有時候我們會忘記內存的的釋放,這種情況下就會產生內存泄露;有時候又會在尚有指針引用的情況下就用delete釋放了內存,這樣又會產生引用非法內存的指針(野指針)。因此,為了更容易地使用動態內存,C++標準庫提供了兩種智能指針,shared_ptr和unique_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++相關:動態內存和智能指針