异常处理概述
异常处理是 C++ 中处理程序运行时错误的核心机制,其本质在于将错误检测与错误处理分离。当程序在运行过程中遇到如除以零、内存分配失败等问题时,可以在出错点抛出异常,然后在合适的层级(如主函数或业务逻辑层)捕获并处理,从而避免程序直接崩溃,提升代码的健壮性。
为什么需要异常处理
在引入异常处理之前,程序通常依赖返回值来判断是否出错。这种方式存在明显缺陷:
- 返回值冲突:例如除法函数返回
-1可能表示错误,但也可能是合法的计算结果。 - 调用链冗余:多层函数调用时,必须逐层传递错误状态,代码冗余且容易遗漏检查。
- 错误传播困难:深层的错误难以传递到顶层进行统一处理。
相比之下,异常处理的优势在于错误检测与处理解耦,异常可以跨函数、跨层级自动传播,并能携带丰富的错误信息(如类型、原因、位置),有效防止小错误导致系统崩溃。
核心组件与流程
C++ 异常处理依赖三个关键字:
- throw:检测到错误时抛出异常对象。
- try:包裹可能抛出异常的代码块,定义异常检测范围。
- catch:捕获并处理匹配的异常类型。
基本流程是:try 块执行代码 → 若出错则 throw 抛出异常 → 程序跳转至最近的匹配 catch 块 → 执行处理后继续向下运行。
基本语法与执行流程
语法结构
try {
// 可能抛出异常的代码
if (错误条件) {
throw 异常值; // 可抛出任意类型:int, string, 自定义类等
}
} catch (异常类型 变量名) {
// 处理特定类型异常
} catch (...) {
// 兜底捕获所有未匹配异常
}
注意 try 块不能单独存在,必须紧跟至少一个 catch 块。catch(...) 作为万能捕获,必须放在最后。
执行流程示例
下面是一个除数为零的异常处理示例,展示了异常如何中断当前执行流并跳转:
#include <iostream>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw string("错误:除数不能为 0!");
}
return a / b;
}
int main() {
int x = 10, y = 0;
try {
cout << "尝试执行除法运算..." << endl;
int result = divide(x, y);
cout << x << " / " << y << " = " << result << endl;
} catch (const string& err_msg) {
cout << "捕获到异常:" << err_msg << endl;
} catch (...) {
cout << "捕获到未知异常!" << endl;
}
cout << "程序继续执行..." << endl;
return 0;
}
预期输出如下:
尝试执行除法运算...
捕获到异常:错误:除数不能为 0!
程序继续执行...
这里可以看到,一旦 divide 函数抛出异常,main 函数中后续的打印语句被跳过,控制权直接交给 catch 块。处理完成后,程序从 catch 块之后继续运行。
异常匹配规则
catch 块按声明顺序匹配异常类型,规则如下:
- 精确匹配:类型完全一致。
- 派生类匹配:抛出的派生类异常可被基类类型的
catch捕获。 - 类型转换:仅支持有限的隐式转换。
- 兜底捕获:
catch(...)匹配剩余所有异常,必须置于最后。
⚠️ 警告:catch 块的顺序至关重要。如果基类异常捕获放在派生类之前,派生类异常将被基类捕获,导致后续代码无法执行。
标准异常库与自定义异常
标准异常类
C++ 标准库提供了一系列预定义异常类,均继承自 std::exception,定义在 <exception> 头文件中。常用类包括:
| 异常类 | 描述 |
|---|---|
std::exception | 所有标准异常的基类 |
std::logic_error | 逻辑错误(如无效参数) |
std::runtime_error | 运行时错误(如 IO 失败) |
std::bad_alloc | 内存分配失败 |
使用 what() 方法可以获取异常的简要描述字符串。
自定义异常类设计
实际开发中,我们常需要业务相关的异常(如'用户不存在'、'权限不足')。自定义异常类应遵循以下原则:
- 继承自
std::exception或其派生类。 - 重写
what()方法返回详细描述。 - 提供必要的构造函数。
#include <iostream>
#include <exception>
#include <string>
using namespace std;
// 基础业务异常类
class BusinessException : public exception {
private:
string err_msg;
public:
BusinessException(const string& msg) : err_msg(msg) {}
const char* what() const noexcept override {
return err_msg.c_str();
}
};
// 派生异常:用户不存在
class UserNotFoundException : public BusinessException {
public:
UserNotFoundException(int user_id)
: BusinessException("用户不存在:ID=" + to_string(user_id)) {}
};
void query_user(int user_id) {
if (user_id < 1000 || user_id > 9999) {
throw UserNotFoundException(user_id);
}
cout << "查询成功:用户 ID=" << user_id << endl;
}
int main() {
try {
query_user(123);
} catch (const UserNotFoundException& e) {
cout << "业务异常:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统异常:" << e.what() << endl;
}
return 0;
}
这样设计的优势在于语义清晰、层次分明,且能兼容标准异常的统一处理逻辑。
高级特性与异常安全
noexcept 关键字
C++11 引入了 noexcept 关键字,用于声明函数是否可能抛出异常。相比旧的异常规格说明,它更简洁且能被编译器优化。
void func() noexcept { /* 保证不抛异常 */ }
若 noexcept 函数内部抛出异常,程序会直接终止,因此需确保该函数确实不会抛出异常。此外,noexcept 还能影响标准库行为,例如 std::vector 的移动构造若标记为 noexcept 会使用移动语义而非拷贝。
异常传播与重新抛出
异常抛出后若未被当前函数捕获,会自动向上传播至调用者。若需在 catch 块中处理部分逻辑(如记录日志)后继续向上抛出,可使用 throw;(不带参数)。
void handle_request(int data) {
try {
process_data(data);
} catch (const string& e) {
cout << "日志记录:" << e << endl;
throw; // 重新抛出原始异常
}
}
异常安全策略
异常安全要求程序在抛出异常时不发生内存泄漏、数据状态不一致或资源未释放。主要解决方案包括:
- 智能指针:利用
std::unique_ptr等自动管理内存。 - RAII 模式:资源获取即初始化,通过析构函数自动释放资源。
- 标准容器:优先使用
vector、string等具备异常安全的组件。
RAII 实战示例
下面是一个基于 RAII 的文件读写工具类,确保即使发生异常文件也能正确关闭:
#include <fstream>
#include <string>
using namespace std;
class FileGuard {
private:
ofstream file;
public:
FileGuard(const string& filename) : file(filename) {
if (!file.is_open()) {
throw string("文件打开失败:" + filename);
}
}
~FileGuard() {
if (file.is_open()) {
file.close();
}
}
void write(const string& content) {
file << content << endl;
}
};
void write_file(const string& filename, const string& content) {
FileGuard file(filename);
file.write(content);
throw string("模拟写入过程中异常"); // 触发异常
}
int main() {
try {
write_file("test.txt", "Hello");
} catch (const string& e) {
cout << "捕获异常:" << e << endl;
}
return 0;
}
即使 write_file 中途抛出异常,FileGuard 对象的析构函数也会被调用,确保文件句柄被正确释放,避免了资源泄漏。
常见错误与最佳实践
常见陷阱
- 过度使用异常:异常应用于异常情况,不应替代正常的控制流(如循环查找元素)。
- 捕获所有异常却不处理:
catch(...)若无日志记录,会导致问题难以排查。 - 按值捕获异常:应使用引用
catch (const Exception& e),避免对象切片和拷贝开销。 - 析构函数抛出异常:析构函数中抛出异常可能导致程序终止,应避免。
最佳实践建议
- 明确场景:仅在真正的错误(如 IO 失败、非法参数)时使用异常。
- 统一规范:系统级错误用标准异常,业务级错误用自定义异常。
- 资源管理:始终使用智能指针或 RAII 管理资源。
- 日志记录:捕获异常后记录详细信息(类型、堆栈、上下文)。
实战案例:文件读写工具
结合上述知识,我们可以构建一个健壮的 FileHandler 类,支持文件读写并处理各类异常。
#include <iostream>
#include <fstream>
#include <vector>
#include <exception>
using namespace std;
// 自定义文件异常基类
class FileException : public exception {
protected:
string err_msg;
public:
FileException(const string& filename, const string& reason) {
err_msg = "文件操作异常:文件\"" + filename + "\", 原因:" + reason;
}
const char* what() const noexcept override {
return err_msg.c_str();
}
};
class FileOpenException : public FileException {
public:
FileOpenException(const string& filename, const string& reason)
: FileException(filename, "打开失败 - " + reason) {}
};
class FileReadException : public FileException {
public:
FileReadException(const string& filename, const string& reason)
: FileException(filename, "读取失败 - " + reason) {}
};
class FileWriteException : public FileException {
public:
FileWriteException(const string& filename, const string& reason)
: FileException(filename, "写入失败 - " + reason) {}
};
// 文件工具类(RAII 模式)
class FileHandler {
private:
string filename;
fstream file_stream;
public:
FileHandler(const string& filename, ios_base::openmode mode) : filename(filename) {
file_stream.open(filename, mode);
if (!file_stream.is_open()) {
throw FileOpenException(filename, "无法打开文件");
}
}
~FileHandler() {
if (file_stream.is_open()) {
file_stream.close();
}
}
vector<string> read_file() {
vector<string> content;
string line;
if (!file_stream.good()) {
throw FileReadException(filename, "文件流状态异常");
}
while (getline(file_stream, line)) {
content.push_back(line);
}
if (file_stream.bad()) {
throw FileReadException(filename, "IO 错误");
}
return content;
}
void write_file(const vector<string>& content) {
if (!file_stream.good()) {
throw FileWriteException(filename, "文件流状态异常");
}
for (const string& line : content) {
file_stream << line << endl;
if (file_stream.fail()) {
throw FileWriteException(filename, "写入失败");
}
}
file_stream.flush();
}
};
int main() {
try {
FileHandler reader("input.txt", ios::in);
auto content = reader.read_file();
FileHandler writer("output.txt", ios::out | ios::trunc);
writer.write_file({"新内容 1", "新内容 2"});
} catch (const FileOpenException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const FileReadException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const FileWriteException& e) {
cout << "错误提示:" << e.what() << endl;
} catch (const exception& e) {
cout << "系统错误:" << e.what() << endl;
}
return 0;
}
该案例展示了如何通过分层异常类和 RAII 模式,在保证资源安全的前提下,为用户提供清晰的错误反馈。

