异常概念
程序在运行过程中可能产生异常,异常是程序运行时可预料的执行分支,Bug是程序错误,是不可被预期的运行方式
C语言处理异常的方式
void func(...)
{
if(判断是否产生异常)
{
正常情况代码逻辑;
}
else
{
异常情况代码逻辑;
}
}
例子:
#include <iostream>
#include <string>
using namespace std;
double divide(double a, double b, int* valid)
{
const double delta = 0.000000000000001;
double ret = 0;
if( !((-delta < b) && (b < delta)) )
{
ret = a / b;
*valid = 1;
}
else
{
*valid = 0;
}
return ret;
}
int main(int argc, char *argv[])
{
int valid = 0;
double r = divide(1, 0, &valid);
if( valid )
{
cout << "r = " << r << endl;
}
else
{
cout << "Divided by zero..." << endl;
}
return 0;
}
这个代码在功能上满足了处理异常的要求,但是做除法操作要用到3个参数
一点都不优雅,divide函数调用后必须判断valid的结果来看是否出现异常,降
低代码的可读性**,我们可以通过setjmp()和longjmp()进行优化**,在使用这
两个函数必须谨慎执行
例子:
#include <iostream>
#include <string>
#include <csetjmp>
using namespace std;
static jmp_buf env;
double divide(double a, double b)
{
const double delta = 0.000000000000001;
double ret = 0;
if( !((-delta < b) && (b < delta)) )
{
ret = a / b;
}
else
{
longjmp(env, 1);
}
return ret;
}
int main(int argc, char *argv[])
{
if( setjmp(env) == 0 )
{
double r = divide(1, 1);
cout << "r = " << r << endl;
}
else
{
cout << "Divided by zero..." << endl;
}
return 0;
}
结果:
"Divided by zero..."
最开始的时候程序从main开始运行,运行到" setjmp(env) == 0 "这个语句时,调用了setjmp把当前上下文保存在env全局变量里面,因为我们是直接调用,保存完了返回值为0,即调用了divide函数,当b为
0时就走到longjump分支,longjump就根据env恢复之前保存的程序上下文,并且返回值为1,回到原来的上下文出口和0比较不成立则输出异常语句,这个程序虽然优雅,但是破坏了结构化设计的三大特性
和涉及到使用全局变量,所以C语言异常处理还是if…else…结构,C++中引入了更好的异常机制
C++语言处理异常的方式
C++内置了异常处理的语法元素try…catch…
try语句处理正常代码逻辑
catch语句处理异常情况
try语句中抛出的异常由对应的catch语句处理
try
{
double r = divide(1,0);
}
catch(...)
{
cout<<"Divided by zero..."<<endl;
}
C++异常处理分析
-throw抛出的异常必须被catch处理
当前函数能够处理异常,程序继续往下执行
当前函数无法处理异常,则函数停止执行,并返回
例子:
#include <iostream>
#include <string>
using namespace std;
double divide(int a,int b)
{
double ret = 0;
if(!((-0.0000000001<b)&&(b<0.0000000001)))
{
ret = a/b;
}
else
{
throw 0;
}
return ret;
}
int main()
{
try
{
double r = divide(1,0);
cout<<"r="<<r<<endl;
}
catch(...)
{
cout<<"the divide throws fault\n"<<endl;
}
}
结果:
sice@sice:~$ ./a.out
the divide throws fault
我们的divide函数中没有catch语句,所以抛出的异常被上次调用者处理即mian函数,
main函数中的catch语句捕获到这个抛出异常,执行对应的处理语句
要点:
同一个try语句可以跟上多个catch语句,catch语句可以定义具体处理的异常类型
不同类型的异常由不同的catch语句负责处理
try语句中可以抛出任何类型的异常
catch(…)用于处理所有类型的异常
任何异常都只能被捕获(catch)一次
例子:
#include <iostream>
#include <string>
using namespace std;
void Demo()
{
try
{
throw 0;
}
catch(char c)
{
cout<<"catch(char c)"<<endl;
}
catch(short c)
{
cout<<"catch(short c)"<<endl;
}
catch(double c)
{
cout<<"catch(double c)"<<endl;
}
catch(int c)
{
cout<<"catch(int c)"<<endl;
}
}
int main()
{
Demo();
try
{
}
catch(...)
{
}
return 0;
}
结果:
catch(int c)
可以看出严格匹配的原则(即使是const char和char都不可以),如果 catch(int c)语句去掉了就会报错,catch(…)表示捕获所有异常,并且只能放在最后,一个异常只能被一个catch语句捕获
C++异常处理进一步解析
1.
catch语句中可以抛出异常
为什么要在catch语句中抛出异常
在工程开发里面,我们用catch语句抛出异常的语句来重新解释异常,当我们的私有库需要用到第三库时我们就去把这个第三方库封装在我们的私有库中,那为什么不直接调用第三方库就好呢?这是为了统一抛出异常,下图中的func(int i)函数抛出的异常为int类型,然而在我们的私有库工程里面定义了自己的异常类型,所以我们去封装这个第三方库将它抛出的异常重新解释为我们工程中的异常
例子:
#include <iostream>
#include <string>
using namespace std;
void Demo()
{
try
{
try
{
throw 'a';
}
catch(int i)
{
cout<<"first: catch(int i)"<<endl;
throw i;
}
catch(char c)
{
cout<<"second: catch(char c)"<<endl;
throw c;
}
}
int main()
{
void Demo();
return 0;
}
结果:
sice@sice:~$ ./a.out
second: catch(char c)
third: catch(...)
可以看出异常是就可以嵌套调用的,接下来我们来实现上述图中的模式
代码
#include <string>
#include <iostream>
using namespace std;
void func(int i)
{
if(i==1)
{
throw 1;
}
if(i==100)
{
throw 100;
}
if(i==-1)
{
throw -1;
}
}
void MyFunc(int i)
{
try
{
func(i);
}
catch(int i)
{
switch (i)
{
case -1:
throw "Invalid Parameter";
break;
case 100:
throw "Runtime Exception";
break;
case 1:
throw "Timeout Exception";
break;
}
}
}
int main()
{
try
{
MyFunc(1);
}
catch(const char* cs)
{
cout << "Exception Info: " << cs << endl;
}
return 0;
}
结果:
sice@sice:~$ ./a.out
Exception Info: Timeout Exception
这个的第三方库func(int i)我们是不知道源码的,我们调用它时把它封装在
我们的私有库中,根据第三方库抛出的异常捕获解释重新抛出异常,做到工程
异常的统一,异常的类型可以是自定义类类型,对于类类型异常的匹配依旧是
至上而下严格匹配,赋值兼容性原则在异常匹配中依然适用,一般而言,匹
配子类异常的catch放在上部,匹配父类异常的catch放在下部,
2.
在工程中会定义一系列的异常类,每个类代表工程中可能出现的一种异常类型,
代码复用时可能需要重解释不同的异常类,在定义catch语句块时推荐使用引用
作为参数,下面一个一个异常类的例子:
#include <string>
#include <iostream>
using namespace std;
class Base
{
};
class Exception:public Base
{
int m_id;
string m_desc;
public:
Exception(int id,string desc)
{
m_id = id;
m_desc = desc;
}
int id()const
{
return m_id;
}
string desc()const
{
return m_desc;
}
};
void func(int i)
{
if(i==1)
{
throw 1;
}
if(i==100)
{
throw 100;
}
if(i==-1)
{
throw -1;
}
}
void MyFunc(int i)
{
try
{
func(i);
}
catch(int i)
{
switch (i)
{
case -1:
throw Exception(-1,"Invalid Parameter");
break;
case 100:
throw Exception(100,"Runtime Exception");
break;
case 1:
throw Exception(1,"Timeout Exception");
break;
}
}
}
int main()
{
try
{
MyFunc(-1);
}
catch(const Exception& e)
{
cout<< "Exception Info: "<<endl;
cout<<"ID :"<<e.id()<<endl;
cout<<"m_desc :"<<e.desc()<<endl;
}
return 0;
}
结果:
sice@sice:~$ ./a.out
Exception Info:
ID :-1
m_desc :Invalid Parameter
这样子的异常信息就更加丰富了呢,可以更方便的给我们定位问题的所在,如果
我们在main函数中加入以下语句
catch (const Base& e)
{
cout<<"catch(const Base& e)"<<endl;
}
结果将会输出catch(const Base& e)语句,这是赋值兼容性原则,子类对象完全
可以当作父类对象来使用,所以上述的语句能够成功匹配打印出了信息,也就是
说我们将父类的分支放在上面的话,子类的分支永远无法运行到了,所以在工程
当中,正如我们上面提到的一致,匹配子类异常的catch放在上部,匹配父类异常
的catch放在下部,
3.
C++标准库中提供了实用异常类族,标准库中的异常都是从exception
类派生的,exception类有两个主要的分支
-logic_error
常用于程序中的可避免逻辑错误
-runtime_error
常用于程序中无法避免的恶性错误
例子:
array.h
#ifndef _ARRAY_H_
#define _ARRAY_H_
#include <stdexcept>
using namespace std;
template
< typename T, int N >
class Array
{
T m_array[N];
public:
int length() const;
bool set(int index, T value);
bool get(int index, T& value);
T& operator[] (int index);
T operator[] (int index) const;
virtual ~Array();
};
template
< typename T, int N >
int Array<T, N>::length() const
{
return N;
}
template
< typename T, int N >
bool Array<T, N>::set(int index, T value)
{
bool ret = (0 <= index) && (index < N);
if( ret )
{
m_array[index] = value;
}
return ret;
}
template
< typename T, int N >
bool Array<T, N>::get(int index, T& value)
{
bool ret = (0 <= index) && (index < N);
if( ret )
{
value = m_array[index];
}
return ret;
}
template
< typename T, int N >
T& Array<T, N>::operator[] (int index)
{
if( (0 <= index) && (index < N) )
{
return m_array[index];
}
else
{
throw out_of_range("T& Array<T, N>::operator[] (int index)");
}
}
template
< typename T, int N >
T Array<T, N>::operator[] (int index) const
{
if( (0 <= index) && (index < N) )
{
return m_array[index];
}
else
{
throw out_of_range("T Array<T, N>::operator[] (int index) const");
}
}
template
< typename T, int N >
Array<T, N>::~Array()
{
}
#endif
main.cpp
void TestArray()
{
Array<int, 5> a;
for(int i=0; i<10; i++)
{
a[i] = i;
}
return 0;
}
int main(int argc, char *argv[])
{
try
{
TestArray();
cout << endl;
}
catch(...)
{
cout << "Exception" << endl;
}
return 0;
}
结果:
Exception,可以看出异常类的作用可以避免很多问题的出现,异常的类型
可以是自定义类型,赋值兼容型原则在异常匹配中依然适用,标准库中的异
常都是从exception类派生的
C++异常的深度问题
首先第一个就是异常不处理的话,最后会传到哪里呢?
使用代码检验下:
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test()";
cout << endl;
}
~Test()
{
cout << "~Test()";
cout << endl;
}
};
int main()
{
static Test t;
throw 1;
return 0;
}
结果:
sice@sice:~$ ./a.out
Test()
terminate called after throwing an instance of 'int'
已放弃 (核心已转储)
对于这个结果是可预想的,关键的点在于最后的语句是谁打印出来的?
根据英文我们可以了解到terminate这个函数在’int’异常抛出后被调用
查资料可以知道如果异常无法被处理,terminate这个函数会在结束时
被自动调用,默认情况下terminate()调用库函数abort()终止程序,abort()
函数使得程序执行异常而立即退出,C++支持替换默认的terminate()
函数实现,
改进上述例子:
#include <iostream>
#include <cstdlib>
using namespace std;
void m_terminate()
{
cout<<"i'm fine"<<endl;
exit(1);//如果使用abort(),则会打印“已放弃”
}
class Test
{
public:
Test()
{
cout << "Test()";
cout << endl;
}
~Test()
{
cout << "~Test()";
cout << endl;
}
};
int main()
{
set_terminate(m_terminate);
static Test t;
throw 1;
return 0;
}
结果:
Test()
i'm fine
~Test()
自定义的m_terminate函数调用了exit(1),所以会调用静态对象t的析构函数
第二个问题析构函数抛出异常会发送什么情况?
#include <iostream>
#include <cstdlib>
#include <exception>
using namespace std;
void my_terminate()
{
cout << "void my_terminate()" << endl;
exit(1);
//abort();
}
class Test
{
public:
Test()
{
cout << "Test()";
cout << endl;
}
~Test()
{
cout << "~Test()";
cout << endl;
throw 2;
}
};
int main()
{
set_terminate(my_terminate);
static Test t;
throw 1;
return 0;
}
结果:
sice@sice:~$ ./a.out
Test()
void my_terminate()
~Test()
已放弃 (核心已转储)
当main函数的异常无处理退出时会调用my_terminate(),而my_terminate()函数
又调用exit(1)释放资源,就会调用到析构函数,析构函数里面会抛出无处理异常,
再次调用my_terminate()函数释放相同的资源就会报错