资讯动态

C++异常处理实战:从机制原理到性能优化与工程规范

发布时间:2026/9/10 4:25:53 来源:尧图企业网站定制
C这门语言发展了这么多年异常处理Exception Handling至今还是争议最大的特性之一。有人把它当成洪水猛兽觉得异常会打断程序流程、带来不可控的性能损耗也有人把异常当成了命根子没有异常就不敢写构造函数、不敢碰STL容器。站在一线开发的立场上我的看法很直接异常处理不是C的附属品而是这门语言资源管理和错误传播体系的基石。如果你不主动掌握它代码里就会悄悄长出无数个“裸指针错误码if判断”的组合最后在某个线上环境不堪重负时突然爆发。这篇文章我想从实战角度把自己折腾异常处理的经验完整梳理一遍覆盖语法机制、栈解旋、异常安全级别、noexcept的语义、多线程下的传播、性能开销的真实面目以及日常排查中的常见坑。无论你是刚学完C语法准备写项目的学生还是已经写了几年C但一直刻意避开异常的开发者这篇内容都能帮你把异常这个工具箱彻底打开知道什么时候该用、什么时候不该用、以及用了之后怎么把副作用摁到最小。1. 异常机制的核心概念与原理解读1.1 先搞清楚异常从哪来、往哪去日常写代码时我们经常把“抛出异常”和“返回错误码”放在一起对比但这两个东西的本质完全不同。错误码是函数的返回值调用方拿到之后可以选择处理也可以选择忽略传递链路完全由调用方控制。异常则是一种独立的控制流它在某个函数里通过throw被构造出来之后会沿着函数调用栈一直向上传播沿途跳过所有没有匹配catch块的作用域直到找到合适的处理者。这个“跳过”的动作是编译器自动完成的不需要传递层主动编写代码。我见过很多新手刚接触异常时最大的困惑是异常到底存在哪里其实异常对象会被创建在栈上的临时存储区然后由运行时库沿着栈向上查找处理者。查找过程中栈上已经构造好的局部对象会被逐个析构这个析构过程就是“栈解旋”。一旦异常被捕获局部对象已经干干净净地销毁了资源不会泄露这是异常机制和goto或者longjmp最本质的区别。很多教程喜欢把异常和“处理错误”画等号严格来说并不准确。异常更适合描述的是“我们无法在当前位置处理、也不想调用方默默忽略”的情况。就拿文件读取来说如果文件不存在导致整个模块无法继续运作这是异常的理想场景但如果只是某个字段解析失败完全可以用std::optional或者错误码表达没必要动用异常这种重型武器。关键在于编码者要明确当前场景属于哪一类这个判断直接决定代码风格的好坏。#include iostream #include stdexcept #include string double divide(double a, double b) { if (b 0.0) { throw std::runtime_error(division by zero); } return a / b; } int main() { try { double result divide(10.0, 0.0); std::cout result result \n; } catch (const std::runtime_error e) { std::cout caught: e.what() \n; } return 0; }这段代码看起来平平无奇但它把异常的三个关键环节都串起来了throw构造异常、运行时栈展开、catch匹配并处理。实际项目中我们不会只用runtime_error还会自定义异常类型但核心机制完全一样。1.2 异常规范的演进从throw()到noexcept如果只看老教材你会看到void func()这样的声明这是C98时代的动态异常规范意思是这个函数不会抛出任何异常。然而这个规范在运行时并不强制检查只要函数内部真的throw了最后就会调用std::unexpected而且编译器为它生成的检查代码还会带来额外开销。正因为太不好用C11直接废除了动态异常规范引入noexcept关键字。noexcept和旧的动态异常规范完全不同。它有两个用途第一告诉编译器这个函数不扔异常第二如果函数运行时真的扔了异常程序会直接调用std::terminate终止而不是继续向上传播。这样设计的核心价值是给编译器提供优化依据特别是移动构造函数、swap、vector扩容这些高性能关键路径noexcept标记直接影响容器选择“拷贝扩容”还是“移动扩容”。这里有个非常典型的例子vector在扩容的时候需要把旧元素搬到新内存。如果移动构造函数不保证不抛异常vector会退化成拷贝构造以避免在搬了一半时异常导致元素状态不可恢复。C标准库的std::vector实现正是通过检测std::is_nothrow_move_constructible来决定走哪条路径的。class Widget { public: Widget() default; Widget(Widget other) noexcept { /* 移动资源 */ } Widget operator(Widget other) noexcept { /* 移动赋值 */ } private: // 资源成员 };在自定义类型里给移动构造函数和移动赋值运算符加上noexcept是我在工作里一直坚持的习惯。别看只是一行关键字它在容器操作和算法析构里带来的性能差异是实打实的。但注意不是所有函数都能随便标noexcept如果内部调用了可能抛异常的逻辑比如分配内存、打开文件盲目加上就是在给自己埋雷。1.3 栈解旋到底是怎么工作的来展开讲讲栈解旋。假设函数A调用函数BB里抛出了异常那么当异常发生时编译器会做两件事第一沿着调用链一路寻找匹配的catch块第二在查找的过程中逐层析构作用域内已经构造完成的局部对象。这个过程被称为unwinding。有一个细节很容易被忽视析构函数本身绝对不能抛出异常。假如栈解旋过程中某个析构函数又抛了异常而当时已经有一个异常在传播中两个异常同时出现会直接触发std::terminate程序当场崩溃。这正是C标准要求析构函数默认加noexcept的原因。我在代码评审中看到有人往析构函数里写fclose、close这类可能失败的调用时就格外警觉因为一旦文件关闭失败析构里抛出的异常会直接把整个程序带走。class FileGuard { public: explicit FileGuard(const char* name) : file(std::fopen(name, r)) { if (!file) { throw std::runtime_error(cannot open file); } } ~FileGuard() noexcept { if (file) { std::fclose(file); } } FileGuard(const FileGuard) delete; FileGuard operator(const FileGuard) delete; private: std::FILE* file; };这段代码就是一个典型的RAII封装构造时打开文件析构时负责关闭析构函数标记noexcept防止意外终止。就算构造函数抛异常类本身没有构造成功析构函数也不会被调用资源自然安全。用实际场景验证一下如果文件打开失败FileGuard对象压根不存在不需要担心析构如果打开成功那在作用域结束时析构一定会执行文件必然被关闭。这就是RAII和异常配合最优雅的地方。2. 异常安全的工程实践写代码前先想清楚保障级别2.1 三种异常安全保证级别讲透了其实不难Herb Sutter在《Exceptional C》里把异常安全分成三个级别基本保证、强保证、不抛保证。基本保证是指抛出异常后程序处于有效但可能被修改过的状态没有资源泄露所有不变量保持一致强保证是指操作要么完全成功要么完全没有效果相当于事务回滚的效果不抛保证则是函数承诺永远不抛出异常比如析构函数、swap函数通常要做到这个级别。这三个级别的概念看起来偏理论但真正落到代码上会直接影响接口设计。举个例子往vector里push_back一个新元素内存分配失败会抛出bad_alloc这是基本保证因为vector本身依然有效只是新增的元素没加进去。但要实现强保证就要先构建好元素副本再采取不会失败的方式接入容器比如swap两个已存在的对象。实际工作中我会用一个简单的类型来描述这个理念class Account { public: void deposit(double amount) { if (amount 0) { throw std::invalid_argument(negative amount); } Account temp(*this); balance amount; swap(temp); } void swap(Account other) noexcept { std::swap(balance, other.balance); } private: double balance{0}; };这种先拷贝再修改再交换的方式让deposit拥有了强异常安全保证。不管中间发生什么异常原始的Account对象都不受影响。当然这种写法不是免费的每次操作都多了一次拷贝所以要根据实际情况权衡不是所有函数都必须做到强保证。2.2 用RAII管好资源异常就好办了一半我一向认为异常和RAII是一对必然搭档。如果没有RAII你在函数里手动管理new出来的指针遇到异常时就得写一堆try/catch去手动释放代码又长又容易漏。有了RAII资源生命周期绑定到栈对象上无论正常返回还是异常展开析构函数都会执行资源管理就自动完成了。看一个容易被坑到的地方如果代码里用裸指针保存资源然后中途某个操作抛出异常裸指针永远不会被释放内存就泄露了。这不是异常机制的锅而是没有用RAII的锅。正确做法是用std::unique_ptr、std::shared_ptr、std::lock_guard这些标准封装。void processData() { auto resource std::make_uniqueResource(); // 中途若抛出异常resource的析构函数会被自动调用 resource-doWork(); }这段代码里没有任何catch块看起来“没有处理异常”但实际上资源和内存安全都已经被RAII兜住了。有经验的C开发者往往不是靠到处写catch来保证安全而是靠“析构函数必然执行”这个不变量来建立信心。这也是为什么我在团队里会要求能用栈对象就用栈对象实在要动态分配就包进智能指针裸指针只能出现在极少数非所有权场景。2.3 异常和错误码到底该怎么选这是C社区的老话题我的判断标准很简单错误码适合“本地可处理、调用方必须显式验证”的场景异常适合“跨层传播、调用方无法局部处理”的场景。比如网络库的底层socket操作返回失败调用链上每层都需要检查这时候错误码会让代码里全是重复的if判断漏掉一个就出问题但如果用异常底层直接抛出“连接超时”中间层完全可以不关心直到最上层统一捕获并提示用户。当然错误码也有自己的优势它适合高频热路径因为在现代CPU上异常抛出和栈展开的性能成本明显高于单个整数比较它也适合“失败是常规状态”的场景比如用户输入校验错误码比异常更自然毕竟只是问一下“输入合法吗”不值得用异常来打断代码流。一个比较实用的经验法则场景推荐方式理由构造函数初始化失败异常构造函数无法返回错误码只能靠异常通知调用方底层资源创建失败异常错误需要跨多层传播用错误码逐层判断容易漏频繁且可预期的小错误错误码 / optional性能开销小代码直白用户输入不合规错误码 / optional不是灾难性状况不应中断正常流程多线程中线程函数出错异常 捕获后传递线程内不能跨线程传播异常需包装处理这个表我在新人培训时讲过很多次每次都能引发讨论。核心不是哪个更高级而是你能不能诚实面对当前场景的复杂度。3. 性能开销的真相与优化手段3.1 异常不是零成本但也没有传说中那么贵很多人不敢用异常是因为听说“异常会拖慢程序”但这个说法需要拆开来看。在不抛异常的时候现代编译器的异常处理模型几乎不会带来额外负担这被称作零开销原则。代价是异常表区域会占据二进制文件的一些空间可代码执行路径上并不会明显变慢。真正昂贵的部分发生在抛出异常的那一刻——需要查表、遍历栈帧、执行析构函数这个过程可能比一次分支判断贵上几个数量级。这意味着什么呢异常的性能问题不是“用了就慢”而是“一旦触发异常控制流跳转的代价比较高”。所以如果你的程序里经常触发异常那就不太合适了。比如一个循环里每次都要解析用户输入输入又不稳定经常抛invalid_argument这种用法确实会拖垮性能应该改成先校验再解析。但如果异常只在极少数异常事件中触发那性能代价完全可以忽略。我曾经压测过一个对象序列化模块正常路径下带异常处理的版本和禁用异常版本几乎无明显差异而模拟网络异常注入时异常处理版本依然能在几毫秒内完成错误恢复。这说明优化重点应该放在“避免让异常成为常规路径”而不是从一开始就否定异常。3.2 noexcept怎么影响代码生成和容器行为noexcept除了语义上的承诺还能让编译器生成更紧凑的代码。因为不需要为函数生成异常展开表项和栈解旋信息二进制体积和调用开销都能降下来。更关键的是标准库的容器和算法会检测类型是否有noexcept移动构造从而决定是否采用移动语义。看一下这个经典例子#include vector #include type_traits class GoodMover { public: GoodMover(GoodMover) noexcept {} }; class BadMover { public: BadMover(BadMover) {} // 没有 noexcept }; static_assert(std::is_nothrow_move_constructibleGoodMover::value, should be true); static_assert(!std::is_nothrow_move_constructibleBadMover::value, should be false);vector在扩容量时会用if constexpr或std::move_if_noexcept来决定调用移动构造还是拷贝构造。如果移动构造不保证不抛异常为了强异常安全保证编译器会退化成拷贝构造性能自然下降。这个细微之处很多性能问题都是从这里来的。所以在自定义类型里移动构造函数和移动赋值运算符务必标上noexcept除非内部确实有抛异常的路径。3.3 异常对象的抛出与捕获方式异常对象的构造和析构也是性能的一部分。如果throw一个字符串类型的异常它的构造成本可能比throw一个整型码高得多如果使用throw std::runtime_error(some message)字符串的拷贝和析构都要计入成本。所以开发时尽量不要在热路径上抛出需要构造复杂数据的异常。捕获时也分两种情况按值捕获会再次拷贝/移动异常对象按引用捕获不会拷贝。所以标准建议是用catch (const std::exception e)不但避免拷贝还能通过基类引用统一处理派生异常。如果写成catch (std::runtime_error e)异常对象会被切片类型信息丢失还可能多一次拷贝性能和安全都不占。try { // something risky } catch (const std::exception e) { // 统一处理 std::cerr e.what() \n; }对于类型不同的异常catch块可以按顺序列出多个编译器会从上到下匹配第一个能处理的类型。注意这里不建议用“异常类型转字符串再判断”的方式处理那是把面向对象的类型系统丢在一边迟早会踩坑。4. 真实项目中的异常处理实战模式4.1 构造函数失败怎么办构造函数是没有返回值的所以它只能通过异常来报告失败。这是异常在C里最不可替代的位置也是初学者最容易漏掉的地方。比如一个数据库连接对象构造函数里要打开连接如果打开失败不抛异常就只能在对象里留一个无效状态调用方必须每次先检查isValid()再使用非常容易漏。正确思路是构造函数里发现无法满足不变量时第一时间throw。这样对象根本不会诞生调用方也绝对不可能拿到一个“半死不活”的对象。class DatabaseConnection { public: DatabaseConnection(const std::string host, int port) { connection initDbConnection(host, port); if (!connection) { throw std::runtime_error(failed to connect to database: host : std::to_string(port)); } } ~DatabaseConnection() noexcept { closeDbConnection(connection); } private: void* connection; };这个设计有几个好处调用方可以放心地使用对象因为构造函数一旦返回对象就是完整可用的如果抛出异常资源由RAII机制守护不会泄露。唯一的代价是调用方必须处理异常否则程序会在未捕获异常时终止。这也是为什么业务代码里最上层一定要有一个兜底的catch块。4.2 多线程环境下的异常传播C标准中一个线程抛出但未捕获的异常不会自动传播到其他线程它会导致std::terminate被调用。因此多线程编程里线程函数内部一定要自己捕获异常否则整个进程可能直接崩溃。实践中我常用的模式是把异常存进std::exception_ptr然后由其他线程取走并重新抛出#include thread #include exception #include iostream #include vector void worker(std::exception_ptr error) { try { throw std::runtime_error(worker error); } catch (...) { error std::current_exception(); } } int main() { std::exception_ptr error; std::thread t(worker, std::ref(error)); t.join(); if (error) { try { std::rethrow_exception(error); } catch (const std::exception e) { std::cerr thread failed: e.what() \n; } } return 0; }这种exception_ptr方案比先把异常转换成字符串再跨线程传递要优雅得多因为异常对象的完整类型信息可以被保留下来。比如子线程抛出一个自定义的NetworkTimeoutError主线程可以按这个类型捕获并做专门处理。这个模式在异步任务框架、线程池、协程库里都很常见值得记下来。4.3 分层处理异常与日志记录异常处理最常见的坏味道是在每一层都写catch打日志然后继续往上抛。这样日志里会出现好几条重复的错误记录而且每层都catch又catch代码噪音大排查时反而分不清哪条才是真正需要关注的。我习惯的做法是“错误边界分层”底层函数完全不用catch让异常自然向上传播中间层如果需要对错误做上下文补充就在捕获后包装成新的异常抛出带上下文的类型最上层比如main函数、事件循环、请求入口采用一个集中式catch块统一记录日志并决定如何展示给用户。int main() { try { runApplication(); } catch (const std::exception e) { std::cerr fatal error: e.what() \n; return EXIT_FAILURE; } catch (...) { std::cerr unknown fatal error\n; return EXIT_FAILURE; } return EXIT_SUCCESS; }这里还有一个小技巧catch (...)可以捕获所有异常包括非标准类型比如throw 42但它拿不到异常对象本身所以只能用来做最终兜底。顺序必须是把特定类型写在前面catch (...)放最后否则编译器会报错因为从继承关系上看std::exception的catch块会拦截所有后续类型。日志记录尽量在真正要处理的边界做不要在每一层重复记录。如果确实需要中间层补充信息可以考虑用std::throw_with_nested与std::rethrow_if_nested保留原始异常链这样日志排错时能从顶层一路看到底层根因。5. 常见问题与排查技巧实录5.1 为什么我catch了异常却还是崩溃这是一个高频问题。很多人以为自己写了catch就万事大吉但程序仍然终止通常原因如下其一异常发生在析构函数或noexcept函数内部。析构函数默认隐式noexcept如果你在析构里抛异常即使外面有catch也无法接住最终调用std::terminate。解决办法是析构函数里绝不抛异常必要时catch后吞掉或仅记录日志。其二异常发生在栈展开过程中。某个函数抛出异常栈在展开时它内部某个局部对象的析构函数又抛出第二个异常两个异常同时存在立刻终止。这就是为什么析构函数必须保守。其三你捕获的是值而非引用类型不匹配。比如函数抛的是std::runtime_error你写catch (std::exception e)没问题但如果抛的是const char*你只写了catch (const std::exception e)那就根本接不住只能靠catch (...)或单独匹配。其四noexcept函数被标记后内部调用了可能抛异常的函数而你自己没有在函数内部处理。比如void process() noexcept { std::vectorint v; v.push_back(1); // 可能抛 bad_alloc }编译器在noexcept函数内遇到底层异常时不会继续向外传播而是直接终止。这种崩溃最难排查因为崩溃点往往在很远的地方所以标noexcept前一定要确认函数体内确实不会产生未捕获异常。5.2 catch(...)会不会把所有问题都吞掉catch (...)是个威力极大的工具但也是把双刃剑。它的本意是捕获未知类型保证程序不因未处理异常而崩掉。但滥用它比如在每层业务逻辑里都写了一个空的catch (...)那么真正的错误会被无声无息地吞掉整个系统进入一种“看起来没事但状态已坏”的情况比崩溃还难调。我的建议是catch (...)只出现在最上层的边界比如main函数、线程入口、任务回调入口并且至少要记录一条日志或把异常信息保存起来绝不能空白吞掉。确实需要“不管什么异常都继续执行”的场景也要考虑用一个状态位或计数器记录错误次数后续定期上报或者决策是否退出。5.3 排查异常问题时的实用技巧排查异常崩溃第一步不是打日志而是看核心转储。未捕获异常导致std::terminate时程序会在终止点留下转储文件用调试器查看调用栈能定位到具体是哪个noexcept函数拦住了异常或者哪个析构函数里抛出了异常。另一个实用技巧是给自定义异常类型加上文件、函数、行号信息。比如在项目中定义一个BaseException构造函数里接收const char* file和int line然后通过宏封装抛出方式这样捕获时就能直接打印出原始抛出位置比只写一个“operation failed”要高效得多。#include exception #include string class BaseException : public std::exception { public: BaseException(std::string msg, const char* file, int line) : msg_(std::move(msg)), file_(file), line_(line) {} const char* what() const noexcept override { return msg_.c_str(); } const char* file() const noexcept { return file_; } int line() const noexcept { return line_; } private: std::string msg_; const char* file_; int line_; }; #define THROW_BASE_EXCEPTION(msg) \ throw BaseException((msg), __FILE__, __LINE__)这样抓异常时日志里就能直接看到哪一行抛出来的排查成本的下降幅度非常大。这个宏在大型项目里的收益我深有体会强烈推荐。还有一个小习惯捕获到异常后如果这个异常不影响程序继续运行尽量把what()写入结构化日志并附加上下文变量值而不是只打印一个空泛的“error”。我在排查一个线上队列消费异常时就靠这种上下文日志几秒内定位到是某个消息在反序列化时出现了非法索引而不是像以前那样面对一行std::exception干瞪眼。5.4 异常安全测试和代码审查的关注点测试异常路径是个技术活。常规单测往往只覆盖正常路径或错误码路径异常路径很难通过普通方法触发。一个可行的思路是把可能有问题的资源分配函数做成可替换用mock在测试里注入异常。比如用依赖注入的方式把内存分配或文件打开操作抽象成函数对象测试时让它抛出异常就能验证RAII和异常安全逻辑是否正确。代码审查时我重点关注几个位置构造函数、析构函数、noexcept标记、资源管理类、以及所有包含裸new/delete或fopen/fclose的函数。看到delete和fclose出现在函数体里而不是封装在析构里我就会特别谨慎看到函数明明会抛异常却标了noexcept会去确认是否有兜底catch。反过来如果一份代码里几乎看不到catch但能看到一堆RAII对象和智能指针那这份代码反而可能比较安全因为它的资源保障机制已经内建在对象生命周期里了。关于异常性能我再补充一个测试数据视角在一个频繁调用的数学函数里如果每次都直接throw并catch性能会降低几十倍因为异常展开代价远高于一次比较但如果只是设置一个分支判断避免异常触发那么加入了异常机制的代码和完全不使用异常的版本性能几乎一样。这说明关键并不是“要不要使用异常”而是“如何使用异常、触发频率高不高”。只要把异常当作真正的异常事件而不是常规流程分支性能问题基本不会找上门。6. 代码风格与团队规范建议6.1 团队约定哪些地方必须用异常哪些地方禁止每个人的口味不同团队如果各写各的代码库一定会乱。根据我参与过的C项目经验我们团队约定这样的规范构造函数、资源获取类、跨模块的错误传播统一使用异常高频循环里、用户输入快速校验、以及底层纯算法模块内部尽量避开异常使用错误码或std::optional析构函数和noexcept函数内禁止抛出异常任何异常必须有一个集中处理边界禁止在中层随意catch又吞掉。这种约定最大的价值是让代码风格可预期。看代码时只要看到某个函数签名就能大概判断出它是“可能抛异常”还是“绝不抛异常”设计意图一目了然。noexcept在这里不仅是编译器优化开关更是一个接口文档。6.2 自定义异常类型与继承体系标准库里的std::runtime_error、std::logic_error只能覆盖基础场景真实项目里异常类型应该能表达业务语义。我通常会定义一个应用级基类再按模块继承出网络异常、数据库异常、协议异常、配置异常等每个类型里可以携带错误码和上下文参数。#include stdexcept #include string class AppException : public std::runtime_error { public: AppException(const std::string msg, int errorCode) : std::runtime_error(msg), errorCode_(errorCode) {} int errorCode() const noexcept { return errorCode_; } private: int errorCode_; }; class NetworkException : public AppException { public: NetworkException(const std::string msg, int errorCode) : AppException(msg, errorCode) {} };这样做的好处是调用方可以通过catch (const NetworkException)精确拦截特定的业务异常而不是对所有异常一刀切。但注意异常类型不宜设计得过深过碎不然调用方不知道到底该catch哪一层反而增加心智负担。我的建议是2到3层就够第一层是标准基类第二层是项目基类第三层按具体业务领域细分。6.3 实战里的代码反面教材最后分享一个大家可能都写过的反面代码void doSomething() { try { // 大量逻辑 } catch (const std::exception e) { // 什么都不做 } }这就是典型的“吞异常”。它会让程序从此变得像一辆仪表盘上全是故障灯但依然能跑的汽车没人知道哪里出了问题。另一种反面教材是void doSomething() { int* p new int[10]; // 使用p delete[] p; }如果“使用p”中间抛了异常delete[] p根本执行不到内存直接泄露。正确写法是用std::unique_ptrint[]或std::vectorint替代裸数组。这类问题在评审中反复出现根源不是开发者不懂得理论而是没有把“异常是常态控制流的一部分”这个观念内化到每一次写代码的瞬间。我自己在早期也犯过同样的错误。那时候项目要求“性能优化”我在一个解析器里大量使用错误码每个函数都返回bool再通过引用参数传出数据代码里到处是分支判断。后来遇到一次数据格式异常程序在底层把返回状态忽略了继续往上用垃圾数据最后产生了一个几乎无法复现的偶发错误。排查了两天才发现是错误码被跳过。改成异常传播之后这类问题再也没有出现过。从那次以后我对待异常的态度从“尽量避免”变成了“该用一定要用并且用得干净”。最后分享一点个人体会我做了这么多年C开发最大的感触是C异常处理不是靠记住几条语法就能掌握的它考验的是你对资源生命周期、栈语义和错误传播的整体把握能力。一个会写异常安全的C程序员写出来的代码通常也更模块化因为只有拆成职责清晰的RAII对象异常才能自动帮你把“半路出错”的烂摊子收拾干净。如果你正打算在项目里大范围推行异常建议先从模块内部开始定义一个统一的异常基类把构造失败、资源获取失败这些“非异常不可”的场景先覆盖再把最上层的catch边界立好剩下的事情就交给RAII和智能指针。不要指望一年之内所有代码都异常安全但每改一个模块就离“可靠地失败”更近了一步。这本来就是C这门语言给我们的独特礼物。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价