第八章 多态性和虚函数
8.1 多态性
静态联编所支持的多态性称为编译时的多态性。当调用重载函数时,编译器可以根据调用时使用的实参在编译时就确定下来应调用哪个函数。动态联编所支持的多态性称为运行时的多态性,这由虚函数来支持。虚函数类似于重载函数,但与重载函数的实现策略不同,即对虚函数的调用使用动态联编。
8.1.1 静态联编中的赋值兼容性及名字支配规律
对象的内存地址空间中只包含数据成员,并不存储有关成员函数的信息。这些成员函数的地址翻译过程与其对象的内存地址无关。
声明的基类指针只能指向基类,派生类指针只能指向派生。它们的原始类型决定它们只能调用各种的同名函数area。
8.1.2 动态联编的多态性
当编译系统编译含有虚函数的类时,将为它建立一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器也为类增加一个数据成员,这个数据成员是一个指向该虚函数表的指针,通常称为vptr。
虚函数的地址翻译取决于对象的内存地址。编译器为含有虚函数类的对象首先建立一个入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序,一一填入函数指针。当调用虚函数时,先通过vptr找到虚函数表,然后再找出虚函数的真正地址。
派生类能继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动称为虚函数。如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数。如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,函数指针会调用改写过的虚函数。
虚函数的调用规则是:根据当前对象,优先调用对象本身的成员函数。这和名字支配规律类似,不过虚函数是动态联编的,是在执行期“间接”调用实际上欲联编的函数。
8.2 虚函数
一旦基类定义了虚函数,该基类的派生类中的同名函数也自动称为虚函数。
8.2.1 虚函数的定义
虚函数只能是类中的一个成员函数,但不能是静态成员,关键字virtual用于类中该函数的声明中。
当在派生类中定义了一个同名的成员函数时,只要该成员函数的参数个数和相应类型以及它的返回类型与基类中同名的虚函数完全一样,则无论是否为该成员使用virtual,它都将成为一个虚函数。
8.2.2 虚函数实现动态性的条件
关键字virtual指示C++编译器对调用虚函数进行动态联编。这种多态性是程序运行到需要的语句处才动态确定的,所以称为运行时的多态性。不过,使用虚函数并不一定产生多态性,也不一定使用动态联编。例如,在调用中对虚函数使用成员名限定,可以强制C++对该函数的调用使用静态联编。
产生运行时的多态性有如下3个前提:
(1)类之间的继承关系满足赋值兼容性规则。
(2)改写了同名函数。
(3)根据赋值兼容性规则使用指针(或引用)。
由于动态联编是在运行时进行的,相对于静态联编,它的运行效率比较低,但它可以使程序员对程序进行高度抽象,设计出可扩充性好的程序。
8.2.3 构造函数和析构函数调用虚函数
在构造函数和析构函数中调用虚函数采用静态联编,即他们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。
目前推荐的C++标准不支持虚构造函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。虚析构函数使用virtual说明。只要基类的析构函数被说明为虚函数,则派生类的析构函数,无论是否使用virtual进行说明,都自动地成为虚函数。
delete运算符和析构函数一起工作(new和构造函数一起工作),当使用delete删除一个对象时,delete隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。一般来说,如果一个类中定义了虚函数,析构函数也应说明为虚函数,尤其是在析构函数要完成一些有意义的任务时,例如,释放内存。
如果基类的析构函数为虚函数,则在派生类为定义析构函数时,编译器所生成的析构函数也为虚函数。
8.2.4 纯虚函数与抽象类
在许多情况下,不能再基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数,将其定义留给派生类去做。说明纯虚函数的一般形式如下:
class 类名{
virtual 函数类型 函数名(参数列表)=0;
};
一个类可以说明多个纯虚函数,包含有纯虚函数的类称为抽象类。一个抽象类只能作为基类来派生新类,不能说明抽象类的对象。但可以说明指向抽象类对象的指针(或引用)。
从一个抽象类派生的类必须提供纯虚函数的实现代码,或在该派生类中仍将它说明为纯虚函数,否则编译器将给出错误信息。这说明了纯虚函数的派生类仍是抽象类。如果派生类给了某类所有纯虚函数的实现,则该派生类不再是抽象类。
如果通过同一个基类派生一系列的类,则将这些类总称为类族。抽象类的这一特点保证了进度类族的每个类都具有(提供)纯虚函数所要求的行为,进而保证了围绕这个类族所建立起来的软件能正常运行,避免了这个类族的用户由于偶然失误而影响系统正常运行。
抽象类至少含有一个虚函数,而且至少有一个虚函数是纯虚函数,以便将它与空的虚函数区分开来。下面是两种不同的表示方法:
virtual void area()=0;
virtual void area(){}
在成员函数内可以调用纯虚函数。因为没有为纯虚函数定义代码,所以在构造函数或虚构函数内调用一个纯虚函数将导致程序运行错误。
8.3 多重继承与虚函数
8.4 类成员函数的指针与多态性
在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍发生多态性。