【C++】:异常

news/2025/3/23 14:07:16/

目录

C语言处理错误的方式

C++异常的概念

C++异常的使用

异常的抛出与捕获匹配原则

函数调用链中的栈展开 

异常重新抛出 

异常安全

异常规范

标准库异常体系

自定义异常体系

异常的优缺点


C语言处理错误的方式

  1. 返回值检查:函数返回特定错误码或值标识失败,但需逐层检查且易被忽略。

  2. 全局变量 errno:依赖全局变量记录错误类型,存在线程安全隐患和覆盖风险。

  3. 断言(Assert):通过断言验证逻辑假设,但仅适用于调试且失败直接终止程序。

  4. 非局部跳转(setjmp/longjmp):支持跨函数错误跳转,但易导致资源泄漏和代码混乱。

  5. 信号处理:捕获系统信号处理严重错误,但处理函数功能受限且不可靠。

  6. Goto清理:集中释放资源避免冗余,但滥用会破坏代码结构化逻辑。

C++异常的概念

C++ 异常处理是一种用于管理程序运行时错误的机制,它通过分离错误处理代码和正常逻辑来提高代码的可维护性。 

  • try:包裹可能抛出异常的代码块

  • throw:抛出异常对象(任意类型)

  • catch:捕获并处理特定类型的异常

try {// 可能抛出异常的代码if (error) throw MyException("Error occurred");
} 
catch (const MyException& e) {// 处理 MyException 类型异常std::cerr << e.what() << std::endl;
}
catch (...) {  // 捕获所有异常std::cerr << "Unknown error" << std::endl;
}

C++异常的使用

异常的抛出与捕获匹配原则

1、类型精确匹配

  • 异常捕获基于 类型匹配catch 块按顺序尝试匹配异常类型

  • 被选中的处理代码(catch块)是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  • 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码,如果抛出的异常对象没有捕获,或是没有匹配类型的捕获,那么程序会终止报错。
try {throw 42;  // 抛出 int 类型异常
}
catch (double d) { /* 不会捕获 */ }
catch (int i) {   // 匹配成功std::cout << "Caught int: " << i;
}

2、继承体系中的匹配

  • 基类 catch 块可以捕获派生类异常(需通过 引用或指针 捕获避免对象切片)

  • 捕获和抛出的异常类型并不一定要完全匹配,可以抛出派生类对象,使用基类进行捕获。
  • 推荐实践:优先捕获派生类异常,再捕获基类

try {throw std::runtime_error("Error");
}
catch (const std::runtime_error& e) {  // 优先匹配具体类型std::cerr << "Runtime error: " << e.what();
}
catch (const std::exception& e) {      // 基类捕获兜底std::cerr << "Standard exception: " << e.what();
}

3、特殊匹配规则

  • catch (...) 捕获所有异常(通常用于资源清理),但捕获后无法知道异常错误是什么。

  • const 修饰不影响匹配:catch (std::exception) 与 catch (const std::exception) 视为相同

函数调用链中的栈展开 

  • 当异常被抛出后,首先检查 throw 本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
  • 如果当前函数栈没有匹配的 catch 则退出当前函数栈,继续在上一个调用函数栈中进行查找匹配的catch。找到匹配的catch子句并处理以后,会沿着 catch 子句后面继续执行,而不会跳回到原来抛异常的地方。
  • 如果到达main函数的栈,依旧没有找到匹配的catch,则终止程序。
void func3() 
{std::vector<int> localObj(100);  // RAII 对象throw std::runtime_error("Boom"); // 抛出异常// localObj 自动析构
}void func2() { func3(); }  // 异常继续传播
void func1() { func2(); }  // 异常继续传播int main() 
{try {func1();}catch (const std::exception& e) {std::cerr << "Caught: " << e.what();}return 0;
}
  1. 函数调用链

    • main() 调用 func1()

    • func1() 调用 func2()

    • func2() 调用 func3()

  2. 异常抛出(func3)

    • 在 func3() 中,首先构造局部对象 std::vector<int> localObj(100)(RAII管理内存)。

    • 执行 throw std::runtime_error("Boom"),抛出异常,函数执行中断。

  3. 栈展开(Stack Unwinding)

    • func3 栈帧销毁localObj 的析构函数自动调用,释放分配的100个int内存(RAII确保资源释放)。

    • func2 栈帧销毁:因无局部对象,直接退出。

    • func1 栈帧销毁:同理,无资源需清理。

  4. 异常捕获(main)

    • 异常传播至 main() 的 try 块。

    • catch (const std::exception& e) 捕获异常(std::runtime_error 是 std::exception 的派生类)。

    • 输出错误信息:Caught: Boom

异常重新抛出 

在 catch 块中使用 throw; 重新抛出当前异常

典型场景

  • 记录日志后继续传播异常

  • 部分处理异常后交由上层处理

异常安全

  1. 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  2. 析构函数主要完成对象资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄露(内存泄露、句柄未关闭等)。
  3. C++中异常经常会导致资源泄露的问题,比如在new和delete中抛出异常,导致内存泄露,在lock和unlock之间抛出异常导致死锁,C++经常使用RAII的方式来解决以上问题。

异常规范

1、优先使用 noexcept

  • 适用场景

    • 移动操作、析构函数、内存释放函数(如 operator delete)。

    • 明确无失败可能的函数(如数学计算)。

double sqrt(double x) noexcept 
{ // 假设输入已校验,不会抛异常return std::sqrt(x); 
}

2. 避免使用动态异常声明

void oldFunc() throw(std::runtime_error); // C++17 已移除,禁止使用
// 替代方案:通过文档说明可能抛出的异常类型。

3、注意事项 

  1. 在函数的后面接throw(type1, type2, ...),列出这个函数可能抛掷的所有异常类型。
  2. 在函数的后面接throw()noexcept(C++11),表示该函数不抛异常。
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。(异常接口声明不是强制的)
// 表示func函数可能会抛出A/B/C/D类型的异常
void func() throw(A, B, C, D);// 表示这个函数只会抛出bad_alloc的异常
void* operator new(std::size_t size) throw(std::bad_alloc);// 表示这个函数不会抛出异常
void* operator new(std::size_t size, void* ptr) throw();

标准库异常体系

C++ 标准库提供了一套层次化的异常类体系,所有标准异常均继承自 std::exception 基类。这些异常类型覆盖了常见的程序错误场景,开发者可以直接使用或继承它们实现自定义异常。

std::exception
├── std::bad_alloc                // 内存分配失败(new 失败)
├── std::bad_cast                 // dynamic_cast 转换失败(非多态类型)
├── std::bad_typeid               // typeid 操作符作用于空指针
├── std::ios_base::failure        // I/O 流错误(如文件打开失败)
|
├── std::logic_error              // 程序逻辑错误(可预防的)
│   ├── std::invalid_argument     // 无效参数(如参数不符合预期范围)
│   ├── std::domain_error         // 数学运算定义域错误(如对负数取对数)
│   ├── std::length_error         // 超出允许长度(如 vector::reserve 超过 max_size)
│   └── std::out_of_range         // 访问越界(如 vector::at 越界索引)
|
└── std::runtime_error            // 运行时错误(不可预见的)├── std::range_error          // 计算结果超出有效范围(如浮点数转换溢出)├── std::overflow_error       // 算术上溢错误├── std::underflow_error      // 算术下溢错误└── std::system_error         // 系统调用错误(含错误码,C++11 引入)
  • exception类的 what成员函数 和 析构函数都定义成了虚函数,方便子类对其进行重写,从而达到多态的效果。
  • 我们也可以去继承exception类来实现自己的异常类,但实际中很多公司都会自己定义一套异常继承体系。

 自定义异常:通过继承 std::runtime_error 或 std::logic_error 添加额外信息。

#include <stdexcept>
#include <string>class NetworkException : public std::runtime_error 
{int error_code_;
public:NetworkException(int code, const std::string& message): std::runtime_error(message), error_code_(code) {}int getErrorCode() const noexcept { return error_code_; }
};// 使用
throw NetworkException(404, "Service not found");

代码示例

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>void readConfigFile(const std::string& filename) 
{std::ifstream file(filename);if (!file) {throw std::runtime_error("无法打开文件: " + filename);}std::string line;while (std::getline(file, line)) {if (line.empty()) {throw std::invalid_argument("配置文件存在空行");}// 解析配置...}
}int main() 
{try {readConfigFile("settings.conf");}catch (const std::invalid_argument& e) {std::cerr << "参数错误: " << e.what() << std::endl;}catch (const std::runtime_error& e) {std::cerr << "运行时错误: " << e.what() << std::endl;}catch (const std::exception& e) {std::cerr << "标准异常: " << e.what() << std::endl;}return 0;
}

自定义异常体系

实际中很多公司都会自定义自己的异常体系进行规范的异常管理。

  • 公司中的项目一般会进行模块划分,让不同的程序员或小组完成不同的模块,如果不对抛异常这件事进行规范,那么负责最外层捕获异常的程序员就非常难受了,因为他需要捕获大家抛出的各种类型的异常对象。
  • 因此实际中都会定义一套继承的规范体系,先定义一个最基础的异常类,所有人抛出的异常对象都必须是继承于该异常类的派生类对象,因为异常语法规定可以用基类捕获抛出的派生类对象,因此最外层就只需捕获基类就行了。

一、为何需要自定义异常?

  1. 错误分类:为特定领域(如文件I/O、网络、数据库)定义明确的错误类型。

  2. 携带额外信息:在异常对象中封装错误码、文件名、操作步骤等上下文信息。

  3. 统一接口:继承自 std::exception,兼容标准异常处理逻辑。

二、设计原则

  1. 继承标准异常:所有自定义异常应直接或间接继承 std::exception

  2. 层次化结构:按错误类型分层(如 NetworkException 派生出 TimeoutException)。

  3. 支持多态:通过虚函数(如 what())提供统一的错误信息接口。

  4. 异常安全:确保自定义异常类的构造函数和成员函数不抛出异常。

三、实现步骤

1. 基类设计(兼容标准异常) 

#include <exception>
#include <string>class Exception
{
public:// 构造函数(允许传入错误描述)Exception(int errid, const char* errmsg):_errid(errid), _errmsg(errmsg){}int GetErrid() const{return _errid;}// 重写 what(),返回错误信息virtual string what() const{return _errmsg;}
protected:int _errid;     //错误编号string _errmsg; //错误描述//...
};

2. 派生具体异常类

// 文件操作异常
class FileIOException : public Exception 
{
public:explicit FileIOException(const std::string& filename, const std::string& action): Exception("File Error: Failed to " + action + " file '" + filename + "'") {}
};// 网络超时异常
class NetworkTimeoutException : public Exception 
{
public:NetworkTimeoutException(const std::string& url, int timeout_sec): Exception("Network Timeout: Request to '" + url + "' timed out after " + std::to_string(timeout_sec) + " seconds") {}
};

 3使用自定义异常

void readFile(const std::string& filename) 
{std::ifstream file(filename);if (!file.is_open()) {// 抛出异常throw FileIOException(filename, "open");}// 文件操作...
}try 
{readFile("config.yaml");
} 
catch (const FileIOException& e) 
{std::cerr << "文件操作失败: " << e.what() << std::endl;// 尝试恢复或重试
}
catch (const MyBaseException& e) 
{std::cerr << "通用错误: " << e.what() << std::endl;
}

异常的优缺点


http://www.ppmy.cn/news/1581105.html

相关文章

用逻辑分析仪分析Usart波形

USART的波形抓取最简单&#xff0c;帧头帧尾只需要电平上升下降沿就可以了&#xff0c;不需要自己定义&#xff0c;也没有ID位&#xff0c;逻辑分析仪可以直接抓取发送的数据&#xff1a; 口配置&#xff1a;9600bps&#xff0c;8数据位&#xff0c;无校验&#xff0c;1个停止位…

6.5840 Lab 3: Raft

论文很重要 raft-zh_cn/raft-zh_cn.md at master maemual/raft-zh_cn GitHub Part 3A: leader election (moderate) 十次test都过了 实现 Raft 的领导者选举和心跳机制&#xff08;AppendEntries RPC&#xff0c;无日志条目&#xff09;。第 3A 部分的目标是实现以下功能&am…

使用 Apktool 反编译、修改和重新打包 APK

使用 Apktool 反编译、修改和重新打包 APK 在 Android 逆向工程和应用修改过程中&#xff0c;apktool 是一个强大的工具&#xff0c;它允许我们解包 APK 文件、修改资源文件或代码&#xff0c;并重新打包成可安装的 APK 文件。本文将介绍如何使用 apktool 进行 APK 反编译、修…

OpenCV vs MediaPipe:哪种方案更适合实时手势识别?

引言 手势识别是计算机视觉的重要应用&#xff0c;在人机交互&#xff08;HCI&#xff09;、增强现实&#xff08;AR&#xff09;、虚拟现实&#xff08;VR&#xff09;、智能家居控制、游戏等领域有广泛的应用。实现实时手势识别的技术方案主要有基于传统计算机视觉的方法&am…

微信 MMTLS 协议详解(五):加密实现

常用的解密算法&#xff0c;对称非对称 加密&#xff0c;密钥协商&#xff0c; 带消息认证的加解密 #生成RSA 密钥对 void GenerateRsaKeypair(std::string& public_key,std::string& private_key) {RSA* rsa RSA_new();BIGNUM* bn BN_new();// 生成 RSA 密钥对BN_s…

期刊分区表2025年名单下载(经济学、管理学)

2025年期刊分区表包括SCIE、SSCI、A&HCI、ESCI和OAJ&#xff0c;共设置了包括自然科学、社会科学和人文科学在内的21个大类 本次分享的是期刊分区表2025年名单经济学类、管理学类&#xff0c;一共7631025条 一、数据介绍 数据名称&#xff1a;期刊分区表2025年名单 数据…

一些硬件知识【2025/3/1】

隔离电源的内部构造&#xff1a; 里面的电源驱动芯片是VPS8702&#xff0c;价格大概在1块钱左右。 可以看到其特点也正符合B0505S这种小型的隔离电源模块。其内部是一个全桥的拓扑&#xff0c;可以驱动外置变压器从而达到将外部输入电源隔离输出的目的。并且他集成了过流检测保…

C#里使用libxl来合并单元格的例子

操作EXCEL的文件格式是常用的功能&#xff0c; 通过不同的单元格的合并&#xff0c;可以生成不同的表格。 如下图所示&#xff1a; 采用libxl来创建上面的EXCEL&#xff0c;使用下面的代码来实现&#xff1a; private void button8_Click(object sender, EventArgs e) {var …