六、继承与面向对象设计
条款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基类最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。