# [23] 继承 — 你所不知道的
## FAQs in section [23]:
* [23.1] 基类的非虚函数调用虚函数可以吗?
* [23.2] 上面那个FAQ让我糊涂了。那是使用虚函数的另一种策略吗?
* [23.3] 当基类构造函数调用虚函数时,为什么不调用派生类重写的该虚函数?
* [23.4] 派生类可以重置(“覆盖”)基类的非虚函数吗?
* [23.5] “`Warning: Derived::f(float) hides Base::f(int)”`是什么意思?
* [23.6] "virtual table" is an unresolved external 是什么意思?
## 23.1 基类的非虚函数调用虚函数可以吗?
可以。有时(_并非总是!_)这是一个好主意。例如,假设所有`Shape`(图形)对象有一个公共的打印算法。但这个算法依赖于它们的面积并且它们都有不同的方法来计算面积。在这种情况下,`Shape`的`area()`方法(译注:得到`Shape`面积的成员函数)必须是virtual的(可能是纯虚(pure-virtual)的),但`Shape::print()`可以在`Shape`中被定义为非虚(non-virtual)的,前提是所有派生类不会需要不同的打印算法。
```
#include "Shape.hpp"
void Shape::print() const
{
float a = this->area(); // area() 为纯虚
// ...
}
```
## 23.2 上面那个FAQ让我糊涂了。那是使用虚函数的另一种策略吗?
是的,那是不同的策略。是的,那的确是使用虚函数的两种不同的基本方法:
1. 假设你遇到了上一个FAQ所描述的情况:每一个派生类都有一个结构完全一样,只有一小块不同的方法。因此算法是相同的,但实质不相同。在这种情况下,你最好在基类写一个全面的算法作为`public:`方法(有时是非虚的),然后在派生类中写那不同的一小块。这一小块在基类中声明(通常是`protected:`的,纯虚的,当然至少是`virtual`的),并且最终在每个派生类中被定义。这种情况下最紧要的问题是包含全面的算法的`public:`方法是否应该是`virtual`的。答案是,如果你认为某些派生类可能需要覆盖它,就让它成为`virtual`的。
2. 假设你遇到了上一个FAQ完全相反的情况,每一个派生类都有一个结构完全不同,但有一小块的大多数(如果不是全部的话)相同的方法。在这种情况下,你最好将全面的算法放在最终在派生类中定义的`public:` `virtual`之中,并且将一小块可以被只写一次的公共代码(避免代码重复)隐藏在某处(任何地方!)。一般放在基类的`protected:`部分,但不是必须的,也可能不是最好的。找个地方隐藏它们就行了。注意,由于`public:`用户不需要/不想做它们做的事情,如果在基类中隐藏它们,通常应该使它们是`protected:`的。假定它们是`protected:`的,那么可能不应该是`virtual`的:如果派生类不喜欢它们之一的行为,可以不必调用这个方法。
强调一下,以上列表中的是“既/又”情况,而不是“二者选一”的。换句话说,在任何给定的类上,不必在两种策略中选择。既有一个符合策略 #1 的方法`f()`,又有一个符合策略 #2 的方法`g()`是非常正常的。换句话说,在同一个类中,有两种策略同时工作是非常正常的。
## 23.3 当基类构造函数调用虚函数时,为什么不调用派生类重写的该虚函数?
当基类被构造时,对象还不是一个派生类的对象,所以如果 `Base::Base()`调用了虚函数 `virt()`,则 `Base::virt()` 将被调用,即使 `Derived::virt()`(译注:即派生类重写的虚函数)存在。
同样,当基类被析构时,对象已经不再是一个派生类对象了,所以如果 `Base::~Base()`调用了`virt()`,则 `Base::virt()`得到控制权,而不是重写的 `Derived::virt()` 。
当你可以想象到如果 `Derived::virt()` 涉及到派生类的某个成员对象将造成的灾难的时候,你很快就能看到这种方法的明智。详细来说,如果 `Base::Base()`调用了虚函数 `virt()`,这个规则使得 `Base::virt()`被调用。如果不按照这个规则,`Derived::virt()`将在派生对象的派生部分被构造之前被调用,此时属于派生对象的派生部分的某个成员对象还没有被构造,而 `Derived::virt()`却能够访问它。这将是灾难。
## 23.4 派生类可以重置(“覆盖”)基类的非虚函数吗?
合法但不合理。
有经验的 C++ 程序员有时会重新定义非虚函数(例如,派生类的实现可能可以更有效地利用派生类的资源),或者为了回避隐藏规则。即使非虚函数的指派基于指针/引用的静态类型而不是指针/引用所指对象的动态类型,但其客户可见性必须是一致的。
## 23.5 “`Warning: Derived::f(float) hides Base::f(int)`” 是什么意思?
意思是:你要完蛋了。
你所处的困境是:如果基类声明了一个成员函数`f(int)`,并且派生类声明了一个成员函数 `f(float)`(名称相同,但参数类型和/或数量不同),那么 `Base` 的 `f(int)`被隐藏(hidden)而不是被重载(overloaded)或被重写(overridden)(即使 基类的`f(int)`是虚拟的)
以下是你如何摆脱困境:派生类必须有一个被隐藏成员函数的`using` 声明,例如:
```
class Base {
public:
void f(int);
};
class Derived : public Base {
public:
using Base::f; // This un-hides Base::f(int)
void f(double);
};
```
如果你的编译器不支持`using`语法,那么就重新定义基类的被隐藏的成员函数,即使它们是非虚的。一般来说这种重定义只不过使用`::`语法调用了基类被隐藏的成员函数,如,
```
class Derived : public Base {
public:
void f(double);
void f(int i) { Base::f(i); } // The redefinition merely calls Base::f(int)
};
```
## 23.6 "virtual table" is an unresolved external 是什么意思?
如果你得到一个连接错误"`Error: Unresolved or undefined symbols detected: virtual table for class Fred`",那么可能是你在 `Fred` 类中有一个未定义的虚成员函数。
编译器通常会为含有虚函数的类创建一个称为“虚函数表”的不可思议的数据结构(这就是它如何处理动态绑定的)。通常你根本不必知道它。但如果你忘了为`Fred` 类定义一个虚函数,则有时会得到这个连接错误。
许多编译器将这个不可思议的“虚函数表”放进定义类的第一个非内联虚函数的编辑单元中。因此如果 `Fred` 类的第一个非内联虚函数是 `wilma()`,那么编译器会将 `Fred` 的虚函数表放在 `Fred::wilma()` 所在的编辑单元里。不幸的是如果你意外的忘了定义 `Fred::wilma()`,那么你会得到一个"`Fred`'s virtual table is undefined"(`Fred`的虚函数表未定义)的错误而不是“`Fred::wilma()` is undefined”(`Fred::wilma()`未定义)。
- C++ FAQ Lite
- [1] 复制许可
- [2] 在线站点分发本文档
- [3] C++-FAQ-Book 与 C++-FAQ-Lite
- [6] 综述
- [7] 类和对象
- [8] 引用
- [9] 内联函数
- [10] 构造函数
- [11] 析构函数
- [12] 赋值算符
- [13] 运算符重载
- [14] 友元
- [15] 通过 <iostream> 和 <cstdio>输入/输出
- [16] 自由存储(Freestore)管理
- [17] 异常和错误处理
- [18] const正确性
- [19] 继承 — 基础
- [20] 继承 — 虚函数
- [21] 继承 — 适当的继承和可置换性
- [22] 继承 — 抽象基类(ABCs)
- [23] 继承 — 你所不知道的
- [24] 继承 — 私有继承和保护继承
- [27] 编码规范
- [28] 学习OO/C++
- [31] 引用与值的语义
- [32] 如何混合C和C++编程
- [33] 成员函数指针
- [35] 模板
- [36] 序列化与反序列化
- [37] 类库