设为首页 加入收藏

TOP

异常处理(一)
2018-12-06 16:09:28 】 浏览:144
Tags:异常 处理

异常处理字面的意思就是:当程序出现了不符合预期的情况(不一定是错误),采取一定的后续措施进行处理。

异常处理机制

我们以一个简单但不是很严谨的例子作为开始,来介绍异常处理机制。

假设我们有一个图书销售系统。系统里面有某个自定义类型class BookISBN;,表示某本书的ISBN编号。

主程序大概是这样:

class BookISBN
{
public:
    // ...
    std::string GetISBN() const;  // 成员函数,返回ISBN编号
    // ...
}

// ...
int main()
{
    Init();     // 初始化系统
    while(1)
    {
        run();  // 运行系统
    }
    
    return 0;
}

假设系统的运行过程中需要比较两本图数的价格:

void run()
{
    // ...
    if(book_a.GetISBN() == book_b.GetISBN())        // book_a book_b 假设已经定义
    {
        // 继续后面的处理
    }
    else
    {
        cout<< "错误!两本书的ISBN不一样,不能进行比较!";
        // 进行出错后的其它后续操作,比如释放内存一类的
    }
}

我们可以看到,在上面的程序中,我们可以采用分支条件来处理意外出现的情况。

现在我们思考两个问题:

  • 如果程序不止一处需要做这样的意外处理呢?
  • 如果意外情况是嵌套在系统的深层(上面的 if-else 是被 run 间接调用),而出现错误后需要跳转到上层调用的某个特定位置呢?

针对第一个问题:我们可以将意外处理(也就是异常处理)封装成一个函数,然后在需要的地方调用。

针对第二个问题:我们可以精心设计函数的接口,使其满足我们的处理流程。但这是很困难的一件事。

所以上面给出的解决方案,理论上可以实现我们的需求,但是较为繁琐,对于程序设计的能力要求也就高。

于是,异常处理机制便出现了。异常处理从感官上来看表现得更为优雅,从功能上来说还能将问题的检测和解决分离开,实现统一处理。

异常处理机制的流程

当程序出现异常的时候,我们需要记录下发生的异常信息,比如数组越界,内存不足等等。而记录异常信息的方式便是通过一个对象来保存这些信息,这个对象也叫做异常对象。

异常对象的类型既可以是STL提供的类型,也可以是自定义的类。如果是自定义异常类型,该类型必须具有拷贝构造函数或者移动构造函数

异常处理机制的流程是:

  • 抛出异常对象。在异常发生的位置通过关键字throw抛出一个异常对象
  • 接收异常对象并进行异常处理。在异常发生的作用域内或者外层作用域内接受异常并处理。

下面主要分两个部分介绍异常处理。

抛出异常对象

我们先定义一个异常类型

class Error
{
public:
    Error() = default;
    Error(string error_type):_error_type(error_type){}
    Error(const Error &error) = default;                  // 使用合成的拷贝构造函数
private:
    string _error_type;
}

那么如何抛出异常呢?

通过关键字throw。抛出异常对象的代码如下:

// ...
Error error("这是一个异常");  // 定义异常对象
throw error;                // 抛出异常
// 当然也可以这样 throw Error("这是一个异常");  

在我们的图数销售系统中使用抛出异常的完整代码如下:

// 系统中的类类型
class BookISBN
{
public:
    // ...
    std::string GetISBN() const;  // 成员函数,返回ISBN编号
}

// 异常对象类型
class Error
{
public:
    Error() = default;
    Error(string error_type):_error_type(error_type){}
    Error(const Error &error) = default;                  // 使用合成的拷贝构造函数
private:
    string _error_type;
}

// 主函数
void run()
{
    // ...
    if(book_a.GetISBN() == book_b.GetISBN())        // book_a book_b 假设已经定义
    {
        // 继续后面的处理
    }
    else
    {
        /************************************************************/
        *    抛出异常                                                 *
        *    异常的处理交给异常接收代码(后面介绍如何接收异常)              *
        *************************************************************/
        throw Error("错误!两本书的ISBN不一样,不能进行比较!");  // 抛出异常
    }
}

// 主程序
int main()
{
    Init();     // 初始化系统
    
    while(1)
    {
        run();  // 运行系统
    }
    
    return 0;
}

程序在执行throw error_object后到底做了什么事呢?

  1. 在全局作用域内创建了一个error_object的副本。这个临时全局对象的地址由编译器进行分配管理,在合适的时候(比如异常处理结束)由编译器进行销毁。程序员不用操心。

    所以这也是为什么异常对象必须要有拷贝(移动)构造函数的原因。

  2. 销毁throw语句前已经创建的局部对象。大家可以把throw理解为具有return功能的关键字。

    所以throw之前一定要释放new/malloc的对象防止内存泄漏。

另外需要声明的一点就是,在创建异常对象的全局副本的时候是按照静态类型来拷贝。

假设类Derived继承自类Base

Derived d;
Base &rd = d;
throw rd;

此时,创建的异常对象d的全局副本只包含基类Base的部分。(这个是很自然的事,特别写出来是担心大家有疑虑)

接收异常对象并处理

当我们抛出对象之后,程序就开始搜索可以接收异常对象的代码。接收异常的方式是使用try catch两个关键字。

具体的用法入下代码:

try
{
    // 这里是包含可能抛出异常的代码
}
catch(ErrorType1 error)  // 
{
    // 处理异常。想怎么处理就怎么写呗。
}
catch(ErrorType2 error)
{
    // 同上
    // catch 分支可以有一个也可以有多个,看自己需要
}
...

我们在try的作用域内抛出异常。编译器在外部作用域查找到catch关键字接收异常,然后就像函数调用一样,用异常对象的全局副本作为实参,传进与之类型相匹配的catch块中,然后继续执行代码。

我们把接收异常的代码加入主程序后的完整代码:

// 系统中的类类型
class BookISBN
{
public:
    // ...
    std::string GetISBN() const;  // 成员函数,返回ISBN编号
}

// 异常对象类型
class Error
{
public:
    Error() = default;
    Error(string error_type):_error_type(error_type){}
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇BZOJ1925: [Sdoi2010]地精部落(dp) 下一篇BZOJ1812: [Ioi2005]riv(树形dp)

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目