TODO:還沒(méi)看太懂的篇章
- item25
- item35
-
模板相關(guān)內(nèi)容
基礎(chǔ)
視C++為一個(gè)語(yǔ)言聯(lián)邦
可以將C++視為以下4種次語(yǔ)言的結(jié)合體:
- C
- 面向?qū)ο?/li>
- 模板
- STL
每個(gè)次語(yǔ)言都有自己的規(guī)范,因此當(dāng)從其中一個(gè)切換到另一個(gè)時(shí),一些習(xí)慣或守則是可能會(huì)發(fā)生變化的。
以const, enum, inline替換#define
用const替換#define有以下2個(gè)原因:
- #define定義的符號(hào)名稱可能沒(méi)有進(jìn)入符號(hào)表而只是簡(jiǎn)單地被替換,因此在得到編譯錯(cuò)誤信息時(shí)不易改錯(cuò)。
例如#define PI 3.14
在報(bào)錯(cuò)時(shí)可能給出3.14而不是PI。 - 預(yù)處理器會(huì)盲目地做替換,例如多次使用PI時(shí)就會(huì)出現(xiàn)多份3.14,而使用const常量則可以只維護(hù)一個(gè)常量。
當(dāng)不想讓別人通過(guò)一個(gè)指針或引用來(lái)指向某個(gè)int常量時(shí),會(huì)用到enum hack做法,即用enum來(lái)代替(const)int的使用。例如取一個(gè)const地址是合法的,但取一個(gè)enum地址是非法的。
最后,對(duì)于形似函數(shù)的宏,最好改用inline。
盡可能使用const
只要某值是保持不變的,就應(yīng)該將它說(shuō)出來(lái)。
const出現(xiàn)在*左邊表示被指物是常量,在*右邊表示指針是常量。
但特殊的,迭代器并非如此。對(duì)于迭代器:
const vector<int>::iterator iter // iter類似于T* const
vector<int>::const_iterator citer // citer類似于const T*
對(duì)于函數(shù),令函數(shù)返回一個(gè)const可以預(yù)防某些無(wú)意義或不消息的錯(cuò)誤,例如if (a * b = c)
,當(dāng)abc是自定義類型時(shí)這句話不會(huì)報(bào)錯(cuò),但是顯然我們要的是==。
const成員函數(shù)
兩個(gè)成員函數(shù)如果只是常量性(constness)(即函數(shù)中行為是否是不變的)不同,則它們是可以被重載的。關(guān)于常量性,有兩個(gè)概念:bitwise/physical constness和logical constness。
前者指const函數(shù)中不更改任何對(duì)象的任何一個(gè)bit,這也是編譯器的策略。
但bitwise constness會(huì)有不在預(yù)期的情況。例如在函數(shù)中有一個(gè)指針,更改其所指物的值,這并不違反bitwise constness,但邏輯上不符合我們的預(yù)期。
因此就有了logical constness。它允許在函數(shù)中做一些修改,只要邏輯上不能變的不變就行,這是通過(guò)關(guān)鍵字mutable
對(duì)變量進(jìn)行修飾實(shí)現(xiàn)的。mutable釋放掉非靜態(tài)成員變量的bitwise constness約束,使其可以在const函數(shù)中改變。
雖然編譯器采用bitwise constness策略,但寫代碼時(shí)應(yīng)該使用logical constness。
另外,一些const函數(shù)和非const函數(shù)有著相同的實(shí)現(xiàn),為了代碼復(fù)用,我們可以令非const函數(shù)調(diào)用它對(duì)應(yīng)的const函數(shù),例如:
class TextBlock {
public:
const char& operator[](size_t position) const {
// 做一些事情,這部分const和非const函數(shù)都要做,即是他們倆的共同實(shí)現(xiàn)
return text[position];
}
char& operator[](size_t position) {
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]
);
}
};
這里做的,首先是將原始的TextBlock&
類型轉(zhuǎn)化為const TextBlock&
進(jìn)行[]
操作(即調(diào)用const版本),然后使用const_cast
從返回值中移除const。
注意不能相反地,用const版本調(diào)用非const版本。因?yàn)閏onst函數(shù)是一種約束,讓它調(diào)用沒(méi)有約束的函數(shù)就是違反了不改變的承諾。
確定對(duì)象使用前已被初始化
在構(gòu)造函數(shù)中使用初始化列表比賦值效率更高,因?yàn)楹笳邥?huì)先調(diào)用無(wú)參默認(rèn)構(gòu)造函數(shù),然后再給成員變量賦值。總是使用初始化列表是沒(méi)有問(wèn)題的。
C++有十分固定的成員初始化順序:基類早于派生類,成員變量總是以聲明的順序被初始化。
另外,為避免多個(gè)編譯單元之間的非局部靜態(tài)對(duì)象初始化次序問(wèn)題,應(yīng)使用一個(gè)函數(shù)返回指向靜態(tài)對(duì)象的引用而非直接使用靜態(tài)對(duì)象自身(這其實(shí)就是單例模式的一個(gè)常見(jiàn)手法)。
所謂靜態(tài)對(duì)象,就是指從被構(gòu)造一直存在到程序結(jié)束的對(duì)象。靜態(tài)對(duì)象包括全局對(duì)象、定義于命名空間作用域內(nèi)的對(duì)象,以及在類內(nèi)、函數(shù)內(nèi)核在文件作用域內(nèi)被聲明為static的對(duì)象。其中,函數(shù)內(nèi)的static對(duì)象被稱為局部靜態(tài)對(duì)象,其他則是非局部靜態(tài)對(duì)象。
構(gòu)造、析構(gòu)和賦值
內(nèi)含引用或常量成員的類的賦值操作需要自己重寫
不想使用自動(dòng)生成的函數(shù)時(shí)應(yīng)主動(dòng)拒絕
如果不想編譯器自動(dòng)生成拷貝函數(shù)等,可以將其聲明為private并且不去實(shí)現(xiàn)它,這樣的話如果別人試圖調(diào)用則會(huì)得到一個(gè)鏈接錯(cuò)誤。
當(dāng)然也可以將這個(gè)鏈接錯(cuò)誤提前到編譯期,做法是設(shè)計(jì)一個(gè)阻止自動(dòng)生成該函數(shù)的基類,例如不想要自動(dòng)生成拷貝構(gòu)造函數(shù)和賦值操作符,可以這樣實(shí)現(xiàn):
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
// 阻止拷貝構(gòu)造和賦值操作符
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
// 然后將自己的類繼承自Uncopyable即可
class MyClass : private Uncopyable {
```
};
為多態(tài)基類聲明虛析構(gòu)函數(shù)
如果不這么做,在用基類指針析構(gòu)派生類對(duì)象時(shí)可能會(huì)造成局部銷毀,即只析構(gòu)了基類的部分。
當(dāng)希望擁有一個(gè)抽象類,但并無(wú)可用的純虛函數(shù)時(shí),就可以為它聲明一個(gè)純虛析構(gòu)函數(shù),并且必須為它提供一個(gè)實(shí)現(xiàn),哪怕為空。
注意給基類聲明虛析構(gòu)函數(shù)這個(gè)規(guī)則只適用于具有多態(tài)性質(zhì)的基類,目的是為了通過(guò)基類接口處理派生類對(duì)象。如果不是具備多態(tài)性,則不用聲明虛析構(gòu)函數(shù)。
別讓異常逃離析構(gòu)函數(shù)
析構(gòu)函數(shù)絕對(duì)不要吐出,而應(yīng)該捕獲任何異常,然后吞下(不處理)或結(jié)束程序。
還有一個(gè)策略是把可能導(dǎo)致異常的函數(shù)不止在析構(gòu)函數(shù)調(diào)用,而且提供一個(gè)它的調(diào)用接口,這樣就給了客戶一個(gè)處理該異常的機(jī)會(huì)。
不要在構(gòu)造和析構(gòu)時(shí)調(diào)用虛函數(shù)
簡(jiǎn)單地說(shuō),在基類構(gòu)造期間,虛函數(shù)還不是虛函數(shù)?;蛘吒镜刂v,在派生類對(duì)象的基類構(gòu)造期間,對(duì)象類型是基類而非派生類。
令operator=返回一個(gè)this引用
所有的=、+=等操作符都應(yīng)該return *this
。
在operator=中處理自我賦值
形如x = x
這樣的自我賦值當(dāng)然不易發(fā)生,但由于基類的指針或引用可以指向其派生類對(duì)象,這就是潛在的自我賦值情況。
自我賦值可能引發(fā)一些問(wèn)題,例如下面這種情況在自我賦值時(shí)會(huì)由于先被delete掉而出錯(cuò):
class MyClass {
// ```
private:
SomeClass* sp;
};
MyClass& Myclass::operator=(const MyClass& rhs) { // rhs指right hand side,即'='右邊的值
delete sp;
sp = new SomeClass(rhs.sp);
return *this;
}
要阻止這種錯(cuò)誤,可以在函數(shù)最前面做一個(gè)identify test來(lái)檢驗(yàn):
MyClass& Myclass::operator=(const MyClass& rhs) {
// identify test
if (this == &rhs)
// 如果是自我賦值則什么都不做
return *this;
delete sp;
sp = new SomeClass(rhs.sp);
return *this;
}
除了自我賦值安全性,還要注意異常安全性,例如上例中new那句可能拋出異常,但此時(shí)指針已經(jīng)被delete。
令人高興的是,讓operator=具備異常安全性的同時(shí)往往會(huì)自動(dòng)地獲得自我賦值安全性,例如這樣寫:
MyClass& Myclass::operator=(const MyClass& rhs) {
SomeClass* temp = sp;
sp = new SomeClass(rhs.sp);
delete sp;
return *this;
}
修改類成員時(shí)記得修改其構(gòu)造函數(shù)、拷貝函數(shù)等
另外,與非const函數(shù)調(diào)用const函數(shù)來(lái)復(fù)用代碼相比,不要令拷貝賦值操作符調(diào)用拷貝構(gòu)造函數(shù),因?yàn)檫@就像構(gòu)造一個(gè)已存在的對(duì)象。
如果要復(fù)用代碼,可以提取共同部分創(chuàng)建一個(gè)新的函數(shù)供它們調(diào)用,這個(gè)函數(shù)往往是private的而且命名為init()。
資源管理
用對(duì)象管理資源
首先明確資源指的就是一旦使用,將來(lái)必須還給系統(tǒng)的東西,把資源放到一個(gè)管理資源的對(duì)象內(nèi),便可以依賴析構(gòu)函數(shù)自動(dòng)調(diào)用的機(jī)制確保其釋放。
這個(gè)觀念也叫作資源獲取即初始化(Rescource Acquisition Is Initialization, RAII),可以使用智能指針或共享指針來(lái)在資源初始化的時(shí)候就獲取它來(lái)實(shí)現(xiàn)該功能,也可以自己實(shí)現(xiàn)一個(gè)資源管理類。
在資源管理類中小心復(fù)制行為
智能指針在使用拷貝構(gòu)造函數(shù)或賦值操作符時(shí),會(huì)將之前的智能指針變?yōu)閚ull,而復(fù)制所得的指針將獲得資源的唯一擁有權(quán),以避免將來(lái)對(duì)同一資源的多次delete。共享指針則沒(méi)有該特性,而是一種引用計(jì)數(shù)型智能指針(reference-counting smart pointer, RCSP),它在引用數(shù)為0時(shí)刪除所指物,如果我們想要在引用為0時(shí)不刪除而是做些其他事情,可以自定義一個(gè)刪除器(deleter)函數(shù)替代其行為。
另外注意,復(fù)制資源管理對(duì)象時(shí),應(yīng)該同時(shí)復(fù)制其擁有的資源,即進(jìn)行深拷貝。
在資源管理類中提供對(duì)原始資源的訪問(wèn)
對(duì)于原始資源的訪問(wèn),可以采用顯式的或隱式的轉(zhuǎn)換方法。
auto_ptr和trl::shared_ptr都提供了一個(gè)get()顯式地訪問(wèn)原始資源,也可以隱式地轉(zhuǎn)換函數(shù),即使用->或*操作符。
RAII類返回原始資源是否違背了封裝呢?是的,但并無(wú)大礙,因?yàn)樗緛?lái)就不是為了封裝而存在的一個(gè)類,它只是為了確保資源的釋放而已。
以獨(dú)立語(yǔ)句將new的對(duì)象放入智能指針
我認(rèn)為這條建議更準(zhǔn)確地說(shuō)應(yīng)該是:RAII類在初始化并獲取資源時(shí),應(yīng)該是一句獨(dú)立語(yǔ)句。
例如在調(diào)用funcA(std::auto_ptr<SomeClass>(new SomeClass), funcB())
這樣的函數(shù)前,編譯器會(huì)做以下三件事:
- 調(diào)用funcB()
- 執(zhí)行new SomeClass
- 調(diào)用std::auto_ptr構(gòu)造函數(shù)
但C++并不保證這些事情的順序,如果以這樣的順序執(zhí)行:
- 執(zhí)行new SomeClass
- 調(diào)用funcB()
- 調(diào)用std::auto_ptr構(gòu)造函數(shù)
并且在調(diào)用funcB()時(shí)觸發(fā)了異常,那么RAII的初始化(new SomeClass)和獲?。ㄓ胊uto_ptr獲得資源)就被分開了,此時(shí)new返回的指針就遺失了。
避免這類問(wèn)題的方法就是將RAII作為一句獨(dú)立語(yǔ)句:
std::auto_ptr<SomeClass> p(new SomeClass);
funcA(p, funcB());
了解new-handler
當(dāng)new拋出異常前,會(huì)調(diào)用一個(gè)客戶指定的錯(cuò)誤處理程序,即所謂的new-handler。
我們重載new時(shí)必須用std::set_new_handler(someFunc)
來(lái)指定new-handler。一個(gè)設(shè)計(jì)良好的new-handler必須做到讓更多的內(nèi)存可被使用,或設(shè)置另一個(gè)new-handler。new-handler要一直設(shè)置并調(diào)用下一個(gè)new-handler,直到足夠的內(nèi)存被釋放出來(lái),或傳給它null指針并拋出異常,當(dāng)然也可以在中間終止程序。
重載new和delete
什么時(shí)候需要重載new和delete呢?
- 需要檢測(cè)運(yùn)行上的錯(cuò)誤
例如在每次分配內(nèi)存時(shí)在首或尾多分配幾個(gè)額外的字節(jié)(即簽名),delete時(shí)則可以檢測(cè)其值是否不變以檢測(cè)是否發(fā)生了錯(cuò)誤(類似于棧保護(hù)中的canary值)。 - 需要加強(qiáng)效率
默認(rèn)的new和delete的分配策略沒(méi)有針對(duì)性,如果我們確定性能瓶頸來(lái)源于內(nèi)存分配策略,那么就需要定制new和delete以提升性能。 - 需要收集分配時(shí)的信息
如果基類重載的new不想用來(lái)new派生類對(duì)象(多數(shù)情況也不應(yīng)如此,它們的大小是不同的),可以這樣來(lái)寫:
void* Base::operator new(std::size_t size) {
if (size != sizeof(Base))
// 用標(biāo)準(zhǔn)的new處理
return ::operator new(size);
}
使用new和delete的對(duì)應(yīng)形式
在new數(shù)組時(shí),也一定要使用delete[]釋放,一般情況當(dāng)然不會(huì)犯這種錯(cuò)誤,但下面這種情況就容易出錯(cuò):
typedef std::string Lines[4]; // Lines的4行中每一行都是一個(gè)string
std::string* lp = new Lines; // 返回一個(gè)string*,就像new string[4]一樣
// 此時(shí)應(yīng)該
delete[] lp;
為了避免這類錯(cuò)誤,盡量不要對(duì)數(shù)組形式做typedef。
另外,考慮SomeClass* p = new SomeClass
,這里調(diào)用了兩個(gè)函數(shù),分配內(nèi)存的new和構(gòu)造函數(shù)。如果new調(diào)用成功而構(gòu)造函數(shù)拋出異常,就可能會(huì)造成內(nèi)存泄漏。為了解決這個(gè)問(wèn)題,運(yùn)行期系統(tǒng)此時(shí)會(huì)調(diào)用這個(gè)new對(duì)應(yīng)的delete,但如果我們重載了new而沒(méi)有重載對(duì)應(yīng)的delete,就真的會(huì)發(fā)生內(nèi)存泄漏。
同樣的情景,如果重載了placement new就一定要寫placement delete。實(shí)際上placement delete只有在“其對(duì)應(yīng)的new觸發(fā)的構(gòu)造函數(shù)出現(xiàn)異?!睍r(shí)才會(huì)被調(diào)用,而簡(jiǎn)單地對(duì)一個(gè)指針使用delete是永遠(yuǎn)不會(huì)調(diào)用placement delete的。因此如果要避免placement new相關(guān)的內(nèi)存泄漏,我們需要提供一個(gè)正常的delete用于構(gòu)造期間無(wú)異常和一個(gè)對(duì)應(yīng)的placement delete用于異常的構(gòu)造期間。
設(shè)計(jì)與聲明
讓接口容易被使用
保證類的約束和一致性等,例如Month類只能有1~12月,可以這樣實(shí)現(xiàn):
class Month {
public:
// 以函數(shù)替代對(duì)象
static Month Jan() {return Month(1);}
static Month Feb() {return Month(2);}
// ···
private:
explicit Month(int m); // 阻止生成新的月份
};
另外,萬(wàn)一客戶忘記使用智能指針怎么辦?較好的辦法是先發(fā)制人地讓工廠函數(shù)返回智能指針,而不是對(duì)象:
SomeClass* createClass(); // 客戶可能不使用智能指針
std::auto_ptr<SomeClass> createClass(); // 強(qiáng)迫客戶使用智能指針
設(shè)計(jì)類時(shí)多考慮一些
- 對(duì)象如何被創(chuàng)建和銷毀
涉及構(gòu)造、析構(gòu)、new和delete的設(shè)計(jì)。 - 對(duì)象如何被值傳遞
拷貝構(gòu)造函數(shù)用于定義值傳遞的實(shí)現(xiàn)。 - 對(duì)象的初始化和賦值
決定了構(gòu)造函數(shù)和operator=的設(shè)計(jì)和不同。 - 對(duì)象的合法值
進(jìn)行約束、檢查和異常拋出。 - 它的繼承圖系
需要虛函數(shù)等。 - 對(duì)象需要怎樣的類型轉(zhuǎn)換
編寫需要的類型轉(zhuǎn)換函數(shù)。 - 需要重載哪些操作符
- 什么樣的行為需要封裝
或者什么樣的標(biāo)準(zhǔn)函數(shù)需要被聲明為private以避免調(diào)用、 - 誰(shuí)會(huì)用到該對(duì)象
決定了什么是public,什么是protected,什么是friends。 - 對(duì)象的未聲明接口是什么
它對(duì)效率、異常安全性和資源運(yùn)用提供何種保證?
盡量引用傳遞而非值傳遞
前者往往效率更高,且可避免切割問(wèn)題(slicing problem),即派生類對(duì)象采用值傳遞的方式,并被視為了一個(gè)基類對(duì)象,會(huì)調(diào)用基類的構(gòu)造函數(shù),進(jìn)而造成構(gòu)造不完全。
注意這條并不適用于內(nèi)置類型和STL的迭代器和函數(shù)對(duì)象。
必須值傳遞時(shí)就別引用傳遞了
如果是棧對(duì)象,就不要用指針或引用返回;如果是堆對(duì)象,就不要用引用返回;如果是局部靜態(tài)對(duì)象且有可能同時(shí)需要多個(gè)這樣的對(duì)象,就不要用指針或引用返回。
將成員變量聲明為private
一旦一個(gè)成員變量被聲明為public或protected且客戶開始使用它,就很難改變那個(gè)變量所涉及的一切,因?yàn)樾枰嗟闹貙憽y(cè)試、編譯等環(huán)節(jié)了。
越多的東西被封裝,我們改變它們的權(quán)利就越大。
用非成員且非友元函數(shù)替換成員函數(shù)
有一個(gè)反直覺(jué)的現(xiàn)象:非成員且非友元函數(shù)的封裝性是要優(yōu)于成員函數(shù)的。
因?yàn)槌蓡T函數(shù)還可以訪問(wèn)它用不到的private變量或函數(shù)等,但非成員且非友元函數(shù)則不行,它只能做自己該做的事情。
比較自然的做法是聲明這樣一個(gè)非成員非友元函數(shù),將其放到和類所在的同一個(gè)命名空間內(nèi)。
若所有參數(shù)都要類型轉(zhuǎn)換,則為其采用非成員函數(shù)
一個(gè)所有參數(shù)都要類型轉(zhuǎn)換的例子就是,比如有理數(shù)類Rational重載了*操作符,就可以Rational r2 = r1 * someInt
,但不能Rational r2 = someInt * r1
,因?yàn)楹笳哒{(diào)用的是int的*操作符。
此時(shí)就應(yīng)該將重載*操作符作為一個(gè)非成員函數(shù)。
那么是否需要將其聲明為友元函數(shù)呢?答案是不用,因?yàn)?操作符并不需要用到Rational類的私有變量。注意如果可以,盡量避免友元函數(shù)。
本條款只適合C++的面向?qū)ο蟠握Z(yǔ)言,而不適合模板次語(yǔ)言。
考慮寫出一個(gè)不拋異常的swap函數(shù)
copy and swap原則:為打算修改的對(duì)象做一個(gè)副本,在副本上做一切修改,如果有問(wèn)題則拋出異常(此時(shí)不影響之前的對(duì)象,因此異常安全)。修改完后,在一個(gè)不拋出異常的操作中交換原對(duì)象和副本。
還有很多看不懂,學(xué)一些模板后再看吧。
實(shí)現(xiàn)
盡可能延后變量的定義
不止應(yīng)該延后變量的聲明到使用為止,還應(yīng)該延后到給它賦初值為止。
如果是循環(huán)內(nèi)的變量,則要綜合考慮它的構(gòu)造、析構(gòu)、賦值代價(jià),以確定是在循環(huán)外還是循環(huán)內(nèi)聲明。
盡量少轉(zhuǎn)型
C++提供四種新的轉(zhuǎn)型方式:
- const_cast
將對(duì)象的常量性移除(cast away the constness)。 - dynamic_cast
安全向下轉(zhuǎn)型,用于決定某對(duì)象是否歸屬繼承體系中的某類型。通常用于當(dāng)要在派生類對(duì)象上執(zhí)行派生類函數(shù),但手上只有一個(gè)基類指針時(shí)。
dynamic_cast通常效率很低,謹(jǐn)慎使用。 - reinterpret_cast
執(zhí)行低級(jí)轉(zhuǎn)型,實(shí)際動(dòng)作取決于編譯器。 - static_cast
強(qiáng)行隱式轉(zhuǎn)換。
另外注意C++中單一對(duì)象可能擁有一個(gè)以上的地址行為,如下:
Derived d;
// 這句隱式地將Derived*轉(zhuǎn)換為Base*
Base* pb = &d;
避免返回指向private的handle(指針或引用)
一是容易引起bitwise constness導(dǎo)致的錯(cuò)誤。
二是不論這個(gè)handle是個(gè)指針、引用或迭代器,還是說(shuō)它是個(gè)常量,或返回它的函數(shù)是個(gè)常量函數(shù),關(guān)鍵是一個(gè)handle被傳出去了,這就可能造成handle比起所指對(duì)象更長(zhǎng)壽的風(fēng)險(xiǎn)。
時(shí)刻考慮異常安全
時(shí)刻想想某函數(shù)拋出異常會(huì)怎樣,它后面的語(yǔ)句本該執(zhí)行而沒(méi)有執(zhí)行會(huì)出問(wèn)題嗎?
關(guān)于異常安全有三個(gè)等級(jí)的保證:
- 基本承諾
如果有異常被拋出,承諾程序內(nèi)的任何事物都仍保持在有效狀態(tài)下,沒(méi)有任何對(duì)象或數(shù)據(jù)被破壞。 - 強(qiáng)烈保證
如果有異常被拋出,保證程序狀態(tài)不變,即函數(shù)如果失敗,程序會(huì)回到調(diào)用函數(shù)之前的狀態(tài)。 - 保證不拋出
保證不拋出異常。
強(qiáng)烈保證往往能以copy and swap實(shí)現(xiàn),但強(qiáng)烈保證并不總是有效,它需要函數(shù)中的所有部分都是強(qiáng)烈保證才行。
了解inline
inline導(dǎo)致的代碼膨脹可能導(dǎo)致額外的換頁(yè)行為,因而降低cache的命中率,進(jìn)而影響效率。
盡量降低文件間的編譯依賴關(guān)系
嘗試接口與實(shí)現(xiàn)分離,關(guān)鍵在于以聲明的依賴性替換定義的依賴性。常用的方法是handle class和接口類。
記住,如果對(duì)象引用或指針可以完成定義,就不要用對(duì)象,否則還會(huì)引出該類型的定義式。并且在要用到其他類時(shí),盡量以它的聲明式替換其定義式。
例如把一個(gè)Person類分割為兩個(gè)類,分別是接口類和實(shí)現(xiàn)類。接口類只有一個(gè)指針成員指向其實(shí)現(xiàn)類,這種設(shè)計(jì)被稱為pimpl(pointer to implementation) idiom,而這種類就被稱為handle class。
另一種制作handle class的方法是,令Person稱為一個(gè)抽象基類,即接口類,這種類的目的是詳細(xì)地一一描述其派生類的接口。它的派生類則需要有辦法為它創(chuàng)建對(duì)象,這就需要一個(gè)特殊的函數(shù)扮演派生類的構(gòu)造函數(shù),這就是工廠函數(shù)。
面向?qū)ο驝++
public繼承表示is-a關(guān)系
public繼承是一種is-a關(guān)系,is-a指的是適用于基類的每一件事情都適用于派生類。但有時(shí),常識(shí)中的is-a關(guān)系在面向?qū)ο笾胁⒉怀闪?,例如鳥會(huì)飛,企鵝繼承鳥,但企鵝不會(huì)飛。因此類設(shè)計(jì)需要更多的考慮這些情況(但不需要超出軟件需求的考慮,如果需求中用不到企鵝飛的部分,就不需要考慮)。
復(fù)合表示has-a或“根據(jù)某物實(shí)現(xiàn)出”關(guān)系
程序中的對(duì)象可以分為應(yīng)用域和實(shí)現(xiàn)域,前者如人、汽車等,后者如緩沖區(qū)、鎖、查找樹等。
當(dāng)復(fù)合在應(yīng)用域時(shí),表示has-a關(guān)系,在實(shí)現(xiàn)域時(shí)則表示is-implemented-in-terms-of(根據(jù)某物實(shí)現(xiàn)出)關(guān)系。
private繼承表示“根據(jù)某物實(shí)現(xiàn)出”關(guān)系
private繼承是一種實(shí)現(xiàn)技術(shù),意為派生類是根據(jù)基類實(shí)現(xiàn)的(這也是為什么繼承而來(lái)的所有東西都變成private的原因,因?yàn)樗鼈兌际菍?shí)現(xiàn)的細(xì)枝末節(jié)),因此private繼承在設(shè)計(jì)上沒(méi)有意義,只在軟件實(shí)現(xiàn)層面有意義。
復(fù)合也可以表示“根據(jù)某物實(shí)現(xiàn)出”的關(guān)系,要盡可能使用復(fù)合,除非當(dāng)派生類需要訪問(wèn)基類的protected成員,或需要重新定義繼承而來(lái)的虛函數(shù)時(shí)才用private繼承,原因有二:
- 當(dāng)一個(gè)類需要private繼承一個(gè)實(shí)現(xiàn),但并不想要自己的派生類改變其實(shí)現(xiàn)時(shí),就需要使用復(fù)合而非private繼承。這相當(dāng)于Java中的final關(guān)鍵字,即阻止派生類重新定義虛函數(shù)。
- 可以降低編譯依賴性。
另外,有一種情況是需要使用private繼承的,即空基類最優(yōu)化(empty base optimization, EBO),此時(shí)使用private繼承可以節(jié)約空間。
所謂的空類(empty class)指不使用空間的類,但由于C++的規(guī)定,空類至少要被安插一個(gè)char的大小到空對(duì)象內(nèi),因此,對(duì)于下面這種情況,sizeof(SomeClass) > sizeof(int)。
class Empty{};
class SomeClass {
private:
int x;
Empty e;
};
此時(shí)使用private繼承,就可以節(jié)約空間,使得sizeof(SomeClass) == sizeof(int)。
class SomeClass : private Empty {
private:
int x;
};
現(xiàn)實(shí)中的空類并不真正的空,它雖然沒(méi)有非靜態(tài)成員,但往往有typedef, enum, static或非虛函數(shù)。
另外補(bǔ)充,如果繼承類型是private,編譯器就不會(huì)自動(dòng)將派生類對(duì)象轉(zhuǎn)換成基類對(duì)象。
避免繼承帶來(lái)的重名問(wèn)題
如果真的有重名,可以使用using來(lái)得到被遮蓋的變量。
itrem34 區(qū)分接口繼承和實(shí)現(xiàn)繼承
- 純虛函數(shù)是為了讓派生類只繼承函數(shù)接口。
它要求“必須提供一個(gè)該函數(shù),但不干涉你怎么實(shí)現(xiàn)”。 - 普通虛函數(shù)是為了讓派生類繼承函數(shù)接口和缺省實(shí)現(xiàn)。
它要求“必須提供一個(gè)該函數(shù),但如果你不想寫,可以用我的缺省版本”。 - 非虛函數(shù)則代表其一般性高于特殊性,因此絕不該在派生類中被重新定義。
非虛函數(shù)是靜態(tài)綁定的。任何一個(gè)派生類對(duì)象都有可能表現(xiàn)出基類或派生類的行為,這取決于指向該對(duì)象的指針在聲明時(shí)的類型是基類指針還是派生類指針。
文中還提到可以利用純虛函數(shù)“也可以先實(shí)現(xiàn),且必須在派生類中重新聲明”的特點(diǎn),避免派生類忘記實(shí)現(xiàn)虛函數(shù)而非預(yù)期地使用其缺省版本。具體做法是將原本的虛函數(shù)改為純虛函數(shù)并實(shí)現(xiàn)它,然后在派生類要用到缺省版本時(shí)手動(dòng)調(diào)用之前實(shí)現(xiàn)的純虛函數(shù),這樣,在本應(yīng)實(shí)現(xiàn)卻忘記實(shí)現(xiàn)該虛函數(shù)時(shí)就會(huì)引發(fā)報(bào)錯(cuò)。
但是和這樣麻煩的做法相比,我覺(jué)得“記得實(shí)現(xiàn)該函數(shù)”不應(yīng)該才是更應(yīng)該做的嗎?
考慮虛函數(shù)以外的其他選擇
絕不重新定義繼承而來(lái)的默認(rèn)參數(shù)
一個(gè)例子:
class Shape {
public:
enum Color {Red, Green, Blue};
// 每個(gè)形狀都要有draw函數(shù),默認(rèn)為紅色
virtual void draw(Color color = Red) const = 0;
};
class Rectangle : public Shape {
public:
virtual void draw(Color color = Blue) const;
/*
* 大錯(cuò)誤!虛函數(shù)是動(dòng)態(tài)綁定,而默認(rèn)參數(shù)是靜態(tài)綁定
* 如果有Shape* pr = new Rectangle;
* 因?yàn)閜r動(dòng)態(tài)類型是Rectangle*,因此調(diào)用的是Rectangle的draw()
* 而其靜態(tài)類型卻是Shape*,因此默認(rèn)參數(shù)是來(lái)自Shape的Red
*/
};
class Circle : public Shape {
public:
virtual void draw(Color color) const;
/*
* 注意,雖然基類有默認(rèn)參數(shù)
* 但用對(duì)象調(diào)用該函數(shù)時(shí),還是一定要指定參數(shù)
* 因?yàn)殪o態(tài)綁定下該函數(shù)并不從基類繼承默認(rèn)參數(shù)
* 但若用指針或引用調(diào)用該函數(shù),則可以獲得默認(rèn)參數(shù)
* 因?yàn)閯?dòng)態(tài)綁定下會(huì)得到繼承
*/
};
當(dāng)想要類似上面的虛函數(shù)表現(xiàn)出期望的行為時(shí),應(yīng)該考慮替換設(shè)計(jì)。例如讓非虛函數(shù)指定默認(rèn)參數(shù),并調(diào)用一個(gè)private虛函數(shù)來(lái)完成真正的工作:
class Shape {
public:
enum Color {Red, Green, Blue};
// 只用來(lái)指定默認(rèn)參數(shù)
void draw(Color color = Red) const {
doDraw(color);
}
private:
// 真正完成工作的函數(shù)
virtual void doDraw(Color color) const = 0;
};
class Rectangle : public Shape {
private:
virtual void doDraw(Color color) const;
}
注意繼承而來(lái)的private的虛函數(shù)是可以重寫的。
謹(jǐn)慎使用多重繼承
多重繼承可能引發(fā)鉆石型繼承,這可能會(huì)造成派生類從多個(gè)路徑繼承重復(fù)的成員,例如:
File中有一個(gè)filename,IOFile則會(huì)繼承到2個(gè)filename。如果不想要,就必須令File成為一個(gè)虛基類。但虛基類帶來(lái)的虛繼承需要付出代價(jià),除了空間,還有虛基類的初始化要由最低端的類負(fù)責(zé)。如果必須使用虛基類,盡可能避免在其中放置數(shù)據(jù)。
模板
模板元編程(Template metaprogramming,TMP)是編寫基于模板的c++程序并執(zhí)行于編譯期間。已經(jīng)證明TMP是一個(gè)圖靈完全機(jī)器,它可以做到任何事情,聲明變量、執(zhí)行循環(huán)等。TMP擅長(zhǎng)的事情有:
- 保證度量單位正確
科學(xué)和工程應(yīng)用中TMP可以確保度量單位的準(zhǔn)確使用。 - 優(yōu)化矩陣運(yùn)算
- 生成定制的設(shè)計(jì)模式
對(duì)模板而言,接口是隱式的,多態(tài)則是通過(guò)模板具現(xiàn)化和函數(shù)重載解析發(fā)生于編譯期。
使用模板可能會(huì)導(dǎo)致代碼膨脹,模板可能生成多個(gè)類和函數(shù)。因非類型的模板參數(shù)造成的代碼膨脹(即模板參數(shù)不是類型,而用于其他用途),只需用函數(shù)參數(shù)或類成員變量來(lái)替代模板參數(shù)即可。而因類型的模板參數(shù)造成的代碼膨脹(如針對(duì)參數(shù)是int和long生成兩種代碼,但有時(shí)它們是相同的),降低膨脹的做法是讓它們共享代碼。
typename還用來(lái)標(biāo)識(shí)嵌套從屬類型
template<typename T>
void func(const T& container) {
T::const_iterator* it;
···
}
T::const_iterator
上面的T::const_iterator
就是嵌套從屬名稱,我們想要的是任意一個(gè)STL容器T的迭代器const_iterator,但如果T::const_iterator
不是類型呢?如果T剛好有個(gè)靜態(tài)成員變量叫做const_iterator,且it恰好是個(gè)全局變量,那么上式就不再是聲明一個(gè)指針,而是讓T::const_iterator
乘以it。
不巧的是,C++的解析器在模板中遇到一個(gè)嵌套從屬名稱時(shí),會(huì)假設(shè)其不是一個(gè)類型。除非我們告訴他,方法就是在其前面加上typename:typename T::const_iterator* it
。
不過(guò)還有例外,在繼承的基類列表和初始化列表中都不能使用typename。
需要類型轉(zhuǎn)換時(shí)為模板定義非成員函數(shù)
繼續(xù)使用有理數(shù)類為例:
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1);
···
};
template<typename T>
const Rational<T> operator*(const Rational<T> lhs, const Rational<T> rhs) {}
此時(shí),如果有
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;
就會(huì)編譯錯(cuò)誤。
編譯器要找到某個(gè)operator*接受兩個(gè)Rational<T>
,就需要推導(dǎo)出T。而為了推導(dǎo)出T就需要查看兩個(gè)參數(shù)。第一個(gè)Rational<T>
參數(shù)oneHalf是Rational<int>
,因此T就一定是int,但第二個(gè)Rational<T>
參數(shù)2是一個(gè)int,就推導(dǎo)不出T的類型。要知道編譯器在模板實(shí)參推導(dǎo)過(guò)程中不會(huì)考慮隱式類型轉(zhuǎn)換函數(shù),因此也就不會(huì)將int隱式轉(zhuǎn)換為Rational<T>
。
解決方法則是將operator*的合并到其類內(nèi):文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-684548.html
template<typename T>
class Rational {
public:
···
friend const Rational<T> operator*(const Rational<T> lhs, const Rational<T> rhs) {}
};
有趣的點(diǎn)在于,雖然使用了friend,但與其傳統(tǒng)用途(訪問(wèn)類的非公有成員)毫不相干。為了讓類型轉(zhuǎn)換發(fā)生到所有實(shí)參身上,我們需要一個(gè)非成員函數(shù)。而為了讓這個(gè)函數(shù)被自動(dòng)具現(xiàn)化,我們需要將其聲明在類內(nèi)部。最終,在類內(nèi)聲明一個(gè)非成員函數(shù)的唯一方法就是,使用friend。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-684548.html
到了這里,關(guān)于effective c++ 筆記的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!