動態內存與智能指針

我們先來看一些對象的生存期。全局對象在程序啓動時分配,在程序結束時銷燬。局部static對象在第一次使用前分配,在程序結束時銷燬。局部自動對象,在進入其定義所在的程序塊兒時被創建,離開塊時銷燬。即,它們都是由編譯器自動創建與銷燬。
而動態分配的對象的生存期與它們在哪裏創建的無關,只有當顯式地被釋放時,這些對象才銷燬。

在C++中,動態內存的管理是通過一對運算符來完成的:

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

動態內存的使用很容易出問題,這裏列舉如下幾點:

緩衝區溢出
空懸指針/野指針
重複釋放: 釋放已經被釋放過了的內存
內存泄漏: 忘記釋放內存
不配對的new[]/delete

先列這幾條,等先了解智能指針的概念以後在來看怎麼用智能指針解決這些問題。

爲了更安全(同時也更容易)地使用動態內存,標準庫提供了智能指針類型來管理動態對象。智能指針的行爲類似於常規指針,重要的區別是它負責自動釋放所指向的對象。

我們首先來看一下shared_ptr類,該類型在memory頭文件中。

shared_ptr類

智能指針也是模板。因此在創建一個智能指針時,必須提供它可以指向的類型。

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

默認初始化的智能指針保存着一個空指針。類似與普通指針,解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用智能指針,效果是檢測它是否爲空。

//如果p1指向一個空string,解引用p1,將一個新值給string
if(p1 && p1->empty()){
   *p1 = "hi";
}

最安全的分配和使用動態內存的方法是調用標準庫函數make_shared。定義在頭文件memory中。該函數在動態內存中分配一個對象並初始化,返回指向此對象的shared_ptr。make_shared也是一個模板,使用方法如下:

//指向一個值爲42的int對象的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);

//p4指向一個"9999999999"的string對象
shared_ptr<string> p4 = make_shared<string>(10,'9');

//p5指向一個值初始化的int,即值爲0
shared_ptr<int> p5 = make_shared<int>();

make_shared用其參數來構造給定類型的對象。例如,調用make_shared時傳遞的參數必須與string的某個構造函數相匹配。通常用auto定義一個對象來保存make_shared的結果:

//p6指向一個動態分配的vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr的拷貝和賦值

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

auto p = make_shared<int>(42);   //p指向的對象只有p一個引用者
auto q(p);                       //pq指向相同的對象,此對象有兩個引用者。

可以認爲每個shared_ptr都有一個關聯的計數器(引用計數)。無論何時,拷貝一個shared_ptr,計數器都會遞增。例如:

用一個shared_ptr初始化另外一個shared_ptr
將一個shared_ptr作爲一個參數傳遞給一個函數
當一個shared_ptr作爲一個函數的返回值

相對的,給一個shared_ptr賦予一個新值或者shared_ptr被銷燬時,計數器就會遞減。
一旦一個shared_ptr的計數器變爲0,它就會自動釋放自己所管理的對象。

auto r = make_shared<int>(42);
r = q;  //q指向的對象的引用計數遞增。
        //r原來指向的對象的引用計數遞減。
        //r原來指向的對象沒有了引用者,會自動釋放。

當指向一個對象的最後一個shared_ptr被銷燬時,shared_ptr類會自動銷燬此對象,它是通過析構函數來完成銷燬動作的。shared_ptr的析構函數會遞減它所指向的對象的引用計數,如果引用計數邊爲0,shared_ptr的析構函數就會銷燬對象,並釋放它佔用的內存。

對於一塊內存,shared_ptr類保證只要有任何shared_ptr對象引用它,它就不會被釋放掉。

但是,如果你忘記了銷燬程序不需要的shared_ptr,程序仍會正確執行,但會浪費內存。一種可能的情況是,將shared_ptr放在一個容器裏,當不需要某些元素時,應該確保用erase刪除那些不需要的shared_ptr元素。

程序使用動態生存期的資源的類 出於以下三種原因之一:

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

下面是一個需要在多個對象間共享數據的例子:
我們定義一個strblob類,保存一組元素。與容器不同,我們希望strblob對象的不同拷貝之間共享相同的元素。我們可以用一個vector來保存元素,但是,不能在一個strblob對象內直接保存vector,因爲一個對象的數據成員在對象銷燬時也會被銷燬。爲了實現strblob對象共享相同的底層數據,我們可以將vector保存在動態內存中,然後爲每個strblob設置一個shared_ptr來管理動態內存分配的vector。這個shared_ptr的成員將記錄有多少個shared_ptr共享相同的vector,並在vector的最後一個使用者被銷燬時釋放vector。


#ifndef _STRBLOB_H
#define _STRBLOB_H
#include<string>
#include<memory>
#include<list>
#include<vector>

using namespace std;
class strblob
{
public:
    strblob();
    strblob(std::initializer_list<std::string> s);
    void print(){
        for(auto v : *data_){
            std::cout << v << std::endl; 
        }
    }

private:
    std::shared_ptr<std::vector<std::string>> data_;
};

strblob::strblob():data_( make_shared<vector<string>>() ){}
strblob::strblob(initializer_list<string> s):data_(make_shared<vector<string>>(s)){}
#endif

下面是一個測試用例

#include"strblob.h"
int main(int argc,char *argv[])
{
    std::initializer_list<std::string> s = {"hello"};

    strblob A;       //使用不帶參數的構造函數
    strblob B(s);    //使用帶參數的構造函數

    A.print();
    B.print();

    std::cout << std::endl;
    //B對象的shred_ptr指向的對象的引用計數遞增,A的遞減並且釋放A的shared_ptr指向的對象
    A = B;    //使用默認賦值
    A.print();
    B.print();
    return 0;
}

其他shared_ptr操作:

我們可以用reset來將一個新的指針賦予一個shared_ptr

p.reset();     //若p是唯一指向對象的shared_ptr,reset會釋放此對象。
p.reset(q);    //若傳遞了可選參數內置指針q,會令p指向q,否則會將p置爲空。
p.reset(q,d);  //如果還傳遞了參數d,將會調用d而不是delete來釋放q

weak_ptr

weak_ptr是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到shared_ptr不會改變shared_ptr的引用計數。一旦最後一個指向對象的shared_ptr被銷燬,對象就會被釋放。即使有weak_ptr指向對象,對象也還是會被釋放,因此weak_ptr的名字抓住了這種智能指針“弱”共享對象的特點。

當我們創建一個weak_ptr時,要用一個shared_ptr來初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  //wp弱共享p; p的引用計數未改變。

由於waek_ptr指向的對象可能不存在,因此我們不能直接使用weak_ptr來訪問對象,而必須調用lock()。此函數檢查weak_ptr指向的對象是否仍存在,如果存在,lock返回一個指向共享對象的shared_ptr。與任何其他shared_ptr類似,只要此shared_ptr存在,它所指向的底層對象也就會一直存在。

if(auto np = wp.lock()){
//進入if條件證明wp指向的對象存在,在這裏np與p共享對象
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章