# Item 22: 將數據成員聲明為 private
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
好了,先公布一下計劃。首先,我們將看看為什么數據成員不應該聲明為 public。然后,我們將看到所有反對 public 數據成員的理由同樣適用于 protected 數據成員。這就導出了數據成員應該是 private 的結論,至此,我們就結束了。
那么,public 數據成員,為什么不呢?
我們從先從語法一致性開始(參見 Item 18)。如果數據成員不是 public 的,客戶訪問一個對象的唯一方法就是通過成員函數。如果在 public 接口中的每件東西都是一個函數,客戶就不必絞盡腦汁試圖記住當他們要訪問一個類的成員時是否需要使用圓括號。他們只要使用就可以了,因為每件東西都是一個函數。一生堅持這一方針,能節省很多撓頭的時間。
但是也許你不認為一致性的理由是強制性的。使用函數可以讓你更加精確地控制成員的可存取性的事實又怎么樣呢?如果你讓一個數據成員為 public,每一個人都可以讀寫訪問它,但是如果你使用函數去得到和設置它的值,你就能實現禁止訪問,只讀訪問和讀寫訪問。嘿嘿,如果你需要,你甚至可以實現只寫訪問:
```
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; // no access to this int
int readOnly; // read-only access to this int
int readWrite; // read-write access to this int
int writeOnly; // write-only access to this int
};
```
這種條分縷析的訪問控制很重要,因為多數數據成員需要被隱藏。每一個數據成員都需要一個 getter 和 setter 的情況是很罕見的。
還不相信嗎?那么該拿出一門重炮了:封裝。如果你通過一個函數實現對數據成員的訪問,你可以在以后用一個計算來替換這個數據成員,使用你的類的人不會有任何察覺。
例如,假設你為一個監視通過的汽車的速度的自動設備寫一個應用程序。每通過一輛汽車,它的速度就被計算,而且那個值要加入到迄今為止收集到的所有速度數據的集合中:
```
class SpeedDataCollection {
...
public:
void addValue(int speed); // add a new data value
double averageSoFar() const; // return average speed
...
};
```
現在考慮成員函數 averageSoFar 的實現:實現它的辦法之一是在類中用一個數據成員來實時變化迄今為止收集到的所有速度數據的平均值。無論何時 averageSoFar 被調用,它只是返回那個數據成員的值。另一個不同的方法是在每次調用 averageSoFar 時重新計算它的值,通過分析集合中每一個數據值它能做成這些事情。
第一種方法(保持一個實時變化的值)使每一個 SpeedDataCollection 對象都比較大,因為你必須為持有實時變化的平均值,累計的和以及數據點的數量分配空間。可是,averageSoFar 能實現得非常高效,它僅僅是一個返回實時變化的平均值的 inline 函數(參見 Item 30)。反過來,無論何時被請求都要計算平均值使得 averageSoFar 的運行比較慢,但是每一個 SpeedDataCollection 對象都比較小。
誰能說哪一個最好?在內存非常緊張的機器(例如,一個嵌入式道旁設備)上,以及在一個很少需要平均值的應用程序中,每次都計算平均值可能是較好的解決方案。在一個頻繁需要平均值的應用程序中,速度是基本的要求,而且內存不成問題,保持一個實時變化的平均值更為可取。這里的重點在于通過經由一個成員函數訪問平均值(也就是說,通過將它封裝),你能互換這兩個不同的實現(也包括其他你可能想到的),對于客戶,最多也就是必須重新編譯。(你可以用在后面的 Item 31 中記述的技術來消除這個麻煩。)
將數據成員隱藏在功能性的接口之后能為各種實現提供彈性。例如,它可以在讀或者寫的時候很簡單地通報其他對象,可以檢驗類的不變量以及函數的前置或后置條件,可以在多線程環境中執行同步任務,等等。從類似 Delphi 和 C# 的語言來到 C++ 的程序員會認同這種類似那些語言中的“屬性”的等價物的功能,雖然需要附加一個帶圓括號的額外的 set。
關于封裝的要點可能比它最初顯現出來的更加重要。如果你對你的客戶隱藏你的數據成員(也就是說,封裝它們),你就能確保類的不變量總能被維持,因為只有成員函數能影響它們。此外,你預留了以后改變你的實現決策的權力。如果你不隱藏這樣的決策,你將很快發現,即使你擁有一個類的源代碼,你改變任何一個 public 的東西的能力也是非常有限的,因為有太多的客戶代碼將被破壞。public 意味著沒有封裝,而且幾乎可以說,沒有封裝意味著不可改變,尤其是被廣泛使用的類。但是仍然被廣泛使用的類大多數都是需要封裝的,因為它們可以從用一種更好的實現替換現有實現的能力中獲得最多的益處。
反對 protected 數據成員的理由是類似的。實際上,它是一樣的,雖然起先看起來似乎不那么清楚。關于語法一致性和條分縷析的訪問控制的論證就像用于 public 一樣可以應用于 protected,但是關于封裝又如何呢?難道 protected 數據成員不比 public 數據成員更具有封裝性嗎?實話實說,令人驚訝的答案是它們不。
Item 23 解釋了如果某物發生了變化,某物的封裝與可能被破壞的代碼數量成反比。于是,如果數據成員發生了變化(例如,如果它被從類中移除(可能是為了替換為計算,就像在上面的 averageSoFar 中)),數據成員的封裝性與可能被破壞的代碼數量成反比。
假設我們有一個 public 數據成員,隨后我們消除了它。有多少代碼會被破壞呢?所有使用了它的客戶代碼,其數量通常大得難以置信。從而 public 數據成員就是完全未封裝的。但是,假設我們有一個 protected 數據成員,隨后我們消除了它。現在有多少代碼會被破壞呢?所有使用了它的派生類,典型情況下,代碼的數量還是大得難以置信。從而 protected 數據成員就像 public 數據成員一樣沒有封裝,因為在這兩種情況下,如果數據成員發生變化,被破壞的客戶代碼的數量都大得難以置信。這并不符合直覺,但是富有經驗的庫實現者會告訴你,這是千真萬確的。一旦你聲明一個數據成員為 public 或 protected,而且客戶開始使用它,就很難再改變與這個數據成員有關的任何事情。有太多的代碼不得不被重寫,重測試,重文檔化,或重編譯。從封裝的觀點來看,實際只有兩個訪問層次:private(提供了封裝)與所有例外(沒有提供封裝)。
Things to Remember
聲明數據成員為 private。它為客戶提供了訪問數據的語法層上的一致,提供條分縷析的訪問控制,允許不變量被強制,而且為類的作者提供了實現上的彈性。
protected 并不比 public 的封裝性強。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射