文章目录
- 一. C语言的错误处理
- 二. C++的异常
- 三. 异常的抛出与捕获
- 1. 异常的抛出与匹配机制
- 2. 在函数调用链中异常栈展开匹配原则
- 3. 异常安全
- 四. 多态在异常中的应用
- 五. C++标准库的异常体系
- 六. 异常规范
- 七. 异常的优缺点
- 结束语
一. C语言的错误处理
在C语言中,我们常见的处理错误的方式就是assert,不过其方式较为暴力,会直接终止程序。
在Linux操作系统中,退出码也是常见的记录错误信息的方式,不过需要我们自己去查找错误原因
二. C++的异常
C++在C语言的基础上,觉得单单返回退出码较为单调,信息不足,所以提出了异常的概念
异常的概念:
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误,或者想让外部函数处理该错误时,就可以抛异常。让函数的直接或间接调用者处理这个错误
异常的三个关键字:
throw
:当问题出现时,可以通过throw关键字抛出,抛出的异常可以是任何类型try
:try块中的代码,代表其可能会抛出异常,需要进行捕获,其后通常跟着一个或者多个catch块catch
:通过catch捕获可能抛出的异常,如果捕捉成功,即可执行catch块中的代码(处理方式)
如果一个块抛出一个异常,就需要try,catch语句捕获。try块中放置可能抛出异常的代码,catch块中放置处理方式等等。try块中的代码被称为保护代码。
三. 异常的抛出与捕获
接下来我们使用除零错误
演示以上三个关键字的使用
#include<iostream>
using namespace std;
//除法函数
double division(double a, double b)
{
if (b == 0)//除零异常,抛出异常
throw "Divide by zero error";
return a / b;
}
//除法函数调用者
void Func()
{
while (1)
{
double a, b;
cin >> a >> b;
cout << division(a, b) << endl;;
cout << "void Func()" << endl;
}
}
//主函数
int main()
{
//捕获
try
{
Func();
}
catch (const char*str)
{
cout << str << endl;
}
return 0;
}
一次运行结果如下:
当没有出现除零异常时,while循环一直进行,“void Func()”也成功打印
但出现除零异常时,代码运行直接跳转到了main函数的catch部分,并且str成功捕捉到了throw的字符串
1. 异常的抛出与匹配机制
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 被选中的处理代码时调用链中
与该对象类型匹配且离抛出异常位置最近
的那一个 - 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在catch后销毁(这里的处理类似于函数的传值返回)
catch(...)
可以捕获任意类型的异常,…是可变参数包。但问题是不知道异常错误是什么- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配。如果使用
多态
,即抛出派生类对象,然后使用基类捕获
,这个在实际中非常实用。
2. 在函数调用链中异常栈展开匹配原则
- 首先检查throw本身是否处于try块内部,如果是,匹配适合的catch语句。如果有匹配的,则跳转到catch的地方进行处理
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,还是没有匹配的,则报错终止程序——异常必须被捕获。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行
上述沿着调用链查找匹配的catch语句的过程称为栈展开
。所以实际中,为了防止未知异常终止程序,我们可以在最后加上catch(…),捕获任意类型的异常
异常没有被捕获
捕获匹配原则
还是除零异常
#include<iostream>
using namespace std;
//除法函数
double division(double a, double b)
{
if (b == 0)
throw "Divide by zero error";
return a / b;
}
//除法函数调用者
void Func()
{
while (1)
{
double a, b;
cin >> a >> b;
try
{
cout << division(a, b) << endl;
}
catch (const char*str)
{
cout << str << endl;
}
cout << "void Func()" << endl;
}
}
//主函数
int main()
{
//捕获
try
{
Func();
}
catch (const char*str)
{
cout << str << endl;
}
return 0;
}
如果Func()中也有匹配的catch语句,那么就会直接在Func中被捕获,不会再跳转到main函数中。
成功捕获后,会执行catch语句后的代码
catch(...)捕获任意类型异常
#include<iostream>
using namespace std;
//除法函数
double division(double a, double b)
{
if (b == 0)
throw "Divide by zero error";
return a / b;
}
//除法函数调用者
void Func()
{
while (1)
{
double a, b;
cin >> a >> b;
try
{
cout << division(a, b) << endl;
}
catch (const char*str)
{
cout << str << endl;
throw 6;//抛出另外的异常
}
cout << "void Func()" << endl;
}
}
//主函数
int main()
{
//捕获
try
{
Func();
}
catch (const char*str)
{
cout << str << endl;
}
catch (...)//捕获任意类型的异常
{
cout << "捕获未知异常" << endl;
}
return 0;
}
3. 异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或者没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄露(内存泄露)
- C++中异常经常导致资源泄露的问题,因为throw抛出异常,会直接查找catch捕捉语句,导致后续的代码无法执行。在互斥锁中,lock和unlock之间抛出异常就会导致死锁。
四. 多态在异常中的应用
多态是使用基类指针或者基类引用接收不同对象,会有不同的效果。
因为异常存在多种形式,多种错误信息,所以可以提供一个基类,其派生类再添加各自的错误信息。
比如,我们定义一个Exception基类
class Exception
{
public:
//构造函数
Exception(string &errMes,int errId)
:_errMes(errMes)
,_errId(errId)
{}
//显示错误信息的虚函数
virtual string what()const
{
return _errMes;
}
//权限需要设置成保护的
//不然派生类无法访问
protected:
string _errMes;
int _errId;
};
我们定义what函数,获取异常的错误信息,派生类通过重写what虚函数,或者添加成员变量来满足不同的需求
比如在项目中,我们可能会用到数据库,网络。那么在不同部分抛出的异常信息不同,就可以使用派生类
//数据库异常
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 += _errMes;
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 += _errMes;
return str;
}
};
//Http异常
class HttpServerException : public Exception
{
public:
HttpServerException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpServerException:";
str += _type;
str += ":";
str += _errMes;
return str;
}
private:
const string _type;
};
在main函数中,不管调用哪个功能,抛出的异常都可以用const Exception&捕获,调用what显示异常信息。
五. C++标准库的异常体系
C++也有提供异常类——excepion
exception就是异常类的基类,提供虚函数what
也实现了很多派生类
像我们使用的new关键字,其实是调用了operator new
如果失败,会抛bad_alloc这个异常
下图是C++提供的异常类
六. 异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面接throw(类型),列出这个函数可能抛出的所有异常类型
- 函数后面接throw(),表示函数不抛异常
- 若无异常接口声明,则此函数可以抛任何类型的异常
比如:
第一个**throw(std::bad_alloc)表明可能会抛std::bad_alloc这个异常
第二个和第三个throw ()**表明不会抛异常
但是因为这个不是强求的,所以很多人并不会遵循这个异常规范。其次,即使throw()标识不会抛异常,但是仍然在其中抛异常也并不会报错。抛出了未标识的异常也不会报错
所以C++11为了更加的规范,就提供了noexcept
比如线程的构造函数
一个函数如果明确不抛异常,可以加noexcept。但是如果加了noexcept仍然抛异常,也不会直接报错或者终止程序,而是会有警告。
如果对加了noexcept的函数进行try,catch捕获,则会终止程序
可能会抛异常,就不加noexcept,C++98的异常规范看个人
七. 异常的优缺点
C++异常的优点
- 异常不同于C语言的错误码,异常可以接收对象,可以包含更多信息,可以更清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
- 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。而异常可以直接在main函数中捕获,达到直接跳转到最外层
- 很多第三方库都使用了一场,比如boost,gtest,gmock等等常用的库,如果要使用这些库,就需要捕获异常
- 部分函数使用异常更好处理,比如构造函数没有返回值,不方便用错误码的方式处理(虽然不建议构造函数抛异常)。返回值如果是模板,也无法返回错误码
C++异常的缺点
- 异常会导致程序的执行流乱跳,并且很混乱。这会使得调试分析变得困难
- C++没有垃圾回收机制,资源需要自己管理。使用异常可能导致内存泄露,死锁等异常完全问题。
- C++标准库的异常体系定义得不太好,导致大家各自定义各自的异常体系。
异常尽量规范使用,随意抛异常,外层捕获的人苦不堪言。所以异常规范有两点:一. 抛出异常类型都继承一个基类
。二. 函数是否抛异常,抛什么异常,可以使用noexcept和throw(...)
标识
结束语
感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。