访问网站 403.14错误太原营销网站建设制作平台
2026/4/6 7:28:03 网站建设 项目流程
访问网站 403.14错误,太原营销网站建设制作平台,苏州企业网站制作报价,云南人社关注我#xff0c;学习c不迷路: 个人主页#xff1a;爱装代码的小瓶子 专栏如下#xff1a; c学习Linux学习 后续会更新更多有趣的小知识#xff0c;关注我带你遨游知识世界 期待你的关注。 文章目录深入探索C虚函数#xff1a;从编译器视角看多态的“幕后魔法”1. 一…关注我学习c不迷路:个人主页爱装代码的小瓶子专栏如下c学习Linux学习后续会更新更多有趣的小知识关注我带你遨游知识世界期待你的关注。文章目录深入探索C虚函数从编译器视角看多态的“幕后魔法”1. 一个奇怪的现象类的大小变了2. 揭秘核心机制虚函数表vtable与虚表指针vptr2.1 什么是虚函数表 (vtable)2.2 什么是虚表指针 (vptr)2.3 动态绑定的全过程3. 继承中的虚表变换重写的本质4. 经典面试题为什么析构函数必须是虚函数4.1 场景复现4.2 运行结果与分析4.3 修正方案5. 纯虚函数与抽象类特点对比表6. 避坑指南那些关于虚函数的“反直觉”陷阱6.1 构造函数中调用虚函数6.2 默认参数是静态绑定的# 总结深入探索C虚函数从编译器视角看多态的“幕后魔法”前言在上一篇文章中我们聊了C多态的基本概念知道了“同一接口多种实现”的妙处。但你是否好奇过编译器在底层到底做了什么手脚为什么加了一个virtual关键字程序就能在运行时“聪明”地找到正确的函数今天小瓶子带大家钻进编译器的“肚子里”不再只谈语法而是从内存布局、虚表指针vptr和虚函数表vtable的角度彻底搞懂虚函数的原理。这部分内容也是大厂面试中考察C深度的重灾区哦1. 一个奇怪的现象类的大小变了在深入原理之前我们先看一段看似简单的代码。请大家猜猜下面两个类的大小sizeof分别是多少假设在64位系统下#includeiostreamusingnamespacestd;// 这是一个普通的空类classA{public:voidfunc(){coutA::funcendl;}};// 这是一个带虚函数的空类classB{public:virtualvoidfunc(){coutB::funcendl;}};intmain(){coutsizeof(A) sizeof(A)endl;coutsizeof(B) sizeof(B)endl;return0;}结果揭晓sizeof(A)1sizeof(B)8(32位系统下是4)为什么类A虽然有成员函数但成员函数是不占对象内存空间的它们存在代码段。空类为了占位编译器会给它分配1字节。类B也是“空”的只有一个函数但因为加了virtual编译器悄悄地在对象内部塞了一个指针。这个指针就是我们今天的主角——虚表指针Virtual Pointer, 简称 vptr。2. 揭秘核心机制虚函数表vtable与虚表指针vptr对于初学者来说这确实是理解C多态最“劝退”的地方。但别急我们把它们拆解开来看。2.1 什么是虚函数表 (vtable)当编译器发现一个类中包含虚函数或者是继承自包含虚函数的基类时它会为这个类注意是类不是对象生成一张表。这张表本质上是一个函数指针数组数组里存放着该类所有虚函数的地址。特性虚函数表 (vtable)归属属于类该类的所有对象共享同一张表存储位置通常在只读数据段.rodata或代码段生成时间编译期确定内容存放该类中实际有效的虚函数地址2.2 什么是虚表指针 (vptr)既然类有了表那对象怎么找到这张表呢编译器会在实例化对象时在对象的内存布局的最前面通常是开头取决于编译器实现自动添加一个指针指向该类的虚函数表。这个指针就是vptr。内存布局示意图对象 obj 的内存布局: ---------------------- | vptr (8 bytes) | ---------- ---------------------------- ---------------------- | Base::vtable | | member var 1 | ---------------------------- ---------------------- | [0] Base::func1 | | member var 2 | | [1] Base::func2 | ---------------------- ----------------------------2.3 动态绑定的全过程当我们使用基类指针调用虚函数时编译器并没有像调用普通函数那样直接把函数地址写死静态绑定而是插入了一段“查表”的代码。Base*ptrnewDerived();ptr-func();这段代码在运行时经历了以下步骤这是多态生效的关键取指针通过ptr找到对象的首地址。找vptr读取对象首地址处的vptr。查vtable通过vptr找到该对象所属类Derived的vtable。调函数从vtable中取出对应的函数地址例如第0个位置然后跳转执行。这就是为什么多态叫“动态绑定”——直到程序运行起来顺藤摸瓜找到了虚表才知道具体执行哪个函数。3. 继承中的虚表变换重写的本质当我们进行派生时编译器是如何处理这张表的呢这里体现了override重写的本质。假设我们有如下关系classBase{public:virtualvoidfunc1(){coutBase::func1endl;}virtualvoidfunc2(){coutBase::func2endl;}};classDerived:publicBase{public:// 重写了 func1virtualvoidfunc1(){coutDerived::func1endl;}// 没重写 func2};编译器构建Derived的虚表时遵循以下规则拷贝先将基类Base的虚表内容拷贝一份过来。覆盖如果派生类重写了某个虚函数如func1则用派生类自己的函数地址覆盖掉表中原有的基类函数地址。追加如果派生类定义了新的虚函数则将其地址添加到表的末尾。小结所谓的“重写”在底层其实就是替换了虚表中对应位置的函数指针。4. 经典面试题为什么析构函数必须是虚函数这绝对是面试中出现频率Top 3的问题4.1 场景复现如果我们用基类指针指向派生类对象然后delete这个指针classBase{public:// 注意这里没有加 virtual~Base(){coutDelete Baseendl;}};classDerived:publicBase{public:~Derived(){coutDelete Derivedendl;}};intmain(){Base*pnewDerived();deletep;// 灾难发生了return0;}4.2 运行结果与分析输出Delete Base问题Derived的析构函数根本没执行如果Derived里申请了堆内存这里就造成了内存泄漏。原因因为~Base()不是虚函数编译器进行的是静态绑定。编译器看到p是Base*类型就直接生成了调用Base::~Base()的指令完全不管它实际指向的是什么对象。4.3 修正方案只要将基类的析构函数加上virtualvirtual~Base(){coutDelete Baseendl;}此时delete p会变成动态绑定通过vptr找到Derived的析构函数先执行然后编译器会自动插入代码调用基类的析构函数保证清理顺序正确先子后父。金科玉律只要一个类可能被继承且可能通过基类指针删除派生类对象其析构函数必须声明为virtual。5. 纯虚函数与抽象类有时候基类并不知道该怎么实现某个函数比如“图形”类的“计算面积”函数这时候就可以使用纯虚函数。classShape{public:// 纯虚函数也就是接口定义virtualdoublegetArea()0;};特点对比表特性普通虚函数纯虚函数语法virtual void f() {}virtual void f() 0;实例化类可以被实例化包含纯虚函数的类称为抽象类不能实例化目的提供默认实现允许覆盖强制派生类必须实现提供接口规范虚表内容存放函数地址在某些编译器实现中该位置可能存放nullptr或报错函数6. 避坑指南那些关于虚函数的“反直觉”陷阱虽然虚函数很强大但也有它无能为力甚至会“坑”你的时候。6.1 构造函数中调用虚函数千万不要在构造函数或析构函数中调用虚函数classBase{public:Base(){func();// 危险}virtualvoidfunc(){coutBase::funcendl;}};classDerived:publicBase{public:virtualvoidfunc(){coutDerived::funcendl;}};当你执行Derived d;时会先调用Base的构造函数。此时Derived的部分还没有初始化对象的类型在编译器眼里暂时还是Base。因此Base构造函数里调用的func()永远是Base::func()多态失效了。6.2 默认参数是静态绑定的classBase{public:virtualvoidshow(intx10){coutBase: xendl;}};classDerived:publicBase{public:virtualvoidshow(intx20){coutDerived: xendl;}};intmain(){Base*pnewDerived();p-show();// 猜猜输出什么}输出结果Derived: 10原因这确实很反直觉函数执行是动态的调用了Derived的函数体。默认参数是静态的编译期根据指针类型Base*决定的。所以你得到了一个“缝合怪”Derived的逻辑加上Base的默认参数。切记绝不要重新定义继承而来的默认参数值。# 总结今天我们从编译器的角度重新审视了虚函数是不是感觉清晰了很多不再是死记硬背语法而是理解了底层的流动。这篇文章我们主要讲了内存变化包含虚函数的类会多出一个vptr指针导致对象大小增加。底层机制多态是通过vptr查表指针和vtable函数地址表配合实现的动态绑定。重要规则基类析构函数必须是virtual以防内存泄漏。避坑细节构造函数中多态失效以及默认参数静态绑定的陷阱。C的魅力就在于此越钻研底层越能体会到设计的精妙。如果你觉得这篇文章对你有帮助别忘了三连支持一下小瓶子哦

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询