effective C++筆記——設計與宣告
文章目錄
- 讓介面容易被正確使用,不易被誤用
- 設計class猶如設計type
- 寧以pass-by-reference-to-const替換pass-by-value
- 必須返回物件是,別妄想返回其reference
- 將成員變數宣告為private
- 寧以non-member、non-friend替換member函式
- 若所有的引數皆需要型別轉換,請為此採用non-member函式
讓介面容易被正確使用,不易被誤用
. 建立一個容易被正確使用的介面,首先必須考慮客戶可能犯什麼樣的錯誤,比如對函式的引數傳遞順序有誤,或者引數是無效的,以及返回值並不安全等,書中舉了一個關於日期的函式,客戶可能將月份和天數的順序輸入弄錯,也可能輸入無效日期如2月30日。
對介面保持一致性
設計class猶如設計type
. c++如同其他面向物件程式語言一樣,定義了一個新的class就定義了一個新的type,如何設計高效的class通常需要面對以下的問題:
- 新type的物件應該如何被建立和刪除: 這將影響class的建構函式和解構函式以及記憶體的分配和釋放問題。
- 物件的初始化和物件的賦值有什麼區別: 這將影響到建構函式和賦值操作符的行為,以及兩者的差異。
- 新type的物件如果被pass-by-value,意味著什麼?
- 什麼是新type的合法值: 這影響對資料的錯誤檢查,函式的異常丟擲等。
- 新type是否需要配合某個繼承體系: 值得注意的是解構函式是否需要宣告為virtual。
- 新type需要什麼樣的轉換?
- 什麼樣的操作符和函式對此type而言是合理的?
- 什麼樣的標準函式應該被駁回: 那些應當被宣告為private
- 誰該取用新type的成員: 對許可權(private、public、protected)和友元的設定是否合理
- 什麼是type的未宣告介面?
- 新的type有多一般化: 有沒有必要直接做泛型處理。
- 真的需要一個新的type嗎: 如果只是為既有的class新增功能,說不定單純的定義一個或多個non-member函式或template,更能達到目標。
寧以pass-by-reference-to-const替換pass-by-value
. 值傳遞對是建立引數的一個副本,這雖然不會改變傳入的引數本身,但是這將帶來更多更費時的操作,使用常引用(指標)的方式傳值將回避這種情況。
必須返回物件是,別妄想返回其reference
. 知道了值傳遞帶來的種種不便後,可能會堅持使用引用傳遞的方式,這時往往會犯一個致命錯誤:開始傳遞一些references指向其實並不存在的東西。比如一個函式的返回值是一個引用,在函式中建立了本地物件,在函式結束的時候將它返回,這個本地物件實際上是被銷燬了,外部的變數取得的值實際上是不知道指向是什麼地方的。
還有就是不要讓這個引用或者指標指向堆上動態分配的記憶體,因為這些記憶體是需要手動去釋放的,在多次呼叫這個函式時,將帶來很多記憶體管理的麻煩。
將成員變數宣告為private
. 眾所周知類的三大特性之一是封裝,將成員變數宣告為private,通過一系列函式來進行訪問能很好的實現封裝的效果。
寧以non-member、non-friend替換member函式
. 假設有一個類用來表示網頁瀏覽器,其中有三個函式分別用來刪除快取、刪除歷史記錄以及刪除所有cookies:
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
假設想一整個執行所有的清除操作,很容易想到兩種方法:
1.為類再新增一個方法,在這個方法中呼叫以上三個步驟:
void clearAll(); //呼叫clearCache、clearHistory和removeCookies
2.寫一個non-member函式來呼叫以上三個步驟:
void clearAll(WebBrowser& b){
b.clearCache();
b.clearHistory();
b.removeCookies();
}
. 以上兩種方式哪種更好呢?根據面向物件守則要求資料應該儘可能被封裝,然而member函式版本比non-member函式版本的封裝性要低。因為增加一個成員函式就增加了訪問類的成員變數的機會。
若所有的引數皆需要型別轉換,請為此採用non-member函式
. 令classes支援隱式型別轉換通常是個糟糕的主意,但是這個規則也有例外,比如自己建立一個有理數的類,應該支援從整型數轉換到有理數的轉換:
class Rational{
public:
Rational(int numerator = 0,int denominator = 1); //不將建構函式設為explicit的,
//允許隱式轉換
int numerator() const;
int denominator() const;
private:
...
};
這個類應該要支援算術運算,例如加法、乘法等,但是應該將運算的函式寫成成員函式還是非成員函式呢?先假設寫成成員函式的版本:
class Rational{
public:
...
const Rational operator*(const Rational& rhs) const;
};
這種方式可以使用運算了,但是有一種情況卻會導致錯誤:
Rational r1(1,8);
Rational r2(1,2);
Rational result = r1 * r2; //正確
result = result * r1; //正確
result = r1 * 2; //正確
result = 2 * r1 ; //錯誤
可以看出怎麼連乘法連交換律都不能實現了,因為錯誤的那個語句和下面的形式一樣:
result = 2.operator(r1 );
r1是一個內含operator成員函式的物件,所以編譯器呼叫該函式,整數2並沒有相應的class,也就沒有operator成員函式。編譯器也會嘗試尋找非成員函式的接受兩個引數(一個int,一個Rational物件)的operator*函式,但是並不存在,所以查詢也會失敗。
因此讓這個函式成為一個非成員函式是一個可行的方法,編譯器會允許在每一個實參身上執行隱式轉換,以上錯誤的形式就能通過編譯了:
const Rational operator*(const Rational& lhs,const Rational& rhs){
...
}
result = 2 * r1; //正確