概要
大家应该听过这样如下面试题:动态多态是如何实现的?虚表指针能否继承?虚函数表呢?
本文对调试信息进行分析后得出结论,系统:ubuntu-20.04 64位,编译器: gcc version 13.1.0
普通继承
- 1.首先有如下继承关系,Son -> Father -> Grandpa,并且Son、Father类均对Grandpa类中的虚函数print进行了重写
#include <iostream>
using namespace std;
class Grandpa
{
public:
virtual void print()
{
cout << "grandpa" << endl;
}
int age = 0x12345678;
};
class Father: public Grandpa
{
public:
virtual void print()
{
cout << "father" << endl;
}
char ch = 'a';
};
class Son : public Father{
public:
virtual void print()
{
cout << "son" << endl;
}
short money = 0x6666;
};
int main(){
Grandpa gp;
Father fa;
Son s;
size_t len_gp = sizeof(gp);
size_t len_fa = sizeof(fa);
size_t len_s = sizeof(s);
return 0;
}
2.编译后使用gdb在最后一行打上断点,运行到断点处
3.查看3个对象分别占用的字节数,可以发现都是16字节
4.查看三个对象的数据,并在内存中查看其对象的地址
小结
可以看到,在普通继承中,无论继承了多少次,都只有一个虚表指针,该虚表指针存放于对象的首地址。注意虚表指针不是直接指向虚表的首地址,而是指向虚表首地址向后偏移16个字节的位置 。
问题:那为什么虚表指针不直接指向虚表而是有16个字节的偏移?
5.通过类Son的虚表指针减去16个字节的偏移再查看虚表内容,结果如下图
可以看到, _ZTV3Son是gcc中name mangle,表示类Son的虚表信息,son的虚表中,第一行为空行;第二行
指向的区域是_ZTI3Son,通过x /16bx 查看内存可知_ZTI3Son为类的type info信息;第三行就是Son的虚表指针指向的位置,通过 info symbol 0x555555555298可知,第三行为Son::print()函数的地址。
从以上内存布局可以看出,类Grandpa、Father、Son的虚表中print函数在不同的内存地址。
问题:那么在动态多态时,编译器是如何知道该调用哪一个类的虚表中的函数呢?
虚函数的偏移
首先虚函数的偏移是不变的,下面通过一个例子来说明。
#include <iostream>
using namespace std;
class Grandpa
{
public:
virtual void print()
{
cout << "grandpa" << endl;
}
virtual void show()
{
cout << "grandpa" << endl;
}
int age = 0x12345678;
};
class Father: public Grandpa
{
public:
virtual void display()
{
cout << "father" << endl;
}
virtual void print()
{
cout << "father" << endl;
}
char ch = 'a';
};
class Son : public Father{
public:
virtual void look(){
}
virtual void print()
{
cout << "son" << endl;
}
short money = 0x6666;
};
int main(){
Grandpa gp;
Father fa;
Son s;
return 0;
}
打上断点并查看三个对象的虚表内容,结果如下图所示:
可以看到,依照在代码中虚函数的声明顺序,print()函数的偏移始终为0,show()函数的偏移始终为1,无论是在对象gp、fa,还是s中,偏移顺序始终不变。这样,编译器就可以通过虚函数的声明顺序得到函数名和偏移之间的映射。而指向派生类对象的基类指针ptr,不管指向哪个对象,都可以通过ptr指向的对象首址,来获得虚表指针。这样通过虚表指针进一步指向虚表中的函数,再通过函数名与偏移的映射就能找到正确的虚函数,函数调用可看作如下通用代码(*ptr->vptr[offset])()(注意优先级)。
总结
所以可以看到在Ubuntu环境下,虚表指针并没有继承,虚函数表也没有继承。