多态特性详见 C++面向对象的三大特性。
动态多态通过虚函数实现。
虚函数virtual
虚函数的用途:
如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的。这样对于通过基类指针或引用访问的对象,能够根据对象的类型来调用,而不是根据指针或引用的类型来处理。
示例:
假设要创建一个数组来保存基类对象和派生类对象,但数组中所有元素的类型必须是相同的。可以创建指向基类的指针数组,这样,每个元素的类型都相同,但由于使用的是公有继承类型,所以指针既可以指向基类对象,也可以指向派生类对象。
virtual与引用、指针的多态特性
如果方法是通过引用或指针调用的:
- 没有使用关键字virtual:程序将根据引用类型或指针类型选择方法;
- 使用了virtual:程序将根据引用或指针指向的对象类型选择方法。
虚函数表
虚函数表是一个数组,表中存储了为类对象进行声明的虚函数的地址。
编译器处理虚函数的方法是,为每个对象(如下一段叙述)添加一个隐藏的指针成员,指向虚函数表。
例如,基类对象包含一个指针,指向基类中所有虚函数的地址表;派生类对象也包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,若没有则保存函数原始版本的地址。
调用虚函数时,程序将查看虚函数表中的函数地址,通过地址转向执行相应的函数。
- 虚函数在内存和执行速度方面的代价:
-
因为要存储地址的空间,所以每个对象都将增大;
-
对于每个类,编译器都创建一个虚函数地址表(数组);
-
对于每个函数调用,都需要执行一项额外操作——到表中查找地址。
虽然非虚函数效率比虚函数高,但不具备动态联编功能。1
虚函数与构造、析构、友元
- 构造函数不能是虚函数:
编译器在构造对象时,必须知道确切类型才能正确地生成对象;构造函数执行之前,对象并不存在。
调用派生类的构造函数创建派生类对象时,派生类对象通过初始化成员列表的方式使用基类构造函数,这种方式不同于继承机制。因此,派生类不继承基类的构造函数,将构造函数声明为虚没有任何意义。 - 基类的析构函数应当是虚函数:
为基类声明一个虚析构函数是一种惯例,这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。
当通过指向对象的基类指针或引用来删除派生对象时,如果析构函数不是虚的,则将只调用对应指针类型的析构函数;如果析构函数时虚的,将调用相应对象类型的析构函数。 - 友元不能是虚函数:
友元不是类成员,只有成员才能是虚函数。
虚函数的重新定义和重载的区别
对于如下代码:
class Dewlling
{
public:
virtual void showperks(int a) const; //接受一个int参数
...
};
class Hovel : public Dewlling
{
public:
virtual void showperks() const; //不接受任何参数
...
};
重新定义不会生成函数的两个重载版本,而是隐藏了基类方法中接受一个int参数的版本。
从中可以看出,如果在派生类中重新定义基类函数,将隐藏所有同名基类方法。
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变。(这种例外只适用于返回值,不适用于参数)
- 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,其他版本将被隐藏,派生类对象将无法使用它们。
总结——一道面试题:那些函数不能定义为虚函数?
经检验下面的几个函数都不能定义为虚函数:
1)友元函数,它不是类的成员函数
2)全局函数
3)静态成员函数,它没有this指针
3)构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)