尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

多态(虚表,动态/静态绑定)
📅 发布时间:2026/6/29 17:52:56

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),就是基类的虚函数在子类,以同样的函数名,参数值相同,返回值相同,写了一遍。 绑定时期:运行期

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

相关新闻

  • 视频修复神器:用Untrunc高效恢复损坏的MP4/MOV文件
  • MSPM0 ADC FIFO模式与事件管理:数据缓冲与高效传输实战解析
  • 烟火杭州:实体店找代运营,别让“套路”寒了心

最新新闻

  • 如何将手机摄像头变成OBS专业直播源:DroidCam OBS插件完整指南
  • 查重率亮红灯反复修改,有哪些真正性价比高的的降AIGC工具推荐?
  • MoE大模型的智能调度:从1.8万亿参数到每token仅激活2%的工程真相
  • 3步解锁RPG游戏资源:RPG Maker MV/MZ文件解密工具完整指南
  • Tribler安全漏洞响应实战:从预警到部署的完整操作手册
  • 百度网盘秒传转存终极指南:3分钟掌握全平台快速分享技巧

日新闻

  • ENVI5.3.1实战:基于Landsat 8影像的区域无缝镶嵌与精准裁剪
  • 3步完成HS2-HF Patch安装:新手快速打造完美HoneySelect2体验
  • 微信好友检测终极指南:3分钟发现谁已悄悄删除你

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号