目录
1、异常的概念及使用
1.1 异常的概念
1.2 异常的抛出和捕获
1.3 栈展开
1.4 查找匹配的处理代码
1.5 异常的重新抛出
1.6 异常的安全问题
1.7 异常的规范
2、标准库的异常(了解)
1、异常的概念及使用
1.1 异常的概念
C语言,出错了,就报错误码,还要去查询错误信息,比较麻烦。错误处理与正常逻辑混杂,容易遗漏检查。
C++,出错了,就(throw)抛出一个异常对象,包含更全面的错误信息,并且(try)检测错误和(catch)处理错误,分开进行。
1.2 异常的抛出和捕获
当程序出现问题时,可以通过抛出(throw)一个异常对象来触发异常处理机制。该对象的类型及当前的调用链共同决定了哪个catch块将处理此异常。
异常处理流程
-
匹配规则:
被选中的处理代码是调用链中与该异常对象类型匹配(通过类型匹配规则),且距离抛出位置最近的catch块。 -
信息传递:
通过异常对象的成员变量或返回what()的字符串,传递错误信息。 -
控制流转移:
-
当throw执行时,其后的语句不再执行。
-
程序从throw点跳转至匹配的catch块,该catch可能在当前函数或调用链上游的某个函数中。
-
控制权转移意味着:
-
调用链中的函数可能提前退出(函数栈帧提前销毁)(栈展开,Stack Unwinding)。
-
栈上已构造的局部对象会按创建顺序的逆序销毁(RAII保证)。
-
-
-
异常对象生命周期:
-
因为异常对象是局部的(类似函数值返回的临时对象),会调用移动构造,(若没有移动构造,就调用拷贝构造)。
-
移动(或复制)后的对象在匹配的catch块结束时 销毁。
-
1.3 栈展开
抛出异常后,程序暂停当前函数的执行,
如果 throw 在 try 块内部,查找匹配的 catch 语句,如果有匹配的,则跳到 catch 的地方进行处理。
如果当前函数中没有 try/catch 子句,或者有 catch 子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的 catch 过程被称为栈展开。
如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序。
如果找到匹配的 catch 子句,处理后,当前 catch 子句后面的代码会继续执行。
1.4 查找匹配的处理代码
一般情况下,抛出对象和catch是类型匹配的,如果有多个类型匹配的,就选择最近的catch。
允许一些例外的类型转换:
非常量 -> 常量(即权限缩小),
数组->数组元素的指针
函数->函数指针,
派生类->基类,这个非常实用。
如果到main函数,异常仍旧没有被匹配就会终止程序,我们是不期望程序终止,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <ctime>
#include <cstdlib>// 一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块
// 每个模块的异常都是 Exception 的派生类,每个模块可以添加自己的数据
// 最后捕获时,我们捕获基类就可以class Exception {
public:Exception(const std::string& errmsg, int id): _errmsg(errmsg), _id(id) {}virtual std::string what() const {return _errmsg;}int getid() const {return _id;}protected:std::string _errmsg;int _id;
};class SqlException : public Exception {
public:SqlException(const std::string& errmsg, int id, const std::string& sql): Exception(errmsg, id), _sql(sql) {}std::string what() const override {std::string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}private:const std::string _sql;
};class CacheException : public Exception {
public:CacheException(const std::string& errmsg, int id): Exception(errmsg, id) {}std::string what() const override {std::string str = "CacheException:";str += _errmsg;return str;}
};class HttpException : public Exception {
public:HttpException(const std::string& errmsg, int id, const std::string& type): Exception(errmsg, id), _type(type) {}std::string what() const override {std::string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const std::string _type;
};void SQLMgr() {if (rand() % 7 == 0) {throw SqlException("权限不足", 100, "select * from name = '张三'");} else {std::cout << "SQLMgr 调用成功" << std::endl;}
}void CacheMgr() {if (rand() % 5 == 0) {throw CacheException("权限不足", 100);} else if (rand() % 6 == 0) {throw CacheException("数据不存在", 101);} else {std::cout << "CacheMgr 调用成功" << std::endl;}SQLMgr();
}void HttpServer() {if (rand() % 3 == 0) {throw HttpException("请求资源不存在", 100, "get");} else if (rand() % 4 == 0) {throw HttpException("权限不足", 101, "post");} else {std::cout << "HttpServer 调用成功" << std::endl;}CacheMgr();
}int main() {srand(time(0));while (1) {std::this_thread::sleep_for(std::chrono::seconds(1));try {HttpServer();}catch (const Exception& e) { // 这里捕获基类,基类对象和派生类对象都可以被捕获std::cout << e.what() << std::endl;}catch (...) {std::cout << "Unkown Exception" << std::endl;}}return 0;
}
1.5 异常的重新抛出
有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新(throw)抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>using namespace std;// 下面程序模拟展示了聊天时发送消息,发送失败捕获异常
// 可能在电梯地下室等场景手机信号不好,需要多次尝试
// 如果多次尝试都发送不出去,则需要捕获异常再重新抛出
// 如果不是网络差导致的错误,捕获后也要重新抛出class Exception {
public:Exception(const string& errmsg, int id): _errmsg(errmsg), _id(id) {}virtual string what() const {return _errmsg;}int getid() const {return _id;}protected:string _errmsg;int _id;
};class HttpException : public Exception {
public:HttpException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type) {}string what() const override {string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void _SendMsg(const string& s) {if (rand() % 2 == 0) {throw HttpException("网络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0) {throw HttpException("你已经不是对方的好友,发送失败", 103, "put");}else {cout << "发送成功" << endl;}
}void SendMsg(const string& s) {// 发送消息失败,则再重试3次for (size_t i = 0; i < 4; i++) {try {_SendMsg(s);break;}catch (const Exception& e) {// 捕获异常,如果是102号错误(网络不稳定),则重新发送// 如果不是102号错误,则将异常重新抛出if (e.getid() == 102) {// 重试三次以后还是失败,则说明网络太差,重新抛出异常if (i == 3) {throw;}cout << "开始第" << i + 1 << "次重试" << endl;}else {throw;}}}
}int main() {srand(time(0));string str;while (cin >> str) {try {SendMsg(str);}catch (const Exception& e) {cout << e.what() << endl << endl;}catch (...) {cout << "Unknown Exception" << endl;}}return 0;
}
如果是102号错误,这个异常不处理,出了catch子句,自动析构,再尝试发送。
1.6 异常的安全问题
异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。解决方案:
1. 可以中间捕获异常,释放资源后再重新抛出,麻烦。
2. 后面的智能指针章节讲的RAII方式解决这种问题更方便。
其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《Effective C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。
1.7 异常的规范
对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,有助于简化代码。
C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1, 类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。
C++98的方式这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加noexcept表示不会抛出异常。
编译器不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译还是会顺利通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数 抛出了异常,程序会调用terminate终止程序。
noexcept(expression)还可以作为一个运算符去检测一个表达式 是否会抛出异常,可能会则返回false,不会就返回true。
一般在外层处理异常。
#include <iostream>
using namespace std;double Divide(int a, int b) // noexcept
{// 当 b == 0 时抛出异常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}int main()
{try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (const char* errmsg){cout << errmsg << endl;}catch (...){cout << "Unknown Exception" << endl;}int i = 0;cout << noexcept(Divide(1, 2)) << endl; // 0cout << noexcept(Divide(1, 0)) << endl; // 0cout << noexcept(++i) << endl; // 1return 0;
}
2、标准库的异常(了解)
exception - C++ Reference
不好用,公司一般自己实现异常库。