news 2026/10/23 0:27:04

【C++】在编译期沉默,在运行期发声:多态的一生

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++】在编译期沉默,在运行期发声:多态的一生

🏆浮世尘弦:个人主页

⭐个人专栏:《C语言》、《数据结构与算法》、《C++》

💎:非淡薄无以明志,非宁静无以致远


前言

学完继承,很多人会觉得自己已经摸到了 C++ 面向对象的门槛。直到遇见多态。

多态值得单独写一篇,不只是因为它被面试反复提起,更因为它是继承和虚函数真正"活"过来的地方。在这里你会撞见一些看上去自相矛盾的现象:同样是virtual,写进构造函数就失去意义;基类析构函数少写一个virtual,程序可能在退出时悄悄出错;还有三个长得几乎一样的词——重载、重写、隐藏——它们决定了那次调用究竟落到谁身上。

这篇会从虚函数讲起,顺着重写规则、多态成立的两个条件、析构函数的重写,一路走到纯虚函数和抽象类;最后一章把视角沉进内存,看看虚函数表指针与虚函数表做了什么。我想让你读完能自己回答一个问题:在编译器沉默的那段时间里,它究竟把什么决定留给了运行期。

让我们开始今天的学习吧!


目录

前言

一,多态的定义和实现

1,认识虚函数

2,虚函数的重写/覆盖

3,多态(动态)的两个条件⭐

4,多态练习题(易错)

5,协变(了解)

二,析构函数的重写⭐

1,重写析构函数

2,override和final关键字

3,重载重写隐藏的对比

三,纯虚函数和抽象类

四,多态的原理⭐

1,虚函数表指针

2,多态的原理

3,动态绑定和静态绑定

4,虚函数表

结尾


一,多态的定义和实现

1,认识虚函数

在认识多态前,我们先要知道虚函数以及虚函数重写。

虚函数:类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。核心功能:实现运行时多态。

声明:虚函数和虚继承使用的同样的关键字,但是两者没有什么关系。

2,虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表类型和个数完全相同(三同)),称派⽣类的虚函数重写了基类的虚函数。
注意点:只要基类写了virtual,即使子类不写,也是虚函数。(一般建议写上)

3,多态(动态)的两个条件⭐

先简单了解一下多态的概念:

多态就是:一个继承关系下的类对象,调用同一个接口(函数名),在对象上,表现出不同的行为。
通俗来说就是:“一个接口,多种实现”。

多态分为两大类:编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态)就是:函数模版和函数重载。

特点:在编译期间就已经确定了具体调用哪个函数。

效率:极高,因为地址在编译期就绑定好了(静态绑定)

运行时多态(动态多态)就是:虚函数+继承

特点:在运行期间,根据指针/引用所指向的实际对象类型,来决定调用哪个类的函数(动态绑定)。

效率:比静态多态稍慢,因为需要在运行时查表(虚函数表)

接下来结合一个代码样例来了解一下多态吧!

class Person { public: virtual void BuyTicket() { cout << "旅游-全价" << endl; } }; class Student :public Person { public: void BuyTicket()//子类不加由于继承也是虚函数 { cout << "旅游-半价" << endl; } }; //这里使用Person是因为只有父类指针才能同时接收父类和子类 void Func(Person& ptr) { ptr.BuyTicket();//实际调用时和类型无关,和ptr指向的对象有关 } int main() { Person p1; Student s1; Func(p1); Func(s1); return 0; }

运行结果:

上面一个继承关系下的类对象,子类继承父类的虚函数。

通过父类对象的指针,调用了不同对象中的虚函数。

由此我们引出构成多态的两个条件:

1,必须是基类的指针或者引用调用了虚函数。原因:只有基类的指针和引用才能即指向基类对象又指向派生类对象。

2,派生类必须完成了对父类虚函数的重写。原因:只有完成了重写,才能有不同的函数形态用来进行调用。

4,多态练习题(易错)

下面这段代码的运行结果是什么?

A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确


分析:这里B完成了对test()函数的继承,但是没有完成对其的重写。B中实际继承的:A::test()

因此p->test会去调用父类的test(),在里面发现func()函数,就根据this的类型(A*)去查找,在A中找到了func()。发现func()实际是一个虚函数,实际的对象是B。this->func()就构成了多态。

去B中找到func()进行调用。

最后一个坑:子类虚函数的默认参数,取决于“调用时指针/引用的静态类型”。这里编译器在编译A::test()时,只知道A::func的默认参数是1。所以不会用到下面的参数。最终打印的结果就是:B->1

直接进行调用时:B::func()或者p->func()这种场景下才会使用val = 0,打印出B->0

C++ 标准委员会早就发现了这个坑,所以极力推荐:绝对不要在子类重写虚函数时,重新定义默认参数!


结果:B:B->1


5,协变(了解)

派⽣类重写基类的虚函数时,与基类的虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引用(不一定是自己的),派⽣类虚函数返回派⽣类对象的指针或者引用时,称为协变。

协变也是多态。

////协变 class A{}; class B : public A {}; class Person { public: //virtual A* BuyTicket() //可以不是自己的 virtual Person* BuyTicket() { cout << "旅游-原价" << endl; return nullptr; } }; class Student : public Person { public: //virtual B* BuyTicket() virtual Student* BuyTicket() { cout << "旅游-半价" << endl; return nullptr; } }; void Func2(Person* p1) { p1->BuyTicket(); } int main() { Person p1; Student s1; Func2(&p1); Func2(&s1); return 0; }

运行结果:

二,析构函数的重写⭐

1,重写析构函数

只要基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。

虽然看起来名字不同不能构成重写,但是实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor

//析构函数的重写 class A { public: virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: //名字被处理成destructor,构成隐藏 ~B() //不加virtual,名字不同也构成虚函数重写 { cout << "~B()" << p1 << endl;//子类会自动调用父类的析构 delete p1; } protected: int* p1 = new int[10]; }; int main() { A* p1 = new A; A* p2 = new B; delete p1; //p2->dectructor()+operator delete //先调用子类的析构后调用父类的析构 delete p2; return 0; }

运行结果:

为什么基类中的析构函数建议设计为虚函数?

因为要构成多态,如果基类指针指向子类对象(如p2),当delete p2时,如果基类析构不是虚函数,就只会调用基类的析构,导致子类申请的额外资源无法被释放,从而造成内存泄漏。所以这里析构的名字被处理成了destrctor,构成了多态关系 ,同时基类不加virtual两者构成了隐藏关系。

2,override和final关键字

override关键字:上面的虚函数重写要三个相同,如果写错了就无法构成重写,这种错误无法在编译期间报错。在运行时得不到想要的结果时,才会进行更正。因此C++11提供了override关键字,可以帮助我们检查是否构成了重写。

final关键字:如果不想让子类重写基类的虚函数,可以在基类当中的虚函数后加上一个final

演示一下:

3,重载重写隐藏的对比

三,纯虚函数和抽象类

  1. 纯虚函数的定义:在虚函数的声明后面写上= 0,这个函数就成为了纯虚函数。

  2. 纯虚函数的实现:纯虚函数通常不需要定义实现(因为它的意义在于被派生类重写,父类给出实现通常没有实际意义),只要声明即可。不过,C++ 语法上是允许纯虚函数拥有实现的。

  3. 抽象类的定义:包含纯虚函数的类被称为抽象类。

  4. 抽象类的限制:抽象类不能实例化出对象。

  5. 派生类的约束:如果一个派生类继承自抽象类,但没有重写父类的纯虚函数,那么这个派生类依然也是抽象类,依然无法实例化。

  6. 设计意义:纯虚函数某种程度上强制了派生类必须重写该虚函数,因为不重写就无法实例化出对象。

代码演示:

//纯虚函数和抽象类 class fruit //抽象类 { public: virtual void Diver() = 0; //纯虚函数声明即可 }; class apple : public fruit { public: virtual void Diver()//重写纯虚函数,不属于抽象类 { cout << "apple-sweet" << endl; } }; class orange : public fruit { public: virtual void Diver() { cout << "orange-sour" << endl; } }; int main() { //父类不能实例化出对象,但是可以使用父类的指针和引用 fruit* a1 = new apple; fruit* o1 = new orange; a1->Diver(); o1->Diver(); return 0; }

运行结果:

四,多态的原理✨

1,虚函数表指针

先来看一个例题:

下面这段代码在32位下的运行结果是什么?

A,8 B,12 C,编译报错 D,运行报错


分析:运行结果为12字节,明明只有两个变量,应该为 8个字节才对,这是为什么呢?

打开监视窗口就可以发现端倪:

这里的b有三个部分构成,除去两个成员新增加了一个_vfptr放在对象的前面,这是一个指针(32位下刚好是4个字节,因此结果为12字节),称为虚函数表指针。

一个含有虚函数的类中都至少含有一个虚函数表指针。因为一个类当中的虚函数的地址都要放到一个函数指针数组当中,这个函数指针数组就是虚函数表,虚函数表又可以称为虚表。


结果:B

2,多态的原理

下面结合一组案例来理解多态的原理:

class Person { public: virtual void BuyTicket() { cout << "旅游-全价" << endl; } string _name; }; class Student :public Person { public: virtual void BuyTicket()//子类不加由于继承也是虚函数 { cout << "旅游-半价" << endl; } int id; }; class OldMan : public Person { public: virtual void BuyTicket() { cout << "旅游-优先" << endl; } int age; }; void Func3(Person* p1) { p1->BuyTicket(); } int main() { Person* p = new Person; Person* s = new Student; Person* o = new OldMan; Func3(p); Func3(s); Func3(o); return 0; }

从底层调用角度Func3()函数中的p1->BuyTicket(),是如何作为p1指向Person对象调用Person::BuyTicket()?p1是如何调用Student::BuyTicket的呢?

可以通过下图得知:

满足多态条件后,编译器不再是编译时通过调用对象确定函数地址,而是运行时去调用对象的虚表中确定虚函数的地址。这样就实现了指针或者引用指向基类的对象就调用基类的虚函数,指向派生类就调用派生类的虚函数。

3,动态绑定和静态绑定

静态绑定:不满足多态条件的函数调用是在编译时绑定的,也就是编译期确定调用函数的地址

动态绑定:满足多态条件的函数调用是在运行时绑定的,也就是在运行时到指向对象的虚函数表中确定调用函数的地址

下面从汇编层面了解一下两种绑定的差异:

从上图可知:静态绑定只需要少量指令,运行速度比动态绑定要快的多。

4,虚函数表

1,基类对象的虚函数表中存着基类所有虚函数的地址。同类型的对象共用一张虚表,不同类型的对象各用各的虚表。基类和子类各用各的虚表。

代码测试:

class Base { public: virtual void Func1() { cout << "Func1" << endl; } virtual void Func2() { cout << "Func2" << endl; } void Func3() { cout << "Func3" << endl; } protected: int a = 1; }; int main() { Base b1; Base b2; return 0; }

监视窗口:

2,派生类有两部分构成:继承父类的部分和自己成员部分。一般只要父类当中有虚表指针,那么子类继承下来的就不需要再生成虚表指针。注意:这个继承下来的虚表指针和父类当中的虚表指针不同。

3,子类重写父类的虚函数,那么子类中从父类继承下来的的虚表中对应的虚函数地址也要更换成重写函数的地址。

4,派生类的虚表中的函数地址可以分成三部分:(1)父类的虚函数地址。(2)子类重写覆盖父类的虚函数地址。(3)子类自己的虚函数地址。

5,虚函数存放在哪里?虚函数代码存放的位置和普通函数一样,编译后都是一段指令,都存在代码段中。

6,虚函数表指针(vptr)存放对象的内存空间中(栈,堆或全局区)。

下面这段代码可以检测出存放的位置:

int main() { int i = 0; static int j = 1; int* p1 = new int; const char* p2 = "xxxxxxxx"; printf("栈:%p\n", &i); printf("静态区:%p\n", &j); printf("堆:%p\n", p1); printf("常量区:%p\n", p2); Base b; Derive d; Base* p3 = &b; Derive* p4 = &d; /*cout << "和上面的各个区域的地址进行对比,找到接近的位于同区域" << endl;*/ printf("Person虚表地址:%p\n", *(int*)p3); printf("Student虚表地址:%p\n", *(int*)p4); //成员函数取地址加上一个& printf("虚函数地址:%p\n", &Base::Func1);//代码段 printf("普通函数地址:%p\n", &Base::Func3); return 0; }

结尾

本篇文章中所涉及到的代码:

gitee链接:多态

以上就是本文的全部内容了,如果对你有所帮助的话,请大佬不妨给博主来个“一键三连”,这对我是大大的支持,也能让博主产出更优质的内容。

往期回顾:

【C++】从基类到派生类:关于C++继承的务实梳理

总结:本文系统讲解C++多态机制,涵盖虚函数、虚函数重写、多态的两个条件(基类指针/引用调用虚函数、派生类重写基类虚函数),并通过代码实例演示多态实现原理。深入剖析虚函数表(vtable)与虚函数表指针(vptr)的作用,阐明动态绑定与静态绑定的区别,强调析构函数设为虚函数的重要性以避免内存泄漏。同时介绍协变、纯虚函数、抽象类、override与final关键字等进阶特性,帮助读者全面掌握运行时多态的核心机制。

⚡把上面的内容吃透就休息一下吧⚡

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 0:26:56

Seurat单细胞分析核心原理与实操避坑指南

1. 项目概述&#xff1a;为什么Seurat成了单细胞分析的“默认起点”如果你刚接触单细胞转录组测序&#xff08;scRNA-seq&#xff09;&#xff0c;大概率会在文献、教程甚至实验室师兄师姐的电脑桌面里&#xff0c;一眼看到Seurat这个名字。它不是某个公司出品的商业软件&#…

作者头像 李华
网站建设 2026/10/23 0:27:04

STM32F103C8T6驱动DHT11时序调试与稳定读取实战

简介&#xff1a;本资源是一套面向STM32初学者的DHT11温湿度传感器实战开发包&#xff0c;聚焦单总线通信原理与嵌入式外设驱动实践&#xff0c;适用于ARM Cortex-M3架构下的STM32F103C8T6最小系统学习与项目验证。压缩包共377个文件&#xff0c;涵盖76个头文件&#xff08;h&a…

作者头像 李华
网站建设 2026/10/21 11:19:29

智慧停车平台使用了哪种 YOLO 模型?

YOLOX-S 是目标检测模型&#xff0c;不是聊天用的大语言模型 在智慧停车中&#xff0c;车辆检测主链路使用 YOLOX-S&#xff0c;并将检测结果交给轨迹关联和停车规则处理。平台还接入了两个分割模型&#xff0c;用于补充车辆轮廓和停车场景信息。 一、先认识 YOLOX-S YOLOX …

作者头像 李华
网站建设 2026/10/22 13:45:48

基于深度学习的路面裂缝智能检测分割系统-PyTorch

本项目为前几天收费帮学妹做的一个项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 路面裂缝智能检测分割系统。基于深度学习对路面图像进行裂缝的像素级分割&#xff…

作者头像 李华
网站建设 2026/10/21 11:27:56

FLUX 3 Image:面向专业工作流的4K级精确图像编辑架构

1. 项目概述&#xff1a;FLUX 3 Image 不是又一个“画图玩具”&#xff0c;而是图像编辑范式的迁移起点Black Forest Labs 这次发布的 FLUX 3 Image&#xff0c;名字里带个“Image”容易让人误以为是张静态图&#xff0c;但实际它是一套全新的基于扩散模型的图像生成与编辑架构…

作者头像 李华
网站建设 2026/10/21 22:45:54

WorkBuddy安装配置与Skill实战:从零搭建AI桌面自动化工作流

1. 先搞清楚WorkBuddy到底是个什么东西1.1 一句话说透&#xff1a;它不是聊天机器人&#xff0c;是AI桌面工作台很多人第一次听到WorkBuddy&#xff0c;下意识觉得“又是一个套壳对话工具”。我一开始也这么想&#xff0c;直到真正把它装到工作机上跑了一周&#xff0c;才发现方…

作者头像 李华