C++学习之路-异常(exception)

编程过程中常见的错误类型

语法错误

不写分号,或者分号写成了中文字符下的分号

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;
	}
}
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值