异常处理遇到过的那些坑

今年有个目标之一就是提升团队代码的质量,所以时常会思索如何把这件事做到更好,不想教条主义,也不想搞出一个代码规范,强制团队照着做,落地的效果不好,反而把大家的积极性给弄没了。所以我的原则是,我们一起看看什么事是我们不能做的,排除掉,剩下的就是我们可以做的,同时真正搞清楚问题在哪里,而不是简单的模仿。从我个人的经验看,代码优化中最重要的一点就是对异常情况的处理。今天,我就借这个话题,谈谈如何来优化我们的代码。


坑1:捕捉异常不做处理


如下段代码所示,不知道各位在自家的代码中见过多少,我是期望大家不要遇见。且不说printStackTrace()这样的代码就不应该出线在正式的上线代码中,不做处理本身这会导致业务的潜在问题被隐藏了,所以这个坑是我认为异常处理中最糟糕的一点。


try

{

....

}

catch(Exception ex)
{

ex.printStackTrace();

System.out.println(XXX);

}



对于异常的捕捉处理,遵循以下的流程:

  1. 处理该异常,执行具体的逻辑处理,加上必要的系统日志记录 (log4j,logcat ...)。

  2. 涉及其他系统或者上一级系统,需要转换成对应的消息通知相关系统。上一级是前端,需要转换成前端可以“读懂”的错误提示。

  3. 无法处理该异常或者该异常需要上一级统一处理,Throw out该异常。


坑2:不处理资源释放


还是见代码说话,如果append出错,那么IO流就不会被关掉,那么最终就会导致整个程序因为溢出崩掉。


FileWriter fileWriter = null;

try

{

fileWriter = new FileWriter("");

fileWriter.append(item.toString());

fileWriter.close();

}

catch (IOException e)

{

...

}

在使用文件、IO流、数据库连接等不会自动释放的资源时,应该在使用完毕后马上将其关闭。关闭资源的代码try...catch...finally的finally内执行,否则就可能因为Exception的原因造成资源无法释放。


坑3:对异常不进行分类处理


代码中,最容易看到的一种情况就是设定catch的异常类型是Exception, 并且只有一套处理逻辑, 如下:

try{

...

}

catch (Exception e){

...

}


这里的问题主要有以下两点:


1. catch的不同Exception,可能需要执行不同的处理逻辑,一个catch要同时处理所有逻辑就很难实现。

2. 由于是捕捉的基类Exception, 那么RuntimeException也会被捕捉到,如果稍微不注意的话,RuntimeException最终没有任何实际处理,代码中的真正错误被掩盖掉了。


所以,正确的做法应该是按照具体的异常进行分类处理, 例如:

try{

...

}

catch (FileNotFoundException e){

// alert that the specified file

// does not exist

}

catch (EOFException e){

// alert that the end of the file

// was reached

}

catch (ObjectStreamException e){

// alert that the file is corrupted

}

catch (IOException e){

// alert that some other I/O

// error occurred

}


坑4:将大段代码放进一个Try-Catch中

  有时可以看到,一些代码的作者恨不得把整个函数里的实现代码都放入单个try中,原因就在于为了图省事,不愿花时间分析一大块代码中哪几行代码会抛出什么异常、异常的具体类型是什么,应该如何处理。这样的做法导致异常发生后,后续调试找问题更麻烦,一大段代码中有太多的地方可能抛出Exception。这样的做法导致,很难去统计和判断要catch哪一些类型的Exception, 只能写一个粗糙的Exception, 又掉进我们说的第3个坑里。


一点总结


在和大家一起分析了上面的异常坑后,如果未来我们想避免踩进一个异常坑,编写异常代码可以遵循以下三个点:


1. 出了什么错?

2. 在哪出的错?

3. 为什么出错?


扫描二维码或手动搜索微信公众号【架构栈】: ForestNotes

欢迎转载,带上以下二维码即可


<think> 嗯,用户这次想了解C++异常处理的注意事项。之前用户提到过gcc memcpy出现data abort的问题,但当前问题已经转向C++异常处理,需要切换思维模式。 用户可能正在学习C++异常机制,或者在实际开发中遇到了异常安全问题。从引用内容看,用户应该需要实用性的指导而非理论探讨,特别是资源管理和RAII这类核心问题。 注意到引用[1]强调异常安全对资源管理的影响很大,引用[2][3]具体讨论标准异常用法和封装陷阱,引用[4][5]补充异常优势和资源释放模式。这些点都需要整合进回答。 核心注意事项应该包括:RAII的关键作用(避免内存泄漏)、异常类型的选择(继承std::exception)、资源清理的三种模式(try-catch清理、智能指针、RAII包装器)、以及跨模块异常的风险。特别是引用[5]展示的new/delete配对场景很典型,需要强调catch中重新抛出(throw;)的正确用法。 用户可能还隐含想知道:为什么C++异常比错误码更适合资源管理?这可以从引用[4]分离正常/错误逻辑的角度展开,但需控制篇幅。此外,DLL异常传递的(引用[3])值得单独提醒,这点容易被忽略。 最后需要补充现代C++特性:noexcept、move语义对异常安全的影响,以及异常对性能的实际影响测量。虽然引用未提及,但属于必须注意事项。 </think> 以下是C++异常处理的核心注意事项及最佳实践,结合关键问题和解决方案: --- ### 一、资源泄漏防范(核心重点) 1. **RAII原则** 使用智能指针或资源管理类替代裸指针: ```cpp // 错误示范:裸指针易泄漏 void unsafe_func() { int* ptr = new int[100]; if (error) throw std::runtime_error("error"); delete[] ptr; // 异常抛出时无法执行 } // 正确做法:使用unique_ptr void safe_func() { auto ptr = std::make_unique<int[]>(100); if (error) throw std::runtime_error("error"); } // 退出时自动释放内存 ``` *引用[1][5]强调RAII是避免资源泄漏的核心手段* 2. **构造函数异常** 构造函数内抛出异常时,析构函数**不会执行**: ```cpp class Resource { int* data; public: Resource() : data(new int[100]) { if (init_fail) throw std::runtime_error("Init failed"); } ~Resource() { delete[] data; } // 构造失败时不会调用 }; ``` **解决**: - 使用成员智能指针 `std::unique_ptr<int[]> data;` - 或分阶段初始化(工厂模式) --- ### 二、异常类型与传播规范 1. **标准异常体系** 继承 `std::exception` 并实现 `what()`: ```cpp class MyError : public std::runtime_error { public: MyError(const std::string& msg) : std::runtime_error("MyError: " + msg) {} }; try { throw MyError("file not found"); } catch (const std::exception& e) { std::cout << e.what(); // 输出"MyError: file not found" } ``` *引用[2][4]强调标准异常提供类型安全和统一接口* 2. **跨模块/DLL边界** - 避免跨DLL边界传递异常(不同编译器异常实现可能不兼容) - DLL接口使用错误码封装异常: ```cpp // DLL导出函数 extern "C" __declspec(dllexport) int safe_call() noexcept { try { /* 可能抛出异常的操作 */ } catch (...) { return -1; } // 转换为错误码 } ``` *引用[3]指出DLL异常传递需特殊处理* --- ### 三、异常安全等级 | 安全等级 | 要求 | 实现方法示例 | |---------------|-------------------------------|------------------------------| | 基本保证 | 资源不泄漏,对象状态有效 | RAII + 异常中立 | | 强保证 | 操作完全成功或状态回滚 | 事务模式(copy-swap惯用法) | | 不抛保证 | 承诺不抛出异常 | `noexcept`关键字修饰函数 | ```cpp // 强保证示例:copy-swap class Vector { void swap(Vector& rhs) noexcept; // 不抛异常的操作 public: Vector& operator=(const Vector& rhs) { Vector temp(rhs); // 可能抛异常(但不影响原对象) swap(temp); // noexcept操作 return *this; } }; ``` --- ### 四、关键禁忌 1. **禁止在析构函数中抛出异常** ```cpp ~MyClass() noexcept { // 必须声明noexcept // 可能抛异常的操作 -> 改为日志记录 } ``` - 若析构函数抛出异常,程序可能直接终止 2. **避免异常屏蔽** 清理操作中需重新抛出原始异常: ```cpp void process() { Connection conn; try { /* 可能抛出 */ } catch (...) { conn.cleanup(); // 清理资源 throw; // 重新抛出原始异常 } } ``` *引用[5]展示资源清理后重新抛出* --- ### 五、性能优化 1. **零开销原则** - 无异常时代码性能 ≈ 返回错误码 - 使用 `noexcept` 帮助编译器优化(如移动操作) ```cpp void fast_op() noexcept; // 承诺不会抛出 ``` 2. **异常 vs 错误码** | 场景 | 推荐方式 | |---------------------|--------------| | 高频失败路径 | 错误码 | | 低频严重错误 | 异常 | | 构造函数/操作符重载 | 异常(唯一选择) | --- ### 最佳实践总结 1. **资源管理**:始终用RAII替代手动管理 2. **异常类型**:继承`std::exception`,提供有意义的`what()` 3. **安全保证**:通过copy-swap实现强异常安全 4. **跨模块**:DLL接口用错误码封装异常 5. **析构函数**:必须声明`noexcept`且禁止抛出 > “异常安全不是特性,而是通过设计约束达成的代码属性。” —— Herb Sutter --- **
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值