Effective C++ 總結 (中)

四.設計與聲明
    

     條款18:讓接口容易被正確使用,不易被誤用

     理想上,如果客戶企圖使用某個接口而卻沒有獲得他所預期的行爲,這個代碼不該通過編譯;如果代碼通過了編譯,它的行爲就應該是客戶所想要的。欲開發一個“容易被正確使用,不容易被誤用”的接口,首先必須考慮客戶可能做出什麼樣的錯誤。
   class Data{
   public:
          Data(int month,int day,int year)
           .... 
   }
  上面這個類如果傳入30,3,1990就會出現問題,因爲我們沒有任何預防機制阻止此類問題的發生
  這個錯誤可以利用導入新類型而獲得預防,我們可以導入簡單的包裝類型(wrapper types)來處理此類問題
struct Day{                                                  struct Month{                                                             struct Year{
   explict Day(int d):val(d){};                             explict Month(int m):val(m){};                                     explict Year(int y):val(y){}; 
   int val                                                            int val                                                                        int val 
  }  } }
  
  從而可以向下面這樣調用:
 class Data{
        public:
               Data(const Month&m, const &Day, const Year& y);
                ..........
     }
    Data d(Month(3),Day(30),Year(1990));
    對日期進行類似的類型封裝,能有效地避免不恰當的日期賦值。
    “除非有好的理由,否則應該儘量令你的類型(定義的類)的行爲與內置類型一致”。

    在資源管理方面,也許我們應該“先發制人”,即讓函數返回一個資源的指針改爲返回一個智能指針。例如:
     std::tr1::shared_ptr<Investment> createInvestment();

    這便實質上強迫客戶將返回值存儲於一個tr1::shared_ptr內,幾乎消除了忘記刪除底部Investment對象的可能性。 tr1::shared_ptr提供的某個構造函數接受兩個實參:一個是被管理的指針,另一個是引用次數變成0時被調用的“刪除器”。 tr1::shared_ptr允許我們自己制定第二個參數,這是安全的。但是留給客戶,那也許存在危險。

     std::tr1::shared_ptr<Investment>         //tr1::shared_ptr構造函數堅持第一個參數必須是個指針。
                pInv(static_cast<Investment*>(0), getRidOfInvestment); 
    tr1::shared_ptr有一個特別好的性質是:它會自動使用它的“每個指針專屬的刪除器”,因而消除另一個潛在的客戶錯誤:所謂的“cross-DLL problem”(對象在一個DLL被new創建,卻在另一個DLL中delete銷燬)。因爲它缺省的刪除器是來自“tr1::shared_ptr誕生所在的那個DLL”的delete。


     請記住:

  • 好的接口很容易被正確使用,不容易被誤用。你應該在你的所有接口中努力達成這些性質。
  • “促進正確使用”的辦法包括接口的一致性,以及與內置類型的行爲兼容。
  • “阻止誤用”的辦法包括建立新類型、限制類型上的操作,束縛對象值,以及消除客戶的資源管理責任。
  • tr1::shared_ptr支持定製刪除器。這可防範DLL問題,可被用來自動解除互斥量等等。   

    條款19:設計class猶如設計type

 

   C++就像在其它面向對象編程語言一樣,當你定義一個新class,也就定義了一個新type。這意味着你並不只是類的設計者,更是類型的設計者。重載函數和操作符、控制內存的分配和歸還、定義對象的初始化和終結......全部在你手上。
    設計優秀的類是一項艱鉅的工作,因爲涉及好的類型是一項艱鉅的工作。好的類型有自然的語法,直觀的語義,以及一或多個高效實現品。
    設計一個良好的類,或者稱作類型,考慮一下設計規範:

  • 新類型的對象應該如何被創建和銷燬?
  • 對象的初始化和對象的賦值該有什麼樣的差別?
  • 新類型的對象如果被passed by value(值傳遞),意味着什麼?
  • 什麼是新類型的“合法值”?
  • 你的新類型需要配合某個繼承圖系嗎?
  • 你的新類型需要什麼樣的轉換?
  • 什麼樣的操作符和函數對此新類型而言是合理的?
  • 什麼樣的標準函數應該駁回?
  • 誰該取用新類型的成員?
  • 什麼是新類型的“未聲明接口”?
  • 你的新類型有多少一般化?
  • 你真的需要一個新類型嗎?   

    請記住:

  • Class的設計就是type的設計。在定義一個新的type之前,請確定你已經考慮過本條款覆蓋的所有討論主題。 
  • 所有討論主題。   

    條款20:寧以pass-by-reference-to-const替代psss-by-value


    缺省情況下C++以by value方式傳遞對象至函數。除非你另外指定,否則函數參數都是以實際實參的副本爲初值,而調用端所獲得的亦是返回值的一個副本。這些副本由對象的拷貝構造函數產生。
    所以在以對象爲by value時,可能會調用相應的構造函數(成員對象的構造、基類對象的構造),然後調用對應的析構函數。所以以by value的形式開銷還是比較大的。
     如果我們用pass-by-reference-to-const,例如:
    
 bool validateStudent(const Student& s);     //const,希望別對傳入對象進行不恰當的修改;
     這種傳遞方式效率高得多:沒有任何構造函數或析構函數被調用,因爲沒有任何新對象被創建。

    以傳引用方式傳遞參數也可以避免對象切割問題:即當一個派生類對象以傳值的方式傳遞並被視爲一個基類對象,基類對象的拷貝構造函數會被調用,而“造成此對象的行爲像個派生類對象”的那些特化性質全被切割掉了,僅僅留下了基類對象。這一般不是你想要的。
    所以我們一般的做法應該是這樣:內置對象和STL的迭代器和函數對象,我們一般以傳值的方式傳遞,而其它的任何東西都以傳引用的方式傳遞。


    請記住:

  • 儘量以pass-by-reference-to-const替代pass-by-value。前者通常比較高效,並可避免切割問題。
  • 以上規則並不使用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較適當。  

    條款21:必須返回對象時,別妄想返回其referenc


  當一個函數“必須返回新對象”時,就讓它返回新對象。絕不要返回pointer 或reference指向一個local stack 對象,或返回reference 指向一個 heap-allocated對象,或返回pointer 或reference指向一個local static 對象。舉例說明如下:

(1)如果返回pointer 或reference指向一個local stack 對象(函數的stack對象指函數的參數、局部變量、返回值;存放在函數stack空間):

const Rational& operator* (const Rational& lhs,const Rational& rhs)
{
 Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //警告!糟糕的代碼!
 return result;
}

 解釋:local對象在函數退出前被銷燬了

(2)返回reference 指向一個heap-allocated對象(Heap-based對象指有new 和 malloc之類的函數分配的對象,存放在heap空間)

const Rational& operator* (const Rational& lhs,const Rational& rhs)
{
 Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
 return *result;
}

 解釋:容易導致內存泄露,因爲沒有誰對new出來的對象實施delete操作。

(3)返回pointer 或reference指向一個local static:

const Rational& operator* (const Rational& lhs, const Rational& rhs) 
{
 static Rational result;
 result = ... ;
 return result;
}

 在下面的調用中,將出現錯誤:

if((a * b) == (c * d)){
 //當乘積相等時,做適當的相應動作;
} 
else {
 //當乘積不等時,做適當的相應動作;
}

 解釋:(a * b) == (c * d)永遠爲true,因爲兩次operator*調用的確調用了各自的static Rational對象值,但他們返回的都是Reference,因此調用 端看到的永遠是static Rational的“現值”。

 所以,我們最終的選擇是通過pass-by-value來返回新的對象。

const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.n*rhs.n, lhs.d*rhs.d);
}



    條款22:將成員變量聲明爲private


   首先是代碼的一致性(調用public成員時不用考慮是成員還是函數)。

   其次封裝性,都寫成函數進行訪問可以提供以後修改訪問方法的可能性,而不影響使用方法。另外,public影響的是所有使用者,而protected影響的是所有繼承者,都影響巨大,所以都不建議聲明成員變量。

   切記將成員變量聲明爲private。這可賦予客戶訪問數據的一致性、可細微劃分訪問控制、允諾條件獲得保證,並提供class作者以充分的實現彈性。

protected並不比public更具封裝性。


    條款23:寧以non-member、non-friend替換member函數


   想像有個class用來表示網頁瀏覽器。這樣的class可能提供的衆多函數中,有一個用來清除下載元素的高速緩衝區(cache of downloaded elements)、清除訪問過的URLs的歷史記錄、以及移除系統中所有的cookies。

class WebBrowse
{
public:
    void clearCache();
    void clearHistroy();
    void removeCookies();
};

  許多用戶想一個函數來執行整個動作,因些WebBrowse也提供這樣一個函數:

class WebBrowse
{
public:
    //...
    void clearEverything();//依次調用clearCache(),clearHistory(),removeCookies()
};

  當然這個功能也可以由一個non-member函數來完成:

void clearEverything(WebBrowse& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

  那麼哪一個比較好呢?

  根據面向對象守則要求,數據以及操作的那些函數應該捆綁在一塊,這意味着member函數是較好的選擇。不幸的是這個建議不正確。面向對象要求數據應該儘可能被封裝。

  member函數帶來的封閉性比non-member函數低,因爲non-member函數並不增加“能夠訪問class內之private成份”的函數數量。

   此外,提供non-member函數可以允許對WebBrowse相關機能能有較大的包裹彈性,而那最終導致較低的編譯相依度,增加WebBrowse的可延伸性。

   如果我們把WebBrowse相關的功能設計爲non-member函數,然後和WebBrowse放在同一個namespace中(namespace可以跨越多個源碼文件,而class不能。namespace可以是不連續的,他有多個文件中定義部分的綜合構成,是可積累的)

namespace WebBrowsesStuff{
	class WebBrowse{..}
	void clearEverything(WebBrowse& wb)
}

  而這種切割方式並不適用於class成員函數,因爲一個class必須整體定義,不能被切割爲片斷。

請記住

  寧可拿non-member non-friend函數替換member函數。這樣做可以增加封裝性包裹彈性機能擴充性



    條款24:若所有參數皆需類型轉換,請爲此採用non-member函數


  當我們爲一個有理數類的設計一個乘法操作符的重載函數,如果我們把它作爲類的成員:

class Rational
{
public:
    //...
    const Rational operator*(const Rational &lhs);
};

  當我們嘗試混合算術的時候,你會發現只有一半行得通:

Rational result, oneHalf;
result = oneHalf * 2;
result = 2 * oneHalf;

  第一個正確,應爲它等價於result=oneHalf.operator(2),2發生了隱式類型轉換,由於Rational的單參數構造函數是non-explicit的,所以以2爲參數調用構造函數生成一個臨時對象,然後用這個臨時對象和oneHalf做“*”,相當於:

  const Rational temp(2);

  result = oneHalf * temp;

  第二個錯誤,因爲它等價於result=2.operator*(oneHalf)。

所以,如果我們想執行上面的操作,我們需要將這個重載函數設計爲non-member函數。

const Rational operator*(const Rational& lhs, const Rational& rhs);

  如果你需要爲某個函數的所有參數(包括被this指針所指向的那個隱喻參數)進行類型轉換,那麼這個函數必須是個non-member

class Rational
{
public:
	Rational(int i):val(i){};
	int getValue() const{return val;};
private:
	int val;
};

const Rational operator*(const Rational& lhs,const Rational& rhs)
{
	
	return Rational(lhs.getValue() * rhs.getValue());
}

int main()
{
	Rational lhs(2);
	Rational rhs(3);
	Rational hs=lhs*rhs;
	cout<<hs.getValue()<<endl;
	return 0;
}

    條款25:考慮寫出一個不拋異常的swap函數

1,當std::swap對你的類型效率不高時,提供一個swap成員函數,並確定這個函數不拋出異常。

2,如果你提供一個member swap,也該提供一個non-member swap來調用前者,對於classes(而非templates),也請特化std::swap。

3,調用swap時應針對std::swap使用using聲明式,然後調用swap並且不帶任何“命名空間資格修飾”。

4,爲“用戶定義類型”進行std template全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西。



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