文章目录异常处理基本概念定义关键字用法代码块代码块C标准异常类层次结构标准异常类层次结构示例自定义异常类示例noexcept验证函数是否 noexcept什么时候使用 noexcept异常安全性异常跨函数传播示例Copy-and-Swap 惯用法与异常安全基本模式异常安全流程拆解关键要点首先effective c中条款第29提到请记住1. 异常安全函数 Exception-safe functions即使发生异常也不会泄露资源或允许任何数据类型败坏。这样的函数区分为三种可能的保证基本型 强烈型 不抛异常型2. “强烈保证” 往往能够以 copy-and-swap 实现出来 但 “强烈保证” 并非对所有函数都可实现或具备现实意义3. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的 “异常安全保证”中的最弱者我需要告诉你本篇甚至大部分的异常处理都围绕基本型异常。强烈型异常基本就用copy-and-swap见文章最后即可不抛异常很简单就是noexcept关键字。详细见下文注意别小看以上内容如果你要真正成为一名程序员一定需要先成为一位异常处理大师本系列所有内容都如同本篇一样优秀喔欢迎点赞关注异常处理异常处理是一项重要的特性它允许程序在运行时遇到错误条件时能够优雅地恢复或终止执行而不是简单地崩溃。C通过try、catch和throw三个关键字来实现异常处理机制。本文会详细介绍。基本概念定义异常是指在程序执行过程中发生的、不符合程序正常流程的事件。在C中异常通常是由于某些错误条件触发的如除以零、数组越界、内存分配失败等。关键字try用于标记可能会抛出异常的代码块称为保护代码。throw当检测到异常条件时使用throw关键字抛出一个异常。throw后面可以跟任意表达式它的类型决定了抛出的异常类型。catch用于捕获并处理异常。catch块紧跟在try块之后并指定了要捕获的异常类型。用法下面是一个简单的例子演示了如何使用try、throw和catch来处理除以零的异常代码块#includeiostreamusingnamespacestd;doubledivision(inta,intb){if(b0){throwDivision by zero!;// 抛出异常}return(a/b);}intmain(){intx50;inty0;doublez0;try{zdivision(x,y);// 可能抛出异常的代码coutzendl;}catch(constchar*msg){// 捕获并处理异常cerrmsgendl;}return0;}在这个例子中如果y为零division函数将抛出一个字符串异常该异常在main函数的catch块中被捕获并处理。一个try块可以跟随多个catch块来捕获多种不同类型的异常代码块#includeiostream#includestdexcept// 包含标准异常类usingnamespacestd;// 为了演示方便项目中不建议这样使用voidtestFunction(){throwruntime_error(Runtime error occurred!);}intmain(){try{testFunction();// 可能抛出异常的函数}catch(constlogic_errore){coutCaught a logic_error: e.what()endl;}catch(construntime_errore){coutCaught a runtime_error: e.what()endl;}catch(...){// 捕获所有其他类型的异常coutCaught an unknown exceptionendl;}return0;}testFunction抛出了一个runtime_error异常该异常在第二个catch块中被捕获并处理。最后一个catch块使用省略号…来捕获所有其他类型的异常。C标准异常类C标准库提供了一系列预定义的异常类这些类都继承自std::exception基类。使用标准异常类可以使代码更加清晰。层次结构如图标准异常类层次结构std::exception所有标准异常的基类。std::bad_alloc内存分配失败时抛出。std::bad_cast动态类型转换失败时抛出。std::bad_typeid使用 typeid 运算符失败时抛出。std::bad_exception在函数声明中使用了异常规格但抛出了未列出的异常时抛出 (C11已弃用)。std::logic_error逻辑错误异常基类包括:std::domain_error数学域错误如sqrt(-1)。std::invalid_argument无效参数错误。std::length_error超出允许长度的错误。std::out_of_range范围错误如访问vector的非法索引。std::runtime_error运行时错误异常基类包括:std::overflow_error上溢错误。std::range_error范围错误与 std::out_of_range 不同用于其他情况。std::underflow_error下溢错误。示例#includeiostream#includestdexceptusingnamespacestd;voidtestLogicError(){throwinvalid_argument(Invalid argument error!);}voidtestRuntimeError(){throwrange_error(range error!);}intmain(){try{testLogicError();// 抛出逻辑错误异常}catch(constlogic_errore){coutCaught a logic_error: e.what()endl;}try{testRuntimeError();// 抛出运行时错误异常}catch(construntime_errore){coutCaught a runtime_error: e.what()endl;}return0;}testLogicError 函数抛出了一个 invalid_argument 异常而 testRuntimeError 函数抛出了一个 range_error 异常。这两个异常分别在对应的 catch 块中被捕获并处理。自定义异常类虽然C标准库提供了丰富的异常类但在某些情况下开发者可能需要定义自己的异常类。我们可以通过继承 std::exception 基类并重载 what 方法来实现。示例#includeiostream#includeexception#includestringusingnamespacestd;classMyException:publicexception{public:MyException(conststringmessage):message_(message){}virtualconstchar*what()constnoexceptoverride{returnmessage_.c_str();}private:string message_;};voidtestCustomException(){throwMyException(Custom exception occurred!);}intmain(){try{testCustomException();// 抛出自定义异常}catch(constMyExceptione){coutCaught a MyException: e.what()endl;}catch(constexceptione){coutCaught an unknown exception: e.what()endl;}return0;}MyException 类继承自 std::exception 并重载了 what 方法。testCustomException 函数抛出了一个 MyException 异常该异常在 main 函数的 catch 块中被捕获并处理。注意这里还添加了一个捕获所有其他 std::exception 子类的 catch 块确保能够捕获所有未知异常。noexcept从C11开始推荐使用noexcept关键字来声明函数不抛出任何异常:voidfunc()noexcept;// 声明函数不抛出任何异常如果func函数在执行过程中抛出了异常程序会直接终止。noexcept关键字还可以用于提高性能因为编译器可以优化不抛出异常的函数调用。如果你看过 gcc 源码你会发现基本上通篇都是noexcept。noexcept会告诉编译器它修饰的函数不会产生异常exception这有利于编译器做更多的优化。C 的异常处理是在运行时检测的而不是在编译时检测为了运行时检测编译器应该会做些额外的操作如果能够通过noexcept明确的告诉编译器这个函数不会抛出异常编译器应该会做一些优化。验证函数是否 noexceptnoexcept还可以当作运算符它可以传入参数来验证某个函数是否是noexcept:voidmay_throw();voidno_throw()noexcept;intmain(){noexcept(may_throw());// falsenoexcept(no_throw());// true}什么时候使用 noexcept移动构造函数移动赋值函数建议使用noexcept修饰因为搭配标准库使用时noexcept作用巨大它可以优先移动而非拷贝推荐阅读 move_if_noexcept。而析构函数默认就是noexcept的不需要显式指定noexcept。在明确确认某个函数不会产生 exception 时可以使用noexcept。推荐看看这几个noexcept相关的文档:https://www.cnblogs.com/sword03/p/10020344.htmlhttps://en.cppreference.com/w/cpp/language/noexcept_spechttps://en.cppreference.com/w/cpp/language/noexcepthttps://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Re-noexcepthttps://stackoverflow.com/questions/10787766/when-should-i-really-use-noexcepthttps://www.heise.de/blog/C-Core-Guidelines-Der-noexcept-Spezifier-und-Operator-4121657.html异常安全性异常安全性是指程序在遇到异常时仍然能够保持正确的状态不会出现资源泄露或者数据不一致等问题。因为C允许程序在执行过程中抛出异常这可能导致程序的控制流发生变化如果资源管理不当就可能出现资源泄露或程序崩溃等问题。异常跨函数传播示例有些场景下通过异常处理错误会更方便特别是跨函数传播的时候。场景描述图像处理流水线假设我们有一个图像处理系统分为三个独立编译的模块可能是不同团队开发的库底层库:ImageDecoder第三方库负责解码图像文件。中间层:ImageProcessor内部库负责图像增强。应用层:MainApp主程序处理用户请求。当底层库解码失败时异常会穿过中间层直达应用层处理。简易代码:// 底层库, 解码classImageDecoder{staticvoiddecode(){throwImageException(decode error);}};// 中间层, 图像处理voidImageProcess(){ImageDecoder::decode();}// 应用层intmain(){try{ImageProcess();}catch(ImageException e){xxx}catch(...){}return0;}Copy-and-Swap 惯用法与异常安全核心思想以拷贝构造 非抛出交换实现赋值操作利用 RAII 自动管理资源确保赋值要么完全成功要么原对象不变强异常安全保证。基本模式classBuffer{int*data_;size_t size_;// 私有辅助非抛出交换friendvoidswap(Buffera,Bufferb)noexcept{usingstd::swap;swap(a.data_,b.data_);swap(a.size_,b.size_);}public:Buffer(size_t n):data_(newint[n]),size_(n){}// 拷贝构造可能抛异常new但这是新对象没问题Buffer(constBufferother):data_(newint[other.size_]),size_(other.size_){std::copy(other.data_,other.data_other.size_,data_);}// 移动构造Buffer(Bufferother)noexcept:data_(other.data_),size_(other.size_){other.data_nullptr;other.size_0;}// 析构函数~Buffer(){delete[]data_;}// 拷贝赋值copy-and-swap 的核心Bufferoperator(constBufferother){// 1. 拷贝构造一个临时对象可能抛异常// 如果抛异常this 原封不动 → 强异常安全Buffertmp(other);// 2. 非抛出交换swap(*this,tmp);// 3. tmp 析构释放旧资源return*this;}// 移动赋值也可以复用 copy-and-swapBufferoperator(Bufferother)noexcept{swap(*this,other);return*this;}};异常安全流程拆解步骤操作若抛异常为什么安全1Buffer tmp(other)拷贝构造抛出 →tmp未构造完毕this不变拷贝在独立对象上进行2swap(*this, tmp)交换指针noexcept不会抛只用noexcept操作交换资源所有权3tmp析构析构不抛tmp现在持有旧资源自动释放关键要点swap必须noexcept— 这是整个惯用法的前提。如果swap可能抛就失去了强异常保证。参数传值 vs 传引用— 可以用传值代替传 const 引用 临时构造编译器可以优化// 简写传值方式copy-and-swap 的另一种写法Bufferoperator(Buffer other)noexcept{swap(*this,other);return*this;}这种写法同时支持拷贝赋值和移动赋值传右值时调用移动构造。自赋值安全— copy-and-swap 天然处理自赋值因为拷贝构造了独立副本交换后再释放旧资源。性能权衡每次赋值都分配新内存对于频繁赋值的场景可能不划算。如果性能敏感可以用copy-on-write或先判断资源是否足够复用。