程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(9)

Effective C++讀書筆記(9)

編輯:C++入門知識

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

Think carefully about copying behaviorin resource-managing classes

條款13介紹了作為資源管理類支柱的 Resource Acquisition IsInitialization (RAII) 原則,並描述了 auto_ptr 和 tr1::shared_ptr 在基於堆的資源上運用這一原則的表現。然而,並非所有的資源都是基於堆的,對於這樣的資源,像 auto_ptr 和 tr1::shared_ptr 這樣的智能指針往往不適合作為資源掌管者。在這種情況下,有時可能要根據你自己的需要去創建自己的資源管理類。

例如,假設使用 C API 提供的 lock 和 unlock 函數去操縱 Mutex 類型的互斥對象:

void lock(Mutex *pm); // 鎖定pm所指的互斥器

void unlock(Mutex *pm); // 將互斥器解除鎖定

為了確保不會忘記解鎖一個被你加了鎖的 Mutex,你希望創建一個類來管理鎖。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); // 鎖定互斥器
... // 執行critical section內的操作

} // 在區塊最末尾,自動解除互斥器鎖定

critical section:每個線程中訪問臨界資源的那段程序稱為臨界區(Critical Section)(臨界資源是一次僅允許一個線程使用的共享資源)。每次只准許一個線程進入臨界區,進入後不允許其他線程進入。不論是硬件臨界資源,還是軟件臨界資源,多個線程必須互斥地對它進行訪問。

這沒什麼問題,但是如果一個 Lock 對象被拷貝應該發生什麼?

Lock ml1(&m); // 鎖定m

Lock ml2(ml1); // 將ml1復制到ml2身上,這會發生什麼?

當一個 RAII 對象被拷貝的時候應該發生什麼?大多數情況下,你可以從下面各種可能性中挑選一個:

禁止拷貝:在很多情況下,允許 RAII 被拷貝並不合理。當拷貝對一個 RAII 類沒有什麼意義的時候,你應該禁止它,通過聲明拷貝操作為私有。對於 Lock,看起來也許像這樣:
class Lock: private Uncopyable { // 禁止復制
public:
... // 如前
};

對底層的資源引用計數:有時我們希望保有資源直到最後一個使用它的對象被銷毀。在這種情況下,拷貝一個 RAII 對象應該增加引用這一資源的對象數目, tr1::shared_ptr正是如此。
通常,RAII 類只需要包含一個 tr1::shared_ptr 數據成員就能夠實現引用計數的拷貝行為。例如Lock 要使用引用計數,他可能要將 mutexPtr 的類型從 Mutex* 改變為 tr1::shared_ptr<Mutex>。然而tr1::shared_ptr 的缺省行為是當引用計數變為 0 的時候將它刪除,但這不是我們要的,我們想要將它解鎖,而不是刪除。

幸運的是,tr1::shared_ptr 允許指定所謂的"deleter"(刪除器)——當引用計數變為 0 時調用的一個函數或者函數對象(這一功能是 auto_ptr 所沒有的,auto_ptr 總是刪除它的指針)。deleter是 tr1::shared_ptr 的構造函數可有可無的第二參數,所以,代碼看起來就像這樣:

class Lock {
public:
    explicit Lock(Mutex *pm):mutexPtr(pm, unlock)

       // 以某個mutex初始化shared_ptr並以unlock函數為刪除器
{ lock(mutexPtr.get());}
private:
    std::tr1::shared_ptr<Mutex> mutexPtr; };

   // 使用shared_ptr替換raw pointer

注意 Lock 類沒有聲明析構函數。類的析構函數(無論它是編譯器生成還是用戶定義)會自動調用這個類的non-static成員變量的析構函數(本例為mutexPtr)。當互斥體的引用計數變為 0 時,mutexPtr 的析構函數會自動調用tr1::shared_ptr 的deleter(本例為unlock)。

拷貝底層資源:有時就像你所希望的你可以擁有一個資源的多個副本。在這種情況下,拷貝一個資源管理對象也要同時拷貝被它包覆的資源。也就是說,拷貝資源管理對象需要進行的是“深層拷貝”。
某些標准字符串類型是由“指向heap內存”的指針構成,那內存用來存放字符串的組成字符。這樣的字符串對象包含一個指針指向一塊heap內存。當一個string 對象被拷貝,這個副本應該由那個指針和它所指向的內存組成。這樣的字符串展現深度復制行為。

傳遞底層資源的所有權。在某些特殊場合,你可能希望確保只有一個 RAII 對象引用一個裸資源(raw resource),而當這個 RAII 對象被拷貝的時候,資源的所有權從被拷貝的對象傳遞到拷貝對象。就像 Item 13 所說明的,這就是使用 auto_ptr 時“拷貝”的含意。
拷貝RAII 對象必須一並拷貝它所管理的資源,所以資源的拷貝行為決定了 RAII 對象的拷貝行為。
普通的 RAII 類的拷貝行為是:阻止拷貝、引用計數,但其它行為也是有可能的。
 

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

Provide access to raw resources inresource-managing classes

很多 API 直接涉及資源,所以除非你計劃堅決放棄使用這樣的 API(太不實際),否則,你就要經常繞過資源管理類而直接處理原始資源(raw resources)。

例如使用類似 auto_ptr 或 tr1::shared_ptr 這樣的智能指針來保存 createInvestment 這樣的 factory 函數的結果,並希望以某個函數處理Investment對象:

std::tr1::shared_ptr<Investment> pInv(createInvestment());

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

int days = daysHeld(pInv); // 錯誤!

daysHeld 要求一個Investment* 指針,但是你傳給它一個類型為 tr1::shared_ptr<Investment> 的對象。你需要一個將 RAII 類(本例為 tr1::shared_ptr)對象轉化為它所包含的原始資源(本例為底部之Investment*)的函數。有兩個常規方法來做這件事:顯式轉換和隱式轉換。

tr1::shared_ptr 和 auto_ptr 都提供一個 get 成員函數進行顯示轉換,也就是說,返回一個智能指針對象內部的原始指針(或它的一個副本):

int days = daysHeld(pInv.get());// 將pInv內的原始指針傳給daysHeld

就像幾乎所有智能指針一樣,tr1::shared_ptr和 auto_ptr 也都重載了指針解引用操作符(operator-> 和 operator*),它們允許隱式轉換到底部原始指針:

class Investment { // investment繼承體系的根類
public:
   bool isTaxFree() const;
   ...
};
Investment* createInvestment(); // factory函數

std::tr1::shared_ptr<Investment> pi1(createInvestment());

bool taxable1 = !(pi1->isTaxFree()); // 經由operator->訪問資源
...

std::auto_ptr<Investment>pi2(createInvestment());

bool taxable2 = !((*pi2).isTaxFree()); // 經由operator*訪問資源

...

再考慮以下這個用於字體的RAII類(對C CPI而言字體是一種原生數據結構):

FontHandle getFont(); // C API,為求簡化略參數

void releaseFont(FontHandle fh); // 來自同一組C API
class Font { // RAII class
public:
    explicit Font(FontHandle fh): f(fh){} // 值傳遞獲得資源

       ~Font() { releaseFont(f); }

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

假設有大量與字體相關的C API,它們處理的是FontHandle,這就需要頻繁地將 Font 對象轉換為 FontHandle。Font 類可以提供一個顯式的轉換函數,比如get:
FontHandle get() const {return f; }

不幸的是,這就要求客戶每次與 API 通信時都要調用 get:

void changeFontSize(FontHandle f, intnewSize); // C API

Font f(getFont());
int newFontSize;
...

changeFontSize(f.get(), newFontSize); // 顯式地將Font轉換為FontHandle

一些程序員可能發現對顯式請求這個轉換的需求足以令人郁悶而避免使用這個類。另一個可選擇的辦法是為 Font 提供一個隱式轉換函數,轉型為FontHandle:
operator FontHandle()const { return f; }

這樣就可以使對C API的調用簡單自然:

changeFontSize(f, newFontSize); // 隱式轉換,與上例作對照

不利的方面是隱式轉換增加錯誤發生機會。例如,一個客戶可能會在有意使用 Font 的地方意外地產生一個 FontHandle:

Font f1(getFont());

...

FontHandle f2 = f1;

// 原意是復制一個Font對象,卻反而將f1隱式轉換為其底部的FontHandle然後才復制

當 f1被銷毀,字體將被釋放,f2則被懸掛(dangle)。

最好的設計就是堅持“使接口易於正確使用,不易被誤用”。通常,類似get的一個顯式轉換函數是更可取的方式,因為它將意外的類型轉換的機會減到最少。而有時候通過隱式類型轉換將提高使用的自然性。

RAII類的存在並非為了封裝什麼東西而是為了確保資源釋放這一特殊行為的發生。此外,一些 RAII類將實現的真正封裝和底層資源的寬松封裝結合在一起如tr1::shared_ptr 封裝了它引用計數的全部機制,但它依然提供對它所包含資源的簡單訪問。就像大多數設計良好的類,它隱藏了客戶不需要看到的,但它也讓客戶確實需要訪問的東西可以被利用。

·    API 經常需要訪問原始資源,所以每一個 RAII 類都應提供取得它所管理資源的方法。

·    訪問可以通過顯式轉換或者隱式轉換進行。通常,顯式轉換更安全,而隱式轉換對客戶來說更方便。

 


 摘自 pandawuwyj的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved