多态(虚表,动态/静态绑定) C 多态完全指南从原理到面试目录什么是多态为什么要有多态怎么用多态静态多态 — 重载 模板静态绑定动态多态 — 继承 虚函数动态绑定虚函数虚函数的表指针虚函数表虚表面试题静态 / 动态绑定override 和 final 关键字overridefinal重载重写/覆盖隐藏什么是多态同一行为(接口/方法)作用于不同对象时表现的不同实现结果为什么要有多态多态存在的唯一目的就是让上层代码调用者无需关心底层具体类型从而在不修改已有代码的前提下通过新增子类无限扩展新功能。怎么用多态多态有两种形态一个时运行时多态(动态多态)一个是编译时多态(静态多态)静态多态-重载 模板核心特征编译器在编译阶段就确定了调用哪个函数的具体地址生成对应的机器码。1.重载#include iostream using namespace std; // 编译时编译器记录了两个不同名字修饰Name Mangling的函数 void print(int x) { cout 打印整数: x endl; } void print(double x) { cout 打印浮点数: x endl; } int main() { int a 10; double b 3.14; // 编译时编译器看到 a 是 int直接在这行生成 call _Z5printi固定地址 print(a); // 编译时编译器看到 b 是 double直接在这行生成 call _Z5printd固定地址 print(b); return 0; }2.模板#include iostream using namespace std; // 模板编译时编译器会根据调用类型生成两份独立函数 // 对于 int生成 int maxValue_int(int a, int b) // 对于 double生成 double maxValue_double(double a, double b) template typename T T maxValue(T a, T b) { return (a b) ? a : b; } int main() { // 编译时生成 int 版本的机器码并直接 call 这个地址 cout maxValue(3, 5) endl; // 编译时生成 double 版本的机器码并直接 call 这个地址 cout maxValue(3.14, 2.71) endl; return 0; }静态绑定是什么编译期决定“调用哪个函数地址”或“取哪个值”。编译器看着代码的静态类型声明时的类型直接写死地址或数值运行时不再改变。编译期直接写死值不改变。什么情况会出现情况说明举例① 调用非虚函数编译器直接写死call A::eatp-eat()eat非虚永远调A的版本。② 函数重载编译期根据参数类型匹配print(1)调print(int)print(1.0)调print(double)。③ 模板实例化编译期生成具体类型的代码maxint(1,2)生成整型版本。④ 通过对象而非指针/引用调用虚函数编译器明确知道类型直接静态绑定甚至内联展开。B b; b.func();即使func是虚函数也直接写死调B::func。⑤ 默认参数的取值大陷阱即使函数体是动态查的默认参数的值在编译期就定死了。下面那道题val1就是在编译期静态绑定的。动态多态-继承虚函数核心特征编译时只检查语法父类有没有这个方法运行期才根据实际对象去虚表vtable里查找。实现它必须满足两个要求:1.必须是基类的指针或者引⽤调⽤虚函数 2.被调⽤的函数必须是虚函数并且完成了虚函数重写/覆盖。#include iostream using namespace std; class Animal { public: // 虚函数编译时编译器知道要生成虚表vtable // 此时会在对象中预留一个隐藏指针vptr virtual void speak() { cout 动物发出某种声音 endl; } virtual ~Animal() {} // 虚析构保证正确释放 }; // 子类1重写 speak class Dog : public Animal { public: void speak() override { // override 是 C11 关键字提高可读性 cout 旺财: 汪汪汪 endl; } }; // 子类2重写 speak class Cat : public Animal { public: void speak() override { cout 咪咪: 喵喵喵 endl; } }; // 一个全局函数接受父类引用多态的经典用法 引用调用 void makeSound(Animal animal) { // 问题来了这行代码编译时编译器只知道 animal 是 Animal // 但运行时传进来的可能是 Dog也可能是 Cat animal.speak(); } //如果是 makeSound(Animal animal)的话它无法分辨因为传入变量不再是之前那个变量 int main() { Dog dog; Cat cat; Animal* animal dog; animal-speak();//基类的指针执行Dog::speak() // 编译时编译器检查 makeSound 接受 Animaldog 是 Dog 类可以隐式转换通过编译。 // 运行时makeSound 函数里的 animal 引用实际绑定的是 Dog 对象 // CPU 会去读取 dog 内存里的 vptr找到 Dog 的虚表执行 Dog::speak() makeSound(dog); // 编译时同上编译器通过。 // 运行时CPU 读取 cat 内存里的 vptr找到 Cat 的虚表执行 Cat::speak() makeSound(cat); return 0; }动态绑定是什么动态绑定Dynamic Binding运行期决定“调用哪个函数地址”。编译器不写死地址而是生成查虚表vtable的指令运行时根据动态类型实际对象类型跳转。编译期不写死地址运行期决定调用哪个地址什么情况会出现必要条件说明① 函数必须是虚函数有virtual普通函数不配查表。② 必须通过指针或引用调用如果是对象实例如B b编译器看穿类型直接静态绑定。③ 派生类重写了该虚函数或者至少存在继承关系如果没重写虽然机制上走了查表动态绑定但行为没变不产生动态多态。实现原理靠动态绑定的原理实现的。虚函数类成员函数前⾯加virtual修饰那么这个成员函数被称为虚函数虚函数的表指针class Base { p ublic: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; sizeof(Base);//为12在32位下 vptr 4 int 4 char 1 补齐 3 12 struct对齐其中还储存一个东西叫vptr虚函数的表指针,指向虚函数表虚函数表(虚表)是什么本质它是一个函数指针数组更准确地说是“地址数组”存储在可执行文件的**只读数据段.rodata**中。归属每个类有一张独立的表。例如A有一张表B有一张表。内容按虚函数声明顺序依次存放该类的虚函数入口地址。A的虚表[0] - A::test[1] - A::funcB的虚表[0] - A::test没重写则沿用[1] - B::func重写了则覆盖与对象的关系每个对象头部隐藏了一个指针vptr占 8 字节指向它所属类的虚表。怎么用编译器为每个含虚函数的类生成一张虚表存放虚函数地址。构造对象时自动将对象的 vptr 指向该类的虚表。调用虚函数时编译器生成“查表指令”——先从对象中取 vptr再从 vptr 指向的虚表中偏移取地址最后跳转执行。这就是动态绑定的底层实现。面试题(静态/动态绑定)只考察动态绑定动态多态和静态绑定class A { public: virtual void func(int val 1){ std::coutA- val std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val 0){ std::coutB- val std::endl; } }; int main(int argc ,char* argv[]) { A* a new B; a-test(); B*p new B; p-test(); return 0; //都打印B-1 }打印B-1,B-1,首先先从编译期来看待问题1.首先p-test()可以看到p为B类且B类里test()为虚函数这里便要走动态绑定不写死地址2.再看A类test里有func函数但是我们也可以看到func也为虚函数要走动态多态不写死地址但是A::test内部this的静态类型是A*C 规定默认参数值根据静态类型决定所以编译器去A::func的声明里取默认值走静态绑定机制即1并把这个1硬编码到即将生成的调用指令中压栈传参。如果当执行Atest时传入func的值就是func(1);func调用哪个取决于运行时给你的类的类型的虚函数表3.形成虚函数表(虚表)A的虚表槽位0 →A::test槽位1 →A::funcB的虚表槽位0 →A::test因为没重写沿用 A 的槽位1 →B::func重写了覆盖再从运行期来看1.cup执行拿到p,指向堆上的B对象 - 读取对象头部获得vptr然后就找到了B的虚表2.要执行test就读取B的虚表槽位0发现是A::test3.执行A::test由于静态绑定写死了传入14.要执行func,p指向的B对象中取vptr查B的虚表找func槽位槽位1。5.发现是B::func地址跳转执行B::func(int val)传入1a-test可以看到他是属于静态类型位A*进入a的类里面发现A::test为虚函数所以走动态绑定再看Atest里面的编码func()可以在A类里面看到为虚函数所以也是走动态绑定,但是func()需要传入值C 规定默认参数值根据静态类型决定所以这是时候走的是静态绑定机制即是1,func(1)也就是说当执行Atest()时候func(1)是走动态绑定的到了运行期发现a的动态类型为B * 取出B类的vptr查看虚表B的虚表槽位0 →A::test因为没重写沿用 A 的槽位1 →B::func重写了覆盖要执行test读取槽位0-a::test,再执行func(1),读取槽位1执行B::func(1);对于a-test()来说编译期来看进入的是A类的test()发现是虚函数所以走动态绑定接下来和上面雷同。还有就是当基类的虚函数在子类里面被隐藏了子类的虚函数表任然有该基类虚函数的地址不会被取代。override和final关键字作用:就是编译期约束他们不产生任何额外的运行时代码,零开销用来帮你揪出代码中的笔误并明确告知代码维护者你的设计意图。override强制检测是否真的重写的基类的虚函数(防止函数名或者参数对不上)仅静态检查class A { virtual void func(int x) {} }; class B : public A { void func(double x) override {} // 编译报错因为基类没有 func(double)强制你改回来 };final强制终止继承链。修饰虚函数则禁止子类重写修饰类则禁止被继承。class A { virtual void func() final {} }; // A 说 func 到此为止 class B : public A { void func() override {}; // 编译报错A 已经 final 了不能重写 }; class A final {}; // A 类不允许有儿子 class B : public A {}; // 编译报错无法从 final 类继承重载重写/覆盖隐藏重载(overlord),就是多个函数在同一作用域上函数名相同参数值不同或者参数个数不同返回值可以相同也可以不同 绑定时期:编译期int speak(int a){;} int speak(char a){;} char speak(char a){;}重写/覆盖(override),就是基类的虚函数在子类以同样的函数名参数值相同返回值相同写了一遍。 绑定时期:运行期隐藏:就是基类的函数(管你是不是虚函数)在子类以同样的函数名但是不符合重写的规则就是隐藏父子的成员变量同样的变量名也称为隐藏 绑定时期:编译期