1.C语言处理错误方式
- 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
- 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误
- C 标准库中setjmp和longjmp组合。这个不是很常用,了解一下
传统的处理错误的缺陷:
a.拿到错误码 需要查找错误码表 才知道是什么错误
b.如果一个函数使用过返回值拿数据 发生错误时很难处理
c.如果调用的函数栈很深 一层层返回错误码 整个处理很难受
2.C++异常
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 **throw **关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常 catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
3.异常的基本使用
#include <iostream>
#include <vector>
using namespace std;
int main()
{
try
{
vector<int> v = { 1,2,3,4,5 };
for (int i = 0; i <= v.size(); ++i)
{
//cout << v[i] << " "; //越界就直接断言
cout << v.at(i) << " ";//try+catch
}
cout << endl;
}
catch (exception& e)
{
cout << e.what() << endl;//what就是发生了什么错误
}
return 0;
}
4.异常的特性
异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
namespace szh
{
//int div(int n, int m, int ret)
//{
// //返回一个值区分不清楚 通过参数来拿
// if (m == 0)
// {
// return -1;
// }
// //不好
// ret = n / m;
// return 0;
//}
int div(int n, int m)
{
//返回一个值区分不清楚 通过参数来拿
if (m == 0)
{
//throw "发生除0错误"; //没有对应 程序会终止
throw string("发生除0错误");//throw 可以抛出任意类型的对象
//直接跳转到catch匹配的地方
//先看f1有没有catch 再看main有没有catch
}
return n / m;
}
}
void f1()
{
try {
int n, m;
cin >> n >> m;
cout << szh::div(n, m) << endl;
}
catch (const string& err)//类型匹配就会优先
{
cout << __LINE__ << " " << err << endl;
}
}
int main()
{
try
{
f1();
}
//捕捉列表可有多个
catch (int err)
{
cout << err << endl;
}
catch (const string& err)
{
cout << __LINE__ << " " << err << endl;
}
catch (...) // 捕获没有匹配的任意类型的异常 避免异常没捕获时程序直接终止了
{
cout << "未知异常" << endl;
}
}
要求你可以自己抛自己定义的异常 但是必须要继承这个基类
这样的话 外层捕获就只需要捕获基类就可以
class Exception
{
public:
Exception(const char* errmsg,int errid)
:_errmsg(errmsg)
,_errid(errid)
{}
virtual string what() = 0;//变为抽象类 强迫子类必须重写
protected:
int _errid; //错误码
string _errmsg; //错误描述
//stack<string> _st; //调用栈帧
};
class SqlException : public Exception
{
public:
//子类构造函数调父类的
SqlException(const char* errmsg, int errid)
:Exception(errmsg,errid)
{}
virtual string what()
{
return "数据库错误:" + _errmsg;
}
};
class NetworkException : public Exception
{
public:
//子类构造函数调父类的
NetworkException(const char* errmsg, int errid)
:Exception(errmsg, errid)
{}
virtual string what()
{
return "网络错误:" + _errmsg;
}
};
void ServerStart()
{
//模拟一下出现问题抛异常报错
if (rand() % 3 == 0)
throw SqlException("数据库启动失败", 1);
if (rand() % 7 == 0)
throw NetworkException("网络连接失败", 3);
cout << "正常运行" << endl;
}
int main()
{
for (size_t i = 0; i < 10; ++i)
{
try
{
ServerStart();
}
catch (Exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
return 0;
}
5.异常安全
new/fopen func() delete/fclose
func()如果抛异常就会导致资源泄漏
所以有了异常规范
异常可能会导致异常安全问题
new/fopen/lock
func()//如果抛异常就会有异常安全问题 -> 解决:捕获重新抛出 or RAII解决
delete/fclose/unlock
函数规范一下 如果要抛异常 你说明清除 不抛异常也说明一下
但是现实中 很多人嫌麻烦 不遵守规范
这里表示这个函数会抛出A/B/C/D中的某种类型的异常 void fun() throw(A,B,C,D);
这里表示这个函数只会抛出bad_alloc的异常 void* operator new (std::size_t size) throw (std::bad_alloc);
这里表示这个函数不会抛出异常 void* operator new (std::size_t size, void* ptr) throw(); void* operator new (std::size_t size, void* ptr) noexcept;
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
6.异常的优缺点
优点:
- 清晰的包含错误信息
- 面对T operator[](int i)这样函数越界错误 异常可以很好的解决
- 多层调用时 里面发生错误 不再需要层层处理 最外层直接捕获即可
- 很多第三方库都是用异常 我们也使用异常可以更好的配合 比如:boost/gtest/gmock
缺点:
- 异常会导致执行流乱跳 会给我们调试分析程序bug带来一些困难
- C++没有GC 异常可能导致资源泄漏等异常安全问题 需要学会使用RAII来解决
- C++的库里面的异常体系定义不太好用 很多公司都会选择自己定义
- C++的异常语法可以抛任意类型的异常 如果项目中没有做很好规范管理 那么会非常混乱 所以一般需要定义出继承体系的异常规范
异常整体而言还是一个利大于弊的东西 所以实际日常练习或者小项目 不太实用
公司一般还是会选择异常来处理错误
【C++】20.异常 完