目录
一、C中的常用处理错误方式
二、C++异常的概念
1. throw
2. catch
3. try
三、异常的使用
1. 异常的抛出和捕获
1.1 异常的抛出和匹配原则
1.2 在函数调用链中异常栈展开匹配原则
四、异常体系
1. 自定义异常体系
2. C++中的异常体系
五、 异常安全
六、异常规范
1. throw()标识异常
2. noexcept关键字
七、异常的优缺点
1. 优点
2. 缺点
一、C中的常用处理错误方式
在C中,如果我们的程序出现错误,通常会有两种处理方法。
(1)终止程序
例如使用assert来终止程序。但是这种终止方式是直接终止程序,用户难以接受。
(2)返回错误码
有时当出现错误时,也会通过返回错误码的方式处理错误。但是这种方式返回的只是错误码,具体的错误需要用户自己去查询。系统的很多库接口就是使用这种方式。
二、C++异常的概念
C的传统的两种处理错误的方式都是不太好的。所以,在C++中又提出了一种新的错误处理方式,即异常。当一个函数发现一个自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
异常通常由如下几个部分组成:
1. throw
当问题出现时,程序会抛出一个异常,这个动作需要使用throw关键字来抛出。
2. catch
这个部分一般放在想要处理问题的地方,用于捕获异常。可以有多个catch进行捕获
3. try
try块中的代码被标识为将激活的特定异常。即try块里面的代码就是可能出现错误的代码。它后面通常跟着一个或多个catch块。
如果一个块抛出异常,捕获方法通常会使用try和catch关键字。try块中放置可能抛出的异常,而try块中的代码就被称为“保护代码”。
try和catch的使用格式也很简单:
三、异常的使用
1. 异常的抛出和捕获
1.1 异常的抛出和匹配原则
(1)异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码:
例如上图就是一个异常捕获,当出现异常时,const char*类型的数据就被匹配到这个catch块中执行代码。
当然,这也就意味着可以有多个catch来捕获异常:
无论有多少个catch,在出现异常时,都只会进入与异常的类型相同的catch中。其他未执行的catch会被跳过。
同时,在同一个try catch中,不允许出现相同类型的catch:
(2)被选中的处理代码是调用链中与该对象的类型且离抛出异常位置最近的那一个
写出如下测试程序:
在这个程序中,main函数和func函数中都有异常捕获。此时运行程序,看看这个异常会在哪里被捕获:
可以看到,这里的打印就表明了它是在func函数中被捕获的,因为func中异常捕获离出现异常的位置最近。
这里的位置最近,指的是函数的调用链最近。可以将函数的调用过程想象成一个链表,每个函数调用时都被链接起来,如下图:
当遇到异常时,现在自己内部找有没有能够匹配的catch,没有返回上一层函数找,找到就进入。没有则继续返回。
(3)抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁(类似与函数的传值返回)
例如抛出一个string对象:
这个string对象如果不拷贝一份,就会在出了if的作用域,即进行跳转的时候被销毁,导致返回错误的信息。
(4)catch(...)可以捕获任意类型的异常。缺点就是难以知道出现了什么异常错误
在实际上,我们可能会碰见有多个有多个位置可能抛异常的情况,如果这些异常都在同一个函数内捕获,就需要写很多个catch,会很麻烦。因此,遇到这种情况时,就可以使用...来捕获异常。这种方式可以捕获任意类型的异常:
但是这种方法无法生成一个临时变量来接收异常信息,所以无法知道出现了什么异常。
因此,这种异常一般可能会用于在写了多个catch后,再在最后补一个捕获任意异常的catch,以防止出现类型不匹配的异常导致程序退出:
这个语句一般在实际中捕获异常时都会要求写,防止出现未知异常导致程序崩溃。
(5)实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配。可以抛出的派生类对象,使用基类捕获。
1.2 在函数调用链中异常栈展开匹配原则
(1)首先检查throw本身是否在try块内部,如果在再查找匹配的catch语句。
如果有匹配的,则跳转到catch中执行代码。如果没有,则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。如果到达main函数的栈中都依然没有匹配的catch,则终止程序。
在这个程序中,就没有可以匹配的catch,因为抛出的异常是const char*,无法与char*匹配。运行该程序:
此时程序直接报错终止。
这种沿着调用链进行匹配的catch子句的过程就被称为“栈展开”
(2)当找到匹配的catch子句并处理后,会跳过后面的catch子句并继续向后执行。
之所以会有捕获catch后不退出程序继续向后执行的特性,是为了应对一些特殊情况,例如下图:
在这个程序中,new了一块空间。我们知道,new出来的空间除非我们自己释放,否则只要这个程序未结束,那么这块空间就会一直被占用。如果没有catch后继续向后执行的机制,在上面的情况中,就可能会导致内存泄漏,因为delete被放在了catch后面,如果不继续执行,就可能导致出现错误。
这一机制一般是配合多个函数内可以实现多个catch,并且在出现异常后会优先进入离异常最近的catch执行。因为在实际中也可能出现如下情况:
在上面的程序中,func函数中new了一块空间,这块空间在出现异常时还未被捕获。此时func中没有可以匹配的catch,就导致异常进入main函数中被捕获。这时异常直接从出现异常的Division函数中跳转到了main函数中,没有执行func中的delete语句。这就可能导致内存泄漏。
因此,在会抛出异常的块中如果调用了其他函数,最好要在本级配备对应的可匹配的catch,以免出现上述情况。
四、异常体系
1. 自定义异常体系
在实际中,可能会遇到这样的情况:一个程序的很多地方都可能抛异常,而这些异常都要在最外层进行接收。此时就会导致外部需要频繁的接收异常,这对于最外层来讲无疑是难以接受的。
在实际中,接收异常都会有一些异常体系。这些异常体系一般是用继承实现的。通过继承的方式,让不同的子类接收某一类型的异常信息。通过这一方式,在最外部的catch就只需捕获父类即可。
在这里,为了模拟出这一场景,所以写了一个简单的程序,用取模来模拟出现异常的情况,然后让main函数接收捕获父类:
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;
}
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if(rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
int main()
{
while (true)
{
Sleep(1000);//<widnows.h>头文件提供的休眠函数,休眠1ss
try
{
CacheMgr();
}
catch (const Exception& e)//捕获父对象
{
cout << e.what() << endl;//多态
}
catch (...)
{
cout << "UnKown Exception" << endl;
}
}
return 0;
}
运行该程序:
此时,该程序就将错误信息打印了出来。
2. C++中的异常体系
C++中也是提供了异常体系的,名字叫做“exception”。它就是通过子类继承父类的方式来实现异常抛出的:
但是这一异常体系并不好用,一般来讲,很多公司都会有一套自己的异常体系,而不会使用这个库中提供的异常体系。所以这里就不过多赘述了。
如果大家用库中的异常体系,直接在catch中捕获exception类即可:
要打印异常信息,则使用这个类中的what()函数。
五、 异常安全
在使用异常时,最好不要在以下几个地方抛异常:
(1)构造函数完成对象的构造和实例化时,最好不要在构造函数中抛出异常,这可能导致对象不完整或没有完全初始化。
(2)析构函数主要完成资源的清理,最好不要再析构函数内抛出异常,否则可能导致资源泄漏。
(3)C++中的异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏;在lock和unlock之间抛出了异常,导致死锁。在C++中,一般采用RAII来解决这些问题。RAII在这里先不讲解。
六、异常规范
在实际中,我们可能不止会调用自己写的函数,也可能调用他人写的函数。虽然我们可以知道自己写的函数中是否会抛异常,但是难以知道他人的函数中是否会抛出异常。为了解决这一问题,就有了C++的异常规范。
1. throw()标识异常
在C++98中,提供了通过throw来标识该函数可能抛出哪些类型的异常:
上面这个函数就表示该函数可能会抛出一个string类型的异常。如果这个throw中什么都没有,就说明这个函数不会抛异常。
但是这种方法有一个很明显的弊端,那就是这个函数内部可能会调用库中的函数或者其他人写的函数。这就会导致如果要在这个throw中标识该函数可能抛出的异常,就要去查看这个函数中调用的其他函数可能抛出的异常。无疑,这项工作时比较繁琐的。并且这个异常标准并不是强制要求的,因此这个标准也就没有被广大的C++用户所接受,可以说是名存实亡。
2. noexcept关键字
由于C++98的异常规范未被接受,因此在C++11的时候,又推出了另一个异常规范,即noexcept关键字。带有这个关键字的函数就表示“不会抛出异常”。同时,这个关键字也会对函数是否抛异常进行检查:
由此,如果大家在未来想要使用标明自己的函数是否会抛出异常,就可以使用noexcept关键字,尽量不要使用C++98的异常标准,只需要在遇见时可以看懂即可。
七、异常的优缺点
1. 优点
(1)异常对象定义好后,相比错误码的方式,可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序bug
(2)返回错误码的传统方式本身就有着一定的缺陷。在函数调用链中,当深层的函数返回了错误码后,我们需要层层向外返回错误,这样才能让最外层拿到这个错误码。
(3)很多的第三方库,例如boost、glest、gmock等常用的第三方库中都包含了异常,在使用这些库时,也需要使用异常
(4)部分函数使用异常更好处理,比如构造函数这种没有返回值的函数。这些函数由于没有返回值,就难以用错误码的方式处理。当然,还有如返回值为T&这种在类模板中的函数,也难以通过返回值的方式来标识错误。
例如上面这个函数,返回错误码的方式就并不太好用。
2. 缺点
(1)异常会导致程序的执行流乱跳,非常的混乱。并且是在运行出错抛异常的时候乱跳,这可能就会导致一些难以预料的问题,比如内存泄漏。同时也会让我们在跟踪调试及分析程序的时候比较困难。
(2)异常会有一些性能的开销,但是这个开销其实并不高,基本可以忽略。
(3)C++和java等语言不同,没有垃圾回收机制,程序申请的资源需要自己管理。有了异常后就很可能导致内存泄漏、死锁等异常安全问题。要解决这一问题,需要使用RAII来处理资源的管理问题,这一方法会放在下一章“智能指针”中讲解。
(4)C++标准库的异常体系定义的不好,导致很多人都不太喜欢用或者说用的比较混乱。
(5)异常不能随意抛,否则会让最外层接收异常的用户非常难受。因此异常一般又两个规范:1是抛出的异常类型都继承自一个基类;而是函数是否抛异常,最好用noexcept标识。
总的来讲,异常是利大于弊的,因此在大家未来的实际工作中还是比较推荐使用异常以帮助定位错误的。