《C++ Primer》P536.

对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。动态绑定只有当通过指针或引用调用虚函数时才会发生,而当通过一个具有普通类型(非指针非引用)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。通过对象进行的函数(虚函数或非虚函数)调用也在编译时确定。

绑定的对象确定了运行时调用的函数版本

当我们使用基类的引用或指针调用基类中定义的函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。
如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型

一日为虚函数,终身为虚函数

一旦某个函数被声明为虚函数,则在所有的派生类中它都是虚函数。派生类在重新定义该虚函数时,virtual关键字可写可不写。
派生类若要覆盖基类的虚函数,则要求:(1)形参类型与基类函数一致,(2)一般也要求返回类型一致。返回类型也可以不一致:例外,当虚函数的返回类型是类本身的指针或引用时,返回类型的要求规则无效。也就是说,如果D由B派生得到,泽基类的虚函数可以返回B而派生类的对应函数可以返回D,只不过这样的返回类型要求从D到B的类型转换是可访问的。

使用override关键字强制覆盖基类的虚函数

只有基类中虚函数在派生类中才能覆盖。
上面讲到,派生类如果要覆盖基类的虚函数,那么派生类中虚函数的形参列表则要与基类中虚函数一致。
如果派生类中定义了一个函数名字与基类中虚函数名字相同但是形参列表不同,这仍然是合法的行为,编译器将认为新定义的这个函数与基类中原有的函数是相互独立的,派生类中的函数并没有覆盖基类中的虚函数。就实际的编程习惯而言,这种声明往往意味着错误,因为我们希望派生类的虚函数覆盖基类的虚函数,可能只是不小心把形参列表搞错了。
在C++11中可以用override关键字说明派生类的虚函数是要覆盖基类的虚函数的,这样编译器会帮我们检查出一些错误。

使用final关键字阻止派生类覆盖基类的函数

我们可以将函数指定为final,如果已经把函数定义为final了,则之后任何尝试覆盖该函数的操作都将引发错误。

final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置的返回类型之后。

可以使用作用域运算符::强制执行虚函数的某个特定版本

通过在函数体的位置(声明语句的分号之前)写=0将虚函数说明为纯虚函数,纯虚函数只能在类内定义。

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类

抽象基类负责定义接口,后续的其他类可以覆盖该接口。不能直接创建一个抽象基类的对象。

链接:https://www.nowcoder.com/questionTerminal/1f67d4e2b6134c298e993e622181b333
来源:牛客网

虚函数的作用:实现多态

基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,且这样的函数调用是无法在编译器期间确认的,而是在运行期确认,也叫做迟绑定。

虚函数的底层实现原理:虚函数表+虚表指针

  • 类中的每个虚函数都对应一个虚函数指针,该类的所有的虚函数指针放在一个虚函数表(virtual table,vtbl),该虚函数表被该类所有对象共享。
  • 含有虚函数的类的实例化对象都有一个指向该类虚函数表的指针,放在所有其他成员前面,这个指针通常称为vptr。vptr的设定(setting)和重置(resetting)由该类的构造函数、析构函数和拷贝赋值运算符自动完成。
  • 如果派生类多继承了多个含有虚函数的基类,则含有多个虚函数表指针,按顺序分别指向不同的基类虚函数表,派生类自身的虚函数放置在自身第一个虚函数表的后面。如果派生类覆写了基类的虚函数,则虚函数表中放置派生类的虚函数地址;否则,仍存放基类的虚函数地址。