目录
C语言处理错误的方式
C++异常的概念
C++异常的使用
异常的抛出与捕获匹配原则
函数调用链中的栈展开
异常重新抛出
异常安全
异常规范
标准库异常体系
自定义异常体系
异常的优缺点
C语言处理错误的方式
-
返回值检查:函数返回特定错误码或值标识失败,但需逐层检查且易被忽略。
-
全局变量
errno
:依赖全局变量记录错误类型,存在线程安全隐患和覆盖风险。 -
断言(Assert):通过断言验证逻辑假设,但仅适用于调试且失败直接终止程序。
-
非局部跳转(setjmp/longjmp):支持跨函数错误跳转,但易导致资源泄漏和代码混乱。
-
信号处理:捕获系统信号处理严重错误,但处理函数功能受限且不可靠。
-
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;
}
-
函数调用链:
-
main()
调用func1()
-
func1()
调用func2()
-
func2()
调用func3()
-
-
异常抛出(func3):
-
在
func3()
中,首先构造局部对象std::vector<int> localObj(100)
(RAII管理内存)。 -
执行
throw std::runtime_error("Boom")
,抛出异常,函数执行中断。
-
-
栈展开(Stack Unwinding):
-
func3 栈帧销毁:
localObj
的析构函数自动调用,释放分配的100个int内存(RAII确保资源释放)。 -
func2 栈帧销毁:因无局部对象,直接退出。
-
func1 栈帧销毁:同理,无资源需清理。
-
-
异常捕获(main):
-
异常传播至
main()
的try
块。 -
catch (const std::exception& e)
捕获异常(std::runtime_error
是std::exception
的派生类)。 -
输出错误信息:
Caught: Boom
。
-
异常重新抛出
在 catch
块中使用 throw;
重新抛出当前异常
典型场景:
-
记录日志后继续传播异常
-
部分处理异常后交由上层处理
异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数主要完成对象资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄露(内存泄露、句柄未关闭等)。
- 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、注意事项
- 在函数的后面接
throw(type1, type2, ...)
,列出这个函数可能抛掷的所有异常类型。 - 在函数的后面接
throw()
或noexcept
(C++11),表示该函数不抛异常。 - 若无异常接口声明,则此函数可以抛掷任何类型的异常。(异常接口声明不是强制的)
// 表示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;
}
自定义异常体系
实际中很多公司都会自定义自己的异常体系进行规范的异常管理。
- 公司中的项目一般会进行模块划分,让不同的程序员或小组完成不同的模块,如果不对抛异常这件事进行规范,那么负责最外层捕获异常的程序员就非常难受了,因为他需要捕获大家抛出的各种类型的异常对象。
- 因此实际中都会定义一套继承的规范体系,先定义一个最基础的异常类,所有人抛出的异常对象都必须是继承于该异常类的派生类对象,因为异常语法规定可以用基类捕获抛出的派生类对象,因此最外层就只需捕获基类就行了。
一、为何需要自定义异常?
-
错误分类:为特定领域(如文件I/O、网络、数据库)定义明确的错误类型。
-
携带额外信息:在异常对象中封装错误码、文件名、操作步骤等上下文信息。
-
统一接口:继承自
std::exception
,兼容标准异常处理逻辑。
二、设计原则
-
继承标准异常:所有自定义异常应直接或间接继承
std::exception
。 -
层次化结构:按错误类型分层(如
NetworkException
派生出TimeoutException
)。 -
支持多态:通过虚函数(如
what()
)提供统一的错误信息接口。 -
异常安全:确保自定义异常类的构造函数和成员函数不抛出异常。
三、实现步骤
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;
}