[C++]動態內存

動態內存和智能指針

靜態內存:局部static對象,類static數據成員,定義在任何函數之外的變量

棧內存:保存定義在函數內的非static對象

堆(自由空間):動態分配的對象

頭文件:memory

智能指針,自動的釋放所指向的對象

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

unique_ptr:獨佔所指向的對象

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


shared_ptr類

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

使用前進行條件判斷,保證p1不是指針,p1指向了一個空string

shared_ptr<string> p1;
if(p1 && p1 -> empty())
{
    *p1 = "hello";
}

shared_ptr和unique_ptr都支持的操作

p -> mem;    //獲取內部成員
p.get();     //返回p中保存的指針,若智能指針釋放了其對象,返回的指針所指向的對象也就消失了
swap(p,q);
p.swap(q)    //交換p,q

shared_ptr獨有的操作

make_shared<T>(args);    //返回一個shared_ptr,指向一個動態分配的類型爲T的對象,用args初始化它
shared_ptr<T>p(q);       //p是shared_ptr q的拷貝,會增加q中的計數器
p = q;                   //保證指針能夠相互轉換,此操作會遞減p的引用計數,遞增q的引用計數;p的引用計數爲0會被刪除
p.unique();              //p.use_count() == 1
p.use_count();           //返回與p共享對象的智能指針數量,很慢,用於調試

通常使用make_shared函數分配使用,若沒傳遞任何參數,會默認進行值初始化

拷貝和賦值的過程本質上都是計數器調整

shared_prt會調用析構函數來銷燬對象

可以使用函數返回shared_ptr類型,return語句會向此函數的調用者返回一個拷貝,此時原對象隨着函數的銷燬而被銷燬,但此時引用計數值並不爲零,所以對應內存還會保留。

shared_ptr<int> shaptr_test(int x)
{
    auto t = make_shared<int>(x);
    return t;
}
int main()
{
    shared_ptr<int> p1 = shaptr_test(5);
    cout << *p1 << endl;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
}

所以,注意要保證在無用之後沒有保留,否則會浪費內存。尤其容易忽略的是,把shared_ptr保存在vector中,對vector重排後,忘記erase掉無用元素。


使用動態內存的原因

(1)程序不知道自己需要使用多少對象:vector

(2)程序不知道所需對象的準確類型

(3)程序需要在多個對象間共享內存

在類中定義share_ptr成員,實現多個對象間的共享內存


直接管理內存

運算符new, delete

int *i = new int(1024);
int *s = new string(5, '9');
vector<int> *v = new vector<int>{0,1,2};
int *i2 = new int();    //值初始化,*i2爲0
int *i3 = new int;      //默認初始化,*i3的值未定義

對於定義了自己的構造函數的類,值初始化都會通過默認的構造函數來初始化;對於內置類型纔會有區別。

使用auto自動判斷

auto p1 = new auto(obj);        //p1指向一個與obj類型相同的對象,使用obj的值進行初始化

用new分配const對象

const int *p = new const int(10);

雖然不能改變const動態對象,但可以用delete來銷燬

對於定義了默認構造函數的類類型,其const對象可以隱式初始化,其它則必須顯式初始化

內存耗盡時,new會拋出std::bad_alloc異常

使用定位new傳遞額外參數nothrow,當分配失敗時,返回一個空指針

int *p = new (nothow) int;

delete之後最好重置指針

在delete之後,很多機器上指針仍然保存着(已經釋放了的)動態內存的地址,此時指針變成了空懸指針。所以,在指針要離開作用域前要釋放它關聯的內存,如果要保留指針,可以在delete之後重置爲nullptr

只是有限的保護,若有多個指針指向同一地址則很難檢測到。


new與shared_ptr結合使用

可以用new初始化shared_ptr

shared_ptr<int> p(new int(35));

接受指針參數的智能指針構造函數是explicit的,故不能講一個內置指針隱式轉換爲智能指針,必須通過顯式的綁定

定義和改變shared_ptr的方法

tr《T

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所指向對象的所有權,使用可調用對象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會釋放,或另p指向q,參數d用於提供delete操作

普通指針和智能指針混合使用易產生的錯誤

void process(shared_ptr<int> ptr)
{
/*……*/
}
/*……*/
int *p(new int(100));
process(shared_ptr<int>(p));
int j = *p;

將臨時的shared_ptr傳入到process後,當調用結束時,臨時對象將被銷燬,引用次數變爲0,p變成空懸指針,此時使用p的行爲是未定義的。

當將一個shared_ptr綁定到一個普通指針時,相當於管理責任的移交,最好不要再使用內置指針。

同樣的,shared_ptr定義了一個get函數,返回一個內置指針,應用場景是:向不能使用智能指針的代碼傳遞一個內置指針,這裏需要注意不要被delete。特別是,永遠不要用get初始化另一個智能指針

在改變底層對象之前,檢查自己是否是當前對象僅有的用戶

if(!p.unique())
    q.reset(new int(*p));
*p += val;


智能指針和異常

使用智能指針,即使程序塊由於異常而過早結束,也能確保在內存不再需要時將其釋放

內置指針則做不到。

對於那些沒有定義析構函數的類,通常要求用戶顯式地釋放所使用的任何資源,這時如果使用shared_ptr來避免內存泄露會默認的使用delete。但對於有些資源並不適用,比如網絡連接,這時可以採用自定義的函數來代替delete。

void end_connection(connection *p) { disconnect(*p); }
/* ……*/
void f(destination &d)
{
    connection c = connect(&d);
    shared_ptr<connection> p (&c, end_connection);
    /* ……*/
}

此時,即便是由於異常而退出,也可以保證connection會被正確光比


unique_ptr

定義

只能將其綁定到一個new返回的指針上

unique_ptr<int> p1;
unique_ptr<int> p2(new int(10));

unique_ptr的對象是獨有的,所以不支持普通的拷貝或賦值

//使用類型爲D的可調用對象d代替delete
unique_ptr<T> u1;
unique_ptr<T, D> u2;
unique_ptr<T, D> u(d);
u = nullptr;    //釋放並置空
u.release();    //放棄控制權,返回當前保存的指針,置空
//釋放原對象,將u指向q的對象
u.reset();
u.reset(q);
u.reset(nullptr);

僅使用release不會釋放內存,而且會丟失原指針,如:p.release()


傳遞和返回unique_ptr

不能拷貝unique_ptr的規則有一個例外:可以拷貝或賦值一個將要被銷燬的unique_ptr。

典型的例子是從函數返回一個unique_ptr



auto_ptr

auto_ptr有拷貝語義,拷貝後源對象變得無效;unique_ptr則無拷貝語義,但提供了移動語義

auto_ptr不可作爲容器元素,unique_ptr可以作爲容器元素

auto_ptr不可指向動態數組(儘管不會報錯,但不會表現出正確行爲),unique_ptr可以指向動態數組


weak_ptr

不控制所指向對象生存期的智能指針,指向一個由shared_ptr管理的對象,不改變shared_ptr的引用計數,一旦shared_ptr被銷燬,weak_ptr沒有任何阻礙的效果。

使用方法

weak_ptr<T> w;
weak_ptr<T> w(sp);    //與shared_ptr sp指向相同對象
w = p;
w.reset();            //w置空
w.use_count();
w.expired();          //w.use_count() == 0
w.lock();             //若expired爲true,返回空shared_ptr,否則返回一個指向w對象的shared_ptr

訪問前注意判定

if(shared_ptr<int> sp = w.lock())
{
    /*……*/
}


動態數組

一次爲很多對象分配內存

new表達式,allocator類

更多的情況還是使用容器


new和數組

int *p = new int[get_size()];    //p指向第一個int

方括號中的大小必須是整型,但不必是常量

由於分配的內存並不是一個數組類型,因此不能對動態數組調用begin或end

關於初始化

int *pi = new int[10];    //10個未初始化
int *pi = new int[10]();    //10個值初始化

還可以使用列表初始化,不能用auto分配數組

有趣的是,可以動態分配一個空數組,使用時除了不能解引用可以進行其它操作,類似於一個尾後迭代器

char arr[0];               //錯誤
char *cp = new char[0];    //正確,但不能解引用

釋放數組

delete [] arr;

即使是使用別名來定義一個數組,釋放時也必須加上[],但是在vs下測試可以不加上[]

typedef int arr[10];    //arr爲10個int的數組的類型別名
int *p = new arr();
delete []p;             //方括號理論上必須

順序是按照駐足中元素的逆序


智能指針和動態數組

可以管理new分配的數組的unique_ptr版本

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

可以使用下標直接訪問

如果希望使用shared_ptr管理一個動態數組,必須提供一個刪除器

shared_ptr<int[]> up_arr(new int[10](), [](int *p) {delete[] p;});
up_arr.reset();

傳遞給一個lambda作爲刪除器。若未提供刪除器,默認情況shared_ptr使用delete來銷燬對象,這與釋放動態數組時沒有加[]產生的問題一樣。

shared_ptr未定義下標運算符,且不支持指針的算術運算,所以訪問只能通過get之後再加減(vs下不支持)


allocator類

將內存分配和構造對象分離,避免浪費。

頭文件memory

是一個模板,分配的內存是原始的、未構造的。

基本操作

allocator<T> a;
a.allocate(n);        //分配內存,保存n個類型爲T的對象,返回起始位置指針
//釋放從指針p出開始的內存,共保存了n個對象,p必須是先前由allocate返回的指針,n必須爲當初構造時的大小
a.deallocate(p, n);
//args被傳遞給T的構造函數,用於在p指向的內存中構造一個對象
a.construct(p ,args);
a.destory(p);        //對p指向的對象析構

分配內存

allocator<string> alloc;
auto p = alloc.allocate(10);
auto q = p;
alloc.construct(p++);                //空字符串
alloc.construct(p++, 5, 'h');        //5個h
alloc.construct(p++, 'hello');

早期版本中不能採用其它構造函數來構造一個元素,只有兩個參數:指針,參數類型的值

逐個destory,但內存並沒有被釋放

while(q != p)
{
    alloc.destory(--p);
}

拷貝和填充

uninitialized_copy(b, e, b2);    //從迭代器b和e間拷貝元素到迭代器b2指定的未構造的原始內存中,假定b2足夠大
uninitialized_copy(b, n, b2);    //拷貝n個元素uninitialized_fill(b, e, t);     //在迭代器b和e指定的原始內存範圍中創建對象,值爲t的拷貝
uninitialized_fill(b, n, t);     //拷貝n個,假定足夠大


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