C++智能指針

直接管理內存

什麼時候需要直接管理

簡而言之,當內存分配在棧上時,不需要直接管理,而當內存分配在堆上時則需要手動回收,或者等到堆上內存分配滿了觸發了自動回收機制。 
關於堆和棧,這篇文章講得淺顯易懂:http://blog.csdn.net/hairetz/article/details/4141043 
一個由C/C++編譯的程序佔用的內存分爲以下幾個部分

  1. 棧區(stack)—— 由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
  2. 堆區(heap)—— 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。
  3. 全局區(靜態區)(static)——全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後由系統釋放。
  4. 文字常量區——常量字符串就是放在這裏的,程序結束後由系統釋放 。
  5. 程序代碼區——存放函數體的二進制代碼。

例子程序

這是一個前輩寫的,非常詳細

//main.cpp    
int   a   =   0;   全局初始化區    
char   *p1;   全局未初始化區    
main() { 
int   b;   棧    
char   s[]   =   "abc";   棧    
char   *p2;   棧    
char   *p3   =   "123456";   123456/0在常量區,p3在棧上。    
static   int   c   =0;   全局(靜態)初始化區    
p1   =   (char   *)malloc(10);    
p2   =   (char   *)malloc(20);    
分配得來得10和20字節的區域就在堆區。    
strcpy(p1,   "123456");   123456/0放在常量區,編譯器可能會將它與p3所指向的"123456" 優化成一個地方。    
}

注意,除了上文的malloc,new分配的內存也在堆中需要手動銷燬。

動態內存

由上文看出,分配在堆上的內存需要手動進行動態分配和釋放,我們將之稱爲動態內存。C++中,動態內存是通過new和delete來進行分配和釋放的。 
new:在動態內存中爲對象分配空間並返回一個指向該對象的指針,我們可以選擇對對象進行初始化。 
delete:接受一個動態對象的指針,銷燬該對象,並釋放與之關聯的內存。 
在自由空間分配的內存是無名的,因此new無法爲其分配的對象命名,而是返回一個指向該對象的指針。

int *pi=new int;        //pi指向一個動態分配的,未初始化的無名對象
可以是使用直接初始化方式來初始化一個動態分配的對象。
操作 說明
shared_ptr<T> sp , unique_ptr<T> up 空智能指針,可以指向類型爲T的對象
p 將p用作一個條件判斷,若p指向一個對象,則爲true
*p 解引用p,獲得它指向的對象
p->mem 等價於(*p).mem
p.get() 返回p中保存的指針。要小心使用,若智能指針釋放了其對象,返回的指針所指向的對象也就消失了
swap(p,q) ,p.swap(q) 交換p和q中的指針

shared_ptr類

創建一個智能指針時,必須提供額外的信息——指針可以指向的類型。

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

shared_ptr獨有的操作

操作 說明
make_shared<T>(args) 返回一個shared_ptr,指向一個動態分配的類型爲T的對象。使用args初始化此對象
shared_ptr<T>p(q) p是shared_ptr q的拷貝;此操作會遞增q中的計數器。q中的指針必須能轉換爲T*
p=q p和q都是shared_ptr,所保存的指針必須能相互轉換。此操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數變爲0,則將其管理的原內存釋放
p.unique() 若p.use_count()爲1,返回true;否則返回false
p.use_count() 返回與p共享對象的智能指針數量;可能很慢,主要用於調試

make_shared函數

最安全的分配和使用動態內存的方法是調用一個名爲make_shared的標準庫函數。此函數在動態內存中分配一個對象並初始化它,返回指向此對象的shared_ptr。

shared_ptr<int> p3=make_shared<int>(42);
auto p6=make_shared<vector<string>>();

shared_ptr的拷貝和賦值 
當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象。 
shared_ptr自動銷燬所管理的對象 
shared_ptr的析構函數會遞減它所指向的對象的引用計數。如果引用計數變爲0,shared_ptr的析構函數就會銷燬對象,並釋放它佔用的內存。 
shared_ptr還會自動釋放相關聯的內存 
return會對shared_ptr指針的引用次數進行遞增操作。 
使用了動態生存期的資源的類 
程序使用動態內存出於以下三種原因之一: 
1. 程序不知道自己需要使用多少對象 
2. 程序不知道所需對象的準確類型 
3. 程序需要在多個對象間共享數據

shared_ptr和new結合使用

接受指針參數的智能指針構造函數是explicit的,我們不能將一個內置指針隱式轉換爲一個智能指針,必須使用直接初始化形式來初始化一個智能指針:

shared_ptr<int> p1=new int(1024);       //錯誤
shared_ptr<int> p2(new int(1024));      //正確
shared_ptr<int> clone(int p){
    return new int(p);                  //錯誤
}
shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p)); //正確
}

定義和改變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將用可調用對象d來代替delete
p.reset()p,reset(q)p,reset(q,d) 若p是唯一指向其對象的shared_ptr,reset會釋放此對象。若傳遞了可選的參數內置指針q,會令p指向q,否則將p置爲空。若還傳遞了參數d,將會調用d而不是delete來釋放q

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

void process(shared_ptr<int> ptr){}
process的參數是傳值方式傳遞的,因此實參會被拷貝到ptr中。拷貝一個shared_ptr會遞增其引用計數,因此,在process運行過程中,引用計數值至少爲2.當process結束時,ptr的引用計數會遞減,但不會變爲0.因此當局部變量ptr被銷燬時,ptr指向的內存不會被釋放。 
正確方式是傳遞給它一個shared_ptr:
shared_ptr<int> p(new int(42));     //引用計數爲1
process(p);                     //
雖然不能傳遞給process一個內置指針,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內置指針顯式構造的。但是,這樣做很可能會導致錯誤:
int *x(new int(1024));              //危險:x是一個普通指針,不是一個智能指針
process(x);                     //錯誤
process(shared_ptr<int>(x));        //合法的,但內存會被釋放
int j=*x;                           //未定義的:x是一個空懸指針
不要使用get初始化另一個智能指針或爲智能指針賦值 
智能指針類型頂一個了一個名爲get的函數,它返回一個內置指針,指向智能指針管理的對象。此函數是爲了這樣一種情況兒設計的:我們需要向不能使用智能指針的代碼傳遞一個內置指針。使用get返回的指針的代碼不能delete此指針。
shared_ptr<int> p(new int(42)); //引用計數爲1
int *q=p.get();             //正確:但使用q時要注意,不要讓它管理的指針被釋放
{
//未定義:兩個獨立的shared_ptr指向相同的內存
    shared_ptr<int>(q);
}//程序塊結束,q被銷燬,它指向的內存被釋放
int foo=*p;                 //未定義:p指向的內存已經被釋放了

智能指針和異常

如果使用智能指針,即使程序塊過早結束,智能指針類也能確保在內存不再需要時將其釋放。 
智能指針和啞類 
使用我們自己的釋放操作 
爲了正確使用智能指針,我們必須堅持一些基本規範:

  • 不適用相同的內置指針初始化(或reset)多個智能指針。
  • 不delete get()返回的指針。
  • 不使用get()初始化或reset另一個智能指針。
  • 如果你使用get()返回的指針,記住當最後一個對應的智能指針銷燬後,你的指針就變爲無效了。
  • 如果你用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器。

unique_ptr

一個unique_ptr“擁有”它所指向的對象。與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定對象。當unique_ptr被銷燬時,它所指向的對象也被銷燬。 
與shared_ptr不同,沒有類似make_shared的標準庫函數返回一個unique_ptr。當我們定義一個unique_ptr時,需要將其綁定到一個new返回的指針上。尅死shared_ptr,初始化unique_ptr必須採用直接初始化形式:

unique_ptr<double> p1;      //可以指向一個double的unique_ptr
unique_ptr<int> p2(new int(42));//p2指向一個值爲42的int

由於一個unique_ptr擁有它指向的對象,因此unique_ptr不支持普通的拷貝或賦值操作 
unique_ptr操作

操作 說明
unique_ptr<T> u1,unique_ptr<T,D> u2 空unique_ptr,可以指向類型爲T的對象,u1會使用delete來釋放它的指針;u2會使用一個類型爲b的可調用對象來釋放它的指針
unique_ptr<T,D> u(d) 空unique_ptr,指向類型爲T的對象,用類型爲D的對象d代替delete
u=nullptr 釋放u指向的對象,將u置爲空
u.release() u放棄對指針的控制權,返回指針,並將u置爲空
u.reset() 釋放u指向的對象
u.reset(q) ,u.reset(nullptr) 如果提供了內置指針q,令u指向這個對象;否則將u置爲空
//將所有權從p1轉移給p2
unique_ptr<string> p2(p1.release());        //release將p1置爲空
unique_ptr<string> p3(new string(“Trex”));
//將所有權從p3轉移給p2
p2.reset(p3.release());;                    //reset釋放了p2原來指向的內存

傳遞unique_ptr參數和返回unique_ptr 
不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷燬的unique_ptr。最常見的例子是從函數返回一個unique_ptr。 
向unique_ptr傳遞刪除器

weak_ptr

weak_ptr指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。 
weak_ptr

操作 說明
weak_ptr<T> w 空weak_ptr可以指向類型爲T的對象
weak_ptr<T> w(sp) 與shared_ptr sp指向相同對象的weak_ptr。T必須能轉換爲sp指向的類型
w=p p可以是一個shared_ptr或一個weak_ptr。賦值後w與p共享對象
w.reset() 將w置爲空
w.use_count() 與w共享對象的shared_ptr的數量
w.expired() 若w.use_count()爲0,返回true,否則返回false
w.lock() 如果expired爲true,返回一個空shared_ptr;否則返回一個指向w的對象的shared_ptr

動態數組

new和delete運算符一次分配/釋放一個對象,但某些應用需要一次爲很多對象分配內存的功能。例如,vector和string都是在連續內存中保存它們的元素,因此,當容器需要重新分配內存時,必須一次性爲很多元素分配內存。 
爲了支持這種需求,C++語言和標準庫提供了兩種一次分配一個對象數組的方法:C++語言定義了動態數組的new方式;標準庫中包含了一個名爲allocator的類。

new和數組

int *pia=new int[get_size()];       //pia指向第一個int
分配一個數組會得到一個元素類型的指針 
雖然我們通常稱new T[]分配的內存爲“動態數組”,但我們用new分配一個數組時,並未得到一個數組類型的對象,而是一個數組元素類型的指針。 
由於分配的內存並不是一個數組類型,因此不能對動態數組調用begin或end。這些函數使用數組維度來返回指向首元素和尾後元素的指針。出於相同的原因,也不能用範圍for語句來處理動態數組中的元素。 
初始化動態分配對象的數組 
可以對數組中的元素進行值初始化,方法是在大笑之後跟一對空括號:
int *pia=new int[10];           //10個未初始化的int
int *pia2=new int[10]();            //10個值初始化爲0的int
動態分配一個空數組是合法的 
雖然我們不能創建一個大小爲0的靜態數組對象,但當n等於0時,調用new[n]是合法的:
char arr[0];                //錯誤
char *cp=new char[0];       //正確
釋放動態數組 
爲了釋放動態數組,我們使用一種特殊形式的delete——在指針前加上一個空方括號對:

delete p;               //p必須指向一個動態分配的對象或爲空
delete [] pa;           //pa必須指向一個動態分配的數組或爲空

數組中的元素按逆序被銷燬。 
智能指針和動態數組 
爲了用一個unique_ptr管理動態數組,我們必須在對象類型後面跟一對空方括號:

//up指向一個包含10個未初始化int的數組
unique_ptr<int[]> up(new int[10]);
up.release();                       //自動用delete[]銷燬其指針

指向數組的unique_ptr 
指向數組的unique_ptr不支持成員訪問運算符(點和箭頭運算符) 
其他unique_ptr操作不便

操作 說明
unique_ptr<T[]> u u可以指向一個動態分配的數組,數組元素類型爲T
unique_ptr<T[]> u(p) u指向內置指針p所指向的動態分配的數組。p必須能轉換爲類型T*
u[i] 返回u擁有的數組中的位置i處的對象,u必須指向一個數組

allocator類

allocator類

標準庫allocator類定義在頭文件memory中,它幫助我們將內存分配和對象構造分離開來。它提供一種類型感知的內存分配方法,它分配的內存是原始的、未構造的。 
類似vector,allocator是一個模板。爲了定義一個allocator對象,我們必須指明這個allocator可以分配的對象類型。當一個allocator對象分配內存時,他會根據給定的對象類型來確定恰當的內存大小和對齊位置:

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

標準庫allocator類及其算法

操作 說明
allocator a 定義了一個名爲a的allocator對象,它可以爲類型爲T的對象分配內存
a.allocate(n) 分配一段原始的、爲構造的內存,保存n個類型爲T的對象
a.deallocate(p,n) 釋放從T*指針p中地址開始的內存,這塊內存保存了n個類型爲T的對象;p必須是一個先前由allocate返回的指針,且n必須是p創建時所要求的大小。在調用deallocate之前,用戶必須對每個在這塊內存中創建的對象調用destroy
a.construct(p,args) p必須是一個類型爲T*的指針,指向一塊原始內存;arg被傳遞給類型爲T的構造函數,用來在p指向的內存中構造一個對象
a.destroy(p) p爲T*類型的指針,此算法對p指向的對象執行西溝函數

allocator分配爲構造的內存 
allocator分配的內存是未構造的,我們按需要在此內存中構造對象。

auto q=p;                   //q指向最後構造的元素之後的位置
alloc.construct(q++);           //*q爲空字符串
alloc.construct(q++,10,’c’);    //*q爲cccccccccc
alloc.construct(q++,”hi”);      //*q位hi!

爲了使用allocate返回的內存,我們必須用construct構造對象。使用爲構造的內存,其行爲是未定義的。 
我們只能對真正構造了的元素進行destroy操作 
拷貝和填充未初始化內存的算法 
它們都定義在頭文件memory中

allocator算法

這些函數在給定目的位置創建元素,而不是由系統分配內存給它們。

操作 說明
uninitialized_copy(b,e,b2) 從迭代器b和e指出的輸入範圍中拷貝元素到迭代器b2指定的爲構造的原始內存中。b2指向的內存必須足夠大,能容納輸入序列中元素的拷貝
uninitialized_copy(b,n,b2) 從迭代器b指向的元素開始,拷貝n個元素到b2開始的內存中
uninitialized_fill(b,e,t) 在迭代器b和e指定的原始內存範圍中創建對象,對象的值均爲t的拷貝
uninitialized_fill_n(b,n,t) 從迭代器b指向的內存地址開始創建n個對象。b必須指向足夠大的爲構造的原始內存,能夠容納給定數量的對象



轉載請註明出處:http://blog.csdn.net/ylbs110/article/details/51049586








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章