C++异常处理详解

概述

这篇博客将深入探讨 C++异常处理的工作原理, 最佳实践以及如何编写异常安全的代码, 配有代码示例和详细说明.


1. 异常的挑战

  1. 性能开销: 异常在失败情况下会带来显著的运行时成本.
    exception performance

    图片来自: Introduction to proposed std::expected - Niall Douglas - Meeting C++ 2017

  2. 推理复杂性: 异常的使用增加了代码分析和调试的难度.

  3. 依赖动态内存: 异常处理往往需要堆内存分配.
    比如在抛出异常时, 分配std::string对象存储错误信息.

  4. 二进制大小增加: 异常机制会增加编译后的二进制文件体积.


2. 异常的工作原理

C++异常机制通过以下几个步骤处理异常:

2.1 关键关键词

  1. throw: 用于抛出异常.

    • 语法: throw expression;
    • 抛出后, 程序会立即停止当前代码的执行, 进入异常处理流程.
  2. try: 定义一个监控异常的代码块.

    • 语法:
      try {
          // 可能抛出异常的代码
      } catch (const std::exception& e) {
          // 异常处理代码
      }
      
  3. catch: 用于捕获和处理异常. 匹配到类型兼容的异常后执行.

    try {
         exampleFunction(1); // Try changing this to 2 or another value
     } catch (const std::runtime_error& re) {
         std::cerr << "Caught a runtime error: " << re.what() << std::endl;
     } catch (const std::logic_error& le) {
         std::cerr << "Caught a logic error: " << le.what() << std::endl;
     } catch (...) {
         std::cerr << "Caught an unknown exception." << std::endl;
     }
    

2.2 堆栈展开(Stack Unwinding)

当异常抛出时:

  1. 逆序销毁对象:
    • 堆栈上的所有本地对象会按照构造的逆序依次调用其析构函数.
  2. 异常传播:
    • 如果当前函数中没有匹配的catch块, 异常会继续向调用链的上一层传播, 直到找到匹配的处理器.
    • 如果最终未捕获异常, 则会调用std::terminate().
示例: 堆栈展开

stack unwinding

输出结果:

Constructing A
Constructing B
Constructing C
Destructing C
Destructing B
Caught exception: Error in C
Destructing A

2.3 未处理的异常

未捕获的异常会导致以下行为:

  1. 调用std::terminate()终止程序.
  2. 堆栈展开不会完成, 导致可能的资源泄漏.
  3. 没有调用任何尚未执行的析构函数.

3. 异常处理的最佳实践

3.1 何时使用异常(何时不使用)

  1. 针对预计很少发生的错误
  2. 针对无法在本地处理的"异常情况"(I/O 错误)
    • 未找到文件
    • 无法在映射中找到键针对运算符和构造函数(即很少有其他机制可以工作的情况)
何时不用异常
  1. 对于预期会频繁发生的错误. 因为异常的代价很大, 频繁使用会使程序运行的很慢.

  2. 对于预期会失败的函数. 比如下面这个将string转为int的函数, 很明显并非所有的字符串都可用转为int, 此时用诸如std::optionalstd::expected更合适(不熟悉的读者可以参考我的博客: 解读 C++23 std::expected 函数式写法, 现代 C++ 必备知识: 解锁 std::optional, std::variantstd::any).

    std::optional<int> str2num(std::string const&s)
    std::expected<int> str2num(std::string const&s)
    
  3. 对响应时间有严格要求

  4. 对于不应该发生的事情, 不要用异常来做兜底. 比如一些编程错误:

    • 解引用空指针
    • 超出范围的访问
    • 释放后使用

3.2 如何使用异常

3.2.1 建立在 std::exception 层次结构上

exception hierarchy

参考: Back to Basics: Exceptions - Klaus Iglberger - CppCon 2020

3.2.2 通过右值抛出

右值抛出是现代 C++ 推荐的异常处理方式, 因为它在语义上更加清晰, 并且在性能上具有优势.

  • 推荐右值抛出原因:

    • 抛出右值会通过值传递, 避免异常对象在调用链中意外被修改.
    • 避免了捕获指针时的内存管理问题, 例如需要手动释放内存.
  • 性能上的优势:

    • 右值对象在现代编译器中可以通过优化减少拷贝.
    • 使用右值抛出的异常对象生命周期由系统自动管理, 无需开发者额外处理.
  • 示例:

#include <stdexcept>
void f(int a, int b) {
  if (b == 0) {
    throw std::runtime_error("Division by zero"); // 推荐的右值抛出
  }
}
  • 对比:

以下是不推荐的指针抛出方式, 因为它引入了额外的内存管理复杂性:

void g(int a, int b) {
  if (b == 0) {
    std::runtime_error* error = new std::runtime_error("Error via pointer");
    throw error; // 不推荐
  }
}

void h() {
  try {
    g(10, 0);
  } catch (std::runtime_error* e) {
    std::cerr << "Caught exception: " << e->what() << std::endl;
    delete e; // 确保释放内存
  }
}

通过右值抛出可以避免如上示例中手动释放内存的风险, 同时提高代码的可维护性.

3.2.3 通过引用捕获

不要按值捕获, 因为这样会:

  • 创建不必要的副本
  • 可能会切分异常
#include <exception>
#include <iostream>

void cf() {
  try {
    throw std::runtime_error("An error occurred");
  } catch (std::exception ex) {  // 😱 按值捕获会创建副本,
                                 // 并且可能会出现类切分(class slicing)
    std::cerr << "Caught exception by value: " << ex.what() << std::endl;
  }
}

void cg() {
  try {
    throw std::runtime_error("An error occurred");
  } catch (const std::exception& ex) {  // 推荐的引用捕获方式
    std::cerr << "Caught exception by reference: " << ex.what() << std::endl;
  }
}

int main() {
  cf();
  cg();
  return 0;
}

输出:

Caught exception by value: std::exception
Caught exception by reference: An error occurred

通过引用捕获(const 引用)不仅能避免创建不必要的副本, 还能确保异常对象的完整性, 防止切分异常.

3.3 异常安全保证

  • 基本异常安全保证
    • 保存不变量
    • 不泄漏任何资源
  • 强异常安全保证
    • 不变量被保留
    • 不泄漏任何资源
    • 无状态改变(提交或回滚)
    • 并非总是可能(例如套接字, 流等)
  • 无抛出保证
    • 操作不会失败
    • 在代码中用noexcept表示

3.4 如何编写异常安全代码

在编写异常安全代码时, 确保某些关键函数绝不能失败至关重要. 这些函数包括析构函数, 移动操作和交换操作. 以下是具体的说明和示例:

不允许失败的函数
  1. 析构函数:

    • 析构函数在堆栈展开期间调用. 如果在此过程中抛出异常, 程序将调用std::terminate().
    • 自 C++11 起, 析构函数隐式标记为noexcept.

    示例:

    class Example {
    public:
      // 确保析构函数不抛出异常
      ~Example() noexcept {
        // 清理资源
      }
    };
    
  2. 移动操作(std::move):

    • 移动构造函数和移动赋值操作应通过noexcept明确声明为不会抛出异常.

    示例:

    class Movable {
    public:
      Movable(Movable&&) noexcept = default;             // 移动构造
      Movable& operator=(Movable&&) noexcept = default;  // 移动赋值
    };
    
  3. 交换操作(std::swap):

    • 自定义的交换函数也应保证不会抛出异常.

    示例:

    void swap(Movable& a, Movable& b) noexcept {
      using std::swap;
      swap(a.data, b.data);
    }
    
noexcept 的好处
  • noexcept 使代码中可见的永不抛出的承诺
  • noexcept 可以使代码(稍微)更快
  • 如果异常导致标有 noexcept 的函数, 则会调用 terminate()
  • 编译器不会检查此承诺
  • noexcept 承诺无法收回
  • 只有少数函数应标有 noexcept
  • 析构函数隐式标有 noexcept
C++ Core Guideline 的指导意见
  1. RAII 是 C++ 编程语言中最重要的习惯用法. 使用它!
  2. 所有函数都应至少提供基本的异常安全保证, 如果可能且合理, 则提供强保证.
  3. 考虑不抛出保证, 但只有当您能够保证它甚至可能针对未来可能发生的变化时才提供它

参考资料

  • Back to Basics: Exceptions - Klaus Iglberger - CppCon 2020

源码链接

源码链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/955597.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

零基础构建最简单的 Tauri2.0 桌面项目 Star 88.4k!!!

目录 预安装环境 安装nodejs windows下安装 linux下安装 nodejs常遇问题 安装C环境 介绍 下载 安装 安装Rust语言 Tauri官网 安装 vscode 安装 rust 插件 安装 Tauri 插件 运行成果 预安装环境 安装nodejs windows下安装 NodeJs_安装及下载_哔哩哔哩_bilibi…

Python基于Django的图像去雾算法研究和系统实现(附源码,文档说明)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

【AI插件】在VS Code中免费使用GitHub Copilot

什么是GitHub Copilot GitHub Copilot 是由 GitHub 和 OpenAI 合作开发的一款 AI 编程助手&#xff0c;旨在帮助开发者提高编程效率。它通过分析大量的开源代码&#xff0c;生成上下文相关的代码建议和自动补全&#xff0c;支持多种编程语言和框架。GitHub Copilot 是基于 Ope…

map和set c++

关联式容器也是⽤来存储数据的&#xff0c;与序列式容器不同的是&#xff0c;关联式容器逻辑结构通常是⾮线性结构&#xff0c;两个位置有紧密的关联关系&#xff0c;交换⼀下&#xff0c;他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/…

2025年01月蓝桥杯Scratch1月stema选拔赛真题—美丽的图形

美丽的图形 编程实现美丽的图形具体要求: 1)点击绿旗&#xff0c;角色在舞台中心&#xff0c;如图所示&#xff1b; 2)1秒后&#xff0c;绘制一个边长为 140的红色大正方形&#xff0c;线条粗细为 3&#xff0c;正方形的中心为舞台中心&#xff0c;如图所示; 完整题目可点击下…

hive连接mysql报错:Unknown version specified for initialization: 3.1.0

分享下一些报错的可能原因吧 1.要开启hadoop 命令&#xff1a;start-all.sh 2.检查 hive-site.xml 和 hive-env.sh。 hive-site.xml中应设置自己mysql的用户名和密码 我的hive-site.xml如下&#xff1a; <configuration><property><name>javax.jdo.opt…

AI编程工具使用技巧——通义灵码

活动介绍通义灵码1. 理解通义灵码的基本概念示例代码生成 2. 使用明确的描述示例代码生成 3. 巧妙使用注释示例代码生成 4. 注意迭代与反馈原始代码反馈后生成优化代码 5. 结合生成的代码进行调试示例测试代码 其他功能定期优化生成的代码合作与分享结合其他工具 总结 活动介绍…

Python编程与在线医疗平台数据挖掘与数据应用交互性研究

一、引言 1.1 研究背景与意义 在互联网技术飞速发展的当下,在线医疗平台如雨后春笋般涌现,为人们的就医方式带来了重大变革。这些平台打破了传统医疗服务在时间和空间上的限制,使患者能够更加便捷地获取医疗资源。据相关报告显示,中国基于互联网的医疗保健行业已进入新的…

大文件上传服务-后端V1V2

文章目录 大文件上传概述:minio分布式文件存储使用的一些技术校验MD5的逻辑 uploadV1 版本 1uploadv2 版本 2 大文件上传概述: 之前项目做了一个文件上传的功能,最近看到有面试会具体的问这个上传功能的细节&#xff0c;把之前做的项目拿过来总结一下&#xff0c;自己写的一个…

[BrainShadow-V1] VR头戴设备统计报告

Brain-Shadow-V1 EventVR headsetsReported byXiao enDate2025/01/15Version1.0 HTC Vive Pro 2 Pro HTC Vive Pro 2 是一款高端虚拟现实头显&#xff0c;配备双 2.5K 显示屏&#xff0c;组合分辨率达到 48962448&#xff0c;提供 120 的视场角和 120Hz 的刷新率。该设备支持…

路由环路的产生原因与解决方法(1)

路由环路 路由环路就是数据包不断在这个网络传输&#xff0c;始终到达不了目的地&#xff0c;导致掉线或者网络瘫痪。 TTL &#xff08;生存时间&#xff09;&#xff1a;数据包每经过一个路由器的转发&#xff0c;其数值减1&#xff0c;当一个数据包的TTL值为0是&#xff0c;路…

工业网口相机:如何通过调整网口参数设置,优化图像传输和网络性能,达到最大帧率

项目场景 工业相机是常用与工业视觉领域的常用专业视觉核心部件&#xff0c;拥有多种属性&#xff0c;是机器视觉系统中的核心部件&#xff0c;具有不可替代的重要功能。 工业相机已经被广泛应用于工业生产线在线检测、智能交通,机器视觉,科研,军事科学,航天航空等众多领域 …

企业邮箱iRedMail搭建

用自己的域名作为邮箱的后缀&#xff0c;好看、有面子&#xff01;只要域名不过期&#xff0c;那么&#xff0c;你的邮箱就永远存在&#xff01; 邮件系统很多&#xff0c;宝塔自带的邮局更是简单&#xff0c;但是若想邮箱可靠&#xff08;丢邮件、发送邮件进入对方垃圾箱等&a…

Redis的安装和配置、基本命令

一、实验目的 本实验旨在帮助学生熟悉Redis的安装、配置和基本使用&#xff0c;包括启动Redis服务、使用命令行客户端进行操作、配置Redis、进行多数据库操作以及掌握键值相关和服务器相关的命令。 二、实验环境准备 1. JAVA环境准备&#xff1a;确保Java Development Kit …

mysql community server社区版M2 macbook快速安装

Django玩的时候用到了mysql&#xff0c;简单整理一下这个老伙计的安装教程 1. 下载地址&#xff1a;MySQL :: Download MySQL Community Server 2. M2芯片mac的话选择第一个下载&#xff0c;按提示安装即可 3. 或者直接用这篇文章附属安装包 4. 但安装之后可能会有zsh: command…

【桌面程序】PyWebview跨平台桌面应用程序

什么是PyWebview PyWebView 是一个轻量级的 Python 库&#xff0c;用于将网页&#xff08;HTML、CSS、JavaScript&#xff09;嵌入到本地应用程序的窗口中。它允许你创建带有图形用户界面&#xff08;GUI&#xff09;的桌面应用程序&#xff0c;并且能够使用 Web 技术&#xf…

迅为RK3568开发板篇OpenHarmony实操HDF驱动控制LED-编写内核 LED HDF 驱动程序

接下来编译 LED 驱动&#xff0c;该驱动用于在基于华为设备框架&#xff08;HDF&#xff09;的系统中控制 LED 灯的开关&#xff0c;完整代码如下所示&#xff1a; 更多内容可以关注&#xff1a;迅为RK3568开发板篇OpenHarmony

【tailscale 和 ssh】当服务器建立好节点,但通过客户端无法通过 ssh 连接

背景 当服务器建立好节点&#xff0c;一切显示正常但通过客户端无法通过 vs code 中的 ssh 连接到服务器 问题解决 因为服务器是重装过的&#xff0c;所以忘记在服务器上下载 ssh 了。。。安装完成并启动 SSH 服务后便可正常连接&#xff01; sudo apt update sudo apt in…

广播网络实验

1 实验内容 1、构建星性拓扑下的广播网络,实现hub各端口的数据广播,验证网络的连通性并测试网络效率 2、构建环形拓扑网络,验证该拓扑下结点广播会产生数据包环路 2 实验流程与结果分析 2.1 实验环境 ubuntu、mininet、xterm、wireshark、iperf 2.2 实验方案与结果分析…

Ubuntu20.04取消root账号自动登录的方法,触觉智能RK3568开发板演示

Ubuntu20.04默认情况下为root账号自动登录&#xff0c;本文介绍如何取消root账号自动登录&#xff0c;改为通过输入账号密码登录&#xff0c;使用触觉智能EVB3568鸿蒙开发板演示&#xff0c;搭载瑞芯微RK3568&#xff0c;四核A55处理器&#xff0c;主频2.0Ghz&#xff0c;1T算力…