虚函数是指用 virtual 关键字修饰的类成员函数。其定义如下所示:
virtual void vir_print(){}
虚函数的作用是允许使用基类的指针来调用子类的函数,从而实现“多态”。含虚函数的类在编译时会产生一张虚函数表vtbl,里面保存了函数的调用地址。类对象通过虚函数表指针vptr找到虚函数表vtbl,通过vtbl找到对应函数的调用地址,然后调用执行。
1、虚函数表指针位置
一般地,虚函数表指针位于对象的开头位置。
我们可以写一段代码来测试一下:
class Base {
public:
int t_i = 8;
virtual void vir_print() {
std::cout << "Base::vir_print()" << std::endl;
}
};
int main()
{
Base base;
return 0;
}
我们用VS2019来调试上面的程序,把断点设在第12行的return 0处,运行后打开“快速监视”工具,在“表达式”窗口输入:&base。可以看到base对象的内存布局如下:
另外,我们也可以通过读取二进制的方式来验证base对象的内存模型。我们在main()函数中加入下面的代码:
std::cout << "sizeof(base) = " << sizeof(base) << std::endl;
int* pt_i = (int*)((char*)&base + 4);
std::cout << "t_i = " << *pt_i << std::endl;
程序的输出结果为:
sizeof(base) = 8
t_i = 8
所以对象的大小是8个字节,第5 - 8个字节是变量t_i,所以1-4个字节是虚函数表指针的。
2、虚函数的手工调用
为了加深对虚函数调用过程的认识,我们直接通过虚函数表指针来调用虚函数。
Base类的定义如下:
class Base {
public:
int t_i = 8;
virtual void vir_f() {
std::cout << "Base::vir_f()" << std::endl;
}
virtual void vir_g() {
std::cout << "Base::vir_g()" << std::endl;
}
virtual void vir_h() {
std::cout << "Base::vir_h()" << std::endl;
}
};
Base类对象的内存布局是下面这样的。
虚函数表和成员函数都是属于Base类的,虚函数表指针是属于base对象的,每个对象都会生成自己的虚函数表指针,但指针指向的虚函数表地址都相同。
main()函数的定义如下:
int main()
{
Base base;
long* pvptr = (long*)&base; //因为对象的首地址是虚函数表指针
long* vptr = (long*)(*pvptr); //指针的值就是指向的虚函数表
//定义函数指针,指向 void (void)函数
typedef void (*Func)(void);
//因为虚函数表是个数组,所以我们可以通过vptr[i]的方式来调用对应的虚函数
Func vir_f = (Func)vptr[0];
Func vir_g = (Func)vptr[1];
Func vir_h = (Func)vptr[2];
vir_f();
vir_g();
vir_h();
}
3、继承虚函数表分析
3.1 单继承
假设有Base, Derive两个类,其中Derive继承自Base类。代码如下所示:
class Base {
public:
int b_i = 8;
virtual void vir_f() {
std::cout << "Base::vir_f()" << std::endl;
}
virtual void vir_g() {
std::cout << "Base::vir_g()" << std::endl;
}
virtual void vir_h() {
std::cout << "Base::vir_h()" << std::endl;
}
int d_i = 9;
};
class Derive :public Base {
public:
int d_i = 9;
void vir_f() {
std::cout << "Derive::vir_f()" << std::endl;
}
void vir_g() {
std::cout << "Derive::vir_g()" << std::endl;
}
void vir_h() {
std::cout << "Derive::vir_h()" << std::endl;
}
};
int main()
{
Derive derive;
return 0;
}
通过VS2019的“快速监视”窗口,我们可以看到derive对象的内存布局是这样的:
从图中我们可以看到derive对象的内存布局顺序从上到下是这样排列的:
虚函数表指针vptr、Base类的int变量b_i、Derive类的int变量d_i。
关于虚函数,有这么几个要点:
(1)只要在父类中是虚函数,子类中不用写virtual也依然是虚函数。
(2)如果父类中有虚函数,就算子类没有虚函数,子类也会创建自己的虚函数表。
(3)子类中的虚函数会覆盖父类中的同名虚函数。
(4)在把子类对象赋值给父类时,会用子类对象的值去初始化父类对象,但父类对象的虚函数表指针指向的依然是它自己的虚函数表。
关于第4点,我们可以通过下面的代码来验证:
Derive derive;
Base base = derive;
long* pvptr = (long*)&base; //因为对象的首地址是虚函数表指针
long* vptr = (long*)(*pvptr); //指针的值就是指向的虚函数表
//定义函数指针,指向 void (void)函数
typedef void (*Func)(void);
//因为虚函数表是个数组,所以我们可以通过vptr[i]的方式来调用对应的虚函数
Func vir_f = (Func)vptr[0];
Func vir_g = (Func)vptr[1];
Func vir_h = (Func)vptr[2];
vir_f();
vir_g();
vir_h();
输出结果如下:
Base::vir_f()
Base::vir_g()
Base::vir_h()
3.2 虚函数的动态绑定
前面说过:在把子类对象赋值给父类时,会用子类对象的值去初始化父类对象,但父类对象的虚函数表指针指向的依然是它自己的虚函数表。
Derive derive;
Base base = derive;
base.vir_f(); //输出Base::vir_f()
这里读者是否有疑问,这个base是用derive对象赋值的,应该调用Derive类的 vir_f()方法才对啊?
这里,必须说一下“静态绑定”、“动态绑定”这2个概念。
(1)静态绑定:绑定的是静态类型,所对应的函数或者属性依赖对象的静态类型,发生在编译期。
(2)动态绑定:绑定的是动态类型,所对应的函数或者属性依赖对象的动态类型,发生在运行期。
虚函数是动态绑定的,Base base = derive;只是把derive对象是值赋给了base对象,base对象已经确定了,没有动态绑定的动作,所以base.vir_f()只能调用自己的函数。要调用子类的虚函数,必须用类指针去动态调用。
Derive derive;
Base* pb = &derive; //输出Derive::vir_f()
pb->vir_f();
Base& b = derive; //输出Derive::vir_f()
b.vir_f();
3.3 多重继承
我们在前面例子的基础上,加入一个新类Base2:
class Base2 {
public:
int b2_i = 10;
virtual void vir_f2() {
std::cout << "Base2::vir_f2()" << std::endl;
}
virtual void vir_g2() {
std::cout << "Base2::vir_g2()" << std::endl;
}
virtual void vir_h2() {
std::cout << "Base2::vir_h2()" << std::endl;
}
};
然后让Derive类继承这个Base2类:
class Derive :public Base, public Base2 {......}
derive对象的内存布局如下:
对于多重继承下的虚函数,有几个要点:
(1)一个类如果继承自多个父类,且父类都有虚函数,则子类就会有多个虚函数表。
(2)类有几个虚函数表,其对象就会有对应的几个虚函数表指针。
(3)子类对象与第一个基类共用1个虚函数表指针。