> 文章列表 > C++ - 多态(2) | 虚表的打印、单继承与多继承的虚表

C++ - 多态(2) | 虚表的打印、单继承与多继承的虚表

C++ - 多态(2) | 虚表的打印、单继承与多继承的虚表

之前的文章中我们介绍了多态的原理以及虚表的知识,本文中我们将继续对多态进行更加深入的学习。

单继承中的虚函数

虚函数表(虚表)的本质是一个函数指针数组,虚函数的地址会进入虚表。首先我们来看一下这样的一段代码:

class Base {public:Base(): _b(10) {++_b;}virtual void Func1() {cout << "Base::Func1()" << endl;}virtual void Func2() {cout << "Base::Func2()" << endl;}void Func3() {cout << "Base::Func3()" << endl;}private:int _b = 1;
};class Derive : public Base {public:virtual void Func1() {cout << "Derive::Func1()" << endl;}virtual void Func4() {cout << "Derive::Func4()" << endl;}private:int _d = 2;
};int main() {Base b;Derive d;return 0;
}

在这段代码中Derive公有继承了Base,在Derive中虚函数重写了Func1函数,又写了一个Func4函数,然后我们对这段代码进行进行调试,发现了一个很神奇的现象,虽然Func4也是虚函数,但是它并没有显示在调试窗口里面,这是就会怀疑,这个表并没有展示完全,再通过内存窗口进行查看会发现有三个很相近的地址,这很有可能就是在Derive中的三个虚函数。下面我们便试图将虚函数表中的虚函数进行打印,这样可以更好的进行观察。(在vs系列的编译器中,编译器会在虚表的末尾添加上nullptr,g++没有)

vs下虚表的打印

编写一个打印虚表的函数得到的结果可以看出我们的猜测没有错误,在内存中的确实是Derive的虚表:

 下面就是打印虚表使用的函数:

// 用程序打印虚表
typedef void(*VF_PTR)(); // 声明一个函数指针,函数指针与一般的typedef不同,不能够写成这样的形式 typedef void(*)() VF_PTR//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table){for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main(){Base b;Derive d;// 有两种方式:// 第一种方式有缺陷只能在32位的系统下使用,若是转换到了64位的系统就需要使用一种占据八个字节的指针// 第二种方式就是直接使用*(VF_PTR**)的方法//PrintVFTable((VF_PTR*)*(int*)&b);//PrintVFTable((VF_PTR*)*(int*)&d);PrintVFTable(*(VF_PTR**)&b);PrintVFTable(*(VF_PTR**)&d);return 0;
}

其余的一些问题

从上述中会有这样的问题:虚表是在什么时候生成? -- 编译;对象中的虚表指针是在什么时候初始化的? -- 构造函数的初始化列表;这从下图中即可看出走出构造函数之后虚表指针就有了值。

虚表存在哪里?下面我们来打印几个地址来进行判断:可以推断出虚表是存放在常量区的

动态绑定与静态绑定 

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。 

多继承中的虚函数表

下面我们再来看一段代码:

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main(){Derive d;return 0;
}

在上述的代码中我们重写了func1,添加了虚函数func3,进行调试会发现:Derive类中的func3函数也是虚函数但是与之前一样并没有不能在内存窗口中查看到,那么它到底存储在哪里?是在base1的虚表中,还是base2的虚表中,又或者是两张虚表中都会存在?于是我们便可以使用前面打印虚表的方法来进行查看。于是编写函数进行打印,可以看到虚函数func3存放在了第一张虚表中:

Derive d;
PrintVFTable((VF_PTR*)(*(int*)&d));
PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1)))); // 这里先要将&dDerive*类型的地址转换为char*的地址才能加上Base1的大小,在与之前一样转换为int*类型再解引用转换成VF_PTR*类型即可
//Base2* ptr2 = &d; // 简便的方法是我们可以对Derive类进行切片获得的自然就是Base2的地址。
//PrintVFTable((VF_PTR*)(*(int*)(ptr2)));

func1函数已经进行了多态的重写,但是在两张虚表中的地址却不相同,这点让人感到非常的奇怪。

下面我们来从汇编的角度来看一下为什么:可以看到Base1的指针直接就指向了func1函数,而Base2的指针需要转好几层才能指向func1函数

这里有一句关键指令sub ecx,8,ecx中一般存放的是this指针,这里做的就是修正this指针的位置,这里减少的8就是Base1的大小。再结合上述的信息就可以得到因为在继承中先继承的是Base1,后继承的是Base2,ptr1调用的时候直接就是虚表的地址,而ptr2调用的时候前面还有一个Base1大小的空间因此无法直接访问,需要先对this指针进行相关处理。下面我们将base1的大小进行修改,再次查看:当对Base1添加一个int类型的成员,我们会看到ecx后面跟的数变成了0Ch也就是12,即虚表指针加上两个int对象的大小。

如果继承的顺序发生了改变,同样修正的对象也会发生改变:

菱形继承中虚拟继承的问题

菱形虚拟继承过于复杂 ,这里不过多的讲解,在这说明之前我们在菱形虚拟继承中留下的一个问题,之前虚基表中的偏移量的第一位全是0,但是在这里可以发现并不是0,而变成了fcffffff,这个数转换成十进制就是-4,可以理解为到首地址的偏移量或者到虚标的地址的偏移量。