【C++】:异常

server/2025/3/28 15:49:27/

目录

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/server/177124.html

相关文章

taosdump备份所有的数据库近10天的数据(deepseek)

以下是使用 taosdump 备份 TDengine 所有数据库中近10天数据的步骤&#xff1a; 1. 获取所有数据库列表 首先登录 TDengine&#xff0c;执行以下命令列出所有非系统数据库&#xff1a; echo "SHOW DATABASES;" | taos | awk NR>2 && $1 !~ /^informatio…

xLua_001 Lua 文件加载

xLua下载 1、HelloWrold 代码 using System.Collections; using System.Collections.Generic; using UnityEngine; using XLua; // 引入XLua命名空间 public class Helloworld01 : MonoBehaviour {//声明LuaEnv对象 private LuaEnv luaenv;void Start(){//实例化LuaEnv对象…

二项式分布(Binomial Distribution)

二项式分布&#xff08;Binomial Distribution&#xff09; 定义 让我们来看看玩板球这个例子。假设你今天赢了一场比赛&#xff0c;这表示一个成功的事件。你再比了一场&#xff0c;但你输了。如果你今天赢了一场比赛&#xff0c;但这并不表示你明天肯定会赢。我们来分配一个…

怎么查看linux是Ubuntu还是centos

要确定你的Linux系统是基于Ubuntu还是CentOS&#xff0c;可以通过几种不同的方法来进行判断。下面是一些常用的方法&#xff1a; 要快速判断 Linux 系统是 Ubuntu 还是 CentOS&#xff0c;可通过以下方法综合验证&#xff1a; 一、查看系统信息文件 1. /etc/os-release 文件…

Linux面试题

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…

libaom 源码分析:scalable_decoder.c 文件

libaom 基本特性 开放和免版税&#xff1a;libaom 提供了一个开放源代码的编码器&#xff0c;任何个人和组织都可以免费使用&#xff0c;无需支付版税&#xff0c;这促进了它在各种应用中的广泛采用。高效的编码&#xff1a;旨在提供高效的视频压缩&#xff0c;以适应不同的网络…

doris:审计日志

Doris 提供了对于数据库操作的审计能力&#xff0c;可以记录用户对数据库的登陆、查询、修改操作。在 Doris 中&#xff0c;可以直接通过内置系统表查询审计日志&#xff0c;也可以直接查看 Doris 的审计日志文件。 开启审计日志​ 通过全局变量 enable_audit_plugin 可以随时…

GCC 预定义宏:解锁编译器的隐藏信息

GCC 预定义宏&#xff1a;解锁编译器的隐藏信息 在 GCC 编译器中&#xff0c;有许多内置的预定义宏&#xff0c;它们可以提供编译环境的信息&#xff0c;如文件名、行号、时间、版本等。这些宏在调试、日志记录、条件编译等场景中非常有用。本文将介绍常见的 GCC 预定义宏&…