目录
1.异常处理机制
1.1.抛异常和捕获异常
1.1.1.异常机制的基本场景
1.1.2.函数调用中异常栈展开的匹配规则:
1.2.异常机制的实际应用场景
2.异常相关知识
2.1.异常安全和异常重新抛出
2.2.noexcept关键字
2.3.异常的优缺点
1.异常处理机制
我们在C语言的学习中,了解了C语言的错误处理机制是通过终止程序和返回错误码这两种形式。
而对于C++,是通过使用“异常”这一种错误处理机制,它允许程序在运行时遇到错误时,能够以一种结构化和可预测的方式进行处理。异常处理机制包括:抛出异常(throw)、捕获异常(catch)
1.1.抛异常和捕获异常
抛出异常
当程序遇到无法处理的错误时,可以使用
throw
关键字抛出一个异常。抛出异常时,可以传递一个值,这个值可以是任何类型的对象,包括基本数据类型和自定义类型。捕获异常
catch
块用于捕获异常。当异常被抛出时,控制流会立即离开当前函数(或代码块),并寻找能够处理该异常的catch
块。一旦找到匹配的catch
块,异常的值就会被传递给该块,并且该块中的代码会被执行。
1.1.1.异常机制的基本场景
抛出异常需要和捕获异常成对出现,接下来我们用数组越界的场景来学习异常机制!
#include<iostream>
#include<windows.h>
using namespace std;
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >=5)
{
throw "数组越界";
}
else
{
return arr[i];
}
}
void Print()
{
int i = 0;
cout<<"需要打印下标为几的数组内容<<endl;
cin >> i;
cout << Exception(i) << endl;
}
int main()
{
while(1)
{
sleep(2000);
// try catch模块捕捉异常
// 异常一定要被捕获,不然会退出
try
{
Print();
}
catch (const char* s)
{
cout << s << endl;
}
// 进入该区域表示 有人没有按照规范抛异常
// 可以捕获任意类型的内容
catch (...)
{
cout << "未知异常" << endl;
}
}
}
这个就是异常最基本的一个场景,这里我们需要注意:
- 抛出异常后需要有接收异常的模块,不然会造成程序崩溃
- 抛出异常的类型需要和接收异常的类型一致,因为异常是通过抛出对象引发的,所以我们需要进行类型的匹配,传回匹配的异常信息
- 我们需要用catch(...)来作为没有按照规范的异常的保险,防止程序崩溃
当我们修改一下,抛异常的代码,改为抛出string对象。(注意catch处需要重载一个string类型)
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >=5)
{
string s = "数组越界";
throw s;
}
else
{
return arr[i];
}
}
这时我们抛出的对象的生命周期当前的Exception。catch接收时需要生成一个拷贝对象,这个对象在接收成功后销毁。我们可以使用右值引用语法来实现移动构造!(类似函数传值返回)
1.1.2.函数调用中异常栈展开的匹配规则:
- 首先检查throw是否在try内部,如果在的话就表示需要有匹配的catch,并且转到匹配的catch进行异常处理
- 如果当前没有匹配的catch就退出当前函数栈,往调用函数的栈中查找匹配的catch
- 如果到达main函数栈时,仍没有匹配的catch,程序将被中止,所以需要在main函数区添加catch(...)作为程序终止的保险,尽量只在main中添加
- 进入匹配的catch语句后,会执行catch语句的内容,并跳出try catch
#include<iostream>
#include<string.h>
using namespace std;
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >=5)
{
string s = "数组越界";
throw s;
}
else
{
return arr[i];
}
}
void Print()
{
try
{
int i = 0;
cin >> i;
cout << Exception(i) << endl;
}
catch (const string s)
{
cout << s << endl;
cout << "aaaaaaa" << endl;
}
}
int main()
{
// 没有异常进入try
try
{
Print();
}
// 异常一定要被捕获,不然会退出
catch (const string s)
{
cout << s << endl;
cout << "aaaaaaa" << endl;
}
// 进入该区域表示 有人没有按照规范抛异常
// 可以捕获任意类型的内容
catch (...)
{
cout << "未知异常" << endl;
}
}
1.2.异常机制的实际应用场景
实际开发企业级应用时,我们需要多人协作,那么避免不了出现异常捕获类型、以及catch内容不统一的问题,而这些问题可能会导致程序出现乱七八糟的结果。我们上面提到过:我们可以通过设置抛出异常的类型为某个对象,接收时通过对象来接受。这样我们可以结合类与对象的知识,通过设置一个Exception的异常基类,当不同的人需要抛异常时,就继承这个基类,实现一个派生类,抛出派生类的异常对象,和catch基类对象来实现。
#pragma once
#include<iostream>
#include<windows.h>
using namespace std;
// 异常机制的实际应用
// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg; // 错误描述
int _id; // 错误编号
};
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;
}
};
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;
}
private:
const string _type;
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
//throw "xxxxxx";
}
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();
}
int main()
{
while (1)
{
Sleep(2000);
try
{
HttpServer();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 但是实际情况,捕获到异常不一定就直接打印异常信息
// 可能我们会进行重新调用原函数
// 比如网络卡了,我们一般进行重新加载,而不是直接表示网络卡了
// 进行若干次重试
//int retry = 5;
//while (e._id == 10 && retry != 0)
//{
// HttpServer();
// retry--;
//}
// 多态,当我们接收不同派生类对象时,调用各自的what函数
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
}
代码解析:
- 首先基类定义一个虚函数what,负责返回错误信息
- 其余派生类都继承这个what函数,并实现各自代码逻辑的重写
- 通过几个模拟的场景,来抛出派生类对象构造的异常,并传入给各自的派生类,最终在不断的while循环模拟中,打印1.中的错误描述信息
2.异常相关知识
2.1.异常安全和异常重新抛出
我们先来看一个异常执行的场景:这里我们修改了Print函数内部的逻辑
#include<iostream>
#include<string.h>
using namespace std;
int Exception(int i)
{
int arr[5] = { 0, 1, 2, 3, 4 };
if (i >=5)
{
throw "数组越界";
}
else
{
return arr[i];
}
}
void Print()
{
int* array = new int[10];
int i = 0;
cin >> i;
cout << Exception(i) << endl;
delete[] array;
cout<<"释放array资源"<<endl;
}
int main()
{
try
{
Print();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
}
在这段代码中,我们new了一个int* array,并且在Exception执行后的代码处才进行修改。这里会出现一个问题:当我们正常执行时,array能够被释放,当我们抛异常时,这个模块的代码会跳转到main中,而不会进行释放array的操作。
直接抛异常这个操作在某些场景会出现内存泄漏问题!!!
那么我们如何解决这种异常安全问题呢? 这时我们进行异常的重新抛出!
void Print()
{
int* array = new int[10];
try
{
int i = 0;
cin >> i;
cout << Exception(i) << endl;
}
//catch (const char* errmsg)
//{
// delete[] array;
// cout << "释放array资源" << endl;
// // 再次抛出异常
// throw errmsg;
//}
// 实际场景会出现多种异常,所以这种写法更好,
// 即使这里不知道类型,但传入到main就可以被catch写入的不同类型接受
catch(...)
{
delete[] array;
cout << "释放array资源" << endl;
throw;
}
}
我们抛出的异常信息,首先先背Print模块中的catch接收,然后再抛出,直到被main函数栈中的catch接收,实现重新抛出的接收!
当我们抛异常时涉及到资源释放时,我们进行异常的重新抛出,来解决异常的安全问题
2.2.noexcept关键字
noexcept是 C++11 提供的一个新特性,用于指明某个函数或函数模板不会抛出异常。这个关键字提供了一种方式,让程序员能够明确地声明一个函数是否可能抛出异常,从而帮助编译器进行更好的优化,并减少因异常传播而导致的性能开销。
void safeFunc() noexcept
{
// 这个函数承诺不会抛出异常
// ...
}
添加noexcept关键字,表示这个函数经过人为检查后定性为不会出现异常,然后编译器读取后就给他打上不出异常的标志,后续运行时不会检查是否出现异常。所以noexcept关键字不能用于会出现异常的函数中,这样子会导致编译器跳过对该函数异常的检查,使得异常抛出却没被接收,进而导致程序终止。
对于可能出现异常的函数我们不能添加noexcept关键字!!!
那么为什么我们需要noexcept这个关键字呢?
- 在异常处理机制引入C++时,我们一般是主动地定义某个函数会抛出什么种类的异常,就是将noexcept对应位置替换为throw(int),表示会抛出int类型的异常
- 但是实际应用时,这个代码规范没什么人愿意遵守,并且某些函数模块抛出的异常种类可能较多,所以C++11就通过noexcept关键字来简化这个过程
另外noexcept关键字的引入也有如下好处:
- 性能优化:编译器可以针对
noexcept
函数进行特定的优化,因为它们不需要为可能的异常传播保留额外的栈空间或进行其他可能降低性能的操作。- 更好的错误处理:通过明确标记哪些函数可能抛出异常,程序员可以更清晰地了解代码的异常安全性,并相应地设计错误处理策略。
- 更明确的错误处理:当函数被标记为
noexcept
时,它向使用该函数的代码明确传达了不会抛出异常的承诺,这有助于减少代码中的不确定性。
2.3.异常的优缺点
优点:
- 实现合理的异常判断,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至包括堆栈调用的信息,这样可以帮助更好的定位程序的bug。(这里因为错误码在函数调用链中,需要层层返回错误,直到最外层才能比对并得到错误信息)
- 通过catch可以直接跳到错误地方,直接读取错误信息,当调用链深时,不用层层返回
- 很多第三方库都使用了异常处理机制,我们也需要匹配
- 无返回值函数无法返回错误码,例如类与对象的构造函数,所以引入异常机制,有助于无返回值函数的错误信息输出
缺点:
- 异常的执行流容易乱跳,当我们抛出异常后,会直接跳转到catch处,当我们调试程序时,会很复杂
- 异常具有一定的性能上的开销,但是可以忽略不计
- C++没有垃圾回收机制,资源需要自行管理,可能出现内存泄漏、死锁等问题
- 异常需要规范使用,如果内部随意抛异常,外部很容易catch出现问题