C++异常详解


前言

在本篇文章中,我们将会详细介绍一下有关C++异常的讲解,主要涉及异常的使用,应用场景,异常的优缺点等方面,同时有了异常才会引出我们之后学习的一个非常重要的知识点————智能指针。

一、回顾C语言

c语言解决错误时一般是通过返回错误码的方式,如果遇到非常严重的错误,就会终止程序。

🌟 终止程序:比如我们遇到除零错误,内存错误,空指针的解引用等操作,我们的程序就会终止,程序终止也意味着进程的终止,这有可能会导致大量重要数据的丢失,这是非常严重的。
🌟 返回错误码:也可以通过返回错误码的方式告诉我们相应的错误,但这只是错误码,我们还需要根据错误码查找相关的错误信息,再进一步的分析程序,才能得出具体的错误信息,这时很不便利的。很多库函数都是把错误信息放到error中,表示错误。

二、异常的概念

我们需要程序告诉是是什么错误。

我们C++在解决错误时采用的方法时异常

当函数发现了自己无法处理的错误时抛异常,让函数直接或者间接的调用者处理这个异常。

我们将会引入三个关键字

🌟 throw:出错时,用于抛异常(可在任意位置抛出)
🌟 try:里面放可能抛异常的代码,其中这里的代码称为保护代码,后面跟着catch
🌟 catch:在想要处理的地方,通过异常处理程序捕获异常,可以有多个catch进行捕获

throw…………

try
{
}
catch(………)
{
}
catch(……)
{
}

其实异常就存在在我们的日常生活中,就比方说微信在网络不好的时候,会出现一个感叹号告诉你消息发不出去,此时的程序就是在抛异常,告诉你当前网络状态不佳。

如果这个异常按照C语言对错误的处理方式进行操作,整个微信进程就会直接崩溃,强行退出。而采用C++的抛异常机制,将抛出的异常捕获,然后处理,比如当出现网络不好抛异常时,微信就会采取尝试多次发送这样的操作,整个微信程序也不会退出。

正式由于在实际中,很多情况下我们是不希望只要产生异常就直接终止的整个进程的,通过抛异常和捕获处理异常的手段便可让程序保持运行。

三、异常的使用

1.异常的抛出和捕获

异常的抛出和匹配原则

🌟异常通过抛出对象引发,这个对象与catch中()中类型进行匹配,这个对象可以是内置类型,也可以是自定义类型。


#include <iostream>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int a;
	int b;
	cin >> a >> b;
	cout << Division(a, b) << endl;
}
int main()
{
	try
	{
		fun();
	}
	catch (const char*errmsy)
	{
		cout << errmsy << endl;
	}
	return 0;
}

我们运行运行抛异常看一下
在这里插入图片描述
我们确实捕捉到了,打印的就是throw抛的内容。

🌟 我们再匹配相应的catch代码时,如果有多个都满足,选取与throw类型匹配且距离较近的catch.

调用链是指函数栈帧建立的先后顺序,就比如下面代码中main函数优先建立栈帧,然后func函数建立栈帧,最后division函数建立栈帧,这样的顺序就叫调用链。

举个例子说明一下

#include <iostream>
using namespace std;
double  Division()
{
	int a;
	int b;
	cin >> a >> b;
	if (b == 0)
	{
			throw "Division by zero condition!";
	}
	else
	{
			return ((double)a / (double)b);
	}
}


void fun()
{
	try
	{
		Division();
	}
	catch (const char* errmsy)
	{
		cout <<"void fun()" << errmsy << endl;
	}
}
int main()
{
	try
	{
		fun();
	}
	catch (const char* errmsy)
	{
		cout <<"int main()" << errmsy << endl;
	}
	return 0;
}

main函数调用了fun函数,fun函数调用了Division函数。
main函数中catch与fun函数中的catch都与Division中的throw类型匹配,那么他会调用哪个呢??

我们抛异常来看一下
在这里插入图片描述
我们发现调用的是fun中的catch,因为两个都满足。所以找最近的那一个,就是fun函数了。
🌟抛出异常对象后,会生成一个异常对象的拷贝,并不是直接传递给catch()里面的对象,而是将throw对象的拷贝传给catch()中的对象,这个拷贝的临时对象会被catch后销毁。


#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		string s("Division by zero condition!");
		throw s;
	}
	else
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int a;
	int b;
	cin >> a >> b;

	cout << Division(a, b) << endl;
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	return 0;
}

我们抛出的是一个string的临时对象s,出了作用域销毁了,如果是直接传给外边,传递不过去。
如果我们抛异常了,打印的还是Division by zero condition!,就能够说明生成了拷贝。

我们测试一次看看
在这里插入图片描述
我们发现确实存在拷贝,这和函数参数的传递有异曲同工之妙。

由于临时对象具有常性,所以当抛出的对象是指针时一定注意在形参上加上const才能被接收。这也解释了上面的代码中为什么error_message的类型为什么是const char而不是char
🌟我们一般在catch最后边加上一个catch(…),表示这个catch可以匹配任意类型,但是不知道异常错误是什么。
我们这个东西可以解决很大的问题,如果我们的异常没有对应的catch,就会报错,如果放到程序中,就会崩溃。

#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else if(y==1)
	{
		return ((double)x / (double)y);
	}
	else
	{
		throw 1;
	}
}

void fun()
{
	int a;
	int b;
	cin >> a >> b;

	cout << Division(a, b) << endl;
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	catch (...)
	{
		cout << "Unkown error" << endl;
	}
	return 0;
}

我们如果输入的第二个数为10,就会抛异常,类型为整形,但是外边没有对应的类型匹配,就会匹配到(…)中。
在这里插入图片描述
🌟有一种特殊情况,不用类型匹配:可以抛出子类对象,使用父类对象再catch中进行捕获,这个在实际中是非常有用的。

在函数调用链中异常栈展开匹配原则

🌟1.查看throw是否在try中,如果在,就进行异常捕获。
🌟2.如果存在匹配的catch,就调到对应的catch进行处理。
🌟3.如果这一层没有匹配的catch,退出当前栈,就到上一层中进行寻找。
🌟4.如果再main函数中,也不存在匹配的catch就报错,
沿着调用链查找匹配catch子句的过程称为栈展开。
🌟5.异常捕获与exit不同。exit直接退出程序,catch处理完异常之后,catch后面的代码会正常执行。

#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else if (y == 1)
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int a;
	int b;
	cin >> a >> b;
	cout << Division(a, b) << endl;
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	catch (...)
	{
		cout << "Unkown error" << endl;
	}

	cout << "异常处理后继续执行相关代码" << endl;
	return 0;
}

在这里插入图片描述

2.异常的重新捕获

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用
链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

我们捕获这个异常并不是为了处理这个异常,而是为了干一些其他的事情之后,再把这个异常抛给上层,继续处理。

我们举一个例子理解一下


#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int*p = new int[10];
	int a;
	int b;
	cin >> a >> b;
	cout << Division(a, b) << endl;
	cout << "delete[] p" << endl;
	delete[] p;
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	catch (...)
	{
		cout << "Unkown error" << endl;
	}
	return 0;
}

在fun函数中new了一块空间,现在抛异常看一下


我们new的空间并没有被释放,发生了内存泄漏

这时就需要用到异常的重新捕获了,我们需要在fun函数中对这个异常进行一次捕获,释放new的空间,再抛给main函数。
为我们也可以直接在fun中捕获异常,不在main中处理呀??
而在我们对异常进行一部分操作时,我们更愿意让所有异常在main函数中进行统一处理,比如对异常进行记录日志这样的操作,此时需要重新使用throw抛出异常。

我们看一下解决代码

#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y)
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int*p = new int[10];
	int a;
	int b;
	cin >> a >> b;
	try
	{
		cout << Division(a, b) << endl;
	}
	catch (...)
	{
		cout << "delete[] p" << endl;
		delete[] p;
		throw;//捕到什么抛什么
	}
	cout << "delete[] p" << endl;
	delete[] p;
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	catch (...)
	{
		cout << "Unkown error" << endl;
	}
	return 0;
}

运行看一下
在这里插入图片描述

一旦catch捕获异常,不能将异常用throw语句再次抛出,这句话是不对的。

三.异常安全与异常规范

1.异常安全

🌟构造函数时用来构造对象和初始化的,最好不要在构造函数抛异常,可能会导致对象不完整或者没有完全初始化。

🌟析构函数是用来释放空间的,对资源进行清理。最好不要在析构函数抛异常,可能会导致资源泄露(内存泄漏,句柄未关闭)。
🌟C++中异常经常会导致资源泄露问题,比如在new和delete中抛异常,导致内存泄漏,在lock和unlock之间抛出异常1导致死锁等问题,我们将会用智能指针来解决这个问题。

2.异常规范

我们在书写异常时,可以抛出任意类型的对象, 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。

C++98,建议,并不会强制报错,如果我们不这样做了只会有警告,这个是为了兼容c语言做出的让步。

🌟我们可以在函数头后买你加上throw( ),括号里存放可能抛出的异常类型。
throw(char,int char*)就表明了这个函数可能抛出char,int,char这三种类型的异常。
🌟这里表示这个函数只会抛出bad_alloc的异常
void
operator new (std::size_t size) throw (std::bad_alloc);
🌟throw(),表明这个函数只会不会抛出异常
🌟如果无异常接口声明,此函数可以抛任意类型的异常

但是有些异常类型是非常复杂的,为了写出可能发生的异常类型,代价会很大,而且写时太繁琐写出来也不美观。因此,这个建议性的规范很少有人用,也正因为它只是一个建议,所以不使用或者不按要求使用也不会报错。

#include <iostream>
#include <string>
using namespace std;
double  Division(int x, int y) throw()//表明不会抛异常
{
	//除零错误,抛异常
	if (y == 0)
	{
		throw "Division by zero condition!";
	}
	else
	{
		return ((double)x / (double)y);
	}
}

void fun()
{
	int a;
	int b;
	cin >> a >> b;
	 cout << Division(a,b) << endl; 
}
int main()
{
	try
	{
		fun();
	}
	catch (const string ret)
	{
		cout << ret << endl;
	}
	catch (...)
	{
		cout << "Unkown error" << endl;
	}
	return 0;
}

但是如果我们硬要抛异常呢??
在这里插入图片描述
只会存在警告,不会强制报错。

C++11

🌟noexcept,表明函数不会抛异常
但如果我们还是硬抛异常会怎样呢???

在这里插入图片描述
代码就给我们直接挂掉了。

四.自定义异常体系

在以后写代码的时候会遇到小组合作的形式,每个小组负责不同的模块,每个小组都会抛出异常,但是每个小组抛出的异常类型不同,放在一起在main函数中进行捕捉就会很复杂。
实际中抛出和捕获的匹配原则有一个例外,类型可以不完全匹配,抛出子类对象用父类进行捕捉。每个小组都可以抛出派生类的异常,在mian函数中使用基类统一捕捉。

实际项目中,可以创建一个父类,父类中有一个错误码(int)和错误描述信息(string),在这个类里面创建一个虚函数用于返回内部的string对象方便使用人员打印。

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};

不同的小组抛出的异常都会具有本小组的特点,用继承的方式创建一个类,子类中添加一个成员变量记录当前模块的特殊错误信息,并且该类所对应的what函数就可以通过重写来添加一个标志性内容,我们就可以更加容易的知道哪里出了问题。

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)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try {
			HttpServer();
		}

		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

main函数中首先调用httpserver函数,生成一个随机数,我们把这个数字看作一种情况,如果这个数字能被3 和4整除那么这种情况就出错了,直接使用throw来抛出异常,如果没有出错的花就调用缓冲区的函数,这个函数里面也是相同道理,在这个函数之后就调用数据库的相关函数遇到一些情况就抛出异常,那么在main函数里面我们就可以统一使用父类类型的catch来统一捕获异常,并使用里面的what函数来打印出从的内容,那么上面的代码运行的结果如下:

五.C++标准库的异常体系

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父
子类层次结构组织起来的,如下所示:

在这里插入图片描述

在这里插入图片描述
由于C++提供的异常体系对项目开发中的异常帮助十分有限,所以这个标准几乎没人用。’

六.异常优缺点

优点

🌟1.相比于错误码,异常可以清晰准确的展现出错误信息,甚至包含堆栈调用的信息,帮助我们更好的定位bug
🌟2.异常会进行多层跳转,不用层层返回进行判断,直到最外层拿到错误。


int ConnnectSql()
{
	// 用户名密码错误
	if (...)
		return 1;

	// 权限不足
	if (...)
		return 2;
}

int ServerStart() {
	if (int ret = ConnnectSql() < 0)
		return ret;
	int fd = socket()
		if(fd < 0return errno;
}

int main()
{
	if (ServerStart() < 0)
		...

		return 0;
}

下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,ServerStart再返回给main函数,main函数再针对问题处理具体的错误。

如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
🌟2.很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常
🌟4.部分函数使用异常更好处理。
T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

缺点:

🌟1.导致执行流乱跳,导致我们追踪调式以及分析程序时,比较困难。
🌟2.异常会有一些性能的开销,。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
🌟3.C++没有垃圾回收机制,资源需要自己管理,有可能发生内存泄漏,死锁等安全问题。
🌟4.C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
🌟异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。

练习题

如何捕获异常可以使得代码通过编译? ()

class A

{
public:
  A(){}
};

void foo()
{  throw new A; }

A.catch (A x)
B.catch (A * x)
C.catch (A & x)
D.以上都不是

正确答案是B, 异常是按照类型来捕获的,throw后抛出的是A*类型的异常,因此要按照指针方式进行捕获

总结

以上就是今天要讲的内容,本文仅仅详细介绍了C++异常的内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lim 鹏哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值