C++ 中错误处理的核心知识点: 包括错误处理的不同方式、异常机制的原理与使用、异常安全、以及实际开发中的最佳实践等 ------ 这是 C++ 编程中保障程序健壮性的核心内容,我会从基础到进阶、结合实际场景全面讲解。
一、C++ 错误处理的核心方式
C++ 提供了多种错误处理手段,不同手段适用于不同场景,先对比理解它们的定位:
| 处理方式 | 核心用法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 返回码 / 错误码 | int func() { return -1; } |
简单函数、底层系统调用 | 轻量、无运行时开销 | 易忽略、无法自动中断流程 |
| 全局错误码 | errno/ 自定义全局变量 |
传统 C 风格代码 | 跨函数传递 | 线程不安全、无法关联上下文 |
| 断言(assert) | assert(条件); |
调试阶段、逻辑断言(必真条件) | 快速定位逻辑错误 | 发布版本可被禁用、直接终止程序 |
| 异常机制 | try-catch-throw |
运行时错误、复杂业务逻辑 | 类型安全、自动中断流程 | 有轻微运行时开销、需注意安全 |
1. 传统错误处理:返回码 / 全局错误码
这是 C 语言继承的方式,也是 C++ 早期常用的错误处理手段。
示例(返回码):
cpp
#include <iostream>
// 模拟文件打开,返回0成功,-1失败
int open_file(const std::string& path) {
if (path.empty()) {
return -1; // 错误码:路径为空
}
std::cout << "文件打开成功" << std::endl;
return 0;
}
int main() {
int ret = open_file("");
if (ret != 0) { // 必须手动检查返回码
std::cerr << "文件打开失败,错误码:" << ret << std::endl;
}
return 0;
}
示例(全局错误码,线程不安全):
cpp
#include <iostream>
#include <cerrno> // C标准错误码头文件
#include <cstring>
// 模拟内存分配,失败时设置errno
void* alloc_mem(size_t size) {
if (size == 0) {
errno = EINVAL; // 全局错误码:无效参数
return nullptr;
}
return malloc(size);
}
int main() {
void* ptr = alloc_mem(0);
if (ptr == nullptr) {
// 解析全局错误码
std::cerr << "分配内存失败:" << strerror(errno) << std::endl;
}
return 0;
}
2. 断言(assert):调试阶段的 "硬检查"
assert是宏(定义在<cassert>),用于检查程序运行时必须为真的条件 ,若条件为假,会打印错误信息并调用abort()终止程序。
核心特点:
- 默认仅在调试模式 (未定义
NDEBUG)生效,发布模式下会被完全移除; - 用于 "逻辑断言"(比如 "指针非空""数组索引合法"),而非处理运行时错误(比如 "文件不存在")。
示例:
cpp
#include <cassert>
#include <iostream>
void process_data(int* data, int size) {
// 断言:data不能为nullptr,size必须>0(逻辑上必真)
assert(data != nullptr && "data pointer is null");
assert(size > 0 && "size must be positive");
std::cout << "处理数据,大小:" << size << std::endl;
}
int main() {
int arr[] = {1,2,3};
// 正常调用:断言通过
process_data(arr, 3);
// 错误调用:断言触发(调试模式下)
process_data(nullptr, 0); // 程序终止,打印:assertion failed: data != nullptr && "data pointer is null"
return 0;
}
3. 异常机制:C++ 的核心错误处理方案
异常是 C++ 专为 "运行时错误" 设计的类型安全机制,核心是将错误检测与错误处理分离,且能自动中断当前流程、向上传播错误。
核心优势:
- 错误不会被忽略(除非故意不捕获);
- 可携带丰富的错误上下文(自定义异常类);
- 适配复杂的函数调用链(比如多层嵌套函数);
- 对
void返回值的函数(如之前的submit),是唯一优雅的错误通知方式。
二、异常机制的核心原理与语法
1. 基本语法:throw → try → catch
异常的生命周期:抛出(throw)→ 捕获(catch)→ 处理,中间会发生 "栈展开(Stack Unwinding)"。
cpp
#include <iostream>
#include <stdexcept>
// 抛出异常的函数
double divide(int a, int b) {
if (b == 0) {
// 抛出标准异常:运行时错误
throw std::runtime_error("division by zero");
}
return static_cast<double>(a) / b;
}
int main() {
// try块:包裹可能抛出异常的代码
try {
std::cout << divide(10, 2) << std::endl; // 正常执行:5
std::cout << divide(10, 0) << std::endl; // 抛出异常
}
// catch块:捕获指定类型的异常(按类型匹配)
catch (const std::runtime_error& e) {
// 处理异常:打印错误信息
std::cerr << "错误:" << e.what() << std::endl;
}
// 万能catch:捕获所有类型的异常(不推荐,无法区分错误类型)
catch (...) {
std::cerr << "未知错误" << std::endl;
}
std::cout << "程序继续执行" << std::endl; // 异常处理后,程序不会终止
return 0;
}
e 是什么?
e 只是一个变量名 (你可以换成任何合法名字,比如 err、ex),它代表的是被捕获到的异常对象。
举个类比:
- 你用
catch (const std::runtime_error& e)捕获异常,就像 "警察(catch)抓住了一个小偷(异常对象),并给这个小偷起了个临时名字叫 e"; - 这个 "小偷(e)" 不是普通变量,而是一个继承自
std::exception的异常类对象 (比如std::runtime_error、std::out_of_range,或你自定义的异常类对象)。
简单示例(替换变量名):
cpp
try {
throw std::runtime_error("除数不能为0");
}
// 把e换成err,完全不影响功能
catch (const std::runtime_error& err) {
// 这里用err.what(),效果和e.what()一样
std::cerr << "错误:" << err.what() << std::endl;
}
what() 是什么?
what() 是 C++ 标准异常基类 std::exception 中定义的一个虚成员函数,作用是:
- 返回一个
const char*类型的字符串,描述当前异常的具体错误信息; - 所有标准异常类(如
std::runtime_error、std::out_of_range)都会重写这个函数,返回对应的错误描述; - 你自定义的异常类也可以重写它,返回自定义的错误信息。
std::exception 类的核心结构(简化版):
cpp
class exception {
public:
// 虚函数:返回异常描述信息
virtual const char* what() const noexcept;
virtual ~exception() = default;
};
// 标准异常类继承并重写what()
class runtime_error : public exception {
private:
std::string msg; // 存储错误信息
public:
explicit runtime_error(const std::string& message) : msg(message) {}
// 重写what(),返回传入的错误信息
const char* what() const noexcept override {
return msg.c_str();
}
};
完整示例:拆解 e.what() 的使用
cpp
#include <iostream>
#include <stdexcept> // 包含标准异常类
int main() {
try {
// 1. 抛出一个std::runtime_error类型的异常对象,携带信息"division by zero"
throw std::runtime_error("division by zero");
}
// 2. 捕获这个异常对象,给它起个名字叫e(const引用避免拷贝)
catch (const std::runtime_error& e) {
// 3. 调用e的what()函数,获取异常的描述信息并打印
std::cerr << "异常信息:" << e.what() << std::endl;
// 输出:异常信息:division by zero
}
// 自定义异常类的what()
try {
// 自定义异常(继承std::exception)
class MyException : public std::exception {
public:
// 重写what(),返回自定义信息
const char* what() const noexcept override {
return "这是我自定义的异常信息";
}
};
throw MyException();
}
catch (const std::exception& e) { // 基类捕获
std::cerr << "自定义异常信息:" << e.what() << std::endl;
// 输出:自定义异常信息:这是我自定义的异常信息
}
return 0;
}
关键注意点
what()返回的字符串生命周期 :标准异常类的what()返回的字符串由异常对象管理,只要异常对象e还在(catch 块内),这个字符串就有效;noexcept特性:what()被声明为noexcept,意味着它本身不会抛出异常(保证获取错误信息时不会再出问题);
2. C++ 标准异常体系
C++ 标准库提供了一套异常继承体系(均继承自std::exception),定义在<stdexcept>/<exception>中,推荐优先使用:
| 异常类 | 头文件 | 适用场景 |
|---|---|---|
std::exception |
<exception> |
所有标准异常的基类 |
std::runtime_error |
<stdexcept> |
运行时错误(可自定义消息) |
std::logic_error |
<stdexcept> |
逻辑错误(比如参数非法、越界) |
std::out_of_range |
<stdexcept> |
索引越界(如 vector::at ()) |
std::bad_alloc |
<new> |
内存分配失败(new 失败) |
std::bad_cast |
<typeinfo> |
动态类型转换失败(dynamic_cast) |
示例:使用标准异常派生类
cpp
#include <stdexcept>
#include <vector>
void access_vector(const std::vector<int>& vec, int idx) {
if (idx < 0 || idx >= vec.size()) {
// 抛出索引越界异常
throw std::out_of_range("index " + std::to_string(idx) + " out of range, size: " + std::to_string(vec.size()));
}
std::cout << "元素值:" << vec[idx] << std::endl;
}
int main() {
std::vector<int> vec = {1,2,3};
try {
access_vector(vec, 5);
}
catch (const std::out_of_range& e) {
std::cerr << "越界错误:" << e.what() << std::endl; // 输出:index 5 out of range, size: 3
}
return 0;
}
3. 异常的匹配规则
- catch 块按从上到下 的顺序匹配,因此派生类异常必须放在基类异常前面;
- 异常捕获遵循 "类型兼容":可以用基类捕获派生类(比如
std::exception捕获所有标准异常); - 推荐捕获const 引用 (
const T&):避免拷贝、支持多态、防止切片。
错误示例(基类 catch 在前):
cpp
try {
throw std::runtime_error("test");
}
// 基类catch在前,派生类catch永远不会被匹配
catch (const std::exception& e) {
std::cerr << "基类异常:" << e.what() << std::endl;
}
catch (const std::runtime_error& e) { // 无效!
std::cerr << "运行时异常:" << e.what() << std::endl;
}
正确示例:
cpp
try {
throw std::runtime_error("test");
}
catch (const std::runtime_error& e) { // 派生类在前
std::cerr << "运行时异常:" << e.what() << std::endl;
}
catch (const std::exception& e) { // 基类在后
std::cerr << "基类异常:" << e.what() << std::endl;
}
4. 异常的传播(栈展开)
当异常被抛出且当前函数没有匹配的 catch 块时,异常会向上传播:
- 终止当前函数的执行;
- 销毁当前函数的局部对象(包括栈上的所有变量,遵循 RAII);
- 回到调用该函数的上层函数,继续寻找 catch 块;
- 若传播到
main()函数仍未捕获,触发std::terminate(),程序崩溃。
示例(栈展开):
cpp
#include <stdexcept>
#include <iostream>
void func3() {
throw std::runtime_error("error in func3");
}
void func2() {
int x = 10; // 局部变量,栈展开时会被销毁
func3(); // 调用func3,抛出异常
std::cout << "func2继续执行" << std::endl; // 不会执行
}
void func1() {
func2();
std::cout << "func1继续执行" << std::endl; // 不会执行
}
int main() {
try {
func1();
}
catch (const std::runtime_error& e) {
std::cerr << "捕获异常:" << e.what() << std::endl;
}
return 0;
}
5. noexcept 说明符(C++11+)
noexcept用于声明函数 "不会抛出异常"(或 "在指定条件下不抛出"),是对编译器和调用者的承诺:
void func() noexcept;:函数绝对不抛出异常;void func() noexcept(条件);:条件为 true 时不抛出;- 若声明
noexcept的函数实际抛出了异常,程序会直接调用std::terminate(),不会触发栈展开。
用途:
- 优化:编译器对
noexcept函数可做更多优化(比如移动构造函数标记noexcept,容器会优先使用移动而非拷贝); - 明确接口:告诉调用者 "无需为该函数写 catch 块";
- 示例:
cpp
// 声明:该函数不会抛出异常
int add(int a, int b) noexcept {
return a + b;
}
// 条件noexcept:当T的移动构造函数不抛出时,本函数也不抛出
template <typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T>) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
三、异常安全(进阶核心)
异常安全是指 "程序抛出异常时,仍能保证:
- 资源不泄漏(内存、文件句柄、锁等);
- 数据结构不变质(不会出现半修改的状态)。"
1. 异常安全的四个级别(从低到高)
| 级别 | 含义 |
|---|---|
| 不保证(No guarantee) | 异常可能导致资源泄漏、数据损坏 |
| 基本保证(Basic guarantee) | 异常后,资源不泄漏,数据处于合法状态 |
| 强保证(Strong guarantee) | 异常后,程序状态回滚到异常前的状态 |
| 不抛出保证(Nothrow guarantee) | 函数永远不抛出异常(noexcept) |
2. RAII:异常安全的基石
RAII(资源获取即初始化)是 C++ 的核心惯用法,通过栈对象的析构函数自动释放资源------ 即使发生异常,栈展开时局部对象的析构函数一定会被调用,从而保证资源不泄漏。
示例(RAII 保证异常安全):
cpp
#include <stdexcept>
#include <fstream>
#include <mutex>
// 错误示例:手动管理资源,异常导致泄漏
void bad_func() {
std::ofstream* file = new std::ofstream("test.txt"); // 动态分配
std::mutex* mtx = new std::mutex();
mtx->lock();
throw std::runtime_error("error"); // 抛出异常
// 以下代码不会执行,资源泄漏
mtx->unlock();
delete mtx;
delete file;
}
// 正确示例:RAII管理资源
void good_func() {
std::ofstream file("test.txt"); // 栈对象,析构时自动关闭
std::lock_guard<std::mutex> lock(std::mutex()); // RAII锁,析构时自动解锁
throw std::runtime_error("error"); // 抛出异常
// 栈展开时,lock和file的析构函数会被调用,资源自动释放
}
四、自定义异常类
实际开发中,推荐继承std::exception(或其派生类)实现自定义异常,便于统一处理:
cpp
#include <stdexcept>
#include <string>
// 自定义线程池异常类
class ThreadPoolException : public std::runtime_error {
public:
// 构造函数1:仅传递错误信息,错误码默认0
explicit ThreadPoolException(const std::string& msg)
: std::runtime_error(msg), err_code_(0) {}
// 构造函数2:传递错误信息 + 自定义错误码
ThreadPoolException(const std::string& msg, int err_code)
: std::runtime_error(msg), err_code_(err_code) {}
// 自定义接口:获取错误码
int error_code() const noexcept {
return err_code_;
}
private:
int err_code_; // 自定义错误码
};
// 使用自定义异常
#include <iostream>
void submit_task() {
throw ThreadPoolException("submit to stopped thread pool", 1001);
}
int main() {
try {
submit_task();
}
catch (const ThreadPoolException& e) {
std::cerr << "错误信息:" << e.what() << std::endl;
std::cerr << "错误码:" << e.error_code() << std::endl;
}
// 基类捕获:兼容所有标准异常
catch (const std::exception& e) {
std::cerr << "标准异常:" << e.what() << std::endl;
}
return 0;
}
五、实际开发中的最佳实践
- 异常只用于 "异常情况":不要用异常处理正常流程(比如 "用户输入为空" 是正常情况,用返回值;"文件无法写入" 是异常情况,用异常);
- 优先使用标准异常 :自定义异常继承
std::exception,便于统一捕获和处理; - 捕获异常用 const 引用:避免拷贝、支持多态、防止切片;
- RAII 管理所有资源:内存、锁、文件句柄等,杜绝异常导致的泄漏;
- 避免 "空 catch" :
catch (...) {}会吞掉所有异常,至少记录日志; - 接口明确异常约定:在函数注释中说明 "可能抛出哪些异常";
- 析构函数不抛出异常 :若析构函数必须处理错误,应内部捕获,避免向外抛出(否则栈展开时会触发
std::terminate())。
六、常见误区
- 过度使用异常:比如简单的数学运算、参数检查用异常,导致性能开销和代码冗余;
- 忽略异常安全:手动管理资源,未用 RAII,导致异常时泄漏;
- 捕获异常后不处理:仅打印日志却不修复状态,导致程序后续逻辑出错;
- throw 值而非引用 :
throw std::runtime_error("msg")会拷贝,推荐捕获引用但抛出值(或直接抛引用); - 在 noexcept 函数中抛异常:直接导致程序崩溃,违背接口承诺。
总结
- C++ 错误处理有返回码、assert、异常三种核心方式,异常是运行时错误的首选,assert 仅用于调试阶段的逻辑检查;
- 异常机制的核心是
throw-catch+ 栈展开,捕获时需遵循 "派生类在前、基类在后",优先捕获const引用; - 异常安全的核心是RAII,通过栈对象析构自动释放资源,避免泄漏;
noexcept用于声明函数的异常行为,自定义异常应继承std::exception,且析构函数绝对不能抛出异常。