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

【c++面向对象编程】第17篇:多态(四):虚析构函数——删除派生类对象时避免内存泄漏

目录一、一个会泄漏内存的程序二、为什么会这样——静态绑定 vs 动态绑定解决方案把基类析构函数声明为虚函数三、虚析构函数的原理虚析构函数也是虚函数析构函数的执行顺序四、虚析构函数的开销1. 对象内存增加一个vptr2. 调用开销增加一次间接寻址什么时候不需要虚析构函数五、完整例子虚析构函数救火六、抽象类中的虚析构函数七、虚析构函数与“三/五法则”八、三个常见错误1. 忘记把析构函数声明为虚函数2. 把不是基类的类析构函数设为虚函数浪费3. 在析构函数中调用虚函数九、这一篇的收获一、一个会泄漏内存的程序先看这段代码猜猜会发生什么cpp#include iostream #include string using namespace std; class Base { public: Base() { cout Base构造 endl; } ~Base() { cout Base析构 endl; } // 注意不是虚函数 }; class Derived : public Base { private: string* data; public: Derived() { data new string(派生类分配的资源); cout Derived构造分配了内存 endl; } ~Derived() { delete data; cout Derived析构释放了内存 endl; } }; int main() { Base* ptr new Derived(); delete ptr; // 这里会发生什么 return 0; }输出textBase构造 Derived构造分配了内存 Base析构 ← 只有基类析构被调用问题Derived的析构函数没有被调用data指向的内存永远没有释放——内存泄漏。这个bug非常隐蔽因为程序不一定会崩溃内存泄漏通常不会立即显现在大型项目中很难追踪二、为什么会这样——静态绑定 vs 动态绑定回忆第14篇的静态绑定与动态绑定非虚函数调用在编译时决定静态绑定虚函数调用在运行时决定动态绑定析构函数也是函数。如果基类析构函数不是虚函数delete ptr时编译器采用静态绑定cppdelete ptr; // ptr的类型是Base*所以调用Base::~Base()编译器不知道ptr实际指向的是Derived对象所以不会去调用Derived的析构函数。解决方案把基类析构函数声明为虚函数cppclass Base { public: virtual ~Base() { cout Base析构 endl; } // 加virtual };现在输出textBase构造 Derived构造分配了内存 Derived析构释放了内存 ← 调用了 Base析构原理析构函数是虚函数delete ptr时动态绑定先调用派生类析构再自动调用基类析构。三、虚析构函数的原理虚析构函数也是虚函数虚析构函数和普通虚函数一样会进入虚函数表vtable。cppclass Base { public: virtual ~Base() {} }; class Derived : public Base { public: ~Derived() override {} // 重写基类的虚析构函数 };vtable布局textBase_vtable: [0] → Base::~Base() Derived_vtable: [0] → Derived::~Derived() ← 覆盖了基类的槽位当通过Base*删除对象时从对象中取出vptr从vtable中取第0个函数析构函数地址调用该函数——由于派生类覆盖了这个槽位调用的是Derived::~Derived()Derived析构函数执行完毕后自动调用Base::~Base()析构函数的执行顺序无论析构函数是否是虚函数执行顺序都是先派生类后基类。区别在于非虚只调用基类析构跳过了派生类部分虚通过动态绑定确保派生类析构先被调用四、虚析构函数的开销虚析构函数带来两个开销1. 对象内存增加一个vptrcppclass NoVirtual { public: ~NoVirtual() {} // 没有其他虚函数 }; class WithVirtual { public: virtual ~WithVirtual() {} }; cout sizeof(NoVirtual) endl; // 1空类占1字节 cout sizeof(WithVirtual) endl; // 864位系统vptr占8字节如果类本身已经有其他虚函数vptr已经存在加虚析构函数不增加额外内存。2. 调用开销增加一次间接寻址非虚析构直接调用虚析构vptr → vtable → 间接调用多2次内存访问什么时候不需要虚析构函数规则只有当类会作为基类被多态使用时才需要虚析构函数。cpp// 不需要虚析构函数不会作为基类 class Point { int x, y; public: ~Point() {} // 非虚即可 }; // 需要虚析构函数会作为基类被多态使用 class Shape { public: virtual void draw() 0; virtual ~Shape() {} // 必须虚 };标准库中的例子std::string没有虚析构函数不应该被继承std::vector没有虚析构函数不应该被继承std::exception有虚析构函数可以被继承五、完整例子虚析构函数救火一个模拟数据库连接池的例子展示没有虚析构函数的后果cpp#include iostream #include string #include vector using namespace std; // 资源类模拟数据库连接 class DBConnection { private: int id; static int nextId; public: DBConnection() : id(nextId) { cout 数据库连接 id 已建立 endl; } ~DBConnection() { cout 数据库连接 id 已释放 endl; } void query(const string sql) { cout 连接 id 执行: sql endl; } }; int DBConnection::nextId 0; // 基类数据访问层没有虚析构函数——错误示范 class BadDataAccess { protected: DBConnection* conn; public: BadDataAccess() { conn new DBConnection(); cout BadDataAccess构造 endl; } ~BadDataAccess() { // ❌ 非虚析构 delete conn; cout BadDataAccess析构 endl; } virtual void execute(const string sql) { conn-query(sql); } }; // 派生类用户数据访问 class BadUserDataAccess : public BadDataAccess { private: string* cache; public: BadUserDataAccess() : BadDataAccess() { cache new string(用户数据缓存); cout BadUserDataAccess构造分配了缓存 endl; } ~BadUserDataAccess() { delete cache; cout BadUserDataAccess析构释放了缓存 endl; } void execute(const string sql) override { cout 【使用缓存】 endl; conn-query(sql); } }; // 正确版本基类有虚析构函数 class GoodDataAccess { protected: DBConnection* conn; public: GoodDataAccess() { conn new DBConnection(); cout GoodDataAccess构造 endl; } virtual ~GoodDataAccess() { // ✅ 虚析构 delete conn; cout GoodDataAccess析构 endl; } virtual void execute(const string sql) { conn-query(sql); } }; class GoodUserDataAccess : public GoodDataAccess { private: string* cache; public: GoodUserDataAccess() : GoodDataAccess() { cache new string(用户数据缓存); cout GoodUserDataAccess构造分配了缓存 endl; } ~GoodUserDataAccess() override { delete cache; cout GoodUserDataAccess析构释放了缓存 endl; } void execute(const string sql) override { cout 【使用缓存】 endl; conn-query(sql); } }; int main() { cout 错误示范基类析构函数非虚 endl; BadDataAccess* badPtr new BadUserDataAccess(); badPtr-execute(SELECT * FROM users); delete badPtr; // 只调用 ~BadDataAccess()缓存泄漏 cout \n 正确示范基类析构函数虚 endl; GoodDataAccess* goodPtr new GoodUserDataAccess(); goodPtr-execute(SELECT * FROM users); delete goodPtr; // 先 ~GoodUserDataAccess()再 ~GoodDataAccess() return 0; }输出text 错误示范基类析构函数非虚 数据库连接 1 已建立 BadDataAccess构造 BadUserDataAccess构造分配了缓存 【使用缓存】 连接1 执行: SELECT * FROM users BadDataAccess析构 数据库连接 1 已释放 ← 注意没有释放cache 正确示范基类析构函数虚 数据库连接 2 已建立 GoodDataAccess构造 GoodUserDataAccess构造分配了缓存 【使用缓存】 连接2 执行: SELECT * FROM users GoodUserDataAccess析构释放了缓存 ← 正确释放了 GoodDataAccess析构 数据库连接 2 已释放六、抽象类中的虚析构函数抽象类有纯虚函数必须提供虚析构函数即使它是空的cppclass Shape { public: virtual double getArea() 0; virtual ~Shape() {} // 即使是空的也必须写 };为什么派生类对象通过Shape*删除时需要调用正确的析构函数如果不写编译器生成的析构函数是非虚的导致上述问题最佳实践任何作为基类使用的类都应该有虚析构函数。七、虚析构函数与“三/五法则”回顾第4篇的三法则如果类需要自定义析构函数通常也需要自定义拷贝构造和拷贝赋值。对于多态基类这条规则有一个例外cppclass PolymorphicBase { public: virtual ~PolymorphicBase() default; // 虚析构但使用默认实现 // 不需要自定义拷贝构造/赋值因为不管理资源 };现代C建议如果类是作为基类使用virtual ~ClassName() default;如果类管理资源遵循五法则析构、拷贝构造、拷贝赋值、移动构造、移动赋值如果不作为基类且不管理资源让编译器生成默认析构函数八、三个常见错误1. 忘记把析构函数声明为虚函数cppclass Base { public: ~Base() {} // 非虚 }; Base* p new Derived(); delete p; // 泄漏2. 把不是基类的类析构函数设为虚函数浪费cppclass Point { // 不会被继承 public: virtual ~Point() {} // 不必要的vptr开销 int x, y; }; // 每个Point对象多8字节浪费内存3. 在析构函数中调用虚函数cppclass Base { public: virtual ~Base() { cleanup(); } // 调用的是Base::cleanup不是派生类版本 virtual void cleanup() {} };和构造函数一样析构函数中虚函数不产生多态行为——派生类部分已经先析构了。九、这一篇的收获你现在应该理解如果基类析构函数不是虚函数delete基类指针时只调用基类析构派生类资源不会被释放规则任何作为基类多态使用的类都应该有虚析构函数虚析构函数会引入vptr带来8字节内存开销但如果已有其他虚函数没有额外开销抽象类必须提供虚析构函数即使是空的虚析构函数遵循动态绑定先调用派生类析构再调用基类析构 小作业写一个Logger基类有虚函数log(const string)以及虚析构函数。实现FileLogger写入文件和ConsoleLogger输出到控制台每个派生类在析构时关闭各自的资源。通过基类指针删除对象验证析构顺序正确。下一篇预告第18篇《多继承与菱形继承一二义性问题与虚拟继承》——C支持一个类继承多个基类。但当两个基类有同名成员或者出现“菱形继承”时会出现二义性问题。虚拟继承是解决方案——但它也有自己的复杂性。下篇开始讲多继承的坑与解法。

相关文章:

【c++面向对象编程】第17篇:多态(四):虚析构函数——删除派生类对象时避免内存泄漏

目录 一、一个会泄漏内存的程序 二、为什么会这样?——静态绑定 vs 动态绑定 解决方案:把基类析构函数声明为虚函数 三、虚析构函数的原理 虚析构函数也是虚函数 析构函数的执行顺序 四、虚析构函数的开销 1. 对象内存增加一个vptr 2. 调用开销增…...

【c++面向对象编程】第16篇:多态(三):抽象类与纯虚函数——设计接口的思想

目录 一、一个没有意义的实现 二、纯虚函数与抽象类 语法 效果 三、接口类:全部是纯虚函数的类 接口类的特征 四、为什么需要抽象类/接口? 1. 强制派生类实现特定功能 2. 定义“契约”,降低耦合 3. 设计模式的基础 五、完整例子&am…...

ESP32-C3移植Zephyr RTOS实战:从环境搭建到Blinky应用开发

1. 项目概述:为什么要在ESP32-C3上折腾Zephyr?最近拿到一块nanoESP32-C3的开发板,手痒想试试新东西。ESP32-C3这颗芯片大家不陌生,RISC-V内核,性价比高,在物联网终端设备里很常见。我们平时玩它&#xff0c…...

古法护目,草本赋能:科霖海京双效方案,助力孩子裸眼视力稳步提升

我国青少年近视率居高不下,越来越多孩子早早戴上眼镜,不仅影响日常学习运动,更可能限制未来升学与职业选择。面对孩子视力下滑的焦虑,科霖海京深耕青少年视力健康领域,以千年中医护眼智慧为根基,独创中医按…...

Vue 3组合式API写到崩溃?Claude实时注释+逻辑补全+TS类型推导,3步救回交付进度

更多请点击: https://intelliparadigm.com 第一章:Vue 3组合式API写到崩溃?Claude实时注释逻辑补全TS类型推导,3步救回交付进度 当 setup() 函数膨胀至 300 行、ref 与 computed 嵌套过深、onMounted 中异步链断裂时,…...

热保护器原理、选型与故障排查全解析:从双金属片到安全设计

1. 项目概述:从一次设备故障说起去年夏天,我工作室里一台用了三年的工业级热风枪突然罢工了。拆开一看,电机没坏,发热丝也没断,但就是不通电。一番排查,最终在发热芯的陶瓷骨架旁边,找到了一个指…...

2026年GEO服务商避坑指南:假榜单活不过24小时,真头部靠什么被AI推荐?

一场碰瓷事件掀开了GEO行业的遮羞布——当虚假榜单在被举报后24小时内被下架,我们更清晰地认识到:能被AI长久推荐、被权威体系真实收录,远比自封的“第一”更有力量。GEO服务商怎么选?谁才是真正靠谱的GEO公司?这是202…...

10人机械设计团队上云第一课:为什么老手都选云飞云而不是传统VDI?

在10人规模的机械设计团队中,使用SolidWorks和UG进行三维设计时,云桌面的选择应聚焦于硬件性能、资源管理、数据安全、协同效率及成本控制五大核心维度。以下是一个基于云飞云智能共享云桌面的推荐方案,该方案已成功应用于多家精密机械制造企…...

The Gentlemen勒索软件深度技术分析:1570+受害者背后的黑色工业化帝国

引言 2026年5月,Check Point Research(CPR)发布了一份震惊全球网络安全界的研究报告:安全团队在一次企业事件响应中,意外渗透了The Gentlemen勒索软件组织的核心C2服务器,导出了完整的内部运营数据库。数据…...

11.8k Star 的开源 AI 笔记神器:内置 RAG 知识库,让 Obsidian 用户都想换

👉 这是一个或许对你有用的社群🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 《项目实战(视频)》:从书中学,往事中…...

TuxGuitar吉他软件:免费开源的吉他谱编辑器终极指南

TuxGuitar吉他软件:免费开源的吉他谱编辑器终极指南 【免费下载链接】tuxguitar Open source guitar tablature editor 项目地址: https://gitcode.com/gh_mirrors/tu/tuxguitar TuxGuitar是一款功能强大的开源吉他谱编辑器和播放器,专为吉他爱好…...

京东连环炮:MyBatis 如何进行分页?分页插件的原理是什么?有没踩过什么坑?

👉 这是一个或许对你有用的社群🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 《项目实战(视频)》:从书中学,往事中…...

向量数据库选型2026:从Chroma到Milvus的工程化决策指南

向量数据库已经成为RAG系统、语义搜索、推荐系统的核心基础设施。市场上有超过20款向量数据库产品,从轻量级的Chroma到企业级的Milvus,如何选择适合自己场景的方案? 本文从工程实践角度,对主流向量数据库进行深度对比,…...

Trainers‘ Legend G:打造完美中文赛马娘游戏体验的终极指南 ✨

Trainers Legend G:打造完美中文赛马娘游戏体验的终极指南 ✨ 【免费下载链接】Trainers-Legend-G 赛马娘本地化插件「Trainers Legend G」 项目地址: https://gitcode.com/gh_mirrors/tr/Trainers-Legend-G 你是否因为语言障碍而错过了赛马娘 Pretty Derby …...

大疆无人机固件自由下载:5个技巧掌握DankDroneDownloader终极指南 [特殊字符]

大疆无人机固件自由下载:5个技巧掌握DankDroneDownloader终极指南 🚁 【免费下载链接】DankDroneDownloader A Custom Firmware Download Tool for DJI Drones Written in C# 项目地址: https://gitcode.com/gh_mirrors/da/DankDroneDownloader 你…...

Prompt工程进阶2026:让LLM输出稳定可靠的工程化实践

Prompt工程从"写个好提示词"进化成了一门严肃的工程学科。2026年的Prompt工程师不只是会写Prompt,而是要能系统性地设计、测试、版本管理、监控Prompt,确保LLM在生产环境中稳定输出符合预期的结果。 本文聚焦Prompt工程的工程化实践&#xff0…...

避开性能坑!在ARM Cortex-M项目里用还是不用Semihosting的实战指南

ARM Cortex-M开发中的Semihosting实战指南:性能陷阱与替代方案 在嵌入式开发的世界里,调试工具的选择往往决定了项目的成败。Semihosting作为一种便捷的调试机制,让开发者能够在目标设备上直接调用主机端的输入输出功能,看似是开发…...

如何快速搭建ComfyUI IPAdapter工作流:从零开始的图像风格控制指南

如何快速搭建ComfyUI IPAdapter工作流:从零开始的图像风格控制指南 【免费下载链接】ComfyUI_IPAdapter_plus 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI_IPAdapter_plus ComfyUI IPAdapter Plus是Stable Diffusion图像生成中实现精准图像条件控…...

绝杀,OpenAI正式接管人类耳朵,首个GPT-5级推理音频模型来了

OpenAI又给世界带来一次震撼。 这一次,他们不卷文字,不卷视频,而是要把那个曾让无数人惊艳、又让无数人遗憾的Samantha——电影《Her》中的AI——彻底带进现实。 OpenAI正式宣布,推出GPT-Realtime-2。 这不仅仅是一次音频模型的…...

别再只盯着Encoder模式了!STM32F4用外部中断+定时器搞定EC11旋转编码器的保姆级配置

STM32F4实战:外部中断定时器驱动EC11旋转编码器的工程化实现 在嵌入式开发中,旋转编码器作为人机交互的重要组件,其稳定可靠的读取方案一直是开发者关注的焦点。传统硬件Encoder模式虽被广泛采用,但在引脚资源受限或定时器通道不匹…...

抖音批量下载神器:高效自动化下载工具深度解析

抖音批量下载神器:高效自动化下载工具深度解析 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖音…...

嵌入式Linux调试笔记(三十一)------SYSRQ机制在串口调试中的实战与内核实现剖析

1. SYSRQ机制:嵌入式Linux调试的"救命稻草" 在嵌入式Linux开发中,系统崩溃、死机、卡顿是家常便饭。想象一下,你的设备正在野外运行,突然系统卡死,没有图形界面,唯一的调试接口就是串口——这时候…...

深度解析magnetW磁力搜索:3大架构优化与5个实战技巧

深度解析magnetW磁力搜索:3大架构优化与5个实战技巧 【免费下载链接】magnetW [已失效,不再维护] 项目地址: https://gitcode.com/gh_mirrors/ma/magnetW magnetW作为一款基于Electron构建的跨平台磁力搜索工具,通过聚合多个BT源站实现…...

Unity语音驱动口型动画终极指南:5分钟实现角色真实对话效果

Unity语音驱动口型动画终极指南:5分钟实现角色真实对话效果 【免费下载链接】LipSync LipSync for Unity3D 根据语音生成口型动画 支持fmod 项目地址: https://gitcode.com/gh_mirrors/lip/LipSync 还在为Unity角色对话时僵硬的口型匹配而烦恼吗?…...

Cursor Pro破解工具终极指南:2025年免费解锁AI编程助手完整功能

Cursor Pro破解工具终极指南:2025年免费解锁AI编程助手完整功能 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached…...

3步掌握WinUtil:Windows系统调校的终极自动化工具

3步掌握WinUtil:Windows系统调校的终极自动化工具 【免费下载链接】winutil Chris Titus Techs Windows Utility - Install Programs, Tweaks, Fixes, and Updates 项目地址: https://gitcode.com/GitHub_Trending/wi/winutil WinUtil是一款开源的Windows系统…...

在 Taotoken 模型广场中根据任务需求与预算进行模型选型的实践

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 在 Taotoken 模型广场中根据任务需求与预算进行模型选型的实践 对于开发者而言,面对众多大模型 API,如何选…...

汽车点火线圈高压产生原理与安全应用解析

1. 项目概述:一个经典的工程式恶作剧在工程院校的宿舍里,恶作剧是学生文化中不可或缺的一部分。但工程师的恶作剧,往往带着一种独特的、基于物理原理的“硬核”色彩。它不是简单的吓唬,而是对能量、电路和材料特性的一种巧妙&…...

开源情报工具curiso:模块化OSINT采集架构与实战部署指南

1. 项目概述:一个开源情报(OSINT)工具的深度探索最近在开源情报(OSINT)的圈子里,一个名为metaspartan/curiso的项目引起了我的注意。这并非一个广为人知的商业工具,而是一个托管在代码托管平台上…...

基于MCP协议构建AI与实时数据分析平台的安全连接方案

1. 项目概述:一个连接AI与实时数据的桥梁最近在折腾AI应用开发,特别是想让大语言模型(LLM)能“看到”并操作我自己的业务数据时,遇到了一个典型痛点:如何让AI安全、高效地访问那些实时变化的数据源&#xf…...