c++ 之四大智慧指標 std::auto_ptr std::shared_ptr std::unuque std::weak_ptr 比較總結
1. 動態記憶體必要性
程式不知道自己需要多少物件;
程式不知道物件的準確型別;
程式需要在多個物件之間共享資料;
2. 動態記憶體在哪裡
程式有靜態記憶體、棧記憶體。靜態記憶體用來儲存區域性static物件、類static資料成員以及定義在任何函式之外的變數。棧記憶體用來儲存定義在函式內的非static物件。分配在靜態或棧記憶體中的物件由編譯器自動建立或銷燬。對於棧物件,僅在其定義的程式塊執行時才存在;static物件在使用之前分配,在程式結束時銷燬。
除了靜態記憶體和棧記憶體,每個程式還擁有一個記憶體池。這部分記憶體被稱作自由空間或堆。程式用堆來儲存動態分配的物件——即,那些在程式執行時分配的物件。動態物件的生存期由程式來控制,也就是說,當動態物件不再使用時,我們的程式碼必須顯式的銷燬它們。(c++ primer P400)
3. 自由儲存區和堆
自由儲存是c++中通過new和delete動態分配和釋放物件的抽象概念,通過new來申請的記憶體區域可稱為自由儲存區
堆是作業系統維護的一塊記憶體
雖然c++編譯器預設使用堆來實現自由儲存。但兩者不能等價
4. 動態記憶體與智慧指標
我們知道c++需要注意的地方之一就是對記憶體的管理,動態記憶體的使用經常會出現記憶體洩漏,或者產生引用非法記憶體的指標
c++11標準庫提供了三種智慧指標型別來管理動態物件:
智慧指標:自動負責釋放所指向的物件,實際上它利用了棧的機制,每一個智慧指標都是一個模板類,呼叫智慧指標實際上是建立了一個智慧指標的物件,物件生命週期到達盡頭的時候,會自動呼叫智慧指標的解構函式,在解構函式裡,釋放掉它管理的記憶體,從而避免手動delete。
- shared_ptr 允許多個指標指向同一個物件
- unique_ptr 獨佔所指向的物件
- weak_ptr shared_ptr的弱引用
- auto_ptr c++98提出 c++11 摒棄 對於特定物件,和unique_ptr一樣只能被一個智慧指標所擁有,這樣,只有擁有該物件的智慧指標的解構函式才會刪除該物件
定義在memory標頭檔案中,他們的作用在於會自動釋放所指向的物件
5. 智慧指標的詳細描述
5.1 auto_ptr描述
auto_ptr主要是用來解決資源自動釋放的問題,比如如下程式碼:
void Function() { Obj*p = new Obj(20); ... if (error occor) throw ... 或者 retrun; delete p; }
在函式遇到錯誤之後,一般會拋異常,或者返回,但是這時很可能遺漏之前申請的資源,及時是很有經驗的程式設計師也有可能出現這種錯誤.
而使用auto_ptr會在自己的析夠函式中進行資源釋放。也就是所說的RAII
使用auto_ptr程式碼如下
void Function()
{
auto_ptr<Obj> ptr( new Obj(20) );
...
if (error occur)
throw exception...
}
這樣無論函式是否發生異常,在何處返回,資源都會自動釋放。
需要提一下的是這是一個被c++11標準廢棄的一個智慧指標,為什麼會被廢棄,先看一下下面的程式碼:
auto_ptr<Obj> ptr1( new Obj() );
ptr1->FuncA();
auto_ptr<Obj> ptr2 = ptr1;
ptr2->FuncA();
ptr1->FuncA(); // 這句話會異常
為什麼在把ptr1複製給ptr2之後ptr1再使用就異常了呢?
這也正是他被拋棄的主要原因。
因為auto_ptr複製建構函式中把真是引用的記憶體指標進行的轉移,也就是從ptr1轉移給了ptr2,此時,ptr2引用了Obj記憶體地址,而ptr1引用的記憶體地址為空,此時再使用ptr1就異常了。
5.2 shared_ptr描述(in memory):
shared_ptr是一個標準的共享所有權的智慧指標,就是允許多個指標指向同一物件,shared_ptr物件中不僅有一個指標指向某某(比如 int型,以下也拿int型別舉例)物件,還擁有一個引用計數器,代表一共有多少指標指向了那個物件。
為什麼shared_ptr允許多個指標指向同一物件?
因為 動態物件的所有權不確定。物件可以在多個作用域中共享,又不能像棧物件一樣自由地值拷貝。只要有一個物件\作用域還持有這個動態物件,他就不能銷燬,當他沒有用時,自動銷燬。
shared_ptr自動銷燬所管理的物件
每當建立一個shared_ptr的物件指向int型資料,則引用計數器值+1,每當銷燬一個shared_ptr物件,則-1.當引用計數器資料為0時,shared_ptr的解構函式會銷燬int型物件,並釋放它佔用的記憶體。
shared_ptr和new的配合使用
接受指標作為引數的智慧指標的建構函式是explicit型別,意味著必須使用直接初始化,不能做隱式型別轉換
shared_ptr<int> p1;
//被初始化成為一個空指標
shared_ptr<int> p2 (new int(4));
//指向一個值是4的int型別資料
shared_ptr<int> p3 = new int(4);
//錯誤,必須直接初始化
shared_ptr<int> Fun(int p)
{
return new int(p); //error
return shared_ptr<int>(new int(p)); //right
}
不能混合使用普通指標和智慧指標,因為智慧指標不是單純的赤裸裸的指標
void process(shared_ptr<int> ptr){
//受到引數值傳遞的影響,ptr被構造並且誕生,執行完函式塊後被釋放
}
int *x(new int (43));
//x是一個普通的指標
process(x);
//錯誤,int * 不能轉換成shared_ptr<int>型別
process(shared_ptr<int> x);
//臨時創造了x,引用數+1,執行完process之後,引用數-1
int j = *x;
//x是一個空懸指標,是未定義的
不能使用get()函式對智慧指標賦值或初始化
原因:get()函式得到的是共享物件的地址,是內建指標,指向智慧指標管理的物件,而智慧指標不僅僅包含地址,兩個東西不是一個型別的,也不能彼此包含,因此不能這樣做。
同樣,把get()返回值 繫結到智慧指標上也是錯誤的
如下:
shared_ptr<int> p (new int(22));
int *q = p.get();
//語義沒問題
{
shared_ptr<int> (q);
//意味著q被繫結,!!!!引用計數器還是1!!!!
//如果這個被執行,程式塊結束以後q和q所指的內容被銷燬,則代表著以後執行(*p)的解引用操作,就成了未定義的了。
}
int r = *p;
//已經不對了,因為p指向的記憶體已經在剛才那個程式碼塊裡被q釋放了
shared_ptr的一些操作
shared_ptr<T> p;
//空智慧指標,可指向型別是T的物件
if(p)
//如果p指向一個物件,則是true
(*p)
//解引用獲取指標所指向的物件
p -> number == (*p).number;
p.get();
//返回p中儲存的指標
swap(p,q);
//交換p q指標
p.swap(q);
//交換p,q指標
make_shared<T>(args)
//返回一個shared_ptr的物件,指向一個動態型別分配為T的物件,用args初始化這個T物件
shared_ptr<T> p(q)
//p 是q的拷貝,q的計數器++,這個的使用前提是q的型別能夠轉化成是T*
shared_pts<T> p(q,d)
//p是q的拷貝,p將用可呼叫物件d代替delete
//上面這個我其實沒懂,也沒有查出來這個的意思
p =q;
//p的引用計數-1,q的+1,p為零釋放所管理的記憶體
p.unique();
//判斷引用計數是否是1,是,返回true
p.use_count();
//返回和p共享物件的智慧指標數量
p.reset();
p.reset(q);
p.reset(q,d);
//reset()沒懂,這個以後再來補充吧
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 測試例子
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void mytest()
{
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::cout << "cout: " << sp2.use_count() << std::endl; // 列印引用計數
std::cout << *sp1 << std::endl;
std::cout << *sp2 << std::endl;
sp1.reset(); // 顯示讓引用計數減一
std::cout << "count: " << sp2.use_count() << std::endl; // 列印引用計數
std::cout << *sp2 << std::endl; // 22
return;
}
int main()
{
mytest();
system("pause");
return 0;
}
5.3 unique_ptr描述(in memory):
unique_ptr的直觀認知應該就是“獨佔”、“擁有”,與shared_ptr不同,某一時刻,只能有一個unique_ptr指向一個給定的物件即不能拷貝和賦值。因此,當unique_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,注意,這時它本身就不再擁有原來指標的所有權了。後面都會提到
unique_ptr的初始化必須採用直接初始化
unique_ptr<string> p(new string("China"));
//沒問題
unique_ptr<string> p (q);
//錯誤,不支援拷貝
unique_ptr<string> q;
q = p;
//錯誤,不支援賦值
unique_ptr的一些操作:
unique_ptr<T> p;
//空智慧指標,可指向型別是T的物件
if(p)
//如果p指向一個物件,則是true
(*p)
//解引用獲取指標所指向的物件
p -> number == (*p).number;
p.get();
//返回p中儲存的指標
swap(p,q);
//交換p q指標
p.swap(q);
//交換p,q指標
unique_ptr<T,D>p;
//p使用D型別的可呼叫物件釋放它的指標
p = nullptr;
//釋放物件,將p置空
p.release();
//p放棄對指標的控制,返回指標,p置數空
p.reset();
//釋放p指向的物件
p.reset(q);
//讓u指向內建指標q
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;
}
編譯器知道要返回的物件將要被銷燬,執行了一種特殊而“拷貝”(移動操作)
如何安全的重用unique_ptr指標
要安全的重用unique_ptr指標,可給它賦新值。C++為其提供了std::move()方法。
unique_ptr<string> pu1(new string("nihao"));
unique_ptr<string> pu2;
pu2 = std::move(pu1);//move
cout<<*pu1<<endl;//賦新值
比較而言auto_ptr由於策略沒有unique_ptr嚴格,無需使用move方法
auto_ptr<string> pu1, pu2;
pu1 = demo2("Uniquely special");
pu2 = pu1;
pu1 = demo2(" and more");
cout<<*pu2<<*pu1<<endl;
由於unique_ptr使用了C++11新增的移動建構函式和右值引用,所以可以區分安全和不安全的用法。
unique_ptr相較auto_ptr提供了可用於陣列的變體
auto_ptr和shared_ptr可以和new一起使用,但不可以和new[]一起使用,但是unique_ptr可以和new[]一起使用
unique_ptr<double[]> pda(new double(5));
pda.release();
//自動用delete[]銷燬其指標釋放記憶體
unique_ptr 測試例子
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void mytest()
{
std::unique_ptr<int> up1(new int(11)); // 無法複製的unique_ptr
//unique_ptr<int> up2 = up1; // err, 不能通過編譯
std::cout << *up1 << std::endl; // 11
std::unique_ptr<int> up3 = std::move(up1); // 現在p3是資料的唯一的unique_ptr
std::cout << *up3 << std::endl; // 11
//std::cout << *up1 << std::endl; // err, 執行時錯誤
up3.reset(); // 顯式釋放記憶體
up1.reset(); // 不會導致執行時錯誤
//std::cout << *up3 << std::endl; // err, 執行時錯誤
std::unique_ptr<int> up4(new int(22)); // 無法複製的unique_ptr
up4.reset(new int(44)); //"繫結"動態物件
std::cout << *up4 << std::endl; // 44
up4 = nullptr;//顯式銷燬所指物件,同時智慧指標變為空指標。與up4.reset()等價
std::unique_ptr<int> up5(new int(55));
int *p = up5.release(); //只是釋放控制權,不會釋放記憶體
std::cout << *p << std::endl;
//cout << *up5 << endl; // err, 執行時錯誤
delete p; //釋放堆區資源
return;
}
int main()
{
mytest();
system("pause");
return 0;
}
5.4 weak_ptr描述(in memory)
weak_ptr是一種不控制所指向物件生存期的智慧指標,指向shared_ptr管理的物件,但是不影響shared_ptr的引用計數。它像shared_ptr的助手,一旦最後一個shared_ptr被銷燬,物件就被釋放,weak_ptr不影響這個過程。
- weak_ptr是為配合shared_ptr而引入的一種智慧指標來協助shared_ptr工作,它可以從一個shared_ptr或另一個weak_ptr物件構造,它的構造和析構不會引起引用計數的增加或減少。沒有過載 * 和 -> 但我們可以通過lock來獲得一個shared_ptr物件來對資源進行使用,如果引用的資源已經釋放,lock()函式將返回一個儲存空指標的shared_ptr。 expired函式用來判斷資源是否失效。
- weak_ptr的使用更為複雜一點,它可以指向shared_ptr指標指向的物件記憶體,卻並不擁有該記憶體,而使用weak_ptr成員lock,則可返回其指向記憶體的一個share_ptr物件,且在所指物件記憶體已經無效時,返回指標空值nullptr。
注意:weak_ptr並不擁有資源的所有權,所以不能直接使用資源。
可以從一個weak_ptr構造一個shared_ptr以取得共享資源的所有權。
weak_ptr的一些操作:
weak_ptr<T> w(sp);
//定義一個和shared_ptr sp指向相同物件的weak_ptr w,T必須能轉化成sp指向的型別
w = p;
//p是shared_ptr或者weak_ptr,w和p共享物件
w.reset();
//w置為空
w.use_count();
//計算與w共享物件的shared_ptr個數
w.expired();
//w.use_count()為0,返回true
w.lock();
//w.expired()為true,返回空shared_ptr,否則返回w指向物件的shared_ptr
weak_ptr 測試例子
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>
void check(std::weak_ptr<int> &wp)
{
std::shared_ptr<int> sp = wp.lock(); // 轉換為shared_ptr<int>
if (sp != nullptr)
{
std::cout << "still: " << *sp << std::endl;
}
else
{
std::cout << "still: " << "pointer is invalid" << std::endl;
}
}
void mytest()
{
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指物件
std::cout << "count: " << wp.use_count() << std::endl; // count: 2
std::cout << *sp1 << std::endl; // 22
std::cout << *sp2 << std::endl; // 22
check(wp); // still: 22
sp1.reset();
std::cout << "count: " << wp.use_count() << std::endl; // count: 1
std::cout << *sp2 << std::endl; // 22
check(wp); // still: 22
sp2.reset();
std::cout << "count: " << wp.use_count() << std::endl; // count: 0
check(wp); // still: pointer is invalid
return;
}
int main()
{
mytest();
system("pause");
return 0;
}
6. 如何選擇智慧指標:
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
#include <stdlib.h>
using namespace std;
unique_ptr<int> make_int(int n)
{
return unique_ptr<int> (new int(n));
}
void show(unique_ptr<int> & pi) //pass by reference
{
cout<< *pi <<' ';
}
int main()
{
vector<unique_ptr<int> > vp(5);
for(int i = 0; i < vp.size(); ++i)
{
vp[i] = make_int(rand() % 1000);//copy temporary unique_ptr
}
vp.push_back(make_int(rand() % 1000));//ok because arg is temporary
for_each(vp.begin(), vp.end(), show);
unique_ptr<int> pup(make_int(rand() % 1000));
// shared_ptr<int> spp(pup);//not allowed. pup is lvalue
shared_ptr<int> spr(make_int(rand() % 1000));
return 0;
}
總結:
- 當多個物件指向同一個物件的指標時,應選擇shared_ptr
- 用new申請的記憶體,返回指向這塊記憶體的指標時,選擇unique_ptr就不錯
- 在滿足unique_ptr要求的條件時,前提是沒有不明確的賦值,也可以使用auto_ptr
- 如上述程式碼所示,unique_ptr為右值(不準確的說類似無法定址)時,可以賦給shared_ptr
- 儘量使用unique_ptr而不要使用auto_ptr
- 一般來說shared_ptr能夠滿足我們大部分的需求
- weak_ptr可以避免遞迴的依賴關係
參考連結:
https://blog.csdn.net/derkampf/article/details/72654883
https://blog.csdn.net/weixin_36888577/article/details/80188414
https://blog.csdn.net/zhuziyu1157817544/article/details/64927834
https://blog.csdn.net/zsc_976529378/article/details/52250597