Effective C++ 總結 (上)

一.讓自己習慣C++

   條款01:視C++爲一個語言聯邦

    爲了更好的理解C++,我們將C++分解爲四個主要次語言:
  • C。說到底C++仍是以C爲基礎。區塊,語句,預處理器,內置數據類型,數組,指針統統來自C。
  • Object-Oreinted C++。這一部分是面向對象設計之古典守則在C++上的最直接實施。類,封裝,繼承,多態,virtual函數等等...
  • Template C++。這是C++泛型編程部分。
  • STL。STL是個template程序庫。容器(containers),迭代器(iterators),算法(algorithms)以及函數對象(function objects)...
   請記住:
  • 這四個次語言,當你從某個次語言切換到另一個,導致高效編程守則要求你改變策略。C++高效編程守則視狀況而變化,取決於你使用C++的哪一部分。

    條款02:儘量以const,enum,inline替換#define
    這個條款或許可以改爲“寧可 以編譯器替換預處理器”。即儘量少用預處理。

     編譯過程:.c文件--預處理-->.i文件--編譯-->.o文件--鏈接-->bin文件

     預處理過程掃描源代碼,對其進行初步的轉換,產生新的源代碼提供給編譯器。檢查包含預處理指令的語句和宏定義,並對源代碼進行相應的轉換。預處理過程還會刪除程序中的註釋和多餘的空白字符。可見預處理過程先於編譯器對源代碼進行處理。預處理指令是指在編譯之前進行處理的命令,以#號開頭,包含3個方面的內容:宏定義、文件包含、條件編譯(#ifdef #endif)。

     例:#define ASPECT_RATIO 1.653

     記號名稱ASPECT_RATIO也許從未被編譯器看見,也許在編譯器開始處理源代碼之前它就被預處理器移走了。即編譯源代碼時ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能並未進入記號表(symbol table)。

     替換:const double AspectRatio = 1.653;

     好處應該有:多了類型檢查,因爲#define 只是單純的替換,而這種替換在目標碼中可能出現多份1.653;改用常量絕不會出現相同情況。

    常量替換#define兩點注意:
  • 定義常量指針:
       const char *authorName = “Shenzi”;
       cosnt std::string authorName("Shenzi");

  • 類專屬常量:

     static const int NumTurns = 5;//static 靜態常量 所有的對象只有一份拷貝。
    萬一你編譯器不允許“static int class常量”完成“in calss初值設定”(即在類的聲明中設定靜態整形的初值),我們可以通過枚舉類型予以補償:
      enum { NumTurns = 5 };
    *取一個const的地址是合法的,但取一個enum的地址就不合法,而取一個#define的地址通常也不合法。如果你不想讓別人獲取一個pointer或reference指向你的某個整數常量,enum可以幫助你實現這個約束。[enum用法鏈接]
     例:#define CALL_WITH_MAX(a,b)    f((a) > (b)) ? (a) : (b))
     宏看起來像函數,但不會招致函數調用帶來的額外開銷,而是一種簡單的替換。
     替換:
      template<typename T>
      inline void callWithMax(cosnt T &a, cosnt T &b)
      {
            f(a > b ? a : b);
       }
      
callWithMax是個真正的函數,它遵循作用於和訪問規則。
      請記住:

  • 對於單純常量,最好以const對象或enums替換#defines;
  • 對於形似函數的宏,最好改用inline函數替換#defines。   


    條款03:儘可能使用const


    const允許你告訴編譯器和其他程序員某值應保持不變,只要“某值”確實是不該被改變的,那就該確實說出來。
    關鍵字const多才多藝:
    例:
        char greeting[] = "Hello";
        char *p = greeting;    //指針p及所指的字符串都可改變;
        const char *p = greeting;    //指針p本身可以改變,如p = &Anyother;p所指的字符串不可改變;
        char * cosnt p = greeting;    //指針p不可改變,所指對象可改變;
        const char * const p = greeting;    //指針p及所致對象都不可改變;
     說明:
  • 如果關鍵字const出現在星號左邊,表示被指物事常量。const char *p和char const *p兩種寫法意義一樣,都說明所致對象爲常量;
  • 如果關鍵字const出現在星號右邊,表示指針自身是常量。   

    STL例子:

        聲明迭代器爲const就像聲明指針爲const一樣(即聲明一個T*const指針),表示這個迭代器不得指向不同的東西,但它所指的東西的值是可以改     動的。如果希望迭代器所指的東西是不可被改動的,需要的是const_iterator。


        const std::vector<int>::interator iter = vec.begin();//iter是const常量, ++iter 錯誤
        std::vector<int>::const_iterator cIter = vec.begin();//iter所指的元素是const常量,*cIter = 10 錯誤
    以下幾點注意:

  • 令函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而不至於放棄安全性和高效性。
    例:const Rational operator*(const Rational& lhs, const Rational& rhs)
   {
            return Rational(lhs.numerator() * rhs.numerator(),
            lhs.denominator() * rhs.denominator());
   }
    返回值用const修飾可以防止允許這樣的操作發生:
   Rational a,b;
   Radional c;
   (a*b) = c;
   一般用const修飾返回值爲對象本身(非引用和指針)的情況多用於二目操作符重載函數併產生新對象的時候。
    類中的成員函數:A fun4()  const; 其意義上是不能修改所在類的的任何變量。 
  • const成員函數使class接口比較容易被理解,它們使“操作const對象”稱爲可能;
    說明:聲明爲const的成員函數,不可改變non-static成員變量,在成員變量聲明之前添加mutable可讓其在const成員函數中可被改變。
    class Block
   {
       public:
         ....
       private:
       char* ch;
     mutable int val  //這個成員變量可能總是會被改變,即使在const成員函數內
    }
    請記住:
  • 將某些東西聲明爲const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體;
  • 編譯器強制實施bitwise constness,但你編寫程序時應該使用“概念上的常量性”(conceptual constness);
  • 當cosnt和non-const成員函數有着實質等價的實現時,令non-const版本調用const版本可避免代碼重複。

    條款04:確定對象被使用前已先被初始化

    永遠在使用對象之前先將它初始化。對於無任何成員的內置類型,你必須手工完成此事。至於內置類型以外的任何其它東西,初始化責任落在構造函數身上,確保每一個構造函數都將對象的每一個成員初始化。
    賦值和初始化:
    C++規定,對象的成員變量的初始化動作發生在進入構造函數本體之前。所以應將成員變量的初始化置於構造函數的初始化列表中。
  在構造函數體內進行初始化實際上包含了2個階段:
   (1)調用默認構造函數進行初始化 
   (2)調用賦值函數覆蓋默認構造函數的值
     ABEntry::ABEntry(const std::string& name, const std::string& address,
                                     const std::list<PhoneNumber>& phones)
     { 
            theName = name;                    //這些都是賦值,而非初始化

            theAddress = address;          //這些成員變量在進入函數體之前已調用默認構造函數,接着又調用賦值函數
            thePhones = phones;            //即要經過兩次的函數調用。            
            numTimesConsulted = 0;

    } 


    ABEntry::ABEntry(const std::string& name, const std::string& address,
                                    const std::list<PhoneNumber>& phones) 
        : theName(name),                       //這些纔是初始化
 
        theAddress(address),                //這些成員變量只以相應的值爲參數調用拷貝構造函數,所以通常效率更高

        thePhones(phones),
        numTimesConsulted(0)

        {    } 

    所以,對於非內置類型變量的初始化應在初始化列表中完成,以提高效率。而對於內置類型對象,如numTimesConsulted(int),其初始化和賦值的成本相同,但爲了一致性最好也通過成員初始化表來初始化。如果成員變量時const或reference,它們就一定需要初值,不能被賦值。
 
     注意:對於const或引用類型的成員變量(聲明引用和const常量時,必須對其進行初始化),以及沒有默認構造函數的類的任何成員 必須使用初始化式。

      C++有着十分固定的“成員初始化次序”。基類總是在派生類之前被初始化,而類的成員變量總是以其說明次序被初始化。所以:當在成員初始化列表中列各成員時,最好總是以其聲明次序爲次序。

    C++類的成員初始化是有着明顯的次序的,一般是基類的成員先初始化,然後派生類的成員按定義的順序初始化。所以類的構造函數初始化列表上的初始化順序跟類真實的成員初始化順序是沒有關係的。

   static對象,其壽命從被構造出來直到程序結束爲止。

   函數內的static對象稱爲local static對象(因爲它們對函數而言是local),其他static對象稱爲non-local static對象。程序結束時static對象會被自動銷燬,也就是它們的析構函數會在main()結束時被自動調用。

    當我們的某個編譯單元內的某個non-local static對象的初始化動作使用了另一編譯單元的某個non-local static對象,它所用到的這個對象可能尚未被初始化。因爲C++對“定義於不同編譯單元內的non-local static對象”的初始化次序並無明確定義。

    比如在a.cpp裏我們定義一個類,一個該類的對象
   
class FileSystem 
{ 
public: 
    size_t numDisks()const; 
}; 
extern FileSystem tfs;

現在同一個項目下的b.cpp文件中有一個類,類構造函數用到了tfs對象。

class Directory   
{   
public:   
    Directory(params);   
};   
Directory::Directory(params)   
{   
    size_t disks = tfs.numDisks();   
}  

現在如果我們創建了一個Directory對象
Directory tempDir(params); 

上面的代碼就可能會出問題,除非能保證tfs在tempDir之前先初始化,否則tempDir的構造函數會用到尚未初始化的tfs。

解決方案:
      C++保證,函數內的local static對象會在該函數被調用期間,首次遇到該對象的定義的時候被初始化。所以如果你以“函數調用”(返回一個reference指向local static對象)替換“直接訪問non-local static對象”,你就獲得了保證,保證你所獲得的那個reference將指向一個已經初始化的對象。


所以如我們把tfs和tempDir設計爲一個函數,函數返回該類的一個static對象引用就可以解決問題了。

所以我們可以改寫上面的代碼:

class FileSystem{...};

FileSystem& tfs()                //用這個函數替換tfs對象,它在
{                                //FileSystem class中可能是個static.
    static FileSystem fs;        //定義並初始化一個local static對象
    return fs;                   //返回一個reference指向上述對象
}

class Directory{...};
Directory::Directory(params)     //同前,但原本的reference to tfs
{                                //現在改爲tfs()
   ...
   int disks = tfs().numDisks();
   ...
}
Directory& tempDir()             //用這個函數替換tempDir對象,它在
{                                //tempDir class中可能是個static.
    static Directory td;         //定義並初始化一個local static對象
    return td;                   //返回一個reference指向上述對象
}

    請記住:
  • 爲內置對象進行手工初始化,因爲C++不保證初始化它們;
  • 構造函數最好使用成員初始化列表,而不要在構造函數本體內使用賦值操作。初始化列表列出的成員變量,其排列次序應該和它們在類中的聲明次序相同; 
  • 爲免除“跨編譯單元之初始化次序”問題,請以local static對象替換non-local static對象。




二.構造/析構/賦值運


    條款05:瞭解C++默默編寫並調用哪些函數


    如果你自己沒有聲明,編譯器就會爲類聲明(編譯器版本的)一個拷貝構造函數一個拷貝賦值操作符一個析構函數。此外如果你沒有聲明任何構造函數,編譯器也會成爲你聲明一個默認構造函數。所有這些函數都是publicinline
     惟有當這些函數被需要(被調用),它們纔會被編譯器創建出來。即有需求,編譯器纔會創建它們
     默認構造函數和析構函數主要是給編譯器一個地方用來放置“藏身幕後”的代碼,像是調用基類和非靜態成員變量的構造函數和析構函數。
     注意:編譯器產生的析構函數是個non-virtual,除非這個類的基類自身聲明有virtual析構函數。
     至於拷貝構造函數和拷貝賦值操作符,編譯器創建的版本只是單純地將來源對象的每一個非靜態成員變量拷貝到目標對象。
     如一個類聲明瞭一個構造函數(無論有沒有參數),編譯器就不再爲它創建默認構造函數。 
     編譯器生成的拷貝賦值操作符:對於成員變量中有指針,引用,常量類型,我們都應考慮建立自己“合適”的拷貝賦值操作符。因爲指向同塊內存的指針是個潛在危險,引用不可改變,常量不可改變。

    引用不可改變:

int main()
{
	string s1="aa";
	string s2="bb";
	string& s=s1;
	s=s2;

	cout<<s<<endl;
	cout<<s1<<endl;
	cout<<s2<<endl;
	return 0;
}

輸出:

bb
bb
bb

由此可見,當給reference s重新賦值時,s1的值也被改變了,所以C++規定reference一旦初始化之後,就不能改變(不能再指向別的對象)。


     請記住:

  • 編譯器可以暗自爲類創建默認構造函數、拷貝構造函數、拷貝賦值操作符,以及析構函數

    條款06:若不想使用編譯器自動生成的函數,就該明確拒絕


     通常如果你不希望類支持某一功能,只要不聲明對應函數就是了。但這個策略對拷貝構造函數和拷貝賦值操作符卻不起作用。因爲編譯器會“自作多情”的聲明它們,並在需要的時候調用它們。
     由於編譯器產生的函數都是public類型,因此可以將拷貝構造函數或拷貝賦值操作符聲明爲private。通過這個小“伎倆”可以阻止人們在外部調用它,但是類中的成員函數和友元函數還是可以調用private函數。解決方法是設計一個專門爲了阻止拷貝動作而設計的基類。(Boost提供的那個類名爲noncopyable)。

       即如果某些對象是獨一無二的(比如房子),你應該禁用copy 構造函數或copy assignment 操作符,可選的方案有兩種:

    (1)將copy構造函數和copy assignment操作符聲明爲private,並不予實現;

    (2)在(1)中,當member函數或friend函數調用copy構造函數(或copy assignment操作符),會出現鏈接錯誤。我們可以將連接期間的錯誤移到編譯期間(這是好事,越早偵測出錯誤越好)。我們可以定義一個Uncopyable公共基類,並將copy構造函數和copy assignment操作符聲明爲private,並不予實現,然後讓所有獨一無二的對象繼承它。

class Uncopyable {
protected:                          //允許derived對象構造和析構
 Uncopyable() {}
 -Uncopyable(){}
private:
 Uncopyable(const Uncopyable&};   //但阻止copying
 Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable{
 …
};

爲了阻止Uncopyable對象被拷貝,我們唯一要做的就是繼承Uncopyable:

class HomeForSale:private Uncopyable{
 //不再聲明copy構造函數和copy assignment操作符
}


這樣是行得通的,因爲我們知道,當外部對象(甚至是member函數或者friend函數)嘗試拷貝HomeForSale對象時,編譯器都會嘗試着生成一個默認的copy構造函數和copy assignment操作符,而這些默認生成的函數會調用其基類base class中對應的函數,因爲基類已經將其聲明爲private,所以這些調用會被編譯器拒絕。


     請記住:
  • 爲駁回編譯器自動(暗自)提供的機能,可將相應的成員函數聲明爲private並且不予實現。使用像noncopyable這樣的基類也是一種做法。    


    條款07:爲多態基類聲明virtual析構函數

     當基類的指針指向派生類的對象的時候,當我們使用完,對其調用delete的時候,其結果將是未有定義——基類成分通常會被銷燬,而派生類的成分還留在堆裏,於是造成一個詭異的“局部銷燬”對象,這樣就會造成資源泄漏。
    解決方法給基類一個virtual析構函數,此後刪除派生類對象就會如你想要的那般,它會銷燬整個對象,包括所有的derived class成分。因爲析構函數的運作方式是,最深層派生(most derived)的那個class其析構函數最先被調用,接着是其每一個base class的析構函數被調用
    注意:如果在高層的基類含有一個純虛析構函數,那麼必須給他提供一份定義,否則當derived對象調用最高層基類的虛構函數時,就會出現未定義的行爲。
class AWOV{  
public:  
    virtual ~AWOV() = 0;    //聲明純虛析構函數  
};  
AWOV::~AWOV(){} // pure virtual析構函數的定義  
     任何類只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數。
     如果一個類不含virtual函數,通常表示它並不意圖被用做一個基類,當類不企圖被當做基類的時候,令其析構函數爲virtual往往是個餿主意。因爲,當用戶將一個函數聲明爲virtual時,C++編譯器會創建虛函數表(vtbl, virtual table)以完成動態綁定功能,這將帶來時間和空間上的花銷。
     STL容器都不帶virtual析構函數,所以最好別派生它們。
     請記住:
  • 帶有多態性質的基類應該聲明一個virtual析構函數。如果一個類帶有任何virtual函數,它就應該擁有一個virtual析構函數。
  • 一個類的設計目的不是作爲基類使用,或不是爲了具備多態性,就不該聲明virtual析構函數。

    條款08:別讓異常逃離析構函數

    C++並不禁止析構函數吐出異常,但它不鼓勵你這樣做。C++不喜歡析構函數吐出異常。

 (1)析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然後吞下它們(不傳播)或結束程序。

 (2)如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼class 應該提供一個普通函數(而非在析構函數中)執行該操作。即如果某個操作可能在失敗時拋出異常,而又存在某種需要必須處理該異常,那麼這個異常必須來自析構函數以外的某個函數。如下面程序所示:

class DBConn{
public:
 ...
 void close()
 {
  db.close();
  closed = true;
 }
 ~DBConn()
 {
  if (!closed)      
  {
   try{        //如果客戶沒有關閉連接
    db.close();
   }
   catch(...){
    //結束程序或吞下異常
    std::abort()
	或者製作運轉記錄,記下對close的調用失敗;
	
   }
  }
 }
private:
 DBConnection db;
 bool closed;
};


    條款09:決不讓構造和析構過程中調用virtual函數


    你不該在構造函數和析構函數中調用virtual函數,因爲這樣的調用不會帶來你預想的結果。
    因爲:基類的構造函數的執行要早於派生類的構造函數,當基類的構造函數執行時,由於其包含了一個virtual函數,而該virtual函數除非被定義,否則連接器將找不到該函數的實現代碼,從而出現未定義行爲。而且,base class構造期間,virtual函數絕不會下降到derived層,他被當做base class的函數直接調用。這樣做的原因在於,當base class構造函數執行時,derived class的成員變量尚未初始化,如果此期間調用的virtual函數下降到derived class(derived class成員尚未初始化),就會出現使用未初始化成員變量的危險行爲。

    唯一好的做法是:確定你的構造函數和析構函數都沒有調用虛函數,而它們調用的所有函數也不應該調用虛函數。
    解決的方法可能是:既然你無法使用虛函數從基類向下調用,那麼我們可以使派生類將必要的構造信息向上傳遞至基類構造函數。即在派生類的構造函數的成員初始化列表中顯示調用相應基類構造函數,並傳入所需傳遞信息。
    請記住:

  • 在構造和析構函數期間不要調用虛函數,因爲這類調用從不下降至派生類。

    條款10:令operator= 返回一個reference to *this


    對於賦值操作符,我們常常要達到這種類似效果,即連續賦值:
      int x, y, z;
      x = y = z = 15;
      爲了實現“連鎖賦值”,賦值操作符必須返回一個“引用”指向操作符的左側實參。
      即:
        Widget & operator = (const Widget &rhs)
        {
            ...
            return *this;
        }
      所有內置類型和標準程序庫提供的類型如string,vector,complex或即將提供的類型共同遵守。

      如果不返回引用,那麼就會調用拷貝構造函數,從而增加了系統開銷。

    

#include <iostream>
using namespace std;
class Widget
{
public:
	Widget(int ii):i(ii){}

	Widget(const Widget &rhs)
	{
		i=rhs.i;
		cout<<"copy"<<endl;
	}

	Widget& operator=(const Widget &rhs)
	{
		this->setValue(rhs.getValue());
		cout<<"assign"<<endl;
		return *this;
	}

	Widget& operator+=(const Widget& rhs) 
	{
		this->setValue(rhs.getValue() + this->getValue());
		return *this;
	}

	int getValue() const {return i;}
	void setValue(int ii){i=ii;}

private:
	int i;
};
int main()
{
	Widget w1(1),w2(2),w3(3);
	w2=w1;
	return 0;
}
如果是Widget& operator=(const Widget &rhs)

則輸出:

assign
如果是Widget operator=(const Widget &rhs)
則輸出:

assign
copy

      請記住:

  • 令賦值操作符返回一個reference to *this。   

    條款12:複製對象時勿忘其每一個成員


    還記得條款5中提到編譯器在必要時會爲我們提供拷貝構造函數和拷貝賦值函數,它們也許工作的不錯,但有時候我們需要自己編寫自己的拷貝構造函數和拷貝賦值函數。如果這樣,我們應確保對“每一個”成員進行拷貝(複製)。
    如果你在類中添加一個成員變量,你必須同時修改相應的copying函數(所有的構造函數,拷貝構造函數以及拷貝賦值操作符)
    在派生類的構造函數,拷貝構造函數和拷貝賦值操作符中應當顯示調用基類相對應的函數,否則編譯器可能又“自作聰明瞭”。
     當你編寫一個copying函數,請確保:    

    (1)複製所有local成員變量;
    (2)調用所有基類內的適當copying函數。   
    但是,我們不該令拷貝賦值操作符調用拷貝構造函數,也不該令拷貝構造函數調用拷貝賦值操作符。想想,一個是拷貝(建立對象),一個是賦值(對象已經存在)。
Class Base{..}
class Derived:public Base{
	Derived(const Derived& rhs);
	Derived& operator=(const Derived& rhs);
}
 
Derived::Derived(const Derived& rhs)
       :Base(rhs)	//調用base class的copy構造函數	
{
        ...
}

Derived& Derived::Derived(const Derived& rhs)		
{
        ...
	Base::operator=(rhs); //對base class成分進行賦值操作
	...
}


    請記住:
  • Copying函數應該確保複製“對象內的所有成員變量”及“所有基類成員”;
  • 不要嘗試以某個copying函數實現另一個copying函數。應該將共同機能放進第三個函數中,並由兩個copying函數共同調用。 


三.資源管理
   

    條款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對象被複制,會發生什麼事???

    Lock m1(&m)  //鎖定m

    Lock m2(m1)  //將m1複製到m2上,這回發生什麼??
    “當一個RAII對象被複制,會發生什麼事?”大多數時候你會選擇一下兩種可能:

  • 禁止複製。如果複製動作對RAII類並不合理,你便應該禁止之,將copying操作聲明爲private。
  • 對底層資源使用”引用計數法“。有時候我們又希望保有資源,直到它的最後一個使用者被銷燬。這種情況下複製RAII對象時,應該將資源的”被引用計數“遞增。tr1::shared_ptr便是如此。
     通常只要內含一個tr1::shared_ptr成員變量,RAII類便可實現”引用計數“行爲。

   由於tr1::shared_ptr缺省行爲是”當引用計數爲0時刪除其所指物“,幸運的是shared_ptr允許我們指定所謂的“刪除器”(deleter),那是一個函數對象,當引用次爲0時被調用。

     class Lock 
    {
        public: 
            explicit Lock(Mutex *pm) 
            : mutexPtr(pm, unlock)        //以某個Mutex初始化shared_ptr,並以unlock函數爲刪除器
        { 
            lock(mutexPtr.get()); 
        } 
        private:
            std::tr1::shared_ptr<Mutex> mutexPtr; 
     }; 

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

   

  • 複製底層資源:深度拷貝(複製指針和所指內存)
  • 轉移底部資源的擁有權:將資源的擁有權從被複制物轉移到目標物,例如auto_ptr

    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;而隱式轉換又可能引起“非故意之類型轉換”。
    請記住:
  • APIs往往要求訪問原始資源,所以每一個RAII類應該提供一個“取得其所管理之資源”的方法。
  • 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。   

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

    爲了避免資源泄漏的危險,最好在單獨語句內以智能指針存儲newed所得對象。

   processWidget(std::trl::shared ptr<W工dget> (new Widget) , priority());
    在調用processWidget之前,編譯器必須創建代碼,做以下三件事:
   (1)   調用priority
   (2)   執行”new Widget”
   (3)  調用trl: : shared_ptr 構造函數
不同的C++ 編譯器執行這三條語句的順序不一樣,但(2)一定在(3)之前被調用,但對priority的調用可以排在第一或第二或第三執行。如果編譯器選擇以第二順位執行且priority函數拋出了異常,則新創建的對象Widget將導致內存泄漏,解決方法如下:
std::trl::shared_ptr<Widget> pw(new Widget); //在獨立語句內以智能指針存儲Widget對象
processWidget(pw, priority()); //這個調用肯定不存在內存泄漏

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


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