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

【c++面向对象编程】第35篇:构造函数与异常:如何避免资源泄露?

目录

一、构造函数可以抛异常吗?

二、构造函数抛异常时,哪些资源会自动释放?

三、裸指针的陷阱:资源泄露

四、解决方案1:用智能指针管理动态资源

五、解决方案2:函数级 try-catch

六、完整例子:对比三种方案

七、构造函数异常与继承

八、最佳实践总结

九、这一篇的收获


一、构造函数可以抛异常吗?

可以,而且有时候必须抛异常。

构造函数没有返回值,无法像普通函数那样返回错误码。如果构造过程中发生错误(比如分配内存失败、参数无效),唯一的选择就是抛出异常。

cpp

class Buffer { private: int* data; size_t size; public: Buffer(size_t n) : size(n) { if (n == 0) { throw std::invalid_argument("大小不能为0"); } data = new int[n]; // 可能抛出 bad_alloc } ~Buffer() { delete[] data; } };

二、构造函数抛异常时,哪些资源会自动释放?

C++ 保证:已完成构造的成员对象会自动析构

cpp

#include <iostream> #include <stdexcept> using namespace std; class Member { string name; public: Member(const string& n) : name(n) { cout << "Member 构造: " << name << endl; } ~Member() { cout << "Member 析构: " << name << endl; } }; class Container { Member m1; Member m2; Member m3; public: Container() : m1("m1"), m2("m2"), m3("m3") { cout << "Container 构造函数体开始" << endl; throw runtime_error("构造失败"); cout << "Container 构造函数体结束" << endl; } ~Container() { cout << "Container 析构" << endl; } }; int main() { try { Container c; } catch (const exception& e) { cout << "捕获异常: " << e.what() << endl; } return 0; }

输出:

text

Member 构造: m1 Member 构造: m2 Member 构造: m3 Container 构造函数体开始 Member 析构: m3 Member 析构: m2 Member 析构: m1 捕获异常: 构造失败

关键观察

  • 先构造成员对象(m1m2m3

  • 构造函数体抛异常

  • 成员对象按构造逆序自动析构m3m2m1

  • Container的析构函数不会被调用(因为对象本身未完成构造)


三、裸指针的陷阱:资源泄露

上面的规则有一个重要的例外:裸指针指向的动态内存不会自动释放

cpp

class Leaky { private: int* p1; int* p2; public: Leaky() { p1 = new int[1000]; // 第一步 p2 = new int[1000]; // 第二步:如果这里抛出异常 } ~Leaky() { delete[] p1; delete[] p2; } };

如果new int[1000]抛出bad_alloc

  1. p1已经指向一块内存

  2. 异常被抛出,Leaky对象构造失败

  3. ~Leaky()不会被调用(因为对象未完成构造)

  4. p1指向的内存永远不会被释放 →内存泄露

这就是构造函数异常中最隐蔽的内存泄露来源。


四、解决方案1:用智能指针管理动态资源

cpp

#include <memory> using namespace std; class Safe { private: unique_ptr<int[]> p1; unique_ptr<int[]> p2; public: Safe(size_t n1, size_t n2) : p1(make_unique<int[]>(n1)), p2(make_unique<int[]>(n2)) { // 即使 p2 的构造抛异常,p1 也会被 unique_ptr 自动释放 // 因为 p1 作为成员对象,已经完成构造,会被自动析构 } // 不需要手动写析构函数! };

原理unique_ptr是成员对象,C++ 保证已构造的成员对象会被自动析构。即使构造函数体还没执行,初始化列表中的成员构造失败也会正确清理。


五、解决方案2:函数级 try-catch

如果必须使用裸指针,可以在构造函数中使用function-try-block

cpp

class Safe { private: int* p1; int* p2; public: Safe(size_t n1, size_t n2) try : p1(new int[n1]), p2(new int[n2]) { // 构造函数体 } catch (...) { // 注意:这里必须清理已分配的资源 delete[] p1; // p2 分配失败时,p1 已被分配,需要手动清理 delete[] p2; throw; // 重新抛出异常 } ~Safe() { delete[] p1; delete[] p2; } };

但这种写法比智能指针繁琐且易错,不推荐。


六、完整例子:对比三种方案

cpp

#include <iostream> #include <memory> #include <stdexcept> using namespace std; // ========== 方案1:裸指针(危险)========== class LeakyPointer { int* big1; int* big2; public: LeakyPointer() { big1 = new int[1000000]; cout << "已分配 big1" << endl; // 模拟第二个分配失败 throw bad_alloc(); big2 = new int[1000000]; } ~LeakyPointer() { delete[] big1; delete[] big2; cout << "LeakyPointer 析构" << endl; } }; // ========== 方案2:智能指针(安全)========== class SafePointer { unique_ptr<int[]> big1; unique_ptr<int[]> big2; public: SafePointer() : big1(make_unique<int[]>(1000000)), big2(make_unique<int[]>(1000000)) { // 如果这里抛异常 cout << "SafePointer 构造完成" << endl; } // 不需要析构函数 }; // ========== 方案3:成员对象管理(安全)========== class BigArray { unique_ptr<int[]> data; public: BigArray(size_t n) : data(make_unique<int[]>(n)) { cout << "BigArray 构造 " << n << " 个元素" << endl; } }; class Composite { BigArray a1; BigArray a2; public: Composite() : a1(1000000), a2(1000000) { cout << "Composite 构造完成" << endl; } // 不需要析构函数 }; int main() { cout << "=== 测试智能指针方案(安全)===" << endl; try { SafePointer sp; } catch (const bad_alloc& e) { cout << "捕获异常: " << e.what() << endl; } cout << "\n=== 测试成员对象方案(安全)===" << endl; try { Composite c; } catch (const bad_alloc& e) { cout << "捕获异常: " << e.what() << endl; } cout << "\n=== 测试裸指针方案(危险)===" << endl; try { LeakyPointer lp; // 内存泄露! } catch (const bad_alloc& e) { cout << "捕获异常: " << e.what() << endl; cout << "注意:big1 的内存已经泄露" << endl; } return 0; }

输出(示意):

text

=== 测试智能指针方案(安全)=== SafePointer 构造完成 === 测试成员对象方案(安全)=== BigArray 构造 1000000 个元素 BigArray 构造 1000000 个元素 Composite 构造完成 === 测试裸指针方案(危险)=== 已分配 big1 捕获异常: std::bad_alloc 注意:big1 的内存已经泄露

七、构造函数异常与继承

派生类构造函数抛异常时,基类子对象已经被构造,会被自动析构。

cpp

class Base { int* data; public: Base() : data(new int[1000]) { cout << "Base 构造" << endl; } ~Base() { delete[] data; cout << "Base 析构" << endl; } }; class Derived : public Base { int* more_data; public: Derived() : Base(), more_data(new int[1000]) { cout << "Derived 构造" << endl; throw runtime_error("构造失败"); } ~Derived() { delete[] more_data; cout << "Derived 析构" << endl; } }; int main() { try { Derived d; } catch (...) { cout << "捕获异常" << endl; } return 0; }

输出:

text

Base 构造 Base 析构 捕获异常

Derived的析构函数没有被调用(因为对象未完成构造),但Base子对象已经被析构,data被正确释放。


八、最佳实践总结

规则说明
用智能指针代替裸指针unique_ptrshared_ptr自动管理资源
遵循 RAII资源应在构造函数中获取,析构函数中释放
使用成员对象管理资源vectorstring、智能指针等 RAII 类持有资源
避免在构造函数体中分配资源优先在初始化列表中分配
如果必须用裸指针使用 function-try-block 并手动清理
不要在构造函数中抛异常后留下裸指针资源这是内存泄露的常见来源

九、这一篇的收获

你现在应该理解:

  • 构造函数可以抛异常,这是报告构造失败的唯一方式

  • 已完成构造的成员对象会自动析构(包括智能指针)

  • 裸指针指向的动态内存不会自动释放,即使它是成员变量

  • 用智能指针unique_ptrshared_ptr)代替裸指针,自动解决这个问题

  • 派生类构造失败时,基类子对象会被正确析构

💡 小作业:写一个FileHandler类,构造函数接收文件名并打开文件(用fopen)。如果文件打开失败,抛出异常。用unique_ptr<FILE, CustomDeleter>管理文件句柄,确保构造失败时不会泄露。写测试代码验证异常安全。


下一篇预告:第36篇《析构函数应永远不抛出异常——原因与最佳实践》——为什么析构函数抛异常是“死刑”?栈展开过程中抛异常会导致terminate。如何写“不抛异常”的析构函数?catch 所有异常并记录日志是最佳实践。

http://www.jsqmd.com/news/855306/

相关文章:

  • 【范式转换】从 XPath 定位到意图驱动:AI 视觉是如何重塑 UI 操作的?
  • 2026年Q2华东区域专业热喷涂服务商排行盘点:湖州,杭州,嘉兴,抗氧化热喷涂/电弧喷涂/电弧热喷涂/等离子热喷涂/选择指南 - 优质品牌商家
  • 2026年一人公司创业指南:OPC模式如何稳健起步
  • 别再手动补面了!ANSA Topo_CONS命令实战:从Paste到Project,5分钟搞定复杂几何修复
  • 2026年知名的铜陵全屋定制家居/铜陵橱柜全屋定制靠谱公司推荐 - 品牌宣传支持者
  • 2026年浙江门窗实测评测:乐道优品、乐道优品门窗、佛山120系列门窗、佛山别墅系统门窗、佛山封阳台门窗、佛山抗台风门窗选择指南 - 优质品牌商家
  • 实战解析:如何利用WRFDA的da_update_bc.exe正确更新WRF边界条件(以4DVAR为例)
  • 2026年5月评价高的海口工地砂石源头厂家哪家好厂家推荐榜,河沙、机制砂、碎石、加气砖、水泥砖厂家选择指南 - 海棠依旧大
  • 五月的风温柔细碎
  • 2026杭州狗主粮选购技术指南:杭州通用型狗粮、通用型狗粮、杭州100%鲜肉狗粮、杭州专用狗粮、杭州中型犬狗粮选择指南 - 优质品牌商家
  • Alist启动报错?别慌!手把手教你用Windows命令排查并解决5244端口占用问题
  • 不止于建模:用AnyLogic仿真优化地铁早高峰限流方案,我的参数调试心得
  • 研一开学前,我用这份保姆级时间表3个月搞定CV基础(附Python/PyTorch/OpenCV避坑指南)
  • GEFFEN格芬智能云控分布式电源管理系统GF-SPMS8
  • 通过Python SDK将Taotoken大模型能力嵌入自动化数据处理脚本
  • Windows进程注入技术全解析:从DLL注入到反射加载与APC机制
  • 连熬大夜帮大家总结了一下Google I/O 2026开发者大会,Gemini 3.5 Flash评价
  • 不同场景怎么处理文档?PDF 翻译、Office 翻译、AI 美化和多语言交付指南
  • 把OpenWrt路由器变成轻量Web服务器:手把手教你配置NGINX并挂载外部存储
  • RK3568核心板+基板硬件设计全解析:从模块化架构到嵌入式系统开发实战
  • 异步复位、异步复位-同步释放
  • 电商人必看!一键出图的快乐,谁用谁懂
  • 嵌入式储能监控系统开发实战:从核心板选型到算法部署
  • 郑州广告同行设计品牌盘点:河南广告同行设计、郑州展厅展馆设计、郑州广告同行设计、郑州文化墙设计、河南展厅展馆设计选择指南 - 优质品牌商家
  • 别再只用串口了!手把手教你用STM32CubeMX配置LIN总线(基于TJA1020收发器)
  • 开源项目Markdown Viewer:如何打造完美的浏览器Markdown阅读体验
  • 【软考高级架构】论文范文23——论分布式事务架构设计及应用
  • STM32CubeMX安装后,HAL库到底怎么选?在线安装慢、离线包找不到的终极解决指南
  • 5分钟轻松搞定GitHub中文界面:智能汉化插件让英文GitHub变母语
  • MiniMax-M2.7-W8A8 双机 DP=2 部署