humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


C++ 2023 年 7 月 7 日

[C++] C++异常处理介绍 分析: 异常概念、异常抛出与捕获匹配原则、重新抛出、异常安全、异常体系...

C语言程序发生错误, 很可能会直接导致程序退出. 而C++引进了 异常的概念, 可以更灵活更快速的 排查处理错误...

C语言 错误处理方式

在C语言中, 代码发生错误一般会有两种处理方式:
  1. 终止程序.

    比如 直接使用assert()断言. 或者直接崩溃

  2. 返回、设置错误码

    C语言某些函数执行失败, 但是结果不足以导致致命问题时, 就会将错误码设置在errno中. 用户可以通过strerr(errno)来获取错误信息.

但是这些针对错误的处理方式, 不灵活. 严重的错误直接就是崩溃, 没有一点回转的余地. 虽然程序出现问题很可能跟编写有关, 不过还是灵活一些比较好.

C++ 异常

异常概念

由于C语言中针对错误的处理不灵活. 所以C++引入了异常的概念.
异常是什么?
异常是一种处理错误的方式, 当一个函数 发现自己无法处理的错误时就可以 抛出异常, 让函数的直接或间接的 调用者 处理这个错误.
double Division(int a, int b) {
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}

void Func() {
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
}

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

    return 0;
}
这段代码就可以展现出最简单的 抛异常、捕捉异常、处理异常的场景.
那么 这段代码执行会出现什么现象呢?
这段代码, 我们在main()try作用域中调用了Func(), 在Func()中调用了Division()计算两数相除.
执行代码后, 可以发现 当遇到除零错误时, 会打印一个字符串. 且 这个字符串就是throw "Division by zero condition!";中的字符串. 并且, 没有返回错误退出信息.
如果将Func()try中移除, 又会是什么结果呢?
此时 发生除零错误, 进程会直接被abort终止. 退出信息为134. 这是编译器帮忙强制终止了
从这里可以看出, C++异常是如何处理的:
  1. throw

    throw是一个关键词, 用来抛出异常, 可以抛出 任意类型. 上述例子中:

    if(b == 0); throw "Division by zero condition!";

    就是在发生除零错误时, 抛出异常"Division by zero condition!"

  2. try

    try也是一个关键词, 一般来说可能会抛出异常的代码, 都放在try块中. 放在try块中的代码, 通常被成为 保护代码. 在本例中:

    Func()try中时, 可以捕获到异常并处理

    不在try中时, 不会捕获异常.

  3. catch

    catch同样是一个关键词, 是用来捕获throw抛出的异常的. 在本例中:

    catch (const char* errmsg)捕获const char*类型的异常. catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

    catch块中的代码, 为 捕获到异常后 要做的处理.

    可以有多个catch针对不同的异常进行捕获, 但是 多个catch中不能设置相同类型的异常

    即, 如果像这样设置catch:

    try {
    }
    catch (const char* e){
    }
    catch (const char* e){
    }

    在一些编译器中会报错, 最少也是一个警告:

    这就表示, 第二个catch (const char* e)捕获不到const char*类型的异常.

这就是C++异常处理的最基本的概念.

异常的使用

上面介绍了 最基础的异常的使用.
不过, 想要用好异常, 还有许多的细节 和 用法需要了解.

1. 异常的抛出 与 捕获 的匹配原则

  1. 异常是通过抛出 对象 而引发的, 该 对象的类型 决定了应该激活哪个catch的处理代码

    即, catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

  2. 被选中的处理代码是调用链中 与该对象类型匹配 且离抛出异常位置最近 的那一个

    这句话是什么意思呢?

    来分析这一段代码:

    #include <iostream>
    
    using std::cout;
    using std::endl;
    using std::cin;
    
    double Division(int a, int b) {
        // 当b == 0时抛出异常
        if (b == 0) {
            try {
                throw "Division by zero condition!";
            }
            catch (const char* errmsg) {
                cout << "Division 捕获了 const char* 异常: " << errmsg << endl;
                return 1;
            }
        }
        else
            return ((double)a / (double)b);
    }
    
    void Func() {
        int len, time;
        cin >> len >> time;
        try {
            cout << Division(len, time) << endl;
        } 
        catch (const int errI) {
            cout << "Func 捕获了 const int 异常: " << errI << endl;
        } 
        catch (const char* errS) {
            cout << "Func 捕获了 const char* 异常: " << errS << endl;
        } 
    }
    
    int main() {
        try {
            Func();
        }
        catch (const char* errmsg) {
            cout << "main 捕获了 const char* 异常: " << errmsg << endl;
        }
    
        return 0;
    }

    这段代码, 如果发生除零错误, 会触发哪个catch捕获异常呢?

    很明显是Divison()内部的catch (const char* errmsg)会捕捉到. 因为 捕捉异常类型与抛出异常类型匹配 且离抛出异常位置最近

    如果 将Division()内部的catch (const char* errmsg)改为catch (const int errI), 那么又会被哪个catch捕捉到呢?

    没错, 就是Func()内部的catch (const char* errS)

  3. throw抛出异常时, 编译器会生成一个 异常对象的临时拷贝. 就像函数返回那样. 所以可以正常的被更上层的函数栈捕捉到.

  4. 使用catch(...)可以 捕获任意类型的异常.

    也就是说, 使用catch(...)之后, 只要 有异常在此之前没有被捕获, 就 会捕获此异常.

    比如可以用这段代码来实验一下:

    #include <iostream>
    
    using std::cout;
    using std::endl;
    using std::cin;
    
    class Exc1 {
    private:
        int _excID;
    };
    
    class Exc2 {
    private:
        int _excID;
    };
    
    double Division(int a, int b) {
        // 当b == 0时抛出异常
        if (b == 0) {
            throw "Division by zero condition!";
        }
        else if (b == 1) {
            throw 1024;
        }
        else if (b == 2) {
            throw 'b';
        }
        else if (b == 3) {
            throw Exc1();
        }
        else if (b < 0) {
            throw Exc2();
        }
        else
            return ((double)a / (double)b);
    }
    
    void Func() {
        int len, time;
        cin >> len >> time;
        try {
            cout << Division(len, time) << endl;
        } 
        catch (const int errI) { 		// 捕获const整型异常
            cout << "Func 捕获了 const int 异常: " << errI << endl;
        } 
        catch (const char errC) {		// 捕获const字符异常
            cout << "Func 捕获了 const char 异常: " << errC << endl;
        } 
    }
    
    int main() {
        while(true) {
            try {
                Func();
            }
            catch (const char* errmsg) {	// 捕获const字符串异常
                cout << "main 捕获了 const char* 异常: " << errmsg << endl;
            }
            catch (...) {
                cout << "main 捕获了 未知异常" << endl;
            }
        }
    
        return 0;
    }

    const char*const intconst char这三种类型的异常, 我们分别在main()Func()中指定捕捉了.

    而 在传入的 b == 3 和 b < 0 时, 抛出的Exc1对象和Exc2对象异常 并没有指定捕捉.

    但是, 他们却执行了cout << "main 捕获了 未知异常" << endl;这个语句.

    即, 执行了catch(...)内的处理.

    这可以说明, catch(...)可以捕捉任意类型的异常. 但是由于没有指定类型, 所以不知道捕捉到的究竟是什么类型的异常.

  5. 虽说 catch的异常类型 必须与throw的异常类型相同. 否则无法捕获目标异常.

    但实际上, 那只是一般情况下. 除此之外, 还存在一个特例:

    如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常

    比如这段代码:

    #include <iostream>
    
    using std::cin;
    using std::cout;
    using std::endl;
    
    class faClass {
    private:
        size_t _faExcID;
    };
    
    class sonClass: public faClass { 
    private:
        size_t _sonExcID;
    };
    
    double Division(int a, int b) {
        // 当b == 0时抛出异常
        if (b == 0) {
            throw "Division by zero condition!";
        }
        else if (b == 1) {
            throw faClass();
        }
        else if (b < 0) {
            throw sonClass();
        }
        else
            return ((double)a / (double)b);
    }
    
    void Func() {
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    
    int main() {
        while (true) {
            try {
                Func();
            }
            catch (const char* errmsg) {
                cout << errmsg << endl;
            }
            catch (const faClass& e) {
                cout << "main 捕获到faClass类异常 或 以faClass为基类的派生类异常" << endl;
            }
        }
    
        return 0;
    }

    这段代码:

    1. b传入0,throw "Division by zero condition!"
    2. b传入1,throw faClass()
    3. b传入负数,throw sonClass().sonClassfaClass的派生类

    而除了catch(const char* errmsg)之外, 只catch(const faClass& e)

    那么, 这段代码发生各种异常的结果是什么?

    可以看到, 当b传入负数1时, 都会执行catch (const faClass& e)内的处理动作.

    这其实就证明了, 如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常

    这个特性非常的有用! 经常在开发中使用!

2. 在函数调用链中 异常栈展开匹配原则

  1. 首先检查throw本身是否在try块内部.

    如果是, 则在当前函数栈帧 查找匹配的catch语句

    如果当前函数栈帧有匹配的, 则 跳到catch的地方进行处理

  2. 如果当前函数栈帧内没有匹配的, 则 退出当前函数栈, 继续在外层调用函数的栈中进行查找匹配的catch

    此操作, 会一直退出到main()函数栈帧中

  3. 如果到达main函数的栈, 依旧没有匹配的catch, 则 终止进程

  4. 整个沿着调用链 向更外层调用函数的栈帧中查找匹配的catch的行为, 被称为 栈展开

  5. 为了避免 由于异常没有匹配的catch导致进程终止, 所以 都会在最后 使用catch(...)捕获未知异常

  6. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

此原则中, 前三条原则 即为栈展开的过程.
假如存在Func1()Func2()Func3():
void Func3() {
    throw "Throw an exception directly!";
    cout << "hello Func3" << endl;
}

void Func2() {
    Func3();
    cout << "hello Func2" << endl;
}

void Func1() {
    Func2();
    cout << "hello Func1" << endl;
}
且,main()函数呢存在以下内容:
int main() {
    try {
        Func1();
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    cout << "hello main" << endl;

    return 0;
}
那么,throw异常之后, 栈展开的过程大概为这样的:
C++异常处理有一个 非常麻烦 的点.
throw之后, 如果找到对应类型的catch, 会直接跳转到对应函数栈帧内执行catch子句, 执行完之后 会留在此函数内继续向下执行. 而不是回到throw继续向下执行.
执行上面的代码也可以证明这一点:
Func1()Func2()Func3()中, 都有一句cout语句. 但是, 只执行了main()中的cout语句.
这样很可能会造成什么后果呢? 可以看一看这段代码:
#include <iostream>

using std::cout;
using std::endl;

void Func1() {
    // new一块空间
    int* arr = new int[20480];

    throw "Throw an exception directly!";
    cout << "hello Func1" << endl;

    delete[] arr;
}

int main() {
    while (true) {
        try {
            Func1();
        }
        catch (const char* errmsg) {
            cout << errmsg << endl;
        }
        cout << "hello main" << endl;
    }

    return 0;
}
这段代码执行之后, 会发生什么后果?
没错, 内存泄漏! 非常严重的内存泄漏!
可以看到, 名为exception_test.exe的内存占用, 在疯狂的上涨.
这是比较明显的内存泄漏. 这是什么原因呢?
其实是因为, 我们new int[20480]出来的空间, 没有被delete[]掉.
但是Func1()函数中,new[]之后 函数结束前 明明delete[]了, 为什么没有delete[]掉呢?
就是因为Func1()函数内throw之后, 直接跳到了main()catch的位置, 并且留在了main()中没有回到throw那里. 所以 在throw之后的 delete[]语句根本就没有执行. 就 造成了内存泄漏
当前阶段, 如何正确的解决这个问题呢?
如果存在new之后会throw的可能, 就需要直接在Func1()内 将throw放在try块内, 就地catch处理并返回:
void Func1() {
    // new一块空间
    int* arr = new int[20480];
	
    try {
        throw "Throw an exception directly!";
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
        delete[] arr;
        return;
    }

    // 这里也要delete[]
    // 因为在其他场景中, 可能并不一定会throw
    delete[] arr;
}
这样, 就不会发生内存泄漏了:
这是当前阶段最简单的处理方式, 不过太难用.

3. 异常的重新抛出

观察这段代码:
double Division(int a, int b) {
    // 当b == 0时抛出异常
    if (b == 0) {
        throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}

void Func() {
    int* array = new int[10];
    
    try {
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (...) {
        cout << "delete []" << array << endl;
        delete[] array;
        throw;
    }
    
    cout << "delete []" << array << endl;
    delete[] array;
}

int main() {
    try {
        Func();
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    
    return 0;
}
Func()new了一个数组. 但是在delete[]之前又有可能throw异常.
此异常的处理动作, 已经在 main()函数中实现了.
但是, 由于还有new出来的空间未delete, 又不得不在Func()函数内添加一个try{...}catch{...}
不过, 这里的捕获异常 可以使用catch(...)捕获任意异常, 并且不处理异常, 只将未释放的空间delete掉, 然后再将异常原从新抛出
抛出之后, 会再沿着调用链进行栈展开寻找对应的catch, 找到真正处理此异常的catch再处理掉异常
上面例子中, Func()catch(...)子句中的 throw 即为 重新抛出异常

由于这里使用的是 catch(...) 没有指定类型捕获, 所以 throw; 就可以重新抛出

如果指定了类型 catch(const char* errmsg), 就需要 throw errmsg; 来实现重新抛出.

4. 异常安全

关于异常的使用, 有一些情况下需要非常的小心:
  1. 构造函数的作用是 完成对象的构造和初始化, 最好 不要在构造函数中抛出异常, 否则 可能导致对象不完整或没有完全初始化

  2. 析构函数的作用是 完成资源的清理, 最好 不要在析构函数内抛出异常, 否则 可能导致资源泄漏(内存泄漏、句柄未关闭等)

  3. 还有就是, 在newdelete中抛出了异常, 导致 内存泄漏. 在lockunlock之间抛出了异常 导致死锁.

    这些问题的更加好用的解决, 需要用到智能指针.

5. C++标准库的异常体系

在介绍 异常的抛出与捕获匹配原则时, 介绍过 如果catch捕捉基类异常, 那么 除了可以捕捉到throw抛出的 此基类异常外, 还可以捕捉到throw抛出的 此基类的派生类异常
并且也举了简单的例子证明.
所以, C++委员会就根据此原则, 实现了一个 异常类体系.
说明白一点, 就是 C++委员会 实现了许多的类 来对应C++可能发生的所有错误, 被称为 异常类. 这些异常类, 都来派生于一个基类 std::exception.
文档中对此类的描述是:
标准库中所有组件抛出的异常对象都派生于此类. 因此, 通过捕获此类, 就可以捕获所有标准异常
此类中, 除了构造、析构等成员函数之外, 还实现了一个共其派生类重写的成员函数what()

what()

what() 有什么用呢?
由于标准异常有很多, 而且都可以通过捕获std::exception来捕获.
所以, 捕获到之后要分辨 捕获到的究竟是什么异常是很麻烦的
所以, std::exception提供了what()函数. 它需要实现的作用是: 获取标识异常的字符串
设置成虚函数, 就是为了让派生类重写此函数, 实现不同派生类可以 返回标识其本身的字符串.
也就是说, 通过捕获std::exception捕获到异常对象之后, 可以调用其成员函数what()并接收其返回值, 来知道捕获到的是什么异常.

C++标准库中的异常类

C++标准库中, 实现了许多的异常类.
  1. bad_alloc

    分配内存失败时, 抛出的异常.

  2. bad_cast

    动态转换时, 抛出的异常

  3. out_of_range

    越界访问时, 抛出的异常

这张图, 可以用来表示 C++标准库中的异常类体系:

6. 自定义异常体系

实际的项目开发中, 许多的公司都会自己定义一套异常体系 来进行规范的异常管理.
原因嘛, 用一句话总结 大概就是: 标准库的异常体系无法满足需求.
这里有一个 自定义异常体系的例子:
#include <iostream>
#include <string>
#include <unistd.h>

using std::string;
using std::cout;
using std::endl;

class Exception {
public:
    Exception(const string& errmsg, int id) 
        : _errmsg(errmsg)
        , _id(id) 
    {}
    
    virtual string what() const {
        return _errmsg; 
    }

protected:
    string _errmsg;
    int _id;
};

class SqlException : public Exception {
public:
    SqlException(const string& errmsg, int id, const string& sql)
        : Exception(errmsg, id)
        , _sql(sql) 
    {}

    virtual string what() const {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;

        return str;
    }

private:
    const string _sql;
};

class CacheException : public Exception {
public:
    CacheException(const string& errmsg, int id) 
        : Exception(errmsg, id) 
    {}
    
    virtual string what() const {
        string str = "CacheException:";
        str += _errmsg;

        return str;
    }
};

class HttpServerException : public Exception {
public:
    HttpServerException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id)
        , _type(type)
    {}

    virtual string what() const {
        string str = "HttpServerException:";
        str += _type;
        str += ":";
        str += _errmsg;

        return str;
    }

private:
    const string _type;
};

void SQLMgr() {
    if (rand() % 7 == 0) {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    else {
        cout << "Sql Success" << endl;
    }
}

void CacheMgr() {
    if (rand() % 5 == 0) {
        throw CacheException("权限不足", 101);
    }
    else if (rand() % 6 == 0) {
        throw CacheException("数据不存在", 102);
    }
    else {
        cout << "Cache Success" << endl;
    }
    
    SQLMgr();
}

void HttpServer() {
    if (rand() % 3 == 0) {
        throw HttpServerException("资源请求错误", 103, "get");
    }
    else if (rand() % 4 == 0) {
        throw HttpServerException("权限不足", 104, "post");
    }
    else {
        cout << "Http Success" << endl;
    }
    
    CacheMgr();
}

int main() {
    srand(time(0));
    while (true) {
        // 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境
        // Windows 平台 需要将其换为 Sleep(1000);
  		// 并将 头文件 unistd.h 换为 Windows.h
        sleep(1);
        try {
            HttpServer();
        }
        catch (const Exception& e) {
            // 多态
            cout << e.what() << endl;
        }
        catch (...) {
            cout << "Unkown Exception" << endl;
        }
    }
    
    return 0;
}
我们先分析一下这段代码:
  1. 首先, 代码实现了 4 个类: 1个基类, 3个派生类

    基类 Exception

    成员变量: _errmsg, string类型 用于存储异常信息. _id, int类型 用于存储异常代码

    成员函数: what(), 返回异常信息, 用于获取当前异常类

    派生类1 SqlException

    成员变量: 除继承于基类的 _errmsg_id 之外. _sql, const string类型 用于存储 异常sql指令

    成员函数: 重写what(), 返回异常信息, 包括 所属类_errmsg_sql

    派生类2 CacheException

    成员变量: 除继承于基类的 _errmsg_id 之外, 无其他成员变量.

    成员函数: 重写what(), 返回异常信息, 包括 所属类_errmsg

    派生类3 HttpServerException

    成员变量: 除继承于基类的 _errmsg_id 之外. _type, const string类型 用于存储 发生异常的服务类型

    成员函数: 重写what(), 返回异常信息, 包括 所属类type_errmsg

  2. 其次, 实现了三个函数 用来模拟不同的服务的异常场景

    SqlMgr():

    模拟数据库管理时的异常场景:

    随机数 % 7 == 0 执行 throw SqlException. 来模拟数据库管理时权限不足的场景.

    异常信息: 权限不足, 异常代码: 100, 异常Sql语句: select * from name = '张三'

    CacheMgr():

    模拟缓存管理时的异常场景:

    随机数 % 5 == 0 执行 throw CacheException("权限不足", 100)

    异常信息: 权限不足, 异常代码: 101

    随机数 % 6 == 0 执行 throw CacheException("数据不存在", 101)

    异常信息: 数据不存在, 异常代码: 102

    最后, 调用 SqlMgr()

    HttpServer():

    模拟HTTP服务可能发生的异常场景:

    随机数 % 3 == 0 执行 HttpServerException("请求资源不存在", 200, "get")

    异常信息: 资源请求错误, 异常代码: 103, 异常服务类型: get

    随机数 % 4 == 0 执行 HttpServerException("权限不足", 100, "post")

    异常信息: 权限不足, 异常代码: 104, 异常服务类型: post

    最后, 调用CacheMgr()

  3. 最后, 主函数内 死循环模拟服务器运行.

    try块内调用HttpServer(), 而HttpServer()内调用CacheMgr(), CacheMgr()内调用SqlMgr(), 模拟服务运行的流程.

    catch (const Exception& e)及代码块实际作用是 捕获三种异常类, 并多态调用不同异常类对象的what() 接受打印相关异常信息

    catch (...)及代码块捕获其他异常, 输出未知异常

至此, 整个代码分析结束, 从HttpServer() 层层调用到 SqlMgr(), 每个函数内都有概率抛出不同的异常, 抛出之后会在 main内被捕获并处理.
查看执行结果:
从结果可以看到, 每次循环 每层调用都有一定的概率抛异常. 并且都会在main函数内被捕捉到并处理.

图中, 光标可能在某行停顿1-2s, 说明此时并没有异常抛出


在添加一个SeedMsg()函数, 模拟发送信息异常.
void SeedMsg(const string& str) {
    if (rand() % 2 == 0) {
        throw HttpServerException("SeedMsg::网络错误", 105, "put");
    }
    else if (rand() % 4 == 0) {
        throw HttpServerException("SeedMsg::你已经不是对方好友", 106, "post");
    }
    else {
        cout << "消息发送成功!->" << str << endl;
    }
}
main() 函数try块中执行的函数 换为此函数, 查看执行:
并尝试, 将 网络错误异常的处理方式 改为 发生异常之后 直接重试再发送10次.
其实很简单, 只需要改动main函数内容就可以(不过要先在 Exception类中添加一个成员函数getid()):
int main() {
    srand(time(0));
    while (true) {
        // 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境
        // Windows 平台 需要将其换为 Sleep(1000);
  		// 并将 头文件 unistd.h 换为 Windows.h
        sleep(1);
        try {
            for(int i = 1; i <= 10; i++) {
                try {
            		SeedMsg("你好啊?");
                    // 能走到这里 一定发送成功
                    // 直接break跳出 for循环
                    break;
                }
                catch (const Exception& e) {
                    if (e.getid() == 105) {
                        // 针对 103 异常处理
                        cout << "网络错误, 重发送, 第 " << i << " 次" << endl;
                        continue;
                    }
                    else {
                        // 不是此异常, 重新抛出
                        throw e;
                    }
                }
            }
        }
        catch (const Exception& e) {
            // 多态
            cout << e.what() << endl;
        }
        catch (...) {
            cout << "Unkown Exception" << endl;
        }
    }
    
    return 0;
}
执行结果:
inline
inline
这里的关键点就是, 异常的重新抛出, 还有 SeedMsg()之后的break.
由于需要特定的处理 _id105的异常, 所以需要先就近catch一下, 然后判断异常代码, 进行处理.
而, 由于只处理105异常, 其他异常就需要再次抛出, 让其他地方处理.
然后, SeedMsg()之后的break. SeedMsg()正常返回, 就一定会顺着向下走, 也表示这发送成功, 就不需要继续for循环, 所以直接break. 如果抛异常, 则会跳过break, 然后异常被下面的catch子句捕获.

介绍到这里, C++关于异常的内容 就暂时介绍完了.
感谢阅读~