一、虚函数及虚函数表的定义
虚函数:虚函数就是在基类中定义一个未实现的函数名,使用虚函数的核心目的就是通过基类访问派生类定义的函数,从而实现运行时多态。
虚函数主要有如下限制:
- 只有类的成员函数才能声明为虚函数。
- 静态成员函数不能是虚函数。
- 内联函数不能是虚函数。
- 构造函数不能为虚函数。
- 基类的析构函数可以是虚函数且通常声明为虚函数。
虚函数表:对于一个类来说,如果类中存在虚函数,那么该类的大小就会多出4个字节,这刚好是一个指针的大小,而这个指针指向的就是一个虚函数表。所以如果对象存在虚函数,那么编译器就会生成一个指向虚函数表的指针,所有的虚函数指针都存在于这个表中,虚函数表就可以理解为一个函数指针数组,每个单元用来存放虚函数的地址。
虚函数表位于只读数据段(.rodata),即C++内存模型中的常量区,虚函数代码则位于代码段(.text),也就是C++内存模型中的代码区。二者在内存中的位置大致如下:
二、虚函数表指针和虚函数表的创建时机
虚函数表指针:对于虚函数表指针vptr来说,由于vptr是基于对象的,所以对象在实例化的时候,vptr就会被创建,即在运行时创建。即在实例化对象的时候调用构造函数,执行vptr的赋值代码,从而将虚函数表的地址赋值给vptr。不过更具体地说,其实在编译时vptr就会被放到类的内存空间里,只是我们看不到而已,但是只有在类实际构造时vptr才会被真正实例化,指向虚函数表,具有实际意义。
虚函数表:对于虚函数表来说,在编译的过程中编译器就会为含有虚函数的类创建虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建。
三、虚函数实现多态的原理
虚函数的调用过程:当一个对象需要调用虚函数时,先通过对象内存空间中的虚函数表指针(vptr)找到该类对应的虚函数表(vtbl),然后在vtbl中通过虚函数指针寻找想要调用的虚函数,进而完成虚函数的调用。
以派生类重写了部分基类虚函数的多继承为例,考虑以下代码:
#include <iostream>
using namespace std;
class BaseA {
public:
virtual void a1() {
cout << "Base a1()" << endl; }
virtual void a2() {
cout << "Base a2()" << endl; }
};
class BaseB {
public:
virtual void b1() {
cout << "Base b1()" << endl; }
virtual void b2() {
cout << "Base b2()" << endl; }
};
class Derive : public BaseA, public BaseB {
public:
void a1() override {
cout << "Derive a1()" << endl; }
void b1() override {
cout << "Derive b1()" << endl; }
};
int main() {
cout << "----------BaseA----------" << endl;
BaseA *base_a = new BaseA;
long *vptr_a = (long *) base_a; // base_a的虚函数表指针
long *vtbl_a = (long *) (*vptr_a); // base_a的虚函数表
for (int i = 0; i < 2; i++) {
printf("BaseA-vtbl[%d]: %p\n", i, vtbl_a[i]);
}
cout << "----------BaseB----------" << endl;
BaseB *base_b = new BaseB;
long *vptr_b = (long *) base_b; // base_b的虚函数表指针
long *vtbl_b = (long *) (*vptr_b); // base_b的虚函数表
for (int i = 0; i < 2; i++) {
printf("BaseB-vtbl[%d]: %p\n", i, vtbl_b[i]);
}
cout << "----------Derive----------" << endl;
Derive *derive = new Derive;
long *vptr_d_a = (long *) derive; // derive的第一个虚函数表指针
long *vtbl_d_a = (long *) (*vptr_d_a); // derive的第一个虚函数表
long *vptr_d_b = (long *) derive + 1; // derive的第二个虚函数表指针,加一表示偏移一个long的长度,刚好越过第一个虚函数表指针
long *vtbl_d_b = (long *) (*vptr_d_b); // derive的第二个虚函数表
for (int i = 0; i < 2; i++) {
printf("Derive-BaseA-vtbl[%d]: %p\n", i, vtbl_d_a[i]);
}
for (int i = 0; i < 2; i++) {
printf("Derive-BaseB-vtbl[%d]: %p\n", i, vtbl_d_b[i]);
}
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -w -std=c++11
atreus@MacBook-Pro % ./main
---------- BaseA ----------
BaseA-vtbl[0]: 0x1028aefa8
BaseA-vtbl[1]: 0x1028aefe4
---------- BaseB ----------
BaseB-vtbl[0]: 0x1028af044
BaseB-vtbl[1]: 0x1028af080
---------- Derive ----------
Derive-BaseA-vtbl[0]: 0x1028af11c
Derive-BaseA-vtbl[1]: 0x1028aefe4
Derive-BaseB-vtbl[0]: 0x1028af194
Derive-BaseB-vtbl[1]: 0x1028af080
atreus@MacBook-Pro %
本例对应内存分配模型为:
当子类重写了父类的虚方法后,会在代码段中生成属于自己的虚函数。而多态的函数调用语句在被编译后,会根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的一系列指令,从而实现多态。
参考:
https://cloud.tencent.com/developer/article/1599283
https://blog.csdn.net/qq_41963107/article/details/107852352
https://blog.csdn.net/weixin_44693625/article/details/117393578
https://blog.csdn.net/bailang_zhizun/article/details/117124494