C++梁哥笔记day21 | 我的日常分享

C++梁哥笔记day21

C++异常

一、异常基本概念

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

一句话:异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0、溢出、数组下标越界、所要读取的文件不存在、空指针、内存不足等等)。

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

C++异常机制相比C语言异常处理的优势。C语言函数的返回值可以忽略,但C++异常不可忽略。有时我们会忘记判断函数返回值,出错了程序并不会终止;但是如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其妙的终止或出现错误的结果。整型的返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。异常处理可以在调用跳级。这是一个代码编写时的问题,假设有多个函数的调用栈出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理,而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。

  • 1、C语言通过返回值来判断错误;第一,容易忽略忘记判断;第二,容易和正常的结果混淆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int myDiv(int a,int b){
    if(b == 0){
    return -1;
    }
    return a/b;
    }

    void test01(){
    int ret = myDiv(10,-10);
    if(ret == -1){
    cout<<"程序异常"<<endl;
    }else{
    cout<<"程序正常"<<endl;
    }
    }

    运行结果:

    image-20210120164618657

    10除以-10是可以的,明明是没有问题,但是最后结果是异常。

    • 2、C++抛出异常并捕获

      抛出异常:throw

      捕获异常:try…catch

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      int myDiv1(int a,int b){
      if(b == 0){
      throw 0;//抛出异常
      }
      return a/b;
      }
      void test02(){
      try{
      int ret = myDiv1(10,0);
      cout<<"ret="<<ret<<endl;
      }catch(int e){//异常是严格的类型匹配,这个是只捕获是异常是int类型
      cout<<"捕获到int类型异常e="<<e<<endl;
      }catch(float e){//这个是只捕获是异常是float类型
      cout<<"捕获到float类型异常e="<<e<<endl;
      }catch(char e){//这个是只捕获是异常是char类型
      cout<<"捕获到char类型异常e="<<e<<endl;
      }catch(...){//只要上面没有列举到的,都到这里
      cout<<"捕获到其他异常"<<endl;
      }
      }

      运行结果:

      image-20210120165442013

      如果没有捕获异常:image-20210120165603705

二、栈解旋(unwinding)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person{
private:
string name;
public:
Person(string name){
this->name = name;
cout<<"有参构造函数"<<endl;
}
~Person(){
cout<<"Person "<<this->name<<"析构函数"<<endl;
}
};
void test03(){
try{
Person ob1("小花1");
Person ob2("小明2");
Person ob3("小王3");
throw 10;
}catch(int e){
cout<<"捕获到了int异常"<<e<<endl;
}
cout<<"其他代码"<<endl;
}

运行结果:

image-20210120233254722

这是一种现象,只不过C++中起的名字比较高大尚。

三、异常接口声明

为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A、B、C及其子类型的异常。如果在函数声明中没有包含异常接口声明,则此函数可以抛出任何类型的异常,例如:void func();,一个不抛出任何类型异常的函数可以声明为:void func() throw();,如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexpected函数会被调用,该函数默认行为调用terminate函数中断程序。

  • 抛出接口中没有的类型异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void fun1() throw(int,char){
    throw 3.14;
    }
    void test04(){
    try{
    fun1();
    }catch(int e){
    cout<<"捕获到了异常int"<<e<<endl;
    }catch(char e){
    cout<<"捕获到了异常char"<<e<<endl;
    }catch(double e){
    cout<<"捕获到了异常double"<<e<<endl;
    }
    }

    运行结果:

    image-20210120235333276

  • 接口中不抛出任何异常时,抛出异常的话,抛不出去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void fun2() throw(int){
    throw 10;
    }
    void test05(){
    try{
    fun2();
    }catch(int e){
    cout<<"捕获到了异常int"<<e<<endl;
    }catch(char e){
    cout<<"捕获到了异常char"<<e<<endl;
    }catch(double e){
    cout<<"捕获到了异常double"<<e<<endl;
    }
    }

    运行结果:

    image-20210120235707287

四、异常变量的生命周期

throw的异常是有类型的,可以是数字、字符串、类对象;

throw的异常是有类型的,catch需要进行严格匹配异常的类型。

  • 1、普通对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class MyException{
    public:
    MyException(){
    cout<<"异常无参构造函数"<<endl;
    }
    MyException(const MyException &ob){
    cout<<"异常拷贝构造函数"<<endl;
    }
    ~MyException(){
    cout<<"异常析构函数"<<endl;
    }
    };
    void test01(){
    try{
    MyException ob;
    throw ob;
    }catch(MyException e){
    cout<<"捕获到MyException异常"<<endl;
    }
    }

    运行结果:

    QQ截图20210121001448

    分析:第15行代码,定义一个MyException对象ob,会发生图片中①;第16行代码,throw ob会将ob给一个临时的区域,会发生图片中的②调用拷贝构造函数;这时候执行到了第17行代码,这时第15行代码所在区域结束了会调用ob对象的析构函数,图片中的③;然后会执行第17行代码,拷贝在临时区域的ob对象将会拷贝给e,调用拷贝构造函数,如图④;然后执行第18行代码,如图⑤;最后临时区域对象释放,调用析构,如图⑥;然后e释放调用析构函数,如图⑦。

    • 2、采用指针

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class MyException{
      public:
      MyException(){
      cout<<"异常无参构造函数"<<endl;
      }
      MyException(const MyException &ob){
      cout<<"异常拷贝构造函数"<<endl;
      }
      ~MyException(){
      cout<<"异常析构函数"<<endl;
      }
      };
      void test02(){
      try{
      throw new MyException();
      }catch(MyException *e){
      cout<<"捕获到MyException *异常"<<endl;
      delete e;// 记得释放
      }
      }

      运行结果:

      202101210029

      分析:第15行代码执行,调用构造函数,如图①;将指针给临时区域,然后临时区域的指针将给第16行代码中的e,然后执行第17行代码,如图②;然后释放e,即第15行创建的对象,调用析构函数,如图中③。

      2相比1来说调用的构造和析构少了很多,但是需要在捕获到异常后,手动执行delete释放。

    • 3、采用引用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class MyException{
      public:
      MyException(){
      cout<<"异常无参构造函数"<<endl;
      }
      MyException(const MyException &ob){
      cout<<"异常拷贝构造函数"<<endl;
      }
      ~MyException(){
      cout<<"异常析构函数"<<endl;
      }
      };
      void test03(){
      try{
      MyException ob;
      throw ob;
      }catch(MyException &e){
      cout<<"捕获到MyException &异常"<<endl;
      }
      }

      运行结果:

      202101210036

      分析:第15行代码,定义一个MyException对象ob,会发生图片中①;第16行代码,throw ob会将ob给一个临时的区域,会发生图片中的②调用拷贝构造函数;这时候执行到了第17行代码,这时第15行代码所在区域结束了会调用ob对象的析构函数,图片中的③;然后会执行第17行代码,给临时区域的ob对象取个别名(定义一个引用)e;然后执行第18行代码,如图中④;最后释放对象ob,如图中⑤,因为在第17行代码中取了个别名,所以在16行代码结束后ob没有被释放。

    • 4、采用匿名对象和引用(推荐这种方式)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class MyException{
      public:
      MyException(){
      cout<<"异常无参构造函数"<<endl;
      }
      MyException(const MyException &ob){
      cout<<"异常拷贝构造函数"<<endl;
      }
      ~MyException(){
      cout<<"异常析构函数"<<endl;
      }
      };
      void test04(){
      //推荐使用这种方式
      try{
      throw MyException();//匿名对象
      }catch(MyException &e){
      cout<<"捕获到MyException &异常"<<endl;
      }
      }

      运行结果:

      4NYAJQVYUDYBH()BTBFDT76

      分析:第16行代码执行,如图中①;然后匿名对象并没有立即释放,而是给了临时的区域,然后第17行代码给临时区域中的匿名对象起了个别名e或者说定义了一个引用e;然后执行第18行代码,如图中②;最后匿名对象释放,如图中③。

      推荐这种方式,析构与构造函数调用少,并且不用手动进行delete释放空间。

五、异常的多态使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//异常基类
class BaseException{
public:
virtual void printError(){

}
};

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

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

void test07(){
try{
fun3();
}catch(BaseException &ex){
ex.printError();
}
}

运行结果:

image-20210121010001516

六、C++标准异常库

标准库(#include<stdexcept>)中也提供了很多的异常类,它们是通过类继承组织起来的。异常类继承层级结构图如下:

image-20210121010722371

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
private:
int age;
public:
Person(int age){
if(age<0 || age>150){
throw out_of_range("age有误");
}
this->age = age;
cout<<"Person有参构造"<<endl;
}
};
void test01(){
try{
Person ob(1000);
}catch(exception &ex){
cout<<"捕获到异常"<<ex.what()<<endl;
}
}

运行结果:

image-20210121164201673

七、补充 cin的拓展(了解)

  • 1、基本用法

    1
    2
    3
    4
    5
    6
    void test01(){
    int data=0;
    cin>>data;
    cout<<"data="<<data<<endl;
    }

    运行结果:

    image-20210121165352363

  • 2、获取一个字符cin.get()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void test02(){
    char ch='\0';
    cin>>ch;
    cout<<"ch="<<ch<<endl;

    char ch1='\0';
    ch1=cin.get();
    cout<<"ch1="<<ch1<<endl;
    }

    运行结果:

    image-20210121165305290

  • 3、获取带空格的字符串cin.getline()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void test03(){
    char name[128]="";
    cin>>name;//默认cin 遇到空格、回车结束获取
    cout<<"name="<<name<<endl;

    char name1[128]="";
    cin.getline(name1,sizeof(name1));//获取一行 可以获取带空格的字符串
    cout<<"name1="<<name1<<endl;
    }

运行结果:

image-20210121165827137

  • 4、忽略缓冲区的前n个字符 cin.ignore()

    1
    2
    3
    4
    5
    6
    void test04(){
    char name3[128]="";
    cin.ignore(2);
    cin>>name3;
    cout<<"name3="<<name3<<endl;
    }

    运行结果:

    image-20210121170240449

  • 5、放回缓冲区 cin.putback()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void test05(){
    char ch='\0';
    ch = cin.get();
    cout<<"ch="<<ch<<endl;

    //char ch1='a';
    //cin.putback(ch1);
    cin.putback(ch);

    char str[32]="";
    cin>>str;
    cout<<"str="<<str<<endl;
    }

    运行结果:

    image-20210121171008211

  • 6、偷窥 cin.peek()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void test06(){
    char ch2 = '\0';
    ch2 = cin.peek();
    cout<<"偷窥到的缓冲区数据为:"<<ch2<<endl;

    char str1[32]="";
    cin>>str1;
    cout<<"str1="<<str1<<endl;
    }

    运行结果:

    image-20210121171414647

    仅仅是拿出来看一下,不会将缓冲区的h拿走,所以str1还是hello,并不是ello