Ⅱ. 多态的原理
0x00 运行时决议与编译时决议
我们刚才知道了,多态调用实现是靠运行时查表做到的,我们再看一段代码。
💬 在刚才代码基础上,让父类子类分别多调用一个 Func3,注意 Func3 不是虚函数:
class Base { public: 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; } private: int _d = 2; }; int main(void) { Base b; Derive d; Base* ptr = &b; ptr->Func1(); // 调用的是父类的虚函数 ptr->Func3(); ptr = &d; ptr->Func1(); // 调用的是子类的虚函数 ptr->Func3(); return 0; }
🚩 运行结果:
❓ 问题:这里 Func3 为什么不是 Derive 的?
💡 解答:因为 Func3 不是虚函数,它没有进入虚表。
如果我们从更深的角度 —— 汇编层面去看,就可以牵扯出编译时决议和运行时决议。
(这个我们前面一直再提,我们现在就来好好讲讲~ 乖♂乖♂站♂好 )
决议的意思就是如何去确定函数的地址,一个是在运行时确定,一个是在编译时确定。
📚 多态调用:运行时决议,即运行时确定调用函数的地址。【通过查虚函数表】
(编译完后通过指令,去对象中虚表里去找虚函数运行,是运行时去找,找到了才调用)
📚 普通调用:编译时决议,编译时确定调用函数的地址。【通过类型】
(所有的编译时确定都是看 ptr 是什么类型,跟对象没有关系,不看指向的对象,自己是什么类型,就去哪里找 Func1)
(查看反汇编)
这正是多态底层实现的原理,编译器去检查,如果满足多态的条件了,它就按运行时决议的方式。
0x01 动态绑定与静态绑定
静态库:指的是链接的那个阶段链接的库。
动态库:程序运行起来后才加载,去动态库里找。
静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如函数重载。
动态绑定:又称后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也成为动态多态。
0x02 静态的多态和动态的多态
多态在有些书上还细分了静态的多态和动态的多态。
静态的多态(编译时):指的是函数重载。
int x = 0, y = 1; double a = 0.0, b = 1.1; swap(x, y); swap(a, b); 这两个 swap 让人感觉是同一个函数, 但实际不是。实际编译链接根据函数名修饰规则找到不同的函数
动态的多态(运行时):指的是本节内容讲的这个。
void Func(Person& p) { p.BuyTicket(); } Person Mike; Func(Mike); Student Jack; Func(Jack);
Ⅲ. 单继承与多继承关系的虚函数表
0x00 单继承中的虚函数表
(需要注意的是,在单继承和多继承关系中,下面我们去关注的是子类对象的虚表模型,因为父类的虚表模型我们前面已经看过了,没什么需要特别研究的地方)
💬 代码:单继承中的虚函数表:
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; } void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; };
我们还是用刚才介绍的方法打印虚表:
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() { Base b; Derive d; }
💬 代码:我们在把虚函数表打印出来看看(32位取头上4个字节,64位需要取头上8个字节):
int main() { Base b; Derive d; PrintVTable((VFPTR*)(*(int*)&d)); return 0; }
🚩 运行结果:
0x01 多继承中的虚函数表
刚才我们看的是单继承,我们现在再看复杂一点的多继承。
💬 代码:Base1 和 Base2 都进行了重写
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; }
这里 Derive 明显会有两张虚表,我们先透过监视简单看一下:
我们的 func3 是放哪一个虚表里?是两张都放一份,还是选择一份放呢?
func1 的两个地址好像不一样,0X0911ae 和 0X901249,因为它们都不是真正的函数的地址。
我们来看看 Derive 中的 func1 真正的地址:
printf("%p\n", &Derive::func1);
这里可能就是多套了一层,是一种保护机制。虽然不一样但是最后都跳到了函数上面去。
🔺 结论:Derive 对象 Base2 虚表中 func1 时,是 Base2 指针 ptr2 取调用,但是这时 ptr2 发生切片指针偏移,需要修正。中途就需要修正存储 this 指针 ecx 的值。
❓ 问题:这里还有一个指针偏移的问题,在多继承中这三个指针的值是一样的吗?
Base1* ptr1 = &d; Base2* ptr2 = &d; Derive* ptr3 = &d; cout << ptr1 << endl; cout << ptr2 << endl; cout << ptr3 << endl;
🚩 运行结果:0073FBA0 0073FBA8 0073FBA0
💡 答案:不一样。给人第一感觉好像是一样的,因为赋过去的值都是 &d,但实际上并不一样。
因为这里要发生切片,切片后赋值兼容,所以它们的地址就不一样了。
0x02 多态的一些题目
1. 什么是多态? 2. 什么是重载、重写(覆盖)、重定义(隐藏)? 3. 多态的实现原理?答:参考本节课件内容 4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为 虚函数要放到虚表中去。 5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。 6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。参考本节课件内容 8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码 段(常量区)的。 10. C++菱形继承的问题?虚继承的原理?