c++ primer 第五版 筆記 第十二章

第二十章 動態內存

因翻譯太耗時,現做筆記如下:

12.1 動態內存和智能指針

new:在動態內存中爲對象分配空間並返回一個指向該對象的指針

delete:接受一個動態內存對象的指針,銷燬該對象,並釋放與之關聯的內存

使用動態內存容易出錯,主要是容易忘記釋放對象,和多次釋放。

因此,提供了兩種智能指針來管理動態內存。

shared_ptr:允許多個指針指向同一個對象

unique_ptr:一次只能一個指針指向對象

weak_ptr:一個弱引用,指向shared_ptr所管理的對象

12.1.1 shared_ptr

shared_ptr<string> p1;//shared_ptr,可以指向string
shared_ptr<list<int>> p2;//shared_ptr,可以指向int的list

默認初始化的智能指針保存着一個空指針。

智能指針的使用方法與普通的指針類似。解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用了智能指針,效果就是檢查他是否爲空

if(p1 && p1->empty())
    *p1 = "hi";

下表列出了shared_ptr和unique_ptr都支持的操作

在這裏插入圖片描述

下表列出了shared_ptr才支持的操作

在這裏插入圖片描述

make_shared函數

該函數在動態內存中分配內存,並用給定的參數初始化它。

//p3指向一個值爲42的int
shared_ptr<int> p3 = make_shared<int>(42);

//p4指向一個值爲9999999999的string
shared_ptr<string> p4 = make_shared<string>(10,'9');

//p5指向一個值初始化的int,值爲0
shared_ptr<int> p5 = make_shared<int>();

注意:make_shared接受的參數,必須和需要構造的類型的某一個構造函數相同。

shared_ptr的拷貝和賦值

當進行拷貝後者賦值操作的時候,每個shared_ptr都會記錄有多少個shared_ptr指向相同的對象。

auto p = make_shared<int>(42);
//p和q指向相同的對象,此對象有兩個引用者
auto q(p);

我們可以認爲,每一個shared_ptr對象都有一個關聯的計數器,稱之爲引用計數。無論何時拷貝一個shared_ptr,計數器都會增加。
當給shared_ptr賦予一個新值或是shared_ptr被銷燬時,計數器就會遞減。

一旦一個shared_ptr的計數器變爲0,他就會自動釋放自己所管理的對象。

auto r = make_shared<int>(42);

r=q;
//給r賦值,令他指向另外一個地址
//遞增q指向的對象
//遞減r原來指向的對象的引用計數
//當r原來指向的對象已沒有引用者,會自動釋放

shared_ptr自動銷燬所管理的對象

當引用計數變爲0時,shared_ptr釋放所指向的對象,在釋放之前,會調用對象的析構函數。

12.1.2 直接管理內存

使用new動態分配和初始化對象

在自由空間分配的內存是無名的,因此new無法爲其分配的對象命名,而是返回一個指向該對象的指針。

int *pi = new int;//pi指向一個動態分配的,未初始化的無名對象

默認情況下,動態內存分配的對象是默認初始化的。因此對於內置類型和組合類型的對象其值是未定義的。而類類型的對象,則使用默認構造函數進行初始化

string *ps = new string;//初始化爲空
int *pi = new int; //pi指向一個未初始化的int

還可以進行直接初始化和列表初始化如下:

int *pi = new int(1024);//pi指向的對象的值爲1024
string *ps = new string(10,'9');//*ps爲999999999

vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

還可以進行值初始化,如下:

string *ps1 = new string;//默認初始化
string *ps = new string() ;//值初始化
int *pi1 = new int;//默認初始化,*pi1的值未定義
int *pi2 = new int();//值初始化,爲0

注意上面的代碼,對於類類型來說,值初始化和默認初始化,都會調用他們的默認構造函數,最後的結果是相同的。而對於內置類型來說,默認初始化和值初始化就不一樣,默認初始化,其值是未定義的。

動態分配const對象

const int *pci = new const int(1024);

const string *pcs = new const string;

注意:如果new失敗,會拋出一個bad_alloc的異常。可以改變new的方式來阻止這種情況

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

上例子中,這種new稱之爲,定位new。定位new允許我們向其傳遞額外的參數。

這種new如果分配失敗,則返回一個空指針。

釋放動態內存

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

使用new和delete管理動態內存存在三個常見的問題:

  1. 忘記delete內存。
  2. 使用已經釋放的內存
  3. 同一塊內存釋放多次

delete之後重置指針

當我們delete一個指針之後,指針就變成無效的了。而此時指針的值還依然保存着
因此,如果不小心使用了這個指針,那麼就會使用一個已經釋放了的內存,所以
delete指針之後,需要將這個指針重置爲空。

12.1.3 shared_ptr和new結合使用

可以使用new返回的指針,來初始化shared_ptr

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

接受指針參數的智能指針的構造函數是explicit的,因此,注意下面的代碼

shared_ptr<int> p1 = new int(1024);//錯誤:必須使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正確:使用了直接初始胡形式

下面給出定義和改變shared_ptr的其他方法

在這裏插入圖片描述

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

考慮下面的例子:

void porcess(shared_ptr<int> ptr){
    //使用ptr
}

int *x(new int(1024));//危險:x是一個普通指針,並不是一個智能指針
process(x);//錯誤:不能將int* 轉換爲一個shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但是內存會被釋放
int j = *x;//未定義的:x是一個無效的指針

在上面例中,構造了一個臨時的shared_ptr傳遞給process函數,一旦函數運行
結束,那麼這個臨時的shared_ptr將被銷燬,它指向的對象將會釋放

此時,x指向的對象已經被釋放,x是一個無效的指針。

也不要使用get初始化另外一個智能指針或爲智能指針賦值

智能指針提供了一個get的成員,它返回智能指針指向的對象的地址。
這個成員函數是爲了如下情況進行設計的:需要向不使用智能指針的代碼
傳遞一個內置指針。

現在,思考下面的代碼

shared_ptr<int> p(new int(42));
int *q = p.get();//正確,但使用q的時候請注意,不要釋放它
{
    shared_ptr<int> (q);
}//程序塊結束,q指向的對象,被釋放
int foo = *p;//未定義:p指向了一個已經被釋放的內存

其他的shared_ptr操作

reset

p = new int(1024);//錯誤:不能將一個指針賦值給一個shared_ptr
p.reset(new int(1024));//正確:p指向一個新對象

reset與賦值類似,會更新引用計數,如果需要會釋放p指向的對象。

12.1.4 智能指針和異常

void f(){
    shared_ptr<int> sp(new int(42));
    //其他代碼
}

函數退出可能兩種情況:1.正常結束;2.發生了異常。

無論哪種情況,局部對象都會被銷燬,因此,sp會被銷燬,它所指向的內存
也會被正確的釋放。

如果上面的程序,變成下面這樣:

void f(){
    int *ip = new int(42);
    //其他代碼
    delete ip;
}

在上面例子中,如果正常結束,那麼ip會被正確釋放,但是如果遇到異常,ip將不會正確釋放

使用自定義的釋放操作

默認情況下,shared_ptr假定他們指向的都是動態內存。因此當一個shared_ptr
被銷燬的時,它默認對他管理的指針進行delete操作。

另外,我們還可以自定義自己的刪除操作,來代替默認的delete操作。

當創建一個shared_ptr時,可以傳遞一個表示刪除的函數,代替默認的delete操作。

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

void f(destination &d /*其他參數*/){
    connection c = connect(&d);
    shared_ptr<connection> p(&c,end_connection);
    //其他代碼
}

當p被銷燬的時候,不會直接調用delete函數,而是調用end_connection函數。
這樣,不管f函數是正常結束 還是異常退出,end_connection必定
會被調用。

上面函數的寫法可能看不出來有什麼好處,那麼作爲對比,請看下面的寫法

void f(destination &d,/*其他參數*/){
    connection c = connect(&d);
    //其他代碼
    //最後,必須調用disconnect()進行資源的釋放
}

那麼上面的例子,一旦中間出現異常,disconnect將不會被調用

爲了能夠正確的使用智能指針,請堅持一些基本規範:

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

12.1.5 unique_ptr

與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定的對象

當unique_ptr被銷燬的時候,他所指的對象也被銷燬

下表列出了unique_ptr特有的操作。與shared_ptr相同的操作在表12.1中

在這裏插入圖片描述

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

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

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

unique_ptr<string> p3;
p2 = p2;//錯誤:unique_ptr不支持賦值

雖然不能拷貝和賦值unique_ptr,但是可以通過release或者reset
將指針的所有權從一個轉移到另外一個。

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

不能拷貝uniqque_ptr 的規則有一個例外:我們可以拷貝或者賦值一個將要被
銷燬的unique_ptr.最常見的例子是從函數返回一個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;
}

向unique_ptr中傳遞刪除器

//p指向一個類型爲objT的對象,並使用一個類型爲delT的對象釋放objT對象
//它會調用一個名爲fcn的delT類型對象
unique_ptr<objT,delT> p(new objT,fcn):

更具體的例子如下:

void f(destination &d/*其他參數*/){
    connection c= connect(&d);
    unique_ptr<connection,decltype(end_connection) *> p(&c,end_connection);
    //其他代碼
}

12.1.6 weak_ptr

weak_ptr是一種不控制所指對象生命週期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。

一旦最後一個指向對象的shared_ptr被銷燬,對象就會被釋放。

下圖是weak_ptr的常見操作

在這裏插入圖片描述

當創建一個weak_ptr時,要用一個shared_ptr來初始化

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);//wp弱共享p,p的引用計數未改變

因爲對象可能不存在,所以不能直接通過weak_ptr來訪問對象,需要調用其lock函數。如果存在,這個函數返回一個shared_ptr對象,然後使用這個對象來訪問。

if(shared_ptr<int> np = wp.lock()){
    //np和p共享同一個對象
}

12.2 動態數組

new還可以一次分配多個對象。

12.2.1 new和數組

int *pia = new int[get_size()];

爲了分配一個數組,需要在類型名後面跟上一個方括號,在方括號內寫上,需要的個數,成功返回第一個對象的地址。

typedef int arrT[42];
int *p = new arrT;//分配一個含有42個元素的int數組

由上面可以看到,這種分配方式,返回的類型並不是數組類型,而是數組中元素類型的指針。

初始化動態分配對象的數組

int *pia = new int[10];
int *pia2 = new int[10]();//值初始化

string *psa = new string[10];
string *psa2 = new string[10]();//值初始化

還可以進行列表初始化

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
//前4個,用給定的值,進行初始化,剩下的進行值初始化
string *psa3 = new string[]{"a","an","the",string(3,'x')};

當 new[n] n等於0時,依然合法。

當new分配一個大小爲0的數組時,new返回一個合法的非空指針。此指針保證與new返回的其他任何指針都不相同。對於零長度的數組來說,此指針就像尾後指針一樣。

釋放動態數組

在delete後面加上方括號

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

第二條語句銷燬pa指向的數組中的元素,並釋放對應的內存。數組中的元素按照逆序進行銷燬。

上面的方括號是必須的,如果忽略方括號,行爲是未定義的。

typedef int arrT[42];
int *p = new arrT;
delete [] p;//此處的方括號也不能省略

智能指針和動態數組

unique_ptr

unique_ptr<int[] > up(new int[10]);
up.release();//自動調用delete[] 銷燬其指針

當unique_ptr指向數組的時候,不能直接使用點運算符和尖頭運算符。但是她能夠使用下標運算符。

下圖給出了支持的操作。

在這裏插入圖片描述

shared_ptr

shared_ptr不支持動態數組,如果要使用動態數組,必須自定義刪除器。

shared_ptr<int> sp(new int[10],[](int *p){delete[] p;});
sp.reset();//使用我們提供的lambada釋放數組,它使用delete[]

如果上面沒有提供刪除器,那麼結果將是未定義的。

如果要訪問此種的元素,下面是一個示例:

for(size_t i = 0;i != 10;++i){
    *(sp.get() + i ) = i;//使用get獲取內置指針
}

12.2.2 allocator類

new將內存分配和對象構造組合在了一起,有時候,我們希望這兩者能夠分開,此時可以使用allocator類。比如下面的例子,就不需要將內存分配和對象構造組合在一起

string *const p = new string[n];

string s;
string *q = p;
while(cin >> s && q != p +n)
    *q++ = s;
const size_t  size = q -p ;
deletep[] p;

上面例子進行了兩處賦值:1.new 數組時,2.while循環體中讀取到數據之後。

allocator類

allocator分配的內存是一種,原始的,未構造的。下表列出了allocator的操作

在這裏插入圖片描述

allocator分配未構造的內存

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

allocator在未初始化的內存中構造對象

allocator分配完內存之後,此時可以按照需要進行對象的構造。

auto q = p;

alloc.construct(q++);//*q爲空字符串
alloc.construct(q++,10,'c');//*q爲cccccccccc
alloc.construct(q++,"hi");//*q爲hi

//q指向最後構造元素之後的位置

construct接受一個指向未初始化內存的指針,和零個或者多個額外的參數,這些參數,必須和需要構造的對象的某一個構造函數的參數類型相匹配。他們跟make_shared的參數類似。

cout << *p << endl;//正確:使用string的輸出運算符
cout << *q << endl;//錯誤:q指向未構造的內存

注意:爲了使用allocate返回的內存,必須使用construct構造對象。使用未構造的內存,其行爲是未定義的。

當使用完對象之後,必須對每個構造的對象調用destroy來銷燬他們。函數destroy接受一個指針,這個指針指向構造的對象。。

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

注意:只能對真正構造了的元素進行destroy操作

一旦destroy完成之後,就可以再次使用allocate方法,進行構造對象。

釋放未初始化的內存,需要調用deallocate函數,此函數,將未初始化的內存,返回給系統

alloc.deallocate(p,n);

傳遞給deallocate的p是allocated返回的值,n爲allocated傳遞進去的值。

拷貝和填充未初始化內存的算法

算法如下表:

在這裏插入圖片描述

例子如下:

auto p = alloc.allocate(vi.size() *2);
auto q = uninitialized_copy(vi.begin(),vi.end(),p);
uninitialized_fill_n(q.vi.size(),42);

本章完

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