item2:
宏坑多,盡量用const和enum。類中定義的 static const int val = 0; 只是聲明,如果要取它的地址必須在實(shí)現(xiàn)文件中定義,定義不能有賦值,因?yàn)槁暶骼锩嬉呀?jīng)賦值了。
item3:
const std::vector<int>::iterator iter // acts like a T * const
std::vector<int>::const_iterator citer // acts like a const T *
重載運(yùn)算操作符的返回值最好設(shè)成const。函數(shù)的const和非const是不一樣的,可以重載。重載[]操作符最好重載兩個(gè)版本??梢杂孟旅孢@個(gè)方式來減少兩個(gè)重載函數(shù)的重復(fù)代碼
const char& operator[](std::size_t position)const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position];
)
}
const成員函數(shù)不能保護(hù)成員指針指向的東西,如果要保護(hù)成員指針指向的東西,要用template寫一個(gè)wrapper類,重載->操作符(具體看看二樓答案)。
用mutable聲明的成員變量即使在const對(duì)象里面也能被修改。
item4:
用初始化列表來初始化成員函數(shù)效率高。
用函數(shù)中的static對(duì)象來解決一個(gè)全局對(duì)象的初始化依賴另外一個(gè)文件的全局對(duì)象,而我們不能控制全局對(duì)象初始化順序的問題,為了效率可以把這個(gè)函數(shù)inline。
item6:禁止拷貝的方法:將拷貝構(gòu)造函數(shù)和賦值操作符設(shè)為private防止類外調(diào)用,不實(shí)現(xiàn)它防止friend調(diào)用,將它們聲明在基類可以把錯(cuò)誤信息從鏈接期移到編譯期,可以直接繼承boost的noncopyable。不過如果因此而多重繼承,noncopyable這個(gè)空基類的空間優(yōu)化可能會(huì)失去。
item7:凡是用了虛函數(shù)的類一定要有虛析構(gòu)函數(shù)。反正用了虛函數(shù)都要插多一個(gè)vptr,多個(gè)虛析構(gòu)也不會(huì)增加成本。
std::string, vector, list, set, tr1::unordered_map沒有虛構(gòu)造,所以不要繼承他。
item8:不要在析構(gòu)函數(shù)里面拋異常。如果遇到必須拋的情況,要處理在析構(gòu)過程中拋異常的情況,應(yīng)該留接口給類用戶在析構(gòu)拋異常的時(shí)候處理好析構(gòu)。這種做法很丑,沒有優(yōu)雅解決辦法的根源是,析構(gòu)函數(shù)的語義就假設(shè)析構(gòu)過程一定是沒問題的。
item9:不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)里面調(diào)用虛函數(shù),因?yàn)槟莻€(gè)時(shí)候vptr指向基類的虛函數(shù)表。如果構(gòu)造函數(shù)和析構(gòu)函數(shù)是通過另外一個(gè)函數(shù)來調(diào)用虛函數(shù),這種錯(cuò)誤的做法不會(huì)被編譯器檢查出來。
item10:賦值操作符要返回*this引用,因?yàn)檫@是賦值運(yùn)算符對(duì)于內(nèi)置類型的語義。
item11:
Object& Object::operator=(const Object& rhs)
{
Object *temp = p_obj;
p_obj = new Object(*rhs.p_obj);
delete temp;
return *this;
}
Object& Object::operator=(const Object& rhs)
{
Object temp(rhs);
swap(temp);
return *this;
}
Object& Object::operator=(const Object rhs)
{
swap(rhs);
return *this;
}
注意swap是自己定義的成員函數(shù)。上面這三種做法能夠防止自己給自己賦值,因?yàn)閐elete放在后面,也可以防止new拋異常以后,p_obj指向空資源。
item12:記得在拷貝構(gòu)造函數(shù)和賦值操作符里面調(diào)用 成員對(duì)象和基類 的拷貝構(gòu)造函數(shù)和賦值操作符,不然編譯器會(huì)給成員對(duì)象和基類調(diào)用默認(rèn)拷貝構(gòu)造函數(shù)而且不調(diào)用他們的賦值操作符??截悩?gòu)造函數(shù)和賦值操作符有相同代碼的時(shí)候,不應(yīng)該讓他們互相調(diào)用,而是將重復(fù)代碼放在第三個(gè)函數(shù)里面。
item13:
用構(gòu)造函數(shù)和析構(gòu)函數(shù)的語義來管理資源,以防止多處return或者函數(shù)中間拋異常而引起資源泄漏的情況(RAII,Resources Acquisition Is Initialization)。
auto_ptr不能用于STL容器,因?yàn)槿绻截愌b著auto_ptr的容器,原來的容器里面的auto_ptr全部被設(shè)成null。但shared_ptr就沒問題。但兩個(gè)智能指針都不能用于數(shù)組。
item14:對(duì)于管理資源的類,要好好考慮他被拷貝的情況??梢越箍截悾梢杂靡眉夹g(shù)??梢园奄Y源也拷貝過去??梢愿淖兯袡?quán)(像auto_ptr)。
item15:
有時(shí)候一些函數(shù)只能以裸指針作為參數(shù)。這個(gè)時(shí)候可以像auto_ptr和shared_ptr那樣提供一個(gè)get方法,也可以定義隱式類型轉(zhuǎn)換。
順帶一提,造智能指針的時(shí)候有應(yīng)該重載指針的應(yīng)有的api,這些api有 * -> ->*
item17:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
當(dāng)該statement以這樣的順序執(zhí)行:1 new Widget. 2 運(yùn)行priority() 3 把new出來的對(duì)象轉(zhuǎn)換成shared_ptr然后調(diào)用shared_ptr的拷貝構(gòu)造函數(shù),而priority又拋異常,那new出來的資源就泄漏了。所以應(yīng)該把new出來的資源交給智能指針的語句單獨(dú)放在一行里面。
item18: 如果接口容易用錯(cuò)(例如傳錯(cuò)參數(shù)),嘗試用C++內(nèi)置的類型檢查讓編譯器來為我們檢查這些錯(cuò)誤(引入新類型,用const)。盡量讓接口保持一致(命名一致,同名函數(shù)功能相似,重載操作符行為與內(nèi)置的一致)。
item22:書中說把成員變量設(shè)成private。其實(shí)我覺得這不重要,關(guān)鍵還是接口應(yīng)該設(shè)計(jì)得夠簡(jiǎn)單而且不那么需要改來改去。
item23:不要把跟類有關(guān)的函數(shù)都塞進(jìn)類里面。類應(yīng)該緊湊,這樣寫類的時(shí)候腦子不會(huì)爆掉。
item24:像封裝數(shù)學(xué)運(yùn)算,由于要支持兩種類型的二元運(yùn)算,所以運(yùn)算函數(shù)不能是成員函數(shù),因?yàn)檫@樣會(huì)限制實(shí)現(xiàn)二元運(yùn)算的交換率。
item25:如果要定制std::swap,最好像STL容器那樣先定義一個(gè)public的swap為成員函數(shù),然后std命名空間下定義一個(gè)swap來調(diào)用這個(gè)成員函數(shù)swap。如果這個(gè)swap也需要模板參數(shù),由于模板函數(shù)不支持partial specialization, 所以我們應(yīng)該重載swap函數(shù),而且要把非成員版本放在該類的命名空間下(因?yàn)椴荒茉趕td里面加template)
template<typename T>
void swap(Widget<T> &a, Widget<T>&b) // 這里原來是 T &a, T & b
{
a.swap(b);
}
item26:變量應(yīng)該在用的時(shí)候才定義。
item27:盡可能不用cast。即使用也盡可能用C++新的cast。注意不能通過下面的方式調(diào)用基類函數(shù),因?yàn)閏ast會(huì)產(chǎn)生新的臨時(shí)對(duì)象,函數(shù)調(diào)用不會(huì)在當(dāng)前對(duì)象中起作用
static_cast<Base>(*this).memfun();
item28:對(duì)象方法返回一個(gè)指向?qū)ο髢?nèi)部成員的指針,引用,迭代器時(shí),要小心外部可以通過這個(gè)對(duì)象方法來修改這些成員(可以通過返回const來解決),更要小心臨時(shí)對(duì)象調(diào)用這個(gè)函數(shù)而讓一個(gè)外部指針,引用,迭代其指向一個(gè)被析構(gòu)對(duì)象的內(nèi)部成員。
item29:非常精華和重要的有一節(jié)!
如果一個(gè)函數(shù)調(diào)用到一半拋異常后,Exception-safe有三個(gè)層次的保證:
1:程序仍然處于一個(gè)valid的狀態(tài)。但是具體處于什么常態(tài)不清楚。
2:程序處于未調(diào)用該函數(shù)前的狀態(tài)。
3:程序壓根不拋異常。
如果資源在函數(shù)開頭分配,在函數(shù)結(jié)束時(shí)釋放,應(yīng)該用RAII。如果要將某個(gè)指針指向一個(gè)新資源,析構(gòu)原來的資源,可以像shared_ptr那樣用reset。還可以用copy and swap,或者保證先分配好資源并且配置妥當(dāng),再釋放原來的資源(這樣即使分配資源或者配置狀態(tài)失敗的時(shí)候,不會(huì)改變?cè)瓉淼臓顟B(tài))。
struct PMImpl
{
std::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu
{
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream & imagSrc)
{
using std::swap;
Lock m1(&mutex);
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->reset(new Image(imagSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
基本的想法是,如果資源分配好,變量也配置好,就交換整個(gè)狀態(tài)。
如果一個(gè)函數(shù)不是Exception-safe,那么它在拋異常以后,狀態(tài)的不確定就導(dǎo)致整個(gè)程序的狀態(tài)不確定。
item30:調(diào)用虛函數(shù)的函數(shù)不能是inline。
item31: "pimpl idiom"(把一個(gè)類分成兩個(gè),一個(gè)負(fù)責(zé)實(shí)現(xiàn),一個(gè)通過提供指向?qū)崿F(xiàn)類對(duì)象的private指針和調(diào)用實(shí)現(xiàn)類方法的public函數(shù)提供接口。為的是防止一個(gè)頭文件修改導(dǎo)致n個(gè)文件要重新編譯)的做法真的好丑。不過,當(dāng)一個(gè)類僅僅用于組合其它類的話,盡量用指針確實(shí)是個(gè)不錯(cuò)的選擇。
在聲明函數(shù)時(shí),即使對(duì)象以值傳遞的方式傳給函數(shù)或者從函數(shù)返回,這個(gè)文件也不需要該類定義,只需要類聲明。因?yàn)槭呛瘮?shù)聲明而不是實(shí)現(xiàn),不需要知道類的大小。
三種方式減少編譯文件之前的依賴:
1 如果可以,在頭文件中用 指針或者引用。
2 在頭文件中進(jìn)可能用類聲明,而不是類定義。
3 將聲明和定義類的放在不同的頭文件里面。
可以使用工廠函數(shù)返回對(duì)象指針可以減少編譯文件間的依賴。
item32:繼承的語義是,子類擁有任何基類的行為。有時(shí)候會(huì)發(fā)現(xiàn)子類不應(yīng)該擁有基類某個(gè)方法或者在完成一項(xiàng)任務(wù)的時(shí)候應(yīng)該擁有與基類不一樣的函數(shù)。這不是設(shè)計(jì)得好不好的問題,而是繼承的語義本來就沒那么通用。
item33:可以在public: 里面using Base::memfun; 來讓被自己重載函數(shù)隱藏的基類同名函數(shù)重見天日。
item34:純虛函數(shù)的語義是定義通用接口(子類必須自己定義行為)。虛函數(shù)的語義是定義通用接口還有默認(rèn)行為。非虛函數(shù)的語義是子類不應(yīng)該改變的行為。
純虛函數(shù)可以定義,但是子類必須顯示調(diào)用(Base:memfun())。這種用法可以用來提供一個(gè)通用接口和默認(rèn)行為,但是使用默認(rèn)行為的時(shí)候必須顯示掉用,這樣又可以提醒需要自定義行為的子類重新定義該方法。
item35:
1 基類不直接定義public的虛函數(shù),而是把它設(shè)為private,而讓一個(gè)public的非虛函數(shù)調(diào)用這個(gè)虛函數(shù)。好處是通過在public的非虛函數(shù)里面可以放一些設(shè)置配置調(diào)用環(huán)境的代碼來保證子類在調(diào)用這個(gè)虛函數(shù)時(shí)處于正確的調(diào)用環(huán)境中。
2 將部分邏輯寫在類外的一個(gè)函數(shù)里面,在構(gòu)造函數(shù)中通過傳入這個(gè)函數(shù)的方式,能讓沒一個(gè)類的對(duì)象有不同的行為。而且可以在運(yùn)行期改變對(duì)象的行為。或者用boost的function和bind。
3 跟2差不多,只不過將類外的函數(shù)寫進(jìn)一個(gè)用類包裹的繼承體系里面(喔,類也能這么靈活。不過相比與lambda和boost的function,這種做法就顯得有點(diǎn)臃腫了)。
item36:不要重定義基類的非虛函數(shù)。
item37:不要在改變虛函數(shù)里面用默認(rèn)參數(shù)。因?yàn)檫@里有個(gè)陷阱,一個(gè)基類指針在調(diào)用子類虛函數(shù)的時(shí)候會(huì)用基類的默認(rèn)參數(shù)!
item39:private繼承表達(dá)的是以什么類來實(shí)現(xiàn)(只重用實(shí)現(xiàn),不重用接口)。通過將一個(gè)虛函數(shù)放進(jìn)內(nèi)部類里面,子類就不能修改這個(gè)虛函數(shù)(子類是可以override基類的private虛函數(shù)的)。private繼承空基類的時(shí)候,會(huì)有empty base optimization(EBO), 這樣編譯器不會(huì)為空類插入一個(gè)char還有padding。private繼承還可以讓子類override基類虛函數(shù)還有調(diào)用基類protected的成員。
item40:從對(duì)象模型來看,要實(shí)現(xiàn)多重繼承和虛擬繼承的語義,坑實(shí)在是太多了。指針要轉(zhuǎn)來轉(zhuǎn)去,運(yùn)行的成本很大。如果不在意效率,通過public繼承接口和private繼承實(shí)現(xiàn)確實(shí)很方便。
item41:類與模板都支持接口和多態(tài)(雖然我腦子里的模板多態(tài)指static polymorphism)。模板的implicit接口真的非常強(qiáng)大,特別是加上C++11的auto(好吧,這對(duì)動(dòng)態(tài)語言來說再平常不過)。
item42:nested dependent name在默認(rèn)情況下不會(huì)被編譯器認(rèn)為是類型,而是一個(gè)類中的成員,因此需要在類型前面加typename說明。但是他不能用于存在與繼承列表和初始化列表的基類的nested dependent name。
item43:在繼承一個(gè)模板基類的時(shí)候(同時(shí)子類也是模板類),由于這個(gè)模板基類可能存在specialization,對(duì)于不同的模板參數(shù),基類的實(shí)現(xiàn)可能完全不一樣!一個(gè)函數(shù)可以在general的實(shí)現(xiàn)有,在specialization的版本沒有。如果子類調(diào)用了這個(gè)函數(shù)(直接使用memfun();),編譯器會(huì)報(bào)錯(cuò)說沒有這個(gè)函數(shù)(因?yàn)榭赡苷娴臎]有)。解決方法有三個(gè):
1 把memefun(); 改成 this->memfun(); 這樣編譯器會(huì)假設(shè)有,如果沒有,編譯器是在模板被instantiated 的時(shí)候報(bào)錯(cuò)。
2 像item33那樣 using Base<T>::memfun(); 他讓編譯器在instantiation之前就去搜索這個(gè)函數(shù)。
3 把memfun(); 改成Base<T>::memfun(); 注意這種做法會(huì)讓虛函數(shù)失效。
item44:模板中與模板參數(shù)無關(guān)的代碼隨著各種版本instantiate,會(huì)讓編譯出來的程序膨脹。解決辦法是可以把這些參數(shù)放進(jìn)函數(shù)里面。是否會(huì)讓程序變快變慢要看實(shí)際。
item45: 非常tricky
template<typename T>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other):heldPtr(other.get())
{...}
T* get()const{ return heldPtr; }
private:
T *heldPtr;
};
這樣,只有U版本的heldPtr能夠隱式轉(zhuǎn)換成T版本的heldPtr,編譯才會(huì)通過,這樣就可以通過內(nèi)置的隱式類型轉(zhuǎn)換,讓自己構(gòu)造的類也能隱式類型轉(zhuǎn)換。這種做法可以用于賦值操作符,拷貝構(gòu)造函數(shù),也可以用于類型轉(zhuǎn)換操作符。不過當(dāng)T 和 U一樣的時(shí)候,編譯器不會(huì)造一個(gè)普通賦值操作符,拷貝構(gòu)造函數(shù)!需要自己重新寫一個(gè)。
item46:
當(dāng)不用模板的時(shí)候(模板函數(shù)加上模板類),在調(diào)用參數(shù)的時(shí)候參數(shù)可以通過構(gòu)造函數(shù)隱式類型轉(zhuǎn)換來得到函數(shù)所需要的參數(shù)類型。但是在模板參數(shù)類型推導(dǎo)的時(shí)候,編譯器不能讓通過構(gòu)造函數(shù)的隱式類型轉(zhuǎn)換發(fā)生。