异常处理是C++体系中为了使得错误和代码分离的最常用的做法,为了达到这种目的,我们需要了解异常处理机制的本质,这样才能让我们更好的使用起来
错误处理技术
- 传统错误处理方法
1.终止程序
2.返回错误码
3.返回合法值,让程序处于某种非法的状态
4.调用一个预先设置的出现错误时调用的函数—回调函数
异常处理
异常,就是当一个函数发现自己没有办法处理错误时会抛出一个异常,让函数的调用者直接或者间接地处理这个问题。
异常的抛出和捕获
- 1.异常是通过抛出对象引发的,该对象的类型决定了应该激活哪个处理代码。
- 2.被选中的处理代码应该是调用链中和该对象类型匹配并且离抛出位置最近的那一个
- 3.抛出异常之后会释放局部存储空间,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象会在传给对应的catch处理之后撤销
由于异常的匹配是依靠类型的,因此我们可以根据类型的不同划分不同的catch进行异常类型匹配
class Exception {
public:
Exception(int errId,const char* errMsg)
:_errId(errId)
,_errMsg(errMsg)
{}
void What()const
{
cout<<"errId:"<<_errId<<endl;
cout<<"errMsg"<<_errMsg<<endl;
}
void Func1(bool isThrow)
{
if(isThrow)
{
throw Exception(1,"抛出Exception对象");
}
printf("Func1(%d)\n",isThrow);
}
void Func2(bool isThrowString,bool isThrowInt)
{
if(isThrowString)
{
throw string("抛出string对象");
}
if(isThrowInt)
{
throw 7;
}
printf("Func2(%d,%d)\n",isThrowString,isThrowInt);
}
void Func ()
{
try
{
Func1(false ); Func2(true , true);
}
catch(const string& errMsg)
{
cout<<"Catch string Object:" <<errMsg<< endl;
}
catch(int errId)
{
cout<<"Catch int Object:" <<errId<< endl;
}
catch(const Exception& e)
{
e.What ();
}
catch(...)
{
cout<<" 未知异常"<< endl;
} printf ("Func()\n");
}
栈展开
抛出异常的时候,会暂停当前函数的执行,开始查找对应的匹配catch子句。
- 首先检查throw本身是否在catch块内部,如果是再查找匹配的catch语句。如果有匹配的,就进行处理,如果没有,则退出当前函数栈桢,继续在调用函数的栈中进行查找。
- 不断重复上述过程,若到达main函数的栈,依旧没有匹配的,则终止程序。
- 上述这个沿着调用链查找匹配的catch子句的过程称为栈展开
- 找到匹配的catch子句并处理以后,会继续沿着catch子句1后面继续执行。
异常捕获的匹配规则
- 异常对象的类型与catch说明符的类型必须完全匹配
- 例外
- 1.允许从非const对象到const的转换
- 2.允许从派生类到基类类型的转换
- 3.将数组转换为指向数组类型的指针,将函数转换为指向函数类型的指针
异常的重新抛出
当单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理
class Exception
{
public :
Exception(int errId = 0, const char * errMsg = "" )
: _errId(errId )
, _errMsg(errMsg )
{}
void What () const
{
cout<<"errId:" <<_errId<< endl;
cout<<"errMsg:" <<_errMsg<< endl;
}
private :
int _errId ; // 错误码
string _errMsg ; // 错误消息
};
void Func1 ()
{
throw string ("Throw Func1 string");
}
void Func2 ()
{
try
{
Func1();
}
catch(string & errMsg)
{
cout<<errMsg <<endl;
// Exception e (1, "Rethorw Exception");
// throw e ; //重新抛出对象不需要写任何参数类型
// throw;
// throw errMsg;
}
}
void Func3 () //在Func3函数中处理重新抛出的异常
{
try
{
Func2();
}
catch (Exception & e)
{
e.What ();
}
}
- 需要强调的是,如果重新抛出的异常没有被本catch捕获,它也会被向上传递,直到被捕获为止。但是如果都没有被捕获的话,
异常与构造函数,析构函数
- 构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化。
- 析构函数主要完成资源的清理工作,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏
这两个点,我先解释一下。异常的处理机制有时候还是存在一些诟病的。就拿一个很简单的特点来说吧。如果一个对象抛出异常,那么后面的语句有可能都不会执行了,它会直接跳出当前语句然后去寻找和它匹配的catch子句。这要是放在构造函数中,我构造了一个对象时,正准备初始化,突然抛出一个异常,然后初始化就没办法执行了。
- 如此,析构函数也是如此的,由于异常的处理机制,如果我们正要释放一个对象时,一个异常抛出,系统不会去管内存是否释放,对象是否正确消失,好吧,那你要去catch你就去吧,内存也就造成泄漏了。
类中的数据成员应该被正确的销毁,即使是在抛出异常的情况下。
在两个异常同时存在的情况下,程序不是结束执行就是导致不明确行为
为了解决抛出异常导致程序不能正常关闭的问题,我们可以在析构函数中调用 close函数
这是为了防止客户没有正常调用而利用析构函数在对象结束使用时自动调用的特性
1.如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”,阻止异常从析构函数传播。
因此调用abort()可以抢先制“不明确行为”于死地
- 请记住
1.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉各种异常,然后吞下他们(不传播)或结束程序
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作
接下来我就用一段代码来给你们演示一下。
class DBConnection{
public:
static DBConnection create();
void close(); // 为了避免没有调用关闭函数,应该像下面这样做
}
class DBConn{
public:
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
异常处理有时候也会抛出一个异常是你无法理解的,比如下面这样。
为什么会这样呢?本质上来说,这和继承派生中常常会出现的切片行为类似。先来复习一下切片知识吧。
所谓切片,就是当一个派生类继承了基类的所有,包括函数和数据,这个时候,当我们调用一个函数时,就有可能出现切片行为,派生类中的基类对象被保留,但是派生类中的对象已经被隐藏了起来。(这是在一个基类指针指向派生类对象时,并通过基类指针调用函数)这种情况在多态里表现的尤其明显,也是为了方便一个基类指针不管指向基类对象还是派生类对象都可以调用相应的函数,才出现了虚函数的使用。
来一个小小的切片实例好啦
class Exception {
public:
Exception()
:_a(a)
,_b(b)
{}
virtual void Func1();
void Func2();
private:
int _a;
int _b;
};
class Exception2 : public Exception {
public:
Exception2()
:_c(c)
{}
void Func1();
private:
int _c;
};
Test()
{
Exception exp;
Exception exp1;
exp1 = exp; //子类对象可以赋值给父类,发生了切片行为,但是父类对象不能赋值给子类
}
异常的重新抛出
当单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理
class Exception
{
public :
Exception(int errId = 0, const char * errMsg = "" )
: _errId(errId )
, _errMsg(errMsg )
{}
void What () const
{
cout<<"errId:" <<_errId<< endl;
cout<<"errMsg:" <<_errMsg<< endl;
}
private :
int _errId ; // 错误码
string _errMsg ; // 错误消息
};
void Func1 ()
{
throw string ("Throw Func1 string");
}
void Func2 ()
{
try
{
Func1();
}
catch(string & errMsg)
{
cout<<errMsg <<endl;
// Exception e (1, "Rethorw Exception");
// throw e ; //重新抛出对象不需要写任何参数类型
// throw;
// throw errMsg;
}
}
void Func3 () //在Func3函数中处理重新抛出的异常
{
try
{
Func2();
}
catch (Exception & e)
{
e.What ();
}
}
既然有这种问题,并且系统也不会帮我们解决,那我们只能自己去避免犯类似的错。就比如说,如果对象将要调用构造函数时抛出一个异常,我们可以把构造函数写在catch子句中,这样,不管系统抛不抛出异常,我们都可以正确的初始化一个对象。
- 这样对于析构函数也就好办多啦。我们把析构函数的调用在catch子句中也放一份吧。你要抛异常我拦不住,但是你匹配到的catch必须帮我执行清理工作,这也是很公平了。
总结:
我们只需要掌握常见的异常处理函数就好啦,具体的用法还是在实际编程过程中逐渐去熟悉,毕竟用得多了我们就可以对于异常有更深刻的认识了
注:本文如有纰漏,欢迎指正啊,互相交流。