C++的多态是如何体现的?一篇文章搞懂C++虚函数机制与常见问题

C++的多态是如何体现的?
一篇尽量清晰、结构化的文章,帮你搞懂虚函数机制、vtable、虚表指针,以及最容易出错的那些点。

1. 多态在C++里到底是什么?

C++支持三种多态:

  • 编译时多态(静态多态):函数重载、运算符重载、模板(泛型)、CRTP
  • 运行时多态(动态多态):通过虚函数 + 指针/引用实现
  • 强制多态(类型转换):static_cast、dynamic_cast 等(较少讨论)

绝大多数人问“C++的多态”时,指的其实就是运行时多态,也就是通过虚函数实现的动态绑定

一句话总结核心:

同一个接口,不同的对象表现出不同的行为,且绑定发生在运行时。

2. 虚函数机制的核心——虚表(vtable)与虚表指针(vptr)

C++的运行时多态实现依赖于以下几个关键概念:

概念英文存放在哪里内容是什么谁拥有它
虚函数表virtual table (vtable)静态存储区(每个类一份)该类的所有虚函数的地址(函数指针数组)类(类型)
虚表指针virtual pointer (vptr)对象内存布局最开头(通常)指向本对象对应类的vtable的指针每个对象
虚函数调用dynamic dispatch通过vptr找到vtable,再通过槽位找到函数地址运行时

最重要的一句话:

只要一个类有虚函数(或继承自有虚函数的类),编译器就会为这个类生成一张虚表,并在类的对象中偷偷插入一个虚表指针 vptr。

3. 虚函数调用流程(最关键的图解过程)

假设有下面这段经典代码:

classAnimal{public:virtualvoidspeak(){ std::cout <<"Animal speaks\n";}virtual~Animal()=default;};classDog:publicAnimal{public:voidspeak()override{ std::cout <<"Woof!\n";}};classCat:publicAnimal{public:voidspeak()override{ std::cout <<"Meow~\n";}};intmain(){ Animal* p =newDog(); p->speak();// 输出 Woof!delete p;}

运行时发生了什么?

  1. 创建 Dog 对象时,编译器在对象内存最开头放了一个 vptr它指向 Dog类的虚表
  2. Dog 类的虚表里,speak() 这一槽位存的是 Dog::speak 的地址。
  3. 调用 p->speak() 时:
    • 取 p 指向对象的 vptr
    • 通过 vptr 找到 虚表
    • 根据 speak() 在虚表中的偏移(槽位索引),取出函数地址
    • 调用该地址 → 执行 Dog::speak()

这就是动态绑定的完整过程。

4. 常见问题与陷阱(面试+实际开发高频)

4.1 构造函数里调用虚函数会怎样?
classBase{public:Base(){whoami();}virtualvoidwhoami(){ std::cout <<"Base\n";}};classDerived:publicBase{public:voidwhoami()override{ std::cout <<"Derived\n";}};intmain(){ Derived d;// 输出 Base}

结论:在构造函数中,vptr 还没有指向派生类的虚表,此时调用虚函数走的是当前正在构造的类的版本。

4.2 析构函数必须是虚函数吗?

必须(只要这个类可能会被指针/引用多态删除)。

原因:如果基类析构不是虚函数,delete base_ptr; 时只调用基类析构,派生类部分不会被析构 → 资源泄漏。

4.3 override 和 final 关键字(C++11+)
virtualvoidf()override;// 明确告诉编译器:我在重写,必须匹配基类签名virtualvoidg()final;// 告诉编译器:这个虚函数到此为止,不允许再被重写

强烈建议在重写时都写 override,能尽早发现签名不匹配的错误。

4.4 纯虚函数 & 抽象类
virtualvoidspeak()=0;// 纯虚函数 → 该类成为抽象类,不能实例化
4.5 虚函数表是每个类一份,还是每个对象一份?

每个有虚函数的类一份(静态的),对象只持有一个指向它的指针。

4.6 多继承下的虚表(最复杂的情况)

多继承时,一个对象可能有多个 vptr(每个继承链一条),虚表也更复杂,还涉及虚基类表(vbptr / vbase)

classA{virtualvoidf();};classB{virtualvoidg();};classC:publicA,publicB{...};

C 的对象内存布局里通常会有 两个 vptr

这是多继承最容易出问题的地方(菱形继承 + 虚继承才能解决二义性)。

4.7 虚函数开销有多大?
  • 空间:每个对象多一个指针(通常 8 字节,64位系统)
  • 时间:一次间接寻址(vptr → vtable → 函数地址),现代 CPU 分支预测 + 内联缓存后开销很小

绝大多数业务场景下,虚函数的性能开销是可以接受的

5. 总结:一句话记住虚函数机制

C++运行时多态 = 虚函数 + 指针/引用 + vtable + vptr + 动态分派

最简记忆口诀

“对象藏 vptr → vptr 指 vtable → vtable 存函数地址 → 运行时查表调用”

希望这篇文章让你对 C++ 虚函数从“会用”变成“知道为什么这样实现”。

如果你还有具体想深入的点(比如:多继承虚表布局、虚函数与模板的冲突、CRTP 静态多态对比、vtable 如何调试查看等),可以直接告诉我,我继续展开。

Read more

Python+Django的多彩吉安红色旅游网站的设计与实现(Pycharm Flask Django Vue mysql)

Python+Django的多彩吉安红色旅游网站的设计与实现(Pycharm Flask Django Vue mysql)

收藏关注不迷路!!需要的小伙伴可以发链接或者截图给我 项目介绍 Python+Django的多彩吉安红色旅游网站的设计与实现(Pycharm Flask Django Vue mysql) 项目展示 详细视频演示 请联系我获取更详细的演示视频 感兴趣的可以先收藏起来,还有大家在毕设选题(免费咨询指导选题),项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人 技术栈 项目编号: 本课题使用Python语言进行开发。代码层面的操作主要在PyCharm中进行,将系统所使用到的表以及数据存储到MySQL数据库中,方便对数据进行操作本课题基于WEB的开发平台 开发语言:Python 框架:flask/django的都有 Python版本:python3.7.7 数据库:mysql 数据库工具:Navicat 开发软件:PyCharm 浏览器:谷歌浏览器 本系统的开发与设计是基于vue为前端页面核心框架为django/flask,技术方面主要采用了Html、Js、CSS3、p

By Ne0inhk
Python 基础语法完全指南:变量、类型、运算符与输入输出(零基础入门)

Python 基础语法完全指南:变量、类型、运算符与输入输出(零基础入门)

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 常量与表达式:Python 当计算器使用 * 1.1 核心算术运算符 * 1.2 关键注意点 * 1.3 实际案例:求平均值 * 二. 变量:保存数据的 “容器” * 2.1 变量定义与使用 * 2.2 变量命名规则 * 三. 数据类型:给数据 “分类” * 3.1 四大基础类型 * 3.2类型相关操作: * 四. 注释:给代码 “加说明”

By Ne0inhk
在 Ubuntu 环境下玩转 Python:从环境配置到实战开发全指南

在 Ubuntu 环境下玩转 Python:从环境配置到实战开发全指南

前言 Ubuntu 作为最流行的 Linux 发行版之一,凭借其稳定的性能、丰富的软件生态和开源特性,成为 Python 开发的理想选择。无论是数据分析、Web 开发还是人工智能领域,Ubuntu 都能为 Python 提供高效的运行环境。本文将从基础环境配置出发,逐步深入到 Python 开发的核心场景,帮助开发者在 Ubuntu 系统中快速搭建稳定、高效的 Python 开发环境,并通过实战案例掌握关键开发技能。 一、Ubuntu 系统下 Python 环境基础配置 1.1 了解 Ubuntu 预装的 Python 版本 Ubuntu 系统默认会预装 Python,但可能同时存在 Python 2.x(部分旧版本系统)和 Python

By Ne0inhk