• 【 C++ 】异常


    目录

    1、C语言传统的处理错误的方式

    2、C++异常概念

    3、异常的使用

            异常的抛出和捕获

            异常的重新抛出

            异常安全

            异常规范

    4、C++标准库的异常体系

    5、自定义异常体系

    6、异常的优缺点


    1、C语言传统的处理错误的方式

    传统的错误处理机制:

    1. 终止程序assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
    2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误

    实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。C++觉得这种处理错误的方式并不好,包括其它的面向对象语言也是这样认为的,因此C++就推出了异常这种对错误的新的处理方式。不过C++也兼容C传统的处理错误的方式。


    2、C++异常概念

    异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

    • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
    • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常,可以有多个catch进行捕获。
    • try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

    如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

    1. try
    2. {
    3. // 保护的标识代码
    4. }
    5. catch (ExceptionName e1)
    6. {
    7. // catch 块
    8. }
    9. catch (ExceptionName e2)
    10. {
    11. // catch 块
    12. }
    13. catch (ExceptionName eN)
    14. {
    15. // catch 块
    16. }

    3、异常的使用

    异常的抛出和捕获

    异常的抛出和匹配原则:

    1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
    2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个,但是同一个位置不允许有两个相同的捕获
    3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回
    4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
    5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。

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

    1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
    2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
    3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
    4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

    如下有三个函数func1、func2、func3,在main函数中调用func3,在func3中调用func2,在func2中调用func1,在func1中抛出一个string类型的异常,并在main函数中用catch捕获:

    1. void func1()
    2. {
    3. throw string("这是一个异常");
    4. }
    5. void func2()
    6. {
    7. func1();
    8. }
    9. void func3()
    10. {
    11. func2();
    12. }
    13. int main()
    14. {
    15. try
    16. {
    17. func3();
    18. }
    19. catch (const string& s)
    20. {
    21. cout << "错误描述:" << s << endl;
    22. }
    23. catch (...)
    24. {
    25. cout << "未知异常" << endl;
    26. }
    27. return 0;
    28. }

    在func1中的异常被抛出后,会沿着栈帧去展开,一层一层在函数栈里找到匹配的catch,如果到达main函数的栈,依旧没有匹配的,就终止程序,具体过程如下:

    1. 首先会检查throw本身是否在try内部,这里由于throw不在try内部,因此会退出func1所在的函数栈,继续在上一个调用的函数栈(func2)中进行查找
    2. 由于func2所在的函数栈也没有匹配的catch,继续退到上一个函数栈(func3)中进行查找
    3. func3所在的函数栈也没有匹配的catch,此时退回到main所在的函数栈进行查找,最终在main函数栈中找到了匹配的catch
    4. 这时就会跳到main函数中对应的catch块中执行对应的代码块,执行后继续执行该代码块后续的代码

    整个过程如下图示:

    再看如下的程序中,Func函数里输入俩数值作为实参并调用Division函数进行除法运算,如果发生除0错误,就throw抛出异常,并且我在main函数中去捕获此异常:

    1. double Division(int a, int b)
    2. {
    3. // 当b == 0时抛出异常
    4. if (b == 0)
    5. throw "Division by zero condition!";
    6. else
    7. return ((double)a / (double)b);
    8. }
    9. void Func()
    10. {
    11. int len, time;
    12. cin >> len >> time;
    13. cout << Division(len, time) << endl;
    14. }
    15. int main()
    16. {
    17. try
    18. {
    19. Func();
    20. }
    21. catch (const char* errmsg)
    22. {
    23. cout << errmsg << endl;
    24. }
    25. return 0;
    26. }

    上述情况是捕获了异常,假设我抛出异常,但是不捕获呢?此时就会直接报错并终止程序:

    综上,异常必须被我们捕获,这里允许多个catch的捕获,异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。上述抛出的异常是字符串类型,假设我们这里有int类型和字符串类型的捕获,当抛出异常时,它会自动跳到类型最匹配的捕获:

    假设我有捕获,但是类型不匹配呢?即我抛出的异常是字符串类型的,捕获却是int类型的,最终只会导致类型不匹配,依旧报错,如下:

    假设我func函数和main函数均有捕获,那么抛异常时,会跳到哪个捕获呢?

    1. double Division(int a, int b)
    2. {
    3. // 当b == 0时抛出异常
    4. if (b == 0)
    5. throw "Division by zero condition!";
    6. else
    7. return ((double)a / (double)b);
    8. }
    9. void Func()
    10. {
    11. try
    12. {
    13. int len, time;
    14. cin >> len >> time;
    15. cout << Division(len, time) << endl;
    16. }
    17. catch (const char* errmsg)
    18. {
    19. cout << errmsg << endl;
    20. }
    21. }
    22. int main()
    23. {
    24. try
    25. {
    26. Func();
    27. }
    28. catch (const char* errmsg)
    29. {
    30. cout << errmsg << endl;
    31. }
    32. return 0;
    33. }

    根据被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个,但是同一个位置不允许有两个相同的捕获我们得知这里跳到里抛出异常最近的Func函数的捕获里。这里我们通过调试进行演示:

    解释catch(...),看如下的小程序:

    1. double Division(int a, int b)
    2. {
    3. // 当b == 0时抛出异常
    4. if (b == 0)
    5. throw "Division by zero condition!";
    6. else
    7. return ((double)a / (double)b);
    8. }
    9. size_t x = 0;//记录抛异常的次数
    10. void Probability()
    11. {
    12. int val = rand();
    13. if (val < RAND_MAX / 4)//概率是%25
    14. {
    15. string str("25%概率抛异常->");
    16. str += to_string(val);
    17. x++;
    18. throw str;
    19. }
    20. else
    21. {
    22. cout << val << endl;
    23. }
    24. }
    25. void Func()
    26. {
    27. try
    28. {
    29. Probability();
    30. int len, time;
    31. len = rand();
    32. time = rand() % 10;
    33. cout << Division(len, time) << endl;
    34. }
    35. catch (const string& s)
    36. {
    37. cout << s << endl;
    38. }
    39. }
    40. int main()
    41. {
    42. srand(time(0));
    43. size_t N = 1000;
    44. for (size_t i = 0; i < N; i++)
    45. {
    46. Func();
    47. }
    48. cout << "抛异常的次数" << x << endl;
    49. cout << "抛异常的概率" << (double)x / N << endl;
    50. return 0;
    51. }
    • 如上的程序中,我有25%的概率会抛异常,此时会进入它对应的catch捕获,但是当我后续发生除0错误时,也抛了一个异常,可是没有对应的catch捕获,此时就会终止程序并报错,可能有人会觉着我单独写上匹配它的catch捕获不就行了吗,虽然确实也是,不过实际在工程代码中,代码量一旦上来了,很可能会有遗漏catch捕获的问题,等发现了问题,再去修改,那岂不完蛋了,试想下这要是一个大型服务器(如美团,你任意时间段都能点外卖,就是因为其服务器一直在运行),如若因为某个抛异常未捕获而导致服务器挂掉了(服务器停机这就是事故哇!除非升级),可不是一时半会儿就能修复的,由于此问题带来的损失很可能就导致某些人要”毕业了”(痛啊!)

    为了解决此问题的产生,catch(...)就登场了,它的作用是捕获没有匹配的任意类型的异常,对上述代码的调整如下:

    1. int main()
    2. {
    3. try
    4. {
    5. srand(time(0));
    6. size_t N = 1000;
    7. for (size_t i = 0; i < N; i++)
    8. {
    9. Func();
    10. }
    11. cout << "抛异常的次数" << x << endl;
    12. cout << "抛异常的概率" << (double)x / N << endl;
    13. }
    14. catch (...)//捕获没有匹配的任意类型异常
    15. {
    16. cout << "未知异常" << endl;
    17. }
    18. return 0;
    19. }

    发生未知异常的好处在于程序还能继续运行,不会像前面那样直接挂掉。


    异常的重新抛出

    有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理

    • 看如下的程序(依旧是除0就抛异常):
    1. double Division(int a, int b)
    2. {
    3. // 当b == 0时抛出异常
    4. if (b == 0)
    5. {
    6. throw "Division by zero condition!";
    7. }
    8. return (double)a / (double)b;
    9. }
    10. void Func()
    11. {
    12. int* array = nullptr;
    13. array = new int[1024 * 1024 * 10];
    14. int len, time;
    15. cin >> len >> time;
    16. cout << Division(len, time) << endl;
    17. cout << "delete []" << array << endl;
    18. delete[] array;
    19. }
    20. int main()
    21. {
    22. try
    23. {
    24. Func();
    25. }
    26. catch (const char* errmsg)
    27. {
    28. cout << errmsg << endl;
    29. }
    30. return 0;
    31. }
    • 如上的程序中,我new了一块array数组,我们都清楚new出来的对象往往都要手动去释放,否则会造成内存泄漏,为了检测此程序是否会在结束后手动delete释放了array数组,我们使用使用打日志的方式直接cout显示出来我们delete了,如若程序显示delete[ ]说明释放了,否则没有(内存泄漏),如下展开讨论:

    当我time ≠ 0时,不会发生除0错误,程序new出数组后会先正常的进行除法运算,随后delete [ ]数组,示例如下:

    如若time == 0时,会在Division函数抛异常,随后走到main函数进行捕获,此时会发现程序并没有delete [ ]数组(发生内存泄漏)

    • 此时就发生了一个严重的错误,当除数time为0时,会在Division函数抛异常,随后走到main函数进行捕获,捕获完就结束了,一整个跳过了delete释放数组的操作,这不就发生了严重的内存泄漏错误嘛。此问题也是典型的异常安全,后续会讲。

    解决办法:(异常的重新抛出)

    先看修正后的代码:

    1. void Func()
    2. {
    3. int* array = nullptr;
    4. array = new int[1024 * 1024 * 10];
    5. try
    6. {
    7. int len, time;
    8. cin >> len >> time;
    9. cout << Division(len, time) << endl;
    10. }
    11. catch (...)
    12. {
    13. cout << "1::delete []" << array << endl;
    14. delete[] array;
    15. throw;//捕获什么抛什么
    16. }
    17. cout << "2::delete []" << array << endl;
    18. delete[] array;
    19. }

    我们直接在可能发生异常的地方(Division函数)进行捕获:

    • 当发生异常的时候,进入catch捕获,此时delete释放new出来的数组(走的是1::delete),随后再throw抛出异常,此处抛出的是前面catch捕获到的异常(除0错误),此时就算是发生了异常也不会发生内存泄漏了,运行效果如下:

    • 当没有发生异常的时候,程序不会进入catch捕获,继而执行2::delete[ ]释放new出来的数组,同样也不会发生内存泄漏,运行效果如下:

    注意:

    • Func中的new和delete之间可能还会抛出其他类型的异常,因此在Func中最好以catch(...)的方式进行捕获,将申请到的内存delete后再通过throw重新抛出。
    • 重新抛出异常对象时,throw后面可以不用指明要抛出的异常对象(正好也不知道以catch(...)的方式捕获到的具体是什么异常对象)。

    上述操作就是典型的异常的重新抛出,不过这样的处理方式有点挫,后续会使用智能指针的方法来解决此问题。


    异常安全

    抛异常导致的安全问题称为异常安全,有以下几点要注意:

    • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
    • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
    • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII我们到下一篇博文智能指针再来讲解

    异常规范

    异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些,C++标准规定如下:

    1. 可以在函数的后面接throw(type1, type2, ...),列出这个函数可能抛掷的所有异常类型。
    2. 函数的后面接throw()或noexcept(C++11),表示函数不抛异常。
    3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

    示例:

    1. // 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
    2. void fun() throw(A,B,C,D);
    3. // 这里表示这个函数只会抛出bad_alloc的异常
    4. void* operator new (std::size_t size) throw (std::bad_alloc);
    5. // 这里表示这个函数不会抛出异常
    6. void* operator delete (std::size_t size, void* ptr) throw();
    7. // C++11 中新增的noexcept,表示不会抛异常
    8. thread() noexcept;
    9. thread(thread&& x) noexcept;

    补充说明:

    1. 尽管C++98期望每个人写一个函数,就声明清楚是否抛异常,抛什么异常,不过实际并不是必须的,也很少会有老老实实“遵守”的。
    2. 于是C++11推出后,C++98的异常规范继续支持,只是简化了这块的规则,如果你不抛异常,就给一个noexcept说明一下即可。

    4、C++标准库的异常体系

    对于C++而言,其库里也搞了一套异常体系,其定义了一个基类(exception),源码如下:

    1. class exception {
    2. public:
    3. exception () throw();
    4. exception (const exception&) throw();
    5. exception& operator= (const exception&) throw();
    6. virtual ~exception() throw();
    7. virtual const char* what() const throw();
    8. //what的作用是返回抛出异常的原因的相关信息
    9. }

    解释说明:

    • exception类中的what成员函数和析构函数都定义成了虚函数,方便了子类对其进行重写,从而达到多态的效果
    • 我们可以捕获父类exception抛子类(利用多态的性质),也可以直接抛派生类

    C++提供的一些标准异常就继承了此exception基类中,我们可以在程序中使用这些异常,它们是以父子类层次结构组织起来的,如图所示:

    下表是对上面继承体系中出现的每个异常的说明: 

    异常描述
    std::exception该异常是所有标准C++异常的父类。
    std::bad_alloc该异常可以通过new抛出。
    std::bad_cast该异常可以通过dynamic_cast抛出。
    std::bad_exception这在处理C++程序中无法预期的异常时非常有用。
    std::bad_typeid该异常可以通过typeid抛出。
    std::logic_error理论上可以通过读取代码来检测到的异常。
    std::domain_error当使用了一个无效的数学域时,会抛出该异常。
    std::invalid_argument当使用了无效的参数时,会抛出该异常。
    std::length_error当创建了太长的std::string时,会抛出该异常。
    std::out_of_range该异常可以通过方法抛出,例如std::vector和std::bitset<>::operator。
    std::runtime_error理论上不可以通过读取代码来检测到的异常。
    std::overflow_error当发生数学上溢时,会抛出该异常。
    std::range_error当尝试存储超出范围的值时,会抛出该异常。
    std::underflow_error当发生数学下溢时,会抛出该异常。

    我们以bad_alloc示例,它是在分配内存失败时抛出的异常,如new失败……,看如下的示例:

    1. int main() {
    2. try
    3. {
    4. size_t i = 0;
    5. while (1)
    6. {
    7. int* myarray = new int[1024 * 1000];
    8. cout << myarray << "->" << i++ << endl;
    9. }
    10. }
    11. catch (std::exception& ba)//捕获父类抛子类//或catch (std::bad_alloc& ba)
    12. {
    13. std::cerr << "bad_alloc caught: " << ba.what() << '\n';
    14. }
    15. return 0;
    16. }

    注意:

    • 正常情况下,C++的程序直接去捕获基类(exception)就可以了,但实际上很多人非常“嫌弃”C++的exception,因为库里的那一套并不能满足不同公司间的需求,所以实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。看下文的示例:

    5、自定义异常体系

    • 实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了。
    • 所以实际中都会定义一套继承的规范体系,先定义一个最基础的异常类,所有人抛出的异常对象都必须是继承于该异常类的派生类对象,而异常语法规定可以用基类捕获抛出的派生类对象这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了

    看如下我模拟实现的服务器开发中通常使用的异常继承体系:定义了一个Exception基类,并实现了数据库组的类(SqlException)、缓存组的类(CacheException)、协议组的类(HttpServerException)均继承了Exception基类,下面分别介绍其内部作用:

    • Exception基类:内部成员变量有_errmsg用来描述错误信息,_id用来描述错误编码,并定义what虚函数来返回错误信息,getid成员函数返回错误编码。
    • 数据库组的类(SqlException):增加了sql成员变量,并对what函数进行了重写(在返回错误信的同时又补充了是哪一条sql语句出错的)。
    • 缓存组的类(CacheException):重写了what函数,除了返回错误信息并标记此错误是缓存组的错误。
    • 协议组的类(HttpServerException):重写了what函数,返回错误信息并标记此错误是协议组的错误,此外还返回了是哪个类型的错误。
    1. // 服务器开发中通常使用的异常继承体系
    2. //基类
    3. class Exception
    4. {
    5. public:
    6. Exception(const string& errmsg, int id)
    7. :_errmsg(errmsg)
    8. , _id(id)
    9. {}
    10. virtual string what() const
    11. {
    12. return _errmsg;
    13. }
    14. int getid() const
    15. {
    16. return _id;
    17. }
    18. protected:
    19. string _errmsg;//描述错误信息
    20. int _id;//错误编码
    21. };
    22. //数据库组
    23. class SqlException : public Exception
    24. {
    25. public:
    26. SqlException(const string& errmsg, int id, const string& sql)
    27. :Exception(errmsg, id)
    28. , _sql(sql)
    29. {}
    30. virtual string what() const
    31. {
    32. string str = "SqlException:";
    33. str += _errmsg;
    34. str += "->";
    35. str += _sql;
    36. return str;
    37. }
    38. private:
    39. const string _sql;
    40. };
    41. //缓存组
    42. class CacheException : public Exception
    43. {
    44. public:
    45. CacheException(const string& errmsg, int id)
    46. :Exception(errmsg, id)
    47. {}
    48. virtual string what() const
    49. {
    50. string str = "CacheException:";
    51. str += _errmsg;
    52. return str;
    53. }
    54. };
    55. //协议组
    56. class HttpServerException : public Exception
    57. {
    58. public:
    59. HttpServerException(const string& errmsg, int id, const string& type)
    60. :Exception(errmsg, id)
    61. , _type(type)
    62. {}
    63. virtual string what() const
    64. {
    65. string str = "HttpServerException:";
    66. str += _type;
    67. str += ":";
    68. str += _errmsg;
    69. return str;
    70. }
    71. private:
    72. const string _type;
    73. };

    实现了如上的基本框架,接下来就要模拟实现抛异常的过程了,这里我们仅仅是用rand概率控制模拟抛出的不同的异常。下面展开来讨论数据库组、缓存组、协议族分别抛异常的过程:

    • 数据库组(SQLMgr):控制%10的概率抛权限不足的异常,其它为成功。
    • 缓存组(CacheMgr):控制%10的概率抛权限不足的异常,%25的概率抛数据不存在的异常,其它为成功,成功就调用数据库组的函数。
    • 协议族(HttpServer):控制%10的概率抛请求资源不存在的异常、%10的概率抛权限不足的异常、其它为成功,成功就调用缓存组函数。
    1. //抛数据库组的异常
    2. void SQLMgr()
    3. {
    4. if (rand() < RAND_MAX / 10)//10%的概率抛权限不足的异常
    5. {
    6. throw SqlException("权限不足", 100, "select * from name = '张三'");
    7. }
    8. else
    9. {
    10. cout << "Sql success" << endl;
    11. }
    12. }
    13. //抛缓存组的异常
    14. void CacheMgr()
    15. {
    16. if (rand() < RAND_MAX / 10)//%10的概率抛权限不足的异常
    17. {
    18. throw CacheException("权限不足", 100);
    19. }
    20. else if (rand() < RAND_MAX / 25)//%25的概率抛数据不存在的异常
    21. {
    22. throw CacheException("数据不存在", 101);
    23. }
    24. else
    25. {
    26. cout << "Cache success" << endl;
    27. }
    28. SQLMgr();
    29. }
    30. //抛协议组的异常
    31. void HttpServer()
    32. {
    33. if (rand() < RAND_MAX / 10)//%10的概率抛请求资源不存在的异常
    34. {
    35. throw HttpServerException("请求资源不存在", 404, "get");
    36. }
    37. else if (rand() < RAND_MAX / 10)//%10的概率抛权限不足的异常
    38. {
    39. throw HttpServerException("权限不足", 501, "post");
    40. }
    41. else
    42. {
    43. cout << "Http success" << endl;
    44. }
    45. CacheMgr();
    46. }

    实现好了上述三个模拟抛异常的函数,接下来进入main函数开始具体的测试,如下就会把多态的作用体现的淋漓尽致:

    • 定义死循环,确保程序一直运行
    • try(调用协议组(HttpServer)的抛异常的函数)catch(捕获父类对象,传的是父类的引用,catch内部调用e.what()输出不同派生类对象抛出的异常所对应的错误信息,
    • 上面就体现了捕获父类抛子类的过程,因为我catch的是父类的引用,根据多态的发生条件:1、父类的指针或引用,2、调用的必须是重写的虚函数。指向谁调用谁。那么此时我抛的是协议组的异常,就调协议组的,抛的是缓存组的异常,就调缓存组的……
    1. int main()
    2. {
    3. srand(time(0));
    4. while (1)
    5. {
    6. ::Sleep(1000);
    7. try
    8. {
    9. cpp::HttpServer();
    10. }
    11. catch (const cpp::Exception& e) // 这里捕获父类对象就可以
    12. {
    13. // 多态
    14. cout << e.what() << endl;
    15. }
    16. catch (const std::exception& e) // 这里捕获库里的基类对象,捕获类似出现new失败等库里抛的异常
    17. {
    18. // 多态
    19. cout << e.what() << endl;
    20. }
    21. catch (...)
    22. {
    23. cout << "Unkown Exception" << endl;
    24. }
    25. }
    26. return 0;
    27. }

    • 上述代码的实现就不会因为异常崩掉了,要么抛的是公司实现的Exception子类的异常,要么抛的是C++库里的异常,最差的情况就是抛了一个未知异常,不过被catch(...)捕获了,同样程序不会崩掉。上述整体代码链接:模拟实现服务器开发中通常使用的异常继承体系

    现在升级我们的需求:假设我现在有个SeedMsg发送消息的函数,如若里面出现网络错误的时候,我们现在要求重试发消息10次。

    1. void SeedMsg(const string& str)
    2. {
    3. if (rand() < RAND_MAX - 10000)//大概率抛网络错误的异常
    4. {
    5. throw HttpServerException("SeedMsg::网络错误", 2, "put");
    6. }
    7. else if (rand() < RAND_MAX / 10)//%10的概率抛权限不足的异常
    8. {
    9. throw HttpServerException("SeedMsg::权限不足,你已经不是对方好友", 1, "post");
    10. }
    11. else
    12. {
    13. cout << "消息发送成功!->" << str << endl;
    14. }
    15. }

    这里我们只需要在抛出异常时跳回的位置进行调整即可,先直接给出正确代码:

    1. int main()
    2. {
    3. srand(time(0));
    4. while (1)
    5. {
    6. ::Sleep(1000);
    7. try
    8. {
    9. //cpp::HttpServer();
    10. //发送消息出现网络错误,要求重试10次
    11. //权限错误就直接报错,
    12. for (size_t i = 0; i < 10; i++)
    13. {
    14. try
    15. {
    16. cpp::SeedMsg("明天星期八!!!");
    17. break;//如果发送成功了,直接break退出,防止发送成功还进入循环
    18. }
    19. catch (const cpp::Exception& e)
    20. {
    21. if (e.getid() == 2)//异常编码的价值,可以针对某个错误进行特殊处理
    22. {
    23. cout << "网络错误,重试发消息第" << i << "次" << endl;
    24. continue;
    25. }
    26. else//其它错误
    27. {
    28. //break;不能break,否则当出现权限错误时,根本不会走到这一步
    29. throw e;//重新抛出异常
    30. }
    31. }
    32. }
    33. }
    34. //……
    35. }
    36. return 0;
    37. }

    解释上述代码逻辑:

    • 这里重试10次的前提是只有网络错误才重试,否则就退出循环
    • 当我发送消息成功时,直接break,防止发送成功还进入循环
    • 当发送失败捕获的getid错误编码为2时,说明是网络错误,此时continue执行重发消息的代码逻辑
    • 当发送失败捕获的getid错误编码不是2时,说明不是网络错误,直接throw e,重新抛出异常,不能直接break,否则当出现权限错误时,根本不会走到这一步。

    整体代码链接:出现网络错误,要求重发10次

    运行如下:


    6、异常的优缺点

    C++异常的优点:

    • 1、异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
    • 2、返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,而异常可以通过抛出捕获直接拿到错误。具体看图示:

    • 3、很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
    • 4、部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator[](size_t pos)这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

    C++异常的缺点:

    1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
    2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
    3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
    4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
    5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

    总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外面向对象的语言基本都是用异常处理错误,这也可以看出这是大势所趋。

  • 相关阅读:
    Docker将本地的镜像上传到私有仓库
    PV与PVC
    java毕业生设计在线党建学习平台计算机源码+系统+mysql+调试部署+lw
    一款适用于.Net的高性能文件上传项目
    机器人控制器编程实践指导书旧版-实践五 数字舵机(执行器)
    强制更新视图
    Python-Requests
    linux应用hook实例(含源码分析)
    HTML 基础
    楼层(冬季每日一题 22)
  • 原文地址:https://blog.csdn.net/bit_zyx/article/details/127583805