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

虚函数虚表

一、基本原理

1.虚函数:用virtual关键字生声明的函数,允许派生类重写

2.虚表:每个包含虚函数的类(或其派生类)会有一个全局唯一的虚表,存储该类所有虚函数的地址

3.虚指针(vptr):每个对象会包含一个指向其类虚表的指针(通常在对象内存布局的开头)

当通过基类指针或引用调用虚函数时,程序会通过对象的虚指针找到对应类的虚表,在虚表中查找要调用的虚函数地址,调用该地址对应的函数(实现动态绑定)。

二、同名覆盖

同名覆盖:派生类中定义的与基类同名的函数,覆盖了基类的同名函数,导致基类的该函数在派生类作用域内不可直接访问的现象。

其本质可分为两种场景,需结合基类函数是否为虚函数区分:

场景1:虚函数的同名覆盖(即“重写”,Override)

当基类函数是虚函数时,派生类定义与基类“函数名、参数列表、返回值完全一致”(协变除外)的同名函数,会覆盖基类虚函数的实现 —— 这是动态多态的核心,覆盖效果在运行时生效(根据对象实际类型确定调用哪个版本)。

场景 2:非虚函数的同名覆盖(即“隐藏”,Hide)

当基类函数非虚函数时,派生类定义与基类同名的函数(无论参数列表是否相同),会遮蔽基类的同名函数 —— 这是编译期的作用域遮蔽,覆盖效果在编译时生效(根据指针/引用的声明类型确定调用哪个版本)。

三、代码示例

class Object { int value; public: Object(int x = 0) :value(x) { cout << "Object" << endl; } virtual void add(int a) { cout << "Object::add:" << a << endl; } virtual void func()const { cout << "Object::func:" << endl; } virtual void print()const { cout << "Object::print:" << endl; } }; class Base :public Object { int num; public: Base(int x = 0) :Object(x + 10), num(x) { cout << "Base" << endl; } virtual void add(int a) { cout << "Base::add:" << a << endl; } virtual void func()const { cout << "Base::func:" << endl; } virtual void show() { cout << "Base::show:" << endl; } }; class Test :public Base { int sum; public: Test(int x = 0) :Base(x + 10), sum(x) { cout << "Test" << endl; } virtual void func()const { cout << "Test::func:" << endl; } virtual void print() const { cout << "Test::print" << endl; } virtual void show() { cout << "Test::show:" << endl; } }; int main() { Test test; return 0; }

执行Test test;时,完整流程(含 vptr 设置 + 成员初始化):

1.分配 Test 对象内存:内存中包含vptr + Object::value + Base::num + Test::sum,vptr 初始为野指针。

2.执行 Object 构造函数:

第一步:设置 vptr 指向Object的虚表;

第二步:执行 Object 的初始化列表value(x)(x=20,由 Base 传递而来);

第三步:执行 Object 构造函数体(你的代码中无额外逻辑,仅输出 "Object");

Object 构造完成。

3.执行 Base 构造函数:

第一步:设置 vptr 指向Base的虚表;

第二步:执行 Base 的初始化列表Object(x+10)(已执行完毕)、num(x)(x=10,由 Test 传递而来);

第三步:执行 Base 构造函数体(输出 "Base");

Base 构造完成。

4.执行 Test 构造函数:

第一步:设置 vptr 指向Test的虚表;

第二步:执行 Test 的初始化列表Base(x+10)(已执行完毕)、sum(x)(x=0,默认参数);

第三步:执行 Test 构造函数体(输出 "Test");

Test 构造完成。

派生类向基类传参

1.传参入口:派生类构造函数通过 初始化列表 给直接基类传参(不能在构造函数体内传);

2.传递规则:多层继承时,参数按 “最派生类→中间基类→顶层基类” 逐层传递,每层都可对参数加工(如x+10);

3.默认值优先级:若派生类给基类传了参数,基类构造函数就不用自己的默认值;没传参时,基类才用自身默认值。

例:Test test;(无显式传参)

Test(x=0 默认值)→ 给 Base 传0+10=10;

Base(收 10)→ 给Object传10+10=20;

最终顶层基类Object用20初始化自身成员。

注意:

1.无论继承多少层,一个对象只有一个vptr,多层继承的vptr是 “覆盖更新”,而非“多个 vptr 并存”;

2.vptr的设置是“构造函数内的第一步”,早于成员初始化,这是 C++ 的规定,目的是避免构造期间调用虚函数时,意外访问到子类中未初始化的成员,导致内存错误或逻辑异常;

3.构造期间 vptr 指向“当前正在构造的类的虚表”,因此构造函数内调用虚函数不会触发多态(比如 Object 构造时调用add,只会调用Object::add,不会调用 Base 或 Test 的版本)。

四、纯虚函数和抽象类

纯虚函数是C++中一种特殊的虚函数,它在基类中声明但不提供实现,强制要求派生类必须重写该函数。纯虚函数的声明格式为:virtual 返回类型 函数名(参数列表) = 0;

含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法,所以抽象类不能实例化对象;

继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。

抽象类的使用规则:

1.抽象类只能作为其他类的基类,不能创建抽象类的对象;

2.抽象类不能用作参数类型、函数返回值类型或显式类型转换;

3.可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类,从而实现运行时多态。

五、面试常见问题

为什么构造函数不能定义成虚函数?

虚函数能实现动态多态,本质是依赖虚指针(vptr)虚函数表(vtable)

每个含虚函数的对象,都有一个vptr,指向所属类的 vtable;

而vptr的初始化,是构造函数执行的第一步—— 进入构造函数后,先把 vptr 指向当前类的 vtable,再初始化成员变量。

若构造函数是虚函数:

调用虚函数需要通过 vptr 查找 vtable;

但 vptr 要等构造函数执行才会初始化,相当于“要调用虚构造函数,必须先有 vptr;但 vptr 要等构造函数执行才存在”,形成逻辑死循环

为什么基类的析构函数要声明为虚函数?
如果基类析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源泄漏。

基类的析构函数不是虚函数时,通过基类指针删除派生类对象时,会发生以下过程:

  1. 编译阶段编译器根据指针 / 引用的声明类型(Object),将析构函数调用绑定到基类的析构函数(~Object())。
  2. 运行阶段:程序执行时,只会调用编译阶段绑定好的基类析构函数,而派生类的析构函数完全不会被触发,因此派生类析构函数不会被调用。

基类析构函数是虚函数时,通过基类指针删除派生类对象时,过程如下:​​​​​​

  1. 运行阶段:程序根据指针实际指向的派生类对象,找到派生类的析构函数并调用;
  2. 派生类析构函数执行完毕后,会自动调用基类的析构函数(这是 C++ 的默认规则:派生类析构函数执行时,会隐式调用基类的析构函数,保证基类部分的资源被释放);
  3. 最终,派生类对象的派生部分基类部分都会被完整销毁。

虚函数和纯虚函数的区别

虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)

使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;

定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;

虚函数必须实现,否则编译器会报错;

对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;

析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。

http://www.cnnetsun.cn/news/27010.html

相关文章:

  • V助手舆情分析智能体:重塑舆情分析,从“人找信息”到“信息为人”
  • 连接2026:十款远程控制软件真实力横评与选择指南
  • 计算机毕业设计springboot基于Spark++Vue.js的学生管理系统 Spark+Vue 高校学生综合信息管理平台 基于 SpringBoot+Spark+Vue 的全链路学生事务中心
  • JavaScript 集合操作的哈希碰撞:攻击者如何利用特殊 Key 导致 Map/Set 性能降级到 O(N)
  • 为什么 C盘空间会莫名其妙减少(即使没装新软件)?
  • 17、深入理解 Linux 文件系统机制与结构
  • 29、Linux 软件使用与故障排除指南
  • 从入门到转行:网络安全自学与跳槽的终极建议
  • 网络安全小白自学之路,别拜师了,求人不如求己_网络安全小白怎么自学
  • 从系统运维到网络安全工程师,8个月转行真实经验分享!
  • 算法系列(Algorithm)- 快速排序
  • RobotStudio2025全功能授权
  • IsaacLab中UR机械臂与Robotiq夹爪的5大配置难点与解决方案
  • cmark Markdown解析器终极指南:从入门到精通
  • 4-bit量化FLUX模型:让专业AI绘图走进寻常百姓家
  • Excel VBA快速入门:7天从零到精通终极指南
  • AutoHotkey鼠标轨迹自动化终极指南:从零开始实现精准操作回放
  • UxPlay 终极指南:在 Linux 系统上实现 AirPlay 镜像的完整教程
  • 1-2 惜败!国安亚冠连败 中超 16 强魔咒难破
  • 一键解锁阅读3.0书源终极合集:1629个精品资源任你选
  • 一般人不懂Windows
  • Java 基于多线程机制的专项实验
  • 51、Linux 系统中 shell 环境管理与脚本编写全解析
  • 53、编写高效 Shell 脚本:从基础到实践
  • 解锁共享单车数据:从入门到精通的完整分析指南
  • KOOM:如何快速解决Android应用内存泄漏的终极方案
  • API 测试- Postman Vs Rest Assured
  • 如何在React Native应用中实现语音交互?
  • 突破创意瓶颈:BlenderMCP如何用AI重塑3D建模工作流
  • 生产环境出现问题,测试人如何做工作复盘?