赞
踩
在C语言中, 代码发生错误一般会有两种处理方式:
终止程序.
比如 直接使用assert()
断言. 或者直接崩溃
返回、设置错误码
C语言某些函数执行失败, 但是结果不足以导致致命问题时, 就会将错误码设置在errno
中. 用户可以通过strerr(errno)
来获取错误信息.
但是这些针对错误的处理方式, 不灵活. 严重的错误直接就是崩溃, 没有一点回转的余地. 虽然程序出现问题很可能跟编写有关, 不过还是灵活一些比较好.
由于C语言中针对错误的处理不灵活. 所以C++引入了异常的概念.
异常是什么?
异常是一种处理错误的方式, 当一个函数 发现自己无法处理的错误时就可以 抛出异常, 让函数的直接或间接的 调用者 处理这个错误.
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 len, time; cin >> len >> time; cout << Division(len, time) << endl; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } return 0; }
这段代码就可以展现出最简单的 抛异常、捕捉异常、处理异常的场景.
那么 这段代码执行会出现什么现象呢?
这段代码, 我们在main()
的try作用域
中调用了Func()
, 在Func()
中调用了Division()
计算两数相除.
执行代码后, 可以发现 当遇到除零错误时, 会打印一个字符串. 且 这个字符串就是throw "Division by zero condition!";
中的字符串. 并且, 没有返回错误退出信息.
如果将Func()
从try
中移除, 又会是什么结果呢?
此时 发生除零错误, 进程会直接被abort
终止. 退出信息为134
. 这是编译器帮忙强制终止了
从这里可以看出, C++异常是如何处理的:
throw
throw
是一个关键词, 用来抛出异常, 可以抛出 任意类型. 上述例子中:
if(b == 0);
throw "Division by zero condition!";
就是在发生除零错误时, 抛出异常"Division by zero condition!"
try
try
也是一个关键词, 一般来说可能会抛出异常的代码, 都放在try
块中. 放在try
块中的代码, 通常被成为 保护代码. 在本例中:
Func()
在try
中时, 可以捕获到异常并处理
不在try
中时, 不会捕获异常.
catch
catch
同样是一个关键词, 是用来捕获throw
抛出的异常的. 在本例中:
catch (const char* errmsg)
捕获const char*
类型的异常. catch
的异常类型 必须与throw
的异常类型相同. 否则无法捕获目标异常.
catch
块中的代码, 为 捕获到异常后 要做的处理.
可以有多个catch
针对不同的异常进行捕获, 但是 多个catch
中不能设置相同类型的异常
即, 如果像这样设置catch
:
try {
}
catch (const char* e){
}
catch (const char* e){
}
在一些编译器中会报错, 最少也是一个警告:
这就表示, 第二个catch (const char* e)
捕获不到const char*
类型的异常.
这就是C++异常处理的最基本的概念.
上面介绍了 最基础的异常的使用.
不过, 想要用好异常, 还有许多的细节 和 用法需要了解.
异常是通过抛出 对象 而引发的, 该 对象的类型 决定了应该激活哪个catch
的处理代码
即, catch
的异常类型 必须与throw
的异常类型相同. 否则无法捕获目标异常.
被选中的处理代码是调用链中 与该对象类型匹配 且离抛出异常位置最近 的那一个
这句话是什么意思呢?
来分析这一段代码:
#include <iostream> using std::cout; using std::endl; using std::cin; double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { try { throw "Division by zero condition!"; } catch (const char* errmsg) { cout << "Division 捕获了 const char* 异常: " << errmsg << endl; return 1; } } else return ((double)a / (double)b); } void Func() { int len, time; cin >> len >> time; try { cout << Division(len, time) << endl; } catch (const int errI) { cout << "Func 捕获了 const int 异常: " << errI << endl; } catch (const char* errS) { cout << "Func 捕获了 const char* 异常: " << errS << endl; } } int main() { try { Func(); } catch (const char* errmsg) { cout << "main 捕获了 const char* 异常: " << errmsg << endl; } return 0; }
这段代码, 如果发生除零错误, 会触发哪个catch
捕获异常呢?
很明显是Divison()
内部的catch (const char* errmsg)
会捕捉到. 因为 捕捉异常类型与抛出异常类型匹配 且离抛出异常位置最近
如果 将Division()
内部的catch (const char* errmsg)
改为catch (const int errI)
, 那么又会被哪个catch
捕捉到呢?
没错, 就是Func()
内部的catch (const char* errS)
throw
抛出异常时, 编译器会生成一个 异常对象的临时拷贝. 就像函数返回那样. 所以可以正常的被更上层的函数栈捕捉到.
使用catch(...)
可以 捕获任意类型的异常.
也就是说, 使用catch(...)
之后, 只要 有异常在此之前没有被捕获, 就 会捕获此异常.
比如可以用这段代码来实验一下:
#include <iostream> using std::cout; using std::endl; using std::cin; class Exc1 { private: int _excID; }; class Exc2 { private: int _excID; }; double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } else if (b == 1) { throw 1024; } else if (b == 2) { throw 'b'; } else if (b == 3) { throw Exc1(); } else if (b < 0) { throw Exc2(); } else return ((double)a / (double)b); } void Func() { int len, time; cin >> len >> time; try { cout << Division(len, time) << endl; } catch (const int errI) { // 捕获const整型异常 cout << "Func 捕获了 const int 异常: " << errI << endl; } catch (const char errC) { // 捕获const字符异常 cout << "Func 捕获了 const char 异常: " << errC << endl; } } int main() { while(true) { try { Func(); } catch (const char* errmsg) { // 捕获const字符串异常 cout << "main 捕获了 const char* 异常: " << errmsg << endl; } catch (...) { cout << "main 捕获了 未知异常" << endl; } } return 0; }
const char*
、const int
、const char
这三种类型的异常, 我们分别在main()
和Func()
中指定捕捉了.
而 在传入的 b == 3 和 b < 0 时, 抛出的Exc1对象和Exc2对象
异常 并没有指定捕捉.
但是, 他们却执行了cout << "main 捕获了 未知异常" << endl;
这个语句.
即, 执行了catch(...)
内的处理.
这可以说明, catch(...)
可以捕捉任意类型的异常. 但是由于没有指定类型, 所以不知道捕捉到的究竟是什么类型的异常.
虽说 catch
的异常类型 必须与throw
的异常类型相同. 否则无法捕获目标异常.
但实际上, 那只是一般情况下. 除此之外, 还存在一个特例:
如果catch
捕捉基类异常, 那么 除了可以捕捉到throw
抛出的 此基类异常外, 还可以捕捉到throw
抛出的 此基类的派生类异常
比如这段代码:
#include <iostream> using std::cin; using std::cout; using std::endl; class faClass { private: size_t _faExcID; }; class sonClass: public faClass { private: size_t _sonExcID; }; double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } else if (b == 1) { throw faClass(); } else if (b < 0) { throw sonClass(); } else return ((double)a / (double)b); } void Func() { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } int main() { while (true) { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } catch (const faClass& e) { cout << "main 捕获到faClass类异常 或 以faClass为基类的派生类异常" << endl; } } return 0; }
这段代码:
b
传入0
,throw "Division by zero condition!"
b
传入1
,throw faClass()
b
传入负数
,throw sonClass()
.sonClass
是faClass
的派生类而除了catch(const char* errmsg)
之外, 只catch(const faClass& e)
那么, 这段代码发生各种异常的结果是什么?
可以看到, 当b
传入负数
或1
时, 都会执行catch (const faClass& e)
内的处理动作.
这其实就证明了, 如果catch
捕捉基类异常, 那么 除了可以捕捉到throw
抛出的 此基类异常外, 还可以捕捉到throw
抛出的 此基类的派生类异常
这个特性非常的有用! 经常在开发中使用!
首先检查throw
本身是否在try
块内部.
如果是, 则在当前函数栈帧 查找匹配的catch
语句
如果当前函数栈帧有匹配的, 则 跳到catch
的地方进行处理
如果当前函数栈帧内没有匹配的, 则 退出当前函数栈, 继续在外层调用函数的栈中进行查找匹配的catch
此操作, 会一直退出到main()
函数栈帧中
如果到达main
函数的栈, 依旧没有匹配的catch
, 则 终止进程
整个沿着调用链 向更外层调用函数的栈帧中查找匹配的catch的行为, 被称为 栈展开
为了避免 由于异常没有匹配的catch
导致进程终止, 所以 都会在最后 使用catch(...)
捕获未知异常
找到匹配的catch
子句并处理以后,会继续沿着catch
子句后面继续执行
此原则中, 前三条原则 即为栈展开的过程.
假如存在Func1()
、Func2()
、Func3()
:
void Func3() {
throw "Throw an exception directly!";
cout << "hello Func3" << endl;
}
void Func2() {
Func3();
cout << "hello Func2" << endl;
}
void Func1() {
Func2();
cout << "hello Func1" << endl;
}
且,main()
函数呢存在以下内容:
int main() {
try {
Func1();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
cout << "hello main" << endl;
return 0;
}
那么,throw
异常之后, 栈展开的过程大概为这样的:
C++异常处理有一个 非常麻烦
的点.
throw
之后, 如果找到对应类型的catch
, 会直接跳转到对应函数栈帧内执行catch
子句, 执行完之后 会留在此函数内继续向下执行. 而不是回到throw
继续向下执行.
执行上面的代码也可以证明这一点:
Func1()
、Func2()
、Func3()
中, 都有一句cout
语句. 但是, 只执行了main()
中的cout
语句.
这样很可能会造成什么后果呢? 可以看一看这段代码:
#include <iostream> using std::cout; using std::endl; void Func1() { // new一块空间 int* arr = new int[20480]; throw "Throw an exception directly!"; cout << "hello Func1" << endl; delete[] arr; } int main() { while (true) { try { Func1(); } catch (const char* errmsg) { cout << errmsg << endl; } cout << "hello main" << endl; } return 0; }
这段代码执行之后, 会发生什么后果?
没错, 内存泄漏! 非常严重的内存泄漏!
可以看到, 名为exception_test.exe
的内存占用, 在疯狂的上涨.
这是比较明显的内存泄漏. 这是什么原因呢?
其实是因为, 我们new int[20480]
出来的空间, 没有被delete[]
掉.
但是Func1()
函数中,new[]
之后 函数结束前 明明delete[]
了, 为什么没有delete[]
掉呢?
就是因为Func1()
函数内throw
之后, 直接跳到了main()
中catch
的位置, 并且留在了main()
中没有回到throw
那里. 所以 在throw之后的 delete[]
语句根本就没有执行. 就 造成了内存泄漏
当前阶段, 如何正确的解决这个问题呢?
如果存在new
之后会throw
的可能, 就需要直接在Func1()
内 将throw
放在try
块内, 就地catch
处理并返回:
void Func1() { // new一块空间 int* arr = new int[20480]; try { throw "Throw an exception directly!"; } catch (const char* errmsg) { cout << errmsg << endl; delete[] arr; return; } // 这里也要delete[] // 因为在其他场景中, 可能并不一定会throw delete[] arr; }
这样, 就不会发生内存泄漏了:
这是当前阶段最简单的处理方式, 不过太难用.
观察这段代码:
double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { int* array = new int[10]; try { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } catch (...) { 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; }
Func()
中new
了一个数组. 但是在delete[]
之前又有可能throw
异常.
此异常的处理动作, 已经在 main()
函数中实现了.
但是, 由于还有new
出来的空间未delete
, 又不得不在Func()
函数内添加一个try{...}catch{...}
不过, 这里的捕获异常 可以使用catch(...)
捕获任意异常, 并且不处理异常, 只将未释放的空间delete
掉, 然后再将异常原从新抛出
抛出之后, 会再沿着调用链进行栈展开寻找对应的catch
, 找到真正处理此异常的catch
再处理掉异常
上面例子中, Func()
的catch(...)
子句中的 throw
即为 重新抛出异常
由于这里使用的是
catch(...)
没有指定类型捕获, 所以throw;
就可以重新抛出如果指定了类型
catch(const char* errmsg)
, 就需要throw errmsg;
来实现重新抛出.
关于异常的使用, 有一些情况下需要非常的小心:
构造函数的作用是 完成对象的构造和初始化, 最好 不要在构造函数中抛出异常, 否则 可能导致对象不完整或没有完全初始化
析构函数的作用是 完成资源的清理, 最好 不要在析构函数内抛出异常, 否则 可能导致资源泄漏(内存泄漏、句柄未关闭等)
还有就是, 在new
和delete
中抛出了异常, 导致 内存泄漏. 在lock
和unlock
之间抛出了异常 导致死锁.
这些问题的更加好用的解决, 需要用到智能指针.
在介绍 异常的抛出与捕获匹配原则时, 介绍过 如果catch
捕捉基类异常, 那么 除了可以捕捉到throw
抛出的 此基类异常外, 还可以捕捉到throw
抛出的 此基类的派生类异常
并且也举了简单的例子证明.
所以, C++委员会就根据此原则, 实现了一个 异常类体系.
说明白一点, 就是 C++委员会 实现了许多的类 来对应C++可能发生的所有错误, 被称为 异常类. 这些异常类, 都来派生于一个基类 std::exception
.
文档中对此类的描述是:
标准库中所有组件抛出的异常对象都派生于此类. 因此, 通过捕获此类, 就可以捕获所有标准异常
此类中, 除了构造、析构等成员函数之外, 还实现了一个共其派生类重写的成员函数what()
what()
what()
有什么用呢?
由于标准异常有很多, 而且都可以通过捕获std::exception
来捕获.
所以, 捕获到之后要分辨 捕获到的究竟是什么异常是很麻烦的
所以, std::exception
提供了what()
函数. 它需要实现的作用是: 获取标识异常的字符串
设置成虚函数, 就是为了让派生类重写此函数, 实现不同派生类可以 返回标识其本身的字符串.
也就是说, 通过捕获std::exception
捕获到异常对象之后, 可以调用其成员函数what()
并接收其返回值, 来知道捕获到的是什么异常.
C++标准库中, 实现了许多的异常类.
bad_alloc
分配内存失败时, 抛出的异常.
bad_cast
动态转换时, 抛出的异常
out_of_range
越界访问时, 抛出的异常
…
这张图, 可以用来表示 C++标准库中的异常类体系:
实际的项目开发中, 许多的公司都会自己定义一套异常体系 来进行规范的异常管理.
原因嘛, 用一句话总结 大概就是: 标准库的异常体系无法满足需求.
这里有一个 自定义异常体系的例子:
#include <iostream> #include <string> #include <unistd.h> using std::string; using std::cout; using std::endl; 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; } }; 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 += _errmsg; return str; } private: const string _type; }; void SQLMgr() { if (rand() % 7 == 0) { throw SqlException("权限不足", 100, "select * from name = '张三'"); } else { cout << "Sql Success" << endl; } } void CacheMgr() { if (rand() % 5 == 0) { throw CacheException("权限不足", 101); } else if (rand() % 6 == 0) { throw CacheException("数据不存在", 102); } else { cout << "Cache Success" << endl; } SQLMgr(); } void HttpServer() { if (rand() % 3 == 0) { throw HttpServerException("资源请求错误", 103, "get"); } else if (rand() % 4 == 0) { throw HttpServerException("权限不足", 104, "post"); } else { cout << "Http Success" << endl; } CacheMgr(); } int main() { srand(time(0)); while (true) { // 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境 // Windows 平台 需要将其换为 Sleep(1000); // 并将 头文件 unistd.h 换为 Windows.h sleep(1); try { HttpServer(); } catch (const Exception& e) { // 多态 cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
我们先分析一下这段代码:
首先, 代码实现了 4 个类: 1个基类, 3个派生类
基类
Exception
成员变量:
_errmsg
,string
类型 用于存储异常信息._id
,int
类型 用于存储异常代码成员函数:
what()
, 返回异常信息, 用于获取当前异常类
派生类1
SqlException
成员变量: 除继承于基类的
_errmsg
和_id
之外._sql
,const string
类型 用于存储 异常sql
指令成员函数: 重写
what()
, 返回异常信息, 包括所属类
、_errmsg
和_sql
派生类2
CacheException
成员变量: 除继承于基类的
_errmsg
和_id
之外, 无其他成员变量.成员函数: 重写
what()
, 返回异常信息, 包括所属类
和_errmsg
派生类3
HttpServerException
成员变量: 除继承于基类的
_errmsg
和_id
之外._type
,const string
类型 用于存储 发生异常的服务类型成员函数: 重写
what()
, 返回异常信息, 包括所属类
、type
和_errmsg
其次, 实现了三个函数 用来模拟不同的服务的异常场景
SqlMgr()
:模拟数据库管理时的异常场景:
随机数 % 7 == 0
执行throw SqlException
. 来模拟数据库管理时权限不足的场景.异常信息:
权限不足
, 异常代码:100
, 异常Sql
语句:select * from name = '张三'
CacheMgr()
:模拟缓存管理时的异常场景:
随机数 % 5 == 0
执行throw CacheException("权限不足", 100)
异常信息:
权限不足
, 异常代码:101
随机数 % 6 == 0
执行throw CacheException("数据不存在", 101)
异常信息:
数据不存在
, 异常代码:102
最后, 调用
SqlMgr()
HttpServer()
:模拟HTTP服务可能发生的异常场景:
随机数 % 3 == 0
执行HttpServerException("请求资源不存在", 200, "get")
异常信息:
资源请求错误
, 异常代码:103
, 异常服务类型:get
随机数 % 4 == 0
执行HttpServerException("权限不足", 100, "post")
异常信息:
权限不足
, 异常代码:104
, 异常服务类型:post
最后, 调用
CacheMgr()
最后, 主函数内 死循环模拟服务器运行.
try
块内调用HttpServer()
, 而HttpServer()
内调用CacheMgr()
, CacheMgr()
内调用SqlMgr()
, 模拟服务运行的流程.
catch (const Exception& e)及代码块
实际作用是 捕获三种异常类, 并多态调用不同异常类对象的what()
接受打印相关异常信息
catch (...)及代码块
捕获其他异常, 输出未知异常
至此, 整个代码分析结束, 从HttpServer()
层层调用到 SqlMgr()
, 每个函数内都有概率抛出不同的异常, 抛出之后会在 main
内被捕获并处理.
查看执行结果:
从结果可以看到, 每次循环 每层调用都有一定的概率抛异常. 并且都会在main
函数内被捕捉到并处理.
图中, 光标可能在某行停顿1-2s, 说明此时并没有异常抛出
在添加一个SeedMsg()
函数, 模拟发送信息异常.
void SeedMsg(const string& str) {
if (rand() % 2 == 0) {
throw HttpServerException("SeedMsg::网络错误", 105, "put");
}
else if (rand() % 4 == 0) {
throw HttpServerException("SeedMsg::你已经不是对方好友", 106, "post");
}
else {
cout << "消息发送成功!->" << str << endl;
}
}
将 main()
函数try块中执行的函数 换为此函数, 查看执行:
并尝试, 将 网络错误异常的处理方式 改为 发生异常之后 直接重试再发送10次.
其实很简单, 只需要改动main
函数内容就可以(不过要先在 Exception
类中添加一个成员函数getid()
):
int main() { srand(time(0)); while (true) { // 此代码中 唯一一个不能跨平台的函数sleep(), 这里用的是 Linux环境 // Windows 平台 需要将其换为 Sleep(1000); // 并将 头文件 unistd.h 换为 Windows.h sleep(1); try { for(int i = 1; i <= 10; i++) { try { SeedMsg("你好啊?"); // 能走到这里 一定发送成功 // 直接break跳出 for循环 break; } catch (const Exception& e) { if (e.getid() == 105) { // 针对 103 异常处理 cout << "网络错误, 重发送, 第 " << i << " 次" << endl; continue; } else { // 不是此异常, 重新抛出 throw e; } } } } catch (const Exception& e) { // 多态 cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
执行结果:
这里的关键点就是, 异常的重新抛出, 还有 SeedMsg()
之后的break
.
由于需要特定的处理 _id
为105
的异常, 所以需要先就近catch
一下, 然后判断异常代码, 进行处理.
而, 由于只处理105异常, 其他异常就需要再次抛出, 让其他地方处理.
然后, SeedMsg()
之后的break
. SeedMsg()
正常返回, 就一定会顺着向下走, 也表示这发送成功, 就不需要继续for
循环, 所以直接break
. 如果抛异常, 则会跳过break
, 然后异常被下面的catch
子句捕获.
介绍到这里, C++关于异常的内容 就暂时介绍完了.
感谢阅读~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。