智慧指標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;
}
編譯器知道要返回的物件將要被銷燬,執行了一種特殊而“拷貝”(移動操作)