《Effective C++》學習筆記(4)

3 資源管理

所謂資源就是,一旦用了它,將來必須還給系統。C++程序中最常使用的資源就是動態分配內存(如果分配內存卻從不歸還它, 就會導致內存泄漏),但內存只是你必須管理的眾多資源之一。其他常見的資源還包括文件描述器、互斥鎖、圖形界面中的字型和筆刷、數據庫連接以及網絡sockets。無論哪一種資源,重要的是,當你不再使用它時,必須將它還給系統。

當你考慮到異常、函數內多重回傳路徑、程序維護員改動軟件卻沒能充分理解隨之而來的沖擊,顯然,資源管理的特殊手段還不很充分利用。

條款13:以對象管理資源

void f()     
{  
    Investment *pInv = createInvestment();
    ...                   //這里存在諸多“不定因素”,可能造成delete pInv;得不到執行,這可能就存在潛在的內存泄露。   
    delete pInv;     
} 

如上代碼,createInvestment的調用端使用了函數返回的對象后,有責任刪除之。但一些情況下delete可能無法執行:

  • “...”區域內有一個過早的return語句;
  • createInvestment和delete動作位于某循環內,而該循環由于某個continue或goto語句過早退出;
  • “...”區域內的語句拋出異常,控制流不會臨幸delete。

把資源放進對象內,我們便可依賴C++的“析構函數自動調用機制”確保資源被釋放。

auto_ptr

許多資源被動態分配于heap(堆)內而后被用于單一區塊或函數內。它們應該在控制流離開那個區塊或函數時被釋放。標準程序庫提供的auto_ptr正是針對這種形勢而設計的特制產品。auto_ptr是個“類指針對象”,也就是所謂的“智能指針”,其析構函數自動對其所指對象調用delete。

void f()     
{      
    std::auto_ptr<Investment> pInv(createInvestment());  
    ...      
}          //函數退出,auto_ptr調用析構函數自動調用delete,刪除pInv;無需顯示調用delete。

上面的例子師范了“以對象管理資源”的兩個關鍵想法:

  • 獲得資源后立刻放進管理對象(managing object)內。
    “資源取得時機便是初始化時機”(Resource Acquisition Is Initialization;RAII)。每一筆資源都在獲得的同時立刻被放進管理對象中。
  • 管理對象運用析構函數確保資源被釋放。
    一旦對象被銷毀,其析構函數被自動調用來釋放資源。如果資源釋放動作可能導致拋出異常,參見條款8的解決方法。

由于auto_ptr被銷毀時會自動刪除它所指之物,所以不能讓多個auto_ptr同時指向同一對象。為了預防這個問題,auto_ptr有一個性質:auto_ptr若通過copy構造函數或copy assignment操作符復制它們,它們會變成NULL,而復制所得的指針將取得資源的唯一擁有權!但這一性質限制了元素不能發揮“正常的”復制行為,如在STL容器上就不適合,因此并非管理動態分配資源的神兵利器。

RCSP

auto_ptr的替代方案是“引用計數型智能指針”(reference-counting smart pointer;RCSP)、它可以持續跟蹤共有多少對象指向某筆資源,并在無人指向它時自動刪除該資源。RCSP類似垃圾回收,但不同的是它無法打破環狀引用(例如兩個其實已經沒被使用的對象彼此互指,因而好像還處在“被使用”狀態)。

TR1的tr1::shared_ptr就是一個"引用計數型智能指針",且可執行正常的復制行為,因此可以被用于STL容器以及其他“auto_ptr之非正統復制行為并不適用”的語境上。

void f()     
{
    ...
    std::tr1::shared_ptr<Investment>  pInv1(createInvestment()); // pInv1指向createInvestment()返回物;
    std::tr1::shared_ptr<Investment>  pInv2(pInv1);              //pInv1,pInv2指向同一個對象; 
    pInv1 = pInv2;                                               //同上,無變化      
    ...      }         // pInv1,pInv2被銷毀,它們所指的對象也被自動銷毀

auto_ptr和tr1::shared_ptr都在其析構函數內做delete而不是delete[],也就意味著在動態分配而得的數組身上使用auto_ptr或tr1::shared_ptr是個潛在危險,資源得不到釋放。還有,vector和string幾乎總是可以取代動態分配而得的數組。

note:

  1. 為防止資源泄漏,請使用RAII對象,它們在構造函數中獲得資源并在析構函數中釋放資源。
  2. 兩個常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。后者通常是較佳選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,復制動作會使他(被復制物)指向NULL。

條款14:在資源管理類中小心 copying 行為

我們在條款13中討論的資源表現在heap堆上申請的資源,而有些資源并不是heap-based,因此不適合被auto_ptr和tr1::shared_ptr所管理。此時,我們需要建立自己的資源管理類。

假設我們處理Mutex類型的互斥器對象,有兩種操作:

void lock(Mutex *pm);     //鎖定pm所指的互斥量
void unlock(Mutex *pm);        //將互斥器解除鎖定

建立遵循RAII守則(資源在構造期間獲得,在析構期間釋放)的資源管理類:

class Lock { 
public: 
    explicit Lock(Mutex *pm) 
       : mutexPtr(pm)  
       { lock(mutexPtr); }     // 獲得資源
    ~Lock() { unlock(mutexPtr); }    // 釋放資源         
private: 
    Mutex *mutexPtr;     
}; 
// 客戶對Lock的正確用法符合RAII方式
Mutex m;         // 定義你需要的互斥器
....
{                         // 建立一個區塊用來定義critical section
    Lock ml(&m);      // 鎖定互斥器
    ......
}                        // 在區塊最末尾,自動接觸互斥器鎖定

上面的使用沒有問題,但問題是,如果Lock對象被復制,會發生什么事呢?就像這樣:

Lock ml1(&m);
Lock ml2(ml1);

當一個RAII對象被復制,會發生什么事?

  1. 禁止復制。
    如果復制動作對RAII class并不合理,就應該禁止它。詳見條款06:將copying操作聲明為private。
  2. 對底層資源祭出“引用計數法”。
    有時候希望保有資源,直到它的最后一個使用者被銷毀,這種情況下復制RAII對象時,應該講資源的“被引用數”遞增。例如trl::shared_ptr。

通常只要內含一個tr1::shared_ptr成員變量,RAII類便可實現”引用計數“行為。

但是,tr1::shared_ptr缺省行為是”當引用計數為0時刪除其所指物“,因此,我們還需指定所謂“刪除器”(一個函數或對象),當引用計數為0時便被調用(此機能并不存在于auto_ptr,它總是將指針刪除)。

class Lock     {        
public:  
        explicit Lock(Mutex *pm)        // 以某個Mutex初始化shared_ptr
          : mutexPtr(pm, unlock)        // 并以unlock函數為刪除器
        {
            lock(mutexPtr.get());       // get見條款15
         }         
private: 
        std::tr1::shared_ptr<Mutex> mutexPtr;      使用shared_ptr
}; 

本例中,并沒說明Lock class的析構函數,因為沒有必要。編譯器為我們生成的析構函數會自動調用其non-static成員變量(mutexPtr)的析構函數。而mutexPtr的析構函數會在互斥量”引用計數“為0時自動調用tr1::shared_ptr的刪除器(unlock)。

note:

  1. 復制RAII對象必須一并復制它所管理的資源,所以資源的copying行為決定RAII對象的copying行為。
  2. 普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實現。

條款15:在資源管理類中提供對原始資源的訪問

前幾個條款提到的資源管理類是你對抗資源泄漏的堡壘。但許多APIs直接指涉資源,這時候我們需要直接訪問原始資源。

std::tr1::shared_ptr<Investment> pInv(createInvestment());    // 見條款13

int dayHeld(const Investment* pi);    // 返回投資天數

int days = dayHeld(pInv);    // 錯誤,dayHeld需要的是Investment*指針(原始指針,raw pointer),
                             // 傳給的卻是tr1::shared_ptr<Investment>對象

這時候需要一個函數可將RAII對象(如tr1::shared_ptr)轉換為其所內含之原始資源(Investment*)。有兩種做法可以達成目標:顯式轉換和隱式轉換。

顯式轉換

tr1::shared_ptr和auto_ptr都提供一個get成員函數,用來執行顯示轉換,也就是返回智能指針內部的原始指針(的復件)。

int days = dayHeld(pInv.get()); 
隱式轉換

像所有智能指針一樣, tr1::shared_ptr和auto_ptr重載了指針取值操作符(operator->和operator*),它們允許隱式轉換至底部原始指針。(即在對智能指針對象實施->和*操作時,實際被轉換為被封裝的資源的指針。)

class Investment {
public:
        bool isTaxFree() const;
        ...
}
Investment * createInvestment();

std::tr1::shared_ptr<Investment> pi1(createInvestment());    // shared_ptr管理資源
bool taxable1 = !(pi1->isTaxFree());    // operator ->訪問資源
...

std::auto_ptr<Investment> pi2(createInvestment());    // auto_ptr管理資源
bool taxable2 = !((*pi2).isTaxFree());    //operator *訪問資源
顯式轉換函數和隱式轉換函數
FontHandle getFont();                             // 這是個C API,參數已經簡化
void releaseFont(FontHandle fh);                  // 來自同一組C API
void changeFontSize(FontHandle f, int newSize);   // 來自同一組C API

class Font {
public:
  explicit Font(FontHandle fh)     // 獲得資源
    :f(fh)
  {}
  ~Font() {releaseFont(f);}        // 釋放資源

  FontHandle get() const {return f;}         // 顯式轉換函數
  operator FontHandle() const{return f;}     // 隱式轉換函數

private:
  FontHandle f;    // 原始(raw)字體資源
}

使用:

int newFontSize;
Font f1(getFont());
...
changeFontSize(f1.get(),newFontSize);   // 顯式轉換

Font f2(getFont());
changeFontSize(f2,newFontSize);         // 隱式轉換

這個隱式轉換會增加錯誤發生機會。例如客戶可能會在需要Font時意外得到一個FontHandle:

Font f1(getFont());
...
FontHandle f2 = f1;        // 原意是要拷貝一個Font對象
                           // 卻反而將f1隱式轉換為其底部的FontHandle,
                           // 然后才復制它。

以上程序有個FontHandle由Font對象f1管理,但那個FontHandle也可以通過直接使用f2取得。如果f1被銷毀,字體被釋放,f2因此成為"虛吊的"(dangle)。

note:

  1. APIs往往要求訪問原始資源(raw resources),所以每一個RAII class應該提供一個“取得其所管理之資源”的方法。
  2. 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。

條款16:成對使用 new 和 delete 時要采取相同形式

當我們使用new,有兩件事情發生:第一,內存被分配出來;第二,針對此內存會有一個(或更多)構造函數被調用。當你使用delete,也有兩件事發生:針對此內存會有一個(或多個)析構函數被調用,然后內存才被釋放。delete的最大問題在于:即將被刪除的內存之內究竟有多少對象?這個問題的答案決定了有多少個析構函數必須被調用起來。

如果new數組時使用[],那么釋放資源時就要用delete[],這會調用多個析構函數去釋放資源;如果使用new對象不使用[],釋放時一定不要使用[]。保持兩者一致。

std::string str = new std::string;
std::string strArr = new std::string[20];
//釋放資源
delete str;
delete[] strArr;

最好盡量不要對數組形式作typedefs動作。C++標準庫的string,vector等template,可將數組的需求降至幾乎為零。

note:
如果你在new表達式中使用[],必須在相應的delete表達式中也使用[]。如果你在new表達式中不使用[],一定不要在相應的delete表達式中使用[]。


條款17:以獨立語句將 newed 對象置入智能指針

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

在調用processWidget前,編譯器必須創建代碼,做以下三件事:

  • 調用priority()
  • 執行new Widget
  • 調用tr1::shared_ptr構造函數

但C++的編譯器對這三個執行的次序并不固定。但C++中可以確定的是,new Widget一定比tr1::shared_ptr先執行,但對priority()函數的調用卻沒有限定。如果以下面的順序:

  • 執行new Widget
  • 調用priority()函數
  • 調用tr1::shared_ptr構造函數

這就會引發一個問題,如果第二步priority()函數發生異常,那么new Widget就無法放入shared_ptr中,這樣就會造成資源泄漏(shared_ptr用來進行資源管理)。

正確的做法是將語句分離,先創建資源并放到資源管理器后,再進行下步操作。

std::tr1::shared_ptr<Widget> pw(new Widget);    // 在單獨語句中以智能指針存儲newed所得對象
processWidget(pw, priority);    // 這個調用動作絕不至于造成泄漏

note:
以獨立語句將newed對象存儲于(置入)智能指針內。如果不這樣做,一旦異常拋出,有可能導致難以察覺的資源泄漏。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容