异常
编程过程中常见的错误类型
语法错误
不写分号,或者分号写成了中文字符下的分号
int a = 10
int a = 10;
这种错误,编译器会直接报错
逻辑错误
明明是想实现两个数的和,但是函数内部却写成了两个数相除
int add(int a, int b) {
return a / b;
}
这种错误,编译器不会报错,也就意味着编译正常通过。但是结果可能就不是我们想要的了。
异常
异常是在程序运行过程中可能会发生的错误,通常是开发人员编写代码时没有考虑周全。(比如:内存不够,0作除数等)
-
比如申请了一块巨大的内存,或者是本来就没有内存,却继续申请。
for (int i = 0; i < 9999; i++) { int *a = new int[999999999]; }
此时如果不处理异常,程序就会一直卡在这,直到内存彻底不足,直接崩掉
-
比如0作了除数
int divide(int a, int b) { return a / b; } int c = divide(10, 0);
对于这种异常,C++内部写好了,所以及时做出了处理,并给出了异常说明:Interger division by zero。
我们到这里可以看出来,如果我们可以主动发现异常,就很容易找到程序崩溃的地方,更加的利于找Bug。但是,如果我们没有主动发现异常(比如在程序的某个地方因为申请了大量的堆空间,导致内存不足),就很难找到问题所在,这对我们开发是很不利的。
因此,我们只要在可能发生异常的地方,稍微提前监视一下,就可以解决这个问题。
异常的捕捉
在开发的过程中,我们知道哪些代码可能会发生异常。当然这些可能发生异常的地方可能很多,所以我们如果可以捕获到异常的发生,就立马知道哪里发生了异常,那样改Bug就会很轻松。
C++提供了一组方法来捕获异常:try-catch组合,字面意思可以理解:尝试(try)做一些可能会发生异常的事,不行的话你就接住(catch)我的异常。
try{
// 将可能出异常的代码放在这里
}
catch (异常类型 变量名){
// 一旦上面try里的发生了异常,就会被catch接住
// 我们就可以在这里日志异常原因,更加有助于我们定位bug
}
所以,申请大量内存时,我们就可以进一步完善代码:
for (int i = 0; i < 9999; i++)
{
try
{
int *a = new int[999999999];
}
catch (...)
{
cout << "异常是:内存不够了" << endl;
break;
}
}
我先尝试的去申请内存,如果发现内存不够,会立马产生异常。catch参数是三个点“…”,表示的是可以收到任何异常类型。
在我进行了异常处理之后,就不会发生程序莫名奇妙终止的情况。
异常发生后会立马被catch接住
异常发生后立马被catch接住。这句话表达的意思就是:当try内部捕捉到某行代码出现了异常,会立马终止后面的代码,直接进去catch里。
举个例子:下方代码,当int *a = new int[999999999];发生异常的那一刻,1和2都不会被打印了,直接进到catch里打印3。并且,我们处理了异常,因此程序正常执行,4也是可以被打印出来的。
try
{
int *a = new int[999999999];
cout << 1 << endl;
cout << 2 << endl;
}
catch (...)
{
cout << 3 << endl;
cout << "异常是:内存不够了" << endl;
}
cout << 4 << endl;
异常的抛出
上面发生异常之后(内存不够),编译器会自动抛出异常给catch函数。但是,我们有时会想要主动抛出异常,也就是通过人为控制抛出异常。
首先想想为什么会有这样的一个需求?:想要主动抛出异常。因为,不是所有的异常编译器都会帮你抛出。
假设0作除数的时候,编译器不会主动帮你抛出异常,这时候,我们就要自己抛出.
int divide(int a, int b)
{
if (b == 0) {
throw 666; //抛出一个异常,类型是int的
}
return a / b;
}
try
{
int c = divide(10, 0);//因为0做了除数,所以会执行throw 666
}
catch (int exception) //因为666是int类型的数据,所以想要接住这个异常,catch的入口参数必须是int类型或者是...
{
cout << "异常是:0做了除数" << endl;
}
所以,我们想要主动的抛出异常,使用throw关键字。抛出的类型是任意的,只不过catch的入口参数要与之对应。
比如,换成抛出字符串类型的异常,就要在catch参数的时候,传入const char *类型,后面的变量名是任意的
int divide(int a, int b)
{
if (b == 0) {
throw "0作除数了";
}
return a / b;
}
try
{
int c = divide(10, 0);
}
catch (const char * exception)
{
cout << "异常是" << exception << endl;
}
catch可以存在多个
存在这样的场景:在一个函数中存在多种可能的异常,因此我抛出的异常也是多个。此时,我们就需要更多的catch接收不同类型的异常。程序就可以这样写:
int divide(int a, int b)
{
if (b == 0) {
throw "0作除数了";
}
if (sizeof(decltype(b)) != 4) { //假设我要求b是4个字节的,如果不是,就抛出异常
throw 444;
}
return a / b;
}
try
{
int c = divide(10, 0);
}
catch (const char * exception)
{
cout << "异常是" << exception << endl;
}
catch (int exception) {
cout << "异常是" << exception << endl;
}
如果,throw之后,找不到与之匹配的catch入口,那么整个程序就会中止。这是因为,异常的抛出必须有catch接住,否则程序就会崩掉。
异常的抛出声明
为了增强可读性和方便团队协作,如果函数内部可能会抛出异常,建议函数声明一下异常类型
int divide(int a, int b) throw (int)
{
}
别人一看到函数这样声明,就知道将来可能抛出int型异常,所以就会特别留意这件事。然后,每次在使用这个函数的时候,后面都会配一个int型的catch,拦截这个函数可能出现的int型异常
try
{
divide(10, 0);
}
catch (int exception) {
cout << "异常是" << exception << endl;
}
自定义异常类型(自定义为类)
首先我定义一个异常的基类,是我整个项目可能存在的所有异常的基类。
// 所有异常的基类
class Exception
{
public:
virtual const char *what() = 0; //异常发生时,说出到底是什么异常,这里写一个纯虚函数
};
//除法异常
class DivideException : public Exception
{
public:
DivideException(){}
const char *what() {
return "不能除以0";
}
};
//加法异常
class AddException : public Exception
{
public:
AddException(){}
const char *what() {
return "加法操作出现异常";
}
};
// ......还有很多异常,到时候根据项目需求自定义即可
然后,我们在写可能出现异常的函数时,就可以抛出我们自定义类型的异常了,而不是单纯的基本数据类型。
int divide(int a, int b) throw (DivideException)
{
if (b == 0) {
throw DivideException();//抛出自定义的异常类型
}
return a/b;
}
try{
divide(10, 0);
}
catch (const DivideException &exception) { //接收自定义的类型
cout << "Exception:" << exception.what() << endl;
}
这样抛出自定义类型异常有什么好处?
- 更加面向对象,程序连异常都是面向对象的
- 一旦面向对象,意味着我们以后可以在异常中扩充很多功能(比如异常不仅仅有what输出异常信息,还有异常码,可以输出该异常的唯一编码,还有很多…)
- 随时可以为一个异常对象赋予更多的功能
- 摒弃了之前,直接抛出一个字符串,直接抛出一个数字。这样更加专业
以后凡是抛出的是自定义异常,我们都在catch的入口参数些微基类参数即可。因为父类指针,可以指向子类对象,任何子类的异常对象都可以赋值给基类指针。由于赋值给基类指针的是子类对象,在调用重写的函数时,就会调用相应的子类函数。(这是多态的内容)
- 异常类的声明
// 所有异常的基类
class Exception {
public:
virtual const char *what() const = 0; //异常发生时,说出到底是什么异常,这里写一个纯虚函数
};
//除法异常
class DivideException : public Exception {
public:
DivideException() {}
const char *what() const{ //重写抽象基类中的纯虚函数
return "不能除以0";
}
};
- 异常的捕获,抛出,接收
int divide(int a, int b)
{
if (b == 0) {
throw DivideException(); //抛出异常
}
return a / b;
}
try
{
int c = divide(10, 0);
}
catch (const Exception &exception) {//接收异常
cout << "Exception:" << exception.what() << endl;
}
C++中已经提供的标准异常(std)
那我们以后在处理异常的时候,多数时候可以为了简单起见,使用C++自带的一些标准异常。这里就涵盖了很多异常情况了,足以满足大部分需求。
for (int i = 0; i < 99999; i++)
{
try
{
int *a = new int[999999999];
}
catch (const std::exception& exception) //exception是标准异常里的基类
{
cout << "异常是:" << exception.what() << endl; //也有what成员
break;
}
}