目录
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; // 0
cout << noexcept(Divide(1, 0)) << endl; // 0
cout << noexcept(++i) << endl; // 1
return 0;
}
2、标准库的异常(了解)
不好用,公司一般自己实现异常库。