動態內存與智能指針

前言
java


1.生命週期安全

全局對象:程序啓動時分配,程序結束時銷燬函數

局部自動對象:執行流進入其定義的塊時分配,執行流退出其定義的塊時銷燬spa

局部static對象:程序啓動時分配(但在其定義的塊或做用域內起做用),程序結束時銷燬指針

動態分配的對象:生命週期與建立地點無關,只有當顯示的被釋放時,纔會被銷燬對象

智能指針:標準庫定義,用於管理動態分配的內存,當一個對象應該被釋放時,指向它的智能指針能夠確保自動的釋放它生命週期


2.內存分類內存

靜態內存:用於存儲局部static對象、類的static數據成員、全局變量
ci

棧內存:用於存儲局部非static對象資源

堆內存(內存池):用於存儲動態分配的對象——動態分配的對象,其生命週期由程序控制,例如使用new或delete


3.動態內存

動態內存使用過程當中容易產生的問題 

內存泄露:使用後忘記釋放內存

引用非法內存:在尚有指針引用內存的狀況下就釋放它

使用動態內存的緣由: 

       程序不知道本身須要使用多少對象

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

       程序須要在多個對象間共享底層數據——若兩個對象共享底層數據,當某個對象銷燬時,咱們不能單方面的銷燬底層數據


4.智能指針

智能指針和常規指針直接的區別:智能指針可以自動釋放所指向的內存(相似java中的垃圾回收機制),而常規指針不能。

智能指針默認初始化爲nullptr

智能指針分類 

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

     unique_ptr:獨佔所指向的對象


5.智能指針和異常

若是使用智能指針,即便程序非正常結束(異常),只要程序離開做用域,局部變量(智能指針)就會被銷燬,若此時引用計數爲0,則所指對象或內存也會被銷燬。

若是使用普通指針,則當程序非正常結束(異常)時,因爲未進行delete操做,所以指針所指的對象或內存未被釋放,形成內存泄露。


6.使用智能指針的基本規範(智能指針的陷阱)

不使用相同的內置指針值初始化(或reset)多個智能指針,不然容易屢次釋放同一對象或內存——由於這樣產生的智能指針是相互獨立的


int *p = new int(1);

shared_ptr<int> p1(p);

shared_ptr<int> p2(p);  // 此時p1和p2相互獨立且引用計數都爲1,離開做用域時會釋放同一對象兩次

不delete``get()返回的指針,不然離開做用域時又要釋放一次內存


{

    shared_ptr<int> p = make_shared<int>(1);

    delete p.get();     // 釋放一次內存

}   // 又要釋放一次內存

不使用get()初始化或reset另外一個智能指針,不然這兩個智能指針是獨立的,離開做用域時會釋放同一內存兩次


{

    shared_ptr<int> p1 = make_shared<int>(1);

    shared_ptr<int> p2(p1.get());   // 此時p1和p2相互獨立且引用計數都爲1

}   // 釋放同一內存兩次

若是你使用get()返回的指針,記住當最後一個對於的智能指針銷燬後,你的指針就無效了——由於此時該指針爲懸空指針


int *p = nullptr;

{

    shared_ptr<int> p1 = make_shared<int>(1);

    p = p1.get();

}   // 此時釋放p1所指向的內存,且p爲懸空指針

若是你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器(用於代替默認的delete)——由於智能指針的自動釋放默認使用delete,而delete只能用於動態內存或nullptr

智能指針

智能指針類所支持的操做

shared_ptr和unique_ptr都支持的操做


操做功能

shared_ptr<T> sp、unique_ptr<T> up空智能指針,能夠指向類型爲T的對象

*p解引用p所指的對象

p->data訪問對象*p中的data元素

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 = q、shared_ptr<T> p(q, d)拷貝初始化,p和q指向同一對象,其中d是一個刪除器(用來代替默認的delete)

p.unique()若p.use_count() == 1,則返回true,不然返回false

p.use_count()返回與p共享對象的智能指針數量(引用計數)

p.reset()、p.reset(q)、p.reset(q, d)若p是惟一指向其對象的shared_ptr,則reset會釋放此對象。若傳遞了可選的參數(內置指針)q,則會令p指向q,不然會將p置爲空。若還傳遞了參數(可調用對象)d,則會調用d而不是delete來釋放p

unique_ptr獨有的操做


操做功能

unique_ptr<T> u指向類型T的對象的空unique_ptr,且默認使用delete釋放指針

unique_ptr<T, D> u指向類型T的對象的空unique_ptr,且使用類型爲D的可調用對象來釋放指針

unique_ptr<T, D> u(d)指向類型T的對象的空unique_ptr,且使用類型爲D的可調用對象d代替delete來釋放指針

u = nullptr釋放u指向的內存,並將u置爲空

u.release()先使指針對象u放棄對指針(值)的控制權,再返回指針(值),最後將u置爲空——即斷開指針對象和所指對象之間的聯繫(只斷開聯繫,不釋放內存)

u1.reset()、u2.reset(q)、u3.reset(nullptr)重置指針(值),使其指向新對象或爲空

shared_ptr類

關鍵概念


初始化 

用make_shared初始化(最安全)

拷貝初始化(拷貝另外一個智能指針)或直接初始化(可由任意類型指針爲參數)

引用計數 

引用計數決定什麼時候釋放內存

智能指針獨有,內置指針沒有

相互關聯的智能指針的引用計數相同

內存釋放 

析構函數

默認delete

自定義刪除器

初始化


使用make_shared標準庫函數初始化(最安全,且會爲對象分配內存):


shared_ptr<string> p1 = make_shared<string>("chensu");

shared_ptr<int> p2 = make_shared<int>(24);

shared_ptr<vector<string>> p3 = make_shared<vector<string>>();  // 當參數爲空時,默認進行值初始化,此時p3所指對象爲空vector<string>

class Person {

private:

    string name;

    int age;

public:

    Person(const string &_name, int _age) : name(_name), age(_age) {}

    void print() const { cout << name << ", " << age << endl; }

};

shared_ptr<Person> p4 = make_shared<Person>(Person("chensu", 24));   // 參數必須與Person類的某個構造函數相匹配

使用make_shared爲智能指針分配動態內存比用new直接初始化更安全的緣由:使得在分配對象的同時就將shared_ptr與之綁定,避免將同一塊內存綁定到多個獨立建立的shared_ptr上:


Person *p = new Person("chensu", 24);

shared_ptr<Person> p1(p);

shared_ptr<Person> p2(p);   // 此時p1和p2相互獨立且引用計數都爲1,程序結束時會釋放統一內存兩次

拷貝初始化(只拷貝指針,無需從新爲對象分配內存)


拷貝智能指針


auto p = make_shared<int>(42);  // 爲p指向的對象分配內存,且此時該對象只有p一個引用者

auto q(p);  // p和q指向同一個對象,此時該對象有兩個引用者

拷貝普通指針(和new結合):因爲接受指針參數的智能指針構造函數是explicit的,所以咱們不能將一個普通指針隱式轉換爲智能指針,而必須使用直接初始化形式或將普通指針強制類型轉換爲智能指針。


shared_ptr<int> p = new int(42);    // 錯誤,不支持隱式轉換,可使用直接初始化或強制類型轉換

shared_ptr<int> p(new int(42));     // 正確,支持直接初始化形式

shared_ptr<int> p = shared_ptr<int>(new int(42));   // 正確,可以拷貝強制轉換後的智能指針

初始化並自定義釋放操做:默認狀況下,系統經過delete運算符來自動釋放智能指針,所以一個用來初始化智能指針的普通指針必須指向動態內存,不然結果未定義。但咱們能夠自定義釋放操做來代替默認的delete,這樣就能夠用指向非動態內存的普通指針來初始化智能指針


shared_ptr<int> p(q, d);  // p將使用可調用對象d來代替delete

引用計數


每一個shared_ptr都各自關聯一個計數器,以避免發生引用非法內存(即在尚有指針引用內存的狀況下就釋放該內存),引用計數的初始值爲1


智能指針的引用計數=所指對象綁定的互相關聯的智能指針的數量 

智能指針必須互相關聯的緣由:拷貝智能指針有兩個階段,且這兩個階段只有互相關聯的智能指針才能完成,互相獨立的智能指針沒有這兩個階段


拷貝引用計數(將源智能指針的引用計數拷貝給目的智能指針)

共同遞增引用計數(分別將源智能指針和目的智能指針的引用計數加1)

例子:


shared_ptr<int> p(new int(42)); // p的引用計數爲1

shared_ptr<int> q(p);   // 由於p和q相互關聯,因此它們的引用計數都遞增爲2

{

    shared_ptr<int> r(p.get());   // 由於p和r相互獨立(經過get初始化的指針互相獨立),因此它們的引用計數都不變(既不拷貝,也不共同遞增),其中p爲2,r爲1

}   // 程序塊結束,r遞減爲0,並釋放r所指向的內存,此時p和q都爲懸空指針

int a = *p; // 未定義,由於此時p爲懸空指針

永遠不要用get得到的指針值初始化一個智能指針,由於這樣作的話,源指針和目的智能指針是相互獨立的


指向同一對象的互相關聯的智能指針具備相同的引用計數

智能指針p所指對象的引用計數遞增的狀況——即p所指的對象與和p互相關聯的其它智能指針發生新的綁定關係:


用p初始化另外一個智能指針q(對象綁定另外一個智能指針):


auto p = make_shared<int>(42);  // 此時p的引用計數爲1

auto q = p;     // 此時p和q的引用計數爲2

將p做爲參數傳遞給一個函數(對象綁定形參):


auto p = make_shared<int>(42);  // 此時p的引用計數爲1

function(p);    // 此時p的引用計數爲2

將p做爲函數的返回值(對象綁定一個右值指針或左值指針):


shared_ptr<int> function() {

    auto p = make_shared<int>(42);  // 此時p的引用計數爲1

    return p;   // 此時p的引用計數爲2 

}

...     // 即便離開了function函數,p所指的對象的引用計數依舊爲1

智能指針p所指對象的引用計數遞減的狀況——即p所指的對象與和p互相關聯的其它智能指針解除舊的綁定關係:


爲p賦予一個和原來不一樣的新值:


auto p = make_shared<int>(42);

auto p = nullptr;     // 此時p的引用計數爲0

p被銷燬(例如程序離開p的做用域):


void function() {

    auto p = make_shared<int>(42);  // 此時`p`的引用計數爲1

}

...     // 此時`p`的引用計數爲0

一旦一個shared_ptr的引用計數變爲0,它就會使用析構函數自動釋放本身所管理的對象(相似於java中的垃圾回收機制)


內存釋放


析構函數: 智能指針經過本身的析構函數完成銷燬操做,析構函數會遞減它所指向對象的引用計數,若引用計數變爲0(即該對象此時沒有綁定任何指針),則析構函數會銷燬對象,並釋放對象所佔用的內存

注意事項 

因爲在最後一個shared_ptr銷燬以前內存都不會釋放,所以保證shared_ptr在無用以後再也不保留就很是重要了,若你忘記銷燬程序中再也不須要的shared_ptr,雖不會操做內存泄露,但會浪費內存,形成程序運行較卡

銷燬再也不須要的shared_ptr指針的方法是令其爲nullptr

默認狀況下,系統經過delete運算符來自動釋放智能指針,所以一個用來初始化智能指針的普通指針必須指向動態內存,不然結果未定義。但咱們能夠自定義釋放操做來代替默認的delete,這樣就能夠用指向非動態內存的普通指針來初始化智能指針

警告


不要混合使用普通指針和智能指針——由於混合使用會干擾內存的正常釋放


void process(shared_ptr<int> ptr) { ... }   // 指針進入函數時引用計數加1,離開時引用計數減1

/* 狀況1:只使用智能指針 */        

shared_ptr<int> p(new int(42));     // p所指內存的引用計數爲1

process(p);     // p離開後引用計數不變,仍爲1

int i = *p;     // 正確,由於內存未被釋放

/* 狀況2:混合使用智能指針和普通指針 */

int *q(new int(42));    // q所指內存的引用計數爲0

process(shared_ptr<int>(q));    // q離開後引用計數不變,仍未0,但在離開時因爲形參的類型爲shared_ptr,所以系統會自動釋放q所指向的內存,此時q爲懸空指針

int j = *q;     // 未定義,此時q爲懸空指針

unique_ptr類

關鍵概念


初始化 

拷貝初始化(拷貝另外一個智能指針)或直接初始化(可由任意類型指針爲參數)

對指針(值)的控制權


全部操做都必須保持指針值的惟一性

某一時刻只能有一個unique_ptr指向一個給定的對象——保持惟一性

不能拷貝或賦值unique_ptr(例外:能夠拷貝或賦值一個將要被銷燬的unique_ptr)——保持惟一性:


unique_ptr<int> clone(int p) {

    return unique_ptr<int>(new int(p));

}

unique_ptr<int> q = clone(1);   // 正確,該函數的返回值是一個右值(臨時變量)

容許轉移對指針(值)的控制權,但源指針值必須改變——保持惟一性

內存釋放 

當unique_ptr爲nullptr時,其原來所指向的內存會被釋放

對象與unique_ptr指針之間的聯繫——若聯繫斷開,則即便unique_ptr指針被銷燬,對象也不會被釋放

只有與對象相關聯的unique_ptr被銷燬時,對象纔會被自動釋放

對unique_ptr指針(值)的控制權


使用release()釋放對指針值或對象內存的控制權(但不會釋放所指對象的內存)


{

    unique_ptr<int> u(new int(1));

    u.release();    // 釋放控制權(斷開u和對象之間的聯繫)

}   // 離開做用域,銷燬u,但因爲此時u和對象之間無關聯,所以沒法釋放對象內存,故形成內存泄漏

使用release()和reset()轉移控制權(目的unique_ptr原來所指向的對象會被自動釋放)


{

    unique_ptr<int> u1(new int(1));

    unique_ptr<int> u2(new int(2));

    u2.reset(u1.release()); // u1先釋放對指針值的控制權(斷開與對象的聯繫),再返回指針值給u2,接着將u1置爲空,此時u2掌握該指針值(或對象內存)的控制權,最後自動釋放以前u2控制的內存

}   // 離開做用域,釋放此時u2控制的內存

向unique_ptr傳遞刪除器


默認狀況下,使用delete釋放它指向的對象

因爲咱們必須在尖括號中unique_ptr指向的對象類型以後提供刪除器類型,所以重載一個刪除器會影響到unique_ptr的類型以及如何構造(或reset)該類型的對象:


void my_deleter(int *ptr) { delete ptr; }

int *q = new int(1);

unique_ptr<int, decltype(my_deleter) *)> p(q, my_deleter)

weak_ptr類

將一個weak_ptr綁定到shared_ptr上不會改變shared_ptr的引用計數

一旦weak_ptr綁定的最後一個shared_ptr被釋放,該weak_ptr就成爲懸空指針

因爲對象可能已被釋放(weak_ptr成爲了懸空指針),所以咱們不能使用weak_ptr直接訪問對象,而必須調用lock檢查weak_ptr所指的對象是否存在,若存在則返回一個shared_ptr:


shared_ptr<int> p = make_shared<int>(42);

weak_ptr<int> wp(p);

shared_ptr<int> q = wp.lock();

總結

智能指針要特別注意同一內存屢次釋放和內存爲釋放問題

shared_ptr的引用計數決定是否釋放其所指向的內存

始終保證至少有一個unique_ptr和對象之間的聯繫,不然容易形成內存泄漏

相關文章
相關標籤/搜索