本文原址:http://m.shnenglu.com/tiandejian/archive/2007/04/11/ECPP_03.html
第3項(xiàng): 盡可能使用 const
const令人贊嘆之處就是:你可以通過它來(lái)指定一個(gè)語(yǔ)義上的約束(一個(gè)特定的不能夠更改的對(duì)象)這一約束由編譯器來(lái)保證。通過一個(gè)const,你可以告訴編譯器和其他程序員,你的程序中有一個(gè)數(shù)值需要保持恒定不變。不管何時(shí),當(dāng)你需要這樣一個(gè)數(shù)時(shí),你都應(yīng)該這樣做,這樣你便可以讓編譯器來(lái)協(xié)助你確保這一約束不被破壞。
const 關(guān)鍵字的用途十分廣泛。在類的外部,你可以定義全局的或者名字空間域的常量,也可以通過添加 static 關(guān)鍵字來(lái)定義文件、函數(shù)、或者程序塊域的對(duì)象。在類的內(nèi)部,你可以使用它來(lái)定義靜態(tài)的或者非靜態(tài)的數(shù)據(jù)成員。對(duì)于指針,你可以制定一個(gè)指針是否是 const 的,其所指的數(shù)據(jù)是否是 const 的,或者兩者都是 const ,或者兩者都不是。
char greeting[] = "Hello";
char *p = greeting; // 非 const 指針,非 const 數(shù)據(jù)
const char *p = greeting; // 非 const 指針, const 數(shù)據(jù)
char * const p = greeting; // const 指針,非 const 數(shù)據(jù)
const char * const p = greeting; // const 指針, const 數(shù)據(jù)
這樣的語(yǔ)法看上去反復(fù)無(wú)常,實(shí)際上并不是這樣。如果 const 關(guān)鍵字出現(xiàn)在星號(hào)的左邊,那么指針?biāo)赶虻?/em>就是一個(gè)常量;如果 const 出現(xiàn)在星號(hào)的右邊,那么指針本身就是一個(gè)常量;如果 const 同時(shí)出現(xiàn)在星號(hào)的兩邊,那么兩者就都是常量。
當(dāng)所指向的為常量時(shí),一些程序員喜歡把 const 放在類型之前;其他一些人則喜歡放在類型后邊,但要在星號(hào)的前邊。這兩種做法沒有什么本質(zhì)的區(qū)別,所以下邊給出的兩個(gè)函數(shù)聲明的參數(shù)表實(shí)際上是相同的:
void f1(const Widget *pw); // f1 傳入一個(gè)指向 Widget 對(duì)象常量的指針
void f2(Widget const *pw); // f2 也一樣
由于這兩種形式在實(shí)際代碼中都會(huì)遇到,所以你都要適應(yīng)。
STL 迭代器是依照指針模型創(chuàng)建的, 所以說(shuō)一個(gè) iterator 更加像一個(gè)指向 T* 的指針。把一個(gè) iterator 聲明為 const 的更像是聲明一個(gè) const 的指針(也就是聲明一個(gè)指向 T* const 的指針): iterator 不允許指向不同類型的內(nèi)容,但是其所指向的內(nèi)容可以被修改。如果你希望一個(gè)迭代器指向某些不能被修改的內(nèi)容(也就是指向 const T* 的指針),此時(shí)你需要一個(gè) const_iterator :
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
// iter 就像一個(gè) T* const
*iter = 10; // 正確,可以改變 iter 所指向的內(nèi)容
++iter; // 出錯(cuò)! Iter 是一個(gè) const
std::vector<int>::const_iterator cIter = vec.begin();
// cIter 就像一個(gè) const T*
*cIter = 10; // 出錯(cuò)! *cIter 是一個(gè) const
++cIter; // 正確,可以改變 cIter
const 在函數(shù)聲明方面還有一些強(qiáng)大的用途。在一個(gè)函數(shù)聲明的內(nèi)部, const 可以應(yīng)用在返回值、單個(gè)參數(shù),對(duì)于成員函數(shù),可以將其本身聲明為 const 的。
讓函數(shù)返回一個(gè)常量通常可以減少意外發(fā)生的可能,而且不用放棄考慮安全和效率問題。好比有理數(shù)乘法函數(shù)( operator* )的聲明,更多信息請(qǐng)參見第 24 項(xiàng)。
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
很多程序員在初次到這樣的代碼時(shí)都不會(huì)正眼看一下。為什么 operator* 要返回一個(gè) c onst 對(duì)象呢?這是因?yàn)槿绻皇沁@樣,客戶端將會(huì)遇到一些不愉快的狀況,比如:
Rational a, b, c;
...
(a * b) = c; // 調(diào)用 operator= 能返回一個(gè) a*b !
我不知道為什么一些程序員會(huì)企圖為兩個(gè)數(shù) 的乘積賦值,但是我確實(shí)知道好多程序員的初衷并非如此。他們也許僅僅在錄入的時(shí)候出了個(gè)小差錯(cuò)(他們的本意也許是一個(gè)布爾型的表達(dá)式):
if (a * b = c) ... // 噢 … 本來(lái)是想進(jìn)行一次比較!
顯而易見,如果 a 和 b 是內(nèi)建數(shù)據(jù)類型,那么這樣的代碼就是非法的。避免與內(nèi)建數(shù)據(jù)類型不必要的沖突,這是一個(gè)優(yōu)秀的用戶自定義類型的設(shè)計(jì)標(biāo)準(zhǔn)之一(另請(qǐng)參見第 18 項(xiàng)),而允許為兩數(shù)乘積賦值這讓人看上去就很不必要。聲明 operator* 函數(shù)時(shí)如果讓其返回一個(gè) const 型數(shù)據(jù)則可以避免這一沖突,這便是要這樣做的原因所在。
const 的參數(shù)沒有什么特別新鮮的——它們與局部 const 對(duì)象的行為基本一致,你在必要的時(shí)候要盡可能使用它們。除非你需要更改某個(gè)參數(shù)或者局部對(duì)象,其余的所有情況最好都聲明為 const 。這僅僅需要你多打六個(gè)字母,但是它可以使你從惱人的錯(cuò)誤(比如我們剛才見到的“我本想打‘ == ’但是卻打了‘ = ’”)中解放出來(lái)。
const 成員函數(shù)
對(duì)成員函數(shù)使用 const 的目的是指明這些成員函數(shù)可以被 const 對(duì)象調(diào)用。這一類成員函數(shù)是很重要的,首先,它們使得類的接口更加易于理解。很有必要了解哪些函數(shù)可以修改而哪些不可以。其次,這些函數(shù)可以與 const 對(duì)象協(xié)同工作。這對(duì)于高效編碼是十分重要的一方面,這是由于(將在第 20 項(xiàng)中展開解釋)提高 C++ 程序性能的一條最基本的途徑就是:傳遞對(duì)象的 const 引用。使用這一技 術(shù)需要一個(gè)前提:這就是首先要有 const 成員函數(shù)存在,并且它們用于處理之前生成的 const 對(duì)象。
如果若干成員函數(shù)之間的區(qū)別僅僅為“是否是 const 的”,那么它們也可以被重載。很多人都忽略了這一點(diǎn),但是這是 C++ 重要特征之一。請(qǐng)觀察下面的代碼,這是一個(gè)文字塊的類:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
// operator[] 用于返回相應(yīng)位置的字符
{ return text[position]; } // 返回一個(gè) const 對(duì)象
char& operator[](std::size_t position)
// operator[] 用于返回相應(yīng)位置的字符
{ return text[position]; } // 返回一個(gè)非 const 對(duì)象
private:
std::string text;
};
TextBlock 的 operator[] 可以這樣使用:
TextBlock tb("Hello");
std::cout << tb[0]; // 調(diào)用非 const 的 TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0]; // 調(diào)用 const 的 TextBlock::operator[]
順便說(shuō)一下,在真實(shí)的程序中, const 對(duì)象在大多數(shù)情況下都以“通過指針傳遞”或“引用一個(gè) const ”的形式出現(xiàn)。 上面的 ctb 的例子純粹是人為的,而下面的例子在真實(shí)狀況中常會(huì)出現(xiàn):
void print(const TextBlock& ctb) // 在這個(gè)函數(shù)中 ctb 是 const 的
{
std::cout << ctb[0]; // 調(diào)用 const 的 TextBlock::operator[]
...
}
通過對(duì) operator[] 的重載以及為每個(gè)版本提供不同類型的返回值,你便可以以不同的方式處理 const 的或者非 const 的 TextBlock :
std::cout << tb[0]; // 正確:讀入一個(gè)非 const 的 TextBlock
tb [0] = 'x'; // 正確:改寫一個(gè)非 const 的 TextBlock
std::cout << ctb[0]; // 正確:讀入一個(gè) const 的 TextBlock
ctb [0] = 'x'; // 錯(cuò)誤 ! 不能改寫 const 的 TextBlock
請(qǐng)注意,這一錯(cuò)誤只與所調(diào)用的 operator[] 的返回值的類型有關(guān),如果僅僅調(diào)用 operator[] 本身則不會(huì)出現(xiàn)任何問題。錯(cuò)誤出現(xiàn)在:企圖為一個(gè) const char& 賦值,而 const char& 則是 operator[] 的 const 版本的返回值類型。
同時(shí)還要注意的是,非 const 的 operator[] 的返回值類型是一個(gè) char 的引用,而不是 char 本身。如果 operator[] 真的簡(jiǎn)單的返回一個(gè) char ,那么下面的語(yǔ)句將不能正確編譯:
這是因?yàn)椋髨D修改一個(gè)返回內(nèi)建數(shù)據(jù)類型的函數(shù)的返回值根本都是非法的。即使假設(shè)這樣做合法,而 C++ 是 通過傳值返回對(duì)象的,所修改的僅僅是由 tb.text[0] 復(fù)制出的一份副本,而不是 tb.text[0] 本身,你不會(huì)得到預(yù)期的效果。
讓我們暫停一小會(huì)兒,來(lái)考慮一下這里邊的哲學(xué)問題。把一個(gè)成員函數(shù)聲明為 const 的有什么涵義呢?這里有兩個(gè)流行的說(shuō)法:按位恒定(也可叫做物理恒定)和邏輯恒定。
按位 恒定陣營(yíng)堅(jiān)信:當(dāng)且僅當(dāng)一個(gè)成員函數(shù)對(duì)于所有對(duì)象的數(shù)據(jù)成員( static 數(shù)據(jù)成員除外)都不做出改動(dòng)時(shí),才需要將這一成員函數(shù)聲明為 const 的,換句話說(shuō),將成員函數(shù)聲明為 const 的條件是:成員函數(shù)不對(duì)對(duì)象內(nèi)部做任何的改動(dòng)。按位恒定的好處之一就是,它使得錯(cuò)誤檢查便得更輕松:編譯器僅需要查找對(duì)數(shù)據(jù)成員的賦值。實(shí)際上,按位恒定就是 C++ 對(duì)于恒定的定義,如果一個(gè) const 的成員函數(shù)調(diào)用了某個(gè)對(duì)象,那么即使該對(duì)象擁有非靜態(tài)數(shù)據(jù)成員,其所有數(shù)據(jù)成員也都是不可修改的。
不幸的是,大多數(shù)不完全是 const 的成員函數(shù)也可以通過按位恒定的檢驗(yàn)。在特定的情況下,如果一個(gè)成員函數(shù)頻繁的修改一個(gè)指針所指的位置,那么我們說(shuō)它就不是一個(gè) const 的成員函數(shù)。但是只要這個(gè)指針存在于一個(gè)對(duì)象中,這個(gè)函數(shù)就是按位恒定的,這時(shí)候編譯器不會(huì)報(bào)錯(cuò)。這樣會(huì)導(dǎo)致編成的行為不符合常規(guī)習(xí)慣。比如說(shuō),我們手頭有一個(gè)類似于 TextBlock 的類,其中保存著 char* 類型的數(shù)據(jù)而不是 string ,因?yàn)檫@段代碼有可能要與一些 C 語(yǔ)言的 API 交互,但是 C 語(yǔ)言中沒有 string 對(duì)象一說(shuō)。
class CTextBlock {
public:
...
char& operator[](std::size_t position) const
// operator[] 不恰當(dāng)?shù)?/font> (但是符合按位恒定規(guī)則)定義方法
{ return pText[position]; }
private:
char *pText;
};
盡管 operator[] 返回一個(gè)對(duì)對(duì)象內(nèi)部數(shù)據(jù)的引用,這個(gè)類仍(不恰當(dāng)?shù)兀⑵渎暶鳛?/span> const 的 成員函數(shù)(第 28 項(xiàng)將深入討論這個(gè)問題)。先忽略這個(gè)問題,請(qǐng)注意 operator[] 的實(shí)現(xiàn)中并沒有以任何形式修改 pText 。于是編譯器便會(huì)欣然接受這樣的做法,畢竟,所有的編譯器所檢查的是“代碼是否符合按位恒定規(guī)則”。但是請(qǐng)觀察,在編譯器的縱容下,還會(huì)有什么樣的事情發(fā)生:
const CTextBlock cctb("Hello");// 聲明對(duì)象常量
char *pc = &cctb[0]; // 調(diào)用 const 的 operator[]
// 從而得到一個(gè)指向 cctb 中數(shù)據(jù)的指針
*pc = 'J'; // cctb 現(xiàn)在的值為 "Jello"
當(dāng)你創(chuàng)建了一個(gè)包含具體值的對(duì)象常量后,你僅僅通過對(duì)其調(diào)用 const 的成員函數(shù),就可以改變它的值!這顯然是有問題的。
輯恒定應(yīng)運(yùn)而生。堅(jiān)持這一宗旨的人們爭(zhēng)論道,一個(gè) const 的成員函數(shù)可能對(duì)其調(diào)用的對(duì)象內(nèi)部做出改動(dòng),但是僅僅以客戶端無(wú)法察覺的方式進(jìn)行。比如說(shuō),你的 CTextBlock 類可能需要保存文字塊的長(zhǎng)度,以便在需要的時(shí)候調(diào)用:
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
std::size_t textLength; // 最后一次計(jì)算出的文字塊長(zhǎng)度
bool lengthIsValid; // 當(dāng)前長(zhǎng)度是否可用
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // 錯(cuò)誤!不能在 const 成員函數(shù)中
lengthIsValid = true; // 對(duì) textLength 和 lengthIsValid 賦值
}
return textLength;
}
以上 length 的實(shí)現(xiàn)絕不是按位恒定的。這是因?yàn)?/span> textLength 和 lengthIsValid 都可以改動(dòng)。盡管看上去它應(yīng)該對(duì)于 CTextBlock 對(duì)象常量可用,但是編譯器不答應(yīng)。編譯器始終堅(jiān)持遵守按位恒定。那么該怎么辦呢?
解決方法很簡(jiǎn)單:利用 C++ 中 與 const 相關(guān)的靈活性,使用可變的( mutable )數(shù)據(jù)成員。 mutable 可以使非靜態(tài)數(shù)據(jù)成員不受按位恒定規(guī)則的約束:
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
mutable std::size_t textLength;// 這些數(shù)據(jù)成員在任何情況下均可修改
mutable bool lengthIsValid; // 在 const 成員函數(shù)中也可以
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // 現(xiàn)在可以修改了
lengthIsValid = true; // 同上
}
return textLength;
}
避免 const 與非 const 成員函數(shù)之間的重復(fù)
mutable 對(duì)于“我不了解按位恒定”的情況不失為一個(gè)良好的解決方案,但是它對(duì)于所有的 const 難題并不能做到一勞永逸。舉例說(shuō), TextBlock (以及 CTextBlock )中的 operator[] 不僅僅返回一個(gè)對(duì)恰當(dāng)字符的引用,同時(shí)還要進(jìn)行邊界檢查、記錄訪問信息,甚至還要進(jìn)行數(shù)據(jù)完整性檢測(cè)。如果將所有這些統(tǒng)統(tǒng)放在 const 或非 const 函數(shù)(我們現(xiàn)在會(huì)得到過于冗長(zhǎng)的隱式內(nèi)聯(lián)函數(shù),不過不要驚慌,在第 30 項(xiàng)中這個(gè)問題會(huì)得到解決)中,看看我們會(huì)得到什么樣的龐然大物:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... // 邊界檢查
... // 記錄數(shù)據(jù)訪問信息
... // 確認(rèn)數(shù)據(jù)完整性
return text[position];
}
char& operator[](std::size_t position)
{
... // 邊界檢查
... // 記錄數(shù)據(jù)訪問信息
... // 確認(rèn)數(shù)據(jù)完整性
return text[position];
}
private:
std::string text;
};
噢!天哪,這讓人頭疼:重復(fù)代碼,以及隨之而來(lái)的編譯時(shí)間增長(zhǎng)、維護(hù)成本增加、代碼膨脹、等等……當(dāng)然,像邊界檢查這一類代碼是可以移走的,它們可以單獨(dú)放在一個(gè)成員函數(shù)(當(dāng)然是私有的)中,然后讓這兩個(gè)版本的 operator[] 來(lái)調(diào)用它,但是你的代碼仍然有重復(fù)的函數(shù)調(diào)用,以及重復(fù)的 return 語(yǔ)句。
對(duì)于 operator[] 你真正需要的是:一次實(shí)現(xiàn),兩次使用。也就是說(shuō),你需要一個(gè)版本的 operator[] 來(lái)調(diào)用另一個(gè)。這樣便可以通過轉(zhuǎn)型來(lái)消去函數(shù)的恒定性。
通常情況下轉(zhuǎn)型是一個(gè)壞主意,后邊我將專門用一項(xiàng)來(lái)告訴你為什么不要使用轉(zhuǎn)型(第 21 項(xiàng)),但是代碼重復(fù)也不會(huì)讓人感到有多輕松。在這種情況下, const 版的 operator[] 與非 const 版的 operator[] 所做的事情完全相同,不同的僅僅是它的返回值是 const 的。通過轉(zhuǎn)型來(lái)消去返回值的恒定性是安全的,這是因?yàn)槿魏稳苏{(diào)用這一非 const 的 operator[] 首先必須擁有一個(gè)非 const 的對(duì)象,否則它就不能調(diào)用非 const 函數(shù)。所以盡管需要一次轉(zhuǎn)型,在 const 的 operator[] 中調(diào)用非 const 版本,可以安全地避免代碼重復(fù)。下面是實(shí)例代碼,讀完后邊的文字解說(shuō)你會(huì)更明了。
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // 同上
{
...
...
...
return text[position];
}
char& operator[](std::size_t position) // 現(xiàn)在僅調(diào)用 const 的 op[]
{
return
const_cast<char&>( // 通過對(duì) op[] 的返回值進(jìn)行轉(zhuǎn)型,消去 const ;
static_cast<const TextBlock&>(*this)// 為 *this 的類型添加 const ;
[position]; // 調(diào)用 const 版本的 op[]
);
}
...
};
就像你所看到的,上面的代碼進(jìn)行了兩次轉(zhuǎn)型,而不是一次。我們要讓非 const 的 operator[] 去調(diào)用 const 版本的,但是如果在非 const 的 operator[] 的內(nèi)部,我們只調(diào)用 operator[] 而不標(biāo)明 const ,那么函數(shù)將對(duì)自己進(jìn)行遞歸調(diào)用。那將是成千上萬(wàn)次的毫無(wú)意義的操作。為了避免無(wú)窮遞歸的出現(xiàn),我們必須要指明我們要調(diào)用的是 const 版本的 operator[] ,但是手頭并沒有直接的辦法。我們可以用 *this 從 TextBlock& 轉(zhuǎn)型到 const TextBlock& 來(lái)取代。是的,我們使用了一次轉(zhuǎn)型添加了一個(gè) const !這樣我們就進(jìn)行了兩次轉(zhuǎn)型:一次為 *this 添加了 const (于是對(duì)于 operator[] 的調(diào)用將會(huì)正確地選擇 const 版本),第二次轉(zhuǎn)型消去了 const operator[] 返回值中的 const 。
添加 const 的那次轉(zhuǎn)型是為了保證轉(zhuǎn)換工作的安全性(從一個(gè)非 const 對(duì)象轉(zhuǎn)換為一個(gè) const 的),這項(xiàng)工作的關(guān)鍵字是 static_cast 。消去 const 的工作只可以通過 const_cast 來(lái)完成,所以在這里我們實(shí)際上并沒有其他的選擇。(從技術(shù)上講,我們有。 C 語(yǔ) 言風(fēng)格的轉(zhuǎn)型在這里也能工作,但是,就像我在第 27 項(xiàng)中所講的,這一類轉(zhuǎn)型在很多情況下都不是好的選擇。如果你對(duì)于 static_cast 和 const_cast 還不熟悉,第 27 項(xiàng)中有詳細(xì)的介紹。)
在眾多的示例中,我們最終選擇了一個(gè)運(yùn)算符來(lái)進(jìn)行演示,因此上面的語(yǔ)法顯得有些古怪。這些代碼可能不會(huì)贏得任何選美比賽,但是通過以 const 版本的形式實(shí)現(xiàn)非 const 版本的 operator[] ,可以避免代碼重復(fù),這正是我們所期望的。為達(dá)到這一目標(biāo)而寫下看似笨拙的代碼,這樣做是否值得全看你的選擇,但是,以 const 版本的形式來(lái)實(shí)現(xiàn)非 const 的成員函數(shù)——了解這一技術(shù)肯定是值得的。
更值得你了解的是按反方向完成上面的工作——通過讓 const 版本的函數(shù)調(diào)用非 const 版本來(lái)避免代碼重復(fù)——一定不要這樣做。請(qǐng)記住,一個(gè) const 成員函數(shù)保證其對(duì)象永遠(yuǎn)不會(huì)更改其邏輯狀態(tài),但是一個(gè)非 const 的成員函數(shù)并沒有這一類的保證。如果你在一個(gè) const 函數(shù)中調(diào)用了一個(gè)非 const 函數(shù),曾保證不會(huì)被改動(dòng)的對(duì)象就有被修改的風(fēng)險(xiǎn)。這就是為什么說(shuō)讓一個(gè) const 函數(shù)調(diào)用一個(gè)非 const 函數(shù)是錯(cuò)誤的:對(duì)象有可能被修改。實(shí)際上,為了使代碼能夠得到編譯,你還需要使用一個(gè) const_cast 來(lái)消去 *this 的 const 屬性,顯然這是不必要的麻煩。上一段中相反的調(diào)用次序才是安全的:非 const 成員函數(shù)可以對(duì)一個(gè)對(duì)象做任何想做的事情,因此調(diào)用一個(gè) const 成員函數(shù)不會(huì)帶來(lái)任何風(fēng)險(xiǎn)。這就是為什么 static_cast 在沒有與 const 相關(guān)的危險(xiǎn)的情況下可以正常工作的原因。
就像本項(xiàng)最開始所說(shuō)的, const 是一個(gè)令人贊嘆的東西。對(duì)于指針和迭代器,以及指針、迭代器和引用所涉及的對(duì)象,函數(shù)的參數(shù)和返回值,局部變量,成員函數(shù)來(lái)說(shuō), const 都是一個(gè)強(qiáng)大的伙伴。只要可能就可以使用它。你會(huì)對(duì)你所做的事情感到高興的。
需要記住的
l 將一些東西聲明為 const 的可以幫助編譯器及時(shí)發(fā)現(xiàn)用法上的錯(cuò)誤。 const 針對(duì)對(duì)象作用于所有的作用域,針對(duì)函數(shù)參數(shù)和返回值、成員函數(shù)作用于整體。
l 編譯器嚴(yán)格遵守按位恒定規(guī)則,但是你應(yīng)該在需要時(shí)應(yīng)用邏輯恒定。
l 當(dāng) const 和非 const 成員函數(shù)的實(shí)現(xiàn)在本質(zhì)上相同時(shí),可以通過使用一個(gè)非 const 版本來(lái)調(diào)用 const 版本來(lái)避免代碼重復(fù)。