1. 程式人生 > >Effective C++讀書筆記

Effective C++讀書筆記

    但是,我們不該令拷貝賦值操作符呼叫拷貝建構函式,也不該令拷貝建構函式呼叫拷貝賦值操作符。想想,一個是拷貝(建立物件),一個是賦值(物件已經存在)。
    請記住:
  • Copying函式應該確保複製“物件內的所有成員變數”及“所有基類成員”;
  • 不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。  
三.資源管理
    所謂資源就是,一旦用了它,將來必須還給系統。C++程式中最常使用的資源就好似動態分配記憶體(如果你new了,卻忘了delete,會導致記憶體洩露),但記憶體只是你必須管理的眾多資源之一。其它常見的有檔案描述符(file descriptors)、互斥器(mutex)、圖形介面中的字形和畫刷。資料庫連線以及網路sockets。當你不使用它們時,記得還給系統。
     當你考慮到異常、函式內多重回傳路徑、程式維護員改動軟體卻沒能充分理解隨之而來的衝擊,那麼資源管理就顯得複雜的多。

    條款13:以物件管理資源
    例:
    
void f()
     {
         Investment *pInv = createInvestment();
         ...                   //這裡存在諸多“不定因素”,可能造成delete pInv;得不到執行,這可能就存在潛在的記憶體洩露。
         delete pInv;
     }

    解決方法:把資源放進物件內,我們便可依賴C++的“解構函式自動呼叫機制”確保資源被釋放。
    許多資源被動態分配於堆內而後被用於單一區塊或函式內。它們應該在控制流離開那個區塊或函式時被釋放。標準程式庫提供的auto_ptr
正是針對這種形勢而設計的特製產品。auto_ptr是個“類指標物件”,也就是所謂的“智慧指標”,其解構函式自動對其所指物件呼叫delete。

    void f()
     {
         std::auto_ptr<Investment> pInv(createInvestment());
         ...
     }
         //函式退出,auto_ptr呼叫解構函式自動呼叫delete,刪除pInv;無需顯示呼叫delete。
    “以物件管理資源”的兩個關鍵想法
  • 獲得資源後立刻放進管理物件內(如auto_ptr)。每一筆資源都在獲得的同時立刻被放進管理物件中。“資源取得時機便是初始化時機”(Resource Acquisition Is Initialization;RAII)。
  • 管理物件運用解構函式確保資源被釋放。即一旦物件被銷燬,其解構函式被自動呼叫來釋放資源。
    由於auto_ptr被銷燬時會自動刪除它所指之物,所以不能讓多個auto_ptr同時指向同一物件。所以auto_ptr若通過copying函式複製它們,它們會變成NULL,而複製所得的指標將取得資源的唯一擁有權!
    看下面例子:
    std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
     std::auto_ptr<Investment> pInv2(pInv1);                      //現在pInv2指向物件,而pInv1被設為NULL;
     pInv1 = pInv2;                                                               //現在pInv1指向物件,而pIn2被設為NULL;

    受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它即“有你沒我,有我沒你”。
    auto_ptr的替代方案是“引用計數型智慧指標”(reference-counting smart pointer;SCSP)、它可以持續跟蹤共有多少物件指向某筆資源,並在無人指向它時自動刪除該資源。
    TR1的tr1::shared_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是個潛在危險,資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。還有,vector和string幾乎總是可以取代動態分配而得的陣列。
    請記住:
  • 為防止資源洩漏,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源。
  • 兩個常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。後者通常是較佳選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,複製動作會使他(被複制物)指向NULL。   
    條款14:在資源管理類中小心拷貝行為
    我們在條款13中討論的資源表現在堆上申請的資源,而有些資源並不適合被auto_ptr和tr1::shared_ptr所管理。可能我們需要建立自己的資源管理類。
    例:
     void lock(Mutex *pm);     //鎖定pm所指的互斥量
     unlock(Mutex *pm);        //將pm解除鎖定
     我們建立的資源管理類可能會是這樣:
     class Lock
    {
        public:
        explicit Lock(Mutex *pm)
        : mutexPtr(pm)
        {
             lock(mutexPtr);
        }
        ~Lock()
        {
             unlock(mutexPtr);
        }
        private:
        Mutex *mutexPtr;
    };

    但是,如果Lock物件被複制,會發生什麼事???
    “當一個RAII物件被複制,會發生什麼事?”大多數時候你會選擇一下兩種可能:
  • 禁止複製。如果複製動作對RAII類並不合理,你便應該禁止之。禁止類的copying函式參見條款6。
  • 對底層資源使用”引用計數法“。有時候我們又希望保有資源,直到它的最後一個使用者被銷燬。這種情況下複製RAII物件時,應該將資源的”被引用計數“遞增。tr1::shared_ptr便是如此。
     通常只要內含一個tr1::shared_ptr成員變數,RAII類便可實現”引用計數“行為。
     class Lock
    {
        public:
            explicit Lock(Mutex *pm)
            : mutexPtr(pm, unlock)        //由於tr1::shared_ptr預設行為是”當引用計數為0時刪除其所指物“,幸運的是                                                          //我們可以指定”引用計數“為9時被呼叫的所謂”刪除器“,即第二個引數unlock
        {
            lock(mutexPtr.get());
        }
        private:
            std::tr1::shared_ptr<Mutex> mutexPtr;
     };

     本例中,並沒說明解構函式,因為沒有必要。編譯器為我們生成的解構函式會自動呼叫其non-static成員變數(mutexPtr)的解構函式。而mutexPtr的解構函式會在互斥量”引用計數“為0時自動呼叫tr1::shared_ptr的刪除器(unlock)。
    Copying函有可能被編譯器自動創建出來,因此除非編譯器所生成版本做了你想要做的事,否則你得自己編寫它們。
    請記住:
  • 複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。
  • 普遍而常見的RAII類拷貝行為是:抑制拷貝,施行引用計數法。不過其它行為也可能被實現。   
     條款15:在資源管理類中提供對原始資源的訪問
    前幾個條款提到的資源管理類很棒。它們是你對抗資源洩漏的堡壘。但這個世界並不完美,許多APIs直接指涉資源,這時候我們需要直接訪問原始資源。
     這時候需要一個函式可將RAII物件(如tr1::shared_ptr)轉換為其所內含之原始資源。有兩種做法可以達成目標:顯示轉換隱式轉換

     tr1::shared_ptr和auto_ptr都提供一個get成員函式,用來執行顯示轉換,也就是返回智慧指標內部的原始指標(的復件)。就像所有智慧指標一樣,
tr1::shared_ptr和auto_ptr也過載了指標取值操作符(operator->和operator*),它們允許隱式轉換至底部原始指標。(即在對智慧指標物件實施->和*操作時,實際被轉換為被封裝的資源的指標。)
    class Font
    {
        public:
        ...
        FontHandle get() const         //
FontHandle 是資源;    顯示轉換函式
        {
            return f;
        }
        operator FontHandle() const         //隱式轉換    這個值得注意,可能引起“非故意之型別轉換”
        {
            return f;
        }

        ...
    };

    是否該提供一個顯示轉換函式(例如get成員函式)將RAII類轉換為其底部資源,或是應該提供隱式轉換,答案主要取決於RAII類被設計執行的特定工作,以及它被使用的情況。
    顯示轉換可能是比較受歡迎的路子,但是需要不停的get,get;而隱式轉換又可能引起“非故意之型別轉換”。
    請記住:
  • APIs往往要求訪問原始資源,所以每一個RAII類應該提供一個“取得其所管理之資源”的方法。
  • 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。   
    條款16:成對使用new和delete時要採取相同形式
    先看下一下程式碼:
     std::string *stringArray = new std::string[100];
     ...
     delete stringArray;

     使用了new動態申請了資源,也呼叫了delete釋放了資源。但這程式碼存在“不明確行為”。stringArray物件中的99個不太可能被適當刪除,因為它們的解構函式很可能沒被呼叫。
     當我們使用new,有兩件事情發生:第一,記憶體被分配出來;第二,針對此記憶體會有一個(或更多)建構函式被呼叫。當你使用delete,也有兩件事發生:針對此記憶體會有一個(或多個)解構函式被呼叫,然後記憶體才被釋放。delete的最大問題在於:即將被刪除的記憶體之內究竟有多少物件?這個問題的答案決定了有多少個解構函式必須被呼叫起來。
     解決以上問題事實上很簡單:如果你呼叫new時使用[],你必須在對應呼叫delete時也使用[]。如果你呼叫new時沒有使用[],那麼也不該在對應呼叫delete時使用[]。
     最好儘量不要對陣列形式作typedefs動作。因為這樣容易引起delete操作的“疑惑”(需不需要[]呢???)。
     請記住:
  • 如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果你在new表示式中不使用[],一定不要在相應的delete表示式中使用[]。    
    條款17:以獨立語句將newed物件置入智慧指標
    為了避免資源洩漏的危險,最好在單獨語句內以智慧指標儲存newed所得物件。
    即:
   int priority();
    void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);


    std::tr1::shared_ptr<Widget> pw(new Widget);    //即在傳入函式之前對智慧指標初始化,而不是在傳入引數中                                                                                    //對其初始化,因為那樣可能引起操作序列的問題。
    processWidget(pw, priority());

 請記住:
  • 以獨立語句將newed物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。   
四.設計與宣告
     所謂軟體設計,是“令軟體做出你希望它做的事情”的步驟和做法,通常以頗為一般性的構想開始,最終變成十足的細節,以允許特殊介面的開發。
     條款18:讓介面容易被正確使用,不易被誤用
     理想上,如果客戶企圖使用某個介面而卻沒有獲得他所預期的行為,這個程式碼不該通過編譯;如果程式碼通過了編譯,它的作為就該是客戶所想要的。
     欲開發一個“容易被正確使用,不容易被誤用”的介面,首先必須考慮客戶可能做出什麼樣的錯誤。
     許多客戶端錯誤可以因為匯入新型別而獲得預防。在防範“不值得擁有的程式碼”上,型別系統是你的主要同盟國。
     struct Day
     {
        explicit Day(int d)        //
explicit 避免隱式的轉換。
        :val(d) {}
        int val;
     };
    對日期進行類似的型別封裝,能有效地避免不恰當的日期賦值。
    “除非有好的理由,否則應該儘量令你的型別(定義的類)的行為與內建型別一致”。