揭開私有繼承的面紗

 

 什麼是私有繼承?以前在學校學習的時候,冥冥乎知道有這樣一個東西,卻沒有仔細研究過。後來工作中用到Boost庫纔開始瞭解它。如果說保護繼承大多是爲了語言完整性的話,私有繼承還是有一些用途的。

私有繼承 vs 公有繼承

公有繼承繼承的是接口與實現,它表示了類與類之間is-a的關係。而私有繼承繼承的僅僅是實現,它表示了has-a(或者 is-implemented-in-terms-of)的關係


在公有繼承中,基類的公有成員在派生類中仍然公有,基類擁有的接口完整無缺地傳給了派生類,也就是說基類對象出現的每一個地方都可以用派生類對象替換(is-a)。因此,編譯器可以安全地把派生類對象的引用、指針隱式轉換爲基類對象的引用、指針。而在私有繼承中,基類的公有和保護成員在派生類中變成了私有,從外界來看,派生類對象不再擁有基類的行爲。因此,編譯器不會做類似的轉換。同樣,對象切片(object slicing)也只有在公有繼承中才會出現。

  1. class Base
  2. {
  3. public:
  4. void f() { cout << "Base::f\n"; }
  5. };
  6. class Derived1: public Base
  7. {};
  8. class Derived2: private Base
  9. {};
  10. void method(Base &b)
  11. {
  12. b.f();
  13. }
  14. int main()
  15. {
  16. Base b;
  17. // 基類對象,ok
  18. method(b);
  19. Derived1 d1;
  20. // 公有派生類對象,隱式轉換成基類對象,也ok
  21. method(d1);
  22. Derived2 d2;
  23. // 私有派生類對象,無法轉換成基類對象,編譯出錯
  24. method(d2);
  25. }
class Base
{
public:
   void f() { cout << "Base::f\n"; }
};

class Derived1: public Base
{};

class Derived2: private Base
{};

void method(Base &b)
{
   b.f();
}

int main()
{
   Base b;
   // 基類對象,ok
   method(b);
   Derived1 d1;
   // 公有派生類對象,隱式轉換成基類對象,也ok
   method(d1);
   Derived2 d2;
   // 私有派生類對象,無法轉換成基類對象,編譯出錯
   method(d2);
}
私有繼承的本質

私有繼承隱藏了基類的接口,但這些被隱藏的函數在派生類的成員函數中是可以調用的。所以,你在實現派生類函數的時候可以調用基類對象函數來完成部分功能(is-implemented-in-terms-of)。下面這個例子中,企鵝類私有繼承了鳥類。沒有公有繼承是因爲企鵝並不是嚴格意義上的鳥,因爲它們不會飛。如果我們要求所有的企鵝在走完路以後都要蹦一下,我們完全可以重用基類的Bird::walk來完成走路這個子過程。

  1. class Bird
  2. {
  3. public:
  4. void fly();
  5. void walk();
  6. };
  7. class Penguin: private Bird
  8. {
  9. public:
  10. // walk()作爲函數實現,其本身對於外界不可見
  11. void jumpAfterWalk() { walk(); jump(); }
  12. private:
  13. void jump();
  14. };
class Bird
{
public:
   void fly();
   void walk();
};

class Penguin: private Bird
{
public:
   // walk()作爲函數實現,其本身對於外界不可見
   void jumpAfterWalk() { walk(); jump(); }
private:
   void jump();
};
私有繼承 vs 組合

實際上,私有繼承只是實現 has-a 的方式之一,對象組合(composition/aggregation/containment)同樣可以達到相同的目的。還是考慮企鵝的例子,鳥類對象作爲了企鵝類的一個私有成員,企鵝的行走動作是通過調用該私有成員(b)的公有成員函數(walk)來完成的。企鵝和鳥對象之間是一種包含關係。

  1. class Bird
  2. {
  3. public:
  4. void fly();
  5. void walk();
  6. };
  7. class Penguin
  8. {
  9. public:
  10. void jumpAfterWalk() { b.walk(); jump(); }
  11. private:
  12. void jump();
  13. Bird b;
  14. };
class Bird
{
public:
   void fly();
   void walk();
};

class Penguin
{
public:
   void jumpAfterWalk() { b.walk(); jump(); }
private:
   void jump();
   Bird b;
};
同樣是 has-a 的關係,它們兩個各有什麼優缺點呢?

組合的優點

(一)組合相對於繼承來說耦合度較小,特別是當Penguin類hold了一個Bird對象的指針而非對象時。這時候包含Penguin定義的頭文件甚至都不用包含Bird.h而只用一個向前聲明即可。這樣的好處是,當Bird的內部實現發生變化時,Penguin類不需要重新編譯。這在大型項目中非常重要。
  1. // bird.h
  2. class Bird
  3. {..};
  4. // penguin.h
  5. class Bird;
  6. class Penguin
  7. {
  8. ..
  9. private:
  10. Bird *b;
  11. };
// bird.h
class Bird
{..};

// penguin.h
class Bird;
class Penguin
{
..
private:
   Bird *b;
};
(二)Penguin類可以同時有多個Bird類的對象成員b1,b2.. 繼承是無法做到這點的。

私有繼承的優點

(一)你要繼承一個類,但只希望保留其中的部分公用接口。首先,公有繼承肯定不合適,因爲基類接口必須全部保留。用組合可以實現,但是由於組合不會把子對象的接口暴露出來,你需要重新定義那些你希望保留的接口。爲了實現接口,你需要調用子對象的相應函數。相比較之下,私有繼承就簡單很多。因爲基類的接口派生類都有,只不過是私有的,重新把它們聲明爲公有即可。比如下面這個例子,派生類只希望暴露f1和f2這兩個接口。
  1. class Base
  2. {
  3. public:
  4. void f1();
  5. void f2();
  6. void f3();
  7. };
  8. // 使用私有繼承,暴露部分接口的工作變得非常簡單
  9. class Derived1: private Base
  10. {
  11. public:
  12. using Base::f1;
  13. using Base::f2;
  14. };
  15. // 使用組合,稍微複雜些
  16. class Derived2
  17. {
  18. public:
  19. void f1() { b.f1(); }
  20. void f2() { b.f2(); }
  21. private:
  22. Base b;
  23. };
class Base
{
public:
   void f1();
   void f2();
   void f3();
};

// 使用私有繼承,暴露部分接口的工作變得非常簡單
class Derived1: private Base
{
public:
   using Base::f1;
   using Base::f2;
};

// 使用組合,稍微複雜些
class Derived2
{
public:
   void f1() { b.f1(); }
   void f2() { b.f2(); }
private:
   Base b;
};
(二)如果你希望重新定義一個類的虛函數,那麼只好用私有繼承。這點並沒有看上去那麼容易理解:由於是私有繼承,怎樣才能實現運行時多態呢?畢竟,基類的指針指向派生類對象是不允許的。如果不能調到派生類重新定義的虛函數,我們實現它又有什麼意義呢?下面這個例子給出了一種可能的模型。
  1. class Base
  2. {
  3. public:
  4. void f1() { vf(); } // 相當於this->vf(),運行時多態
  5. protected:
  6. virtual void vf() { cout << "Base::vf()\n"; }
  7. };
  8. class Derived: private Base
  9. {
  10. public:
  11. void f2() { f1(); }
  12. private:
  13. virtual void vf() { cout << "Derived::vf()\n"; }
  14. };
  15. int main()
  16. {
  17. Derived d;
  18. d.f2(); // 打印 "Derived::vf()"
  19. }
class Base
{
public:
   void f1() { vf(); } // 相當於this->vf(),運行時多態
protected:
   virtual void vf() { cout << "Base::vf()\n"; }
};

class Derived: private Base
{
public:
   void f2() { f1(); }
private:
   virtual void vf() { cout << "Derived::vf()\n"; }
};

int main()
{
   Derived d;
   d.f2(); // 打印 "Derived::vf()"
}
(三)如果要保證一個類不具有拷貝屬性(例如mutex,數據庫連接等),應該怎麼做?一種方法是把拷貝構造函數、賦值運算符私有化。但還是有一個問題,類的成員函數還是可以調用它們。一個更好的辦法是繼承boost::noncopyable。只有當有對象拷貝動作的時候,編譯器纔會合成出拷貝構造函數等,這時候編譯出錯,因爲基類的拷貝函數無法訪問;當沒有拷貝動作時,不會生成拷貝函數,一切正常。注意,由於noncopyable的構造析構是保護屬性,組合方式不可行,只能使用繼承。雖然私有和公有繼承技術上都可行(因爲所有noncopyable的函數都已經不是公有了),但私有繼承更好,因爲這裏正是一個is-implemented-in-terms-of而非 is-a 的關係。
  1. class noncopyable
  2. {
  3. protected:
  4. noncopyable() {}
  5. ~noncopyable() {}
  6. private: // emphasize the following members are private
  7. noncopyable( const noncopyable& );
  8. const noncopyable& operator=( const noncopyable& );
  9. };
  10. class MyClass: private boost::noncopyable
  11. {..}
  12. int main
  13. {
  14. MyClass d1, d2;
  15. d1 = d2; // 編譯出錯
  16. }
class noncopyable
{
protected:
   noncopyable() {}
   ~noncopyable() {}
private:  // emphasize the following members are private
   noncopyable( const noncopyable& );
   const noncopyable& operator=( const noncopyable& );
};

class MyClass: private boost::noncopyable
{..}

int main
{
   MyClass d1, d2;
   d1 = d2; // 編譯出錯
}

(四)最後,當你需要用到一個類保護成員的時候,不得不用私有繼承。因爲保護成員對於外界不可見,但對於派生類可見。

虛析構函數的思考

當私有繼承一個基類時,派生類對象不再是基類對象,編譯器也不會執行隱式類型轉換,因此也不會出現通過基類指針釋放派生類對象的問題。所以,如果你寫了一個類不打算做基類,或者只打算作爲以後派生類的實現(私有繼承),那麼大可不必有虛析構函數。上面的 boost::noncopyable 就是一個例子。

小結:

  1. 私有繼承中,基類所有的成員在派生類中都爲私有。編譯器也因此不會做從派生類到基類的隱式類型轉換。
  2. 公有繼承表示 is-a 的關係,私有繼承表示has-a is-implemented-in-terms-of 的關係。
  3. 私有繼承和組合都表示 has-a 關係,各有千秋。一般來說,能用組合儘量用組合,因爲它鬆耦合,維護也比較簡單直觀。當必須使用私有繼承時才使用它。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章