17、【C++】异常

目录

一、异常的基本概念

1.1 为什么需要异常(相比错误码)

在C++中,处理错误的传统方式是错误码(如返回-1表示失败),但异常机制具有以下优势:

  • 不污染返回值:函数返回值可专注于业务逻辑,无需保留错误码。
  • 集中处理:错误处理代码与正常逻辑分离,提高可读性。
  • 强制处理:未处理的异常会导致程序终止,避免错误被忽略。
  • 传播性:异常可沿调用栈向上传播,直至被捕获,适合深层调用。

错误码示例(传统方式)

int divide(int a, int b) {
    if (b == 0) return -1; // 错误码
    return a / b;
}

// 使用时需检查返回值
int result = divide(10, 0);
if (result == -1) {
    // 处理错误
}

异常示例

int divide(int a, int b) {
    if (b == 0) throw std::runtime_error("division by zero");
    return a / b;
}

// 使用时集中处理异常
try {
    int result = divide(10, 0);
} catch (const std::exception& e) {
    std::cout << "错误:" << e.what() << std::endl;
}

1.2 异常的工作流程

  1. 抛出异常:通过throw关键字抛出异常对象。
  2. 查找处理:编译器沿调用栈向上查找匹配的catch块。
  3. 栈展开:销毁从throw点到catch点之间的所有自动对象。
  4. 处理异常:执行匹配的catch块代码。
  5. 继续执行:异常处理完成后,从catch块后继续执行。

1.3 异常的核心术语

  • 异常对象throw表达式创建的对象,用于传递错误信息。
  • try块:可能抛出异常的代码块,后跟一个或多个catch块。
  • catch块:捕获并处理特定类型异常的代码块。
  • 栈展开:异常抛出时,自动销毁局部对象的过程。
  • 未捕获异常:未被任何catch块捕获的异常,导致std::terminate调用。

二、异常的语法

2.1 try块(监控异常)

try块用于包裹可能抛出异常的代码,其后必须跟一个或多个catch块:

try {
    // 可能抛出异常的代码
    risky_operation();
} catch (const std::exception& e) {
    // 处理异常
}

2.2 throw表达式(抛出异常)

throw用于抛出异常对象,类型可以是内置类型或自定义类型:

// 抛出内置类型
throw 42; // 抛出int类型异常

// 抛出标准异常
throw std::out_of_range("index out of bounds");

// 抛出自定义异常
throw MyException("custom error");

2.3 catch块(捕获异常)

catch块根据异常类型捕获异常,语法为catch (类型名 变量名)

try {
    throw std::runtime_error("error");
} catch (const std::runtime_error& e) { // 捕获runtime_error
    std::cout << e.what() << std::endl;
} catch (const std::exception& e) { // 捕获其他exception派生类
    std::cout << e.what() << std::endl;
} catch (...) { // 捕获所有类型异常(兜底)
    std::cout << "unknown error" << std::endl;
}

2.4 多个catch块的匹配顺序

catch块按声明顺序匹配异常类型,派生类异常必须在基类异常前捕获,否则派生类异常会被基类catch块捕获:

try {
    throw std::runtime_error("runtime error");
} catch (const std::exception& e) { // 基类异常(错误:应放在派生类后)
    std::cout << "exception: " << e.what() << std::endl;
} catch (const std::runtime_error& e) { // 派生类异常(永远不会被匹配)
    std::cout << "runtime error: " << e.what() << std::endl;
}

正确顺序

try {
    throw std::runtime_error("runtime error");
} catch (const std::runtime_error& e) { // 派生类在前
    std::cout << "runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) { // 基类在后
    std::cout << "exception: " << e.what() << std::endl;
}

三、异常类型与传播

3.1 异常类型(内置类型与自定义类型)

  • 内置类型:如intconst char*,简单但信息有限。
  • 标准异常std::exception派生类,提供what()方法返回错误信息。
  • 自定义类型:继承std::exception,实现自定义错误信息。

示例

// 内置类型异常
try {
    throw "error message"; // const char*类型
} catch (const char* msg) {
    std::cout << msg << std::endl;
}

// 标准异常
try {
    throw std::logic_error("logical error");
} catch (const std::logic_error& e) {
    std::cout << e.what() << std::endl;
}

3.2 异常传播(调用栈展开)

异常抛出后,若当前函数没有匹配的catch块,异常会沿调用栈向上传播,直到被捕获或程序终止:

void func3() {
    throw std::runtime_error("error from func3"); // 抛出异常
}

void func2() {
    func3(); // 未捕获异常,继续传播
}

void func1() {
    func2(); // 未捕获异常,继续传播
}

int main() {
    try {
        func1(); // 异常传播到main函数
    } catch (const std::exception& e) {
        std::cout << "捕获异常:" << e.what() << std::endl;
    }
    return 0;
}

3.3 未捕获异常(std::terminate)

若异常传播至main函数仍未被捕获,系统调用std::terminate终止程序(默认调用std::abort):

void func() {
    throw "未捕获的异常";
}

int main() {
    func(); // 异常未被捕获,调用std::terminate
    return 0;
}

3.4 异常对象的生命周期

异常对象由throw表达式创建,生命周期持续到最后一个捕获它的catch块结束

std::string func() {
    try {
        throw std::string("exception object");
    } catch (std::string& s) {
        s = "modified"; // 修改异常对象
        throw; // 重新抛出异常(保持原始类型)
    }
}

int main() {
    try {
        func();
    } catch (std::string& s) {
        std::cout << s << std::endl; // 输出"modified"
    }
    return 0;
}

四、栈展开(Stack Unwinding)

4.1 栈展开的过程

当异常抛出时,编译器自动执行以下操作:

  1. 销毁当前函数中从throw点到函数入口之间的所有自动对象(按构造逆序)。
  2. 返回到调用函数,重复步骤1,直到找到匹配的catch块。
  3. 若找到catch块,执行该块代码;否则继续传播。

4.2 局部对象的析构

栈展开过程中,所有自动存储期对象的析构函数会被调用,确保资源释放:

class MyObject {
public:
    ~MyObject() { std::cout << "MyObject destroyed" << std::endl; }
};

void func() {
    MyObject obj; // 自动对象
    throw std::runtime_error("error"); // 抛出异常,obj的析构函数被调用
}

int main() {
    try {
        func();
    } catch (...) {}
    // 输出:MyObject destroyed
    return 0;
}

4.3 栈展开的示例(代码+图解)

代码

void func3() {
    MyObject obj3;
    throw std::runtime_error("from func3");
}

void func2() {
    MyObject obj2;
    func3();
}

void func1() {
    MyObject obj1;
    func2();
}

int main() {
    try {
        func1();
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

栈展开顺序

  1. func3抛出异常,销毁obj3 → 调用obj3.~MyObject()
  2. 返回func2,销毁obj2 → 调用obj2.~MyObject()
  3. 返回func1,销毁obj1 → 调用obj1.~MyObject()
  4. 返回maincatch块捕获异常。

输出

MyObject destroyed (obj3)
MyObject destroyed (obj2)
MyObject destroyed (obj1)
from func3

五、异常安全(Exception Safety)

5.1 异常安全的三个级别

5.1.1 基本保证(Basic Guarantee)

异常抛出后:

  • 程序状态保持一致(不崩溃、资源不泄漏)。
  • 对象 invariants(不变式)被维护。
  • 但具体状态可能不确定。

示例

void basic_guarantee(std::vector<int>& v, int index, int value) {
    if (index >= v.size()) {
        throw std::out_of_range("index");
    }
    v[index] = value; // 若抛出异常(如value的构造函数),v可能被部分修改
}
5.1.2 强保证(Strong Guarantee)

异常抛出后:

  • 程序状态完全回滚到异常抛出前的状态(仿佛操作从未执行)。
  • 要么操作成功,要么没有任何副作用。

示例(使用copy-and-swap)

void strong_guarantee(std::vector<int>& v, int index, int value) {
    std::vector<int> temp(v); // 拷贝副本
    temp[index] = value; // 在副本上操作
    v.swap(temp); // 交换,无异常风险
}
5.1.3 不抛出保证(No-Throw Guarantee)

函数绝不抛出异常,无论输入或环境如何。

示例

void no_throw_guarantee(int& a, int& b) noexcept {
    std::swap(a, b); // std::swap是noexcept的
}

5.2 RAII与异常安全

RAII(资源获取即初始化) 是实现异常安全的核心技术,通过对象生命周期管理资源:

// RAII封装文件资源
class File {
public:
    File(const char* filename) : _fd(fopen(filename, "r")) {
        if (!_fd) throw std::runtime_error("open failed");
    }
    ~File() { if (_fd) fclose(_fd); } // 确保关闭
private:
    FILE* _fd;
};

void func() {
    File file("data.txt"); // RAII对象,异常抛出时自动关闭文件
    // ... 可能抛出异常的操作 ...
}

5.3 编写异常安全的代码

  • 优先使用RAII:如std::unique_ptrstd::lock_guard
  • 使用copy-and-swap:实现强异常安全。
  • 避免在构造函数中分配多个资源:可使用工厂函数。
  • 析构函数不抛出异常:确保栈展开时资源释放。

六、标准异常库

6.1 std::exception基类

C++标准库异常均继承自std::exception,该类提供what()方法返回错误描述:

class exception {
public:
    virtual const char* what() const noexcept; // 返回错误信息
    virtual ~exception() noexcept;
};

6.2 常见派生异常类

6.2.1 逻辑错误(std::logic_error)

表示程序逻辑错误(编译期可检测):

  • std::domain_error:参数域错误。
  • std::invalid_argument:无效参数。
  • std::length_error:长度超出范围。
  • std::out_of_range:索引越界(如vector::at)。
6.2.2 运行时错误(std::runtime_error)

表示运行时错误(编译期不可检测):

  • std::range_error:数值超出有效范围。
  • std::overflow_error:算术上溢。
  • std::underflow_error:算术下溢。
  • std::system_error:系统调用错误(如IO错误)。

6.3 标准异常的使用示例

#include <vector>
#include <stdexcept>

int main() {
    std::vector<int> v(5);
    try {
        v.at(10) = 42; // 抛出std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << std::endl;
    }

    try {
        throw std::invalid_argument("invalid argument");
    } catch (const std::invalid_argument& e) {
        std::cout << "invalid_argument: " << e.what() << std::endl;
    }
    return 0;
}

七、自定义异常

7.1 继承std::exception

自定义异常应继承std::exception,并重写what()方法:

#include <exception>
#include <string>

class MyException : public std::exception {
private:
    std::string _msg;
public:
    MyException(const std::string& msg) : _msg(msg) {}
    // 重写what(),返回错误信息
    const char* what() const noexcept override {
        return _msg.c_str();
    }
};

7.2 重写what()方法

what()方法应返回C风格字符串,且不抛出异常(使用noexcept):

class NetworkException : public std::exception {
private:
    std::string _msg;
public:
    NetworkException(int code, const std::string& msg) {
        _msg = "Error " + std::to_string(code) + ": " + msg;
    }
    const char* what() const noexcept override {
        return _msg.c_str();
    }
};

7.3 自定义异常的抛出与捕获

void connect() {
    throw NetworkException(404, "connection failed");
}

int main() {
    try {
        connect();
    } catch (const NetworkException& e) {
        std::cout << e.what() << std::endl; // 输出"Error 404: connection failed"
    } catch (const std::exception& e) {
        // 处理其他异常
    }
    return 0;
}

八、异常与构造函数/析构函数

8.1 构造函数抛出异常

构造函数抛出异常表示对象未完全构造,其析构函数不会被调用

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructed" << std::endl;
        throw std::runtime_error("construction failed");
    }
    ~MyClass() { std::cout << "MyClass destroyed" << std::endl; }
};

int main() {
    try {
        MyClass obj; // 构造函数抛出异常
    } catch (...) {}
    // 输出:MyClass constructed(析构函数未被调用)
    return 0;
}

8.2 析构函数不应抛出异常

析构函数抛出异常会导致程序终止(栈展开过程中遇到第二个异常):

class BadClass {
public:
    ~BadClass() noexcept(false) { // 析构函数抛出异常(危险)
        throw "destructor exception";
    }
};

int main() {
    try {
        BadClass obj;
        throw "main exception"; // 第一个异常
    } catch (...) {}
    // 析构函数抛出第二个异常,调用std::terminate
    return 0;
}

8.3 示例:构造函数异常导致资源泄漏

若构造函数中分配多个资源,部分资源可能因异常未释放:

class Resource {
public:
    Resource(int id) : _id(id) {
        std::cout << "Resource " << _id << " acquired" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource " << _id << " released" << std::endl;
    }
private:
    int _id;
};

class ResourceUser {
public:
    ResourceUser() : _res1(1) {
        // _res1构造成功,_res2构造前抛出异常
        throw std::runtime_error("error");
        Resource _res2(2); // 未执行
    }
private:
    Resource _res1; // 构造成功
    Resource _res2; // 未构造
};

int main() {
    try {
        ResourceUser user;
    } catch (...) {}
    // 输出:Resource 1 acquired(_res1未释放,内存泄漏)
    return 0;
}

解决:使用RAII和成员初始化列表顺序:

class ResourceUser {
public:
    ResourceUser() : _res2(2), _res1(1) {} // 按声明顺序初始化,而非列表顺序
private:
    Resource _res1; // 先声明,后初始化
    Resource _res2; // 后声明,先初始化
};

九、noexcept关键字

9.1 noexcept的语法与作用

noexcept用于指定函数是否可能抛出异常

// 函数不抛出异常
void safe_func() noexcept {
    // ... 不抛出异常的代码 ...
}

// 函数可能抛出异常(默认)
void risky_func() noexcept(false) {
    throw std::runtime_error("risky");
}

9.2 noexcept操作符

noexcept(表达式)根据表达式是否可能抛出异常返回truefalse

void func() noexcept;
void func2();

constexpr bool b1 = noexcept(func()); // true
constexpr bool b2 = noexcept(func2()); // false

9.3 noexcept与函数重载

noexcept可作为函数重载的区分条件:

void print(int x) noexcept {
    std::cout << "noexcept version: " << x << std::endl;
}

void print(int x) {
    std::cout << "throwing version: " << x << std::endl;
}

int main() {
    print(42); // 调用哪个版本?(歧义,编译错误)
    return 0;
}

9.4 noexcept的优化作用

编译器可对noexcept函数进行优化(如省略异常处理代码):

// 向量移动构造函数在元素类型为noexcept时可优化
std::vector<MyNoexceptType> v1;
std::vector<MyNoexceptType> v2 = std::move(v1); // 高效移动

std::vector<MyThrowingType> v3;
std::vector<MyThrowingType> v4 = std::move(v3); // 可能回退到拷贝

十、异常的优缺点

10.1 优点(相比错误码)

  • 分离错误处理与正常逻辑:代码更清晰。
  • 强制处理错误:未处理异常会终止程序,避免错误被忽略。
  • 传播性:深层调用的错误可被上层统一处理。
  • 丰富的错误信息:异常对象可携带详细信息。

10.2 缺点

  • 性能开销:异常处理可能增加代码大小和运行时开销。
  • 控制流跳转:异常跳转可能使调试困难。
  • 异常安全复杂:编写异常安全代码难度高。
  • 兼容性:与不使用异常的代码(如C语言)集成困难。

十一、异常处理最佳实践

11.1 只抛出异常对象,不抛出指针

抛出指针需手动管理内存,易导致泄漏:

// 错误:抛出指针
throw new std::runtime_error("error"); // 需手动delete

// 正确:抛出对象
throw std::runtime_error("error"); // 自动管理生命周期

11.2 捕获具体类型,避免过度使用catch(…)

catch(...)会捕获所有异常,但无法获取错误信息,可能隐藏bug:

try {
    // ...
} catch (const std::out_of_range& e) { // 捕获具体类型
    // 处理越界错误
} catch (const std::exception& e) { // 捕获其他已知异常
    // 处理标准异常
} // 不使用catch(...),让未知异常终止程序,便于调试

11.3 使用RAII确保资源释放

RAII是异常安全的基石,避免手动释放资源:

// 错误:手动管理资源
void bad() {
    int* ptr = new int;
    risky_operation(); // 异常可能导致内存泄漏
    delete ptr;
}

// 正确:RAII管理资源
void good() {
    std::unique_ptr<int> ptr(new int); // RAII
    risky_operation(); // 异常时自动释放
}

11.4 异常规格说明的使用

  • C++11前throw(type1, type2)指定可能抛出的异常类型(已弃用)。
  • C++11后:使用noexcept指定是否抛出异常。

十二、常见误区与注意事项

12.1 析构函数抛出异常

析构函数抛出异常会导致程序终止,应在析构函数内捕获所有异常:

class SafeClass {
public:
    ~SafeClass() {
        try {
            risky_cleanup(); // 可能抛出异常的清理操作
        } catch (...) {
            // 记录错误,不传播异常
            std::cerr << "cleanup failed" << std::endl;
        }
    }
};

12.2 捕获所有异常(catch(…))的风险

catch(...)会捕获包括std::bad_alloc在内的所有异常,可能导致程序在不可恢复状态下继续运行:

try {
    // ...
} catch (...) {
    // 危险:未处理根本错误(如内存耗尽)
    std::cout << "something went wrong" << std::endl;
}

12.3 异常导致的资源泄漏

未使用RAII的代码在异常抛出时可能泄漏资源:

void leak() {
    FILE* file = fopen("data.txt", "r");
    if (!file) return;
    throw "error"; // 文件未关闭,资源泄漏
    fclose(file);
}

12.4 异常与多线程

异常不能跨线程传播,每个线程需独立处理异常:

#include <thread>

void thread_func() {
    try {
        throw std::runtime_error("thread error");
    } catch (const std::exception& e) {
        std::cout << "thread caught: " << e.what() << std::endl;
    }
}

int main() {
    std::thread t(thread_func);
    t.join(); // 异常在子线程内处理
    return 0;
}

以上内容,全面覆盖了C++异常处理的核心知识点,包括语法、栈展开、异常安全、标准异常库、自定义异常等。合理使用异常可提高代码的健壮性和可读性,但需注意异常安全和最佳实践,避免常见误区。

Logo

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

更多推荐