Effective C++讀書筆記(七)設計與宣告部分(下)
Item22 將成員變數宣告為private
如果public介面內的每樣東西都是函式,客戶就不需要在打算訪問class成員時迷惑地試著記住是否該使用小括號。
細微的劃分訪問控制破有必要,因為許多成員變數應該被隱藏起來。
如果你通過函式訪問成員變數,日後可改以某個計算替換這個成員變數,而class客戶一點也不會知道class的內部實現已經起了變化。
封裝的重要性比你最初見到它時更重要。如果一個public成員變數被替換,或刪除,可能會導致很多程式碼被破壞。
Item23 寧以non-member, non-friend替換member函式
面向物件守則要求儘可能的對資料進行封裝。與直觀相反的,non-member, non-friend函式更利於物件的封裝。
這個論述只適用於non-member non-friend函式。只因在意封裝性而讓函式“成為class的non-member”並不意味它“不可以是另一個class的member”。
封裝可以使我們改變事務而隻影響有限客戶。
我們計算能夠訪問該資料的函式數量,作為一種粗糙的度量。越多函式可訪問它,資料的封裝性就越低。
namespace和class不同。前者可以跨越多個原始碼檔案而後者不能。
將所有便利函式放在多個頭檔案內但隸屬同一個名稱空間,意味客戶可以輕鬆擴充套件這一組便利函式。
例子見Item23
Item24 若所有引數皆需型別轉換,請為此採用non-member函式
下面例子中,若使用成員函式,則在使用如下兩句(乘法交換律)時,卻會發生不同的結果。
//Rational r5 = r2 * 2; //正確,r2有operator*成員函式,2(作為引數)可以通過建構函式進行隱式轉換,
//從int轉換成Rational
//Rational r6 = 2 * r2; //錯誤,2(作為呼叫者)並沒有operator*成員函式,也無法自己對自己進行轉換
#ifndef _RATIONAL_H_
#define _RATIONAL_H_
class Rational
{
//friend const Rational operator*(const Rational& multiplicator1, const Rational& multiplicator2);
public:
Rational(const int numerator, const int denominator = 1);
~Rational();
const Rational operator*(const Rational& rhs) const;
private:
int m_Numerator;
int m_Denominator;
};
#endif // !_RATIONAL_H_
只有當引數被列為引數列內,這個引數才是隱式型別轉換的合格參與者。this物件絕不是隱式型別轉換的合格參與者。
如果需要為某個函式的所有引數(包括this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式一定是個non-member.
補充:對於非成員函式,比如友元函式,不能對函式使用const限定符。
例子見Item21
Item25 考慮寫出一個不拋異常的swap函式
預設情況下swap動作可由標準程式提供swap演算法完成。
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
但在這個swap中,涉及到三次複製,a複製到temp,b複製到a,temp複製到b. 有時候,這些複製並不必要。例如在使用了“以指標指向一個物件,內含真正資料”那種型別中。
可以看到,在如下程式中,std::swap(w1,w2)導致了一次copy建構函式呼叫,2次copy運算子呼叫。
#include "Widget.h"
int main()
{
Widget w1;
Widget w2;
std::swap(w1, w2);
return 0;
}
對程式碼進行修改,將std::swap提供一個全特化版本,然後提供一個成員函式用來進行Widget內部的資料交換。則可以看到,前面三次copy建構函式和copy assignment的呼叫就消除了。
void Widget::swap(Widget& other)
{
std::swap(this->pWImpl, other.pWImpl);
}
#include "Widget.h"
namespace std
{
template<>
void swap(Widget& a, Widget& b)
{
a.swap(b);
}
}
int main()
{
Widget w1;
Widget w2;
std::swap(w1, w2);
return 0;
}
注:函式模板的特化: 函式模板特化是在一個統一的函式模板不能在所有型別例項下正常工作時,需要定義型別引數在例項化為特定型別時函式模板的特定實現版本。
假設Widget和WidgetImpl都是class templates而非classes,
template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};
嘗試偏特化函式模板是不符合語法的。比如:
namespace std
{
template<typename T> //企圖偏特化函式模板,那不合法
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
解決方法是簡單的提供一個過載版本
namespace std
{
template<typename T>
//過載swap,但很不幸,不允許新增新的templates(或function,或class)在std名稱空間中
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
如果希望軟體有預期的行為,請不要新增任何新東西到std裡頭(但可以全特化std中的模板函式)。
解決方法是把上面的swap過載版本放入到另一個名稱空間中即可。
namespace WidgetStuff
{
template<typename T>
//過載swap,但很不幸,不允許新增新的templates(或function,或class)在std名稱空間中
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
C++的名稱查詢法則確保將找到global作用域或T所在之名稱空間內的任何T專屬swap.
查詢順序:
1)所在名稱空間的swap.
2)std空間中特化版本的swap.
3)std空間的普通swap.
最後,請記住,成員版的swap絕不拋異常。注意,這一準則只適用於成員版。
例子見Item25