好吧,以下是我們的計(jì)劃:我們首先要分析為什么數(shù)據(jù)成員不應(yīng)該是公有的,然后繼續(xù)分析為什么數(shù)據(jù)成員也不能是protected的。然后就引出本條款的結(jié)論:數(shù)據(jù)成員必須是私有的。結(jié)論引出,計(jì)劃完成。
那么,數(shù)據(jù)成員為什么不能是public的?
讓我們從討論語(yǔ)義一致性問(wèn)題開(kāi)始(另請(qǐng)參見(jiàn)條目18)。如果數(shù)據(jù)成員不是公有的,那么客戶要想訪問(wèn)對(duì)象就只剩下成員函數(shù)一種方法。如果公有接口中所有的東西都是函數(shù),那么客戶在期望訪問(wèn)類(lèi)成員時(shí),由于一切都是函數(shù),所以就可以任意使用,而不用擔(dān)心是否需要使用括號(hào)。在整個(gè)過(guò)程中,這樣做可以讓你節(jié)省大量躊躇不定的時(shí)間。
但是也許你會(huì)發(fā)現(xiàn),并沒(méi)有強(qiáng)制規(guī)定來(lái)要求語(yǔ)義的一致性。那么你是否會(huì)發(fā)現(xiàn):使用函數(shù)可以讓你更精確地控制數(shù)據(jù)成員的訪問(wèn)權(quán)?如果把一個(gè)數(shù)據(jù)成員定義為public的,那么每個(gè)人對(duì)其都擁有“可讀可寫(xiě)”的訪問(wèn)權(quán),但是如果你使用函數(shù)來(lái)為數(shù)據(jù)成員賦值,或者獲取數(shù)據(jù)成員的值,那么你可以將其實(shí)現(xiàn)為“禁止訪問(wèn)”、“只讀”以及“可讀可寫(xiě)”幾種級(jí)別的訪問(wèn)權(quán);嘿,如果需要,你甚至可以將其實(shí)現(xiàn)為“只寫(xiě)”的訪問(wèn)權(quán):
class AccessLevels {
public:
...
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess; // 此int值禁止訪問(wèn)
int readOnly; // 此int值擁有只讀級(jí)別訪問(wèn)權(quán)
int readWrite; // 此int值擁有可讀可寫(xiě)級(jí)別訪問(wèn)權(quán)
int writeOnly; // 此int值擁有只寫(xiě)級(jí)別訪問(wèn)權(quán)
};
很有必要將訪問(wèn)權(quán)管理得如此有條不紊,因?yàn)樵S多數(shù)據(jù)成員本應(yīng)該被隱藏起來(lái)。并不是每個(gè)數(shù)據(jù)成員都需要一個(gè)取值器(getter)和一個(gè)賦值器(setter)。
還不是十分肯定?那么現(xiàn)在是時(shí)候使出殺手锏了:“封裝”。如果你通過(guò)程序?qū)崿F(xiàn)了對(duì)一個(gè)數(shù)據(jù)成員的訪問(wèn),那么你就可以使用一次計(jì)算來(lái)代替這個(gè)數(shù)據(jù)成員,使用這一個(gè)類(lèi)的人完全不會(huì)有所察覺(jué)。
請(qǐng)看下邊的示例,假設(shè)你正在為一種自動(dòng)裝置編寫(xiě)一個(gè)應(yīng)用程序,這一裝置可以監(jiān)視通過(guò)汽車(chē)的行駛速度,當(dāng)一輛汽車(chē)通過(guò)時(shí),這一應(yīng)用程序就會(huì)計(jì)算出它的速度,然后將這一數(shù)值保存到一個(gè)小型數(shù)據(jù)庫(kù)中,其中保存著曾通過(guò)所有車(chē)輛的速度數(shù)據(jù):
class SpeedDataCollection {
...
public:
void addValue(int speed); // 添加新的數(shù)據(jù)值
double averageSoFar() const; // 返回速度的平均值
...
};
現(xiàn)在請(qǐng)注意成員函數(shù)averageSoFar的具體實(shí)現(xiàn)問(wèn)題。一種實(shí)現(xiàn)方法是:為類(lèi)添加一個(gè)數(shù)據(jù)成員,讓它保存速度的平均值,隨數(shù)據(jù)庫(kù)的改動(dòng)更新這一成員的數(shù)值。當(dāng)調(diào)用averageSoFar時(shí),它僅僅返回這一數(shù)據(jù)成員的值。另一種做法是:在每次調(diào)用averageSoFar時(shí)都計(jì)算出這一平均值,此時(shí)需要檢查數(shù)據(jù)庫(kù)中所有的數(shù)據(jù)值。
因?yàn)榈谝环N手段(保存即時(shí)更新的平均值)中,你需要為保存即時(shí)更新平均值、累計(jì)總和以及數(shù)據(jù)的個(gè)數(shù)這幾種數(shù)據(jù)成員分配空間,因此這一方法使得SpeedDataCollection對(duì)象都變得更大一些。然而,averageSoFar卻十分的高效。可以把它寫(xiě)成一個(gè)內(nèi)聯(lián)函數(shù)(參見(jiàn)條目30),所做的僅僅是返回這一即時(shí)更新的。相反地,在需要時(shí)進(jìn)行計(jì)算,averageSoFar速度上會(huì)慢一些,但是SpeedDataCollection對(duì)象的體積更小。
二者孰優(yōu)孰劣,誰(shuí)又能斷定呢?在一個(gè)內(nèi)存較為局促的機(jī)器(比如嵌入式的公路設(shè)備)上,并且該應(yīng)用程序不會(huì)頻繁的調(diào)用平均值,那么實(shí)時(shí)計(jì)算的方案就更為優(yōu)秀。相反地,在平均值需要頻繁使用,速度是程序的關(guān)鍵,內(nèi)存不是問(wèn)題的情況下,則更應(yīng)采用保存一個(gè)即時(shí)平均值的方案。最重要的一點(diǎn)是,在通過(guò)成員函數(shù)訪問(wèn)平均值時(shí)(也就是“封裝”),你可以交替使用這兩種實(shí)現(xiàn)方案(當(dāng)然,你可能還會(huì)想到其它重要的問(wèn)題),客戶頂多要做的一件事就是重新編譯一下代碼。(即使編譯所帶來(lái)的不方便也可以排除。參見(jiàn)條目31中介紹的技術(shù)。)
將數(shù)據(jù)成員隱藏在函數(shù)式接口的背后可以使得任意種類(lèi)的實(shí)現(xiàn)方法更加靈活多變。比如說(shuō),這樣做可以非常容易地做到下面幾件事情:在數(shù)據(jù)成員進(jìn)行讀寫(xiě)操作時(shí)告知其他對(duì)象,驗(yàn)證類(lèi)的恒定性和函數(shù)運(yùn)行前后的狀態(tài),在多線程系統(tǒng)下進(jìn)行同步操作,等等。如果讓Delphi或C#的程序員使用C++,他們會(huì)發(fā)現(xiàn)C++這一特性與這些語(yǔ)言中的“屬性”很相像,只是C++中需要添加一對(duì)括號(hào)。
封裝是C++的一個(gè)博大精深的特性。如果你對(duì)客戶隱藏了數(shù)據(jù)成員的話(也就是將它們封裝起來(lái)),你就可以確保類(lèi)永遠(yuǎn)保持一致性,這是因?yàn)橹挥谐蓡T函數(shù)可以影響到數(shù)據(jù)成員,同時(shí)你也保留了在以后改變具體實(shí)現(xiàn)方法的權(quán)利。如果你不將這些方法隱藏起來(lái),那么你很快就會(huì)發(fā)現(xiàn),即使你擁有類(lèi)的源代碼,對(duì)公有接口的修改也是受到嚴(yán)格限制的,因?yàn)檫@樣做會(huì)破壞許多客戶端代碼。公有就意味著未封裝,同時(shí)從實(shí)用角度講,未封裝就意味著無(wú)法更改,較為廣泛應(yīng)用的類(lèi)更甚之。然而廣泛應(yīng)用的類(lèi)最需要使用封裝,因?yàn)樗鼈兛梢詮?#8220;具體實(shí)現(xiàn)可以不斷改良”這一點(diǎn)上獲得最大程度的收益。
上面的分析對(duì)于protected數(shù)據(jù)成員也適用。盡管二者乍看上去有一定的區(qū)別,但實(shí)際上它們是完全一致的。在使用public數(shù)據(jù)成員時(shí),我們分析了語(yǔ)意一致性問(wèn)題和訪問(wèn)權(quán)條理性問(wèn)題,這一分析過(guò)程對(duì)于使用protected數(shù)據(jù)同樣適用。但還有一個(gè)問(wèn)題——封裝。protected數(shù)據(jù)成員不是比public的更具有封裝性嗎?從實(shí)用角度講,你會(huì)得到一個(gè)令人吃驚的答案:不是。
條目23中將介紹這一問(wèn)題:C++中封裝程度與代碼的健壯程度(這段代碼相關(guān)部分被修改時(shí)抵御破壞的能力)成正比。因此,數(shù)據(jù)成員的封裝程度與代碼的健壯程度也是成正比的。比如,當(dāng)一個(gè)數(shù)據(jù)成員從類(lèi)中移除時(shí)(可能你期望使用一次計(jì)算來(lái)代替,就像上文中的averageSoFar一樣),代碼是否會(huì)遭到破壞,將取決于封裝程度。
請(qǐng)考慮這個(gè)問(wèn)題:假設(shè)我們有一個(gè)public數(shù)據(jù)成員,然后我們把它刪除了,那么將有多少的代碼將遭到破壞呢?我們說(shuō),所有使用它的客戶端代碼。這將是一個(gè)無(wú)法預(yù)知的巨大數(shù)字。公有數(shù)據(jù)成員就是這樣完全沒(méi)有封裝性的。但是繼續(xù)考慮:我們有一個(gè)protected數(shù)據(jù)成員,然后我們把它刪除了,此時(shí)將破壞多少代碼?我們說(shuō),所有使用它的派生類(lèi),這同樣是一個(gè)無(wú)法預(yù)知的巨大數(shù)字。由于在這兩種情況下,如果數(shù)據(jù)成員被更改了,那么將會(huì)為客戶帶來(lái)無(wú)法估量的損失,因此可以說(shuō)protected數(shù)據(jù)成員與public的一樣沒(méi)有封裝性。這是違背直覺(jué)的,但是有經(jīng)驗(yàn)的庫(kù)實(shí)現(xiàn)者會(huì)告訴你,這是千真萬(wàn)確的。一旦你聲明了一個(gè)public或protected的數(shù)據(jù)成員,然后客戶開(kāi)始使用它,你就很難再對(duì)這一數(shù)據(jù)成員做出修改。因?yàn)檫@樣做會(huì)帶來(lái)太多的代碼重寫(xiě)、重新測(cè)試重新編寫(xiě)文檔和重新編譯等等工作。按封裝的理念來(lái)說(shuō),對(duì)于數(shù)據(jù)成員僅僅存在兩個(gè)層次的訪問(wèn)權(quán),那就是:private(可以提供封裝性)和非private(不提供封裝性)。
時(shí)刻牢記
l 要將數(shù)據(jù)成員聲明為私有的。這樣可以讓客戶端訪問(wèn)數(shù)據(jù)時(shí)擁有一致的語(yǔ)義,提供有序的訪問(wèn)控制,強(qiáng)制類(lèi)保持一致性,為類(lèi)作者提供更高的靈活性。
l protected并不會(huì)帶來(lái)比public更高的封裝性。