程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++智能指針

C++智能指針

編輯:C++入門知識

C++智能指針


直接管理內存

什麼時候需要直接管理

簡而言之,當內存分配在棧上時,不需要直接管理,而當內存分配在堆上時則需要手動回收,或者等到堆上內存分配滿了觸發了自動回收機制。
一個由C/C++編譯的程序占用的內存分為以下幾個部分

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

例子程序

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

//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指向一個動態分配的,未初始化的無名對象

可以是使用直接初始化方式來初始化一個動態分配的對象。

int *pi=new int(1024);
string *ps=new string(10,’9’);

也可以對動態分配的對象進行值初始化,只需在類型名之後跟一對空括號即可:

string *ps1=new string;         //默認初始化為空string
string *ps=new string();            //值初始化為空string

動態分配const對象
類似於其他任何const對象,一個動態分配的const對象必須進行初始化。對於一個定義了默認構造函數的類類型,其const動態對象可以隱式初始化,而其他類型的對象就必須顯式初始化。

內存耗盡

一旦一個程序用光了它所有可用的內存,new表達式就會失敗。默認情況下,如果new不能分配所要求的內存空間,他會拋出一個類型為bad_alloc的異常。我們可以改變使用new的方式來阻止他拋出異常:

int *p1=new int;            //如果分配失敗,new拋出std::bad_alloc
int *p2=new (nothrow) int   //如果分配失敗,new返回一個空指針

釋放動態內存

delete p;                   //p必須指向一個動態分配的對象或是一個空指針

釋放一塊並非new分配的內存,或者將相同的指針值釋放多次,其行為是未定義的。

int i,*pil=&i,*pi2=nullptr;
double *pd=new double(33),*pd2=pd;
delete i;               //錯誤:i不是一個指針
delete pil;         //未定義:pil指向一個局部變量
delete pd;          //正確
delete pd2;         //未定義:pd2指向的內存已經被釋放了
delete pi2;         //正確:釋放一個空指針總是沒有錯誤的

對於通過內置指針類型來管理的動態對象,直到被顯式釋放之前他都是存在的。
在delete之後,指針就變成了空懸指針,即,指向一塊曾經保存數據對象但現在已經無效的內存的指針。
如果我們需要保留指針,可以在delete之後將nullptr賦予指針,這樣就清除地指出指針不指向任何對象。

智能指針

智能指針與常規指針的重要區別是它負責自動釋放所指向的對象,兩種智能指針的區別在於管理底層指針的方式:
shared_ptr允許多個指針指向同一個對象;unique_ptr則“獨占”所指向的對象。標准庫還定義了一個名為week_ptr的伴隨類,它是一種弱引用,指向shared_ptr所管理的對象。這三種類型都定義在memory頭文件中。

shared_ptr和unique_ptr都支持的操作

操作 說明 shared_ptr sp , unique_ptr 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 p1;
shared_ptr> p2;

shared_ptr獨有的操作

操作 說明 make_shared(args) 返回一個shared_ptr,指向一個動態分配的類型為T的對象。使用args初始化此對象 shared_ptrp(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 p3=make_shared(42);
auto p6=make_shared>();

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 p1=new int(1024);       //錯誤
shared_ptr p2(new int(1024));      //正確
shared_ptr clone(int p){
    return new int(p);                  //錯誤
}
shared_ptr clone(int p){
    return shared_ptr(new int(p)); //正確
}

定義和改變shared_ptr的其他方法

操作 說明 shared_ptr p(q) p管理內置指針q所指向的對象;q必須指向new分配的內存,且能夠轉換為T*類型 shared_ptr p(u) p從unique_ptr u那裡接管了對象的所有權;將u置為空 shared_ptr p(q,d) p接管了內置指針q所指向的對象的所有權。q必須能轉換為T*類型。p將使用可調用對象d來代替delete shared_ptr 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 ptr){}

process的參數是傳值方式傳遞的,因此實參會被拷貝到ptr中。拷貝一個shared_ptr會遞增其引用計數,因此,在process運行過程中,引用計數值至少為2.當process結束時,ptr的引用計數會遞減,但不會變為0.因此當局部變量ptr被銷毀時,ptr指向的內存不會被釋放。
正確方式是傳遞給它一個shared_ptr:

shared_ptr p(new int(42));     //引用計數為1
process(p);                     //

雖然不能傳遞給process一個內置指針,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內置指針顯式構造的。但是,這樣做很可能會導致錯誤:

int *x(new int(1024));              //危險:x是一個普通指針,不是一個智能指針
process(x);                     //錯誤
process(shared_ptr(x));        //合法的,但內存會被釋放
int j=*x;                           //未定義的:x是一個空懸指針

不要使用get初始化另一個智能指針或為智能指針賦值
智能指針類型頂一個了一個名為get的函數,它返回一個內置指針,指向智能指針管理的對象。此函數是為了這樣一種情況兒設計的:我們需要向不能使用智能指針的代碼傳遞一個內置指針。使用get返回的指針的代碼不能delete此指針。

shared_ptr p(new int(42)); //引用計數為1
int *q=p.get();             //正確:但使用q時要注意,不要讓它管理的指針被釋放
{
//未定義:兩個獨立的shared_ptr指向相同的內存
    shared_ptr(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 p1;      //可以指向一個double的unique_ptr
unique_ptr p2(new int(42));//p2指向一個值為42的int

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

操作 說明 unique_ptr u1,unique_ptr u2 空unique_ptr,可以指向類型為T的對象,u1會使用delete來釋放它的指針;u2會使用一個類型為b的可調用對象來釋放它的指針 unique_ptr 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 p2(p1.release());        //release將p1置為空
unique_ptr 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 w 空weak_ptr可以指向類型為T的對象 weak_ptr 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 up(new int[10]);
up.release();                       //自動用delete[]銷毀其指針

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

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

allocator類

allocator類

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

allocator 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必須指向足夠大的為構造的原始內存,能夠容納給定數量的對象

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved