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