一文带你深入了解异常
C++异常处理机制详解 本文系统介绍了C++异常处理的核心知识。主要内容包括: 异常基础语法:try/catch/throw关键字的使用方法 RAII资源管理:通过对象生命周期自动管理资源,保证异常安全 异常规范:从动态异常规范到C++11的noexcept说明符 标准异常体系:exception类及其派生类的层次结构 构造/析构函数中的异常处理注意事项 面试高频考点:RAII原理、noexcep
一文带你深入了解异常
文章目录
C++异常处理详解及面试考点
异常是C++中处理错误和异常情况的重要机制。我来系统地讲解异常的核心知识,并总结面试中常见考点。
一、异常基础
1. 基本语法
#include <iostream>
#include <stdexcept>
using namespace std;
// 可能抛出异常的函数
double divide(double a, double b) {
if (b == 0) {
throw runtime_error("除数不能为0"); // 抛出异常
}
return a / b;
}
int main() {
try {
double result = divide(10, 0);
cout << "结果: " << result << endl;
}
catch (const runtime_error& e) { // 捕获特定类型异常
cout << "捕获到异常: " << e.what() << endl;
}
catch (const exception& e) { // 捕获基类异常
cout << "其他异常: " << e.what() << endl;
}
catch (...) { // 捕获任何异常(省略号)
cout << "未知异常" << endl;
}
return 0;
}
| 关键字 | 作用 |
|---|---|
| try | 包含可能抛出异常的代码块 |
| throw | 抛出异常对象 |
| catch | 捕获并处理特定类型的异常 |
2. 异常对象
可以抛出任何类型的对象:
// 抛出基本类型
throw 42;
throw "error occurred";
// 抛出标准异常
throw runtime_error("runtime error");
throw out_of_range("index out of range");
// 抛出自定义异常
class MyException {
public:
MyException(string msg) : message(msg) {}
string what() const { return message; }
private:
string message;
};
throw MyException("my error");
二、异常安全与RAII
1. 什么是RAII?
RAII(Resource Acquisition Is Initialization)是C++管理资源的核心思想:资源的获取即初始化,资源的释放即析构。
// 错误的做法:手动管理资源
void badExample() {
int* p = new int[1000];
// ... 一些操作
if (error) {
throw runtime_error("错误"); // ❌ p泄漏了!
}
delete[] p;
}
// 正确的做法:RAII
void goodExample() {
vector<int> v(1000); // RAII类自动管理内存
if (error) {
throw runtime_error("错误"); // ✅ vector析构时会自动释放
}
} // 离开作用域,v自动释放
2. 异常安全级别
| 级别 | 说明 | 示例 |
|---|---|---|
| 无保证 | 异常后资源可能泄漏 | 裸指针操作 |
| 基本保证 | 异常后对象状态有效但不确定 | 多数STL容器 |
| 强保证 | 异常后状态回滚,如同没调用 | 事务性操作 |
| 不抛异常 | 保证不会抛出异常 | 简单getter |
三、异常规范(C++17前)
1. 动态异常规范(C++98/03,已废弃)
// 声明可能抛出的异常类型
void func1() throw(runtime_error, logic_error); // 可能抛出这两种
void func2() throw(); // 不抛出任何异常
void func3(); // 可能抛出任何异常
2. noexcept(C++11起)
void func1() noexcept; // 保证不抛出异常
void func2() noexcept(true); // 同上
void func3() noexcept(false); // 可能抛出异常
// 如果noexcept函数抛出异常,程序会直接终止
面试考点:移动构造函数通常应该声明为 noexcept,这样STL容器在重新分配时才会优先使用移动而不是拷贝。
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 移动资源
}
};
四、标准异常体系
exception (基类)
├── logic_error
│ ├── invalid_argument
│ ├── domain_error
│ ├── length_error
│ ├── out_of_range
│ └── future_error (C++11)
├── runtime_error
│ ├── range_error
│ ├── overflow_error
│ ├── underflow_error
│ ├── system_error (C++11)
│ └── regex_error (C++11)
├── bad_alloc (new失败)
├── bad_cast (dynamic_cast失败)
├── bad_typeid (typeid失败)
├── bad_exception (意外异常)
└── ios_base::failure (I/O错误)
try {
vector<int> v;
v.at(100) = 10; // 抛出 out_of_range
int* p = new int[100000000000]; // 抛出 bad_alloc
}
catch (const out_of_range& e) {
cout << "越界: " << e.what() << endl;
}
catch (const bad_alloc& e) {
cout << "内存不足: " << e.what() << endl;
}
catch (const exception& e) { // 捕获所有标准异常
cout << "标准异常: " << e.what() << endl;
}
五、构造函数中的异常
构造函数抛出异常是常见的错误处理方式:
class File {
FILE* f;
public:
File(const string& filename) : f(nullptr) {
f = fopen(filename.c_str(), "r");
if (!f) {
// 构造函数没有返回值,只能用异常
throw runtime_error("打开文件失败: " + filename);
}
cout << "文件打开成功" << endl;
}
~File() {
if (f) fclose(f);
cout << "文件关闭" << endl;
}
};
int main() {
try {
File f("nonexist.txt"); // 抛出异常
// 不会执行到这里
}
catch (const exception& e) {
cout << e.what() << endl;
}
return 0;
}
注意:如果构造函数抛出异常,对象的析构函数不会执行,但已经构造完成的成员对象会被正常析构。
六、析构函数中的异常
析构函数不应该抛出异常!
class Bad {
public:
~Bad() {
throw runtime_error("析构异常"); // ❌ 危险!
}
};
// 如果两个异常同时存在,程序会终止
try {
Bad b;
throw runtime_error("另一个异常");
}
catch (...) { // 可能捕获不到析构的异常
}
原因:
- 如果析构函数在异常传播过程中被调用(栈展开),又抛出异常,会导致
std::terminate - 析构函数应该
noexcept(默认就是)
七、面试高频考点
考点1:异常安全与RAII
问题:什么是RAII?为什么RAII能保证异常安全?
答案:RAII将资源生命周期与对象生命周期绑定。当异常导致栈展开时,所有局部对象的析构函数会被自动调用,确保资源释放。这是C++异常安全的基石。
考点2:异常规格说明
问题:noexcept 的作用是什么?什么时候应该用?
答案:
noexcept告诉编译器函数不抛出异常- 移动构造函数、析构函数、swap函数通常应设为
noexcept - STL容器在重新分配时,会优先选择
noexcept的移动构造函数,否则用拷贝构造
考点3:构造函数异常
问题:构造函数抛出异常后,对象是否会被析构?
答案:不会。因为对象构造尚未完成,不算完整对象。但已经构造完成的成员对象(包括基类子对象)会被自动析构。
考点4:析构函数异常
问题:析构函数能抛出异常吗?为什么?
答案:不应该。如果析构函数在栈展开时抛出异常,会导致 std::terminate。析构函数应该捕获所有异常并处理,或者声明为 noexcept。
考点5:性能开销
问题:异常有性能开销吗?
答案:
- 无异常时:几乎没有开销(零成本异常模型)
- 抛出异常时:开销较大(栈展开、对象析构、匹配catch)
- 所以异常应该用于异常情况,而不是正常控制流
考点6:catch的顺序
问题:多个catch块的顺序重要吗?
答案:非常重要。catch按出现顺序匹配,应该把最具体的异常类型放在前面,基类类型放后面。
try {
// ...
}
catch (const out_of_range& e) { } // ✅ 先具体
catch (const logic_error& e) { } // ✅ 后基类
catch (const exception& e) { } // ✅ 更后
catch (...) { } // ✅ 最后
考点7:重新抛出异常
问题:如何重新抛出当前捕获的异常?
答案:使用 throw;(不带参数),这保留了异常的原类型和栈信息。
try {
// ...
}
catch (const exception& e) {
// 记录日志
cout << "Log: " << e.what() << endl;
throw; // 重新抛出,类型不变
}
考点8:异常与多态
问题:为什么通常用引用捕获异常?
答案:
- 避免对象切片(如果按值捕获,派生类异常会被切成基类)
- 避免额外的拷贝开销
- 可以修改异常对象(如果需要)
catch (const exception& e) // ✅ 推荐
catch (exception e) // ❌ 对象切片,性能差
八、完整示例:综合应用
#include <iostream>
#include <exception>
#include <vector>
#include <fstream>
using namespace std;
// 自定义异常体系
class DatabaseException : public exception {
string msg;
public:
DatabaseException(const string& s) : msg("数据库错误: " + s) {}
const char* what() const noexcept override { return msg.c_str(); }
};
class ConnectionException : public DatabaseException {
public:
ConnectionException() : DatabaseException("连接失败") {}
};
class QueryException : public DatabaseException {
public:
QueryException() : DatabaseException("查询失败") {}
};
// RAII资源类
class DatabaseConnection {
bool connected = false;
public:
DatabaseConnection(const string& connStr) {
cout << "连接数据库: " << connStr << endl;
// 模拟连接失败
if (connStr.empty()) {
throw ConnectionException();
}
connected = true;
}
~DatabaseConnection() noexcept {
if (connected) {
cout << "断开数据库连接" << endl;
}
}
void query(const string& sql) {
if (!connected) throw DatabaseException("未连接");
if (sql.empty()) throw QueryException();
cout << "执行查询: " << sql << endl;
}
};
// 业务逻辑
void processData(const string& connStr, const string& sql) {
DatabaseConnection conn(connStr); // RAII保证资源释放
conn.query(sql);
cout << "数据处理完成" << endl;
}
int main() {
try {
processData("", "SELECT * FROM users"); // 连接失败
}
catch (const ConnectionException& e) {
cout << "连接异常: " << e.what() << endl;
// 处理连接问题
}
catch (const QueryException& e) {
cout << "查询异常: " << e.what() << endl;
// 处理查询问题
}
catch (const DatabaseException& e) {
cout << "数据库异常: " << e.what() << endl;
}
catch (const exception& e) {
cout << "其他异常: " << e.what() << endl;
}
return 0;
}
九、总结
| 考点 | 核心要点 |
|---|---|
| RAII | 资源获取即初始化,异常安全的基石 |
| noexcept | 移动构造、析构函数应该用 |
| 构造函数异常 | 对象不会析构,但成员会 |
| 析构函数异常 | 不应该抛出,否则terminate |
| 捕获顺序 | 具体类型在前,基类在后 |
| 重新抛出 | throw; 保留原类型 |
| 按引用捕获 | 避免切片,提高效率 |
异常处理的黄金法则:
- 用RAII管理资源
- 析构函数不抛异常
- 只对异常情况用异常
- 建立清晰的异常层次
总结
这篇文章是作者搜集大量面经和资料这里出来的。感谢你的支持
作者wkm是一名中国矿业大学(北京) 大一的新生,希望得到你的关注
如果可以的话,记得一键三联!
更多推荐



所有评论(0)