【C++】面向对象编程的三大特性:深入解析多态机制(二)https://developer.aliyun.com/article/1617395
九、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行,调用具体的函数,也成为动态多态。
十、单继承与多继承的虚函数表
10.1 单继承的虚函数表
class Base { public : virtual void func1() { cout<<"Base::func1" <<endl;} virtual void func2() {cout<<"Base::func2" <<endl;} private : int a; }; class Derive :public Base { public : virtual void func1() {cout<<"Derive::func1" <<endl;} virtual void func3() {cout<<"Derive::func3" <<endl;} virtual void func4() {cout<<"Derive::func4" <<endl;} private : int b; };
调试窗口进行观察(不够准确)
子类继承了父类虚表,得到了Func2虚函数及其Func1完成了虚函数的重写;问题在于监视窗口观察不到Func3和Func4,这里是编译器的监视窗口故意隐藏。
内存窗口进行观察
如果通过内存窗口来观察的话,虽然我们可以大致确定就是Func3和Func4虚函数的地址,但是如何证明呢?这里就需要使用到了打印虚表中函数了
10.2 打印虚表中函数
通过调式窗口来看,虚表指针是存储在头4字节上的,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
如果是函数指针数组话,类型是难以书写,可以使用typedef对于类型重定义typedef void(*VFPTR) ();
(这里数组指针和函数指针重定义写法是比较特殊的)
如果需要取头4个字节,能不能直接强转为int类型就行。这里强转是没有用的,只有相同类型才能进行强制类型转化,那么怎么办?
10.2.1 指针高级用法
打印虚表中虚函数地址实现步骤:
步骤:
- 先取b的地址,强制成一个int*的指针,指针可以随便转,指针本质是地址编号是整型,虽然不能直接转化为int类型,但是可以通过int *类型的指针间接的转化,是一种指针高级用法。
- 再解引用取值,就得到了b对象头4个字节的值,这个值就是指向虚表的指针
- 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组
- 虚表指针传递给printVTTable进行打印虚表
- 需要声明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题,我们只需要清理解决方案,在次编译就行了
//得到数据,重新定义个函数指针数组 VFPTR* vTableb = (VFPTR*)(*(int*)&b); PrintVTable(vTableb); VFPTR* vTabled = (VFPTR*)(*(int*)&d); PrintVTable(vTabled);
打印虚表中虚函数地址函数逻辑:
void PrintVTable(VFPTR vTable[]) { // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数 cout << " 虚表地址>" << vTable << endl; for (int i = 0; vTable[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; }
10.3 多继承中虚函数表
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; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) { cout << " 虚表地址>" << vTable << endl; for (int i = 0; vTable[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; } int main() { Derive d; VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); PrintVTable(vTableb1); VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1))); PrintVTable(vTableb2); return 0; }
从上面的可以观察出来,多继承体制中派生类是继承了两张虚表,同时继承下来的虚函数是不同的,至于为什么不放在一张虚表,可以想一下切片,如果只有一个切片,如何实现多态的指向谁调用谁的逻辑呢?
10.3.1 打印多继承中第二张虚表中虚函数的地址
第一种办法 :
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)) PrintVTable(vTableb2);
第一种办法使用指针运算法则进行移动指针指向位置,但是只适应不考虑内存对齐等因素情况下。由于内存对齐等因素,可能会导致会导致指向错误。更加推荐下面通过取地址直接访问的办法
第二种方法:
十一、菱形继承、菱形虚拟继承
实践种我们不建议设计出菱形继承、菱形虚拟继承,一方面太复杂容易出现问题,另一方面这样的模型,访问基类成员有一定性能损耗。所以继承、菱形虚拟继承继承虚表情况,我们不就不需要看了,一般我们也不需要研究清楚,实践中也很少用,如果需要了解通过下面两篇链接文章。
11.1 菱形虚拟继承(简单了解)
菱形虚拟继承,每个类都有一个虚函数,除了虚表指针也有我们的虚基表指针。这里虚基表有存储两个偏移量一个是距离虚表的偏移量和距离共享虚基类A的偏移量。
这里由于虚基类A是共享的,B C类的虚函数不能放进去,所以只能单独建立虚表。没有继承父类的虚表,这里是不能利用父类的虚表,不能放放我自己的虚函数,A是共享,派生类单独建立虚表
十二、相关面试题
- inline函数可以是虚函数吗?
- 答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
- 静态成员可以是虚函数吗?
- 答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?
- 答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
- 答:可以,并且最好把基类的析构函数定义成虚函数。
- 对象访问普通函数快还是虚函数更快?
- 答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?
- 答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!