[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
类有虚函数ddraw()
、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++
;而不要给我发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*。