C++ 學習筆記之(12) - 動態內存、智能指針和動態數組

C++ 學習筆記之(12) - 動態內存、智能指針和動態數組

程序中所使用的對象都有嚴格定義的生存期。

  • 全局對象:在程序啓動時分配,程序結束時銷燬
  • 局部自動對象:程序進入其定義所在塊時創建,離開塊時銷燬
  • 局部static對象:第一次使用前分配,程序結束時銷燬
  • 動態分配對象:顯示創建,顯示釋放

內存存放區間

  • 靜態內存:局部static對象、類static數據成員以及定義在任何函數之外的而變量
  • 棧內存:定義在函數內的非static對象
  • 自由空間(堆):動態分配對象,動態分配對象的生存期由程序控制

動態內存與智能指針

動態內存的管理是通過一對運算符完成的

  • new:在動態內存中爲對象分配空間並返回一個指向該對象的指針,用來對對象進行初始化
  • delete:接受一個動態對象的指針,銷燬該對象,並釋放與之關聯的內存

動態內存的使用容器出問題

  • 內存泄漏:忘記釋放內存
  • 非法內存指針:在尚有指針引用內存的情況下就釋放了呢村

新標準庫提供了兩種智能指針類型管理動態對象,智能指針與常規指針的區別在於它負責自動釋放所指向的對象

  • shared_ptr:允許多個指針指向同一個對象
  • unique_ptr:獨佔所指向的對象
  • weak_ptr:一種弱引用,指向shared_ptr所管理的對象

shared_ptr

shared_ptr_and_unique_ptr_specific_operations

創建智能指針

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

make_shared 函數

make_shared在動態內存中分配一個隊形並初始化它,返回指向此對象的shared_ptr, 用其參數來構造給定類型的對象

// 指向一個值爲“9999999999” 的 string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// 也可使用 auto, p6 指向一個動態分配的空 vector<string>
auto p6 = make_shared<vector<string>>();

引用計數

  • 拷貝時計數器遞增
    • 一個shared_ptr初始化另一個shared_ptr
    • shared_ptr作爲參數傳遞給函數
    • shared_ptr作爲函數的返回值
  • 計數器遞減
    • shared_ptr賦予新值時
    • shared_ptr銷燬時(比如局部shared_ptr對象離開其作用域)
auto r = make_shared<int>(42);  //  r 指向的 int 只有一個引用者
r = q;  // 給 r 賦值,令它指向另一個地址
        // 遞增 q 指向的對象的引用計數
        // 遞減 r 原來指向的對象的引用計數
        // r 原來指向的對象已沒有引用者,會自動釋放

shared_ptr銷燬和釋放內存

shared_ptr的析構函數會遞減它所指向的對象的引用計數,如果引用技術變爲0shared_ptr的析構函數就會銷燬對象,並釋放其佔用的內存

void function(T arg)
{
    shared_ptr<int> p = make_shared<int>(125);
          // 情況1:不返回, p 爲局部變量,函數結束時被銷燬,p被銷燬時,引用計數遞減爲0,故內存被釋放
    return p;  // 情況2:返回 p 的拷貝, 引用計數進行遞增操作,故即使 p 被銷燬,但內存還有其他使用者,不會被釋放
}

使用動態內存的原因

  • 程序不知道自己需要使用多少對象, 比如容器類
  • 程序不知道所需對象的準確類型
  • 程序需要在多個對象間共享數據

直接管理內存

C++語言定義了兩個運算符來分配和釋放動態內存,相對於智能指針,直接管理容器出錯

  • new:分配內存,並返回一個指向該對象的指針。

    // 默認情況下,動態分配對象是默認初始化的,即內置類型或組合類型的對象的值將未定義,類使用默認構造函數
    int *pi = new int;  // pi 指向一個動態分配的、未初始化的無名對象,即默認初始化,`*pi值未定義
    // 也可對動態分配對象進行值初始化,只需在類型名之後跟一對控括號
    int *pi2 = new int();  // 值初始化爲0, *pi2 爲0
    ///若括號中僅有單一初始化器,可使用 auto 推斷想要分配的對象類型
    auto p1 = new auto(obj);  // p1類型是指針,指向從 obj 自動推斷出的類型
    auto p2  new auto{a, b, c};  // 錯誤:括號中只能有單個初始化器
    //用 new 分配 const 對象是合法的, 下爲分配並初始化一個 const int
    const int *pci = new const int(1024);
  • delete:銷燬給定的指針指向的對象,釋放對應的內存。必須是指向動態分配的指針,或空指針。釋放一塊並非new分配的內存,或者將相同的指針釋放多次,其行爲是未定義的

  • 空懸置鎮:指向一塊曾經保存數據對象但現在已經無效的內存的指針

shared_ptrnew結合使用

  • 接受指針參數的智能指針構造函數是explicit的,故不能講一個內置指針隱式轉換爲智能指針,必須使用直接初始化方式

    shared_ptr<int> p1 = new int(1024);  // 錯誤:必須使用直接初始化方式
    shared_ptr<int> p2(new int(1024));  // 正確:使用了直接初始化方式
    

    shared_ptr_define_and_modify_other_operations

  • 不要混合使用普通指針和智能指針,因爲內置指針訪問智能指針所負責的對象是很危險的,因爲無法知道對象何時會被銷燬

  • 不要使用get初始化另一個智能指針或爲智能指針賦值。get定義在智能指針類型中,迎來返回一個內置指針,指向智能指針管理的對象。

    shared_ptr<int> p(new int(42));  // 引用計數爲 1 
    int *q = p.get();  // 正確:但使用 q 時要注意,不要讓他管理的指針被釋放
    {// 新程序塊
        // 未定義:兩個獨立的`shared_ptr`指向相同的內存
      shared_ptr<int>(q)
    }// 程序塊結束,q 被銷燬,它指向的內存被釋放
    int foot = *p;  // 未定義:p 指向的內存已經被釋放了

智能指針和異常

  • 在發生異常時,智能指針仍能正確釋放,但直接管理的內存不會自動釋放

    void f()
    {
      shared_ptr<int> sp(new int(42));  // 分配一個新對象
        int *ip = new int(42);  // 動態分配一個新對象
        // 這段代碼拋出一個異常,且在 f 中未被捕獲
        delete ip;  // 在推出之前釋放內存,發生異常時,沒有執行,被跳過
    } // 在函數結束時 shared_ptr 自動釋放內存
  • 刪除器:釋放 shared_ptr中保存的指針,用來取代shared_ptr被銷燬時默認進行的delete操作,在創建shared_ptr時即可指定

  • 正確使用智能指針的接本規範

    • 不使用相同的內置指針值初始化(或reset)多個智能指針
    • delete``get()返回的指針
    • 不使用get()初始化或reset另一個智能指針
    • 如果你使用get()返回的指針,記住當最後一個對應的智能指針銷燬後,它會失效
    • 如果智能指針管理的資源表示new分配的內存,記住傳遞給他一個刪除器

unique_ptr

unique_ptr擁有它所指向的對象,即某時刻只能有一個unique_ptr指向某對象,當其被銷燬時,所指向的對象也被銷燬

  • unique_ptr無類似make_shared函數。定義時,需要將其綁定到new返回的指針上

  • 類似shared_ptr,初始化unique_ptr需採用直接初始化形式

  • unique_ptr獨佔其指向的對象,故不支持普通的拷貝或賦值操作

    unique_ptr<string> p1(new string("hao"));  // p2 指向一個值爲 hao 的 string
    unique_ptr<string> p2(p1);  // 錯誤:unique_ptr 不支持拷貝
    unique_ptr<string> p3;
    p3 = p2;  // 錯誤:unique_ptr 不支持賦值
  • unique_ptr雖然不能賦值或拷貝,但可以調用releasereset將所有權從非constunique_ptr轉移到另一個unique_ptr

    unique_ptr_operations

    // 接上
    unique_ptr<string> p2(p1.release());  // release 將 p1 置爲空,並將所有權轉移給 p2
  • unique_ptr在將被銷燬時可以拷貝或賦值, 比如從函數返回unique_ptr

    unique_ptr<int> clone(int p)
    {
      return unique_ptr<int>(new int(p));
    }
  • 類似shared_ptr, unique_ptr也可重載其默認的刪除器

    // p 指向一個類型爲 objT 的對象,並使用一個類型爲 delT 的對象釋放 objT對象
    // 它會調用一個名爲 fcn 的 delT 類型對象
    unique_ptr<objT, delT> p (new objT, fcn);

weak_ptr

一種不控制所指向對象生存期的智能指針,指向由一個shared_ptr管理的對象。且綁定後不會改變shared_ptr的引用計數

weak_ptr

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp 弱共享 p; p 的引用計數未改變
// 由於對象可能不存在,故不能使用 weak_ptr 直接訪問隊形,而必須調用 lock
if(shared_ptr<int> np = wp.lock())  // 如果 np 不爲空則條件成立
{
    // 在 if 中, np 與 p 共享對象
}

動態數組

C++語言和標準庫提供了兩種一次分配一個對象數組的方法。

  • C++ 語言定義了另一種new表達式語法,可以分配並初始化一個對象數組
  • 標準庫提供了allocator類,允許分配和初始化分離,性能更好且更靈活

new和數組

  • new分配數組

    int *pia = new int[5];  // pia 指向第一個 int
    typedef int arrT[42];  // arrT 表示 42 個 int 的數組類型
    int *p = new arrT;  // 分配一個 42 個int 的數組; p 指向第一個 int
  • 雖然通常稱new T[]分配的內存爲動態數組,但並未得到數組類型對象,而是得到一個數組元素類型的指針,即動態數組並不是數組類型,不能使用某些函數比如beginend或範圍for語句

  • 默認,new分配的對象,都執行默認初始化。也可以執行值初始化,只要在後面加()即可

    int *pia = new int[10];  // 10 個未初始化的 int
    int *pia2 = new int[10]();  // 10 個值初始化爲 0 的 int
    int *pia3 = new int[10]{0, 1, 2, 3, 4, ,5};  // 列表初始化,剩餘元素執行值初始化
  • 不能在括號中給出初始化器,即不能用auto分配數組

  • 釋放動態數組時要在指針前加一個空方括號,即使使用類型別名定義數組,元素逆序銷燬

    typedef int arrT[42];  // arrT 表示 42 個 int 的數組類型
    int *p = new arrT;  // 分配一個 42 個int 的數組; p 指向第一個 int
    delete [] p;  // 方括號必需,因爲分配的是一個數組
  • 標準庫提供了可管理new分配的數組的unique_ptr版本

    unique_ptr<int[]> up(new int[10]);  // up 指向一個包含10個未初始化 int 的數組
    up.release();  // 自動用 delete[] 銷燬其指針

    unique_ptr_point_to_array

  • 若希望使用shared_ptr管理動態數組,必須提供自定義的刪除器

    shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});  // 提供自定義刪除器
    sp.reset();  // 使用自定義的 lambda 釋放數組,它使用 delete[]

allocator

allocator類將內存分配和對象構造分離,其分配的內存是原始的、未構造的。

allocator_class_and_its_algorithms

allocator<string> alloc;  // 可以分配 string 的 allocator 對象
auto const p = alloc.allocate(n);  // 分配 n 個未初始化的 string

auto q = p;  // q 指向最後構造的元素之後的位置
alloc.construct(q++);  // *q 爲空字符串
alloc.construct(q++, 10, 'c');  // *q 爲 cccccccccc
alloc.construct(q++, "hi");  // *q 爲 hi

while(q != p)
    alloc.destroy(--q);  // 釋放我們真正構造的 string

alloc.deallocate(p, n);  // 釋放內存
  • 爲了使用allocator返回的內存,必須用construct構造對象,使用位構造的內存,行爲未定義

  • 使用完,必須對每個構造的元素調用destroy來銷燬,destroy接受一個指針,對指向的對象執行析構函數

  • 銷燬後,可重新使用這部分內存保存其他 string, 也可以釋放內存還給系統

  • 拷貝和填充未初始化內存的算法

    allocator_algorithms

    vector<int> vi{1, 2, 3};
    allocator<int> alloc;
    auto p = alloc.allocate(vi.size() * 2);  // 分配比 vi 中元素所佔空間大一倍的動態內存
    auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷貝vi中元素構造從p開始的元素
    uninitialized_fill_n(q, vi.size(), 42);  // 將剩餘元素初始化爲42

結語

在C++中,內存通過new 表達式分配,通過delete表達式釋放。標準庫還定義了一個``allocator類來分配動態內存塊

現在C++程序應儘可能使用智能指針,因爲直接管理很容易出錯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章