虚函数
虚函数是实现多态的一种方式,它允许派生类对基类中同名函数的重新定义,从而使得派生类对象在运行时调用基类中同名函数时,调用的是派生类中重新定义的函数。
虚函数
虚函数的本质
-
虚函数表(virtual table):存放所有类的虚函数的地址的一个数组,每个类都有一个虚函数表。
-
虚函数指针(virtual function pointer):指向虚函数表中某个虚函数的指针。
-
虚函数调用:当调用虚函数时,实际上是调用虚函数指针指向的函数。
虚函数的存放位置
存放在全局数据区的只读数据区(read-only data section)
-
全局虚函数表(global virtual table):存放所有类的虚函数的地址,每个类都有一个全局虚函数表。
-
每个类的虚函数表(virtual table):存放该类的虚函数的地址,每个类都有一个虚函数表。
虚函数创建的情况
在编译期
-
编译器发现virtual关键字
-
虚函数表(virtual table)的创建
-
编译器生成虚函数的调用代码
我的总结
虚函数其实在类当中隐式地增加了一个成员函数,用来指向虚函数表所在的一个指针,这个指针指向了虚函数表中存放的虚函数的地址。
在带有virtual关键词的类A,在构造期间,会将虚函数表的地址存放在类的全局数据区,在运行期间,会将这个地址赋值给虚函数指针。假设这时候有一个类B派生于A类,那么在B的构造函数中,B会生成一个成员变量指向自己的虚函数表,然后B的成员函数会时不时地更换指向,一会指向A的虚函数表,一会指向B的虚函数表。
虚函数的灵魂是虚函数表,它在编译期间就已经确定了,运行期间才会确定调用哪个虚函数。
- 因此**构造函数(constructor)**不可以是虚函数,因为构造函数是在对象创建的时候调用的,而虚函数表是在编译期间就已经确定了,运行期间才会确定调用哪个虚函数。
- 析构函数(destructor)可以是虚函数,因为析构函数是在对象销毁的时候调用的,而虚函数表是在编译期间就已经确定了,运行期间才会确定调用哪个虚函数。
静态绑定和动态绑定
- 静态绑定(static binding):在编译期确定调用哪个虚函数,调用的是虚函数表中的函数地址。
- 动态绑定(dynamic binding):在运行期确定调用哪个虚函数,调用的是虚函数指针指向的函数(运行时,从某个寄存器当中读取)。
-
若一个类当中定义了虚函数,则该类为虚类。在编译阶段,编译器会给这个类生成一个唯一的虚函数表(vftable)。虚函数表的内容为RTTI(Run-Time Type Information)、虚函数地址。当程序运行时,每张虚函数表都会被加载到内存的.rodata区(read-only data section)。
-
若一个类当中定义了虚函数,那么在这个类当中,除了其显示的成员变量,编译器还会隐式地在内存地址空间当中生成一个vptr指针(在构造期间),这个指针指向虚函数表的地址。根据偏移就可以得到虚函数的地址,在运行期间,程序将这个虚函数地址存到寄存器当中供调用。
-
但对于一个衍生类(继承而来的基类存在virtual关键词)来说,它编译期间,也会生成一个指针。而到了运行期间,指针的指向会动态地改变。
面试问题
构造函数不能是虚函数
-
从vptr角度解释
虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针(vptr可以参考C++的虚函数表指针vptr)指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化! -
从多态角度解释
虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。那使用虚函数也没有实际意义。 在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。
析构函数可以且常常是虚函数
此时 vtable 已经初始化了,完全可以把析构函数放在虚函数表里面来调用。C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内存泄漏的问题。