经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
异常处理
来源:cnblogs  作者:ReFantasy  时间:2018/12/7 9:33:23  对本文有异议

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

异常处理机制

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

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

主程序大概是这样:

  1. class BookISBN
  2. {
  3. public:
  4. // ...
  5. std::string GetISBN() const; // 成员函数,返回ISBN编号
  6. // ...
  7. }
  8. // ...
  9. int main()
  10. {
  11. Init(); // 初始化系统
  12. while(1)
  13. {
  14. run(); // 运行系统
  15. }
  16. return 0;
  17. }

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

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

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

现在我们思考两个问题:

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

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

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

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

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

异常处理机制的流程

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

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

异常处理机制的流程是:

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

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

抛出异常对象

我们先定义一个异常类型

  1. class Error
  2. {
  3. public:
  4. Error() = default;
  5. Error(string error_type):_error_type(error_type){}
  6. Error(const Error &error) = default; // 使用合成的拷贝构造函数
  7. private:
  8. string _error_type;
  9. }

那么如何抛出异常呢?

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

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

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

  1. // 系统中的类类型
  2. class BookISBN
  3. {
  4. public:
  5. // ...
  6. std::string GetISBN() const; // 成员函数,返回ISBN编号
  7. }
  8. // 异常对象类型
  9. class Error
  10. {
  11. public:
  12. Error() = default;
  13. Error(string error_type):_error_type(error_type){}
  14. Error(const Error &error) = default; // 使用合成的拷贝构造函数
  15. private:
  16. string _error_type;
  17. }
  18. // 主函数
  19. void run()
  20. {
  21. // ...
  22. if(book_a.GetISBN() == book_b.GetISBN()) // book_a book_b 假设已经定义
  23. {
  24. // 继续后面的处理
  25. }
  26. else
  27. {
  28. /************************************************************/
  29. * 抛出异常 *
  30. * 异常的处理交给异常接收代码(后面介绍如何接收异常) *
  31. *************************************************************/
  32. throw Error("错误!两本书的ISBN不一样,不能进行比较!"); // 抛出异常
  33. }
  34. }
  35. // 主程序
  36. int main()
  37. {
  38. Init(); // 初始化系统
  39. while(1)
  40. {
  41. run(); // 运行系统
  42. }
  43. return 0;
  44. }

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

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

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

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

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

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

假设类Derived继承自类Base

  1. Derived d;
  2. Base &rd = d;
  3. throw rd;

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

接收异常对象并处理

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

具体的用法入下代码:

  1. try
  2. {
  3. // 这里是包含可能抛出异常的代码
  4. }
  5. catch(ErrorType1 error) //
  6. {
  7. // 处理异常。想怎么处理就怎么写呗。
  8. }
  9. catch(ErrorType2 error)
  10. {
  11. // 同上
  12. // catch 分支可以有一个也可以有多个,看自己需要
  13. }
  14. ...

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

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

  1. // 系统中的类类型
  2. class BookISBN
  3. {
  4. public:
  5. // ...
  6. std::string GetISBN() const; // 成员函数,返回ISBN编号
  7. }
  8. // 异常对象类型
  9. class Error
  10. {
  11. public:
  12. Error() = default;
  13. Error(string error_type):_error_type(error_type){}
  14. Error(const Error &error) = default; // 使用合成的拷贝构造函数
  15. private:
  16. string _error_type;
  17. }
  18. // 主函数
  19. void run()
  20. {
  21. // ...
  22. if(book_a.GetISBN() == book_b.GetISBN()) // book_a book_b 假设已经定义
  23. {
  24. // 继续后面的处理
  25. }
  26. else
  27. {
  28. throw Error("错误!两本书的ISBN不一样,不能进行比较!"); // 抛出异常
  29. }
  30. }
  31. // 主程序
  32. int main()
  33. {
  34. Init(); // 初始化系统
  35. while(1)
  36. {
  37. /************************************************************/
  38. * 将可能抛出异常的代码用 try 块包含 *
  39. * 一旦 try 中的代码抛出异常 跟随 try 后面的 catch 便会捕捉到抛出 *
  40. * 的异常。(如果没有匹配的catch块就继续往上一层搜索,如果最后没有任 *
  41. * 何相匹配的catch,则程序直接终止。 *
  42. *************************************************************/
  43. try
  44. {
  45. run(); // 运行系统
  46. }
  47. catch(Error e)
  48. {
  49. // 处理异常
  50. }
  51. }
  52. return 0;
  53. }

另外需要注意的三点说明:

  • 注意catch块的接收顺序,尤其是异常类型是具有继承关系的类型。

    1. class Base;
    2. class Derived:public Base;
    3. void f()
    4. {
    5. try
    6. {
    7. throw Derived(); // 抛出 Derived 类型的异常
    8. }
    9. catch(Base b)
    10. {
    11. // 异常会被 Base 接收。(进行了隐式类型转换。这是和赋值操作一样的嘛,很好理解。)
    12. }
    13. catch(Derived d)
    14. {
    15. // 异常不会进入这里
    16. }
    17. }

    catch会进行的隐式类型转换只有三种:常量->非常量、子类->父类、数组->指向数组首元素的指针(函数类似)。其它的任何类型都不会隐式转换,比如catch(int) 不能就收double类型的异常对象。

  • 重抛出

    如果直接throw;不跟异常对象,那么就会抛出当前作用域内的异常对象,如果当前作用域没有则抛出上层作用域内的对象。

    1. try
    2. {
    3. // ...
    4. }
    5. catch(Error e)
    6. {
    7. // ...
    8. throw ; // 相当于 throw e;
    9. }
  • 捕获所有的异常

    如果catch的参数是...catch(...)则这个catch块将接收当前作用域内(包括嵌套的内存作用域)之前所有没被接收的异常对象。

异常处理的其它相关细节

构造函数初始化列表抛出异常

  1. class Test
  2. {
  3. public:
  4. Test(Object ob)
  5. :_ob(bo) // 如果此时抛出异常,那么构造函数内的异常处理并不能接收到
  6. { // 这个异常将会跑到构造Test类的作用域中,而且未构造完的Test对象并不会析构
  7. // ...
  8. try
  9. {
  10. }
  11. catch
  12. {
  13. }
  14. }
  15. private:
  16. Object _ob;
  17. }

替换的方法如下:

  1. class Test
  2. {
  3. public:
  4. Test(Object ob) try:_ob(bo)
  5. {
  6. // ...
  7. }
  8. catch(...) // catch既能接收初始化列表抛出的异常
  9. { // 也能够接收构造函数块里面的异常
  10. }
  11. private:
  12. Object _ob;
  13. }

noexcept说明符

noexcept的意思是不抛出异常

  1. void f()noexcept
  2. {
  3. }

如果声明了 noexcept的函数抛出了异常,则程序直接终止。局部对象是否被释放是未定义的。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号