在编码过程中,发生错误是必不可少的。而错误的类别是多种多样的,一个优秀的错误机制应该是允许程序中独立开发的部分能够在运行时就出现的问题经行通信并作出相应的处理。面对重大的错误可以告诉程序员错误发生在什么地方,是什么样子的错误。
传统的错误处理机制:1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
异常使得我们可以将问题的检测与解决问题的检测与解决过程分离开程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。检测环节无需知道问题处理模块的所有细节,反之亦然。
throw: 异常检测部分使用throw表达式表示遇到了无法解决的问题,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。我们说throw引发了异常。catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛 出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
异常的抛出和匹配原则1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。(这里位置最近是从函数调用栈帧的角度上讲,而不是函数执行顺序,也就是说抛出异常后,即使同函数中的catch模块在该throw之上也会优先调用同函数的catch,详情请看下面对于栈栈展开的描述)3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似 于函数的传值返回)4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。所以应该放在模块的最后面5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。
当执行一个throw时,跟在throw后面的语句将不再被执行,相反,程序的控制权从throw转移到与之匹配的catch模块。(该catch模块可能是桶一函数中的局部catch,也可能是位于调用发生异常的函数上) 。将控制权从一处转移到另一处,这意味着
- 沿着调用链的函数可能会提前退出。
- 一但程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
栈展开
在函数调用链中异常栈展开匹配原则1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行
一个异常如果没有被捕获将中止当前程序。
throw;
——>空的throw只能出现在catch语句,或者catch直接或者间接调用的函数之中。如果在处理代码之外出现将调用系统库函数terminate(结束当前程序)
很多时候catch会将抛出内容做出修改后再重新抛出,这时只有catch的异常声明是引用类型我们对参数的变化才能被保留并继续传播;
catch(my_error &eObj) //引用类型
{
eObj.status = errCodes::severErr;//修改了异常对象
throw; //异常对象的status对象是severErr
}
catch( other_error eObj) //非引用对象
{
other.status = errCodes::badErr;//只修改了异常对象的局部副本
throw; //异常对象没有改变
}
有时我们希望不论抛出的异常是什么类型,程序都可以捕获它们,以防止throw语句在程序中“乱跑”或者直接将程序终止。而想要捕获所有可能出现异常是相当有难度的(因为我们无法保证与catch对应的try语句块内部调用的函数的写法是规范的,也无法确保是否会有调用的库函数抛出异常。),而即使我们知道所有的类型,也很难为所有类型的异常提供唯一的catch语句。为了一次性捕获所有类型,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常(catch-all)的代码处理,形如catch(...)
当捕获所有异常(catch-all)与异常的重新抛出结合,
void manip(){ try{ //这里将抛出一个异常 } catch(...) { //处理异常的某些特殊操作 throw; } }
当catch(...)与其他catch语句一起出现,则catch(...)必须在最后位置。出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄 漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题
对于用户和编译器来说,预先知道一个函数不会抛出异常是十分重要。
好处:
1、有助于简化调用该函数的代码
2、如果编译器知道一个函数不会抛出异常就可以对其经行一些优化,而这些优化不适用于一些会抛出异常的函数.
在c++11中定义了一个新的关键字noexcept指定某个函数不会抛出任何异常。其形式如下
void recoup(int)noexcept; //表示不会抛出异常
void alloc(int ); //可能会抛出异常
对于一个函数来说要noexcept要不出现,要么出现在该函数的所有声明语句和定义语句。
至于noexcept出现的位置应该在函数尾置返回类型之前,此外我们也可以在函数指针的定义和声明中指定noexcept。而在typdef或类别别名中不能出现noexcept。在成员函数中noexcept出现在需要跟在const及引用限定符后面,而final(修饰虚函数,表示该虚函数不能再被重写)、 override(检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。)或虚函数的=0前
//返回一个指针,指向一个有10个int的数组
auto func(int i)noexcept ->int(*)[10];
void (*pf1)(int )noexcept=recoup;
void f() noexcept
{
throw exception();
}
上面的代码违反了异常说明但事实上他任然可以顺利通过编译(有的编译器会发出警告)。
所以有可能有一种情况尽管函数声明了它不会抛出异常,但实际上还是抛出了。一旦一个这样的函数抛出了异常,程序就会调用terminate(终止程序执行过程的库函数)。因此他会出现在以下两种情况
1、确保不会出现异常
2、根本不知道如何处理异常
noexpect说明符接受一个可选的实参,该参数必须能转换为bool类型,如果是true,则说明不会抛出异常,反之则可能抛出异常。
他是一个一元运算符,他的返回值为一个bool类型的右值常量表达式,表示是否给定表达式会抛出异常(和sizeof类似)也不会求其运算对象的值。
noexcept(recoup(i))//如果不抛出异常则为true,反之则false
//更简单的是
noexcept(e)
//当e所调用的所有函数都做了不抛出说明且本身不含有throw语句,上表达式为true;否则为false
所以我们可以结合使用,如下
void f() noexcept(noexcept(g()))//f与g异常说明一至
noexcept有两层含义:当跟在函数参数列表后面时他是异常说明符,而当作为noexcept异常说明的bool实参时,他是一个运算符。
函数指针及其所指向的函数必须有一致性的异常说明, 而当一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也不允许抛出异常,与之相反基类的虚函数没有承诺了它不会抛出异常,则派生类中可以允许抛出也可以不允许。
当编译器合成拷贝控制成员,同时也生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的。如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)。而且,如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。
标准异常类构成了下图所构成的继承体系。
类型exception仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为what的虚成员。其中what函数返回一个const char*,该指针指向一个以null结尾的字符数组,并且确保不会抛出任何异常。
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;
int _id;
};
针对不同的异常我们可以通过继承多肽的方式经行编写符合我们需求的类型,比如
class SqlException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& sql)
:Exception(errmsg, id)
, _sql(sql)
{}
virtual string what() const
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql;
};
class CacheException : public Exception
{
public:
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误.3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。