C++ 异常机制详解

一、异常处理入门

程序的错误大致分为三种:

  1. 语法错误,在编译和链接阶段就能发现;
  2. 逻辑错误,可以通过调试解决;
  3. 运行时错误,异常机制是为解决此类错误引入。

一个运行时错误的例子

int main(){
    string str = "asdfa";
    char ch1 = str[10];     // 越界
    cout << ch1 << endl;    // 程序崩溃
    char ch2 = str.at(100);     // 越界,抛出异常
    cout << ch2 << endl;
    return 0;
}

修改代码,使用异常机制捕获异常:

int main(){
    string str = "asdfa";
    try{
        char ch1 = str[10];     // 越界
        cout << ch1 << endl;    // 程序崩溃
    }catch(exception &e){       // 不会捕获,因为[]不会检查下标,不会抛出异常
        cout << "[1]out of bound!" << endl;
    }

    try{
        char ch2 = str.at(100);     // 越界,抛出异常
        cout << ch2 << endl;      
    }catch(exception &e){
        cout << "[2]out of bound!" << endl;
    }

    return 0;
}
运行结果:
[2]out of bound!

try-catch 语法:

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

发生异常时必须将异常明确地抛出,try 才能检测到。

当异常点跳转到 catch 所在位置时,位于异常点之后,且在当前 try 语句块内的语句都不会再执行,即使 catch 成功处理了错误。

异常可以发生在当前 try 块中,也可以发生在 try 块中调用的某个函数,或者所调用函数调用的另外一个函数中。发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇到 try 才停止,调用链中剩下的未被执行代码都会跳过,没有执行机会。

二、异常类型及多级 catch 匹配

C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。

可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。

但 catch 和真正的函数调用相比又有区别,多了一个「在运行阶段将实参和形参匹配」的过程。

如果不希望 catch 处理异常数据,也可以将 variable 省略掉,也即写作:

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

多级 catch

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

异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接受的类型逐个匹配,一旦找到类型匹配的 catch 就停止,如果没有找到,会交给系统处理,终止程序。

class Base{};
class Derived : public Base{};

int main(){
    try{
        throw Derived();    // 抛出异常,实际上是创建一个 Derived 类型的匿名对象
        cout << "此语句不会再执行" << endl;
    }catch(int){
         cout<<"Exception type: int"<<endl;
    }catch(Base){   // 匹配成功,向上转型
         cout<<"Exception type: Base"<<endl;
    }catch(Derived){
         cout<<"Exception type: Derived"<<endl;
    }
    return 0;
}

catch 匹配过程中的类型转换

普通函数(非函数模板)实参和形参类型转换:

  • 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  • 向上转型:也就是派生类向基类的转换,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
  • const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  • 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  • 用户自定的类型转换。

catch 异常匹配中的转换只包括:1)「向上转型」 2)「const 转换」 3)「数组或函数指针转换」。其他的都不能应用于 catch。

三、throw(抛出异常)

异常处理流程:

抛出(Throw)--> 检测(Try) --> 捕获(Catch)

通过 throw 关键字来显式抛出异常,语法为:

throw exceptionData;

exceptionData 是“异常数据”,可以是基本类型,也可以是聚合类型。

string str = "fasdf";
string *pstr = str;

class Base{};
Base obj;

throw 100;
throw str;
throw pstr;
throw obj;

动态数组例子

// 自定义异常类
class OutOfRange{
public:
    OutOfRange():m_flag(1){};
    OutOfRange(int len, int index):m_len(len), m_index(index), m_flag(2){}
    void what() const; // 获取具体错误信息
private:
    int m_flag;     // 错误类型标识
    int m_len;      // 当前数组长度
    int m_index;    // 当前使用数组下标
};

void OutOfRange::what() const {
    if(m_flag == 1){
        cout<<"Error: empty array, no elements to pop."<<endl;
    }else if(m_flag == 2){
        cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
    }else{
        cout<<"Unknown exception."<<endl;
    }
}

// 动态数组
class Array{
public:
    Array();
    ~Array(){free(m_p);}
    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;           // 内存指针
    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<<"Empty array! No elements to print."<<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);
    
    try{        //尝试访问第 20 个元素
        cout<<nums[20]<<endl;
    }catch(OutOfRange &e){
        e.what();       // Error: out of range( array length 10, access index 20 )
    }
    
    try{        // 尝试弹出 20 个元素
        for(int i=0; i<20; i++)
            nums.pop();
    }catch(OutOfRange &e){
        e.what();       // Error: empty array, no elements to pop.
    }
    
    printArray(nums);   // Empty array! No elements to print.
    return 0;
}

throw 用作异常规范(C++11 后弃用)

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

double func(char param) throw (int);

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

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

如果函数不会抛出任何异常,那么( )中什么也不写,这样函数不能抛出任何异常,即使抛出 try 也检测不到:

double func (char param) throw ();

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) 更严格
}

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

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

四、C++异常的基类 exception

C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。

try{
    // ...
}catch(exception &e){   // 使用引用是为了提高效率,不使用引用会执行一次对象拷贝
    // ...
}
class exception{
public:
    exception () throw(); //构造函数
    exception (const exception&) throw(); //拷贝构造函数
    exception& operator= (const exception&) throw();    //运算符重载
    virtual ~exception() throw(); //虚析构函数
    virtual const char* what() const throw(); //虚函数
};

下图展示了 exception 类的继承层次:

exception 类的直接派生类:

异常名称 说 明
logic_error 逻辑错误。
runtime_error 运行时错误。
bad_alloc 使用 new 或 new[ ] 分配内存失败时抛出的异常。
bad_typeid 使用 typeid 操作一个 NULL 指针,而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。
bad_cast 使用 dynamic_cast 转换失败时抛出的异常。
ios_base::failure io 过程中出现的异常。
bad_exception 这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。

logic_error 的派生类:

异常名称 说明
length_error 试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。
domain_error 参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。
out_of_range 超出有效范围。
invalid_argument 参数不合适。在标准库中,当利用 string 对象构造 bitset 时,而 string 中的字符不是 0 或 1 的时候,抛出该异常。

runtime_error 的派生类:

异常名称 说 明
range_error 计算结果超出了有意义的值域范围。
overflow_error 算术计算上溢。
underflow_error 算术计算下溢。
上一篇:C++学习笔记之 异常


下一篇:进阶练习:手写JavaScript数组多个方法的底层实现