文章目录
- 《Effective C++》条款34:区分接口继承与实现继承
-
- 公有继承的本质
- 三种成员函数的对比
- 深入理解三种函数的设计意图
-
- 1. 纯虚函数:强制实现契约
- 2. 虚函数:提供缺省实现与风险
- 3. 非虚函数:强制不变性
- 常见设计错误与准则
- 总结
请记住: 1. 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。 2. pure virtual 函数只具体指定接口继承。 3. – 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。 4.- non-virtual 函数具体指定接口继承以及强制性实现继承
《Effective C++》条款34:区分接口继承与实现继承
公有继承的本质
公有继承(public inheritance)实质上是两种继承的复合体:函数接口的继承和函数实现的继承。而如何混合搭配这两种继承,就体现在你为基类成员函数所选择的类型上。
三种成员函数的对比
| 纯虚函数 (Pure Virtual) | 仅继承接口 | 强制派生类必须实现该接口。强调"做什么"由基类规定,但"如何做"完全由派生类决定。 |
| 虚函数 (Impure Virtual) | 继承接口和一份缺省实现 | 派生类应该实现该接口,但如果不实现,则使用基类提供的缺省行为。这是灵活性,也潜藏风险。 |
| 非虚函数 (Non-Virtual) | 继承接口和一份强制性实现 | 派生类不应该改变该函数的行为。它代表一种不变性,强调"如何做"是固定的,所有派生类必须遵守。 |
深入理解三种函数的设计意图
1. 纯虚函数:强制实现契约
纯虚函数通过 = 0 的语法,明确告诉派生类的设计者:“你必须支持这个操作,但我无法提供通用实现”。这最适合定义一种"契约"或"能力"。
代码示例:
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数: 所有图形都必须能被绘制
virtual ~Shape() = default;
};
Shape::draw() 是纯虚函数,因为画一个圆形和画一个矩形的算法完全不同,基类 Shape 无法给出有意义的默认实现。任何从 Shape 派生的具体类(如 Circle,Rectangle),如果不实现 draw(),自己也会成为抽象类,无法实例化。这就从语法层面保证了接口的一致性。
一个重要的技巧:纯虚函数可以有实现! 你可以为纯虚函数提供定义,但调用它必须使用完全限定名(如 shapePtr->Shape::draw())。这通常用于提供一些公共基础功能,但即便如此,派生类也必须重写该函数。
2. 虚函数:提供缺省实现与风险
虚函数则在强制接口之外,提供了一份"保底"的缺省实现。它说:“你最好自定义这个行为,但如果你不自定义,可以用我这个默认的”。
代码示例:
// 初始设计: 存在风险
class Airplane {
public:
virtual void fly(const Airport& destination) {
// 默认的飞行逻辑…
}
};
class ModelA : public Airplane { ... }; // 使用默认fly
class ModelB : public Airplane { ... }; // 使用默认fly
这个设计的风险在于,如果新加入的 ModelC 有特殊的飞行方式,但程序员忘记重写 fly 函数,那么 ModelC 的飞机就会错误地使用默认飞行模式,可能导致严重问题。
更安全的设计:接口与默认实现分离
为了解决这个问题,条款推荐了两种方法:
将默认实现分离为独立的保护函数:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0; // 纯虚接口
protected:
void defaultFly(const Airport& destination); // 独立的默认实现
};
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination) override {
defaultFly(destination); // 显式申请使用默认实现
}
};
利用纯虚函数本身提供默认实现:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
};
// 为纯虚函数提供实现
void Airplane::fly(const Airport& destination) { /* 默认实现 */ }
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination) override {
Airplane::fly(destination); // 显式调用基类的纯虚函数实现
}
};
这两种方法的核心思想都是:切断"继承接口"和"继承缺省实现"的自动关联,要求派生类显式地表明意图。这样,如果 ModelC 不重写 fly,就会因为基类是抽象类而无法编译,错误在编译期就被发现。
3. 非虚函数:强制不变性
非虚函数代表的意义是"不变性凌驾于特异性之上"。它表示这个行为是基类认定的、所有派生类都不应该改变的核心特性。
代码示例:
class Shape {
public:
int objectID() const { ... } // 非虚函数
};
对于 objectID() 这样的函数,其计算对象ID的方式应该是全局统一的,不应该因为对象是矩形或圆形而改变。如果派生类重定义非虚函数(这本身是允许的,但会遮蔽基类函数),通常意味着设计出了问题,应该重新审视这个函数是否真的应该是非虚函数。
常见设计错误与准则
理解了这三种函数的区别,就能避免两个常见的设计错误:
将所有函数声明为非虚函数:这会让派生类没有特化的空间,僵化了设计。尤其需要注意的是,如果类要做基类,析构函数必须是虚函数。
将所有函数声明为虚函数:这暗示类设计中缺乏不变性,所有行为都允许被改变,可能意味着职责划分不清。同时,对于完全不希望被重写的函数,应该用 non-virtual 明确表达设计意图。
关于虚函数的性能,有一个 80-20法则:一个程序80%的时间往往运行在20%的代码上。因此,只要将虚函数用在真正需要多态的地方,它们带来的运行时开销通常是可接受的。
总结
记住条款34的精髓在于培养一种精确的设计意识:
- 使用纯虚函数 来定义强制性的接口契约。
- 使用虚函数 来提供可选的缺省实现,但最好通过接口与实现分离的技术来避免潜在风险。
- 使用非虚函数 来规定强制性的不变实现。

