C++--20.异常

C 语言传统的处理错误的方式
C++ 异常概念
异常的用法
自定义异常体系
标准库异常体系
异常的优缺点

C语言传统的处理错误的方式

传统的错误处理机制:
1. 终止程序,如 assert ,发生严重错误时,主动调用exit(xx);缺陷:用户难以接受。如发生内存错误,除 0 错误时就会终止程序。
2. 返回错误码 ,有些API接口都是把错误码放到errno中,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno 中,表示错误
3. C 标准库中 setjmp longjmp 组合。这个不是很常用
实际中 C 语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误

可以看到,C语言中处理错误的方式是很麻烦的,所以在C++中,引入了异常,来解决上面的这些问题

C++异常概念

我们先来看一段代码

 很显然,我们vector访问越界了,但是这里仅仅是停在了这里,发生的是断言错误,就中断了,很粗暴的报错

 

 而当我们运行上面这段代码时,产生的报错,却是这样的,相对而言好一点,没有捕获异常,所以我们需要捕获异常

 那么我们给其加上异常,这便是最经典的异常使用方式

异常是一种处理错误的方式, 当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接 的调用者处理这个错误
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常 . catch 关键字用于捕获异常,可以有多个catch 进行捕获。
try: try 块中的代码标识将被激活的特定异常 , 它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try catch 关键字。 try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。

我们再来看这样一段代码

namespace bit
{
	int div(int m, int n)
	{
		if (n == 0)
		{
		//	return ....;
		}
		else
		{
			return m / n;
		}
	}
}
	int main()
	{
		int n, m;
		cin >> n >> m;
		cout << bit::div(n, m) << endl;
		system("pasuse");
		return 0;
	}

我们设置了一个简单的函数,可以发现当我们对其进行输入n/0时,0做分母了,就会出现问题,我们在C语言中的解决办法是当其为0时,返回一个数,那么问题来了,我们应该返回什么数才合适呢?有些会选择-1,有些会选择0,等但是这些方式解决的都不好,那么此时我们的异常就起作用了

 利用我们的异常,throw,可以抛出任意类型,当我们除0时,catch会捕捉到-1,直接跳转到catch匹配的地方,来执行下面的语句,会自动来提醒我们,这里我们不一定要抛出-1,随便一个对应下面catch的捕捉的类型即可,完美地解决问题

异常的使用

异常的抛出和捕获

异常的抛出和匹配原则

1. 异常是通过 抛出对象而引发 的,该 对象的类型 决定了应该激活哪个 catch 的处理代码。
2. 选中的处理代码 是调用链中 与该对象类型匹配且离抛出异常位置最近 的那一个。也就是说我们在try时,去匹配的离他最近的那个catch,只要类型匹配,意味着存在截胡
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch 以后销毁。(这里的处理类似于函数的传值返回)
4. catch(...) 可以捕获任意类型的异常,问题是不知道异常错误是什么。其目的是解决上面没有匹配到的任意类型的异常,避免异常没捕获时程序终止,不过无法知晓捕获的是什么类型的
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配, 可以抛出的派生类对象,使用基类捕 获,这个在实际中非常实用

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

1. 首先 检查 throw 本身是否在 try 块内部,如果是再查找匹配的 catch 语句 。如果有匹配的,则调到 catch的地方进行处理。
2. 没有匹配的 catch 则退出当前函数栈,继续在调用函数的栈中进行查找匹配的 catch
3. 如果到达 main 函数的栈,依旧没有匹配的,则终止程序 。上述这个沿着调用链查找匹配的 catch 子句的过程称为栈展开 。所以实际中我们最后都要加一个 catch(...) 捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的 catch 子句并处理以后,会继续沿着 catch 子句后面继续执行

 异常的重新抛出

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

我们来看这段代码

	double Division(int a, int b) {
		// 当b == 0时抛出异常
		if (b == 0)
		{
			throw "Division by zero condition!";
		}
		return (double)a / (double)b;
	}
	void Func()
	{
		// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
		// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
		// 重新抛出去。
		int* array = new int[10];
		try {
			int len, time;
			cin >> len >> time;
			cout << Division(len, time) << endl;
		}
		catch (...)
		{
			cout << "delete []" << array << endl;
			delete[] array;
			throw;
		}
		// ...
		cout << "delete []" << array << endl;
		delete[] array;
	}
	int main()
	{
		try
		{
			Func();
		}
		catch (const char* errmsg)
		{
			cout << errmsg << endl;
		}
		return 0;
	}

我们可以看到,在上面这份代码中我们在throw与try中间还new了一个对象,但是我们因为异常的出现,直接跳转到了catch那里,new过后的对象没有进行delete,所以我们需要再次抛出异常,以避免内存泄漏

异常安全

构造函数完成对象的构造和初始化 最好不要 在构造函数中抛出异常,否则 可能导致对象不完整或没有 完全初始化
析构函数主要完成资源的清理 最好不要 在析构函数内抛出异常,否则 可能导致资源泄漏 ( 内存泄漏、句柄未关闭等)
C++ 中异常经常会导致资源泄漏的问题,比如在 new delete 中抛出了异常,导致内存泄漏,在 lock 和unlock之间抛出了异常导致死锁, C++ 经常使用 RAII 来解决以上问题

异常规范

1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型 ) ,列出这个函数可能抛掷的所有异常类型。
2. 函数的后面接 throw() ,表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 异常可能会导致异常安全问题
//new / fopen / lock
//
//func();  // 如果抛异常就会有异常安全问题 -> 捕获重新抛出 or RAII 解决
//
//delete/fclose/ unlock

// 函数规范一下,如果要抛异常,你说明清楚,不抛异常也说明一下
// 但是现实中,很多人不遵守规范

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
// void fun() throw(A, B, C, D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);

// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
void* operator delete (std::size_t size, void* ptr) noexcept;

自定义异常体系

实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了

 下面我们来看一个异常的实例

// 公司会给一个基类
// 要求你可以自己抛自己定义的异常,但是必须继承这个基类
// 这样的话,外层捕获就只需要捕获基类就可以
class Exception//公司给的基类
{
public:
	Exception(const char* errmsg, int errid)//初始化列表
		: _errmsg(errmsg)
		,_errid(errid)
	{}

	virtual string what() = 0;//定义虚函数,继承
protected:
	int _errid;       // 错误码
	string _errmsg;   // 错误描述
	//stack<string> _st; // 调用栈帧
};

class SqlException : public Exception//数据库错误子类
{
public:
	SqlException(const char* errmsg, int errid)
		:Exception(errmsg, errid)
	{}

	virtual string what()
	{
		return "数据库错误:" + _errmsg;
	}
};

class NetworkException : public Exception//网络错误子类
{
public:
	NetworkException(const char* errmsg, int errid)
		:Exception(errmsg, errid)
	{}

	virtual string what()
	{
		return "网络错误:" + _errmsg;
	}
};

void ServerStart()
{
	// 模拟一下出现问题抛异常报错
	if (rand() % 11 == 0)
		throw SqlException("数据库启动失败", 1);

	if (rand() % 7 == 0)
		throw NetworkException("网络连接失败", 3);

	cout << "正常运行" << endl;
}

int main()
{
	/*for (size_t i = 0; i < 100; i++)//检测100个随机数
	{
		try
		{
			ServerStart();//若出现错误
		}
		catch (Exception& e)
		{
			cout << e.what() << endl;//抛出对应的异常
		}
		catch (...)//未检测出但仍接收到了异常
		{
			cout << "未知异常" << endl;
		}
	}*/

	try{
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		v.reserve(1000000000);

		// 这里越界会抛异常
		v.at(10) = 100;
	}
	catch (const exception& e) // 这里捕获父类对象就可以,会自动匹配到子类
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}


	return 0;
}

C++标准库的异常体系

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

 异常的优缺点

  优点:
 1、清晰的包含错误信息
 2、面对T operator[](int i)这样函数越界错误,异常可以很好的解决
 3、多层调用时,里面发生错误,不再需要层层处理,最外层直接捕获即可
 4、很多第三方库都是用异常,我们也使用异常可以更好的使用他们。比如:boost、gtest、gmock

 缺点:
 1、异常会导致执行流乱跳。会给我调试分析程序bug带来一些困难。
 2、C++没有GC,异常可能到导致资源泄露等异常安全问题,需要学会使用RAII来解决。
 3、C++的库里面的异常体系定义不太好用,很多公司都会选择自己定义。
 4、C++的异常语言可以抛任意类型的异常,如果项目中没有做很好规范管理,那么会非常的混乱,所以一般需要定义出继承体系的异常规范。

总的来说,异常还是利大于弊的,小项目可能用不到,大公司一般还是会选择用异常来处理错误的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值