揭开私有继承的面纱

 

  什么是私有继承?以前在学校学习的时候,冥冥乎知道有这样一个东西,却没有仔细研究过。后来工作中用到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 关系,各有千秋。一般来说,能用组合尽量用组合,因为它松耦合,维护也比较简单直观。当必须使用私有继承时才使用它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值