总言
主要介绍异常。
文章目录
- 总言
- 1、C++异常
- 1.1、C语言传统的处理错误的方式
- 1.2、异常概念
- 1.3、异常的基本用法
- 1.3.1、异常的抛出和捕获
- 1.3.1.1、异常的抛出和匹配原则
- 1.3.1.2、 在函数调用链中异常栈展开匹配原则
- 1.3.2、异常的重新抛出
- 1.3.2.1、演示一
- 1.3.2.2、演示二
- 1.3.3、异常安全
- 1.3.4、异常规范
- 2、 自定义异常体系
- 2.1、基本说明
- 2.2、相关演示
- 3、标准库异常体系
- 4、异常的优缺点
- 4.1、优点
- 4.2、缺点
1、C++异常
1.1、C语言传统的处理错误的方式
1)、传统的错误处理机制:
1. 终止程序,如assert。缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
2. 返回错误码。缺陷:需要自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
PS:实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。
1.2、异常概念
1)、基本介绍
说明:
①异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
②如果有一个块抛出一个异常,捕获异常的方法会使用 try
和 catch
关键字。try
块中放置可能抛出异常的代码,try
块中的代码被称为保护代码。
try
{
// 保护的标识代码
}
catch (ExceptionName e1)
{
// catch 块
}
catch (ExceptionName e2)
{
// catch 块
}
catch (ExceptionName eN)
{
// catch 块
}
throw
: 当问题出现时,程序会 “抛出” 一个异常,通过使用 throw 关键字来完成的。
catch
: 在想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常,可以有多个catch进行捕获。
try
: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
PS:相比于C语言,C++的异常是通过对象(ExceptionName
)接收的,根据之前所学,一个对象内可存储的数据信息多种多样,如此丰富了错误信息。
2)、基础演示
相关代码如下:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
//int a = 2, b = 3;
int a = 2, b = 0;
cout << "a: " << a << ", b: " << b << endl;
cout << Division(a, b) << endl;
}
int main()
{
try {
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...) {
cout << "unkown exception" << endl;
}
return 0;
}
1.3、异常的基本用法
1.3.1、异常的抛出和捕获
1.3.1.1、异常的抛出和匹配原则
1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
演示如下:这里有两个catch,但上述throw "Division by zero condition!"
相对匹配的类型是char*
的对象。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
//throw 123;
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
//int a = 2, b = 3;
int a = 2, b = 0;
cout << "a: " << a << ", b: " << b << endl;
cout << Division(a, b) << endl;
}
int main()
{
try {
Func();
}
catch (const int errid)
{
cout << "catch (const int errid)" << endl;//用于验证
cout << errid << endl;
}
catch (const char* errmsg)
{
cout << "const char* errmsg" << endl;//用于验证
cout << errmsg << endl;
}
catch (...) {
cout << "unkown exception" << endl;
}
return 0;
}
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的调用链中那一个。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw 123;
//throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
try {
//int a = 2, b = 3;
int a = 2, b = 0;
cout << "a: " << a << ", b: " << b << endl;
cout << Division(a, b) << endl;
}
catch (const char* errmsg)
{
cout << "Func() ------ const char* errmsg" << endl;//用于验证
cout << errmsg << endl;
}
}
int main()
{
try {
Func();
}
catch (const int errid)
{
cout << "main() ------ catch (const int errid)" << endl;//用于验证
cout << errid << endl;
}
catch (...) {
cout << "unkown exception" << endl;
}
return 0;
}
PS:异常必须被捕获,不能存在没有匹配的异常类型的情况(会报错/终止程序,但这样子的程序行为是过于“大惊小怪的”。例如,①使用异常处理,本意是出错后弹出个对话框,用以错误警告,但该程序其它功能仍旧能正常执行,②若因没有匹配类型而直接把程序终止掉,这是我们不期望的行为方式。)
为了防止出现此类情况,引入catch(...)
:
3. catch(...)
可以捕获任意类型的异常,只是不知道该异常原因具体是什么。(只有在其它类型的捕获异常都匹配不上时,才用...
捕获,表示未知异常。)
4. 虽然在throw处我们可以自定义填写内容,但一般情况都是抛一个异常对象。写法如下:
class Exception//用于返回异常的对象
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_errid(id)
{}
int GetErrid()const
{
return _errid;
}
string GetErrmsg()const
{
return _errmsg;
}
protected:
string _errmsg;//错误信息
int _errid;//标识id
};
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
Exception e("除零错误!", 1);
throw e;
}
else
return ((double)a / (double)b);
}
void Func()
{
//int a = 2, b = 3;
int a = 2, b = 0;
cout << "a: " << a << ", b: " << b << endl;
cout << Division(a, b) << endl;
}
int main()
{
try {
Func();
}
catch (const Exception& e)
{
cout << e.GetErrid() << "---" << e.GetErrmsg() << endl;
}
catch (...) {
cout << "未知异常!!!" << endl;
}
return 0;
}
PS:抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。
PS:此处相关演示代码见下述小节2:自定义异常体系。
1.3.1.2、 在函数调用链中异常栈展开匹配原则
根据上述异常的使用,当项目中存在多个函数调用时,异常的匹配规则如下:
1. 首先检查throw本身是否在try块内部,如果在,则查找匹配的catch语句。若有匹配的,则调到catch的地方进行处理。
2. 没有匹配的catch,则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。 上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。(实际中都会在最后加一个catch(...)
捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。)
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
1.3.2、异常的重新抛出
在一些场景中,有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
1.3.2.1、演示一
以下述小节2中,自定义异常体系的代码为例。假设在 HttpServer
中,我们要求当异常为“网络错误”时,要先连续请求三次,若都失败才将异常抛出。
// 服务器开发中通常使用的异常继承体系
class Exception//基类:异常对象
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
int Geterrid()const
{
return _id;
}
protected:
string _errmsg;//错误信息
int _id;//错误码
};
class HttpServerException : public Exception//异常对象:表示由协议产生的异常,继承了基类 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 += _errmsg;
return str;
}
private:
const string _type;
};
void SendMsg(const string & str)
{
srand(time(0));
if (rand() % 2 == 0)
{
throw HttpServerException("当前网络错误", 1, "get");
}
else if (rand() % 5 == 0)
{
throw HttpServerException("当前权限不足", 2, "post");
}
cout << "本次请求成功:" << str << endl;
}
void HttpServer()
{
int count = 3;//用于表示请求次数
string str = "客户端发送了一个请求.";
while (1)
{
Sleep(1000);
try {
SendMsg(str);
}
catch (const Exception& e)
{
if (e.Geterrid() == 1 && count)
{
count--;
cout << "count:" << count << endl;//方便测试观察
continue;//继续while循环,再次try
}
else
{
throw e;//将异常抛出到上一层
}
}
}
}
int main()
{
while (1)
{
cout << "------------------------" << endl;//用以区分每一回合:方便测试观察
try {
HttpServer();//服务端层层调用
}
catch (const Exception& e) // Exception是基类对象。尽管上述通过继承设置了不同的异常体系,但这里直接捕获父类对象就可以。
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
演示结果如下:
1.3.2.2、演示二
除了上述情况,还有以下这类情景:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "除零错误!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];//new:在堆区开辟空间
try {
int a = 2, b = 0;
cout << Division(a, b) << endl;
}//需要在当前try块内部进行异常捕获
catch (...)//因为throw直接抛出到main的栈中,这里堆区开辟的空间没有得到释放,会造成内存泄漏
{
cout << "delete []" << array << endl;//但这里捕获只是为了释放堆区空间
delete[] array;
throw;//还需要将异常重新抛出,交给外部做后续处理。
}
// ...
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
演示结果如下:
1.3.3、异常安全
1、构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
2、析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
3、C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII在智能指针中讲解。
1.3.4、异常规范
1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型)
,列出这个函数可能抛掷的所有异常类型。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
2. 函数的后面接throw()
,表示函数不抛异常。
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
注意事项:异常所提供的规范,只是期望能按照此规则执行,但不代表强制性行为。实际大多时候,C++中都会使用自定义异常体系,以下将进行讲解说明。
2、 自定义异常体系
2.1、基本说明
说明:实际使用中,很多时候会自定义自己的异常体系,进行规范的异常管理。通常,抛出的都是继承的派生类对象,使用一个基类进行捕获。
2.2、相关演示
相关代码如下:
// 服务器开发中通常使用的异常继承体系
class Exception//基类:异常对象
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
int Geterrid()const
{
return _id;
}
protected:
string _errmsg;//错误信息
int _id;//错误码
};
class SqlException : public Exception //异常对象一:表示由SQL产生的异常,继承了基类 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//异常对象二:表示由缓存产生的异常,继承了基类 Exception
{
public:
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
class HttpServerException : public Exception//异常对象三:表示由协议产生的异常,继承了基类 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 += _errmsg;
return str;
}
private:
const string _type;
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 5, "select * from name = 'SQL'");
}
//throw "xxxxxx";
cout << "本次请求成功" << endl;
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 3);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 4);
}
SQLMgr();
}
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 1, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 2, "post");
}
CacheMgr();
}
int main()
{
while (1)
{
Sleep(1000);
try {
HttpServer();//服务端层层调用
}
catch (const Exception& e) // Exception是基类对象。尽管上述通过继承设置了不同的异常体系,但这里直接捕获父类对象就可以。
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
3、标准库异常体系
相关链接:std::exception
C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。(若有需要可查阅文档学习了解)
4、异常的优缺点
4.1、优点
1、相比错误码的方式,异常对象可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2、传统方式中,返回错误码有一个很大的问题:在函数调用链中,深层的函数返回错误时,需要层层返回,才能在最外层拿到错误。(而若是异常体系,只需要抛出异常,会直接跳到相应catch捕获的地方,直接处理错误)
3、 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用这些库时也需要使用到异常。
4、部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator(const size_t pos)
这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
4.2、缺点
1、异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
2、 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
3、C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
4、C++标准库的异常体系定义得不完善,导致大家各自定义各自的异常体系,造成混乱。
5、异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:①抛出异常类型都继承自一个基类。②函数是否抛异常、抛什么异常,都使用 func()
throw()
;的方式规范化。