c++的异常机制

为什么需要 C++ 异常

C 语言的错误处理方式存在明显缺陷,这也是 C++ 引入异常的原因。

C 语言传统错误处理方式

  1. **终止程序(如 assert)**直接终止程序,用户体验极差,适用于致命错误(如内存错误),但无法恢复。

    复制代码
    #include <assert.h>
    int Division(int a, int b) {
        assert(b != 0); // b为0时直接终止程序,打印断言信息
        return a / b;
    }
  2. 返回错误码函数返回特定值表示错误(如 - 1、errno),但需程序员手动检查,且深层调用需层层传递错误码,代码冗余。

    复制代码
    #include <errno.h>
    #include <stdio.h>
    int Division(int a, int b) {
        if (b == 0) {
            errno = EINVAL; // 设置错误码
            return -1;
        }
        return a / b;
    }
    int Func() {
        int ret = Division(10, 0);
        if (ret == -1) return -1; // 需手动传递错误码
        return ret;
    }
    int main() {
        if (Func() == -1) {
            printf("错误:%d\n", errno); // 需手动解析错误码
        }
        return 0;
    }

C++ 异常的优势

  • 错误信息更丰富:可抛出任意类型对象(包含详细错误描述);
  • 无需层层传递错误码:异常直接跳转到最近的匹配 catch,简化代码;
  • 适配特殊场景:构造函数无返回值,无法用错误码,异常是唯一选择。

C++ 异常核心概念与基础用法

异常的核心是「抛出 - 捕获」机制,依赖三个关键字:trythrowcatch

1. 核心概念

  • try :包裹可能抛出异常的代码(保护代码块),后续必须跟一个或多个catch
  • throw:当检测到错误时,抛出异常对象(触发异常机制);
  • catch :捕获特定类型的异常并处理,catch(...)可捕获任意类型异常(兜底)。

2. 基础语法框架

复制代码
#include <iostream>
using namespace std;

int main() {
    try {
        // 保护代码:可能抛出异常的逻辑
        int a = 10, b = 0;
        if (b == 0) {
            throw "除0错误!"; // 抛出字符串类型异常
        }
        cout << a / b << endl;
    }
    catch (const char* errmsg) { // 捕获字符串类型异常
        cout << "捕获异常:" << errmsg << endl;
    }
    catch (...) { // 兜底捕获:处理所有未匹配的异常
        cout << "捕获未知异常" << endl;
    }
    return 0;
}

3. 运行结果

复制代码
捕获异常:除0错误!

异常的抛出与捕获规则

1. 核心规则

规则 1:异常类型决定匹配的 catch

抛出的异常对象类型,需与 catch 的参数类型完全匹配(或派生类匹配基类,后续讲)。

复制代码
#include <iostream>
using namespace std;

void Func() {
    throw 10; // 抛出int类型异常
}

int main() {
    try {
        Func();
    }
    catch (int err) { // 匹配int类型,触发
        cout << "捕获int异常:" << err << endl;
    }
    catch (double err) { // 不匹配,跳过
        cout << "捕获double异常:" << err << endl;
    }
    catch (...) { // 兜底,若前面无匹配则触发
        cout << "未知异常" << endl;
    }
    return 0;
}

运行结果捕获int异常:10

规则 2:栈展开机制(调用链查找)

若抛出异常的位置不在try块,或当前try无匹配的catch,则退出当前函数栈,向上层调用链查找,直到找到匹配的catch或到达main函数(未找到则终止程序)。

复制代码
#include <iostream>
using namespace std;

void Func1() {
    cout << "Func1:抛出异常" << endl;
    throw "Func1的错误"; // 抛出异常
}

void Func2() {
    cout << "Func2:调用Func1" << endl;
    Func1(); // 调用Func1,未处理异常
}

void Func3() {
    cout << "Func3:调用Func2" << endl;
    Func2(); // 调用Func2,未处理异常
}

int main() {
    try {
        cout << "main:调用Func3" << endl;
        Func3();
    }
    catch (const char* errmsg) { // 最终在此捕获
        cout << "main捕获异常:" << errmsg << endl;
    }
    return 0;
}

运行结果

复制代码
main:调用Func3
Func3:调用Func2
Func2:调用Func1
Func1:抛出异常
main捕获异常:Func1的错误
异常对象的拷贝与销毁

抛出的异常对象会生成一个临时拷贝(类似函数传值返回),捕获后原临时对象会销毁。

复制代码
#include <iostream>
using namespace std;

class Error {
public:
    Error() { cout << "Error构造" << endl; }
    Error(const Error&) { cout << "Error拷贝构造" << endl; }
    ~Error() { cout << "Error析构" << endl; }
};

int main() {
    try {
        throw Error(); // 抛出临时对象,触发拷贝
    }
    catch (Error e) { // 捕获拷贝后的对象
        cout << "捕获Error异常" << endl;
    }
    return 0;
}

运行结果

复制代码
Error构造
Error拷贝构造
捕获Error异常
Error析构
Error析构

异常高级用法

1. 异常的重新抛出

单个catch无法完全处理异常时,可通过throw;重新抛出,交给上层函数处理(常用于资源释放)。

场景 :函数内new了资源,抛出异常前需先释放资源,再重新抛异常。

复制代码
#include <iostream>
#include <functional>
using namespace std;

double Division(int a, int b) {
    if (b == 0) {
        throw "除0错误"; // 抛出异常
    }
    return (double)a / b;
}

void Func() {
    int* arr = new int[10]; // 申请堆内存
    try {
        int len = 10, time = 0;
        cout << Division(len, time) << endl; // 触发除0异常
    }
    catch (...) { // 捕获所有异常
        cout << "Func:释放arr内存" << endl;
        delete[] arr; // 释放资源
        throw; // 重新抛出异常,交给上层处理
    }
    // 正常执行时也需释放资源
    delete[] arr;
}

int main() {
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << "main捕获异常:" << errmsg << endl;
    }
    return 0;
}

运行结果

复制代码
Func:释放arr内存
main捕获异常:除0错误

异常安全(避免资源泄漏)

异常可能导致资源泄漏(如newdelete、锁未释放),需遵守以下原则:

  • 构造函数:避免抛异常(可能导致对象初始化不完整);
  • 析构函数:避免抛异常(可能导致资源释放失败);
  • RAII(资源获取即初始化)管理资源(如智能指针、锁封装类)。

反例:构造函数抛异常导致内存泄漏

复制代码
class A {
public:
    A() {
        _p = new int[10];
        throw "构造失败"; // 抛出异常,_p未释放
    }
    ~A() {
        delete[] _p; // 构造异常时,析构函数不会执行!
    }
private:
    int* _p;
};

int main() {
    try {
        A a;
    }
    catch (const char* err) {
        cout << err << endl; // 输出"构造失败",但_p内存泄漏
    }
    return 0;
}

异常规范(明确函数异常行为)

异常规范用于告知函数使用者:该函数可能抛出哪些异常(C++98)或是否抛出异常(C++11)。

(1)C++98 语法(逐渐被淘汰)
  • throw(类型列表):函数仅抛出列表中的异常;

  • throw():函数不抛出任何异常。

    // 仅抛出int或const char类型异常
    void Func1() throw(int, const char
    );
    // 不抛出任何异常
    void Func2() throw();

(2)C++11 noexcept(推荐)

noexcept 表示函数不会抛出异常 ,编译器会优化代码(比throw()更高效)。

复制代码
// 明确表示不会抛异常
void Func() noexcept {
    cout << "不会抛异常的函数" << endl;
}

// 移动构造函数常用noexcept
class MyString {
public:
    MyString(MyString&& other) noexcept {
        // 移动资源,不抛异常
    }
};

自定义异常体系(工程实践)

实际项目中,需统一异常类型(避免混乱),通常设计一套继承体系 :基类Exception,派生类对应不同业务异常(如 SQL 异常、缓存异常),利用多态捕获。

完整示例(服务器开发常用)

复制代码
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std;

// 异常基类
class Exception {
public:
    Exception(string errmsg, int id) 
        : _errmsg(errmsg), _id(id) {}
    
    // 虚函数:支持多态
    virtual string what() const {
        return "错误ID:" + to_string(_id) + ",错误信息:" + _errmsg;
    }
protected:
    string _errmsg;
    int _id;
};

// SQL异常(派生类)
class SqlException : public Exception {
public:
    SqlException(string errmsg, int id, string sql)
        : Exception(errmsg, id), _sql(sql) {}
    
    virtual string what() const override {
        return "SqlException:" + Exception::what() + ",SQL语句:" + _sql;
    }
private:
    string _sql;
};

// 缓存异常(派生类)
class CacheException : public Exception {
public:
    CacheException(string errmsg, int id)
        : Exception(errmsg, id) {}
    
    virtual string what() const override {
        return "CacheException:" + Exception::what();
    }
};

// HTTP服务异常(派生类)
class HttpException : public Exception {
public:
    HttpException(string errmsg, int id, string method)
        : Exception(errmsg, id), _method(method) {}
    
    virtual string what() const override {
        return "HttpException:" + Exception::what() + ",请求方法:" + _method;
    }
private:
    string _method;
};

// 模拟SQL操作
void SqlOperate() {
    srand(time(0));
    if (rand() % 7 == 0) {
        throw SqlException("权限不足", 100, "select * from user where name='张三'");
    }
}

// 模拟缓存操作
void CacheOperate() {
    srand(time(0));
    if (rand() % 5 == 0) {
        throw CacheException("数据不存在", 200);
    }
    SqlOperate();
}

// 模拟HTTP服务
void HttpServer() {
    srand(time(0));
    if (rand() % 3 == 0) {
        throw HttpException("资源不存在", 300, "GET");
    }
    CacheOperate();
}

int main() {
    while (1) {
        this_thread::sleep_for(chrono::seconds(1)); // 每秒执行一次
        try {
            HttpServer();
            cout << "服务正常运行" << endl;
        }
        catch (const Exception& e) { // 多态捕获:仅需捕获基类
            cout << e.what() << endl;
        }
        catch (...) { // 兜底捕获未知异常
            cout << "未知异常" << endl;
        }
        cout << "------------------------" << endl;
    }
    return 0;
}

运行结果(示例)

复制代码
HttpException:错误ID:300,错误信息:资源不存在,请求方法:GET
------------------------
服务正常运行
------------------------
SqlException:错误ID:100,错误信息:权限不足,SQL语句:select * from user where name='张三'
------------------------

C++ 标准库异常体系

C++ 标准库提供了一套预定义异常(定义在<exception>头文件),所有异常均继承自std::exception基类,核心子类如下:

异常类 用途
std::bad_alloc 内存分配失败(如new失败)
std::out_of_range 越界访问(如vector::at()
std::invalid_argument 无效参数(如atoi("abc")
std::bad_cast 动态类型转换失败(如dynamic_cast

代码示例:捕获标准库异常

复制代码
#include <iostream>
#include <vector>
#include <exception>
using namespace std;

int main() {
    try {
        // 1. 内存分配失败(模拟)
        vector<int> v;
        v.reserve(1000000000000); // 申请超大内存,触发bad_alloc
        
        // 2. 越界访问(若上面未触发,执行此句)
        // vector<int> v(10);
        // v.at(10) = 100; // at()会抛out_of_range
    }
    catch (const std::out_of_range& e) {
        cout << "越界异常:" << e.what() << endl;
    }
    catch (const std::bad_alloc& e) {
        cout << "内存分配异常:" << e.what() << endl;
    }
    catch (const std::exception& e) { // 捕获所有标准库异常
        cout << "标准库异常:" << e.what() << endl;
    }
    return 0;
}

运行结果

复制代码
内存分配异常:std::bad_alloc

执行流的变动

异常处理的核心逻辑是「跳转执行」而非「恢复执行」------ 当异常被成功捕获并处理后,程序会从最后一个匹配的catch块之后的代码继续执行,而不会回到抛出异常的原始位置(原位置的执行环境已被栈展开破坏)。

步骤 1:异常抛出,触发栈展开(离开原执行位置)

throw语句执行时,程序会立即终止当前函数的执行,且不会回到throw之后的代码。随后启动「栈展开」:退出当前函数栈,向上层调用链查找匹配的catch块(期间会销毁沿途函数的局部变量)。

步骤 2:找到匹配的catch,执行异常处理

找到第一个类型匹配的catch块后,执行其中的处理逻辑(如打印错误、释放资源)。

步骤 3:处理完成,从catch之后继续执行

异常处理完毕后,程序会跳过所有剩余的catch块,直接执行catch块后面的代码 ,永远不会回到throw语句所在的原始位置。

直观验证执行流

我们基于Division除 0 异常例子,添加执行痕迹,清晰展示执行流走向:

复制代码
#include <iostream>
using namespace std;

double Division(int a, int b) {
    cout << "Division:开始执行(a=" << a << ", b=" << b << ")" << endl;
    if (b == 0) {
        throw "除0错误!"; // 异常抛出点(执行流从此处跳出Division函数)
        // 👇 以下代码永远不会执行(throw后函数已终止)
        cout << "Division:throw之后的代码(永远不会运行)" << endl;
    }
    return (double)a / b;
}

void Func() {
    cout << "Func:开始执行" << endl;
    int len = 10, time = 0;
    cout << "Func:调用Division" << endl;
    // 👇 调用Division时抛出异常,Func函数立即终止,不会执行后续代码
    cout << Division(len, time) << endl;
    // 👇 以下代码永远不会执行(异常导致Func提前退出)
    cout << "Func:Division调用后的代码(永远不会运行)" << endl;
}

int main() {
    cout << "main:程序开始" << endl;
    try {
        cout << "main:进入try块,调用Func" << endl;
        Func(); // 调用Func,触发异常
        // 👇 以下代码永远不会执行(异常导致try块内后续代码跳过)
        cout << "main:Func调用后的代码(永远不会运行)" << endl;
    }
    catch (const char* errmsg) {
        // 👇 异常处理逻辑
        cout << "main:捕获异常:" << errmsg << endl;
    }
    catch (...) {
        cout << "main:捕获未知异常" << endl;
    }
    // 👇 异常处理完后,从这里继续执行(核心!)
    cout << "main:异常处理完成,程序继续执行(不会回原抛出点)" << endl;
    return 0;
}

运行结果(关键执行流标记):

复制代码
main:程序开始
main:进入try块,调用Func
Func:开始执行
Func:调用Division
Division:开始执行(a=10, b=0)
main:捕获异常:除0错误!
main:异常处理完成,程序继续执行(不会回原抛出点)

关键观察:

  1. Divisionthrow之后的代码(cout << "Division:throw之后的代码")完全没执行;
  2. FuncDivision调用之后的代码(cout << "Func:Division调用后的代码")完全没执行;
  3. maintry块中Func调用之后的代码(cout << "main:Func调用后的代码")完全没执行;
  4. 最终执行流停在catch块之后的代码(main:异常处理完成...)。

C++ 异常的优缺点

优点

  1. 错误信息更清晰:异常对象可包含详细信息(如错误 ID、SQL 语句、堆栈),便于调试;
  2. 无需层层传递错误码 :异常直接跳转到匹配的catch,简化深层调用链的错误处理;
  3. 适配特殊场景 :构造函数、operator[]等无返回值的函数,只能用异常处理错误;
  4. 兼容第三方库:Boost、GTest 等主流库均使用异常,使用这些库需支持异常。

缺点

  1. 执行流混乱:异常会打断正常执行流,调试时难以跟踪代码逻辑;
  2. 性能开销:异常的栈展开和拷贝会有少量性能损耗(现代硬件可忽略);
  3. 资源泄漏风险 :若未正确释放资源(如new、锁),容易导致内存泄漏(需用 RAII 解决);
  4. 标准库异常体系不完善:不同项目可能自定义异常体系,缺乏统一标准;
  5. 学习成本高:需掌握异常规则、异常安全、RAII 等知识点。

最佳实践

  • 统一异常体系:所有异常继承自同一个基类;
  • 明确异常规范:用noexcept标记不抛异常的函数;
  • 优先使用 RAII:用智能指针、锁封装类管理资源,避免泄漏;
  • 兜底捕获:main函数中必须加catch(...),防止程序意外终止。

总结

C++ 异常是一把 "双刃剑",虽有学习成本和潜在风险,但在工程实践中利大于弊,是处理错误的主流方式。核心是:规范异常类型、保证异常安全、合理使用 try/catch/throw,结合 RAII 机制,可大幅提升代码的健壮性和可维护性。

相关推荐
小豪GO!2 小时前
操作系统-八股
java
爱吃山竹的大肚肚2 小时前
达梦(DM)数据库中设置表空间
java·数据库·sql·mysql·spring·spring cloud·oracle
萧曵 丶2 小时前
JVM 虚拟机类加载机制浅谈
jvm
程序猿编码2 小时前
无状态TCP技术:DNS代理的轻量级实现逻辑与核心原理(C/C++代码实现)
c语言·网络·c++·tcp/ip·dns
Geek攻城猫2 小时前
Java 实现大文件上传与断点续传:原理、实践与优化
java
Kratzdisteln2 小时前
【1902】自适应学习系统 - 完整技术方案
java·python·学习
期货资管源码2 小时前
智星期货资管子账户软件pc端开发技术栈的选择
c语言·数据结构·c++·vue
Dream it possible!2 小时前
蓝桥杯_工作时长_C++
c++·蓝桥杯·竞赛
讳疾忌医丶2 小时前
C++中虚函数调用慢5倍?深入理解vtable和性能开销
开发语言·c++