浅谈虚函数

什么是多态?什么是虚函数?如何实现的?

多态的实质

就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定

多态的实现

多态其实一般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数生成符号表时的不同规则,重载只是一种语言特性,与多态无关,与面向对象也无关,但这又是 C++中增加的新规则,所以如果非要说重载算是多态的一种,那就可以说:
多态可以分为静态多态动态多态
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;
动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态,
一般情况下我们不区分这两个时所说的多态就是指动态多态。
动态多态的实现与虚函数表,虚函数指针相关。

虚函数

在基类的函数前加上 virtual 关键字,就是一个虚函数。在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果是基类,就调用基类的函数。

实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自自的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。后续如果有一个基类类型的指针,指向派生类,那么当调用虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调用派生类的虚函数表中的虚函数以此实现多态。

虚函数的调用过程和非虚成员函数有什么区别?

调用方式上的区别。

非虚函数是静态调用的。而虚函数在调用时会根据 object 的类型(注意:不是指针或引用的类型)来确定实际被 call 的函数。在现代编译器的实现中,首先通过对象中隐藏的一个指针找到虚函数表,然后从虚函数表对应的 offset 中取出函数指针。

什么是纯虚函数?

纯虚函数就是加了 virtual foo() = 0; 的函数。含有纯虚函数的类被称为抽象类,它无法实例化。子类必须实现父类的所有纯虚函数,否则它将也是一个抽象类。

实际上,纯虚函数的出现就是为了让继承可以出现多种情况:

  • 派生类只继承成员函数的接口;

  • 派生类既继承成员函数的接口,又继承成员函数的实现,而且可以在派生类中可以重写成员函数以实现多态;

  • 派生类在继承成员函数接口和实现的情况下,不能重写缺省的实现。

其实,声明一个纯虚函数的目的就是为了让派生类只继承函数的接口,而且派生类中必需提供一个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进行实例化。
对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调用这个实现的唯一方式是在派生类对象中指出其 class 名称来调用。