18. C++异常

作者: 飞扬code | 来源:发表于2019-04-13 12:24 被阅读38次

    程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:

    1. 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
    2. 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
    3. 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。异常(Exception)机制就是为解决运行时错误而引入的。
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main(){
        string str = "https://www.baidu.com";
        char ch1 = str[100];  //下标越界,ch1为垃圾值
        cout<<ch1<<endl;
        char ch2 = str.at(100);  //下标越界,抛出异常
        cout<<ch2<<endl;
        return 0;
    }
    

    18.1 捕获异常

    借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

    try{
        // 可能抛出异常的语句
    }catch(exceptionType variable){
        // 处理异常的语句
    }
    

    try和catch都是 C++ 中的关键字,后跟语句块,不能省略{ }。try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。

    #include <iostream>
    #include <string>
    #include <exception>
    using namespace std;
    
    int main(){
        string str = "https://www.baidu.com/";
        try{
            char ch1 = str[100];
            cout<<ch1<<endl;
        }catch(exception e){
            cout<<"越界1"<<endl;
        }
    
        try{
            char ch2 = str.at(100);
            cout<<ch2<<endl;
        }catch(exception &e){  //exception类位于<exception>头文件中
            cout<<"越界2"<<endl;
        }
    
        return 0;
    }
    
    image.png

    第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。

    第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。

    异常的处理流程:
    抛出(Throw)--> 检测(Try) --> 捕获(Catch)


    18.2 多级catch匹配

    catch 和真正的函数调用又有区别:
    1、真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
    2、而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。

    总起来说,catch 和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。

    try{
        //可能抛出异常的语句
    }catch (exception_type_1 e){
        //处理异常的语句
    }catch (exception_type_2 e){
        //处理异常的语句
    }
    //其他的catch
    catch (exception_type_n e){
        //处理异常的语句
    }
    

    当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base{ };
    class Derived: public Base{ };
    
    int main(){
        try{
            throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
        }catch(int){
            cout<<"int 异常"<<endl;
        }catch(char *){
            cout<<"char* 异常"<<endl;
        }catch(Base){  //匹配成功(向上转型)
            cout<<"基类异常"<<endl;
        }catch(Derived){
            cout<<"派生类异常"<<endl;
        }
    
        return 0;
    }
    
    image.png

    18.2.1 catch 在匹配过程中的类型转换

    C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
    1、 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
    2、 向上转型:也就是派生类向基类的转换。
    3、 const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
    4、数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
    5、用户自定的类型转换。

    catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。

    const 转换以及数组和指针的转换例程:

    #include <iostream>
    using namespace std;
    
    int main(){
        int nums[] = {1, 2, 3};
        try{
            throw nums;
        }catch(const int *){
            cout<<"const int * 异常"<<endl;
        }
    
        return 0;
    }
    
    image.png

    nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *。


    18.3 throw(抛出异常)

    throw 关键字来显式地抛出异常,它的用法为:

    throw 异常数据;
    

    异常数据,可以包含任意的信息,由程序员决定,可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。

    char str[] = "https://www.baidu.com/";
    char *pstr = str;
    
    class Base{};
    Base obj;
    
    throw 100;  //int 类型
    throw str;  //数组类型
    throw pstr;  //指针类型
    throw obj;  //对象类型
    

    动态数组异常例程

    #include <iostream>
    #include <cstdlib>
    using namespace std;
    
    //自定义的异常类型
    class OutOfRange{
    public:
        OutOfRange(): m_flag(1){ }
        OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
    public:
        void what() const;  //获取具体的错误信息
    private:
        int m_flag;  //不同的flag表示不同的错误
        int m_len;  //当前数组的长度
        int m_index;  //当前使用的数组下标
    };
    
    void OutOfRange::what() const {
        if(m_flag == 1){
            cout<<"空数组,元素无法获取"<<endl;
        }else if(m_flag == 2){
            cout<<"数组越界当前长度是:"<<m_len<<", 使用了 "<<m_index<<" 下标位置"<<endl;
        }else{
            cout<<"未知异常"<<endl;
        }
    }
    
    //实现动态数组
    class Array{
    public:
        Array();
        ~Array(){ free(m_p); }
    public:
        int operator[](int i) const;  //获取数组元素
        int push(int ele);  //在末尾插入数组元素
        int pop();  //在末尾删除数组元素
        int length() const{ return m_len; }  //获取数组长度
    private:
        int m_len;  //数组长度
        int m_capacity;  //当前的内存能容纳多少个元素
        int *m_p;  //内存指针
    private:
        static const int m_stepSize = 50;  //每次扩容的步长
    };
    
    Array::Array(){
        m_p = (int*)malloc( sizeof(int) * m_stepSize );
        m_capacity = m_stepSize;
        m_len = 0;
    }
    int Array::operator[](int index) const {
        if( index<0 || index>=m_len ){  //判断是否越界
            throw OutOfRange(m_len, index);  //抛出异常(创建一个匿名对象)
        }
    
        return *(m_p + index);
    }
    int Array::push(int ele){
        if(m_len >= m_capacity){  //如果容量不足就扩容
            m_capacity += m_stepSize;
            m_p = (int*)realloc( m_p, sizeof(int) * m_capacity );  //扩容
        }
    
        *(m_p + m_len) = ele;
        m_len++;
        return m_len-1;
    }
    int Array::pop(){
        if(m_len == 0){
             throw OutOfRange();  //抛出异常(创建一个匿名对象)
        }
    
        m_len--;
        return *(m_p + m_len);
    }
    
    //打印数组元素
    void printArray(Array &arr){
        int len = arr.length();
    
        //判断数组是否为空
        if(len == 0){
            cout<<"空数组无元素,无法打印"<<endl;
            return;
        }
    
        for(int i=0; i<len; i++){
            if(i == len-1){
                cout<<arr[i]<<endl;
            }else{
                cout<<arr[i]<<", ";
            }
        }
    }
    
    int main(){
        Array nums;
        //向数组中添加十个元素
        for(int i=0; i<10; i++){
            nums.push(i);
        }
        printArray(nums);
    
        //尝试访问第20个元素
        try{
            cout<<nums[20]<<endl;
        }catch(OutOfRange &e){
            e.what();
        }
    
        //尝试弹出20个元素
        try{
            for(int i=0; i<20; i++){
                nums.pop();
            }
        }catch(OutOfRange &e){
            e.what();
        }
    
        printArray(nums);
    
        return 0;
    }
    
    
    image.png

    18.4 throw 用作异常规范

    throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范,也称为异常指示符或异常列表。

    double func (char param) throw (int);
    

    这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。

    如果函数会抛出多种类型的异常,那么可以用逗号隔开:

    double func (char param) throw (int, char, exception);
    

    如果函数不会抛出任何异常,那么( )中什么也不写:

    double func (char param) throw ();
    

    如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。

    18.4.1 虚函数中的异常规范

    派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。

    class Base{
    public:
        virtual int fun1(int) throw();
        virtual int fun2(int) throw(int);
        virtual string fun3() throw(int, string);
    };
    class Derived:public Base{
    public:
        int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
        int fun2(int) throw(int);   //对!有相同的异常规范
        string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
    }
    
    

    18.4.2 异常规范与函数定义和函数声明

    异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。

    //错!定义中有异常规范,声明中没有
    void func1();
    void func1() throw(int) { }
    
    //错!定义和声明中的异常规范不一致
    void func2() throw(int);
    void func2() throw(int, bool) { }
    
    //对!定义和声明中的异常规范严格一致
    void func3() throw(float, char*);
    void func3() throw(float, char*) { }
    

    注意:异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。我们只是了解这个知识就可以了。


    18.5 C++异常基类exception

    exception 类位于 <exception> 头文件中,它被声明为:

    class exception{
    public:
      exception () throw(); //构造函数
      exception (const exception&) throw(); //拷贝构造函数
      exception& operator= (const exception&) throw(); //运算符重载
      virtual ~exception() throw(); //虚析构函数
      virtual const char* what() const throw(); //虚函数
    }
    

    what() 函数返回一个能识别异常的字符串,C++标准并没有规定这个字符串的格式,各个编译器的实现也不同,所以 what() 的返回值仅供参考。


    exception 类的继承层次 exception 类的直接派生类 logic_error 的派生类 runtime_error 的派生类

    相关文章

      网友评论

        本文标题:18. C++异常

        本文链接:https://www.haomeiwen.com/subject/euslwqtx.html