Item 32提出public繼承表示is-a的關系,這是因為編譯器會在需要的時候將子類對象隱式轉換為父類對象。 然而private繼承則不然:
class Person { ... };
class Student: private Person { ... }; // inheritance is now private
void eat(const Person& p); // anyone can eat
Person p; // p is a Person
Student s; // s is a Student
eat(p); // fine, p is a Person
eat(s); // error! a Student isn't a Person
Person
可以eat
,但Student
卻不能eat
。這是private繼承和public繼承的不同之處:
子類繼承了父類的實現,而沒有繼承任何接口(因為public成員都變成private了)。 因此private繼承是軟件實現中的概念,與軟件設計無關。 private繼承和對象組合類似,都可以表示is-implemented-in-terms-with的關系。那麼它們有什麼區別呢? 在面向對象設計中,對象組合往往比繼承提供更大的靈活性,只要可以使用對象組合就不要用private繼承。
我們的Widget
類需要執行周期性任務,於是希望繼承Timer
的實現。 因為Widget
不是一個Timer
,所以我們選擇了private繼承:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // automatically called for each tick
};
class Widget: private Timer {
private:
virtual void onTick() const; // look at Widget usage data, etc.
};
在Widget
中重寫虛函數onTick
,使得Widget
可以周期性地執行某個任務。為什麼Widget
要把onTick
聲明為private
呢? 因為onTick
只是Widget
的內部實現而非公共接口,我們不希望客戶調用它(Item 18指出接口應設計得不易被誤用)。
private繼承的實現非常簡單,而且有時只能使用private繼承:
Widget
需要訪問Timer
的protected成員時。因為對象組合後只能訪問public成員,而private繼承後可以訪問protected成員。Widget
需要重寫Timer
的虛函數時。比如上面的例子中,由於需要重寫onTick
單純的對象組合是做不到的。
我們知道對象組合也可以表達is-implemented-in-terms-of的關系, 上面的需求當然也可以使用對象組合的方式實現。但由於需要重寫(override)Timer
的虛函數,所以還是需要一個繼承關系的:
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
內部類WidgetTimer
public繼承自Timer
,然後在Widget
中保存一個WidgetTimer
對象。 這是public繼承+對象組合的方式,比private繼承略為復雜。但對象組合仍然擁有它的好處:
Widget
的子類重定義onTick
。在Java中可以使用finel
關鍵字,在C#中可以使用sealed
。 在C++中雖然沒有這些關鍵字,但你可以使用public繼承+對象組合的方式來做到這一點。上述例子便是。Widget
和Timer
的編譯依賴。如果是private繼承,在定義Widget
的文件中勢必需要引入#includetimer.h
。 但如果采用對象組合的方式,你可以把WidgetTimer
放到另一個文件中,在Widget
中保存WidgetTimer
的指針並聲明WidgetTimer
即可, 見Item 31。
我們講雖然對象組合優於private繼承,但有些特殊情況下仍然可以選擇private繼承。 需要EBO(empty base optimization)的場景便是另一個特例。 由於技術原因,C++中的獨立空對象也必須擁有非零的大小,請看:
class Empty {};
class HoldsAnInt {
private:
int x;
Empty e;
};
Empty e
是一個空對象,但你會發現sizeof(HoldsAnInt) > sizeof(int)
。 因為C++中獨立空對象必須有非零大小,所以編譯器會在Empty
裡面插入一個char
,這樣Empty
大小就是1。 由於字節對齊的原因,在多數編譯器中HoldsAnInt
的大小通常為2*sizeof(int)
。更多字節對齊和空對象大小的討論見Item 7。 但如果你繼承了Empty
,情況便會不同:
class HoldsAnInt: private Empty {
private:
int x;
};
這時sizeof(HoldsAnInt) == sizeof(int)
,這就是空基類優化(empty base optimization,EBO)。 當你需要EBO來減小對象大小時,可以使用private繼承的方式。
繼承一個空對象有什麼用呢?雖然空對象不可以有非靜態成員,但它可以包含typedef, enum, 靜態成員,非虛函數 (因為虛函數的存在會導致一個徐函數指針,它將不再是空對象)。 STL就定義了很多有用的空對象,比如unary_function
, binary_function
等。