章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(2)-讀書筆記
《Effective C++》第4章 設計與宣告(1)-讀書筆記
《Effective C++》第4章 設計與宣告(2)-讀書筆記
《Effective C++》第8章 定製new和delete-讀書筆記
條款22:將成員變數宣告為private
首先,有兩個小點理由支援你將成員變數宣告為private。
(1)介面的一致性。
如果public介面的都是函式,那麼客戶在呼叫時就不用考慮是否需要加小括號,因為每個呼叫的都是函式,必須加小括號。
(2)精細的訪問控制。
使用函式可以讓你對成員變數的處理有更精確的控制,如果你令成員變數為public,每個人都可以讀寫它。
最重要的是private提供封裝性:
如果你通過函式訪問成員變數,後面可以更改某個計算替換這個成員,而class客戶一點也不會知道class的內部已經變化了,只需重新編譯即可。
假設你將一個成員變數宣告為public或protected而客戶開始使用它,就很難改變那個成員變數所涉及的一切。太多程式碼需要重寫、測試、重寫文件、編譯。從封裝的角度,其實只有兩種訪問許可權:private和其他(不提供封裝)。
請記住:
(1)切記將成員變數宣告為private。這可賦予客戶訪問資料的一致性、可細微劃分訪問控制、允諾約束條件獲得保證,並提供class作者以充分的彈性。
(2)protected並不比public更具封裝性。
條款23:寧以non-member、non-friend函式替換member函式
下面有一個class:
class WebBrowser { public: void clearCache(); void clearHistory(); void removeCookies(); };
使用者希望把這三個介面通過提供一個函式去做,可以定義一個成員函式呼叫這三個函式。
void WebBrowser::clearEverything() //成員函式,呼叫clearCache、clearHistory和removeCookies { clearCache(); clearHistory(); removeCookies(); }
當然,這個功能也可以通過一個非成員函式提供:
void clearBrowser(WebBrowser& wb) //非成員函式 { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
現在要考量的是哪一種做法比較好?
物件導向守則要求,資料以及運算元據的函式應該被捆綁在一起,這意味著它建議member是較好的選擇。這個建議是錯誤的,是對物件導向真實意義的一個誤解。
物件導向守則要求資料應該儘可能被封裝。與直觀相反,clearEverything帶來的封裝性比clearBrowser要低。因為non-member non-friend函式不能訪問class內的private。
注意:這裡考量的是member和non-member non-friend二者提供相同的機能時的一個抉擇。friends對private的訪問權利與member函式相同。
條款24:若所有引數皆需型別轉換,請為此採用non-member函式
通常令class支援隱式型別轉換是不好的,但也有例外。假設一個class表示有理數,那麼允許整數“隱式轉換”為有理數是比較合理的。
class Rational { public: Rational(int numerator = 0, int denominator = 1); int numerator() const; //分子訪問函式 int denominator() const; //分母訪問函式 };
你想讓這個class支援乘法運算。可能宣告operator*為成員函式。
class Rational { public: ... const Rational operator*(const Rational& rhs) const; };
這個設計按照下面這種方式使用是沒問題的:
Rational oneEighth(1, 8); Rational oneHalf(1, 2); Rational result = oneHalf * oneEighth; //好的,沒問題 result = result * oneEighth; //好的,沒問題
當你想使用混合運算時:
result = oneHalf * 2; //好的,沒有問題 result = 2 * oneHalf; //錯誤,不滿足交換律
本質上上面的用法與下面的等價:
result = oneHalf.operator*(2); //result = oneHalf * 2; result = 2.operator*(oneHalf); //result = 2 * oneHalf;
所以你就明白為什麼不滿足交換律了。為了支援混合運算,需要將operator*設定為non-member函式。
const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
下面的呼叫的OK的:
Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; //好的,沒問題 result = 2 * oneFourth; //好的,沒問題
這裡還要說明的一點是建構函式一定不能宣告為explicit的,否則整型的數值2就不能隱式轉換為Rational物件了。
請記住:如果你需要為某個函式的所有引數(包括被this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式必須是non-member的。
條款25:考慮寫出一個不丟擲異常的swap函式
swap函式就是將兩物件的值彼此賦予對方。預設情況下,swap可由STL提供的swap演算法完成。典型實現如下:
namespace std { template<typename T> void swap(T& a, T& b) { T temp(a); a = b; b = temp; } }
只要T支援拷貝(通過拷貝建構函式和copy assignment操作符完成),就可以利用該演算法。
但存在某些情況,預設的swap行為往往效率較低。例如,以指標指向一個物件。考慮下面的class:
class WidgetImpl { public: ... private: int a, b, c; vector<double> v; }; class Widget { public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) { ... *pImpl = *(rhs.pImpl); ... } private: WidgetImpl *pImpl; };
如果我們要交換Widget物件,預設的演算法會拷貝3個Widget物件和3個WidgetImpl物件,效率很低。實際上,我們只需要置換pImpl指標的指向即可。
為了解決效率問題,我們需要告訴std::swap,當交換Widget物件時,只需要交換指標就好了。可以將std::swap針對Widget特化。
namespace std { template<> void swap<Widget>(Widget& a, Widget& b) { swap(a.pImpl, b.pImpl); //交換指標值 } }
這只是個思路,一般我們不能改變std內的任何東西,我們可以令Widget宣告一個swap的public成員函式做置換工作,然後將std::swap特化。
class Widget { public: void swap(Widget& other) { using std::swap; // swap(pImpl, other.pImpl); } };
注意:成員swap函式絕不可丟擲異常。因為swap的一個最好應用是幫助class提供強烈的異常安全性保障。
請記住:當std::swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常。
補充說明:條款25還有一些其他知識,我沒有理解的特別好,就沒有說明,但整個問題是由swap效率引發的,下次回顧時再補充吧。