前言
回顾C语言对于错误处理
1、assert —— 断言,直接终止程序,并且assert只能在debug环境下执行,在release版本下是无法生效的,过于暴力
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main() {
FILE* file = fopen("nonexistent_file.txt", "r");
assert(file);//fopen不会失败,断言file不为空,如果失败,直接终止程序,并告诉我们程序的终止位置
// 文件操作...
fclose(file);
file = nullptr;
return 0;
}
2、errno、perror —— 返回错误码,打印错误码对应的错误信息,需要程序员自己去调用perror查找对应的错误信息,不灵活
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main() {
FILE* file = fopen("nonexistent_file.txt", "r");
if (file == NULL) {
printf("Failed to open file. Error code: %d\n", errno);//errno是int类型的,就是1 2 3 4这些数字,我们不知道这些数字代表的错误信息,需要用perror打印当前errno中存储的错误码对应的错误信息
perror("Error");//perror接收一个字符串,然后会在该字符串后面加上“: ”然后带上errno对应的错误码信息
return -1;
}
// 文件操作...
fclose(file);
return 0;
}
C++中进行了革新,引入了异常这个概念
概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接调用者(多层调用)处理这个错误
这里会涉及到三个关键字:throw、catch、try
try - 用于检测,try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码,它后面通常跟着一个或多个catch块
throw - 在某种场景下,遇到某种错误时,抛出异常
catach:在你想要处理问题的地方,通过异常处理程序捕获异常,可以有多个catch进行捕获
单看这里的关键字概念,会觉得比较吃力,我们下面通过例子认识这三个关键字充当的作用
//除0错误
double Division(int a, int b)//a == 3 b == 0
{
if (b == 0)//b == 0时用throw抛出异常,throw抛出的异常是const char*类型的异常
{
throw "Division by zero condition";
}
else
{
return (double)a / (double)b;
}
}
void Exception_test1()
{
//try会检测大括号中是否有抛异常的行为
try
{
cout << Division(3, 0) << endl;
cout << Division(3, 1) << endl;
}
//如果有抛异常的行为,会被这里的catch捕获
//注意:这里的catch捕获的是const char*类型的异常,throw的也是const char*类型的异常,只有throw的类型和这里catch的类型匹配,才能被捕获,如果后面多个catch都不能被捕获,程序会报错
catch (const char* errmsg)//error_message
{
cout << errmsg << endl;
}
//...
}
整个程序的执行过程:进入try,调用函数Division(3, 0),发生异常,throw抛出异常,直接跳转到catch,被catch捕获执行cout << errmsg << endl; 然后执行后面的 //… ,所以需要注意到这里的cout << Division(3, 1) << endl;根本没有执行到,出现这种情况的原因是,throw抛出异常后是跳转寻找catch,效率较高,但是也就会带来程序上的乱跳现象
异常相关规则
在函数调用链中异常栈展开匹配原则
1、首先检查throw本身是否在try块内部,如果是,再查看匹配的catch语句,如果该栈中有匹配的,则调到catch的地方进行处理
2、没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch
3、如果到达main函数的栈,依旧没有匹配的,则终止程序。
上述这个沿着调用链查找匹配的catch子句的过程称为栈展开,所以在实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没有被捕获,程序就会直接终止
4、找到匹配的catch子句并处理之后,会继续沿着该catch子句后面的代码继续执行
#include <iostream>
void thirdLevel() {
try {
//throw...
// 一些代码,可能会抛出异常
} catch (const char* str) {
std::cout << "Caught exception in third level: " << str << std::endl;
// 重新抛出异常,传递给第二层
throw;
}
}
void secondLevel() {
try {
thirdLevel();
} catch (const char* str) {
std::cout << "Caught exception in second level: " << str << std::endl;
// 可以选择处理异常或重新抛出异常,传递给第一层
throw;
}
}
void firstLevel() {
try {
secondLevel();
} catch (const char* str) {
std::cout << "Caught exception in first level: " << str << std::endl;
// 可以选择处理异常或终止程序
}
}
int main() {
try {
firstLevel();
} catch (const char* str) {
std::cout << "Caught exception in main: " << str << std::endl;
}
return 0;
}
异常的抛出和匹配原则
1、异常是通过抛出对象而引发的,该对象的类型(整形、字符串类型、自定义类型…)决定了应该激活哪个catch的处理代码
//除0错误
double Division(int a, int b)//a == 3 b == 0
{
if (b == 0)
{
throw "Division by zero condition";
}
else if(a == 0)
{
string str = "answer always zero";
throw str;
}
else
{
return (double)a / (double)b;
}
}
void Exception_test2()
{
try
{
cout << Division(3, 0) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;//Division(3, 0)激活这里
}
catch (string errmsg)
{
cout << errmsg << endl;
}
try
{
cout << Division(0, 2) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (string errmsg)
{
cout << errmsg << endl;//Division(0, 2)激活这里
}
}
2、被选中的处理代码是调用链中与该对象类型匹配且距离抛出异常位置最近的那一个
3、抛出异常对象时,会生成一个异常对象的拷贝,这是为了防止抛出的异常对象是临时对象(局部变量),捕获的位置在栈外,出了作用域该临时对象被销毁,这个拷贝的临时对象在被catch捕获以后会自动销毁(类似于函数传值返回的处理)
4、catch(…)可以捕获任意类型的异常,一般用于兜底,防止调用的函数中抛一些不规范的异常,报错信息一般是未知错误
double Division(int a, int b)//a == 3 b == 0
{
if (b == 0)
{
throw "Division by zero condition";
}
else if (a == 0)
{
throw 1;
}
else
{
return (double)a / (double)b;
}
}
void Exception_test2()
{
try
{
cout << Division(0, 2) << endl;
}
catch (const char* errmsg)
{
cout << "Divisione err: " << errmsg << endl;
}
catch (...)//一般放在多个catch的最后,用于上面的catch都没有捕获抛出的异常的情况
{
cout << "Exception errer" << endl;
}
}
5、捕获的匹配原则中有个重要的、常用的例外:我们并不要求类型完全匹配,可以抛出派生类对象,使用基类捕获,这个特性在工程中用途最广
异常体系
Exception体系
项目中捕获异常 try{}catch(){} 一般放在main函数中集中处理,通常会配合记录日志,用于后期快速查错
错误信息为什么不让乱抛?需要区分错误
为了统一管理,有一个自定义类型Exception,一般包含两个基本信息:错误码和错误信息
class Exception
{
public:
Exception(const string& errmsg, int id)
:_id(id)
,_errmsg(errmsg)
{}
virtual string what() const
{
return _errmsg;
}
virtual int getid()const
{
return _id;
}
private:
int _id;// 错误编码(更好的分辨错误)
string _errmsg;// 错误描述
};
我们思考,对于一个项目,异常的错误类型有很多:网络、缓存、数据库、权限管理…等多模块,每一个模块都是项目中的一个组去做,比如有一个组是做网络的,这个组会设计网络相关的函数、类、以及网络异常的ExceptionNetwork类,我们throw了ExceptionNetwork类型的错误,那我们是否需要在外层加一个catch(const ExceptionNetwork& errmsg){}呢?那又多了一个模块,多了一个Exceptiontype呢?不需要加,我们这里就需要充分利用 异常的抛出和匹配原则中的第五点,抛派生类,用基类接收
整体框架如上,也就是说深层使用下面派生类中的自定义类型函数时,如果throw了错误信息,main中都只需要用catch(const Exception& errmsg){}捕获各种类型的异常,以及一个catch(…){}防止深层乱抛异常
所以实际使用中很多公司都会自定义自己的异常体系进行规范异常的管理,因为一个项目中如果大家随意抛异常,那么外层的调用者就非常麻烦十个二十个catch等着他,这样让大家抛出的异常都是派生类对象,让一个基类类型的对象捕获就可以了
class Exception
{
public:
Exception(const string& errmsg, int id)
:_id(id)
, _errmsg(errmsg)
{}
virtual string what() const
{
return _errmsg;
}
virtual int getid()const
{
return _id;
}
protected:
int _id;
string _errmsg;
};
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;//组合成了一个str
}
private:
const string _type;
};
void HttpServer()
{
//...
srand((unsigned)time(0));
if(rand() % 3 == 0)//模拟在某种情况下,出现了某种问题
{
throw HttpServerException("资源不存在", 100, "get");
}
else if(rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
}
void Exception_test5()
{
while (1)
{
Sleep(500);
try
{
HttpServer();//网络服务
}
catch (const Exception& e)
{
//多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unknow Exception" << endl;
}
}
}
所以可以看出:异常在工程中的使用,其实从知识层面来讲:异常就是多态的运用
对于网络错误,我们通常不能直接打死,一般会进行多次try尝试(网路不好发送信息时看到的那个旋转的小圈圈)
void Exception_test4()
{
int n = 100;
while (n--)//重试100次
{
try
{
SeedMsg(msg);
break;//发送成功
}
catch (const Exception& e)
{
if (e.getid() == 3)//满足XXX条件时进行重试,这里是满足e的错误id是3时重试
{
continue;//重试
}
else
{
//记录日志、界面错误展示
break;
}
}
}
}
下面我给出一个较为完整的异常体系代码
//基类
class Exception
{
public:
Exception(const string& errmsg, int id)
:_id(id)
, _errmsg(errmsg)
{}
virtual string what() const
{
return _errmsg;
}
virtual int getid()const
{
return _id;
}
protected:
int _id;
string _errmsg;
};
//Http Sql Cache三个子类
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;//组合成了一个str
}
private:
const string _type;
};
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;
}
};
//三个调用函数
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "error from name = '张三'");
}
}
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
//然后查看数据库资源函数
SQLMgr();
}
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
//网络资源请求成功,去执行缓冲资源函数
CacheMgr();
}
void Exception_test6()
{
while (1)
{
Sleep(500);
try {
HttpServer();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
}
如果有一个类没有添加父类Exception,如果使用相关类时发生了抛出错误,外面的catch是无法捕获的,此时走到catch(…),日志信息中就会有未知错误,此时,可以用排除法,查看有哪些类,没有被日志记录,来精准的找到错误原因、和出错的人
C++标准库的异常体系
C++标准库中有exception类
这里的throw是异常规范,后面会提到,这里不用管
exception类有多个子类,来处理各种类型的异常
下面这些是logic_error、runtime_error…的子类(子类的子类)
虽然C++有标准的异常体系,但是用的很少(不能满足工程中的需求),而且我们也不需要注意太多,只需要在可能有问题的地方使用try,然后后面使用exception捕获异常,用多态的what捕获各种类型的异常即可,没有必要去了解具体每一个子类(注意引头文件#include<exception>
)
#include<exception>
void exception_test7()
{
try
{
while (1)
{
int* ret = new int[1000 * 1000];
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
这里的校正处理一般用于 修正变量或状态:根据异常的类型和内容,可能需要修正一些变量或状态以恢复正常状态。例如,如果出现除以零的异常,可以尝试修改除数为一个非零值,以避免异常的再次发生。或者,如果出现动态开辟的空间,可以在该catch中先将动态开辟的空间释放,然后在重新抛出给外层处理。
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;//这里的catch只释放了动态开辟的空间
delete[] array;
throw;//重新抛出,让外层的catch去打印errmsg信息
}
// ...
cout << "delete []" << array << endl;
delete[] array;
}
void Exception_test8()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
}
异常规范
异常规范是异常接口的声明,是用来说明函数内会抛哪些类型的异常,方便函数使用者了解函数(需要注意的是异常规范是非强制的,不遵守异常规范的要求,也不会报错)
1、在函数后面加throw(类型1, 类型2, 类型3, …),说明该函数中可能会抛出这些类型的异常
void Fun() throw(HttpServerException, CacheException, SqlException)
{
srand((unsigned)time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw CacheException("内存不足", 97);
}
else if (rand() % 5 == 0)
{
throw SqlException("权限不足", 100, "error from name = 'grass'");
}
}
2、函数后面加上throw(),表示函数不抛异常
由于这种非强制性行为,会带来很多困扰,在C++11中出现了noexcept,noexcept是强制性行为,让该函数不能抛出异常
3、函数后面无没有异常规范throw(XXX),表示此函数可以抛任意类型的异常
异常安全
对于使用异常有一些需要注意的点
1、构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
2、析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
3、C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII我们智能指针这节进行讲解。(这一点我们之后的博客讲解)
异常的优缺点
优点
1、异常对象定义好后,相比错误码的方式,可以更清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,可以帮助我们定位程序的bug
2、返回错误码的传统方式有一个很大的问题,在函数调用链中,深层的函数返回了错误,那么我们需要一层层返回错误,最外层才能拿到错误
3、很多第三方库都包含异常,比如boost、gtest、gmock等常用的库,我们使用它们也需要使用异常
4、部分函数使用异常更方便处理,比如构造函数没有返回值,不方便使用错误码方式处理,比如T& operator[](size_t pos)这样的函数,如果pos越界了,只能使用异常或者终止程序处理,没办法通过返回值表示错误
缺点
1、异常会导致程序的执行流乱跳,十分混乱,这会导致我们跟踪调试时以及分析程序时比较困难(类似于goto的问题)
2、C++没有垃圾回收机制,资源需要自己管理,有了异常之后非常容易导致内存泄漏、死锁等(以后讲)异常安全问题。这个需要使用RALL来处理资源的管理问题。
3、C++标准库的异常体系定义的不够好,导致各大公司都是自己写的一套异常体系,十分混乱
4、异常尽量规范使用,随意抛异常,外层捕获的程序员会很难受,调试时寻找错误也很难受