Item 39:明智地使用 private 继承
private 继承
public 继承表示 "is-a" 的关系,这是因为编译器会在需要的时候将子类对象隐式转换为父类对象。 然而 private 继承则不然:
class Person { ... };
class Student: private Person { ... };
void eat(const Person& p);
Person p;
Student s;
eat(p); // fine, p is a Person
eat(s); // error! a Student isn't a Person
Person 可以 eat,但 Student 却不能 eat。这是 private 继承和 public 继承的不同之处:
- 编译器不会把子类对象转换为父类对象
- 父类成员(即使是public、protected)都变成了private
子类继承了父类的实现,而没有继承任何接口(因为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 的内部实现而非公共接口,我们不希望客户调用它。
private 继承的实现非常简单,而且有时只能使用 private 继承:
- 当 Widget 需要访问 Timer 的 protected 成员时。因为对象组合后只能访问 public 成员,而 private 继承后可以访问 protected 成员。
- 当 Widget 需要重写 Timer 的虚函数时。比如上面的例子中,由于需要重写 onTick 单纯的对象组合是做不到的。
对象组合
我们知道对象组合也可以表达 "is-implemented-in-terms-of" 的关系, 上面的需求当然也可以使用对象组合的方式实现。但由于需要重写 Timer 的虚函数,所以还是需要一个继承关系的:
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
内部类 WidgetTimer public 继承自 Timer,然后在 Widget 中保存一个 Widget Timer对象。 这是 public 继承+对象组合的方式,比 private 继承略为复杂。但对象组合仍然拥有它的好处:
- 你可能希望禁止 Widget 的子类重定义 onTick。在 C++ 中可以使用 public 继承+对象组合的方式来做到这一点。
- 减小 Widget 和 Timer 的编译依赖。如果是 private 继承,在定义 Widget 的文件中势必需要引入#include"timer.h"。 但如果采用对象组合的方式,你可以把 WidgetTimer 放到另一个文件中,在 Widget 中保存 WidgetTimer 的指针并声明 WidgetTimer 即可。
EBO 特性
我们讲虽然对象组合优于 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)
。 但如果你继承了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 等。
总结
- 私有继承意味着是根据……实现的。它通常比复合更低级,但当一个派生类需要访问保护基类成员或需要重定义继承来的虚拟函数时它就是合理的。
- 与复合不同,私有继承能使 empty base optimization(空基优化)有效。这对于致力于最小化对象大小的库开发者来说可能是很重要的。
- 在面向对象设计中,对象组合往往比继承提供更大的灵活性,只要可以使用对象组合就不要用 private 继承。