de风——【从零开始学C++】(十五)多态:从虚函数到抽象类,解锁面向对象核心能力 📅 发布时间:2026/7/16 9:10:04 👁️ 浏览次数: 前言你好我是小小风在前面的篇章中我们已经学习了继承——通过继承子类可以复用父类的成员。但继承只是手段真正的杀手锏是多态。多态是C面向对象三大特性中最灵活、也最强大的一个。今天我们就来彻底搞懂它本章内容思维导图一、什么是多态1. 简介多态Polymorphism顾名思义就是多种形态。在C中多态指的是同一个函数调用根据对象类型的不同会执行不同的行为。通俗地理解你对一只猫说叫一声它喵喵叫你对一只狗说叫一声它汪汪叫同样是叫这个指令不同的动物有不同的响应——这就是多态2. 多态的分类C中的多态分为两大类类型发生时机机制示例静态多态编译时多态编译阶段函数重载、模板add(1, 2)和add(1.5, 2.5)动态多态运行时多态运行阶段虚函数 继承父类指针调用子类重写的函数本篇重点讲解的是动态多态它也是面试和实际开发中最常考、最常用的部分。3. 动态多态的三个条件要实现动态多态必须同时满足以下三个条件有继承关系子类继承父类子类重写父类的虚函数函数名、参数列表、返回值类型都要相同父类的指针或引用指向子类对象然后调用该虚函数二、虚函数1. 简介虚函数是在父类中用virtual关键字修饰的成员函数。它的作用是允许子类重写该函数从而在运行时根据实际对象类型决定调用哪个版本的函数。简单来说虚函数就是给子类一个覆盖的机会——你父类定义的默认行为子类可以重新实现自己的版本。语法格式cppclass 父类名 { public: virtual 返回值类型 函数名(参数列表) { // 默认实现 } };2. 示例2.1 不使用虚函数的情况静态绑定这个例子用来说明不使用virtual关键字时父类指针只能调用父类版本的函数无法实现多态。cpp#include iostream using namespace std; class Animal { public: void speak() { // 没有virtual cout 动物在叫 endl; } }; class Cat : public Animal { public: void speak() { // 隐藏了父类的speak cout 小猫喵喵叫 endl; } }; class Dog : public Animal { public: void speak() { cout 小狗汪汪叫 endl; } }; int main() { Animal* a1 new Cat; // 父类指针指向子类对象 Animal* a2 new Dog; a1-speak(); // 输出动物在叫不是喵喵叫 a2-speak(); // 输出动物在叫不是汪汪叫 delete a1; delete a2; return 0; }运行结果动物在叫 动物在叫可以看到虽然a1指向的是Cat对象但调用的却是Animal的speak()。这是因为编译器在编译阶段就根据指针类型Animal*绑定了函数这叫静态绑定。2.2 使用虚函数实现动态多态这个例子用来说明加上virtual关键字后程序会在运行时根据实际对象类型调用对应的函数真正实现多态。cpp#include iostream using namespace std; class Animal { public: virtual void speak() { // 加上virtual cout 动物在叫 endl; } }; class Cat : public Animal { public: void speak() { // 重写父类虚函数 cout 小猫喵喵叫 endl; } }; class Dog : public Animal { public: void speak() { cout 小狗汪汪叫 endl; } }; int main() { Animal* a1 new Cat; Animal* a2 new Dog; a1-speak(); // 输出小猫喵喵叫 a2-speak(); // 输出小狗汪汪叫 delete a1; delete a2; return 0; }运行结果小猫喵喵叫 小狗汪汪叫只加了一个virtual关键字结果就完全不同了这就是动态绑定——程序在运行时根据对象实际类型来决定调用哪个函数。2.3 用引用实现多态这个例子用来说明父类的引用也可以实现多态不仅仅是指针。cpp#include iostream using namespace std; class Shape { public: virtual double area() { return 0; } }; class Circle : public Shape { private: double r; public: Circle(double radius) : r(radius) {} double area() { return 3.14 * r * r; } }; class Rectangle : public Shape { private: double w, h; public: Rectangle(double width, double height) : w(width), h(height) {} double area() { return w * h; } }; // 用父类引用作为参数可以接收任何子类对象 void printArea(Shape s) { cout 面积是 s.area() endl; } int main() { Circle c(5); Rectangle r(3, 4); printArea(c); // 输出面积是78.5 printArea(r); // 输出面积是12 return 0; }运行结果面积是78.5 面积是12这种用法非常常见——函数参数使用父类引用就能接收所有子类对象代码复用性极强。2.4 override关键字C11这个例子用来说明override关键字可以帮助我们检查是否正确重写了父类虚函数防止因拼写错误导致的隐藏bug。cpp#include iostream using namespace std; class Base { public: virtual void func() { cout Base::func() endl; } }; class Derived : public Base { public: void func() override { // 正确重写编译通过 cout Derived::func() endl; } // void Func() override { } // 错误父类没有Func函数编译报错 }; int main() { Base* b new Derived; b-func(); // 输出Derived::func() delete b; return 0; }运行结果Derived::func()小贴士建议在子类重写虚函数时都加上override关键字它不产生任何运行时开销却能在编译期帮你发现错误是防止bug的好习惯3. 经典Bug忘记写virtual导致内存泄漏这是一个非常隐蔽的bug新手特别容易踩坑cpp#include iostream using namespace std; class Base { public: Base() { cout Base构造 endl; } ~Base() { cout Base析构 endl; } // 没有加virtual }; class Derived : public Base { private: int* data; public: Derived() { data new int[100]; cout Derived构造分配了内存 endl; } ~Derived() { delete[] data; cout Derived析构释放了内存 endl; } }; int main() { Base* b new Derived; delete b; // 只调用了Base的析构函数Derived的析构没被调用 return 0; }运行结果Base构造 Derived构造分配了内存 Base析构注意看Derived的析构函数没有被调用data数组的内存泄漏了原因delete b时编译器根据指针类型Base*调用了Base的析构函数。由于析构函数不是虚函数不会发生动态绑定所以Derived的析构函数被跳过了。解决方案把父类的析构函数声明为虚函数详见后文虚析构函数部分。三、纯虚函数1. 简介纯虚函数是一个在父类中只声明、不实现的虚函数它的实现交给子类去完成。语法格式cppvirtual 返回值类型 函数名(参数列表) 0; // 0表示纯虚函数作用当父类本身不知道该怎么实现某个功能时就把它声明为纯虚函数强制子类去实现比如动物这个类你无法确定动物到底怎么叫但你可以要求每种具体动物必须实现自己的叫声2. 示例2.1 纯虚函数的基本使用这个例子用来说明纯虚函数没有函数体必须由子类实现。cpp#include iostream using namespace std; class Animal { public: // 纯虚函数只声明不实现 virtual void speak() 0; // 普通成员函数也可以和纯虚函数共存 void breathe() { cout 动物在呼吸 endl; } }; class Cat : public Animal { public: void speak() { cout 小猫喵喵叫 endl; } }; class Dog : public Animal { public: void speak() { cout 小狗汪汪叫 endl; } }; int main() { Cat c; Dog d; c.speak(); // 输出小猫喵喵叫 c.breathe(); // 输出动物在呼吸 d.speak(); // 输出小狗汪汪叫 return 0; }运行结果小猫喵喵叫 动物在呼吸 小狗汪汪叫注意如果子类没有实现纯虚函数那子类也会变成抽象类仍然无法实例化对象。2.2 纯虚函数也可以有函数体这个例子用来说明纯虚函数也可以提供默认实现子类可以通过父类名::函数名()来调用。cpp#include iostream using namespace std; class Base { public: virtual void func() 0; // 声明纯虚函数 // 纯虚函数的实现写在类外 void Base::func() { cout Base的纯虚函数默认实现 endl; } }; // 纯虚函数在类外实现 void Base::func() { cout Base的纯虚函数默认实现 endl; } class Derived : public Base { public: void func() { Base::func(); // 调用父类纯虚函数的默认实现 cout Derived重写的func endl; } }; int main() { Derived d; d.func(); return 0; }运行结果Base的纯虚函数默认实现 Derived重写的func这个特性不常用了解即可。主要记住纯虚函数一般是不带函数体的。四、抽象类1. 简介抽象类是指包含至少一个纯虚函数的类。抽象类的特点无法实例化对象不能new也不能直接定义对象可以定义指针和引用指向子类对象子类必须实现所有纯虚函数否则子类也是抽象类抽象类通常用作接口规定子类必须具备哪些行为可以这样理解抽象类就像一个合同模板它规定了子类必须做什么但不管你怎么做。2. 示例2.1 抽象类的基本使用这个例子用来说明抽象类不能实例化子类必须实现纯虚函数后才能使用。cpp#include iostream using namespace std; // 抽象类图形 class Shape { public: virtual double area() 0; // 纯虚函数求面积 virtual double perimeter() 0; // 纯虚函数求周长 void printInfo() { cout 面积 area() 周长 perimeter() endl; } }; // 圆形 class Circle : public Shape { private: double r; public: Circle(double radius) : r(radius) {} double area() { return 3.14 * r * r; } double perimeter() { return 2 * 3.14 * r; } }; // 矩形 class Rectangle : public Shape { private: double w, h; public: Rectangle(double width, double height) : w(width), h(height) {} double area() { return w * h; } double perimeter() { return 2 * (w h); } }; int main() { // Shape s; // 错误抽象类不能实例化 Circle c(5); Rectangle r(3, 4); c.printInfo(); // 面积78.5周长31.4 r.printInfo(); // 面积12周长14 // 用抽象类指针实现多态 Shape* s1 c; Shape* s2 r; s1-printInfo(); s2-printInfo(); return 0; }运行结果面积78.5周长31.4 面积12周长14 面积78.5周长31.4 面积12周长14这个例子非常经典Shape定义了所有图形必须能算面积和周长但具体怎么算交给子类。Circle和Rectangle各自实现自己的计算方式而printInfo()这个普通函数可以被所有子类复用——这就是接口与实现分离的设计思想。2.2 子类未完全实现纯虚函数时也是抽象类这个例子用来说明子类如果没实现全部纯虚函数自己也会变成抽象类无法实例化。cpp#include iostream using namespace std; class Animal { public: virtual void speak() 0; virtual void move() 0; }; // 只实现了speak没实现move class Bird : public Animal { public: void speak() { cout 叽叽喳喳 endl; } // 没有实现move() }; // 完整实现了两个纯虚函数 class Parrot : public Bird { public: void move() { cout 鹦鹉飞走了 endl; } void speak() { cout 叽叽喳喳 endl; } }; int main() { // Bird b; // 错误Bird还是抽象类不能实例化 Parrot p; p.speak(); // 输出叽叽喳喳 p.move(); // 输出鹦鹉飞走了 return 0; }运行结果叽叽喳喳 鹦鹉飞走了小结只有把父类所有纯虚函数都实现了子类才能变成具体类才能实例化对象。五、虚析构函数1. 简介虚析构函数就是把父类的析构函数声明为virtual。语法cppvirtual ~类名() { // 清理资源 }作用当用父类指针delete子类对象时保证子类的析构函数也会被调用避免内存泄漏。只要一个类打算被继承并且可能通过父类指针来操作子类对象就应该把析构函数声明为虚函数。这是C编程的黄金法则之一。2. 示例2.1 虚析构函数的正确使用这个例子用来说明使用虚析构函数后delete父类指针会先调用子类析构再调用父类析构资源正确释放。cpp#include iostream using namespace std; class Base { public: Base() { cout Base构造 endl; } virtual ~Base() { cout Base析构 endl; } // 虚析构 }; class Derived : public Base { private: int* data; public: Derived() { data new int[100]; cout Derived构造分配了内存 endl; } ~Derived() { delete[] data; cout Derived析构释放了内存 endl; } }; int main() { Base* b new Derived; delete b; // 现在会正确调用Derived的析构函数 return 0; }运行结果Base构造 Derived构造分配了内存 Derived析构释放了内存 Base析构对比之前经典Bug中的例子加了virtual后析构顺序变成了先子类析构 → 再父类析构内存正确释放了六、多态的底层原理拓展了解这一节适合有一定基础的同学新手可以跳过不影响使用。1. 虚函数表vftable当一个类中有虚函数时编译器会为这个类生成一个虚函数表本质上是一个函数指针数组存放着每个虚函数的地址。2. 虚表指针vfptr每个含有虚函数的对象内部都会多出一个隐藏的指针成员通常在对象内存布局的最前面指向该类对应的虚函数表。工作流程创建对象时编译器在构造函数中初始化虚表指针指向该对象所属类的虚函数表通过父类指针调用虚函数时程序先通过对象的虚表指针找到虚函数表再在虚函数表中查找真正的函数地址进行调用cpp#include iostream using namespace std; class Animal { public: virtual void speak() { cout 动物叫 endl; } }; class Cat : public Animal { public: void speak() { cout 喵喵叫 endl; } }; int main() { Animal a; Cat c; // 查看对象大小多了虚表指针 cout Animal大小 sizeof(a) endl; // 864位系统一个指针 cout Cat大小 sizeof(c) endl; // 8 return 0; }所以使用虚函数是有空间开销的——每个对象多了一个指针的大小8字节/64位系统同时调用虚函数比普通函数稍慢需要多一次指针跳转。但这点开销相对于多态带来的灵活性来说微不足道。七、多态的实际应用案例这个例子展示多态在实际开发中的典型用法——模拟一个简单的绘图系统。cpp#include iostream #include vector using namespace std; // 抽象基类图形 class Shape { public: virtual void draw() 0; // 纯虚函数绘制 virtual double area() 0; // 纯虚函数面积 virtual ~Shape() {} // 虚析构函数 }; // 圆形 class Circle : public Shape { private: double r; public: Circle(double radius) : r(radius) {} void draw() { cout 画一个圆形半径 r endl; } double area() { return 3.14 * r * r; } }; // 矩形 class Rectangle : public Shape { private: double w, h; public: Rectangle(double width, double height) : w(width), h(height) {} void draw() { cout 画一个矩形宽 w 高 h endl; } double area() { return w * h; } }; // 三角形 class Triangle : public Shape { private: double base, height; public: Triangle(double b, double h) : base(b), height(h) {} void draw() { cout 画一个三角形底 base 高 height endl; } double area() { return 0.5 * base * height; } }; int main() { // 用父类指针管理所有图形多态的精髓 vectorShape* shapes; shapes.push_back(new Circle(5)); shapes.push_back(new Rectangle(3, 4)); shapes.push_back(new Triangle(6, 8)); cout 绘制所有图形 endl; for (auto s : shapes) { s-draw(); // 多态调用每个图形画法不同 } cout \n 计算总面积 endl; double total 0; for (auto s : shapes) { total s-area(); // 多态调用每个图形面积算法不同 } cout 所有图形总面积 total endl; // 释放内存 for (auto s : shapes) { delete s; } return 0; }运行结果 绘制所有图形 画一个圆形半径5 画一个矩形宽3高4 画一个三角形底6高8 计算总面积 所有图形总面积102.5这就是多态的威力main函数里完全不需要知道具体是哪种图形只需要用Shape*调用draw()和area()即可。以后要新增一种图形比如梯形只需要写一个新类继承Shape主程序一行都不用改——这就是开闭原则对扩展开放对修改关闭。八、重写、重载和隐藏的区别这三个概念初学者容易混淆这里统一对比一下。概念作用范围函数名参数列表virtual发生条件重载overload同一类中相同不同无关同一作用域内重写override父子类间相同相同父类需要有继承关系 虚函数隐藏hide父子类间相同无关无关继承关系 非虚函数简单记忆重载同类同名不同参就像同一个名字的多个工具重写子类重新实现父类虚函数真正的多态隐藏子类同名函数遮住了父类函数不是多态是覆盖名字总结本篇我们学习了C多态的核心内容总结如下多态 同一调用不同行为。分为静态多态编译时和动态多态运行时虚函数用virtual修饰是实现动态多态的关键纯虚函数 0结尾只有声明没有实现强制子类去实现抽象类含有纯虚函数的类不能实例化只能作为接口使用虚析构函数父类析构函数加virtual防止通过父类指针删除子类对象时内存泄漏override关键字帮助编译器检查是否正确重写推荐使用多态的底层靠虚函数表和虚表指针实现多态是C面向对象的精髓也是面试的高频考点。理解多态的关键在于动手敲代码、多画图、多调试。下一篇我们将学习二叉搜索树敬请期待如果觉得有帮助欢迎点赞收藏关注三连有问题欢迎评论区交流~
骁龙移动平台双卡双待DSDS的技术内幕(7): DSDS功耗优化与DRX/eDRX策略 系列文章目录:本文是“骁龙移动平台双卡双待DSDS的技术内幕”系列的第七篇,分析双卡场景下的功耗挑战及 DRX/eDRX/PSM 等省电机制的应用。1. DSDS 为什么更耗电? 1.1 功耗对比 下表展示单卡 vs 双卡(DSDS) 功耗对比&am… 2026/7/16 9:08:03
建站承诺SEO上首页,否则退费,里面的猫腻!你不要脸也可以承诺。 做网站、做线上推广的企业,大概率都听过这句话:我们承诺 SEO 包上首页,规定时间内不上首页,全额退 SEO 费用!这句话听起来零风险、稳赚不亏,完美打消了企业的推广顾虑,也是很多建站公司、SEO 外… 2026/7/16 9:06:03
Adobe Illustrator 2026安装实操指南:环境校验、风险规避与稳定性加固 1. 项目概述:这不是“AI 2026”,而是Adobe Illustrator 2026的安装实操手记 最近在设计圈、平面工作室和高校设计系群里,频繁刷到“Ai2026软件 安装教程”这个关键词。很多人一搜就点进各种标题党页面,结果发现所谓“Ai2026”根本… 2026/7/16 9:06:03
AlloyDB AI实战:原生集成Gemini实现语义搜索与自然语言查询 1. 先搞清楚 AlloyDB AI 到底解决了什么实际问题 如果你正在处理企业级数据库应用,特别是需要结合传统 SQL 查询和现代 AI 能力的场景,Google Cloud 为 AlloyDB 嵌入 Gemini 模型这个组合值得重点关注。它最核心的价值不是简单的功能叠加,而是… 2026/7/16 11:09:25
操作系统引导实验:从原理到实践的完整指南 1. 系统引导实验为何成为操作系统课程的"拦路虎"每次操作系统实验课开始前,实验室里总会此起彼伏地响起"系统引导好难"的哀叹。作为计算机专业核心课程,操作系统实验中的引导环节确实让不少同学栽了跟头。究其原因,系统引… 2026/7/16 11:07:22
Codex Desktop 2026最新版:离线AI编程工作站深度解析 1. Codex Desktop 是什么?它和你用过的那些“AI 编程助手”根本不是一回事Codex Desktop 不是又一个 VS Code 插件,也不是某个大厂套壳的网页版 AI 工具。如果你之前装过 Claude Code、试过 GitHub Copilot 的本地离线版、甚至折腾过 Ollama 拉取 deepse… 2026/7/16 11:05:21
AT指令集在物联网设备开发中的实战测试与自动化脚本编写 1. AT指令集在物联网开发中的核心作用 第一次接触AT指令时,我盯着串口调试工具里输入的"ATCGMI"发呆——这行看似简单的文本,竟然让GSM模块乖乖返回了制造商信息。这种"对话式"的设备控制方式,彻底改变了我对硬件通信的认… 2026/7/16 11:05:21
吉方工控G-3288-02开发板实战与工业应用优化 1. 吉方工控G-3288-02开发板初体验作为一名嵌入式开发工程师,第一次拿到吉方工控G-3288-02开发板时,最直观的感受就是它的工业级设计。这块基于Rockchip RK3288处理器的开发板,采用了全金属外壳和宽温设计,接口布局非常工整。板载… 2026/7/16 11:03:21
如何在浏览器中直接查看SQLite数据库文件? 如何在浏览器中直接查看SQLite数据库文件? 【免费下载链接】sqlite-viewer View SQLite file online 项目地址: https://gitcode.com/gh_mirrors/sq/sqlite-viewer 还在为查看SQLite数据库文件而烦恼吗?无需安装任何软件,无需配置复杂… 2026/7/16 11:01:20
A--10 Codex Review与GitHub PR工作流实战指南:从代码审查到安全合并 摘要:本文系统讲解如何利用Codex App的Review功能与GitHub PR工作流,实现从代码修改到安全合并的完整流程。涵盖Review面板深度使用、/review命令实战、GitHub Connector配置、PR描述撰写技巧,以及常见问题排查方法。通过多个实战案例和流程图,帮助开发者建立高效的AI辅助代… 2026/7/16 0:00:26
HAM未来路线图:下一代高可用迁移技术的发展方向与展望 HAM未来路线图:下一代高可用迁移技术的发展方向与展望 【免费下载链接】ham Based on the remote memory access capability and high bandwidth of the UB, deterministic duration virtual machine live migration is achieved, addressing planned downtime issu… 2026/7/16 0:04:27
月球是否是从地球分离出去的?——容度原理解释 月球是否是从地球分离出去的?——容度原理解释一、月球起源的“三大假说”与容度原理的重新审视月球起源的三大假说——捕获说(月球是太阳系中独立的星体,被地球引力捕获)、共生说(月球与地球同时从原始星云中形成&… 2026/7/16 0:06:27
Git reset 与 revert 深度对比:5个关键差异与 3 种典型应用场景 Git Reset 与 Revert 深度对比:5个关键差异与3种典型应用场景在团队协作开发中,代码版本管理如同行走钢丝——一步失误可能导致整个项目陷入混乱。作为Git进阶用户,你是否曾在深夜面对错误的提交束手无策?是否在强制推送后收到同事… 2026/7/13 8:31:55
GitHub 学生包申请避坑:5个常见失败原因与开发者工具调试方案 GitHub 学生包申请技术排障指南:5个高频失败场景与开发者工具实战方案第一次尝试申请GitHub学生包时,我盯着屏幕上那个不断转圈的加载动画整整15分钟,最终只等来了一行冰冷的错误提示。这可能是许多开发者共同的经历——明明按照教程操作&… 2026/7/16 3:47:53
冒烟测试用例设计规范:5%-10%覆盖率下的3类核心场景与执行标准 冒烟测试用例设计的黄金法则:5%-10%覆盖率下的精准筛选策略在快节奏的敏捷开发环境中,冒烟测试作为质量保障的第一道防线,其重要性不言而喻。当测试资源有限而时间紧迫时,如何从海量测试用例中精准筛选出那关键的5%-10%࿰… 2026/7/14 5:09:41