一文带你深入了解异常



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; 保留原类型
按引用捕获 避免切片,提高效率

异常处理的黄金法则

  1. 用RAII管理资源
  2. 析构函数不抛异常
  3. 只对异常情况用异常
  4. 建立清晰的异常层次

总结

这篇文章是作者搜集大量面经和资料这里出来的。感谢你的支持
作者wkm是一名中国矿业大学(北京) 大一的新生,希望得到你的关注
如果可以的话,记得一键三联!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐