Item18 Make interfaces easy to use correctly and hard to use incorrectly.讓接口容易被正確使用,不易被誤用
好的接口設計應該是既容易被正確使用,又不易被誤用。
就例如書中的Sample,關于時間的,我們一般的做法就是在創建Day對象時,追加校驗函數來判斷年月日是不是有效。
建議的做法一是:創建新的類型
定義:
class Month {
public:
static Month Jan() {return Month(1); }
static Month feb() {return Month(2); }
…
private Month(int m);
};
Date d(Month::Mar(), Day(30), Year(1995) );
為啥不直接使用靜態變量?
參考Item4 P30,簡單說就是不能保證在使用non-local static objects時,這個對象就已經初始化了。如果non-local static objects在另一個文件了,又恰巧沒有初始化,系統當然就會翹辮子了。
另一種方法:加上const來限制type可以做的事情。
先參考Item3 P19
class Rational { …};
const Rational operator*(const Rational &lhs, const Rational &rhs);
之所以強制設置為const就是為了避免client在使用時出錯。因為如果沒有clien,那么:
Rational a,b,c;
…
(a*b)=c
這種寫法是對的,但是如果a,b是內置類型,這種寫法就是錯誤的。
除非有必要,否則就要保證你的類型type的行為和內置類型一致。
一致性導致接口容易被正確使用。
STL是榜樣,java在這里成了反面教材,因為如果想知道容器內對象的數量,用Array,要訪問屬性length,String要用length函數,ArrayList要用size函數,這就是不一致性。
使用std::tr1::shared_ptr,消除client對資源管理的責任
找出以下寫法的兩個易錯的地方:
Investment* createInvestment();
1 忘記刪除createInvestment()返回的指針
2 刪除這個指針多次
so,修改定義:
std::tr1::shared_ptr< Investment > createInvestment();
如果出現這種情形:從createInvestment得到Investment*的函數要把這個指針傳遞個給叫做getRidOfInvestment,由getridOfInvestment取代使用delete。
這里就出現了一個新的client易錯的點,用戶或許會使用錯的資源釋放機制。因為delete被getRidOfInvestment取代了。
std::tr1::shared_ptr< Investment >
pInv(static_cast<Investment*>(0), getRidOfInvestment);
那么定義就應該是這樣的:
std::tr1::shared_ptr< Investment > createInvestment()
{
std::tr1::shared_ptr< Investment >
retVal(static_cast<Investment*>(0), getRidOfInvestment); //這不能讓client來做
retVal = …;
return retVal;
}
tr1::shared_ptr的優點是允許在一個DLL創建對象,在另一個DLL里刪除對象。
牢記
- Good interfaces are easy to use correctly and hard to use in correctly.
- 接口一致性,于內置數據類型的行為兼容
- 阻止錯誤的方式還包括創建新的類型(例如Month),限制類型上的操作,束縛對象值,以及消除客戶的資源管理的責任
- tr1::shared_ptr支持定制類型的刪除器deleter,允許在一個DLL創建對象,在另一個DLL里刪除對象。
Item19:Treat class design as type design.設計class猶如設計type
在設計一個類的時候,要回答一系列的問題哦。
參考大師在P85-P86之間給出的常常的清單吧,其實實際上,我在設計類的時候的確沒有想過這么多,問過自己這么的為什么,所以這也是我總是在追求代碼重用,卻總是發現自己寫的代碼重用度很低的一個原因把。
Item20:Prefer pass-by-reference-to-const to pass-by-value.寧以pass-by-reference-to-const替換pass-by-value
But先學習一個單詞,characteristic,KAO,這竟然是個名詞。
再學一個地道的說法:解決問題的方法:The way around the slicing problem is…
函數都是值傳遞。pass by-value。function parameters are initialized with copies of the actual arguments, and function callers goes back a copy of the value returned by the function.這樣當然開銷就大了,每次都先copy一份進來,完事以后,再copy一份出去。
假設函數的參數是一個Student對象,bool validateStudent(Student s);調用這個函數,額外的隱性開銷包括要先調用copy constructor創建一個Student對象用于函數內部,函數執行結束再調用析構函數釋放這個對象。
開銷太大了,改進一下:pass by reference-to-const
bool validateStudent(const Student& s);
引用是通過指針來實現實現的,因此傳遞引用實際上就是在傳遞指針。references are typically implemented as pointers.
但是這個規則對于內置數據類型不適用,也不是適用STL iterator和函數對象function objects。
即使再小的對象也應該不要使用值傳遞,而是要使用pass by reference-to-const。
關于slicing problem的另一種描述
slicing problem是在多態規則里面容易產生的。
看一個簡單的基類、派生類的定義
class Window
{
public:
int height;
int width;
};
class TextWindow : public Window
{
public:
int cursorLocation;
};
…
Window win;
TextWindow *tWinPtr;
tWinPtr = new TextWindow;
win = *tWinprt;
win是一個Window對象,C++規定:給win分配的內存看見的大小,由其靜態類型決定。就是說默認的拷貝函數導致信息會出現丟失。這就是slicing problem。
試想一下這要是通過值傳遞的方式傳遞參數,實參一copy就已經丟失信息了。
牢記
- Prefer pass-by-reference-to-const over pass-by-value.這樣既有效,又可以避免slicing problem。
- 但是這個規則對于內置數據類型,STL iterator和函數對象function objects不適用。對于它們傳遞值就好了。
Item 21:Don't try to return a reference when you must return an object.必須返回對象時,別妄想返回其reference
heap and stack
堆和棧這是2個不同的概念,哎喲,我一直以為是一個詞。
heap:堆
- 棧是系統提供的功能,特點是快速高效,缺點是有限制,數據不靈活;
- 堆是函數庫內部數據結構,不一定唯一。
- 堆空間的分配總是動態的,雖然程序結束時所有的數據空間都會被釋放回系統,但是精確的申請內存/釋放內存匹配是良好程序的基本要素。
- 使用new創建的對象是在heap上分配內存空間。
stack:棧
- 而堆是函數庫提供的功能,特點是靈活方便,數據適應面廣泛,但是效率有一定降低。
- 棧是系統數據結構,對于進程/線程是唯一的;不同堆分配的內存無法互相操作。
- 棧空間分靜態分配和動態分配兩種。靜態分配是編譯器完成的,比如自動變量(auto)的分配。動態分配由alloca函數完成。棧的動態分配無需釋放(是自動的),也就沒有釋放函數。為可移植的程序起見,棧的動態分配操作是不被鼓勵的!
- 定義的局部變量是在stack上分配內存空間的。
牢記
Item22:Declare data members private.將成員變量聲明為private
why data members shouldn’t be public?
argument
(against) 爭論,意見
實參
形參是parameters
protected data member is mot more encapsulated than public one.
牢記
- data member一定要封裝。
- protected不必public有更好的封裝。
Item 23:Prefer non-member non-friend functions to member functions.寧以non-member、non-friend替換member函數
這是一個典型的例子:
class WebBrowser {
public:
…
void clearCache();
void clearHistory();
void removeCookies();
…
};
為了提供一個執行所有操作的函數,所以就在WebBrowser里面追加定義:
void clearEverything();
哎,我一直就是這么寫的,并自以為有很好的封裝,But
void clearBrowser(WebBrowser wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
第一:前者并不比后者有很好的封裝
這就要解釋一下什么叫做“封裝”?以及封裝的判別標準。
封裝的判別標準:可以通過統計能夠訪問這個data的函數的數目來計算,函數越多,這個data封裝也就月不好,因此前一種寫法的封裝就沒有后者好。這也可以用來解釋Item22里面,為什么要求數據成員不能定義為public。另外增加clearEverything()作為member function,實際上是降低了封裝性。而后面的non-member non-friend functions的定義就沒有改變WebBrowser的封裝性。
第二:后者還能提供更加靈活的打包package,增加擴展性。
put all convenience functions in multiple header files, but one namespace.
第三:增加函數的可擴展性。
你可以定義自己的convenience functions,寫到一個header file里面,放到同一個namespace里面。這是member function做不到的。
牢記
- 優先使用non-member non-friend函數來替換member函數。
Item 24:Declare non-member functions when type conversions should apply to all parameters.若所有參數皆需類型轉換,請為此采用non-member函數
原因:
Parameters are eligible for implicit type conversion only if they are listed in the parameter list.
結論:
make operator* a non-member function, thus allowing compilers to perform implicit type conversions on all arguments.
class Rational {
…
};
const Rational operatior*(const Rational& lhs, Rational& rhs)
{
return Rationan(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs. denominator () );
}
一個誤區:
如果一個函數,和某個類相關,而又不能定義成member,那么這個函數就一定要定義成friend。
上面這個例子就說明這個說法并不正確。真愛生命,慎用friend functions。
Item 25:Consider support for a non-throwing swap.考慮寫出一個不拋異常的swap函數
- default swap
就是指std里面定義的swap
- member swap
- nonmember swap
- specializations of std::swap
member swap
Widget::我們希望的是交換指針,但swap實際做的是不僅copy了3個Widget對象,而且還copy了3個WidgetImpl對象。太浪費了!都低碳時代了。
class Widget{
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl;);
}
…
};
template<> void swap<Widget>( Widget& a, Widget&b)
{
a.wap(b);
}
nonmember swap
接下來要討論的是如果Widget和WidgetImpl不是類而是類模板會怎么樣?
約束條件:不能在std里面增加新的template,只能特化(specialize)std內的template。
如果非要定義,say sorry。behavior is undefined。KAO,其實這比異常還討厭。
解決方法是把它定義到一個自己的namespace里面,而不要定義到std里面。
namespace WidgetStuff {
…
template<typename T>
class Widget{…};
…
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
specializations of std::swap
如果僅僅是針對一個class,那就特化std::swap。
If you want to have your class-specializing version of swap called in as many contexts as possible, you need to write both a non-member version in the same namespace as your class and a specialization of std::swap.
這部分十分繞,P111還對于C++的name lookup的規則進行了詳細的描述。值得重新溫習。