目录
一.C语言错误处理方式
1.assert(断言)
2.返回/设置错误码
二.C++异常的概念与使用
1.异常的概念
2.异常的使用
三.自定义异常体系MyException
四.异常的重新抛出
五.异常安全问题
六.异常规范
七.异常的优缺点对比
一.C语言错误处理方式
一个C语言程序, 在运行期间出现问题(除零错误, 非法访问空指针...), 传统的处理这些问题的机制:
1.assert(断言)
使用需要包头文件assert.h, 一但进程运行期间出现错误直接终止程序, 并且只能在debug模式下起到作用, release版本将会直接优化掉assert
2.返回/设置错误码
编译器自动设置一个全局变量errno来存放错误码, 并且需要手动查找几号错误对应的错误信息, 例如调用strerror()等函数, strerror(errno), 或者使用perror()函数
二.C++异常的概念与使用
1.异常的概念
C++属于面向对象的语言, 并且设计了一套完整的异常体系, 用于处理程序出现错误出现异常的情况
相较于C语言的assert或错误码而言, 使用异常体系判断错误信息更加便捷, 规范, 完整, 是面向对象语言判断错误的首选方式
异常被抛出并且捕获之后, 进程不会终止, 而是会在捕获处理结束之后继续执行
异常三板斧:
"捕" -- try
"抛" -- throw
"获" -- catch
写在try代码块中的函数被捕捉到, 意为要检查是否有异常抛出
一但try代码块中有throw抛出异常, 直接跳转到catch
将抛出内容以传参的形式传到catch中, 再由catch获取到抛出的异常, 在catch代码块内进行打印说明
以上流程结束后, 继续执行catch以下的逻辑
注意点: 一但有异常抛出必须被获取, 否则程序强行终止
void func()
{
throw ...;//抛
}
try//捕
{
func()
}
catch(参数)//获
{}
catch(参数)//获
{}
catch(...)//获
{}
2.异常的使用
重点说在前: 在使用异常体系时, 一但有异常抛出必须被获取, 否则程序强行终止
异常体系使用准则
1.异常通过抛出对象引发, 并且基于抛出对象类型来确定跳转到哪一个catch, 跳转到catch的过程就类似于传参的过程
2.如果有多层异常捕捉, 采用栈展开的形式, 自动匹配距离最近的上一层栈帧中的catch, 若上一层没有匹配的, 则继续再向上展开, 直到main函数, 若main中也没匹配, 直接进程终止
3.抛出异常对象之后, 会自动生成一个该对象的拷贝, 为了防止异常对象是一个临时对象, 拷贝的临时对象在被catch之后销毁, 类似函数传值返回
4.catch(...)可以捕捉任意类型的异常, 这是异常体系中获取异常的最后一道防线, 因为在实际使用时throw的对象是多样的, 如果异常因类型不匹配而没有catch进程则被强制终止, 后面会统一throw对象的标准, 但为了以防万一, 每一次try...catch最终都一定要加上最后防线
5.throw和catch的类型并不都是完全匹配, 唯一特例则是可以throw派生类对象,使用基类类型catch, 再结合多态之后, 实际价值极高
异常体系执行流程
想要throw抛出异常, 必须先对函数进行try捕捉, 执行到函数时, 创建并进入函数栈帧, 执行函数内容
在执行期间, 1. 如果throw抛出异常, throw之后的内容不被执行, 直接跳转至对应严格匹配的catch处
catch捕捉块内逻辑开始执行, 执行结束之后, 整套异常捕获流程结束, 开始catch之后的代码逻辑
2. 如果没有出现异常, 则函数返回后, 继续执行try中, 之后的逻辑
注: 以上如果throw异常, 有两处代码逻辑被跳过, 代码块中: 1. try之后的逻辑被跳过 2. throw之后的逻辑被跳过
以上流程中, throw就必须被catch, 否则程序终止, 而catch(...)则是守护这一原则的最后底线, 它会捕捉所有类型的异常对象
#include<iostream>
#include<string>
using namespace std;
void _func(int a, int b)
{
if (b == 0)
{
throw "_func除零错误";
}
cout << a / b << endl;
}
void func()
{
try
{
_func(10, 0);
}
catch (const string& str)
{
cout << "func捕获 const string& str : " << str << endl;
}
catch (const char* str)//避免throw传值返回对象销毁, 加const延长临时对象生命周期, 下面同理, 如果不加const, 异常就会被下面最合适的catch捕捉
{
cout << "func捕获 const char* str: " << str << endl;
}
catch (...)
{
cout << "func捕获 未知错误" << endl;
}
}
int main()
{
try
{
func();
}
catch (int x)
{
cout << "main捕获 int x: " << x << endl;
}
catch (const char* str)
{
cout << "main捕获 const char* str: " << str << endl;
}
catch (const string& str)
{
cout << "main捕获 const string& str : " << str << endl;
}
catch (...)
{
cout << "main捕获 未知错误" << endl;
}
//...
//接下来的逻辑
cout << "以上异常测试结束\n";
return 0;
}
以上代码中还有一个值得一提的是: 异常在throw后匹配对应catch时, 对于抛出的对象不会隐式类型转换, 例如string str = "除零错误"; 在异常体系中是不会这么做的
异常体系只是单纯且严格的找catch中类型匹配的那一个, 例如以上代码"除零错误"这是一个字符串, 而它只能匹配const char* str, 字符指针, 并不能匹配const string& str
这并不是选择一个最优的catch匹配而得到的结果, 因为如果没有写const char* str类型的catch, 则直接判定没有对应的catch, 也就没有获取到而直接导致进程终止, 这块博主是自己做了测试的
三.自定义异常体系MyException
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<ctime>
#include<cstdlib>
#include<windows.h>
using namespace std;
//自定义异常体系
//整体思想: 派生类抛出, 基类捕捉
//原理: 使用基类捕捉从而达成多态调用, 详细展示出问题所在模块的详细信息
class MyException
{
public:
MyException(const string& errmsg, int id)
:_errmsg(errmsg), _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;
int _id;
};
class MyException1 :public MyException
{
public:
MyException1(const string& errmsg, int id, const string& errmsg1)
:MyException(errmsg, id),
_errmsg1(errmsg1)
{}
virtual string what() const
{
string resStr = "MyException1: ";
resStr += _errmsg;
resStr += "-->";
resStr += _errmsg1;
resStr += "-->";
resStr += to_string(_id);
return resStr;
}
protected:
string _errmsg1;
};
class MyException2 :public MyException
{
public:
MyException2(const string& errmsg, int id, const string& errmsg1)
:MyException(errmsg, id),
_errmsg1(errmsg1)
{}
virtual string what() const
{
string resStr = "MyException2: ";
resStr += _errmsg;
resStr += "-->";
resStr += _errmsg1;
resStr += "-->";
resStr += to_string(_id);
return resStr;
}
protected:
string _errmsg1;
};
class MyException3 :public MyException
{
public:
MyException3(const string& errmsg, int id, const string& errmsg1)
:MyException(errmsg, id),
_errmsg1(errmsg1)
{}
virtual string what() const
{
string resStr = "MyException3: ";
resStr += _errmsg;
resStr += "-->";
resStr += _errmsg1;
resStr += "-->";
resStr += to_string(_id);
return resStr;
}
protected:
string _errmsg1;
};
void exception_test_3()
{
int a = rand() % 10 + 1;
if (a == 3 || a == 8)
{
throw MyException3("error", a, "MyException3");
}
cout << "执行成功\n";
}
void exception_test_2()
{
int a = rand() % 15 + 1;
if (a == 8 || a == 12)
{
throw MyException2("error", a, "MyException2");
}
else
{
exception_test_3();
}
}
void exception_test_1()
{
int a = rand() % 10 + 1;
if (a == 2 || a == 3)
{
throw MyException1("error", a, "MyException1");
}
else
{
exception_test_2();
}
}
int main()
{
srand((unsigned int)time(nullptr));
while (1)
{
try
{
exception_test_1();
}
catch (const MyException& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知错误\n";
}
Sleep(1000);
}
return 0;
}
四.异常的重新抛出
//异常重新抛出
//如果整套逻辑中出现3号错误, 则要重试三次再抛出异常
void _Server()
{
int x = rand() % 3 + 1;
if (x == 3 || x == 1)
{
throw x;
}
cout << "本次无异常\n";
}
void Server()
{
//处理一些服务, 服务底层调用_Server
//为了能够重新运行三次, 在这里截获异常, 筛选后不符合条件就重新抛出
//此外3号异常重试三次以后也要抛出
int n = 3;
while (n--)
{
try
{
_Server();
break;
}
catch (int errnum)
{
if (errnum == 3 && n > 0)//是3号异常且还未重试3次,继续重试
{
cout << "本次为3号异常, 正在重试...\n";
continue;
}
else
{
//异常重新抛出!
if (errnum == 3)
{
int a = 10;
}
throw errnum;
}
}
catch (...)
{
cout << "未知异常\n";
}
}
}
int main()
{
srand((unsigned int)time(nullptr));
while (1)
{
try
{
Server();
}
catch (int errnum)
{
cout << errnum << "号异常已捕获" << endl;
}
catch (...)
{
cout << "未知异常\n";
}
Sleep(1000);
}
return 0;
}
五.异常安全问题
1.构造函数完成对象构造与初始化时, 不要在构造函数中抛出异常, 会导致对象不完整或没有完全初始化
2.析构函数在释放资源时, 不要在其中抛出异常, 会导致对象资源释放不完全, 导致内存泄漏问题
3.在异常体系中, 如果出现new/delete, 若在new之后出现异常, 则由于异常捕获的跳转, 则无法释放资源导致内存泄漏问题
4.在lock与unlock之间若抛异常则易发生死锁
(3)(4)点问题在C++中使用RAII(资源托管)思想, 智能指针可以完美解决
六.异常规范
//异常规范
//表示func1函数会抛出int,float,bool这三种类型范围之内的异常
void func1() throw(int, float, bool);
//表示func2函数不会抛出异常(C++98)
void func2() throw();
//表示func4只会抛出int类型异常
void func4() throw(int);
//以上在C++11不再推荐使用
//表示func3函数不会抛出异常(C++11)
void func3() noexcept;
注: 这些规范编译器不会强制要求, 因为要兼容C语言, C语言没有异常体系
七.异常的优缺点对比
优点:
1.可以清晰准确展示错误信息, 更好定位到bug的位置
2.与传统的返回错误码对比, 不用层层返回错误信息, 而是直接跳转到捕获位置一目了然
3.使用异常更好处理错误, 如传统解决方式, assert只会在debug模式下生效, 并且一但断言成功程序直接终止, 返回码也不是在所有函数下都能返回的, 有可能返回信息被占用
缺点:
1.导致执行流不稳定, 若发生异常会直接跳转, 不方便调试, 例如: 打断点调试有可能直接在断点之前就抛出异常从而跳转, 无法走到指定断点位置
2.额外性能开销, 但在当代计算机的计算能力来看可以忽略不计
3.容易导致内存泄漏, 异常安全问题, 需要借助C++智能指针RAII思想来解决
4.标准库的异常定义较为混乱, 通常情况下使用自己实现的异常体系
5.由于兼容C语言, 导致异常规范犹如道德一般, 是否遵守并无法强制规定, 但作为一名合格程序员, 必须遵守异常规范! 若不遵守规范会诱发更多麻烦问题
但在C++11只推荐使用, 如果没有异常则使用noexcept标注即可.
总体而言: 异常的使用利大于弊