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