异常的概念及使用

1.异常的概念

C 语言错误处理:

  • 函数出错时,返回一个数字(错误码)
  • 或者用全局变量 errno

C++ 异常的设计目标:

  • 出错地方不立即处理,沿调用链往上抛
  • 抛出一个对象,携带完整错误信息(编号 + 描述 + 现场)
  • 检测与处理分离,代码更清晰

2.异常基本语法:throw /try/catch

2.1抛出异常:throw

  • throw 抛出任意类型(int /string/ 自定义对象)
  • 抛出后执行流直接跳转,throw 后面代码不再执行
throw string("除零错误");

//除零错误
int a = 10;
int b = 0;

if (b == 0)
{
    // 除数是0,非法!
    // 主动报错,抛出异常
    throw string("除数不能为0,除零错误!");
}

// 下面这句 a / b 根本不会走到!
int res = a / b;
为什么 b==0 就要抛异常?
数学里:
10 ÷ 0 本身就是非法运算,电脑算不出来,程序直接崩
所以我们不能真的执行 a/b
一旦执行 10/0 程序直接死机崩溃
所以我们提前判断:
if(b==0) → 发现除数是 0
→ 主动抛出异常,告诉上层:这里出错了,不能算


//抛 int 数字异常
int num = -1;
if (num < 0)
{
    throw 1001;  // 抛出数字错误码
}
cout << "正常逻辑代码" << endl;
规定:num 不能是负数
发现 num<0 不符合规则
直接 throw 1001 扔一个数字错误编号出去
throw 一执行,下面 cout 永远不运行

//抛 string 字符串异常
throw string("参数非法错误");
cout << "不会执行" << endl;
一上来就出错,直接扔一段中文文字说明错误
不用查表,一看就知道哪里错
扔完之后,后面代码全部作废



//抛自定义类对象异常
class MyError
{
public:
    string msg;
    MyError(string m) : msg(m) {}
};

throw MyError("自定义运行异常");
cout << "不会执行" << endl;
自己写一个异常类
可以存:错误编号、错误原因、出错位置、时间…… 超多信息
直接扔整个对象出去,携带信息最全
同样:throw 之后后面代码绝不执行

2.2 捕获异常:try + catch

try {
    // 可能出错的代码
}
catch (string err) {
    // 处理错误
}


#include <iostream>
using namespace std;

int main()
{
    int num = -1;

    try
    {
        if(num < 0)
        {
            throw 1001;
        }
        cout << "正常数字" << endl;
    }
    catch(int err)
    {
        cout << "错误编号:" << err << endl;
    }

    return 0;
}

try:放可能出错的代码
throw:抛出错误
catch:接住并处理错误

2.3 捕获规则(重点)

  1. 类型必须匹配
  2. 沿调用链向上查找,找到最近且匹配的 catch
  3. 找到就执行 catch,之后程序继续正常运行
  4. 没找到 → 程序终止(闪退)

3.栈展开

异常抛出后,并不是 “直接飞” 到 catch,而是:

  1. 暂停当前函数
  2. 逐层退出函数
  3. 每层局部对象正常析构
  4. 直到找到匹配 catch

这叫 栈展开(Stack Unwinding)

注意:

  • 栈展开不会内存泄漏
  • 局部对象会被正确析构
  • 动态分配的内存(new/malloc)不会自动释放(重点坑

看例子:

#include <iostream>
#include <string>
using namespace std;

// 最内层函数:抛出异常
void func1()
{
    throw string("func1 出现除零错误!");
}

// 第二层:调用func1,自己不处理异常
void func2()
{
    func1();
}

// 第三层:调用func2,自己不处理异常
void func3()
{
    func2();
}

// 最顶层main:统一捕获处理所有异常
int main()
{
    try
    {
        func3();
    }
    catch(string err)
    {
        // 一路从func1跑上来,在这里接住
        cout << "main捕获到异常:" << err << endl;
    }

    cout << "异常处理完毕,程序正常结束" << endl;
    return 0;
}

执行过程

  • main 运行 → 调用 func3
  • func3 运行 → 调用 func2
  • func2 运行 → 调用 func1
  • func1 执行 throw,抛出异常
  • func1 没有 catch → 函数立刻结束,异常往上走
  • func2 没有 catch → 函数立刻结束,异常继续往上走
  • func3 没有 catch → 函数立刻结束,异常继续往上走
  • ✅ 跑到mainmain有匹配 catch → 处理错误
  • 程序正常往下运行,不会崩溃

4.查找匹配的处理代码

4.1异常常规匹配规则

  • 默认情况下:throw 异常类型 和 catch 类型必须严格匹配
  • 多个 catch 匹配时,优先选择离抛出位置最近的 catch 语句
  • 异常沿着函数调用链逐层向上栈展开查找

4.2异常类型特殊转换(允许匹配例外)

C++ 异常不是死匹配,支持几种合法类型转换:

  1. 非常量类型 → 常量类型转换(权限缩小)
  2. 数组类型 → 对应数组指针
  3. 函数类型 → 函数指针
#include <iostream>
#include <string>
using namespace std;

// 基类
class Base {
public:
    virtual void show() { cout << "Base异常" << endl; }
};

// 派生类
class Derive : public Base {
public:
    void show() override { cout << "Derive异常" << endl; }
};

int main() {
    try {
        // 1. 抛 派生类对象
        throw Derive();
    }
    // 2. 基类能接住(派生类→基类 允许转换)
    catch (const Base& e) {
        e.show();
    }
    // 3. 万能兜底:捕获所有未知异常
    catch (...) {
        cout << "未知异常" << endl;
    }
}

/////////////////////
throw 类型和 catch 类型可以不是严格匹配
这里 throw Derive ()
catch (Base&) 能接住 ✔
支持派生类 → 基类转换(最重要)
子类异常,父类能捕获 ✔
catch (...) 万能兜底
任何异常都能接住,防止程序崩溃 ✔

4.3万能捕获 catch (...)

  • 可以捕获任意类型的异常,没有类型限制
  • 缺点:无法获取异常具体信息,不知道错在哪
  • 作用:放在所有 catch 最后做兜底,避免异常一路跑到 main 外导致程序直接终止崩溃

例子:

#include <iostream>
using namespace std;

int main() {
    try {
        throw 123;
    }
    catch (int a) {
        cout << "捕获int:" << a << endl;
    }
    catch (...) {
        cout << "捕获所有其他异常" << endl;
    }
}

5.C++ 异常重新抛出 throw

5.1什么是异常重新抛出

抓到异常以后分两种情况:

  1. 临时可恢复错误(网络卡顿):本地重试处理
  2. 无法修复错误(参数非法):不用处理,直接向上交给外层

语法格式:只写 throw; 后面不加任何东西

作用:把当前捕获到的异常,原封不动、原样继续向上传递

例子:

#include <iostream>
#include <string>
using namespace std;

// 异常基类
class Err
{
public:
    int code;
    Err(int c) : code(c) {}
    virtual string msg() { return "错误"; }
};

// 余额不足:可以重试换金额
class MoneyErr : public Err
{
public:
    MoneyErr() : Err(1) {}
    string msg() override { return "余额不足,可更换金额重试"; }
};

// 银行卡冻结:没法处理,直接上报
class CardErr : public Err
{
public:
    CardErr() : Err(2) {}
    string msg() override { return "银行卡冻结,无法取款"; }
};

// 底层:取钱操作
void getMoney()
{
    int type = rand() % 2;
    if(type == 0)
        throw MoneyErr();   // 余额不够
    else
        throw CardErr();    // 卡冻结
}

// 中层:最多重试2次取钱
void bank()
{
    for(int i=0; i<2; i++)
    {
        try
        {
            getMoney();
            cout << "取款成功!" << endl;
            return;
        }
        catch(const Err& e)
        {
            // 余额不足:重试
            if(e.code == 1)
            {
                cout << "第" << i+1 << "次重试取款..." << endl;
                if(i == 1) throw;  // 2次都失败,重新抛出
            }
            else
            {
                throw;  // 卡冻结,直接上交
            }
        }
    }
}

int main()
{
    srand(time(0));
    try
    {
        bank();
    }
    catch(const Err& e)
    {
        cout << "银行最终提示:" << e.msg() << endl;
    }
}
  • 余额不足 → 多试两次
  • 两次还不行 → throw; 往上抛
  • 银行卡冻结 → 根本没法重试,直接 throw; 上交
  • throw; 不带东西 = 原样把错误传给上层

6.异常安全问题

6.1异常安全

异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。

异常安全 = 程序抛异常时,不能丢资源!

资源包括:

  • new 出来的内存
  • 打开的文件
  • 加的锁
  • 连接的数据库
  • 套接字

6.2异常导致的灾难:

先申请资源中间抛异常(后面代码没有执行) → 释放资源的代码没跑到资源泄漏

double Divide(int a , int b)
{
    if (b == 0)
    {
        // 除 0 错误,直接抛异常
        throw "Division by zero condition !";
    }
    return (double)a / (double)b;
}

void Func()
{

    int* array = nullptr;  // 先初始化为空
    try
    {
        // 1. 先申请内存
        array = new int[10];


        int len , time;
        cin >> len >> time;

        // 2. 这里可能抛异常!
        cout << Divide(len , time) << endl;
    }
    catch (...)  // 捕获所有异常
    {
        // 3. 异常来了!必须先释放内存!
        cout << "delete []" << array << endl;
        delete[] array;

        // 4. 释放完,再把异常抛给外层处理
        throw;
    }

    // 正常路径:也会释放
    cout << "delete []" << array << endl;
    delete[] array;
}

执行过程:

6.3异常安全终极方案

RAII = Resource Acquisition Is Initialization(智能指针)

资源获取即初始化

大白话:

  • 把资源交给一个对象管理
  • 构造函数拿资源
  • 析构函数自动释放
  • 离开作用域就自动释放
  • 抛异常也会自动析构 → 绝对不泄漏
#include <iostream>
#include <memory>
using namespace std;

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

void Func() {
    unique_ptr<int[]> array(new int[10]); // 智能指针

    int a, b;
    cin >> a >> b;
    cout << Divide(a, b) << endl; // 这里可能抛异常
}

int main() {
    try {
        Func();
    }
    catch (const char* msg) {
        cout << msg << endl;
    }
    return 0;
}

7.异常规范(noexcept)

7.1 C++98 旧写法(了解)

  • throw() 表示不抛异常
  • throw(int, string) 表示可能抛这些类型
  • 缺点:太麻烦、实际几乎不用

7.2 C++11 新写法(记住)

  • noexcept:表示不会抛异常
  • 不加:表示可能抛异常

7.3 重要规则

  • 编译器不检查你是否真的不抛
  • 如果你加了 noexcept 却抛了异常→ 程序直接崩溃!

7.4 noexcept 运算符

  • noexcept(表达式)
  • 不会抛异常 → 返回 true
  • 会抛异常 → 返回 false
#include <iostream>
using namespace std;

// 1. 声明不抛异常
void func() noexcept
{
    cout << "我承诺不抛异常\n";
    // throw "异常"; // 一旦打开,程序直接崩溃!
}

// 2. 没加 noexcept → 可能抛异常
double Div(int a, int b)
{
    if (b == 0)
        throw "除零错误";
    return (double)a / b;
}

// 3. noexcept 运算符:检测是否会抛异常
int main()
{
    cout << noexcept(func()) << endl;    // 1(不抛)
    cout << noexcept(Div(1,0)) << endl;  // 0(会抛)

    try {
        Div(1, 0);
    }
    catch (const char* msg) {
        cout << msg << endl;
    }
    return 0;
}

更多推荐