C++:异常

转载:C++异常捕获和处理

更多C++知识点:C++目录索引


0. 写在前面

  异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个错误,返回退出码或者终止程序等等,此时我们只知道有错误,但不能更清楚的知道哪种错误,因此,使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

1. 异常的抛出和处理

  1. 异常处理的语句

    1. try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。
    2. throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
    3. catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。

    例2:

        #include <iostream>
        using namespace std;
    
    
        void Test1()
        {
            try
            {
                char* p = new char[0x7fffffff];  //抛出异常
            }
            catch (exception e)
            {
                cout << e.what() << endl;   //捕获异常,然后程序结束
            }
    
        }
        int main()
        {
            Test1();
            system("pause");
            return 0;
        }
          
          
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    结果:
      当使用new进行开空间时,申请内存失败,就会抛出异常,此时捕获到异常时,就可告诉使用者是哪里的错误,便于修改
    这里写图片描述

2. 异常的处理规则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的匿名对象,异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。

例2:

class Exception//异常类
{
public:
    Exception(const string& msg, int id)
    {
        _msg = msg;
        _id = id;
    }

    const char* What() const
    {
        return _msg.c_str();
    }

protected:
    string _msg;
    int    _id;
};

template<size_t N = 10>
class Array
{
public:
    int& operator[](size_t pos)
    {
        if (pos >= N)
        {
            Exception e("下标不合法", 1); //出了这个作用域,抛出的异常对象就销毁了,这时会生成一个匿名对象先接受这个对象,并传到外层栈帧。
            throw e;
        }
        return a[pos];
    }
protected:
    int a[N];
};

int f()
{
    try
    {
        Array<> a;
        a[11];
    }
    catch (exception& e)
    {
        cout << e.what() << endl; //类型不匹配,找离抛出异常位置最近且类型匹配的那个。
    }

    return 0;
}

int main()
{
    try
    {
        f();
    }
    catch (Exception& e)
    {
        cout << e.What() << endl;
    }
    system("pause");
    return 0;
} 
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

结果:
  f()函数中捕获的异常是标准库里面的异常,但抛出异常的对象是自己定义的异常类,故类型不匹配,找离抛出异常最近的且类型匹配的Exception

这里写图片描述

3. 异常处理栈展开

  1.在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问。
  
  2.栈展开(寻找异常处理(exception handling)代码)

   栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行

具体过程:

  • 当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句
  • 对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。
  • 没有找到匹配的catch子句,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。

  3. catch子句的查找:
  
   catch子句是按照出现的顺序进行匹配的(以例2来说,异常先会匹配catch(exception e)子句,然后在匹配 catch (Exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行类型转换,但是比较严格:

  1. 允许从非常量转换到常量的类型转换(权限缩小)
  2. 允许从派生类到基类的转换。
  3. 允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针(降级问题)
  4. 标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。

4. 异常处理中需要注意的问题

  1. 如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止

  2. 一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。

  3. 异常处理仅仅通过类型而不是通过值来匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。

  4. 函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。

  5. 应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.
     
     注:那么当异常抛出后新对象如何释放?

    异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间构造起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。

  6. catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。

  7. 编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </div>

更多C++知识点:C++目录索引


0. 写在前面

  异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个错误,返回退出码或者终止程序等等,此时我们只知道有错误,但不能更清楚的知道哪种错误,因此,使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

1. 异常的抛出和处理

  1. 异常处理的语句

    1. try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。
    2. throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
    3. catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。

    例2:

        #include <iostream>
        using namespace std;
    
    
        void Test1()
        {
            try
            {
                char* p = new char[0x7fffffff];  //抛出异常
            }
            catch (exception e)
            {
                cout << e.what() << endl;   //捕获异常,然后程序结束
            }
    
        }
        int main()
        {
            Test1();
            system("pause");
            return 0;
        }
        
        
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    结果:
      当使用new进行开空间时,申请内存失败,就会抛出异常,此时捕获到异常时,就可告诉使用者是哪里的错误,便于修改
    这里写图片描述

2. 异常的处理规则

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的匿名对象,异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。

例2:

class Exception//异常类
{
public:
    Exception(const string& msg, int id)
    {
        _msg = msg;
        _id = id;
    }

    const char* What() const
    {
        return _msg.c_str();
    }

protected:
    string _msg;
    int    _id;
};

template<size_t N = 10>
class Array
{
public:
    int& operator[](size_t pos)
    {
        if (pos >= N)
        {
            Exception e("下标不合法", 1); //出了这个作用域,抛出的异常对象就销毁了,这时会生成一个匿名对象先接受这个对象,并传到外层栈帧。
            throw e;
        }
        return a[pos];
    }
protected:
    int a[N];
};

int f()
{
    try
    {
        Array<> a;
        a[11];
    }
    catch (exception& e)
    {
        cout << e.what() << endl; //类型不匹配,找离抛出异常位置最近且类型匹配的那个。
    }

    return 0;
}

int main()
{
    try
    {
        f();
    }
    catch (Exception& e)
    {
        cout << e.What() << endl;
    }
    system("pause");
    return 0;
} 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

结果:
  f()函数中捕获的异常是标准库里面的异常,但抛出异常的对象是自己定义的异常类,故类型不匹配,找离抛出异常最近的且类型匹配的Exception

这里写图片描述

3. 异常处理栈展开

  1.在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问。
  
  2.栈展开(寻找异常处理(exception handling)代码)

   栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行

具体过程:

  • 当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句
  • 对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。
  • 没有找到匹配的catch子句,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。

  3. catch子句的查找:
  
   catch子句是按照出现的顺序进行匹配的(以例2来说,异常先会匹配catch(exception e)子句,然后在匹配 catch (Exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行类型转换,但是比较严格:

  1. 允许从非常量转换到常量的类型转换(权限缩小)
  2. 允许从派生类到基类的转换。
  3. 允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针(降级问题)
  4. 标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。

4. 异常处理中需要注意的问题

  1. 如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止

  2. 一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。

  3. 异常处理仅仅通过类型而不是通过值来匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。

  4. 函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。

  5. 应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.
     
     注:那么当异常抛出后新对象如何释放?

    异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间构造起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。

  6. catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。

  7. 编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </div>

猜你喜欢

转载自blog.csdn.net/w_y_x_y/article/details/80396287