• 【C++】异常机制


    目录

    异常的概念

    异常的基本语法

    栈解旋(unwinding)

    异常接口声明

    异常类型

    异常对象生命周期

    C++标准异常exception处理类

    编写自己的异常类

    继承在异常中的应用


    异常的概念

            在 C++ 中,异常处理的过程是:某个库软件或者代码提供的机制,能在异常处理情况时发出信号,这称为抛出异常(throwing the exception)。在程序的另一个地方,需要添加合适的代码处理异常情况,这称为处理异常(handing the exception)。这种编程方式可生成更有条理的代码

    • C 语言处理异常的方式
    1. int test(int x, int y) {
    2. if (!y) return -1; // 用 -1 表示失败
    3. else return x / y;
    4. }

    用 -1 表示失败,但是如果传入的 x 和 y 为 10 和 -10 时,是允许的,但返回的 -1 却也可以是失败的意思

    • C++异常优点
    1. 函数返回值可以忽略,异常不可以忽略。如果程序出现异常,但没有被获取,程序就会终止,使程序更具有健壮性,而如果使用 C语言 的 error 宏或者函数返回值,调用者可能会忘记检查,从而没有对错误进行处理,产生错误结果
    2. 整形返回值没有任何语义信息,异常包含语义信息
    3. 整形返回值缺乏相关的上下文信息,异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息
    4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在多个函数的调用栈中出现了某个错误,使用整形返回码要求你在每一级函数中都要进行处理,而使用异常处理的栈展开机制,只需要在一处进行处理就可以,需要每级函数都处理

    异常的基本语法

    抛出异常:可以使用 throw 语句在代码块中的任何地方抛出异常,throw 语句的操作数可以是任意的表达式(常量或者变量表达式),表达式的结果的类型决定抛出异常的类型

    1. int divide(int x, int y) {
    2. if (!y) {
    3. throw y; // 抛异常
    4. }
    5. return x / y;
    6. }

    捕获异常catch 块跟在 try 块后面,用于捕获异常,可以指定想要捕获的异常类型,类型由 catch 关键字后面的括号内的异常声明决定的。

    1. try
    2. {
    3. // 保护代码
    4. }catch( ExceptionName e )
    5. {
    6. // 能处理任何异常的代码
    7. }

    捕获任意异常:如果想让 catch 块能处理 try 块抛出的任何类型异常,则可以在异常声明的括号里使用 ...

    1. try
    2. {
    3. // 保护代码
    4. }catch(...)
    5. {
    6. // 能处理任何异常的代码
    7. }

    小示例

    代码示例:

    1. #include
    2. using namespace std;
    3. int divide(int x, int y) {
    4. if (y == 0) {
    5. throw y; // 抛异常
    6. }
    7. return x / y;
    8. }
    9. // 异常基本语法
    10. void test01() {
    11. // 试着捕获异常
    12. try {
    13. divide(10, 0);
    14. }
    15. // 异常根据类型进行匹配
    16. catch(int e) {
    17. cout << "除数为 " << e << endl;
    18. }
    19. }
    20. int main() {
    21. test01();
    22. system("pause");
    23. return 0;
    24. }

    运行结果:

    C++异常是必须处理的,一场会随着函数不断往上走,直到捕获异常,且可以跨函数

    测试代码:

    1. #include
    2. using namespace std;
    3. int divide(int x, int y) {
    4. if (y == 0) {
    5. throw y; // 抛异常
    6. }
    7. return x / y;
    8. }
    9. void CallDivide(int x, int y) {
    10. divide(x, y);
    11. }
    12. void test02() {
    13. try {
    14. CallDivide(10, 0);
    15. } catch (int e) {
    16. cout << "除数为 " << e << endl;
    17. }
    18. }
    19. int main() {
    20. test02();
    21. system("pause");
    22. return 0;
    23. }

    运行结果:


    栈解旋(unwinding)

            异常被抛出后,从进入 try 块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序和构造的顺序相反,这一过程称为栈的解旋(unwinding)

    1. #include
    2. using namespace std;
    3. class Person {
    4. public:
    5. Person() {
    6. cout << "Person 构建" << endl;
    7. }
    8. ~Person() {
    9. cout << "Person 析构" << endl;
    10. }
    11. };
    12. int divide2(int x, int y) {
    13. Person p1, p2;
    14. if (!y) throw y; // 抛出异常,可以理解为函数退出
    15. // 下面的都不执行,Person类的对象也就析构了
    16. cout << "不会打印" << endl;
    17. return x / y;
    18. }
    19. int main() {
    20. try {
    21. divide2(10, 0);
    22. }
    23. catch (int) {
    24. cout << "异常捕获" << endl;
    25. }
    26. system("pause");
    27. return 0;
    28. }

    运行结果:


    异常接口声明

    • 为加强程序的可读性,可以在函数声明中列出抛出异常的所有类型,例如 void func() throw(A, B, C);,就只能抛出 A,B,C及其子类型的异常
    • 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()
    • 一个不抛任何类型异常的函数可以声明为:void func() throw()
    • 如果一个函数抛出了它的异常接口声明锁不允许抛出异常,unexcepted 函数会被调用,该函数默认行为调用 terminate 函数中断程序
    • 函数 func01() 只能抛出 int,float,char 三种类型异常,抛出其他类型就会报错 
    1. void func01()throw(int, float, char) {
    2. }
    •  函数 func02() 不能抛出任何异常
    1. void finc02()throw() {
    2. }
    • 函数 func03() 可以抛出任何异常
    1. void func03() {
    2. }

    异常类型

    • throw 的异常是有类型的,可以是数字,字符串,类对象
    • throw 的异常有类型,catch 需严格匹配异常类型
    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. using namespace std;
    4. void func01() {
    5. throw 1; // 抛出 int 类型异常
    6. }
    7. void func02() {
    8. throw "exception"; // 抛出 const char* 类型异常
    9. }
    10. // 自定义的异常类
    11. class MyException {
    12. public:
    13. // 有参构造
    14. MyException(const char* str) {
    15. error = new char[strlen(str) + 1];
    16. strcpy(error, str);
    17. }
    18. // 拷贝构造
    19. MyException(const MyException& me) {
    20. this->error = new char[strlen(me.error) + 1];
    21. strcpy(this->error, me.error);
    22. }
    23. // 重载 = ,深拷贝
    24. MyException& operator=(const MyException& me) {
    25. if (this->error != NULL) {
    26. delete[] error;
    27. error = NULL;
    28. }
    29. this->error = new char[strlen(me.error) + 1];
    30. strcpy(this->error, me.error);
    31. return *this;
    32. }
    33. void what() {
    34. cout << error << endl;
    35. }
    36. // 析构
    37. ~MyException() {
    38. if (error != NULL) {
    39. delete[] error;
    40. error = NULL;
    41. }
    42. }
    43. public:
    44. char* error;
    45. };
    46. void func03() {
    47. throw MyException("<异常信息>"); // 抛出匿名对象异常
    48. }
    49. void test01() {
    50. try {
    51. func01();
    52. }
    53. catch (int) {
    54. cout << "异常捕获" << endl;
    55. }
    56. //-----------------------------------
    57. try {
    58. func02();
    59. }
    60. catch (const char*) {
    61. cout << "异常捕获" << endl;
    62. }
    63. //-----------------------------------
    64. try {
    65. func03();
    66. }
    67. catch (MyException e) {
    68. e.what(); // 调用异常对象
    69. }
    70. //-----------------------------------
    71. }
    72. int main() {
    73. test01();
    74. system("pause");
    75. return 0;
    76. }

    异常对象生命周期

    catch 捕获完后生命周期结束

    普通的 = 方式取捕获

    1. // 自定义异常类
    2. class MyException {
    3. public:
    4. // 构造
    5. MyException() {
    6. cout << "构造函数" << endl;
    7. }
    8. // 拷贝构造
    9. MyException(const MyException& me) {
    10. cout << "拷贝构造" << endl;
    11. }
    12. // 析构
    13. ~MyException() {
    14. cout << "析构函数" << endl;
    15. }
    16. };
    17. void func() {
    18. throw MyException(); // 创建匿名对象,调用构造
    19. }
    20. void test01() {
    21. // 普通元素,异常对象 catch 处理完后就析构
    22. try {
    23. func();
    24. }
    25. catch (MyException ex) // 调用拷贝构造
    26. {
    27. cout << "异常捕获" << endl;
    28. }
    29. }

    运行结果:

     先在抛出异常匿名对象时调用构造函数,然后捕获异常匿名对象调用拷贝构造函数,最后在处理完异常匿名对象调用析构函数。

    引用 & 的方式捕获

    1. // 自定义异常类
    2. class MyException {
    3. public:
    4. // 构造
    5. MyException() {
    6. cout << "构造函数" << endl;
    7. }
    8. // 拷贝构造
    9. MyException(const MyException& me) {
    10. cout << "拷贝构造" << endl;
    11. }
    12. // 析构
    13. ~MyException() {
    14. cout << "析构函数" << endl;
    15. }
    16. };
    17. void func() {
    18. throw MyException(); // 创建匿名对象,调用构造
    19. }
    20. void test02() {
    21. // 引用 不用调用拷贝构造,异常对象 catch 处理完析构
    22. try {
    23. func();
    24. }
    25. catch (MyException& ex) // 不调用拷贝构造
    26. {
    27. cout << "异常捕获" << endl;
    28. }
    29. }

    运行结果:

     在抛出异常匿名对象时调用构造函数,引用是给匿名对象其的别名,没有调用拷贝构造函数,最后在异常处理结束后匿名对象调用析构函数。

    指针 * 的方式捕获

    1. // 自定义异常类
    2. class MyException {
    3. public:
    4. // 构造
    5. MyException() {
    6. cout << "构造函数" << endl;
    7. }
    8. // 拷贝构造
    9. MyException(const MyException& me) {
    10. cout << "拷贝构造" << endl;
    11. }
    12. // 析构
    13. ~MyException() {
    14. cout << "析构函数" << endl;
    15. }
    16. };
    17. void func() {
    18. throw new MyException();
    19. }
    20. void test03() {
    21. // 指针,抛异常的时候就要抛出地址
    22. try {
    23. func();
    24. }
    25. catch (MyException* ex) // 不调用拷贝构造
    26. {
    27. cout << "异常捕获" << endl;
    28. delete ex; // 手动释放
    29. }
    30. }

    运行结果:

     指针的方式去接受匿名对象,就要在抛出异常的时候抛出地址,但要使用 new 出来的对象,因为不适用new,只是单纯的对匿名对象取地址,在函数 func3() 结束完,就被析构了,通过 new 出来的对象需要手动去释放(析构)。


    C++标准异常exception处理类

    C++给我们提供了标准的异常处理类,它用来抛出C++标准库中函数执行时的异常

    使用C++自带的标准异常类,需要包含对应的头文件

    C++提供的标准异常类的层次结构如图:

    所有的异常类都继承自exception基类,exception类下的logic_errorruntime_error又是两个比较大类,包含有多个子类,它们分表代表逻辑类错误运行时错误

    •  使用new开辟内存时,如果遇到空间不足,则会抛出bad_alloc异常。
    • 使用dynamic_cast()进行动态类型转化失败时,则抛出bad_typeid异常。
    • 计算数值超过该类型表示的最大范围时,则抛出overflow_error异常。
    • 使用string类下标但越界时,则抛出out_of_range异常。
    异常描述
    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当发生数学下溢时,会抛出该异常。

    编写自己的异常类

    • 建议自己写的异常类要继承标准异常类,因为C++可以抛出任何异常类型,所以我们的异常类可以不继承标准异常类,但是这样可能会导致程序混乱,尤其协同开发。
    • 当继承标准异常类时,应该重载父类的 what 函数和虚析构函数。
    • 因为栈展开的过程中,需要复制异常类型,所以就要根据类中添加的成员考虑是否提供自己的复制构造函数。

    代码示例:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. using namespace std;
    4. #include
    5. // 自定义异常类
    6. class MyOutOfRange :public exception { // 继承标准异常类
    7. public:
    8. // 有参构造
    9. MyOutOfRange(const char* error) {
    10. this->pError = new char[strlen(error) + 1];
    11. strcpy(this->pError, error);
    12. }
    13. // 析构
    14. ~MyOutOfRange() {
    15. if (pError != NULL) {
    16. delete[] pError;
    17. pError = NULL;
    18. }
    19. }
    20. // what 方法
    21. virtual char const* what() const {
    22. return pError;
    23. }
    24. public:
    25. char* pError;
    26. };
    27. void func01() {
    28. throw MyOutOfRange("<自定义异常>");
    29. }
    30. void test01() {
    31. try {
    32. func01();
    33. }
    34. catch (exception& e) {
    35. cout << e.what() << endl;
    36. }
    37. }
    38. int main() {
    39. test01();
    40. system("pause");
    41. return 0;
    42. }

    运行结果:

    用基类去接受异常可以省去多个catch去捕获 


    继承在异常中的应用

    代码示例:

    1. #include
    2. using namespace std;
    3. // 异常基类
    4. class BaseMyException {
    5. public:
    6. virtual void what() = 0; // 纯虚函数
    7. virtual ~BaseMyException() {} // 虚析构
    8. };
    9. // 目标文件异常类
    10. class TargetSapceNullException :public BaseMyException {
    11. public:
    12. // 重写父类纯虚函数
    13. virtual void what() {
    14. cout << "目标空间为空" << endl;
    15. }
    16. ~TargetSapceNullException() {}
    17. };
    18. // 源文件异常类
    19. class SourceSpaceNullException :public BaseMyException {
    20. public:
    21. // 重写父类纯虚函数
    22. virtual void what() {
    23. cout << "源空间为空" << endl;
    24. }
    25. ~SourceSpaceNullException() {}
    26. };
    27. void copy_str(char* target, const char* source) {
    28. if (target == NULL) {
    29. throw TargetSapceNullException();
    30. }
    31. if (source == NULL) {
    32. throw SourceSpaceNullException();
    33. }
    34. while (*source != '\0') {
    35. *target = *source;
    36. target++;
    37. source++;
    38. }
    39. }
    40. int main() {
    41. const char* source = "hello";
    42. char buf[1024] = { 0 };
    43. try {
    44. copy_str(buf, source);
    45. }
    46. catch (BaseMyException& e) {
    47. e.what();
    48. }
    49. cout << buf << endl;
    50. system("pause");
    51. return 0;
    52. }

    当buf位置传入空指针,source位置不为空

    运行结果:

    当buf位置不为空,source 位置为空指针

    运行结果:

  • 相关阅读:
    国际腾讯云直播推流配置教程!
    【RocketMQ系列一】初识RocketMQ
    [hive] 窗口函数 ROW_NUMBER()
    哈工大李治军老师操作系统笔记【4】:系统调用的实现(Learning OS Concepts By Coding Them !)
    PTA程序辅助实验平台——2023年软件设计综合实践_3(分支与循环)
    python采集火热弹幕数据并做词云图可视化分析
    C语言每日一题(34)链表的回文结构
    vue组件之间传参方式
    实验33:RFID门禁卡实验
    MySQL8的ONLY_FULL_GROUP_BY SQL模式兼容问题
  • 原文地址:https://blog.csdn.net/xuan3215/article/details/126272213