C++异常处理
一、异常的基本概念
1.1 什么是异常
异常是程序运行时发生的、可预料但不可避免的错误,会导致程序正常执行流程中断。例如:
- 内存申请失败(
new抛出bad_alloc) - 打开不存在的文件
- 数组越界访问
- 除零错误
- 网络连接断开
核心区别:编译错误是语法问题,编译阶段就能发现;异常是运行时错误,只有程序执行到特定逻辑才会触发。
1.2 C 语言的错误处理方式(面试对比考点)
C 语言没有原生异常机制,主要通过以下方式处理错误,这些方式存在明显缺陷,也是 C++ 引入异常的原因:
(1)函数返回错误码
通过函数返回值表示执行结果,通常用0表示成功,非0表示错误。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("nonexist.txt", O_RDONLY);
if (fd == -1) { // 必须手动检查返回值,否则错误会被忽略
perror("open failed"); // 打印错误信息
return 1;
}
close(fd);
return 0;
}
(2)全局错误号 errno
系统调用失败时会设置全局变量errno,配合strerror(errno)或perror()解析错误信息。
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main() {
int* p = (int*)malloc(1024 * 1024 * 1024 * 1024); // 申请过大内存
if (p == NULL) {
printf("malloc failed: %s\n", strerror(errno));
return 1;
}
free(p);
return 0;
}
(3)断言 assert
用于调试阶段检查逻辑错误,表达式为假时直接终止程序。发布版本会被禁用,不能用于处理运行时错误。
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // 调试阶段有效,发布版失效
return a / b;
}
C 语言错误处理的核心缺陷(面试必问)
- 错误码易被忽略:程序员忘记检查返回值时,错误会静默传播,导致后续更严重的问题。
- 返回值冲突:函数正常返回值和错误码可能重叠(比如返回
-1既可能是正常结果也可能是错误)。 - 调用链传递麻烦:深层函数发生错误时,需要逐层向上传递错误码,代码冗余。
- 无法自动清理资源:错误发生时,需要手动释放之前申请的内存、文件句柄等,容易遗漏导致资源泄露。
二、C++ 异常处理机制
C++ 提供了throw(抛出异常)、try(监控异常)、catch(捕获异常)三个关键字,实现了错误与正常逻辑的分离。
2.1 异常抛出 throw
语法:throw 异常对象;
- 可以抛出任意类型的对象:基本类型(int、double)、字符串、自定义类对象、标准异常类对象。
- 异常抛出后,当前函数立即终止执行,开始向上层调用者传播异常。
#include <iostream>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw "Division by zero"; // 抛出const char*类型异常
}
return a / b;
}
int main() {
try {
cout << divide(5, 2) << endl; // 正常执行
cout << divide(8, 0) << endl; // 抛出异常
cout << divide(7, 1) << endl; // 不会执行
} catch (const char* e) { // 捕获const char*类型异常
cout << "Error: " << e << endl;
}
return 0;
}
运行结果:
2
Error: Division by zero
2.2 异常捕获 try-catch
语法结构
try {
// 可能抛出异常的代码
} catch (异常类型1 变量名) {
// 处理类型1的异常
} catch (异常类型2 变量名) {
// 处理类型2的异常
} catch (...) {
// 处理所有未被上面捕获的异常(必须放在最后)
}
异常匹配规则(面试必问)
- 精确匹配优先:优先匹配类型完全相同的 catch 块。
- 基类引用可以接收派生类对象:如果用基类引用捕获,必须放在派生类 catch 块的后面,否则派生类异常会被基类块提前拦截。
catch(...)捕获所有异常:作为兜底,必须放在所有 catch 块的最后。
错误示例(基类在前导致派生类异常无法捕获)
#include <stdexcept>
#include <iostream>
using namespace std;
int main() {
try {
throw invalid_argument("Invalid argument"); // 派生自logic_error
} catch (const exception& e) { // 基类在前,会拦截所有派生类异常
cout << "Base exception: " << e.what() << endl;
} catch (const invalid_argument& e) { // 永远不会执行
cout << "Invalid argument: " << e.what() << endl;
}
return 0;
}
2.3 异常的传播与栈展开(面试核心)
异常传播过程
- 异常抛出后,首先检查当前函数是否在
try块内,且有匹配的catch块。 - 如果当前函数无法处理,立即终止当前函数执行,销毁所有局部对象(栈展开),回到上层调用函数。
- 重复上述过程,直到找到匹配的
catch块;如果到main函数仍未处理,调用terminate()终止程序。
栈展开(Unwinding)
异常抛出后,从进入try块到异常抛出点之间,所有在栈上构造的对象会自动按构造的逆序调用析构函数,释放资源。
这是 C++ 异常最核心的优势之一:自动清理局部资源,避免 C 语言手动清理的遗漏问题。
教材示例 12-2:栈展开演示
#include <iostream>
#include <string>
using namespace std;
class Demo {
public:
Demo() { cout << "Constructor of Demo" << endl; }
~Demo() { cout << "Destructor of Demo" << endl; }
};
void func() {
Demo d; // 栈上对象,异常抛出时会自动析构
cout << "Throw exception in func()" << endl;
throw "Exception from func";
}
int main() {
try {
func();
} catch (const char* e) {
cout << "Caught: " << e << endl;
}
return 0;
}
运行结果:
Constructor of Demo
Throw exception in func()
Destructor of Demo
Caught: Exception from func
三、自定义异常类
C++ 允许自定义异常类,推荐继承标准库的std::exception类,这样可以和标准异常统一处理。
3.1 标准异常类体系(面试必背)
标准异常定义在<stdexcept>头文件中,基类是std::exception,核心派生类分为两大类:
| 类别 | 含义 | 常见子类 |
|---|---|---|
logic_error |
逻辑错误(可提前避免) | invalid_argument(无效参数)out_of_range(越界)length_error(长度超限) |
runtime_error |
运行时错误(不可预料) | bad_alloc(内存申请失败)range_error(计算结果越界)overflow_error(上溢) |
基类exception提供了虚函数what(),返回异常描述字符串:
virtual const char* what() const noexcept;
3.2 自定义异常类示例
方式 1:直接继承std::exception
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// 自定义账户异常类,继承runtime_error
class AccountException : public runtime_error {
private:
string accountId; // 异常关联的账户ID
public:
AccountException(const string& id, const string& msg)
: runtime_error(msg), accountId(id) {}
// 新增方法获取账户ID
const string& getAccountId() const { return accountId; }
};
class Account {
private:
string id;
double balance;
public:
Account(const string& id, double balance) : id(id), balance(balance) {}
void withdraw(double amount) {
if (amount > balance) {
// 抛出自定义异常
throw AccountException(id, "Insufficient balance");
}
balance -= amount;
}
};
int main() {
try {
Account acc("123456", 1000);
acc.withdraw(2000);
} catch (const AccountException& e) {
cout << "Account " << e.getAccountId() << " error: " << e.what() << endl;
} catch (const exception& e) {
cout << "Other error: " << e.what() << endl;
}
return 0;
}
运行结果:
plaintext
Account 123456 error: Insufficient balance
方式 2:完全自定义异常类(不推荐)
虽然语法允许,但无法和标准异常统一捕获,不符合 C++ 规范:
class MyException {
private:
string msg;
public:
MyException(const string& msg) : msg(msg) {}
const string& what() const { return msg; }
};
四、noexcept 关键字(C++11 面试高频)
4.1 基本语法与作用
noexcept用于标识函数不会抛出异常,编译器可以据此进行优化(例如省略栈展开的额外代码,提高运行效率)。
两种形式
- 无参数形式:承诺函数绝对不会抛出异常
void func() noexcept; // func不会抛出任何异常 - 带表达式形式:表达式为
true时承诺不抛异常// func的异常说明和swap一致:swap不抛则func不抛 void func() noexcept(noexcept(swap(a, b)));
4.2 核心注意事项(面试必问)
-
noexcept只是承诺,不是强制:如果noexcept函数内部真的抛出了异常,会直接调用terminate()终止程序,不会进行栈展开,局部对象不会被析构,可能导致资源泄露。void bad_func() noexcept { throw "Exception in noexcept function"; // 编译通过,运行时直接terminate } -
推荐使用
noexcept的场景:- 所有析构函数(C++11 后析构函数默认
noexcept) - 构造函数、拷贝构造函数、移动构造函数、赋值运算符(如果确定不会抛异常)
- 简单的只读函数(如
getter方法) swap函数(STL 所有容器的swap都是noexcept)
- 所有析构函数(C++11 后析构函数默认
-
noexcept运算符:编译期判断一个表达式是否承诺不抛异常,返回bool值#include <iostream> #include <vector> using namespace std; void func1() noexcept {} void func2() {} int main() { cout << boolalpha; cout << noexcept(func1()) << endl; // true cout << noexcept(func2()) << endl; // false cout << noexcept(vector<int>().swap(vector<int>())) << endl; // true return 0; }
五、异常安全性(面试高级考点)
异常安全性是指:异常发生时,程序不会发生资源泄露,且对象状态保持合法。分为三个级别:
1. 基本保证(Basic Guarantee)
异常发生后,程序处于合法状态,没有资源泄露,但对象状态可能发生改变(例如部分操作完成)。
例如:修改后的栈
push函数,赋值失败时栈的状态不变。
2. 强保证(Strong Guarantee)
异常发生后,程序状态完全回滚到操作前,就像操作从未发生过。
实现技巧:先创建临时对象完成所有可能抛异常的操作,再用
noexcept的swap交换临时对象和原对象。
#include <vector>
#include <algorithm>
using namespace std;
// 强保证的逆序函数
void reverse(vector<int>& v) {
vector<int> tmp(v.rbegin(), v.rend()); // 所有可能抛异常的操作在临时对象上完成
v.swap(tmp); // swap是noexcept,不会抛异常
}
3. 不抛保证(No-throw Guarantee)
操作绝对不会抛出异常,是最高级别的异常安全。
例如:基本类型的赋值、指针操作、STL 容器的
swap函数。
六、面试高频问题汇总
-
C++ 异常和 C 语言错误处理的区别?
- C++ 异常将错误处理和正常逻辑分离,代码更清晰;C 语言错误码和业务逻辑混杂。
- C++ 异常会自动进行栈展开,清理局部资源;C 语言需要手动释放资源。
- C++ 异常支持类型安全的错误传递,且可以传递复杂的错误信息;C 语言错误码只能传递整数。
- C++ 异常无法被忽略;C 语言错误码容易被程序员遗漏。
-
什么是栈展开?异常抛出后,从
try块入口到异常抛出点之间,所有栈上的局部对象会按构造逆序自动调用析构函数,释放资源,直到找到匹配的catch块。 -
为什么析构函数不要抛异常?异常传播的栈展开过程中,会调用局部对象的析构函数。如果析构函数再抛出异常,会导致两个异常同时存在,C++ 无法处理,会直接调用
terminate()终止程序。 -
throw、throw;和throw ex;的区别?throw ex;:抛出一个新的异常对象ex。throw;:重新抛出当前正在处理的异常(只能在catch块内使用),不会创建新对象。- 空
throw用于将异常向上层传递,保留原始异常的类型和信息。
-
noexcept函数抛异常会怎么样?会直接调用terminate()终止程序,不会进行栈展开,局部对象不会被析构,可能导致资源泄露。 -
标准异常类中
logic_error和runtime_error的区别?logic_error:逻辑错误,是程序员编写代码时可以避免的错误(例如传入无效参数、数组越界)。runtime_error:运行时错误,是程序运行时由环境因素导致的不可预料的错误(例如内存不足、文件损坏)。
更多推荐



所有评论(0)