作者:几冬雪来
时间:2023年11月21日
内容:C++板块异常讲解
目录
前言:
异常:
C语言传统处理错误的方式:
捕获异常:
异常的相关特性:
string与抛异常:
catch(...):
类型匹配:
常用的抛异常:
异常规范:
异常的优缺点:
优点:
缺点:
总结:
结尾:
前言:
在上一篇博客中我们讲解了C++的重要知识点——map与set,map和set可谓是C++一大必须学习的知识点,那么在消化之前,我们来讲解C++中的异常。
异常:
在学习无论是C语言又或者C++,只要是编程语言那就肯定存在出错的情况,而编译器在代码出错的时候就会产生异常。
C语言传统处理错误的方式:
首先我们先来说C语言处理错误的方式。
第一个方式就是终止程序,类似断言或者退出函数等等行为。、
下一个则是返回错误码,这是C语言常见的处理错误的方式,但是它也会存在一些问题。比如我们无法得知代码在哪个模块出事的。
捕获异常:
就像是错题要找出来错在哪里一样,既然在编程的时候发现了异常,那么我们就应该将异常给捕获住。
而捕获异常就需要对应的代码去进行该操作。
在进行捕获异常的时候就需要用到这里的代码就需要使用到这里的try...catch()。
而try...catch()就是用来捕获异常的,接下来通过一组代码来了解一下捕获异常。
这个地方发生异常的地方可能是在Func函数中,也可能是在Func函数的下一层出现了异常的问题。
如果这个地方发生了除0失误的话,这里结果就会抛异常,然后再将其捕获最后将其输出。
这发生的除0失误,导致程序终止了,这里就将Dibision函数中的抛异常的语句输出出来,如果输入的两个值没有发生除0失误的话就正常的执行,返回,输出结果。
然后就是try...catch语句中的catch语句。
在一般情况下程序是不会去执行catch的代码,只有一种情况会执行,那就是抛异常之后,程序会直接跳到catch的地方去,这也是抛异常的优势所在。
如果是以前我们的代码在太深处发生错误的话,这个地方只能一层一层的返回然后判断在重复以上操作。
但是抛异常并不是是这样做,哪怕函数有多深,只要检查到了异常,它会直接跳转到catch处函数栈帧也会依次销毁,这就是catch的一大优势所在。
异常的相关特性:
在了解了一定程度的异常后,这个我们来看一下异常的相关特性。
首先第一条,异常是通过抛出对象引发的。
然后代码在throw的时候会异常,而代码在报错的时候会执行throw,throw的后面不止能跟字符串对象,C++规则在这个地方我们可以抛任意类型的对象。
也就是可以throw任意类型的对象。
同时捕获的时候也有类型,如果catch的类型与throw的类型不匹配是捕获不到的。
但是如果代码抛异常了,却没有被成功的捕获的话,这种情况下编译器就会发生报错。
而且没有捕获的情况有很多:
上边就是没有捕获可能会发生的原因。
C++规定异常必须被捕获,如果没能完成异常的捕获,这个地方我们的编译器就会弹框报错,程序直接终止。
如果成功捕获异常后,它会沿着catch的子句继续往后执行。
就类似这里,即使这个地方抛异常了,下面的输出“xxx”的指令依旧会执行。
接下来还有一个重要的点,如果在程序执行的中间将异常捕获成功的话,这个地方就只需要跳转到离它近的catch即可,而catch只用执行一次即可。
string与抛异常:
再然后就是string与抛异常之间的关系。
在讲解抛异常的时候我们有说过throw的类型是没有限制的,那么这个地方就有人提出了问题。既然类型没有限制,那么它的类型能不能更改为string类型。
众所周知string在离开作用域的时候会调用虚构,而throw是一口气跳转到第一个catch处。
这里是可以正常抛一个string的异常出来, 因为在抛异常的原则中又说,抛异常和返回值很像,它会进行拷贝,类似传值返回。
这个拷贝的对象在被捕获之后才被销毁。
catch(...):
再然后讲解一下catch处的问题。
在上文中有提及throw的类型可以是整形,可以是浮点型,甚至能是string型。那么相对应的,在catch接收处的类型会不会出现问题呢?
这个地方为了防止有人一不小心抛了异常又没有将其捕获,异常的捕获机制给了这样的一种方式——catch(...)。
这里的catch(...)中的省略号表示它可以捕获任意类型的异常。
但是这种方式有一个缺陷,那就是我们无法检测出是什么错误,因此我们将这个错误称为未知异常,它用来捕获一些不规范的异常。
并且catch(...)的抛异常一般都是放在最后一个位置处,作为编译器的最后一道防线,前面都走完了到最后进行捕获。
这也就意味着有人没有按规范书写代码。
并且捕获异常的位置通常都比较统一,有些情况捕获的异常还需要进行处理。
类型匹配:
在上文中,我们有提到了过catch的类型要与throw的类型相匹配,这样才能成功返回。
但是实际中抛出和捕获的匹配原则有一个例外,这个地方我们可以抛出的派生类对象,使用基类进行捕获。
而这里基于上面的情况就出现了一种异常体系。
常用的抛异常:
再接下来就来谈一谈我们经常用到的抛异常。
在这张表中我们重点要关注的是exception,bad_alloc与out_of_range这三个异常。
其中exception是所以标志C++异常的父类,其他的异常可以认为是exception的派生类对象。bad_alloc就是抛new的异常,最后一个out_of_range则是用量抛vector等的异常,如果它们越界那就抛异常。
平时主要还是使用exception来抛异常。
就如同上图就是我们new抛异常的代码书写,就是使用到了bad_alloc。
异常规范:
在基本了解为空异常的内容和操作之后接下来我们来讲解异常的一些规范。
而在之前的书写异常的过程中就有有关异常规范的代码。
例如在new进行抛异常的时候,在它的函数后面会加上这一串的代码。
这里的两串代码都被称为异常规范。
而异常规范出现的原因是因为异常代码执行的时候会出现一个问题,这个问题会引起值停留乱跳,即使打断点都断不住。
在上图下边的代码表示,如果不抛异常就用throw(),如果要抛异常类似上图的这串代码就表示会抛bad_alloc的异常。
如果带抛异常的时候我们要求抛多个类的异常的话可以在函数后面写上throw与要抛异常的类,它们之间要用“,”隔开。
但是这种行为并不是强制的,因为C++兼容C语言,而C语言中没有这套规范。
而因为它不是强制的,所以只有极少数人使用它。
在这种情况下我们搞清楚抛哪几种异常,则是非常麻烦的一件事情。
因此在C++11中对这种行为进行了简化。
如果确定不抛异常就在函数的后面书写上一个noexcept,如果我们会抛异常但是不知道抛什么异常的话,这里就什么都不写。
但是也有例外,那就是析构函数没有保证抛不抛异常。
这里简化的规则是——标记出不抛异常的函数。
异常的优缺点:
在讲解完了异常之后,临近结束时我们来讲解一下异常的优缺点。
优点:
这里异常的第一个有用的点就是可以清晰准确的展示出各种错误信息,可以更好的定位程序的错误。
再则如果深层函数发生了错误,异常不需要一层一层返回。
第三点,很多第三方库都包含异常。
以上都是异常的优点。
缺点:
接下来讲解一下异常的缺点。
首先就是执行流乱跳问题,这会使代码非常的混乱。
第二点就是异常会有一定程度上的性能的开销。
然后就是C++没有垃圾回收机制,有异常后任意导致内存泄漏,死锁等安全问题。
这里异常规范不是强制性。
总结:
但是总体来说,异常的利大于弊 ,在OO的语言(面向对象)会经常使用到。
代码:
#include<iostream>
#include<string>
using namespace std;
//double Division(int a, int b)
//{
// if (b == 0)
// {
// throw "Division by zero condition!";
// }
// else
// {
// return ((double)a / (double)b);
// }
//}
//
//void Func()
//{
// try
// {
// int len;
// int time;
// cin >> len >> time;
// cout << Division(len, time) << endl;
//
// }
// catch (const char* errmsg)
// {
// cout << errmsg << endl;
// }
//}
//
//int main()
//{
// try
// {
// Func();
// }
// catch (const char* errmsg)
// {
// cout << errmsg << endl;
// }
// cout << "xxx" << endl;
// return 0;
//}
//double Division(int a, int b)
//{
// if (b == 0)
// {
// string s ("Division by zero condition!");
// throw s;
// }
// else
// {
// return ((double)a / (double)b);
// }
//}
//
//void Func()
//{
// int len;
// int time;
// cin >> len >> time;
// cout << Division(len, time) << endl;
//}
//
//int main()
//{
// try
// {
// Func();
// }
// catch (const char* errmsg)
// {
// cout << errmsg << endl;
// }
// catch (...)
// {
// cout << "未知异常" << endl;
// }
// cout << "xxx" << endl;
// return 0;
//}
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;
};
结尾:
到这里我们的异常部分的知识就要暂告一段落了,但是这并不意味着C++里面异常的知识就此结束了,这个我们提到了异常可能发生执行流乱跳的问题,而且这个问题将在后面的智能指针处得到优化,只不过那都是以后的事情了,最后希望这篇博客能给各位带来帮助。