1. 程式人生 > >智慧指標shared_ptr與unique_ptr詳解

智慧指標shared_ptr與unique_ptr詳解

為什麼使用動態記憶體

程式不知道自己需要多少物件;
程式不知道物件的準確型別;
程式需要在多個物件之間共享資料;

動態記憶體在哪裡

程式有靜態記憶體、棧記憶體。靜態記憶體用來儲存區域性static物件、類static資料成員以及定義在任何函式之外的變數。棧記憶體用來儲存定義在函式內的非static物件。分配在靜態或棧記憶體中的物件由編譯器自動建立或銷燬。對於棧物件,僅在其定義的程式塊執行時才存在;static物件在使用之前分配,在程式結束時銷燬。
除了靜態記憶體和棧記憶體,每個程式還擁有一個記憶體池。這部分記憶體被稱作自由空間或堆。程式用堆來儲存動態分配的物件——即,那些在程式執行時分配的物件。動態物件的生存期由程式來控制,也就是說,當動態物件不再使用時,我們的程式碼必須顯式的銷燬它們。(c++ primer P400)

自由儲存區和堆

自由儲存是c++中通過new和delete動態分配和釋放物件的抽象概念,通過new來申請的記憶體區域可稱為自由儲存區
堆是作業系統維護的一塊記憶體
雖然c++編譯器預設使用堆來實現自由儲存。但兩者不能等價

動態記憶體與智慧指標

我們知道c++需要注意的地方之一就是對記憶體的管理,動態記憶體的使用經常會出現記憶體洩漏,或者產生引用非法記憶體的指標
新的標準庫提供了兩種智慧指標型別來管理動態物件:
(1)shared_ptr 允許多個指標指向同一個物件
(2)unique_ptr 獨佔所指向的物件
定義在memory標頭檔案中,他們的作用在於會自動釋放所指向的物件

智慧指標的本質

智慧指標的實質是一個物件,行為卻表現的像一個指標

unique_ptr的“獨佔”?

先說說為什麼shared_ptr允許多個指標指向同一物件吧
因為 動態物件的所有權不確定。物件可以在多個作用域中共享,又不能像棧物件一樣自由地值拷貝。只要有一個物件\作用域還持有這個動態物件,他就不能銷燬,當他沒有用時,自動銷燬,這個機制後面再講。
unique_ptr的“獨佔”是指:不允許其他的智慧指標共享其內部的指標,不允許通過賦值將一個unique_ptr賦值給另一個unique_ptr。例如:

std::unique_ptr<int> p (new int);
std:
:unique_ptr<int> q = p; //error

但是unique_ptr允許通過函式返回給其他的unique_ptr,還可以通過std::move來轉移到其他的unique_ptr,注意,這時它本身就不再擁有原來指標的所有權了。

std::unique_ptr<int> p (new int);
std::unique_ptr<int> q = std::move(p); //ok

shared_ptr基操

shared_ptr也是一個模板,所以我們在建立的時候,需要提供指標指向的型別。

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

預設初始化的智慧指標為一個空指標,智慧指標的使用類似於普通指標:

if (p1 &&p1->empty()) //如果p1不為空且p1指向一個空string 
{
    *p1 = "lvbai";  //賦值
}
  • shared_ptr和unique_ptr都支援的一些操作
    shared_ptr<T> p1; //空智慧指標,可以指向型別為T的物件
    unique_ptr<T> p2;

    if (p1) //將p1用作一個條件判斷,若p指向一個物件,為true 
    {...}

    *p1;    //解引用p1,獲得他指向的物件

    p->member;  //等價於*(p1).member

    p1.get();   //返回p1中儲存的指標。若智慧指標釋放了物件,則這個指標指向的物件也消失了 

    swap(p1, q);     //交換p和q中的指標即p1.swap(q);
  • 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

    p.use_count()   //返回與p共享物件的智慧指標的數量,可能很慢,主要用於除錯

make_shared函式

什麼是make_shared函式,他的作用是什麼呢?
make_shared函式是一個安全的分配和使用動態記憶體的方法。他會在動態記憶體中分配一個物件並初始化它,返回值是一個指定型別的shared_ptr,同樣也定義在memory的頭中

    //指向一個值為42的int的shared_ptr
    shared_ptr<int> p = make_shared<int> (42);
    //指向一個值為"999"的string
    shared_ptr<string> q = make_shared<string> (3, '9');
    //指向一個值初始化的int,即值為0
    shared_ptr<int> w = make_shared<int> ();
  • make_shared怎麼初始化
    make_shared用其引數來構造給定型別的物件,也就是說,我們的引數,必須符合給定型別的某一建構函式,不傳參就預設初始化

  • 與shared_ptr的區別,為什麼使用make_shared更好
    他相比shared_ptr減少了記憶體分配的次數,而記憶體分配的代價較高;他會立即獲得申請的裸指標,不會造成記憶體洩漏

  • make_shared的缺點
    當建構函式是保護或私有的時,無法使用make_shared;物件調額記憶體可能無法及時回收

shared_pt的強引用、弱引用

關於智慧指標怎麼實現自動釋放物件,簡單來說,智慧指標的內部有一個引用計數,當有指標指向其物件,引用計數就會++,相反–,當減為0時,自動釋放該物件

到底是用一個計數器還是其他資料結構來記錄有多少指標共享物件,完全由標準庫的具體實現來決定。關鍵是智慧指標類能記錄有多少個shared_ptr指向相同的物件,並能在恰當的時候自動釋放

強引用和弱引用就是shared_ptr用來維護引用計數的資訊

  • 強引用
    用來記錄當前有多少個存活的 shared_ptrs 正持有該物件. 共享的物件會在最後一個強引用離開的時候銷燬( 也可能釋放).
  • 弱引用
    用來記錄當前有多少個正在觀察該物件的 weak_ptrs. 當最後一個弱引用離開的時候, 共享的內部資訊控制塊會被銷燬和釋放 (共享的物件也會被釋放, 如果還沒有釋放的話).

  • 當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他的shared_ptr指向相同的物件

  • 當指向一個物件的最後一個shared_ptr被銷燬,shared_ptr類會自通過呼叫對應的解構函式銷燬此物件  

  • shared_ptr會自動釋放相關聯的記憶體

//該函式返回一個T型別的動態分配的物件,物件是通過一個型別為Q的引數來進行初始化的
shared_ptr<T> Fun(Q arg) 
{
    //對arg進行處理
    //shared_ptr負責釋放記憶體
    return make_shared<T>(arg);
    //由於返回的是shared_ptr,我們可以保證他分配的物件會在恰當的時候釋放
}
void use_Fun(Q arg) 
{
    shared_ptr<T> p = Fun(arg);
    //使用p
}
//函式結束,p離開了作用域,他指向的記憶體被自動釋放
shared_ptr<T> use_Fun(Q arg) 
{
    shared_ptr<T> p = Fun(arg);
    //使用p
    return p; //返回p時,引用計數遞增
}
//p離開了作用域,但他不會釋放指向的記憶體

shared_ptr和new結合使用

    shared_ptr<int> p1(new int(1));//直接初始化
    shared_ptr<int> p2 = new int(1);//錯誤,int是內建型別

接受指標引數的智慧指標建構函式是explicit的,必須使用直接初始化,不能做隱式型別轉換

shared_ptr<int> FUn(int p) 
{
    return new int(p); //error
    return shared_ptr<int>(new int(p)); //right
}

定義自己的釋放操作

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的拷貝,呼叫d來代替delete

//刪除器必須接受所指定型別的引數,如,上述中就要接受一個型別為T*的引數

p.reset() 
p.reset(q)
p.reset(q, d)
//若p是唯一指向其物件的shared_ptr,reset會釋放此物件。若傳遞了可選的引數內建指標q,會令p指向q,否則會將p置空。若還傳遞了引數d,則會呼叫d而不是delete來釋放q

//例如:
void del(int* p)
{...}
shared_ptr<int> p(new int(1), del);
shared_ptr<int> p(new int, del);

智慧指標的陷阱和缺陷

  • 不要混合使用智慧指標和普通指標 
void process(shared_ptr<int> ptr) 
{
    ...
}
//ptr離開作用域,被銷燬
int* x(new int(1024));
process(x); //error
process(shared_ptr<int>(x)); //合法,但記憶體會被釋放
int j = *x; //未定義,x是一個空懸指標

//使用一個內建指標來訪問一個智慧指標所負責的物件是很危險的,因為我們無法知道物件何時被銷燬
  • 永遠不要使用get初始化另一個智慧指標或為智慧指標賦值
    智慧指標定義的get函式返回值是一個內建指標,指向智慧指標管理的物件,所以結合我們上文所述,這樣有可能導致產生空懸指標,從而發生未定義的行為。

此函式是為了:需要向不能使用智慧指標的程式碼傳遞一個內建指標。

void f(int* q) 
{
    shared_ptr<int> tmp(q);
}//釋放了q指向的內容
int main()
{
    shared_ptr<int> p(new int(1));
    int* q = p.get();
    f(q);
    int qq = *q; //未定義的行為
}
//對上述程式碼編譯器並不會給出錯誤資訊
get用來將指標的訪問許可權傳遞給程式碼,只有在確定程式碼不會delete指標的情況下,才能使用get

unique_ptr

unique_ptr的直觀認知應該就是“獨佔”、“擁有”了吧。它的意思就是某個時刻只能有一個unique_ptr指向一個給定的物件,即不能拷貝和賦值

    int* p (new int(3));
    shared_ptr<int> p1(p);
    auto p2 = p1; //ok
    unique_ptr<int> p3(p);
    unique_ptr<int> p4(p); //ok,智慧指標畢竟只是一個幫助你管理的一個工具,它並不能知道有多少初始化的動作
    unique_ptr<int> p5 = p3; //error
  • 初始化 
    unique_ptr沒有類似make_shared的操作,只能直接初始化
    unique_ptr<int> p(new int(1024)); //ok
    unique_ptr<int> p1 = new int; //error
    unique_ptr<int> p2(p); //error
  • unique_ptr基操

    unique_pre<T> p1; //空unique_ptr,可以指向型別為T的物件,p1會使用delete來釋放它的指標,p2會使用一個型別為D的可呼叫物件來釋放它的指標  

    unique_ptr<T, D> p2; 
    unique_ptr<T, D> p(d); //空unique_ptr,指向型別為T的物件,用型別為D的物件d來代替delete

    p = nullptr; //釋放p指向的物件,將p置為空
    p.release(); //p放棄對指標的控制權,返回指標,並將p置空
    p.reset(); //釋放p所指向的物件
    p.reset(q); //如果提供了內建指標q,令p指向這個物件;
    p.reset(nullptr); 
  • 雖然我們不能拷貝和賦值,但我們可以呼叫上述所說的reset和release將指標的所有權從一個(非const)unqiue_ptr轉移給另一個unique_ptr
//將所有權從p1轉移給p2
    (1)unique_ptr<string> p2(p1.release());
    (2)p2.reset(p1.release());
  • 單純呼叫release是錯誤的
p.release(); //錯誤,release放棄了控制權不會釋放記憶體,丟失了指標
auto q = p.release(); //正確,記得delete掉q
  • 特殊的版本
    unique_ptr針對new出來的陣列特殊化,是一個特殊化的版本
unique_ptr<int []> q(new int[10]);
q.release();
//自動用delete[]銷燬其指標釋放記憶體
  • unique_ptr作為引數傳遞和返回值
    unique_ptr的不能拷貝有一個例外:
unique_ptr<int> Fun(int p) 
{
    return unique_ptr<int>(new int(p));
}
//返回一個區域性物件的拷貝
unique_ptr<int> Fun(int p) 
{
    unique_ptr<int> ret(new int(p));
    //...
    return ret;
}

編譯器知道要返回的物件將要被銷燬,執行了一種特殊而“拷貝”(移動操作)