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