Effective c++第三版
讓自己習(xí)慣C++
條款01:視C++為一個語言聯(lián)邦
- C++是一個同時支持過程形式拇惋、面向?qū)ο笮问健⒑瘮?shù)形式眷柔、泛型形式、元編程形式的語言
- 了解四個次語言
C
Object-Oriented C++
Template C++
STL
C++高效編程守則視情況而變化驴一,取決于你使用C++的哪一個部分
條款02:盡量以const、enum灶壶、inline替換#define
- 盡量不使用#define
#define ASPECT_RATIO 1.653
ASPECT_RATIO可能沒有進(jìn)入記號表內(nèi)肝断,當(dāng)運用這個常量時獲得一個編譯錯誤的信息時,可能會出現(xiàn)問題,因為錯誤信息可能會只提到1.653這個數(shù)值而不是ASPECT_RATIO胸懈。如果ASPECT_RATIO被定義在非自己所寫的文件里担扑,排查錯誤將非常麻煩。
- 解決方法
用一個常量替換上述的宏:
const double AspectRatio = 1.653;
AspectRatio是一個語言常量趣钱,肯定能夠被編譯器所看見涌献,即會進(jìn)入記號表中。且使用常量可能比使用#define導(dǎo)致較小量的碼首有。
- 兩種特殊情況
1.定義常量指針燕垃。常量指針通常放在頭文件中,因此有必要將指針?biāo)傅牡刂仿暶鳛閏onst井联。則就要用指向常量的常指針卜壕。如:
const char* const authorName = "S
2.class專屬常量。為了將常量的作用域限制于class內(nèi)低矮,你必須讓他成為class的一個成員。而為了確保常量只有一個實體被冒,你必須使他成為一個static成員:
class GamePlayer
{
private:
static const int NumTurns = 5; //常量聲明式
int scores[NumTurns];
...
};
其中NumTurns是一個聲明式军掂,而不是一個定義式。如果要取某個class專屬常量的地址昨悼,必須提供定義式如下:
const int GamePlayer::NumTurns; //NumTurns的定義蝗锥;
//下面說明為什么沒有給予數(shù)值
這個式子放入實現(xiàn)文件(cpp文件)而非頭文件。由于class常量在聲明時獲得初值率触,因此不可以再設(shè)初值终议。
注意,我們無法利用#define創(chuàng)建一個class專屬常量葱蝗,因為#define不重視作用域穴张。一旦宏被定義,它就在其后的編譯過程中有效两曼。#define不能夠用來定義class專屬常量皂甘,也不能夠提供任何封裝性。
如果編譯器不支持上述語言悼凑,可作如下改變:
class CostEstimate
{
private:
static const double FudegeFactor; //static class常量聲明
... //位于頭文件內(nèi)
};
const double CostEstimate::FudegeFactor = 1.35; //位于實現(xiàn)文件內(nèi)
但是上述中的GamePlayer::scores的數(shù)組必須要知道常量值偿枕。可以改用所謂的the enum hack補償做法户辫。
其理論依據(jù)是:“一個屬于枚舉類型的數(shù)值可權(quán)充ints被使用”渐夸,于是GamerPlayer可定義如下:
class GamePlayer
{
private:
enum { Numturns = 5}; //"the enum hack" 令NumTurns成為5的一個記號名稱就沒問題了
int scores[NumTurns]
...
};
enum hack的行為比較像#define而不像const,有時候這正是你想要的渔欢。例如取一個const的地址是合法的墓塌,但取一個enum的地址就不合法,取#define的地址通常也不合法。
如果你不像讓別人活得一個指針或者引用指向你的某個整數(shù)常量桃纯,enum可以幫助你實現(xiàn)這個目標(biāo)酷誓。
enum hac—>實用主義,是模板元編程的基礎(chǔ)技術(shù)态坦。
當(dāng)宏帶著宏實參時可以用模板內(nèi)聯(lián)函數(shù)取代:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
//可被取代為:
template<typename T>
inline void callWithMax(const T& a,const T& b) //不知道T是什么盐数,因此采用常引用采值的方式
{
f(a > b ? a : b);
}
callWithMax是個真正的函數(shù),它遵守作用域和訪問規(guī)則伞梯。即可以用于class中玫氢,作為一個private inline 函數(shù)。
有了const谜诫、enum漾峡、inline,我們隊預(yù)處理器的需求降低了喻旷。
- 對于單純常量生逸,最好以constexpr、const對象或者enum替換#define且预。
- 對于形似函數(shù)的宏槽袄,最好改用inline函數(shù)替換#define。
條款03: 盡可能使用const
const可修飾它在class外部global或namespace作用域的常量锋谐,或修飾文件遍尺、函數(shù)、區(qū)塊作用域中被聲明為static的對象涮拗。也可修飾class內(nèi)部中static和non-static成員變量乾戏。對于指針,可修飾指針本身不可變三热,指針?biāo)肝锊豢勺児脑瘢蛘邇烧叨疾皇腔蚨际莄onst:
char greeting[ ] = "hello";
char* p = greeting; //非常量指針
const char* p = greeting; //指向常量的指針
const char* const p = greeting; //指向常量的常指針
const出現(xiàn)在星號左邊,表示被指物是常量就漾。如果出現(xiàn)在星號右邊惯退,則表示指針是常量,如果出現(xiàn)在星號兩邊从藤,表示被指物和指針都是常量催跪。
const int * = int const *;
STL迭代器以指針為根據(jù)塑模出來,迭代器的作用就像個T指針夷野。聲明迭代器為const就相當(dāng)于聲明指針為const一樣(T const指針)懊蒸,表示一個常指針,如果你希望STL模擬一個const T*指針悯搔,你需要的是const_iterator:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); //iter的作用像個T* const
*iter = 10骑丸; //沒問題,改變iter所指物
++iter; //錯誤! iter是const
std::vector<int>::cosnt_iterator cIter = vec.begin();
*cIter = 10; //錯誤通危!cIter是const
++cIter; //正確铸豁,改變cIter
面對函數(shù)聲明時,const可以和函數(shù)返回值菊碟、各參數(shù)节芥、函數(shù)自身產(chǎn)生關(guān)聯(lián)。
令函數(shù)返回一個常量值逆害,往往可以降低因客戶錯誤而造成的意外头镊,且保證了安全性和高效性。例如:
有理數(shù)的operator*聲明式魄幕。
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational &rhs);
為什么要返回一個const對象相艇?因為用戶可能會產(chǎn)生這樣的行為:
Rational a, b, c;
...
(a * b) = c; //在a*b的成果上調(diào)用operator=
程序員可能在做bool運算時,不經(jīng)意間這么做:
if( a*b = c)... //其實只是想做一個比較的操作
如果a纯陨,b是內(nèi)置類型坛芽,顯然不合法,而一個“良好的用戶自定義類型”的特征是它們避免無端地與內(nèi)置類型不兼容(見條款18)翼抠,因此才將operator*的回傳值聲明為const咙轩,預(yù)防上面的操作。
我們應(yīng)該在必要的時候使用const机久,避免出現(xiàn)鍵入“==”卻意外鍵成“=”的錯誤臭墨。
const成員函數(shù)
const實施于成員函數(shù)的目的是為了確認(rèn)該成員函數(shù)可作用于const對象赔嚎。這一類成員函數(shù)之所以重要膘盖,原因如下:
1.它們使class接口比較容易被理解。這是因為尤误,得知哪個函數(shù)可以改動對象內(nèi)容而哪個函數(shù)不行侠畔。
2.它們使“操作const對象”成為可能。因為如條款20所言损晤,改善c++程序效率的根本方法是以pass by reference-to-const方式傳遞對象软棺,而此技術(shù)可行的前提條件是,我們有const成員函數(shù)可以用來處理取得的const對象尤勋。
兩個成員函數(shù)如果只是常量性不同喘落,可以被重載。
class TextBlock
{
public:
...
const char& operator[ ] (std::size_t position) const
{return text[position];} //const對象
char& operator[ ] (std::size_t position)
{return text[position];} //non-const對象
private:
std::string text;
};
void print(const TextBlock& ctb)
{
std::cout<<ctb[0]; //調(diào)用const TextBlock::operator[]
...
只要重載operator[ ]并對不同的版本給予不同的返回類型最冰,就可以令const和non-const TextBlocks獲得不同的處理:
std::cout << tb[0]; //沒問題
tb[0]= 'x'; //沒問題
std::cout<< ctb[0]; //沒問題
ctb[0] = 'x' //錯誤
上述錯誤只因operator[ ]的返回類型導(dǎo)致的瘦棋,錯在對于一個返回值為const char&實施賦值。
對于想要在const成員函數(shù)中改變類中成員變量的值暖哨,此時應(yīng)該將該成員變量聲明為mutable赌朋。
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText); //錯誤!在const成員函數(shù)內(nèi)
lengthIsValid = true; //不能賦值給lengthIsValid和textLength
}
return textLength;
}
//改
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //此成員變量可以在const成員函數(shù)里更改
mutable bool lengthIsValid //此成員函數(shù)也可以在const成員函數(shù)中更改
};
std:: size_t CTextBlock::length() const
{
if(!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true;
return textLength;
}
在const和non-const成員函數(shù)中避免重復(fù)
class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
...
...
...
return text[positon];
}
char& ooperator[ ] (std::size_t position)
{
...
...
...
return text[position];
}
private:
std::string text;
};
兩個版本的operator[]函數(shù)重復(fù)了一些代碼,應(yīng)該做的是實現(xiàn)operator[ ]的技能一次并使用它兩次沛慢。令另一個調(diào)用另一個赡若。即:
class TextBlock
{
public:
...
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)[positon]);
}
...
};
第一次將TextBlock& 轉(zhuǎn)型為const TextBlock&。第二次則是從const operator[ ]的返回值中移除const
請記住
- 將某些東西聲明為const可幫助編譯器偵測出錯誤用法团甲。const可被施加于任何作用域內(nèi)的對象逾冬、函數(shù)參數(shù)、函數(shù)返回類型伐庭、成員函數(shù)本體粉渠。
- 編譯器強(qiáng)制實施bitwise constness
- 當(dāng)const和non-const成員函數(shù)有著實質(zhì)等價的實現(xiàn)時,令non-const版本調(diào)用const版本可避免代碼重復(fù)圾另。
條款04:確定對象被使用前已被初始化
C++中的對象初始化反復(fù)無常霸株。如果這樣寫:
int x;
在某些語境下x保證被初始化(為0),但在其他語境中卻不能保證集乔。
class Point
{
int x, y;
};
...
Point p;
p的成員變量由時候被初始化(為0)去件,有時候不會。
讀取為初始化的值會導(dǎo)致不明確行為扰路。有可能導(dǎo)致程序終止運行尤溜。導(dǎo)致一些不可測的行為。
好的處理方式是:永遠(yuǎn)在使用對象之前先將它初始化汗唱。對于無任何成員的內(nèi)置類型宫莱,你必須手工完成此事。例如:
int x = 0; //對int進(jìn)行手工初始化
const char* text = "A C-style string"; //對指針進(jìn)行手工初始化
double d;
std::cin>>d; //以讀取input stream的方式完成初始化
至于內(nèi)置類型之外的任何其他東西哩罪,初始化責(zé)任落在構(gòu)造函數(shù)身上授霸。即:確保每一個構(gòu)造函數(shù)都將對象的每一個成員初始化。
注意:不能混淆賦值和初始化际插。
class PhoneNumber {...};
class ABEntry
{
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones;
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name;
theAddress = address; //這些都是賦值而非初始化
thePhones = phones;
numTimesConsulted = 0;
}
這會導(dǎo)致ABEntry對象帶有你期望的值碘耳,但不是最佳做法。C++規(guī)定框弛,對象的成員變量的初始化動作發(fā)生在進(jìn)入構(gòu)造函數(shù)本體之前辛辨。在ABEntry構(gòu)造函數(shù)內(nèi),theName瑟枫、theAddress和thePhones都不是被初始化斗搞,而是被賦值。初始化的發(fā)生時間更早慷妙,發(fā)生于這些成員的default構(gòu)造函數(shù)被自動調(diào)用之時僻焚。
ABEntry構(gòu)造函數(shù)的一個較好的寫法是,使用所謂的成員初始值列表化替換賦值操作:
ABEntry::ABEntry(const std::string& name, const std::string& address, //現(xiàn)在這些都是初始化
const std::list<PhoneNumber>& phones)
:theName(name),theAddress(address),thePhones(phones),numTimesConsulted(0)
{ }
這個構(gòu)造函數(shù)和上一個的最終結(jié)果相同景殷,但通常效率較高溅呢≡杪牛基于賦值的那個版本首先調(diào)用默認(rèn)構(gòu)造函數(shù)為theName、theAddress和thePhones設(shè)初值咐旧,然后立刻對他們賦予新值驶鹉。default構(gòu)造函數(shù)的一切作為因此浪費了。成員初始化列表的做法避免了這個問題铣墨,因為初始列中針對各個成員變量而設(shè)的實參室埋,被拿去作為各成員變量之構(gòu)造函數(shù)的實參。
對于內(nèi)置對象如numTimesConsulted伊约,其初始化和賦值的成本相同姚淆,但為了一致性最好也通過成員初始化。
ABEntry::ABEntry( )
:theName(), theAddress(), thePhones(), //調(diào)用theNmae屡律、theAddress腌逢、thePhones的默認(rèn)構(gòu)造函數(shù)
numTimesConsulted(0) //記得將numTimesConsulted顯示初始化為0
{...}
- 我們立下一個規(guī)則:總是在初始列中列出所有成員變量。
有些情況下即使面對的成員變量屬于內(nèi)置類型超埋,也一定要使用初值列(int x(5))搏讶。如果成員變量是const或引用,它們就一定需要初值霍殴,不能被賦值(見條款5)媒惕。
為避免需要記住成員變量何時必須在成員初始化列表,何時不需要来庭。一個簡單的做法是:
- 總是使用成員初始化列表妒蔚。
當(dāng)許多class擁有多個構(gòu)造函數(shù),存在許多成員變量和或者base class月弛,可以合理地在成員初始化列表中遺漏哪些“賦值表現(xiàn)和初始化一樣好”得成員變量肴盏,改用它們的賦值操作,并將那些賦值操作移往某個函數(shù)(通常是private)尊搬,供所有構(gòu)造函數(shù)調(diào)用叁鉴。當(dāng)成員變量的初值是由文件或數(shù)據(jù)庫讀入時特別有用土涝。
比起由賦值操作完成的“偽初始化”佛寿,通過成員初始化列表完成的“真正初始化”通常更加可取。
C++有著固定的“成員初始化次序”但壮〖叫海基類應(yīng)該比其子類更先被初始化(見條款12),而class的成員變量總是以其聲明次序依次被初始化蜡饵。如上述中的ABEntry類弹渔,其中theName、theAddress溯祸、thePhones依次被初始化肢专。
注意:兩個成員變量的初始化或許必須帶有次序性舞肆。例如初始化數(shù)組時要指定大小,即代表大小的成員要先被初始化博杖。
當(dāng)內(nèi)置型成員變量明確地加以初始化椿胯,而且也確保你的構(gòu)造函數(shù)運用“成員初始化列表”初始化基類和成員變量,剩下的就是不同編譯單元內(nèi)定義的非局部靜態(tài)對象的初始化次序剃根。
static對象:存在時間是從被構(gòu)造出來知道程序結(jié)束為止哩盲,因此stack和hea-based對象都被排除。static對象包括全局對象狈醉、定義域namespace作用域內(nèi)的對象廉油、在class內(nèi)、函數(shù)內(nèi)苗傅、以及在file作用域內(nèi)被聲明為static的對象抒线。函數(shù)內(nèi)的static對象稱為local static對象,其他static對象稱為non-local static對象渣慕。程序結(jié)束時static對象會自動銷毀十兢,即在main()結(jié)束時自動調(diào)用其的析構(gòu)函數(shù)。
編譯單元:單一目標(biāo)文件的源代碼摇庙。即:單一源碼文件加上其所含的頭文件旱物。
- non-local static對象在main()開始之前就已經(jīng)被構(gòu)造出來了
現(xiàn)在,我們關(guān)心的問題設(shè)計兩個源碼文件卫袒,每一個至少有一個non-local static對象宵呛。
問題:如果某編譯單元內(nèi)的某個non-loacl static 對象的初始化動作使用了另一個編譯單元內(nèi)的某個non-local static對象,它所用到的這個對象可能尚未初始化夕凝,因為C++對“定義于不同編譯單元內(nèi)的non-local static對象”的初始化次序并無明確定義宝穗。
實例如下:
class FileSystem
{
public:
...
std::size_t numDisks() const; //眾多成員函數(shù)之一
...
};
extern FileSystem tfs; //預(yù)備給客戶使用的對象;
//另一個class
class Directory
{
public:
Directory( params );
...
};
Directory::Directory( params)
{
...
std::size_t disks = tfs.numDisks( ); //使用tfs對象
...
}
//進(jìn)一步假設(shè)码秉,這些客戶決定創(chuàng)建一個Directory對象逮矛,用來放置臨時文件:
Directory tempDir( params ); //為臨時文件而做出的目錄
現(xiàn)在,初始化次序的重要性就體現(xiàn)出來了:除非tfs在tempDir之前先被初始化转砖,否則tempDir的構(gòu)造函數(shù)會用到未初始化的tfs须鼎。由于tfs和tempDir是不同的人在不同的時間于不同的源代碼建立恰里的,他們是定義于不同編譯單元內(nèi)的non-local static對象府蔗。如何確定tfs在tempDir之前被初始化呢晋控?
答案是無法確定。
此問題的解決辦法:即將每個non-local static對象搬到自己的專屬函數(shù)內(nèi)(該對象在此函數(shù)內(nèi)被聲明為static)姓赤,這些函數(shù)返回一個引用指向它所含的對象赡译。然后用戶調(diào)用這些函數(shù),而不直接指涉這些對象不铆。換句話說蝌焚,non-local static對象被local static對象替換了裹唆。這是單例模式的一個常見實現(xiàn)手法。
此技術(shù)在tfs和temp身上只洒,結(jié)果如下:
class FileSystem {...};
FileSystem& tfs() //使用函數(shù)來代替tfs對象品腹。
{
static FileSystem fs; //定義并初始化一個local static對象,返回一個引用指向上述對象红碑。
return fs;
}
class Directory {...}; //同上
Directory::Dorectory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td; //同上
return td;
}
這樣修改之后舞吭,系統(tǒng)程序的客戶完全可以像以前一樣地使用它。不同之處是tfs析珊、tempDir變成了tfs()\tempDir()羡鸥。這種做法可能在多線程的情況下有問題。
運用引用-返回函數(shù)防止“初始化次序問題”忠寻,前提是其中有著一個對對象而言合理的初始化次序惧浴。即:如果對象A的初始化必須在B之前初始化,但是A能否初始化成功卻又受制于B是否已初始化奕剃。這樣就存在問題衷旅。
為了避免在對象初始化之前過早地使用它們,你需要做三件事纵朋。
1.手工初始化內(nèi)置型的非成員對象柿顶。
2.使用成員初始化列表
請記住
- 為內(nèi)置型對象進(jìn)行手工初始化。
- 構(gòu)造函數(shù)最好使用成員初始化列表操软,不要在構(gòu)造函數(shù)本體內(nèi)使用賦值操作嘁锯。初始化列表中的成員變量的次序應(yīng)該和它們在class