中山网站制作网页,wordpress改成自己网站,做设计用的素材下载网站,wordpress专栏插件目录 前言 一、什么是异常 二、C中的异常 2.1 C语言中的异常处理 2.2 C中的异常处理 2.3 异常的抛出与捕获 2.4 栈展开 2.5 查找匹配的处理代码 2.6 异常重新抛出 2.7 异常安全问题 2.8 异常规范 2.9 标准库的异常 前言
在之前我们已经学习了C中不少知识了#xff0c;但是其中…目录 前言 一、什么是异常 二、C中的异常 2.1 C语言中的异常处理 2.2 C中的异常处理 2.3 异常的抛出与捕获 2.4 栈展开 2.5 查找匹配的处理代码 2.6 异常重新抛出 2.7 异常安全问题 2.8 异常规范 2.9 标准库的异常 前言
在之前我们已经学习了C中不少知识了但是其中的一些异常我们当时可能就随便糊弄过去了我们不清楚C中处理异常的机制而今天我们就来学习一下C中如何处理异常。 一、什么是异常
异常指程序、系统或事物运行中出现的不符合预期或正常逻辑的状态或事件可能导致流程中断、错误结果或功能失效这是我们在百度上的对异常这个名词的权威解释。那么编程语言中的异常是指程序在运行过程中遇到的一些不正常的情况如除以零、访问不存在的文件、内存不足等。这些异常如果不处理可能导致程序崩溃或产生不可预期的结果。异常处理机制允许程序检测到这些异常并采取相应的措施如记录错误信息、提示用户或尝试恢复程序从而提高程序的健壮性和用户体验。常见的异常处理机制包括try-catch块用于包围可能抛出异常的代码并在catch块中处理异常。有效的异常处理可以帮助开发者写出更加可靠和稳定的代码。
简单来说我们在编程中所遇到的那些异常就是因为我们的某些操作导致出现了一些可能会让计算机程序崩溃的情况我们把这些情况都叫做异常。
二、C中的异常
2.1 C语言中的异常处理 我们在C语言阶段当时我们学习时是没有学习到异常的因为在C语言中并没有内置的处理异常的机制。在C语言中处理异常通常是使用返回值或者全局变量。例如当我们的一个函数出现异常时它会返回一个错误码信息从而使得编译器发生报错。这样做看起来十分地麻烦因为每个函数都返回一个错误码的话就会导致代码冗长复杂。
2.2 C中的异常处理
C中的异常处理机制相较于C语言更为强大和灵活提供了try、catch、throw等关键字支持类型化异常和栈展开从而更优雅地处理错误减少资源泄漏的风险。这样一来即使出现了异常我们也有办法去处理。接下来我们就来学习学习C中处理异常的方式。
2.3 异常的抛出与捕获
当程序出现问题时我们通过抛出throw一个对象来引发一个异常该对象的数据类型以及当前的调用链决定了应该由哪个catch的处理代码来处理异常。这里我们需要注意的是我们的try块语句不一定就和下一个catch进行匹配它们是根据数据类型进行匹配的被选中的处理代码是调用链中与改对象类型匹配且与抛出异常位置最近的那一个。根据抛出对象的类型和内容程序的抛出异常部分会告知异常处理部分到底发生了什么错误。当throw执行时throw后面的语句就不再被执行。程序的执行从throw位置跳到与之匹配的catch模块catch可能是同一函数中的一个局部的catch也可能时调用链中另一个函数中的catch控制权从throw转移到了catch位置。这里还有两个重要的含义1.沿用调用链的函数可能提早退出2.一旦程序开始执行异常处理程序沿用调用链创建的对象都将被销毁。抛出异常对象后会生成一个异常对象的拷贝因为抛出的异常对象可能是一些局部对象我们在离开那个函数后那些局部对象就销毁所以会生成一个拷贝对象这个拷贝的对象会在catch子句后销毁。这里类似我们之前的函数传值返回我们只是传递的是函数的值对于形参的操作并不会影响对实参的内容。我们这里对原来那个异常对象的拷贝对象进行处理不会影响原来的那个异常对象
2.4 栈展开
当我们抛出异常时程序会暂停当前函数的执行开始寻找与之匹配的catch子句首先检查throw本身是否在try块内部如果在则查找匹配的catch子句如果有匹配的则跳到对应的catch的地方进行处理。这一系列操作在运行时动态进行的如果当前函数中没有try/catch子句或者有try/catch子句但是类型不匹配则退出当前函数继续在外层调用函数链中进行查找上述的查找catch过程被称作为栈展开。如果到达main函数依旧没有找到匹配的catch子句程序会调用标准库中的 terminate 函数终止程序。如果找到匹配的catch子句处理后catch子句代码会继续执行。
如下图是我们抛出异常后栈展开的示意图 接下来我们用如下示例代码来给大家展示一下异常是用哪个catch来进行捕获的我们写了两个函数一个除法函数我们在函数体中使用try...catch语句来捕获异常;还有一个函数则是在try块语句中调用了除法函数同时也抛出异常了最后我们在main函数中try块语句中调用我们的Func函数这样就形成了一个层层调用的形式我们来通过运行结果来看看最后匹配的catch是哪个
double Divide(int a, int b)
{try{// 当b 0时抛出异常if (b 0){string s(Divide by zero condition!);throw s;}else{return ((double)a / (double)b);}}catch (int errid){cout errid endl;}return 0;
}void Func()
{int len, time;cin len time;try {cout Divide(len, time) endl;}catch (const char* errmsg){cout errmsg endl;cout Func endl;}}int main()
{while (1){try{Func();}catch (const string errmsg){cout errmsg endl;cout main endl;}}return 0;
}从上面的运行代码结果我们可以看出来它最终匹配的catch是我们main函数中的catch因为我们在Divide函数中抛出的异常是一个string类型的语句由于我们的匹配原则是根据数据类型来进行匹配的因此我们匹配的是main函数中的catch语句它的catch参数是string类型的。我们通过上面的例子我们更加清晰地认识到了我们try中的异常来匹配catch是通过抛出的异常数据类型与catch参数类型是否匹配来进行匹配选择的。
2.5 查找匹配的处理代码
一般情况下抛出对象和catch类型是完全匹配的如果有多个类型匹配的就选择离它位置更近的那个。但是有一些例外它允许从非常量向常量类型进行转换即将普通变量向有const修饰的变量进行转换即我们之前所说的权限缩小它还允许数组转换成指向数组元素类型的指针函数转换成函数的指针允许从派生类向基类类型进行转换这点非常常用实际中继承体系基本都是用这个方式来进行设计的。如果到main函数异常仍没有被匹配的话就终止程序。不是在发生严重的错误情况下我们是不期望程序终止的所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常这样我们就不用写好几个不同类型的catch语句来进行捕获异常了但是这种catch语句我们是不知道异常错误是什么。
如下代码我们通过一个异常基类来获取一些基本的信息错误码id等然后我们再将几个派生类来继承那个基类除此之外加上自己的一些异常信息。我们是为了测试查找匹配的异常处理代码于是我们在main函数中设置一系列的随机数并定时出现结果。
#includethreadclass 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 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 HttpException : public Exception
{
public:HttpException(const string errmsg, int id, const string type):Exception(errmsg, id), _type(type){}virtual string what() const{string str HttpException:;str _type;str :;str _errmsg;return str;}
private:const string _type;
};void SQLMgr()
{if (rand() % 7 0){throw SqlException(权限不足, 100, select * from name 张三);}else{cout SQLMgr 调用成功 endl;}
}void CacheMgr()
{if (rand() % 5 0){throw CacheException(权限不足, 100);}else if (rand() % 6 0){throw CacheException(数据不存在, 101);}else{cout CacheMgr 调用成功 endl;}SQLMgr();
}void HttpServer()
{if (rand() % 3 0){throw HttpException(请求资源不存在, 100, get);}else if (rand() % 4 0){throw HttpException(权限不足, 101, post);}else{cout HttpServer调用成功 endl;}CacheMgr();
}int main()
{srand(time(0));while (1){this_thread::sleep_for(chrono::seconds(1));try{HttpServer();}catch (const Exception e) // 这里捕获基类基类对象和派生类对象都可以被捕获{// 多态cout e.what() endl;}catch (...){cout Unkown Exception endl;}}return 0;
} 我们可以看到由于我们输入的是随机信息因此最后的结果也是随机的。我们从上面的结果可以看出我们匹配catch语句它的数据类型是可以通过派生类继承基类来实现的而这也是我们以后主要使用到的。 2.6 异常重新抛出
有时catch到一个异常对象后需要对我们所查找的异常对象所造成的错误进行分类其中的某种异常错误需要进行特殊的处理其他错误则重新抛出异常给外层调用链来进行处理。捕获异常后需要重新抛出我们直接使用throw这个关键字就可以把捕获的对象直接抛出。
其实对异常重新抛出就是对我们所抛出的那个异常在放到一个循环中我们在这个循环中来进行对异常的处理达到条件我们就将其抛出。如下代码是我们模拟我们平时信息对话信号不好尝试多次才发送出去的情况。
void _SeedMsg(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{_SeedMsg(s);break;}catch (const Exception e){// 捕获异常if中是102号错误网络不稳定则重新发送// 捕获异常else中不是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 Unkown Exception endl;}}return 0;
}我们从上面的代码可以看出异常的重新抛出我们就是将我们的主要异常封装成一个函数然后我们在另一个函数体中使用循环多次调用那个函数同时使用throw来抛出异常这里由于我们抛出的异常我们在第一个函数中就已经展示出来了这里我们的throw就不用接其他内容了。
2.7 异常安全问题
异常抛出后后面的代码就不再执行前面申请了资源内存锁等后面进行释放但是中间可能会抛出异常就会导致资源没有释放这里由于异常就引发了资源泄露产生安全性问题。中间我们需要捕获异常释放资源后再重新抛出。其次析构函数中如果抛出异常后也要谨慎处理比如析构函数需要释放10个资源释放到第5个时抛出异常的话我们也是需要进行捕获处理的否则后面的5个资源就没法释放也就造成了资源泄露了。
下面的代码是有资源申请的我们在捕获异常时需要对资源进行释放另外我们在捕获异常的后面对两个指针进行释放再输出相应的地址。
double Divide(int a, int b)
{try{// 当b 0时抛出异常if (b 0){string s(Divide by zero condition!);throw s;}else{return ((double)a / (double)b);}}catch (int errid){cout errid endl;}return 0;
}void Func()
{int* ptr1 new int[10];int* ptr2 new int[10];int len, time;cin len time;try {cout Divide(len, time) endl;}catch (...){delete[] ptr1;cout delete: ptr1 endl;delete[] ptr2;cout delete: ptr2 endl;// 重新抛出throw;}delete[] ptr1;cout delete: ptr1 endl;delete[] ptr2;cout delete: ptr2 endl;
}int main()
{while (1){try{Func();}catch (const string errmsg){cout errmsg endl;}catch (...){cout 未知异常 endl;}}return 0;
} 我们从上面的运行结果我们可以看出来地址都是相同的说明最后都是被释放的。我们在catch语句块中选择了对指针的资源释放 2.8 异常规范
对于用户和编译器而言预先知道某个程序会不会抛出异常会大有裨益知道某个函数是否会抛出异常有助于简化调用函数的代码。C98中函数参数列表的后面接throw表示函数不抛出异常函数参数列表的后面接throw类型1类型2...)表示可能会抛出多种类型的异常可能会抛出的异常类型用逗号来进行分割。C98的方式过于复杂实践中并不好用在C11中进行了简化在函数参数列表后面加上noexcept则表示不会抛出异常啥都不加的话就表示可能会抛出异常。编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了但是同时又包含了throw语句或者调用的函数可能会抛出异常编译器还是会顺利编译通过的有些编译器可能会报个警告但是一个声明了noexcept的函数抛出了异常程序会调用 terminate 来终止程序。noexceptexpression还可以作为一个运算符去检测一个表达式是否会抛出异常可能会抛出异常的话就返回false不会抛出异常的话就返回true。
有时候我们传递一个函数会抛出异常的函数的话它是根据那个函数是否会抛出异常来返回返回值的我们不是通过看传递的参数来进行判断的即使我们传递的参数不会造成异常但是这个函数是可能会抛出异常的因此它还是会返回一个false的。
// C98
// 这⾥表⽰这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这⾥表⽰这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C11
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
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 Unkown Exception endl;}int i 0;cout noexcept(Divide(1,2)) endl;cout noexcept(Divide(1,0)) endl;cout noexcept(i) endl;return 0;
}
2.9 标准库的异常
在C标准库中也定义了一套自己的异常继承体系库基类是exception所以我们日常写程序时需要在主函数捕获exception即可要获取异常信息调用what函数what函数是一个虚函数因为我们在前面就说过了我们可以使用派生类来继承一个异常的基类我们可以在派生类中重写那些what虚函数。如下图是一些标准库中的异常