C++之异常

目录

一、异常的概念及使用

1.1、异常的概念

1.2、异常的抛出和捕获

1.3、栈展开

1.4、查找匹配的处理代码

1.5、异常重新抛出

1.6、异常安全问题

1.7、异常规范

1.8、C++异常的优缺点

二、标准库的异常


一、异常的概念及使用

1.1、异常的概念

  • 异常处理机制允许程序中独⽴开发的部分能够在运⾏时对于出现的问题进⾏通信并做出相应的处理, 异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节。
  • C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以后还要去查询错误信息,⽐较麻烦。C++中,通过throw抛出一个异常对象,该对象可以携带错误信息(如错误描述,错误码等),用于在异常处理流程中传递详细的错误上下文。

C语言异常示例:

1.2、异常的抛出和捕获

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接调用者处理这个错误。

  • **throw:**当问题出现时,程序会抛出一个异常。这是通过使用throw关键字来完成的。
  • **catch:**在想要处理问题的地方,通过异常处理程序捕获异常,catch关键字用来捕获异常,可以有多个catch进行捕获。
  • **try:**try块中的代码可能会抛出异常,其后通常跟着一个或者多个catch块用于捕获并处理这些异常。

异常的抛出和匹配原则:

  • 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
  • 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
  • catch(...)可以捕获任意类型的异常,问题是不知道异常的错误是什么。
  • 异常的匹配执行的是严格匹配,不会进行隐式类型转换,例如抛出int类型的异常,用size_t类型接收该异常会因为类型不匹配而接收不到这个异常,但是实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出派生类对象,使用基类捕获,这个在实际中非常实用。
  • 当throw执⾏时,throw后⾯的语句将不再被执⾏。程序的执⾏从throw位置跳到与之匹配的catch 模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,控制权从throw位置转移到了catch位置。这⾥还有两个重要的含义:1、沿着调⽤链的函数可能提早退出。2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。

示例代码一:

cpp 复制代码
double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		//可以抛任意类型异常
		throw "Division by zero condition!";
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	int len, time;
	cin >> len >> time;

	try
	{
		cout << Division(len, time) << endl;
	}
	catch (size_t x) 
	{
		cout << x << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}

	cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "unkown exception" << endl;
	}

	return 0;
}

结果:

**解释:**上面代码在Division函数中抛出一个异常,类型是字符串(char*),⾸先检查throw本⾝是否在try块内部,这里检查发现没有,继续向上层调用的函数(Func)查找,上层中调用Division时被try块包裹,查找对应的catch,如果找到执行,如果没有继续向上层查找,这里找到了,所以直接执行catch中的代码,且因为异常跳转到该函数,执行完catch块中的代码后,该函数中catch块下面的代码也会执行,调用Func的上层函数main函数也有匹配的catch代码块,但是没有Func中的近(一级一级向上查找,先遇到哪个合适就执行哪个),所以main函数中的catch块没有执行。异常一旦被某一个catch代码块捕获,就不会再去找其他的catch代码块了。

示例代码二:

cpp 复制代码
double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		string s("Division by zero condition!");
		throw s;
	}
	else
	{
		return ((double)a / (double)b);
	}
}

void Func()
{
	int len, time;
	cin >> len >> time;

	try
	{
		cout << Division(len, time) << endl;
	}
	catch (size_t x)
	{
		cout << x << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}

	cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}

	return 0;
}

结果:

**解释:**上面代码中Division抛出了一个异常,但是一直找到main函数都没有符合的catch代码块,所以程序结束并报错。

示例代码三:

cpp 复制代码
int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const string& errmsg)
		{
			cout << errmsg << endl;
		}
		catch (...)
		{
			cout << "unkown exception" << endl;
		}
	}

	return 0;
}

**解释:**为了防止因为一些没有对应catch代码块的异常导致的程序崩溃,可以在最后通过catch(...)来接收任意类型的异常。

示例代码四:

cpp 复制代码
// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

	int getid() const
	{
		return _id;
	}

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;
};

void SQLMgr()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}

	cout << "调用成功" << endl;
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}

void seedmsg(const string& s)
{
	//cout << "void seedmsg(const string& s)" << endl;

	//throw HttpServerException("网络不稳定,发送失败", 102, "put");

	if (rand() % 2 == 0)
	{
		throw HttpServerException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 3 == 0)
	{
		throw HttpServerException("你已经不是对象的好友,发送失败", 103, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

void HttpServer()
{
	/*if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}*/

	// 失败以后,再重试3次
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			seedmsg("今天一起看电影吧");
			break;
		}
		catch (const Exception& e)
		{
			if (e.getid() == 102)
			{
				if (i == 3)
					throw e;

				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				throw e;
			}
		}
	}

	//CacheMgr();
}

int main()
{
	srand(time(0));

	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try 
		{
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			using std::chrono::system_clock;
			// 多态
			system_clock::time_point today = system_clock::now();
			std::time_t tt = system_clock::to_time_t(today);
			cout << ctime(&tt) << e.what() << endl << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}

	return 0;
}

**解释:**实际场景中,通常会定义一个异常的基类,然后对应不同的情况创建不同的子类并继承基类,这样抛出异常时可以根据需要抛出不同的异常,并使用基类来统一接收。

注意:异常可以抛任意类型,异常必须在try-catch语句中被捕获,如果一个异常被抛出,但没在try-catch语句中被捕获,程序将会报错。

1.3、栈展开

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

  • 抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。
  • 如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。
  • 如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  • 如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。

1.4、查找匹配的处理代码

  • ⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
  • 但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的。
  • 如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

1.5、异常重新抛出

有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。

示例代码:

cpp 复制代码
double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}

void Func()
{
	// 这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
	// 重新抛出去。
	try 
	{
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "继续向上抛出异常" << endl;

		throw; // 异常重新抛出,捕获到什么抛出什么
	}

	// ...

}

1.6、异常安全问题

  • 异常抛出后,后⾯的代码就不再执⾏,前⾯申请了资源(内存、锁等),后⾯进⾏释放,但是中间可能会抛异常就会导致资源没有释放,这⾥由于异常就引发了资源泄漏,产⽣安全性的问题。中间我们需要捕获异常,释放资源后⾯再重新抛出,当然后⾯智能指针章节讲的RAII⽅式解决这种问题是更好的。
  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  • 其次析构函数中,如果抛出异常也要谨慎处理,⽐如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后⾯的5个资源就没释放,也资源泄漏了。《Effctive C++》第8个条款也专⻔讲了这个问题,别让异常逃离析构函数。

1.7、异常规范

  • 对于用户和编译器而言,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
  • C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1, 类型2...)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。
  • C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加 noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。
  • 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序。
  • noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回 false,不会就返回true。

1.8、C++异常的优缺点

C++异常的优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助我们更好的定位程序的bug。返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。
  2. 很多第三方库都包含异常,比如boost,gtest,gmock等等常用的库,那么我们使用它们也需要使用异常。
  3. 部分函数使用异常更好处理,比如T& operator这样的函数,如果访问越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

T& operator[](int i)

{

if(i >= _size)

throw out_of_range("越界");

return _a[i];

}

C++异常的缺点:

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销,不过在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了一场非常容易造成内存泄漏,死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。
  4. C++标准库的异常体系定义的不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言,所有异常规范有两点:一、抛出异常的类型都继承自一个基类。二、函数是否抛异常,抛什么异常,都使用func() throw();或noexcept的方式规范化。

二、标准库的异常

官网:exception - C++ Reference
C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。

相关推荐
jerry60910 分钟前
c++流对象
开发语言·c++·算法
fmdpenny11 分钟前
用python写一个相机选型的简易程序
开发语言·python·数码相机
虾球xz15 分钟前
游戏引擎学习第247天:简化DEBUG_VALUE
c++·学习·游戏引擎
崔高杰35 分钟前
On the Biology of a Large Language Model——Claude团队的模型理解文章【论文阅读笔记】其一CLT与LLM知识推理
论文阅读·人工智能·笔记·语言模型·自然语言处理
樂50236 分钟前
关于 Web 服务器的五个案例
linux·服务器·经验分享
海盗强1 小时前
Babel、core-js、Loader之间的关系和作用全解析
开发语言·前端·javascript
猿榜编程1 小时前
python基础-requests结合AI实现自动化数据抓取
开发语言·python·自动化
0509151 小时前
测试基础笔记第十四天
笔记
我最厉害。,。1 小时前
PHP 反序列化&原生类 TIPS&字符串逃逸&CVE 绕过漏洞&属性类型特征
android·开发语言·php
爱编程的鱼1 小时前
C# 类(Class)教程
开发语言·c#