在这一章中,我们介绍了虚函数的概念及应用,知道了虚函数和基类指针在一起可以实现多态。那么虚函数到底是怎么实现的呢?
我们知道,一般的成员函数和普通的函数一样,都是一段代码,而编译器在编译函数调用的时候会将对象的this指针作为函数隐藏的第一个参数,这样成员函数就能访问其他类成员了。对于这种非虚函数的情况来说,基类指针就算指向派生类,调用的函数版本也是基类的,因为这两个函数是不同的函数,函数入口也不同,编译器并不知道基类指针指向的是派生类。
因此,为了实现多态,我们需要一个在连编译器都不知道基类指针指向什么类的情况下也能调用正确虚函数的方法。而那就是,我们可以给每个类一个专属的空间,其中放置着几个自己的虚函数,指针可以通过编号来调用虚函数。虽然基类对象和派生类对象的专属空间不一样,但是虚函数的编号一样,这样编译器就算没有任何信息也能调用到正确的虚函数。这样的一个专属的存放虚函数地址的空间就是虚表(Virtual Table,也就是vtable),而由于每个类只需要一个虚表,为了节省空间,类的对象只需要一个指向虚表的虚指针(Virtual Pointer,也就是vptr)就行了。
动手写9.6.1
动手写9.6.1展示了虚函数的开销,运行结果如图9.6.1所示:
(www.xing528.com)
图9.6.1 虚函数的开销
在上一章的“知识拓展”中我们了解到空类的大小是1字节,而这里同样是空类的几个类,它们的大小却有4字节,这4字节其实就是虚指针的大小。接下来我们通过一张图来理解虚指针和虚表是如何实现多态的:
图9.6.2 虚函数原理图解
在图9.6.2中可以看到,Vehicle类和Airplane类分别有自己的虚表,这两个虚表在编译的时候就放好了自己版本的虚函数。到了创建对象的时候,构造函数会自动将对象的虚指针初始化为虚表的地址。这样的话,假设我们有一个指向Vehicle类的指针“vehiclePtr,vehiclePtr->move()”,就会被翻译成“*((vehiclePtr->vptr)+0)()”,而“vehiclePtr->printName()”会被翻译成“*((vehiclePtr->vptr)+1)()”。这其实就是用当前对象的虚指针加上偏移量(虚指针指向虚表首地址,也就是第一个虚函数的地址,而指针加法相当于数组下标操作)而获得虚函数的地址,然后解引用并调用函数。由于Airplane类中move()也在虚表中的同样位置,指向Airplane类的指针airplanePtr也可以进行“*((airplanePtr->vptr)+0)()”的操作并调用到派生类版本的虚函数。
总而言之,虚函数实现多态的秘诀就是将各个类对应的虚函数放在各自虚表的同一个位置,由于所有类都有虚指针,因此这个信息不会因为类型转换而丢失。
此外,虚析构函数如果声明了,也会出现在虚表之中。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。