16、基础 | C++ 异常

@[toc]

C++异常机制概述

异常处理是C++ 的一项语言机制,用于在程序中处理异常事件。异常事件在C++中表示为异常对象。异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,由操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,依次匹配catch语句中的异常对象(==只进行类型匹配,catch参数有时在catch语句中并不会使用到==)。若匹配成功,则执行catch块内的异常处理语句,然后接着执行try...catch...块之后的代码。如果在当前的try...catch...块内找不到匹配该异常对象的catch语句,则由更外层的try...catch...块来处理该异常;如果当前函数内所有的try...catch...块都不能匹配该异常,则递归回退到调用栈的上一层去处理该异常如果一直退到主函数main()都不能处理该异常,则调用系统函数terminate()终止程序。

一个最简单的try...catch...的例子如下所示。我们有个程序用来记班级学生考试成绩,考试成绩分数的范围在0-100之间,不在此范围内视为数据异常:

int main()
{
    int score=0;
    while (cin >> score)
    {
        try
        {
            if (score > 100 || score < 0)
            {
                throw score;
            }
            //将分数写入文件或进行其他操作
        }
        catch (int score)
        {
            cerr << "你输入的分数数值有问题,请重新输入!";
            continue;
        }
    }
}

throw 关键字

在上面这个示例中,throw是个关键字,与抛出表达式构成了throw语句。其语法为:

throw 表达式;

throw语句必须包含在try块中,也可以是被包含在调用栈的外层函数的try块中,如:

//示例代码:throw包含在外层函数的try块中
void registerScore(int score)
{
    if (score > 100 || score < 0)
        throw score; //throw语句被包含在外层main的try语句块中
    //将分数写入文件或进行其他操作
}
int main()
{
    int score=0;
    while (cin >> score)
    {
        try
        {
            registerScore(score);
        }
        catch (int score)
        {
            cerr << "你输入的分数数值有问题,请重新输入!";
            continue;
        }
    }
}

执行throw语句时,==throw表达式将作为对象被复制构造为一个新的对象,称为异常对象==。异常对象放在内存的特殊位置,该位置既不是栈也不是堆在window上是放在线程信息块TIB中。这个构造出来的新对象与本级的try所对应的catch语句进行类型匹配,类型匹配的原则在下面介绍。 image 在本例中,依据score构造出来的对象类型为int,与catch(int score)匹配上,程序控制权转交到catch的语句块,进行异常处理代码的执行。如果在本函数内与catch语句的类型匹配不成功,则在调用栈的外层函数继续匹配,如此递归执行直到匹配上catch语句,或者直到main函数都没匹配上而调用系统函数terminate()终止程序。 当执行一个throw语句时,跟在throw语句之后的语句将不再被执行,throw语句的语法有点类似于return,因此导致在调用栈上的函数可能提早退出。

异常对象

异常对象是一种特殊的对象,编译器依据异常抛出表达式复制构造异常对象,这要求抛出异常表达式不能是一个不完全类型(一个类型在声明之后定义之前为一个不完全类型。不完全类型意味着该类型没有完整的数据与操作描述),而且可以进行复制构造,这就要求异常抛出表达式的复制构造函数(或移动构造函数)、析构函数不能是私有的。

异常对象不同于函数的局部对象,局部对象在函数调用结束后就被自动销毁,==而异常对象将驻留在所有可能被激活的catch语句都能访问到的内存空间中,也即上文所说的TIB==。当异常对象与catch语句成功匹配上后,在该catch语句的结束处被自动析构。

在函数中返回局部变量的引用或指针几乎肯定会造成错误,同样的道理,==在throw语句中抛出局部变量的指针或引用也几乎是错误的行为==。如果指针所指向的变量在执行catch语句时已经被销毁,对指针进行解引用将发生意想不到的后果。

throw出一个表达式时,==该表达式的静态编译类型将决定异常对象的类型==。所以当throw出的是基类指针的解引用,而该指针所指向的实际对象是派生类对象,此时将发生派生类对象切割。

除了抛出用户自定义的类型外,C++标准库定义了一组类,用户报告标准库函数遇到的问题。这些标准库异常类只定义了几种运算,包括创建或拷贝异常类型对象,以及为异常类型的对象赋值。

标准异常类 描述 头文件
exception 最通用的异常类,只报告异常的发生而不提供任何额外的信息 exception
runtime_error 只有在运行时才能检测出的错误 stdexcept
rang_error 运行时错误:产生了超出有意义值域范围的结果 stdexcept
overflow_error 运行时错误:计算上溢 stdexcept
underflow_error 运行时错误:计算下溢 stdexcept
logic_error 程序逻辑错误 stdexcept
domain_error 逻辑错误:参数对应的结果值不存在 stdexcept
invalid_argument 逻辑错误:无效参数 stdexcept
length_error 逻辑错误:试图创建一个超出该类型最大长度的对象 stdexcept
out_of_range 逻辑错误:使用一个超出有效范围的值 stdexcept
bad_alloc 内存动态分配错误 new
bad_cast dynamic_cast类型转换出错 type_info

catch 关键字

catch语句匹配被抛出的异常对象。如果catch语句的参数是引用类型,则该参数可直接作用于异常对象,即参数的改变也会改变异常对象,==而且在catch中重新抛出异常时会继续传递这种改变==。如果catch参数是传值的,则复制构函数将依据异常对象来构造catch参数对象。在该catch语句结束的时候,先析构catch参数对象,然后再析构异常对象。

在进行异常对象的匹配时,编译器不会做任何的隐式类型转换或类型提升。除了以下几种情况外,异常对象的类型必须与catch语句的声明类型完全匹配:

  • 允许从非常量到常量的类型转换。
  • 允许派生类到基类的类型转换。
  • 数组被转换成指向数组(元素)类型的指针。
  • 函数被转换成指向函数类型的指针。

寻找catch语句的过程中,匹配上的未必是类型完全匹配那项,而在是最靠前的第一个匹配上的catch语句(我称它为==最先匹配原则==)。所以,派生类的处理代码catch语句应该放在基类的处理catch语句之前,否则先匹配上的总是参数类型为基类的catch语句,而能够精确匹配的catch语句却不能够被匹配上。

在catch块中,如果在当前函数内无法解决异常,可以继续向外层抛出异常,让外层catch异常处理块接着处理。此时可以使用不带表达式的throw语句将捕获的异常重新抛出:

catch(type x)
{
    //做了一部分处理
    throw;
}

==被重新抛出的异常对象为保存在TIB中的那个异常对象==,与catch的参数对象没有关系,若catch参数对象是引用类型,可能在catch语句内已经对异常对象进行了修改,那么重新抛出的是修改后的异常对象;若catch参数对象是非引用类型,则重新抛出的异常对象并没有受到修改。

使用catch(...){}可以捕获所有类型的异常,根据最先匹配原则,catch(...){}应该放在所有catch语句的最后面,否则无法让其他可以精确匹配的catch语句得到匹配。通常在catch(...){}语句中执行当前可以做的处理,然后再重新抛出异常。注意,catch中重新抛出的异常只能被外层的catch语句捕获。

栈展开、RAII

其实栈展开已经在前面说过,就是从异常抛出点一路向外层函数寻找匹配的catch语句的过程,寻找结束于某个匹配的catch语句或标准库函数terminate。这里重点要说的是栈展开过程中对局部变量的销毁问题。我们知道,在函数调用结束时,函数的局部变量会被系统自动销毁,类似的,throw可能会导致调用链上的语句块提前退出,此时,语句块中的局部变量将按照构成生成顺序的逆序,依次调用析构函数进行对象的销毁。例如下面这个例子:

//一个没有任何意义的类
class A
{
public:
    A() :a(0){ cout << "A默认构造函数" << endl; }
    A(const  A& rsh){ cout << "A复制构造函数" << endl; }
    ~A(){ cout << "A析构函数" << endl; }
private:
    int  a;
};
int main()
{
        try
        {
            A a ;
            throw a;
        }
        catch (A a)
        {
            ;
        }
    return 0;
}

程序将输出:

A默认构造函数
A复制构造函数
A复制构造函数
A析构函数
A析构函数
A析构函数

定义变量a时调用了默认构造函数,使用a初始化异常变量时调用了复制构造函数,使用异常变量复制构造catch参数对象时同

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C/C++面试必考必会 文章被收录于专栏

【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。

全部评论

相关推荐

老板加个卤鸡蛋:HR看了以为来卧底来了
点赞 评论 收藏
分享
03-18 01:22
门头沟学院 Java
多多爱我我爱多多:linkedList 替换 arrayList 是怎么实现20倍提升的 好奇
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务