目录:
- 前言
- 异常
- (一) c语言原有的错误处理方式
- (二) 异常的概念
- (三)异常的使用
- 1.异常的抛出与捕捉
- 2.函数调用链中异常栈的展开原则
- (四)5组测试及对应结论
- 1.常规测试
- 2.异常重新抛出
- (五)异常安全
- (六)异常规格化
- (七)自定义异常体系
- (八)c++标注库中的异常体系
- 总结
前言
打怪升级:第90天 |
---|
异常
(一) c语言原有的错误处理方式
- 终止程序,如assert,当遇到错误时会直接终止进程,比如访问QQ空间,由于某些原因QQ空间无法查看,如果使用assert,就会直接把你的QQ退掉,显然用户无法接受。
- 返回错误码errno,C语言很多库的接口函数都是通过errno返回错误码,缺点是错误信息需要程序员自己查看,并且错误信息并不全面。
实际中,大多数情况都使用错误码标记错误,少数极端错误才会使用assert。
(二) 异常的概念
异常是错误处理的一种方式,当函数遇到自己无法处理的错误时就可以抛异常,由函数自己或函数的直接、间接调用者来处理异常。
throw:抛出异常,当遇到特定错误时就通过throw关键字抛出异常对象。
catch:捕捉异常,在你想要处理特定异常的地方,使用catch关键字捕捉该异常。
try:try块中为可能抛出异常的代码,只有在try块中的异常才能被对应的catch捕捉。
关键字使用:
try
{
if(condition)
{
throw(...);
}
else
{}
}
catch(exception_type1 e)
{}
catch(exception_type2 e)
{}
catch(...)
{}
(三)异常的使用
1.异常的抛出与捕捉
- 异常抛出的是对象,根据对象的类型来决定应该激活哪段catch处理代码;
- 抛出的对象可以是任何类型;
- 异常的捕捉会可以在当前函数调用链的任意位置进行;
- 被选择的处理代码是与该异常类型匹配且在该调用链上距离异常最近的一个;
- catch(…)可以捕捉任意类型的异常,只是无法获取异常信息;
- 异常类型匹配有一个特例:派生类的异常可以使用父类类型进行捕捉,这个在实际中非常常用。
2.函数调用链中异常栈的展开原则
- 首先检查throw本身是否在try块中,
1.1 如果是,再检查是否有匹配的catch,
1.1.1如果有就执行匹配的catch处理代码;
1.1.2如果没有就沿着函数调用链往回查找是否处于调用函数的try块中,重复上述操作;
1.2如果没有就沿着调用函数链往回查找是否处于调用函数的try块中,重复上述操作; - 如果到main函数的栈中都不在try块中,或没有找到匹配的catch就会终止进程。
上述沿着调用链查找catch子句的过程,称为栈展开。
(四)5组测试及对应结论
1.常规测试
#include<iostream>
using namespace std;
int main() {
while (1)
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division by zero");
else
cout << "a / b = " << a / b << endl;
}
return 0;
}
结论1:抛出异常后,如果不进行捕捉,依然会终止整个进程。
#include<iostream>
using namespace std;
int main() {
while (1)
{
try
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division by zero");
else
cout << "a / b = " << a / b << endl;
}
catch (const char* s) // 捕捉 const char* 类型的异常
{
cout << s << endl;
}
}
return 0;
}
结论2:对异常进行捕捉后,可以获取异常对象(此处为一个字符串)
#include<iostream>
using namespace std;
int main() {
while (1)
{
try
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division by zero");
else
cout << "a / b = " << a / b << endl;
}
// catch (const char* s) // 捕捉 const char* 类型的异常
catch (int s) // 捕捉 int 类型的异常
{
cout << s << endl;
}
}
return 0;
}
结论3:要捕捉到异常,需要有与之对应的捕捉类型。
#include<iostream>
#include<string>
using namespace std;
void Div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division by zero");
else
cout << "a / b = " << a / b << endl;
}
void Func()
{
Div();
}
int main() {
while (1)
{
try
{
Func();
}
catch (const char* s)
{
cout << s << endl;
}
}
return 0;
}
结论4:异常并非只能在哪里抛出就在哪里捕捉,而是会沿着调用链查找第一个与之匹配的catch捕捉。
并且这里的跳转到catch并非层层出栈,而是直接跳转到main函数中的catch位置,并没有再进入func函数(有坑)。
void Func()
{
int* a = new int[10];
try
{
Div();
}
catch (...)
{
cout << "delete array" << endl;
delete[] a;
throw;
}
cout << "delete array" << endl;
delete[] a;
}
2.异常重新抛出
结论5:抛出异常后,执行流会直接跳转到最近的类型匹配的catch,由于跳过了func函数,所以导致了内存泄漏,
我们需要让执行流在func位置停留,以便释放堆区空间,但是由于我们并不是在func中对异常进行处理,所以要再次转发出去。
(五)异常安全
- 构造函数用于初始化对象,尽量不要在构造函数处抛异常,防止对象初始化不完全;
- 析构函数用于销毁对象,尽量不要在析构函数处抛异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等);
- 由于抛异常后会直接跳转到类型匹配的catch位置,会导致一些函数后续执行被打乱,比如在new 和 delete之间导致内存泄漏、
在lock 和 unlock之间导致死锁,在open 和 close 之间导致系统内存减少等,C++经常使用RAII来解决以上问题。
请查看上方最后一个示例。
(六)异常规格化
异常规格化是为了方便函数使用者知道该函数会抛出那些异常,具体做法为在函数参数列表之后加上 throw(类型…),列出这个函数可能抛出的所有异常,throw() 表示该函数不会抛出异常;
但是需要注意,该方式只是说明可能抛出的异常类型,编译器不会进行检查,因此是否使用以及使用的规范性完全看函数设计者。
#include<iostream>
using namespace std;
void Func() throw(const char*)
{
int a = rand() % 6;
if (a == 0)
throw("zero");
else if (a == 1)
throw(a);
else
cout << a << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const char* s)
{
cout << "const char* s: " << s << endl;
}
}
return 0;
}
如图所示,上方的异常规格化方式为c++98的设定,由于没有严格的检查,在使用者之间也没有形成统一的规范,因此很多使用者选择弃而不用,因此这个规格化方式形同虚设。
- c++11中对规格化做了简化,如果该函数不可能抛异常就在函数后加上 noexcept(等同于 throw() ),否则就什么也不写,由用户自己去查看,但是同之前一样,noexcept也未进行严格的限制,上方只是报错;
- noexcept只会检查当前函数是否抛异常,嵌套一层就无法检查出;
- 设置了noexcept后,该函数即使抛异常,也无法被捕获,直接终止进程。
(七)自定义异常体系
上方我们所抛出的异常都是一个整形或者一个字符串,而这些提供的信息还是很少,并没有很大程度上改善报错信息的情况,
因此在实际中,我们一般会采用返回结构体对象的方式来获取更加详细的信息,此处就离不开继承与多态了。
在公司实际开发时,一个项目基本都是多个开发人员联合开发,如果对异常操作没有规定,程序猿A在异常抛出位置就进行捕捉,程序猿B则打算在main函数处进行统一处理,而程序猿C则是在有的处理了有的没有处理,
并且假设每只程序猿至少抛出了5种类型的异常,那么最后一位负责main函数处理的程序猿M怎么做,我是应该处理多少个异常,你这个异常捕捉后处理了没有,“哎,我这里怎么出错了没有抛异常”,“哎,你这个人捕捉了异常怎么不处理”。。。
因此为了统一异常的使用,公司一般会规定在main函数统一进行异常处理,并且使用多态来重写不同的错误情况。
下方我们模拟一个QQ用户登录并访问空间的场景,我们将异常打印在显示器上,而在公司中则是需要写入日志。
#include<iostream>
#include<Windows.h>
using namespace std;
class Exception
{
public:
Exception(int erron, const string& msg)
:_erron(erron)
, _errmsg(msg)
{}
virtual void Print()
{
GetErr();
}
void GetErr()
{
cout << _erron << ": " << _errmsg << ", ";
}
private:
int _erron; // 错误码
string _errmsg; // 错误信息
};
class LoginID:public Exception // 用户登录
{
public:
LoginID(int erron, const string& msg, int id)
:Exception(erron, msg)
, _userid(id)
{}
virtual void Print() override
{
GetErr();
cout << "_userid: " << _userid << endl;
}
private:
int _userid; // 错误用户id
};
class Webio :public Exception // 网络请求
{
public:
Webio(int erron, const string& msg, int port)
:Exception(erron, msg)
, _portnum(port)
{}
virtual void Print() override
{
GetErr();
cout << "_portnum: " << _portnum << endl;
}
private:
int _portnum; // 错误端口号
};
class MySQL :public Exception // 数据库服务
{
public:
MySQL(int erron, const string& msg, int id)
:Exception(erron, msg)
, _userid(id)
{}
virtual void Print()override
{
GetErr();
cout << "_userid: " << _userid << endl;
}
private:
int _userid; // 错误用户id
};
void Func()
{
int num = rand() % 10;
if (num < 3)
{
throw(LoginID(1, "LoginID ERRON", num));
}
else if (num < 6)
{
throw(Webio(2, "Webio ERRON", num));
}
else if (num < 9)
{
throw(MySQL(3, "MySQL ERRON", num));
}
else
{
cout << "success, num = " << num << endl;
}
}
int main()
{
while (1)
{
try
{
Func();
}
catch (Exception& e) // 多态
{
e.Print();
Sleep(1000);
}
catch (...)
{
cout << "unknow exception" << endl;
}
}
return 0;
}
(八)c++标注库中的异常体系
exception
C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
实际中我们可以可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。
new失败抛出异常:
// bad_alloc example
#include <iostream> // std::cout
#include <new> // std::bad_alloc
#include<exception> // std::exception
using namespace std;
int main() {
try
{
int* myarray = new int[int(1e10)];
}
/*catch (const exception& e)
{
cerr << "std::exception: " << e.what() << endl;
}*/
catch (bad_alloc& ba) // 捕捉结果同上
{
cerr << "bad_alloc caught: " << ba.what() << endl;
}
return 0;
}
总结
- C++异常的优点:
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
- 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误;而异常则直接跳转到匹配的catch。
- 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
- 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
- C++异常的缺点:
- 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
- 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
- C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
- C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
- 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func()noexcept;的方式规范化。
总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋。