1 异常基本概念

提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。也就是《C++ primer》中说的:将问题检测和问题处理相分离。
一种思想:在所有支持异常处理的编程语言中(例如java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误”。当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。
异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等)。

在C语言中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。当然C++中仍然是可以用这两种方法的。
这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。
还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。

c++异常机制相比C语言异常处理的优势?

  1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
  2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
  3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
  4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
  1. //如果判断返回值,那么返回值是错误码还是结果?
  2. //如果不判断返回值,那么b==0时候,程序结果已经不正确
  3. //A写的代码
  4. int A_MyDivide(int a,int b){
  5. if (b == 0){
  6. return -1;
  7. }
  8. return a / b;
  9. }
  10. //B写的代码
  11. int B_MyDivide(int a,int b){
  12. int ba = a + 100;
  13. int bb = b;
  14. int ret = A_MyDivide(ba, bb); //由于B没有处理异常,导致B结果运算错误
  15. return ret;
  16. }
  17. //C写的代码
  18. int C_MyDivide(){
  19. int a = 10;
  20. int b = 0;
  21. int ret = B_MyDivide(a, b); //更严重的是,由于B没有继续抛出异常,导致C的代码没有办法捕获异常
  22. if (ret == -1){
  23. return -1;
  24. }
  25. else{
  26. return ret;
  27. }
  28. }
  29. //所以,我们希望:
  30. //1.异常应该捕获,如果你捕获,可以,那么异常必须继续抛给上层函数,你不处理,不代表你的上层不处理
  31. //2.这个例子,异常没有捕获的结果就是运行结果错的一塌糊涂,结果未知,未知的结果程序没有必要执行下去

2 异常语法

2.1 异常的基本语法

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include<iostream>
  3. using namespace std;
  4. #include <string>
  5. class MyException
  6. {
  7. public:
  8. void printError()
  9. {
  10. cout << "我自己的异常" << endl;
  11. }
  12. };
  13. class Person
  14. {
  15. public:
  16. Person()
  17. {
  18. cout << "Person的默认构造函数调用" << endl;
  19. }
  20. ~Person()
  21. {
  22. cout << "Person的析构函数调用" << endl;
  23. }
  24. };
  25. int myDivision(int a , int b)
  26. {
  27. if ( b == 0)
  28. {
  29. //return -1;
  30. //throw 1; //抛出int类型的异常
  31. //throw 'a'; //抛出char类型的异常
  32. //throw 3.14; //抛出double类型的异常
  33. /*string str = "abc";
  34. throw str;*/
  35. //从try代码块开始,到throw抛出异常之前,所有栈上的数据都会被释放掉,
  36. //释放的顺序和创建顺序相反的,这个过程我们称为栈解旋
  37. Person p1;
  38. Person p2;
  39. throw MyException(); //抛出 MyException的匿名对象
  40. }
  41. return a / b;
  42. }
  43. void test01()
  44. {
  45. int a = 10;
  46. int b = 0;
  47. //C语言处理异常 有缺陷:返回值不统一,返回值只有一个,无法区分是结果还是异常
  48. //int ret =myDivision(a, b);
  49. //if ( ret == -1)
  50. //{
  51. // cout << "异常" << endl;
  52. //}
  53. try
  54. {
  55. myDivision(a, b);
  56. }
  57. catch (int)
  58. {
  59. cout << "int类型异常捕获" << endl;
  60. }
  61. catch (char)
  62. {
  63. cout << "char类型异常捕获" << endl;
  64. }
  65. catch (double)
  66. {
  67. //捕获到了异常,但是不想处理,继续向上抛出这个异常
  68. //异常必须有函数进行处理,如果没有任何处理,程序自动调用 terminate 函数,让程序中断
  69. throw;
  70. cout << "double类型异常捕获" << endl;
  71. }
  72. catch (MyException e)
  73. {
  74. e.printError();
  75. }
  76. catch (...)
  77. {
  78. cout << "其他类型异常捕获" << endl;
  79. }
  80. }
  81. int main(){
  82. try
  83. {
  84. test01();
  85. }
  86. catch (double)
  87. {
  88. cout << "double函数中 double类型异常捕获" << endl;
  89. }
  90. catch (...)
  91. {
  92. cout << "main函数中 其他类型异常捕获" << endl;
  93. }
  94. system("pause");
  95. return EXIT_SUCCESS;
  96. }
  97. /* 输出结果
  98. Person的默认构造函数调用
  99. Person的默认构造函数调用
  100. Person的析构函数调用
  101. Person的析构函数调用
  102. 我自己的异常
  103. */

总结:

  1. 若有异常则通过throw操作创建一个异常对象并抛出。
  2. 将可能抛出异常的程序段放到try块之中。
  3. 如果在try段执行期间没有引起异常,那么跟在try后面的catch字句就不会执行。
  4. catch子句会根据出现的先后顺序被检查,匹配的catch语句捕获并处理异常(或继续抛出异常)
  5. 如果匹配的处理未找到,则运行函数terminate将自动被调用,其缺省功能调用abort终止程序。
  6. 处理不了的异常,可以在catch的最后一个分支,使用throw,向上抛。

c++异常处理使得异常的引发和异常的处理不必在一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。

2.2 异常严格类型匹配

异常机制和函数机制互不干涉,但是捕捉方式是通过严格类型匹配

  1. void TestFunction(){
  2. cout << "开始抛出异常..." << endl;
  3. //throw 10; //抛出int类型异常
  4. //throw 'a'; //抛出char类型异常
  5. //throw "abcd"; //抛出char*类型异常
  6. string ex = "string exception!";
  7. throw ex;
  8. }
  9. int main(){
  10. try{
  11. TestFunction();
  12. }
  13. catch (int){
  14. cout << "抛出Int类型异常!" << endl;
  15. }
  16. catch (char){
  17. cout << "抛出Char类型异常!" << endl;
  18. }
  19. catch (char*){
  20. cout << "抛出Char*类型异常!" << endl;
  21. }
  22. catch (string){
  23. cout << "抛出string类型异常!" << endl;
  24. }
  25. //捕获所有异常
  26. catch (...){
  27. cout << "抛出其他类型异常!" << endl;
  28. }
  29. system("pause");
  30. return EXIT_SUCCESS;
  31. }

2.3 栈解旋(unwinding)

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding).

  1. class Person{
  2. public:
  3. Person(string name){
  4. mName = name;
  5. cout << mName << "对象被创建!" << endl;
  6. }
  7. ~Person(){
  8. cout << mName << "对象被析构!" << endl;
  9. }
  10. public:
  11. string mName;
  12. };
  13. void TestFunction(){
  14. Person p1("aaa");
  15. Person p2("bbb");
  16. Person p3("ccc");
  17. //抛出异常
  18. throw 10;
  19. }
  20. int main(){
  21. try{
  22. TestFunction();
  23. }
  24. catch (...){
  25. cout << "异常被捕获!" << endl;
  26. }
  27. system("pause");
  28. return EXIT_SUCCESS;
  29. }

2.4 异常接口声明

  1. 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类型的异常。
  2. 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()
  3. 一个不抛任何类型异常的函数可声明为:void func() throw()
  4. 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted函数会被调用,该函数默认行为调用terminate函数中断程序。
  1. //可抛出所有类型异常
  2. void TestFunction01(){
  3. throw 10;
  4. }
  5. //只能抛出int char char*类型异常
  6. void TestFunction02() throw(int,char,char*){
  7. string exception = "error!";
  8. throw exception;
  9. }
  10. //不能抛出任何类型异常
  11. void TestFunction03() throw(){
  12. throw 10;
  13. }
  14. int main(){
  15. try{
  16. //TestFunction01();
  17. //TestFunction02();
  18. //TestFunction03();
  19. }
  20. catch (...){
  21. cout << "捕获异常!" << endl;
  22. }
  23. system("pause");
  24. return EXIT_SUCCESS;
  25. }

2.5 异常变量生命周期

  1. throw的异常是有类型的,可以是数字、字符串、类对象。
  2. throw的异常是有类型的,catch需严格匹配异常类型 ```cpp

    define _CRT_SECURE_NO_WARNINGS

    include

    using namespace std;

class MyException { public: MyException() { cout << “MyException默认构造函数调用” << endl; }

  1. MyException(const MyException & e)
  2. {
  3. cout << "MyException拷贝构造函数调用" << endl;
  4. }
  5. ~MyException()
  6. {
  7. cout << "MyException析构函数调用" << endl;
  8. }

};

void doWork() { throw new MyException(); }

void test01() { try { doWork(); } //抛出的是 throw MyException(); catch (MyException e) 调用拷贝构造函数 效率低 //抛出的是 throw MyException(); catch (MyException &e) 只调用默认构造函数 效率高 推荐 //抛出的是 throw &MyException(); catch (MyException e) 对象会提前释放掉,不能在非法操作 //抛出的是 throw new MyException(); catch (MyException e) 只调用默认构造函数 自己要管理释放 catch (MyException *e) { cout << “自定义异常捕获” << endl; delete e; }

}

int main(){ test01();

system("pause");
return EXIT_SUCCESS;

}

/ 输出结果 MyException默认构造函数调用 自定义异常捕获 MyException析构函数调用 /



<a name="uT29n"></a>
### 2.6 异常的多态使用
```cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

//异常的基类
class BaseException
{
public:
    virtual void printError() = 0;
};

//空指针异常
class NULLPointerException :public BaseException
{
public:
    virtual void printError()
    {
        cout << "空指针异常" << endl;
    }
};

//越界异常
class OutOfRangeException :public BaseException
{
public:
    virtual void printError()
    {
        cout << "越界异常" << endl;
    }

};

void doWork()
{
    //throw NULLPointerException();
    throw OutOfRangeException();
}

void test01()
{
    try
    {
        doWork();
    }
    catch (BaseException & e)
    {
        e.printError();
    }
}


int main(){

    test01();

    system("pause");
    return EXIT_SUCCESS;
}

3 C++标准异常库

3.1 标准库介绍

标准库中也提供了很多的异常类,它们是通过类继承组织起来的。异常类继承层级结构图如下:
image.png
每个类所在的头文件在图上方标识出来

标准异常类的成员:

  1. 在上述继承体系中,每个类都有提供了构造函数、复制构造函数、和赋值操作符重载。
  2. logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述
  3. 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。

标准异常类的具体描述:

异常名称 描述
exception 所有标准异常类的父类
bad_alloc 当operator new and operator new[],请求分配内存失败时
bad_exception 这是个特殊的异常,如果函数的异常抛出列表里声明了bad_exception异常,当函数内部抛出了异常抛出列表中没有的异常,这是调用的unexpected函数中若抛出异常,不论什么类型,都会被替换为bad_exception类型
bad_typeid 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出bad_typeid异常
bad_cast 使用dynamic_cast转换引用失败的时候
ios_base::failure io操作过程出现错误
logic_error 逻辑错误,可以在运行前检测的错误
runtime_error 运行时错误,仅在运行时才可以检测的错误

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 算术计算下溢
invalid_argument 参数不合适。在标准库中,当利用string对象构造bitset时,而string中的字符不是’0’或’1’的时候,抛出该异常
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include <stdexcept> //  std 标准  except 异常


class Person
{
public:
    Person(int age)
    {
        if (age < 0 || age > 150)
        {
            throw out_of_range("年龄必须在 0 ~ 150之间");
            //throw length_error("年龄必须在 0 ~ 150之间");
        }
        else
        {
            this->m_Age = age;
        }
    }

    int m_Age;
};

void test01()
{
    try
    {
        Person p(151);
    }
    //catch ( out_of_range &e)
    catch ( exception &e)
    {
        cout << e.what() << endl;
    }

}

int main(){
    test01();


    system("pause");
    return EXIT_SUCCESS;
}
/* 输出结果
年龄必须在 0 ~ 150之间
*/

3.2 编写自己的异常类

  1. 标准库中的异常是有限的;
  2. 在自己的异常类中,可以添加自己的信息。(标准库中的异常类值允许设置一个用来描述异常的字符串)。

编写自己的异常类的方法

  1. 建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时。
  2. 当继承标准异常类时,应该重载父类的what函数和虚析构函数。
  3. 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class MyOutOfRangeException:public exception
{
public:

    MyOutOfRangeException( const char * str)
    {
        //const char * 可以隐式类型转换为 string  反之不可以
        this->m_errorInfo = str;
    }

    MyOutOfRangeException(string str)
    {
        this->m_errorInfo = str;
    }

    virtual const char *  what() const
    {
        //将string 转为 const char * 
        return m_errorInfo.c_str();
    }

    string m_errorInfo;
};

class Person
{
public:
    Person(int age)
    {
        if (age < 0 || age > 150)
        {
            throw MyOutOfRangeException( string( "年龄必须在0到150之间"));
        }
        else
        {
            this->m_Age = age;
        }
    }

    int m_Age;
};


void test01()
{
    try
    {
        Person p(1000);
    }
    catch (exception & e)
    {
        cout << e.what() << endl;
    }


}

int main(){

    test01();

    system("pause");
    return EXIT_SUCCESS;
}
/* 输出结果
年龄必须在 0 ~ 150之间
*/