1. 程式人生 > >c++ 之四大智慧指標 std::auto_ptr std::shared_ptr std::unuque std::weak_ptr 比較總結

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。 

  1. shared_ptr 允許多個指標指向同一個物件 
  2. unique_ptr 獨佔所指向的物件
  3. weak_ptr  shared_ptr的弱引用 
  4. 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;
}

總結:

  1. 當多個物件指向同一個物件的指標時,應選擇shared_ptr 
  2. 用new申請的記憶體,返回指向這塊記憶體的指標時,選擇unique_ptr就不錯 
  3. 在滿足unique_ptr要求的條件時,前提是沒有不明確的賦值,也可以使用auto_ptr 
  4. 如上述程式碼所示,unique_ptr為右值(不準確的說類似無法定址)時,可以賦給shared_ptr 
  5. 儘量使用unique_ptr而不要使用auto_ptr
  6. 一般來說shared_ptr能夠滿足我們大部分的需求
  7. 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

https://www.cnblogs.com/lsgxeva/p/7788061.html