《Effective C++》学习笔记——条款39

来源:互联网 发布:三菱伺服选型软件 编辑:程序博客网 时间:2024/05/26 02:51


六、继承与面向对象设计


条款39、明智而审慎的使用private继承




is-a? No!

在之前的条款32中,我们讨论了public继承,它是一个 is-a 关系。
此处,我们继续用那个例子…的一部分,进而阐述此条款

class Person { ... };class Student: private Person { ... };void eat(const Person& p);void study(const Student& s);Person p;Student s;eat(p);eat(s);

在这个例子中,我们用private继承。
当我们调用 eat(s) 时发现,会报错,所以,显然 private继承并不代表 is-a 关系。
因为,派生类对象不会被转换为基类对象;而且 派生类从基类继承而来的所有成员,都将成为private的形式。

implemented-in-terms-of 根据某物实现出

那private继承意味着什么呢? 在上一个条款也浅谈过,private继承实际上意味着 implemented-in-terms-of,就是 根据某物实现出。
- 我们让一个派生类,private形式继承基类,是为了采用基类的某些特性,并不是说它俩之间有什么关系。
- private继承 在设计层面上没有意义,只在软件实现层面上有意义。


private继承 与 复合

上一个条款刚指出,复合 其中一个意义也是 根据某物实现出。
对于这两者,有一个原则:

  • 尽可能的使用复合,必要时使用private继承

那么,什么时候算是必要的情况呢?

  • 当 protected 成员 或 virtual函数 相关
  • 当空间方面的利害关系足以踢翻private继承支柱
用 复合 而非 private继承

比如,我们程序中用到Widget类,我想知道Widget成员函数的使用次数,所以,我就要修改一下Widget类,让它记录每个成员函数的调用次数。
我们可以用timer类,因为定时器每滴答一次,就会调用里面的onTick函数,我们就可以重定义那个函数。
1.我们可以用private继承 来实现

class Widget : private Timer {private:    virtual void onTick() const;    // 实现想要的操作    ...};

通过private继承,Timer的public函数 onTick在 Widget类内就成了private,我们重新声明并定义它。

2.用 复合 方式来实现

class Widget {private:    class WidgetTimer:public Timer {    public:        virtual void onTick() const;        ...    };    WidgetTimer timer;    ...};

在Widget类内声明了一个嵌套是private类,这个类以public形式来继承Timer并重定义onTick方法,然后在Widget类放该类的一个对象。

为什么用复合 优于 private继承 呢?

  • 首先,我们想设计Widget使它拥有派生类,但同时又不想派生类重定义onTick。
    如果直接继承,肯定无法这样实现;但是,我们可以在类内实现这样的东西,像上面的例子一样。
  • 其次,要将Widget的编译依存性降至最低。如果Widget直接继承Timer,当Widget被编译时,Timer的定义必须可见,所以Timer定义 也需要包含进来;但,如果Widget类内内含一个指向WidgetTimer的指针,就可以只带着一个简单的WidgetTimer声明式。
用 private继承 而非 复合

之前有谈到过,private继承主要用于“当一个想成为派生类的类想访问一个想成为基类的protected成分,或为了重新定义virtual函数。
但是,当要处理的类不带任何数据时,为了空间最优化,就应该用private继承,而非 继承+复合。
这样的类没有non-static成员变量,没有virtual函数,也没有virtual base class,这种类的对象不使用任何空间,因为没有该对象的数据存储。

class Empty  { };    // 没有数据class HoldsAnInt {private:    int x;    Empty e;};

理论上来讲,因为Empty类没有任何数据,所以对象应该不使用内存,所以HoldsAnInt类所占用的内存就是 x的 int 所占用的。
但实际上, sizeof(Empty) 将得到1,因为面对 “大小为零之独立对象”,C++ 官方将安装一个char到空对象内。
但并不是所有编译器都放一个char,有些编译器可能放一些其他的,甚至能大到存放一个int。
但是,这个约束不适用于继承,因为当你继承一个Empty类

class HoldsAnInt: private Empty {private:    int x;};

这里 HoldsAnInt 的内存占用 与 int 是一样的。这就是 EBO(Empty Base Optimization 空白基类最优化)。

上面所说的都是理想情况,在现实中 所谓的 Empty class 并不是真正的什么都没有,里面常常含有 typedef、enums、static成员变量。

总结一下,大多数类并非empty,所以EBO很少成为private继承的正当理由。大多数继承相当于 is-a,这是指 public 继承; private 继承 与 复合+private继承 都意味着 is-implemented-in-terms-of,但复合比较容易理解,所以只要可以用复合,就应该用复合。


请记住

  • Private继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当派生类需要访问protected 基类的成员,或需要重新定义继承而来的virtual函数时,这样的设计是合理的。
  • 和 复合 不同,private继承可以造成empty基类最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。




0 0