只是记录下自己的理解,不对的地方还请指出,不胜感激。
最近发现,在多继承中,第二顺位及之后的虚函数表,某些情况下(派生类实现了基类)保存的地址并不是虚函数的实际地址,会通过thunk对象做一次跳转。
网上查阅了很多文章,基本都是一句盖过,大概意思就是:
需要调整this指针的指向,使其指向正确的对象内存,达到this调用的准确性。
这个概念比较笼统,初学者不太容易理解。。
为了更容易理解,我们先看几个例子,编译器为vs2022
看个例子:
class Son1
{
public:
virtual void fun1() {}
public:
int var_a;
};
class Son2
{
public:
virtual void fun1() {}
public:
int var_b;
};
class Grandson :public Son1, public Son2
{
public:
virtual void fun1()
{
var_a = 1; //同时对Son1和Son2的属性赋值
var_b = 2;
var_g = 3;
}
public:
int var_g;
};
int main(int) {
Grandson obj;
Son1* son1 = &obj; // 指向地址:0
Son2* son2 = &obj; // 指向地址:8
Grandson* grandson = &obj; // 指向地址:0
son1->fun1();
son2->fun1();
grandson->fun1();
}
Grandson 对象内存图
从上面的内存布局中,可以看出,son1和grandson起始地址都是0,son2起始地址为8,
虚函数表2中,并没有直接保存函数地址,而是进行了thunk跳转,为什么呢?
原因: 由于fun1在Son1和Son2中都存在,又在Grandson中重写了(如果不重写,编译器会报两仪性错误,共同子类Grandson不知道调用哪个父类实现),fun1中对son1和Son2中的属性赋值了,如果虚函数表2中直接保存函数地址,当son2->fun1(); 到表2中找到Groundson::fun1直接调用,我们都知道,成员函数调用需要传如this,而这里的this = son2,由son2指向地址8,fun1中又对son1的变量有赋值,如果不对this指针做偏移,直接用son2指针(地址指向8)是操作不到son1的内存的。这里就是上面的概念的意义所在了。。而vs采用的是thunk跳转调整this指针(&thunk: this-=8),当然你也可以自己实现跳转,不用thunk。。视情况而定。
二:在什么情况下第二虚函数表中会用thunk跳转呢
派生类C有两个基类A和B,其中A和B中存在虚函数show()(不管是继承而来还是原生的),且C类重写了虚函数show(),这样的C类中的第二张虚函数表(B类实例)里边保存的表项就不是D::show()虚函数地址。
// 下面是一些概念理解,可以不用看
1. 指针的调用取决于指针指向的内存地址和指针类型(数据的内存布局)
2. 在继承链中,子对象的内存中会依次包含基类独立的内存布局,并且对齐方式一致。
3. 单继承下,子对象中包含的基类对象的内存起始地址就是子对象的内存的起始地址;多继承下,单链中还是一样,后继基类对象的内存起始地址依次偏移排列。如下:
(1) 自然链(单继承链):
// 单链继承
class Base
{
public:
virtual void fun1() {}
public:
int a;
};
class Son1 :public Base
{
public:
virtual void fun1() {}
public:
int a;
};
class Grandson :public Son1
{
public:
virtual void fun1() {}
public:
int a;
};
PS: 在自然链中,每个类对象的起始地址都是0,也就是起始地址一样
(2) 多重继承:
// 多重继承
class Son1
{
public:
virtual void fun1() {}
public:
int a;
};
class Son2
{
public:
virtual void fun1() {}
public:
int a;
};
class Grandson :public Son1, public Son2
{
public:
virtual void fun1() {}
public:
int a;
};
可以看到第一链(Son1)起始地址都是0,第二链(Son2)的起始地址为8,可以
看出,非自然链的基类对象内存依次偏移排列。
4. 非多态(不含虚函数)的继承链中,
在非多态继承中,指针类型决定调用内存,要实现多态,需要虚函数支撑,如:
Son1* ptr1 = new Grandson; // ptr1只能的作用域仅仅为Son1的内存块
Son2* ptr2 = new Grandson; // ptr1只能的作用域仅仅为Son2的内存块