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
15int 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;
}
}运行结果:
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
20int 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;
}
}运行结果:
如果没有捕获异常:
二、栈解旋(unwinding)
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上(局部)构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding)
1 | class Person{ |
运行结果:
这是一种现象,只不过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
14void 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;
}
}运行结果:
接口中不抛出任何异常时,抛出异常的话,抛不出去
1
2
3
4
5
6
7
8
9
10
11
12
13
14void 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;
}
}运行结果:
四、异常变量的生命周期
throw的异常是有类型的,可以是数字、字符串、类对象;
throw的异常是有类型的,catch需要进行严格匹配异常的类型。
1、普通对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class 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;
}
}运行结果:
分析:第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
20class 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;// 记得释放
}
}运行结果:
分析:第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
20class 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;
}
}运行结果:
分析:第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
20class 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;
}
}运行结果:
分析:第16行代码执行,如图中①;然后匿名对象并没有立即释放,而是给了临时的区域,然后第17行代码给临时区域中的匿名对象起了个别名e或者说定义了一个引用e;然后执行第18行代码,如图中②;最后匿名对象释放,如图中③。
推荐这种方式,析构与构造函数调用少,并且不用手动进行delete释放空间。
五、异常的多态使用
1 | //异常基类 |
运行结果:
六、C++标准异常库
标准库(#include<stdexcept>
)中也提供了很多的异常类,它们是通过类继承组织起来的。异常类继承层级结构图如下:
1 | class Person{ |
运行结果:
七、补充 cin的拓展(了解)
1、基本用法
1
2
3
4
5
6void test01(){
int data=0;
cin>>data;
cout<<"data="<<data<<endl;
}运行结果:
2、获取一个字符
cin.get()
1
2
3
4
5
6
7
8
9void test02(){
char ch='\0';
cin>>ch;
cout<<"ch="<<ch<<endl;
char ch1='\0';
ch1=cin.get();
cout<<"ch1="<<ch1<<endl;
}运行结果:
3、获取带空格的字符串
cin.getline()
1
2
3
4
5
6
7
8
9void test03(){
char name[128]="";
cin>>name;//默认cin 遇到空格、回车结束获取
cout<<"name="<<name<<endl;
char name1[128]="";
cin.getline(name1,sizeof(name1));//获取一行 可以获取带空格的字符串
cout<<"name1="<<name1<<endl;
}
运行结果:
4、忽略缓冲区的前n个字符
cin.ignore()
1
2
3
4
5
6void test04(){
char name3[128]="";
cin.ignore(2);
cin>>name3;
cout<<"name3="<<name3<<endl;
}运行结果:
5、放回缓冲区
cin.putback()
1
2
3
4
5
6
7
8
9
10
11
12
13void 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;
}运行结果:
6、偷窥
cin.peek()
1
2
3
4
5
6
7
8
9void test06(){
char ch2 = '\0';
ch2 = cin.peek();
cout<<"偷窥到的缓冲区数据为:"<<ch2<<endl;
char str1[32]="";
cin>>str1;
cout<<"str1="<<str1<<endl;
}运行结果:
仅仅是拿出来看一下,不会将缓冲区的h拿走,所以str1还是hello,并不是ello