# [20] 继承 — 虚函数
## FAQs in section [20]:
* [20.1] 什么是“虚成员函数”?
* [20.2] C++ 怎样同时实现动态绑定和静态类型?
* [20.3] 虚成员函数和非虚成员函数调用方式有什么不同?
* [20.4] 析构函数何时该时虚拟的?
* [20.5] 什么是“虚构造函数(`virtual` constructor)”?
## 20.1 什么是“虚成员函数”?
从面向对象观点来看,它是 C++ 最重要的特征:[6.8], [6.9].
虚函数允许派生类取代基类所提供的实现。编译器确保当对象为派生类时,取代者(译注:即派生类的实现)总是被调用,即使对象是使用基类指针访问而不是派生类的指针。这样就允许基类的算法被派生类取代,即使用户不知道派生类的细节。
派生类可以完全地取代基类成员函数(覆盖(override)),也可以部分地取代基类成员函数(增大(augment))。如果愿意的话,后者由派生类成员函数调用基类成员函数来完成。
## 20.2 C++ 怎样同时实现动态绑定和静态类型?
当你有一个对象的指针,而对象实际是该指针类型的派生类(例如:一个 `Vehicle*`指针实际指向一个Car 对象)。由此有两种类型:指针的(静态)类型(在此是`Verhicle`),和指向的对象的(动态)类型(在此是Car)。
_静态类型_意味着成员函数调用的合法性被尽可能早地检查:编译器在编译时。编译器用指针的静态类型决定成员函数调用是否合法。如果指针类型能够处理成员函数,那么指针所指对象当然能很好的处理它。例如,如果 `Vehicle` 有某个成员函数,则由于`Car`是一种`Vehicle`,那么`Car` 当然也有该成员函数。
_动态绑定_意味着成员函数调用的代码地址在最终时刻才被决定:基于运行时的对象动态类型。因为绑定到实际被调用的代码这个过程是动态完成的(在运行时),所以被称为“动态绑定”。动态绑定是虚函数导致的结果之一。
## 20.3 虚成员函数和非虚成员函数调用方式有什么不同?
非虚成员函数是静态确定的。也就是说,该成员函数(在编译时)被静态地选择,该选择基于指象对象的指针(或引用)的类型。
相比而言,虚成员函数是动态确定的(在运行时)。也就是说,成员函数(在运行时)被动态地选择,该选择基于对象的类型,而不是指向该对象的指针/引用的类型。这被称作“动态绑定”。大多数的编译器使用以下的一些的技术:如果对象有一个或多个虚函数,编译器将一个
隐藏的指针放入对象,该指针称为“virtual-pointor”或“v-pointer”。这个v-pointer指向一个全局表,该表称为“虚函数表(virtural-table)”或“v-table”。
编译器为每个含有至少一个虚函数的类创建一个v-table。例如,如果`Cirle`类有虚函数d`draw()`、`move()` 和 `resize()`,那么将有且只有一个和Cricle类相关的v-table,即使有一大堆Circle对象。并且每个 `Circle`对象的 v-poiner将指向 `Circle`的这个 v-table。该 v-table自己有指向类的各个虚函数的指针。例如,`Circle` 的v-table 会有三个指针:一个指向`Circle::draw()`,一个指向 `Circle::move()`,还有一个指向`Circle::resize()`。
在分发一个虚函数时,运行时系统跟随对象的 v-pointer找到类的 v-table,然后跟随v-table中适当的项找到方法的代码。
以上技术的空间开销是存在的:每个对象一个额外的指针(仅仅对于需要动态绑定的对象),加上每个方法一个额外的指针(仅仅对于虚方法)。时间开销也是有的:和普通函数调用比较,虚函数调用需要两个额外的步骤(得到v-pointer的值,得到方法的地址)。由于编译器在编译时就通过指针类型解决了非虚函数的调用,所以这些开销不会发生在非虚函数上。
注意:由于没有涉及诸如多继承,虚继承,RTTI等内容,也没有涉及诸如page fault,通过指向函数的指针调用函数等空间/时间论的内容,所以以上讨论是相当简单的。如果你想知道其他的内容,请询问 _[`comp.lang.c++`](news:comp.lang.c++)_;而不要给我发E-MAIL!
## 20.4 析构函数何时该时虚拟的?
当你可能通过基类指针删除派生类对象时。
虚函数绑定到对象的类的代码,而不是指针/引用的类。如果基类有虚析构函数,`delete basePtr`时(译注:即基类指针),`*basePtr` 的对象类型的析构函数被调用,而不是该指针的类型的析构函数。这通常是一件好事情。
_TECHNO-GEEK WARNING; PUT YOUR PROPELLER HAT ON._
从技术上来说,如果你打算允许其他人通过基类指针调用对象的析构函数(通过`delete`这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。如果一个类有显式的析构函数,或者有成员对象,该成员对象或基类有重要的析构函数,那么这个类就有重要的析构函数。(注意这是一个递归的定义(例如,某个具有重要析构函数的类,它有一个成员对象(它有基类(该基类有成员对象(它有基类(该基类有显式的析构函数))))))
_END TECHNO-GEEK WARNING; REMOVE YOUR PROPELLER HAT_
如果你对以上的规则理解有困难,试试这个简单的:类应该有虚析构函数,除非这个类没有虚函数。原理:如果有虚函数,说明你想通过基类指针来使用派生对象,并且你所可能做的事情之中,可能包含了调用析构函数(通常通过`delete`隐含完成)。一旦你在类中加上了一个虚函数,你就已经需要为每一个对象支付空间代价(每个对象一个指针;注意这是理论上的编译器特性;实际上每个编译器都是这样做的),所以这时使析构函数成为虚拟的通常不会额外付出什么。
## 20.5 什么是“虚构造函数(`virtual` constructor)”?
一种允许你做一些 C++ 不直接支持的事情的用法。
你可能通过虚函数 `virtual` `clone()`(对于拷贝构造函数)或虚函数 `virtual` `create()`(对于默认构造函数),得到虚构造函数产生的效果。
```
class Shape {
public:
virtual ~Shape() { } // 虚析构函数
virtual void draw() = 0; // 纯虚函数
virtual void move() = 0;
// ...
virtual Shape* clone() const = 0; // 使用拷贝构造函数_
virtual Shape* create() const = 0; // 使用默认构造函数
};
class Circle : public Shape {
public:
Circle* clone() const { return new Circle(*this); }
Circle* create() const { return new Circle(); }
// ...
};
```
在 `clone()` 成员函数中,代码 `new Circle(*this)` 调用 `Circle` 的拷贝构造函数来复制`this`的状态到新创建的`Circle`对象。在 `create()`成员函数中,代码 `new Circle()` 调用`Circle`的默认构造函数。
用户将它们看作“虚构造函数”来使用它们:
```
void userCode(Shape& s)
{
Shape* s2 = s.clone();
Shape* s3 = s.create();
// ...
delete s2; // 在此处,你可能需要虚析构函数
delete s3;
}
```
这个函数将正确工作,而不管 `Shape` 是一个`Circle`,`Square`,或是其他种类的 `Shape`,甚至它们还并不存在。
注意:成员函数`Circle`'s `clone()`的返回值类型故意与成员函数`Shape`'s `clone()`的不同。这种特征被称为“协变的返回类型”,该特征最初并不是语言的一部分。如果你的编译器不允许在`Circle`类中这样声明`Circle* clone() const`(如,提示“The return type is different”或“The member function's type differs from the base class virtual function by return type alone”),说明你的编译器陈旧了,那么你必须改变返回类型为`Shape*。`
- 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] 类库