news 2026/7/20 11:51:41

C++异常处理系列从基础到通天(一篇即毕业系列)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++异常处理系列从基础到通天(一篇即毕业系列)

文章目录

  • 异常处理
    • 基本概念
      • 定义
      • 关键字
    • 用法
      • 代码块
      • 代码块
    • 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来处理除以零的异常:

代码块

#include<iostream>usingnamespacestd;doubledivision(inta,intb){if(b==0){throw"Division by zero!";// 抛出异常}return(a/b);}intmain(){intx=50;inty=0;doublez=0;try{z=division(x,y);// 可能抛出异常的代码cout<<z<<endl;}catch(constchar*msg){// 捕获并处理异常cerr<<msg<<endl;}return0;}

在这个例子中,如果y为零,division函数将抛出一个字符串异常,该异常在main函数的catch块中被捕获并处理。

一个try块可以跟随多个catch块,来捕获多种不同类型的异常:

代码块

#include<iostream>#include<stdexcept>// 包含标准异常类usingnamespacestd;// 为了演示方便,项目中不建议这样使用voidtestFunction(){throwruntime_error("Runtime error occurred!");}intmain(){try{testFunction();// 可能抛出异常的函数}catch(constlogic_error&e){cout<<"Caught a logic_error: "<<e.what()<<endl;}catch(construntime_error&e){cout<<"Caught a runtime_error: "<<e.what()<<endl;}catch(...){// 捕获所有其他类型的异常cout<<"Caught an unknown exception"<<endl;}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:在函数声明中使用了异常规格,但抛出了未列出的异常时抛出 (C++11已弃用)。
  • 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:下溢错误。

示例

#include<iostream>#include<stdexcept>usingnamespacestd;voidtestLogicError(){throwinvalid_argument("Invalid argument error!");}voidtestRuntimeError(){throwrange_error("range error!");}intmain(){try{testLogicError();// 抛出逻辑错误异常}catch(constlogic_error&e){cout<<"Caught a logic_error: "<<e.what()<<endl;}try{testRuntimeError();// 抛出运行时错误异常}catch(construntime_error&e){cout<<"Caught a runtime_error: "<<e.what()<<endl;}return0;}

testLogicError 函数抛出了一个 invalid_argument 异常,而 testRuntimeError 函数抛出了一个 range_error 异常。这两个异常分别在对应的 catch 块中被捕获并处理。

自定义异常类

虽然C++标准库提供了丰富的异常类,但在某些情况下,开发者可能需要定义自己的异常类。我们可以通过继承 std::exception 基类并重载 what 方法来实现。

示例

#include<iostream>#include<exception>#include<string>usingnamespacestd;classMyException:publicexception{public:MyException(conststring&message):message_(message){}virtualconstchar*what()constnoexceptoverride{returnmessage_.c_str();}private:string message_;};voidtestCustomException(){throwMyException("Custom exception occurred!");}intmain(){try{testCustomException();// 抛出自定义异常}catch(constMyException&e){cout<<"Caught a MyException: "<<e.what()<<endl;}catch(constexception&e){cout<<"Caught an unknown exception: "<<e.what()<<endl;}return0;}

MyException 类继承自 std::exception 并重载了 what 方法。testCustomException 函数抛出了一个 MyException 异常,该异常在 main 函数的 catch 块中被捕获并处理。

注意,这里还添加了一个捕获所有其他 std::exception 子类的 catch 块,确保能够捕获所有未知异常。

noexcept

从C++11开始,推荐使用noexcept关键字来声明函数不抛出任何异常:

voidfunc()noexcept;// 声明函数不抛出任何异常

如果func函数在执行过程中抛出了异常,程序会直接终止。noexcept关键字还可以用于提高性能,因为编译器可以优化不抛出异常的函数调用。

如果你看过 gcc 源码,你会发现,基本上通篇都是noexcept

noexcept会告诉编译器,它修饰的函数不会产生异常(exception),这有利于编译器做更多的优化。

C++ 的异常处理是在运行时检测的,而不是在编译时检测,为了运行时检测,编译器应该会做些额外的操作,如果能够通过noexcept明确的告诉编译器这个函数不会抛出异常,编译器应该会做一些优化。

验证函数是否 noexcept?

noexcept还可以当作运算符,它可以传入参数,来验证某个函数是否是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.html
  • https://en.cppreference.com/w/cpp/language/noexcept_spec
  • https://en.cppreference.com/w/cpp/language/noexcept
  • https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Re-noexcept
  • https://stackoverflow.com/questions/10787766/when-should-i-really-use-noexcept
  • https://www.heise.de/blog/C-Core-Guidelines-Der-noexcept-Spezifier-und-Operator-4121657.html

异常安全性

异常安全性是指程序在遇到异常时仍然能够保持正确的状态,不会出现资源泄露或者数据不一致等问题。

因为C++允许程序在执行过程中抛出异常,这可能导致程序的控制流发生变化,如果资源管理不当,就可能出现资源泄露或程序崩溃等问题。

异常跨函数传播示例

有些场景下,通过异常处理错误会更方便,特别是跨函数传播的时候。

场景描述:图像处理流水线,假设我们有一个图像处理系统,分为三个独立编译的模块(可能是不同团队开发的库):

  1. 底层库:ImageDecoder(第三方库,负责解码图像文件)。
  2. 中间层:ImageProcessor(内部库,负责图像增强)。
  3. 应用层: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(Buffer&a,Buffer&b)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(constBuffer&other):data_(newint[other.size_]),size_(other.size_){std::copy(other.data_,other.data_+other.size_,data_);}// 移动构造Buffer(Buffer&&other)noexcept:data_(other.data_),size_(other.size_){other.data_=nullptr;other.size_=0;}// 析构函数~Buffer(){delete[]data_;}// 拷贝赋值:copy-and-swap 的核心Buffer&operator=(constBuffer&other){// 1. 拷贝构造一个临时对象(可能抛异常)// 如果抛异常,this 原封不动 → 强异常安全Buffertmp(other);// 2. 非抛出交换swap(*this,tmp);// 3. tmp 析构释放旧资源return*this;}// 移动赋值(也可以复用 copy-and-swap)Buffer&operator=(Buffer&&other)noexcept{swap(*this,other);return*this;}};

异常安全流程拆解

步骤操作若抛异常为什么安全
1Buffer tmp(other)拷贝构造抛出 →tmp未构造完毕,this不变拷贝在独立对象上进行
2swap(*this, tmp)交换指针noexcept,不会抛只用noexcept操作交换资源所有权
3tmp析构析构不抛tmp现在持有旧资源,自动释放

关键要点

  1. swap必须noexcept— 这是整个惯用法的前提。如果swap可能抛,就失去了强异常保证。

  2. 参数传值 vs 传引用— 可以用传值代替传 const 引用 + 临时构造,编译器可以优化:

    // 简写:传值方式(copy-and-swap 的另一种写法)Buffer&operator=(Buffer other)noexcept{swap(*this,other);return*this;}

    这种写法同时支持拷贝赋值和移动赋值(传右值时调用移动构造)。

  3. 自赋值安全— copy-and-swap 天然处理自赋值,因为拷贝构造了独立副本,交换后再释放旧资源。

  4. 性能权衡:每次赋值都分配新内存,对于频繁赋值的场景可能不划算。如果性能敏感,可以用copy-on-write或先判断资源是否足够复用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/20 11:50:46

主流 AI 证书横向测评:通用型、岗位型、厂商型到底怎么选?

随着大模型产业化持续推进&#xff0c;AI 相关岗位需求持续扩张&#xff0c;市场上人工智能证书数量激增。前程无忧《2026 届校招市场 AI 人才需求报告》数据显示&#xff0c;应届生群体中持有正规 AI 认证的求职者&#xff0c;起薪平均高出无证书人群 18.7%&#xff1b;0-3 年…

作者头像 李华
网站建设 2026/7/20 11:50:46

黑苹果USB端口定制实战:从硬件识别到完美驱动

黑苹果USB端口定制实战&#xff1a;从硬件识别到完美驱动 【免费下载链接】Hackintosh Hackintosh long-term maintenance model EFI and installation tutorial 项目地址: https://gitcode.com/gh_mirrors/ha/Hackintosh 你是否遇到过这样的困扰&#xff1a;在黑苹果系…

作者头像 李华
网站建设 2026/7/20 11:50:10

5个理由告诉你为什么需要GIMP的批量图像处理神器BIMP

5个理由告诉你为什么需要GIMP的批量图像处理神器BIMP 【免费下载链接】gimp-plugin-bimp BIMP. Batch Image Manipulation Plugin for GIMP. 项目地址: https://gitcode.com/gh_mirrors/gi/gimp-plugin-bimp 你是否曾经面对成百上千张需要处理的图片感到束手无策&#x…

作者头像 李华
网站建设 2026/7/20 11:49:17

3个核心技巧:高效提取Android OTA payload文件的完整指南

3个核心技巧&#xff1a;高效提取Android OTA payload文件的完整指南 【免费下载链接】payload-dumper-go an android OTA payload dumper written in Go 项目地址: https://gitcode.com/gh_mirrors/pa/payload-dumper-go 在Android系统开发与维护中&#xff0c;处理OTA…

作者头像 李华
网站建设 2026/7/20 11:47:52

Hermes Agent AI工作流:从文生图到网页设计全解析

1. Hermes Agent技能全解析&#xff1a;从视频演示到网页设计的AI工作流在数字内容创作领域&#xff0c;Hermes Agent正逐渐成为多面手工具的代表。这个集成了多种AI能力的智能平台&#xff0c;能够将简单的文字提示转化为精美的视觉作品和结构化内容。不同于单一功能的AI工具&…

作者头像 李华
网站建设 2026/7/20 11:47:11

Br介导Pd电催化CO₂高效转化DMC的突破研究

1. 研究背景与核心突破电催化CO₂还原反应&#xff08;CO₂RR&#xff09;是当前能源化学领域的前沿课题&#xff0c;这项发表在《德国应用化学》&#xff08;Angewandte Chemie International Edition&#xff09;的工作&#xff0c;首次实现了在工业级电流密度&#xff08;10…

作者头像 李华