首页 > 编程笔记

C++异常处理机制(非常详细)

在 C++ 中,如果函数在调用时发生异常,异常通常会被传递给函数的调用者进行处理,而不在发生异常的函数内部处理。

如果函数调用者也不能处理异常,则异常会继续向上一层调用者传递,直到异常被处理为止。如果最终异常没有被处理,则 C++ 运行系统就会捕捉异常,终止程序运行。

异常处理方式

C++ 的异常处理机制使得异常的引发和处理不必在同一函数中完成,函数的调用者可以在适当的位置对函数抛出的异常进行处理。这样,底层的函数可以着重解决具体的业务问题,而不必考虑对异常的处理。

C++ 的异常处理通过throw关键字和try…catch语句结构实现。

通常情况下,被调用的函数如果发生异常,就通过 throw 关键字抛出异常,而函数的上层调用者通过 try…catch 语句检测、捕获异常,并对异常进行处理。

throw 关键字抛出异常的语法格式如下所示:

throw 表达式;

在上述格式中,throw 后面的表达式可以是常量、变量或对象。如果函数调用中出现异常,就可以通过 throw 将表示异常的表达式抛给它的调用者。

函数调用者通过 try…catch 语句捕获、处理异常,try…catch 语句的语法格式如下所示:

try
{
…       //可能会出现异常的代码
}
catch (异常类型1)
{
…        //异常处理代码
}
catch (异常类型2) 
{
…        //异常处理代码
}

catch (异常类型n) 
{
…        //异常处理代码
}
catch (...)
{
…        //异常处理代码
}

在上述语法格式中,try 语句块用于检测可能发生异常的代码(函数调用)。

如果这段代码抛出了异常,则 catch 语句会依次对抛出的异常进行类型匹配;如果某个 catch 语句中的异常类型与抛出的异常类型相同,则该 catch 语句就捕获异常并对异常进行处理。

在使用 try…catch 语句时,有以下几点需要注意:
在使用 try…catch 语句处理异常时,如果 try 语句块中的某一行代码抛出了异常,则无论异常是否被处理,抛出异常的语句后面的代码都不再被执行。例如,有如下代码:
try
{
func();
add();
cout << 3/0<< endl;
}
catch (int)
{
cout << "异常处理" << endl;
}
cout << "异常处理完毕,程序从此处开始向下执行" << endl;
在上述代码中,如果 try 语句块中的 func() 函数调用抛出了异常,并且 catch 语句成功捕获到了异常,则异常处理结束之后,程序会执行 try…catch 语句后面的代码,而不会执行 try 语句块中的 add() 函数。

示例1

下面通过案例演示 C++ 的异常处理,代码如下:
#include<iostream>
#include<fstream>
using namespace std;
class AbstractException   //定义抽象异常类AbstractException
{
public:
    virtual void printErr() = 0;   //纯虚函数printErr()
};
//定义文件异常类FileException公有继承AbstractException
class FileException : public AbstractException
{
public:
    virtual void printErr()   //实现printErr()函数
    {
        cout << "错误:文件不存在" << endl;
    }
};
//定义整除异常类DivideException公有继承AbstractException
class DivideException:public AbstractException
{
public:
    virtual void printErr()   //实现printErr()函数
    {
        cout << "错误:除零异常" << endl;
    }
};
void readFile()   //定义readFile()函数
{
    ifstream ifs("log.txt");   //创建文件输入流对象ifs并打开log.txt文件
    if(!ifs)       //如果文件打开失败
    {
        throw FileException();  //抛出异常
    }
    ifs.close();   //关闭文件
}
void divide()   //定义divide()函数
{
    int num1 = 100;
    int num2 = 2;
    if(num2 == 0)   //如果除数num2为0
    {
        throw DivideException();   //抛出异常
    }
    int ret = num1/num2;
    cout << "两个数相除结果:" << ret << endl;
}
int main()
{
    try  
    {
        readFile();   //检测readFile()函数调用
        divide();   //检测divide()函数调用
    }
    catch(FileException& fex)  //捕获FileException&类型异常
    {
        fex.printErr();   //调用相应函数输出异常信息
    }
    catch(DivideException& dex)   //捕获DivideException&类型异常
    {
        dex.printErr();
    }
    catch(...)   //捕获任意类型异常 
    {
        cout << "处理其他异常" << endl;
    }
    cout << "程序执行结束" << endl;
    return 0;
}
运行结果:

错误:文件不存在
程序执行结束

示例分析:
由运行结果可知,程序输出了“文件不存在”的错误提示信息,这表明在调用 readFile() 函数时,由于文件 log.txt 不存在,readFile() 抛出了异常。

通过第 54 行代码的 catch 语句捕获了该异常,在 catch 语句块中通过对象 fex 调用 printErr() 函数输出了异常信息。同时,catch 语句捕获异常之后,程序直接执行了第 66 行代码,并没有返回执行第 52 行代码的 divide() 函数。

栈解旋

C++ 不仅能够处理各种不同类型的异常,还可以在异常处理前释放所有局部对象。

从进入 try 语句块开始到异常被抛出之前,在栈上创建的所有对象都会被析构,析构的顺序与构造的顺序相反,这一过程称为栈解旋或栈自旋。

示例2

下面通过案例演示栈的解旋过程,C++ 代码如下:
#include<iostream>
using namespace std;
class Shape   //定义形状类Shape
{
public:
    Shape();   //构造函数
    ~Shape();   //析构函数
    static int count;    //静态成员变量count
};
int Shape::count = 0;   //count初始值为0
Shape::Shape()   //实现构造函数
{
    count++;
    if(Shape::count == 3)
        throw "纸张画不下啦!!";
    cout << "Shape构造函数" << endl;
}
Shape::~Shape()   //实现析构函数
{
    cout << "Shape析构函数" << endl;
}
int main()
{
    Shape circle;   //画圆形
    try   //try语句块检测可能抛出异常的代码
    {
        int num = 2;   //定义int类型变量num,表示纸张可画两个图形
        cout << "纸张可画图形个数:" << num << endl;
        Shape rectangle;  //画长方形
        Shape triangle;   //画三角形
    }
    catch(const char* e)   //捕获异常
    {
        cout << e << endl;
    }
    return 0;
}
运行结果:

Shape构造函数
纸张可画图形个数:2
Shape构造函数
Shape析构函数
纸张画不下啦!!
Shape析构函数

示例分析:
由运行结果可知,程序抛出了异常,catch 语句捕获并输出了异常提示信息:“纸张画不下啦!!”程序在运行时:
在抛出异常之前,程序会将 try 语句块中创建的对象(num和rectangle)都释放。因此,本例异常信息输出之前调用了一次 Shape 析构函数,用来析构对象 rectangle。

在 try 语句块之外创建的对象 circle,待异常处理完成之后才析构。因此,本例异常信息输出之后又调用了一次 Shape 析构函数。

注意,栈解旋只能析构栈上的对象,不会析构动态对象。

优秀文章