news 2026/6/5 9:20:50

第三章 异常(一)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第三章 异常(一)

第三章 异常(一)

条款9:利用destructors避免泄露资源

一、核心概念解析

首先,我们要理解这个条款解决的核心问题:手动管理资源(如内存、文件句柄、网络连接等)时,容易因忘记释放、程序提前退出(如异常)等原因导致资源泄露

C++ 的析构函数(destructor)有一个关键特性:当一个对象的生命周期结束(如离开作用域、被 delete)时,其析构函数会自动调用。利用这个特性,我们可以将资源的释放逻辑封装到析构函数中,让资源的生命周期与对象绑定 —— 这就是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的核心思想。

问题场景:手动管理资源的风险

先看一个反例,直观感受资源泄露的问题:

#include <iostream> #include <string> // 模拟一个需要手动释放的资源(如动态内存) void createResource(std::string*& ptr) { ptr = new std::string("我是需要释放的资源"); } void releaseResource(std::string* ptr) { delete ptr; ptr = nullptr; } void riskyFunction(bool throwException) { std::string* res = nullptr; createResource(res); // 获取资源 // 模拟业务逻辑:如果抛出异常,后续的releaseResource不会执行 if (throwException) { throw std::runtime_error("业务逻辑异常"); } // 即使没有异常,也可能忘记写这行,导致内存泄露 releaseResource(res); } int main() { try { riskyFunction(true); // 传入true触发异常,资源泄露 } catch (const std::exception& e) { std::cout << "捕获异常:" << e.what() << std::endl; } // 程序结束后,res指向的内存未被释放,发生泄露 return 0; }

问题分析

1.如果riskyFunction中抛出异常,releaseResource不会执行,资源泄露;

2.即使没有异常,手动调用releaseResource容易遗漏,导致泄露;

3.代码需要手动配对 “获取 - 释放”,心智负担重。

二、解决方案:用析构函数自动释放资源

我们可以封装一个资源管理类,在构造函数中获取资源,析构函数中释放资源。只要这个类的对象离开作用域,析构函数就会自动调用,资源被释放,从根本上避免泄露。

代码示例:实现一个简单的资源管理类
#include <iostream> #include <string> #include <stdexcept> // 资源管理类:遵循RAII原则 class ResourceGuard { private: std::string* m_resource; // 管理的资源(这里以动态字符串为例) public: // 构造函数:获取资源(资源获取即初始化) explicit ResourceGuard(const std::string& content) : m_resource(new std::string(content)) { std::cout << "资源已获取,地址:" << m_resource << std::endl; } // 析构函数:自动释放资源(无论正常退出还是异常退出) ~ResourceGuard() { if (m_resource != nullptr) { delete m_resource; m_resource = nullptr; std::cout << "资源已释放" << std::endl; } } // 禁用拷贝构造和拷贝赋值(避免浅拷贝导致重复释放) ResourceGuard(const ResourceGuard&) = delete; ResourceGuard& operator=(const ResourceGuard&) = delete; // 提供访问资源的接口(可选) std::string& getResource() const { if (m_resource == nullptr) { throw std::runtime_error("资源已释放"); } return *m_resource; } }; // 安全的函数:使用资源管理类 void safeFunction(bool throwException) { // 创建资源管理对象,构造函数获取资源 ResourceGuard guard("我是受保护的资源"); // 模拟业务逻辑:即使抛出异常,guard的析构函数仍会执行 if (throwException) { throw std::runtime_error("业务逻辑异常,但资源不会泄露"); } // 正常使用资源 std::cout << "资源内容:" << guard.getResource() << std::endl; // 函数结束时,guard离开作用域,析构函数自动释放资源 } int main() { try { safeFunction(true); // 触发异常 } catch (const std::exception& e) { std::cout << "捕获异常:" << e.what() << std::endl; } std::cout << "程序正常结束" << std::endl; return 0; }
关键细节解释:

1.RAII 核心ResourceGuard的构造函数负责 “获取资源”,析构函数负责 “释放资源”,资源的生命周期与guard对象绑定;

2.异常安全:即使safeFunction抛出异常,guard对象的析构函数仍会被调用(C++ 保证栈上对象的析构函数在异常展开时执行),资源不会泄露;

3.禁用拷贝:如果允许拷贝,多个ResourceGuard对象会管理同一份资源,析构时会重复释放导致崩溃,因此禁用拷贝构造和拷贝赋值(C++11 后也可使用移动语义);

4.通用性:这个思路不仅适用于内存,还适用于文件句柄、锁、网络连接等所有需要手动释放的资源(比如std::fstream自动关闭文件、std::lock_guard自动释放锁,都是这个原理)。

进阶:使用标准库的智能指针(更推荐)

实际开发中,我们不需要自己写资源管理类,C++ 标准库提供了现成的智能指针(std::unique_ptr/std::shared_ptr),它们的底层就是利用析构函数自动释放资源:

#include <iostream> #include <string> #include <memory> // 包含智能指针头文件 #include <stdexcept> void smarterFunction(bool throwException) { // std::unique_ptr:独占式智能指针,析构时自动delete std::unique_ptr<std::string> res = std::make_unique<std::string>("智能指针管理的资源"); if (throwException) { throw std::runtime_error("异常发生,但智能指针会自动释放资源"); } std::cout << "资源内容:" << *res << std::endl; } int main() { try { smarterFunction(true); } catch (const std::exception& e) { std::cout << "捕获异常:" << e.what() << std::endl; } return 0; }

std::unique_ptr是条款 9 的最佳实践落地 —— 它完全遵循 RAII,无需手动管理,且性能几乎与裸指针一致。

总结

  1. 核心思想:将资源的释放逻辑封装到析构函数中,利用析构函数 “自动调用” 的特性,避免手动释放资源的遗漏或异常导致的泄露(RAII 原则);
  2. 关键做法:不要直接管理裸资源,而是用对象(如自定义资源管理类、标准库智能指针)包裹资源,让对象的生命周期与资源绑定;
  3. 实践推荐:优先使用 C++ 标准库提供的智能指针(std::unique_ptr/std::shared_ptr),而非手写资源管理类,避免重复造轮子且更安全。

条款10:在constructors内阻止资源泄露(resource leak)

一、核心问题:构造函数的特殊性

C++ 的构造函数没有返回值,且如果在构造过程中抛出异常,当前对象的析构函数不会被调用。这意味着:如果构造函数中分配了资源(如动态内存、文件句柄、锁、网络连接等),但在资源分配后、构造完成前抛出了异常,这些已分配的资源就无法被析构函数释放,从而导致资源泄露

二、解决方案:RAII(资源获取即初始化)

条款 10 的核心解决方案是RAII(Resource Acquisition Is Initialization):将资源的生命周期绑定到对象的生命周期 —— 资源在对象构造时获取,在对象析构时释放。具体来说:

1.把资源封装到独立的 “资源管理类” 中;

2.在构造函数中只创建资源管理类的对象,而非直接操作裸资源;

3.即使构造函数抛出异常,资源管理类的析构函数仍会被调用,从而保证资源释放。

三、代码示例:反例(有资源泄露)+ 正例(无泄露)

#include <iostream> #include <stdexcept> using namespace std; // 模拟一个需要手动释放的资源(如动态内存、文件句柄) class Resource { public: Resource() { cout << "Resource 分配成功\n"; } ~Resource() { cout << "Resource 释放成功\n"; } // 析构释放资源 void use() const { /* 资源使用逻辑 */ } }; // 有资源泄露风险的类 class BadClass { private: Resource* res1; // 裸指针管理资源1 Resource* res2; // 裸指针管理资源2 public: BadClass() { // 第一步:分配资源1(成功) res1 = new Resource(); // 第二步:模拟构造过程中抛出异常(比如资源2分配失败、逻辑错误) throw runtime_error("构造函数执行中发生异常"); // 第三步:分配资源2(永远不会执行) res2 = new Resource(); } ~BadClass() { // 析构函数不会被调用!因为构造函数抛异常,对象未完全构造 delete res1; delete res2; cout << "BadClass 析构,释放所有资源\n"; } }; int main() { try { BadClass obj; // 构造时抛异常 } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } // 输出:Resource 分配成功 → 捕获异常 → 无"Resource 释放成功" // 结论:res1的资源永远无法释放,造成泄露 return 0; }
2. 正例:用 RAII 封装资源(解决泄露)

核心思路:用智能指针(如 std::unique_ptr)替代裸指针 —— 智能指针是 RAII 的典型实现,其析构函数会自动释放管理的资源,即使构造函数抛异常。

#include <iostream> #include <stdexcept> #include <memory> // 包含智能指针头文件 using namespace std; // 待管理的资源(同上) class Resource { public: Resource() { cout << "Resource 分配成功\n"; } ~Resource() { cout << "Resource 释放成功\n"; } void use() const { /* 资源使用逻辑 */ } }; // 安全的类:用RAII(智能指针)管理资源 class GoodClass { private: // 用std::unique_ptr(独占所有权)替代裸指针,自动管理资源 unique_ptr<Resource> res1; unique_ptr<Resource> res2; public: GoodClass() { // 第一步:分配资源1(封装到unique_ptr中) res1 = make_unique<Resource>(); // C++14及以上,等价于 unique_ptr<Resource>(new Resource()) // 第二步:模拟构造过程中抛异常 throw runtime_error("构造函数执行中发生异常"); // 第三步:分配资源2(不会执行) res2 = make_unique<Resource>(); } ~GoodClass() { // 即使析构函数不手动释放,unique_ptr也会自动释放资源 cout << "GoodClass 析构\n"; } }; int main() { try { GoodClass obj; // 构造时抛异常 } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } // 输出:Resource 分配成功 → 捕获异常 → Resource 释放成功 // 结论:res1的资源被unique_ptr的析构函数自动释放,无泄露 return 0; }

3. 扩展:自定义 RAII 资源管理类(理解底层原理)

如果需要管理非内存资源(如文件句柄、锁),可以自定义 RAII 类:

#include <iostream> #include <stdexcept> #include <cstdio> // FILE相关头文件 using namespace std; // 自定义RAII类:管理文件句柄(非内存资源) class FileHandle { private: FILE* file; // 裸句柄(仅在RAII类内部使用) public: // 构造:获取资源(打开文件) FileHandle(const char* filename, const char* mode) { file = fopen(filename, mode); if (!file) { throw runtime_error("文件打开失败"); } cout << "文件 " << filename << " 打开成功\n"; } // 析构:释放资源(关闭文件) ~FileHandle() { if (file) { fclose(file); cout << "文件关闭成功\n"; } } // 禁用拷贝(避免资源重复释放) FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; // 提供资源访问接口 FILE* get() const { return file; } }; // 使用自定义RAII类的业务类 class FileProcessor { private: FileHandle fh; // RAII对象,绑定文件资源 public: FileProcessor(const char* filename) : fh(filename, "w") { // 模拟构造过程中抛异常 throw runtime_error("FileProcessor构造异常"); } }; int main() { try { FileProcessor fp("test.txt"); } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } // 输出:文件 test.txt 打开成功 → 捕获异常 → 文件关闭成功 // 结论:即使构造抛异常,FileHandle的析构仍会执行,文件句柄无泄露 return 0; }

总结

  1. 核心风险:构造函数抛异常时,对象未完全构造,析构函数不会执行,直接管理的裸资源会泄露;
  2. 核心方案:采用RAII思想,将资源封装到 “资源管理类” 中(如 std::unique_ptr、std::shared_ptr,或自定义 RAII 类),利用资源管理类的析构函数自动释放资源;
  3. 最佳实践:在构造函数中避免直接操作裸资源,优先使用标准库提供的智能指针,自定义资源(如文件、锁)需封装为独立 RAII 类,杜绝构造过程中的资源泄露。

关键点回顾:

  • 构造函数抛异常 → 析构函数不执行 → 裸资源泄露;
  • RAII:资源绑定到对象生命周期,构造获取、析构释放;
  • 智能指针是 RAII 的 “现成方案”,自定义 RAII 类适配非内存资源。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 17:43:15

2026国产时序数据库风云录:金仓“融合多模”架构异军突起

> 摘要&#xff1a;进入2026年&#xff0c;在“数字中国”与工业物联网浪潮的强劲推动下&#xff0c;国产时序数据库市场持续繁荣&#xff0c;竞争格局日趋清晰。本文将对当前主流的国产时序数据库进行梳理盘点&#xff0c;并特别聚焦于金仓数据库&#xff08;Kingbase&…

作者头像 李华
网站建设 2026/6/3 22:17:37

Python+django的基于人脸识别的学生考勤请假选课软件系统

目录基于人脸识别的学生考勤请假选课系统&#xff08;PythonDjango&#xff09;开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;基于人脸识别的学生考勤请假选课系统&#xff08;PythonDjang…

作者头像 李华
网站建设 2026/5/31 15:33:57

AI培训:这不是又一个“割韭菜”风口,而是一个时代的基建革命

最近很多人问我怎么看现在满天飞的AI课&#xff0c;是不是割韭菜的又来了。我说&#xff0c;有些确实是&#xff0c;但更大的真相是&#xff1a;现在很多讲AI的人&#xff0c;根本不知道真正的复杂组织是怎么运作的。他们没做过实体项目&#xff0c;不懂生产线上的瓶颈在哪&…

作者头像 李华
网站建设 2026/5/28 23:41:02

YOLOv8 实战剪枝:L1 剪枝精度仅降 0.8%,推理速度暴涨 10.7 FPS!

剪枝后对比图: 文章目录 代码 原理介绍 核心做法概述 移植代码 下载yolov8代码 在工作根目录创建compress.py 创建ultralytics\models\yolo\detect\compress.py ultralytics\models\yolo\detect\compress.py 移植ultralytics\nn\extra_modules 移植ultralytics\cfg\hyp.scra…

作者头像 李华
网站建设 2026/5/28 21:55:56

RK3588 实战级 YOLOv5 Android 全解析:NPU 量化 + 多线程 + 跟踪算法,源码一次买齐

文章目录 一、项目概述与目标 为什么选择RK3588? 二、开发环境准备 硬件与软件配置 项目源码获取 三、快速上手:编译与运行 项目目录结构解析 一键运行 四、系统架构深度解析 核心挑战与解决方案 1. 相机集成挑战 2. 并发处理难题 3. 图像格式转换 4. 数据缓存管理 系统整体架…

作者头像 李华
网站建设 2026/5/28 22:42:21

从原理到落地:Mamba-YOLOv8 全面实战指南(源码 + 训练 + 部署一次学会)

文章目录前言一、技术背景与动机1.1 传统架构的局限性1.2 Mamba的创新优势二、Mamba-YOLOv8架构详解2.1 整体架构设计2.2 核心模块&#xff1a;VSSblock2.3 SS2D模块工作原理三、完整实现流程3.1 环境配置3.2 代码集成步骤3.3 训练与微调四、性能分析与优化4.1 精度提升策略4.2…

作者头像 李华