C++动态绑定机制,及多重继承虚函数表的分布
C++如果想满足动态绑定, 及基类指针或引用调用派生类函数,需要满足三个条件:
1. 基类存在虚函数
2. 基类指针或引用指向派生类对象
3. 派生类需要重写基类的虚函数
class A
{
public:
virtual void func()
{
cout << “A::func” << endl;
}
int a;
};
class B :public A
{
public:
int b;
};
int main()
{
A* p = new B;
p->func(); //此时会调用 A::func 函数
}
此时 A* p 指针会指向 B类对象 内存布局中的 A类的基类子对象 ,从而找到vptr(虚函数表指针),接着找到B类中的虚函数表,由于B类中并未重写A类虚函数,所以使用 A*p 指针调用func函数会调用到 A::func
class A
{
public:
virtual void func()
{
cout << “A::func” << endl;
}
int a;
};
class B :public A
{
public:
void func()
{
cout << “B::func” << endl;
}
int b;
};
int main()
{
A* p = new B;
p->func(); //此时会调用 B::func 函数
}
如果 B 类重写了,基类A中的虚函数,那么B类的虚函数表中对应的func函数的地址就会被改为&B::func ,所以此时用 A *p 指针调用func函数 , 就会调用到 B::func , 调用流程大概是:
p指针先指向了A类的基类子对象 -> 接着找到了vptr指针 -> 接着找到B类中的虚函数表 -> 调用func函数
需要注意的有以下几点:
1.如果是单继承,派生类中仅含有一个vptr(虚函数表指针),该指针继承与基类
2.每个类仅有一个虚函数表(如果是多继承的话可以理解为多个),派生类的虚 函数表中的数据(也就是函数地址),复制于基类的虚函数表,如果派生类中重写了基类虚函数,那么该派生类的虚函数表中对应的基类虚函数地址,会更改为派生类重写后的函数地址,也就是派生类的函数地址
3.基类和派生类的虚函数表不是同一个,每个类有属于自己的虚函数表,派生类虚函数表只是复制于基类
下面讨论多继承的情况下,虚函表是怎样生成的
class A
{
public:
virtual void funcA()
{
cout << “A::funcA” << endl;
}
int a;
};
class B :public A
{
public:
virtual void funcB()
{
cout << “B::funcB” << endl;
}
int b;
};
class C :public A, public B
{
public:
int c;
};
int main()
{
C c;
A* a_p = &c;
B* b_p = &c;
}
此时 C 类有两个基类 A和B , 所以 C 类中有两个vptr, 这两个vptr分别指向两个虚函数表, 这两个虚函数表都属于C类 , 其中的数据分别复制与 A 和 B 的虚函数表 , 如果此时使用 A* a_p 指针调用funcA函数,由于C类中并未重写该虚函数,已导致虚函数表中的地址还是&A::funcA, 所以会调用到 A::func, 如果使用B*b_p指针调用funcB函数也是如此 , 他们都会指向C类对象内存布局中属于该指针类型的基类子对象,从而找到vptr,调用虚函数表中对应的函数,
如果C类重写了虚函数,那么重写了哪一个基类的虚函数,则该基类子对象内的vptr指针指向的虚函数表中的函数地址就会改变为重写后的函数地址, 以下就不各个列举了
为什么前面会说,如果多继承的情况下可以理解为有多个虚函数表,实际上也可以理解为一个大表,因为经过测试每个虚函数表的地址是连续的,如果理解为多个虚函数表应该会更便于理解,下面就说一下连续的情况,还拿上一个多继承的例子为例
多继承情况下虚函数表的布局大致就如上面所示,可以理解为是一个数组,每个复制与基类的虚函数表都存放在这个数组中,每个基类间用NULL分割 , 这也可以解释为什么虚函数表的最后一个元素值(地址)会为NULL了
还有一种情况就是,如果C类自身拥有虚函数,那么会生成一个新的虚函数表么,答案是不会的,如果C类自身拥有虚函数的话,那么这个函数地址会被添加到复制基类的虚函表最后一个位置中,如果是多继承的情况下,会被添加到继承顺序最先继承的基类虚函数表中,大致如下
如果有小伙伴想测试我上述所说的结论,但是不知道怎么测试的话,下面这个范例供参考
class A
{
public:
virtual void func()
{
cout << “A::func” << endl;
}
int a;
};
typedef void (*func_p)(void);
int main()
{
A* p = new A;
int** m = (int**)p;
((func_p)m[0][0])(); //此处会调用A::func函数 m[0]就是虚函数表的地址 m[0][0]就是虚函数表中第一个函数地址,也就是A::func 可以把虚函数表理解为一个数组里面存放函数指针
}
需要注意的有以下几点
1.如果用基类指针指向了派生类对象,并不是指向了该派生类对象的首地址,而是会指向该派生类中基类子对象的首地址
2.多继承情况下,会按继承顺序生成基类子对象,如果是上述例子中也就是先生成A后生成B,最后生成C类自己的成员
3.一般情况下vptr指针会在基类子对象的类成员上方,(有一种解释是为了效率,不用进行偏移就可以找到vptr指针从而找到虚函数表)
4.如果基类虚函数中参数含有缺省值(默认值),这个缺省值并不会实现多态,即便调用了派生类函数,但是这个缺省值还是来自于基类函数,(为了执行的效率,缺省值并不实现多态),这里就不测试了
5. 以上所有测试结论来自于我本身所用的环境 VS2019 32位 环境下 ,各各编译器实现不一定相同,这里不做多的探究了