当前位置: 首页 > news >正文

多态(虚表,动态/静态绑定)

C++ 多态完全指南:从原理到面试

目录

  • 什么是多态
  • 为什么要有多态
  • 怎么用多态
    • 静态多态 — 重载 + 模板
      • 静态绑定
    • 动态多态 — 继承 + 虚函数
      • 动态绑定
      • 虚函数
        • 虚函数的表指针
        • 虚函数表(虚表)
      • 面试题(静态 / 动态绑定)
  • override 和 final 关键字
    • override
    • final
  • 重载,重写/覆盖,隐藏

什么是多态

同一行为(接口/方法),作用于不同对象时,表现的不同实现结果

为什么要有多态

多态存在的唯一目的,就是让上层代码(调用者)无需关心底层具体类型,从而在不修改已有代码的前提下,通过新增子类无限扩展新功能。

怎么用多态

多态有两种形态一个时运行时多态(动态多态),一个是编译时多态(静态多态)

静态多态-重载 + 模板

核心特征:编译器在编译阶段就确定了调用哪个函数的具体地址,生成对应的机器码。

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)
③ 模板实例化编译期生成具体类型的代码max<int>(1,2)生成整型版本。
④ 通过对象(而非指针/引用)调用虚函数编译器明确知道类型,直接静态绑定,甚至内联展开。B b; b.func();(即使func是虚函数,也直接写死调B::func)。
⑤ 默认参数的取值大陷阱!即使函数体是动态查的,默认参数的值在编译期就定死了。下面那道题,val=1就是在编译期静态绑定的。
动态多态-继承+虚函数

核心特征:编译时只检查语法(父类有没有这个方法),运行期才根据实际对象去虚表(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 是 C++11 关键字,提高可读性 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 接受 Animal&,dog 是 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::func

    • B的虚表:[0] -> A::test(没重写则沿用),[1] -> B::func(重写了则覆盖)

  • 与对象的关系:每个对象头部隐藏了一个指针(vptr,占 8 字节),指向它所属类的虚表。

怎么用

编译器为每个含虚函数的类生成一张虚表,存放虚函数地址。构造对象时,自动将对象的 vptr 指向该类的虚表。调用虚函数时,编译器生成“查表指令”——先从对象中取 vptr,再从 vptr 指向的虚表中偏移取地址,最后跳转执行。这就是动态绑定的底层实现。

面试题(静态/动态绑定)

只考察动态绑定,动态多态和静态绑定

class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val = 0){ std::cout<<"B->"<< 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硬编码到即将生成的调用指令中(压栈传参)。如果当执行A::test时传入func的值就是func(1);func调用哪个取决于运行时给你的类的类型的虚函数表

3.形成虚函数表(虚表)

  • A的虚表:槽位0 →A::test,槽位1 →A::func
  • B的虚表:槽位0 →A::test(因为没重写,沿用 A 的),槽位1 →B::func(重写了,覆盖)

再从运行期来看

1.cup执行拿到p,指向堆上的B对象 -> 读取对象头部获得vptr,然后就找到了B的虚表

2.要执行test就读取B的虚表槽位0,发现是A::test

3.执行A::test,由于静态绑定写死了传入1,

4.要执行func,p指向的B对象中取vptr,查B的虚表,找func槽位(槽位1)。

5.发现是B::func地址,跳转执行B::func(int val),传入1

a->test可以看到他是属于静态类型位A*进入a的类里面,发现A::test为虚函数,所以走动态绑定,再看A::test里面的编码,func()可以在A类里面看到为虚函数所以也是走动态绑定,但是func()需要传入值,C++ 规定默认参数值根据静态类型决定,所以这是时候走的是静态绑定机制,即是1,func(1),也就是说当执行A::test()时候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),就是基类的虚函数在子类,以同样的函数名,参数值相同,返回值相同,写了一遍。 绑定时期:运行期

隐藏:就是基类的函数(管你是不是虚函数),在子类,以同样的函数名,但是不符合重写的规则,就是隐藏,父子的成员变量,同样的变量名也称为隐藏 绑定时期:编译期

http://www.jsqmd.com/news/1090890/

相关文章:

  • 物理AI与“世界模型”:让机器不仅会“看”,更要会“想”
  • 科技创业读什么在职硕士能扩展产业人脉-交大MTT非全班型与校友资源全解
  • 规范的一键生成论文工具势力榜(2026 精选)
  • 【课程设计/毕业设计】基于 SpringBoot 的校园在线投票评选平台的设计与实现【附源码、数据库、万字文档】
  • 攻克贝尔吉比特G-120W-B光猫:从Telnet到Crontab的自动化运维实战
  • 第42期 字节跳动千人芯片团队:Arm+RISC-V双架构自研CPU全解析
  • 我用AI帮一个小商家解决了“不招人忙死,招人亏死”的困境
  • 视频修复神器:用Untrunc高效恢复损坏的MP4/MOV文件
  • 2026最新AI Agent面试通关手册!从核心原理到工程落地高频考点全覆盖
  • 【AI应用实战-hermes】Mac下安装hermes完整步骤(二)
  • T1200碳纤维意味着什么?
  • MSPM0 ADC FIFO模式与事件管理:数据缓冲与高效传输实战解析
  • Win 11 安装 Android Studio 遇阻:深入剖析 android-emulator-hypervisor-driver 权限弹窗的根源与静默修复
  • AI产品经理爆火!2026高薪岗位,普通人也能进?深度解析+进阶指南!
  • 烟火杭州:实体店找代运营,别让“套路”寒了心
  • Linux VPS 如何迁移到新服务器?2026 最新 rsync 教程:几乎 1:1 无损迁移网站、Docker 和数据
  • 法律技术中的版权保护合同管理与合规审查
  • 5G 启示录:从改变社会到万物智联
  • 220kV降压变电站电气主系统设计:从负荷分析到设备选型的工程实践
  • 【单片机毕业设计】基于 STM32 的带管理员权限电子密码锁设计,基于单片机的智能密码门禁控制系统开发(012501)
  • 3步让老旧Mac重获新生:OpenCore Legacy Patcher终极升级指南
  • 5步精通缠论自动化分析:通达信ChanlunX插件终极实战指南
  • 【单片机毕业设计】基于 STC89C52 的温湿度智能风扇控制系统设计,基于 51 单片机的温湿度采集与风扇调速系统设计(012701)
  • AI 写小说新手实战指南
  • 如何通过5个步骤高效掌握M3U8视频下载的完整解决方案
  • 看懂大语言模型:AI只会猜词,根本不会真正理解
  • 暗黑3自动化革命:D3KeyHelper释放你的双手,专注战斗策略
  • 掌握AXI-Stream时序:从握手信号到数据流传输
  • OpenCV copyTo()函数:从基础复制到掩膜(Mask)精准操控
  • 利用Surfer精准提取地理边界:从BLN文件生成到实际应用