【C++】异常之道,行者无疆:解锁 C++ 的异常捕获哲学

发布于:2024-12-08 ⋅ 阅读:(144) ⋅ 点赞:(0)

C语言处理错误

  • 终止程序:利用 assert() 断言去终止程序,当 ()的表达结果为 false 时会终止程序。
  • 返回错误码:手动查找对应的错误,系统的接口函数将作错误码放到 errno 中表示错误。

C语言中的 strerror 将参数对应 errno 的错误信息的字符串返回。errno 是一个全局变量,当使用标准库的函数发生错误时,就会将对应的的错误码放到 errno 中,每个错误码对应着不同的错误信息,strerror 就可以将错误码对应的字符串返回。
以下为错误码 0~10 对应的信息:

#include <iostream>
#include <errno.h>
using namespace std;

int main()
{
	for (size_t i = 0; i <= 10; ++i)
		cout << i << ":" << strerror(i) << endl;
	return 0;
}

在这里插入图片描述

C++异常

C++的异常处理是一种应对程序运行时错误的机制,允许在程序中独立开发的部分在运行时就出现的错误进行通信并作出相应的处理,使得将问题的检测与解决问题的过程分开,程序的一部分检测问题的出现,然乎将解决任务传递给程序的一部分。

总的来说,异常能够提供一种结构化的方法来捕获和处理错误,从而提高代码的健壮性和可维护性。

异常的抛出与捕获

基本语法

C++的常处理依赖于三个关键字:

  • try:用于定义可能抛出异常的代码块。
  • throw:用于抛出异常对象。
  • catch:用于捕获异常,并定义处理异常的逻辑。

使用实例

void testException(int x)
{
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
}

int main()
{
	try {
		testException(0);
	}
	catch (const char* e)
	{
		cout << "Caught an exception: " << e << endl;
	}

	return 0;
}

catch 的匹配原则

1.异常的类型

C++支持抛出任意类型的对象作为异常,例如:

  • 基本类型:如 intcharfloat
  • 标准库类型:如 string
  • 自定义类型:通过类和结构体定义的类型。

示例:

throw 42;                // 抛出整数
throw std::string("Error"); // 抛出字符串
throw std::runtime_error("Runtime error"); // 抛出标准库异常类

在这里插入图片描述

2.标准异常类

C++标准库还提供了一组异常类,位于 exception 头文件中。常见的标准异常类包括:

  • exception:所有标准异常的基类。
  • runtime_error:运行时错误。
  • logic_error:逻辑错误,如非法参数、越界访问等。
  • 其他派生类:如 out_of_rangeinvalid_argument

使用标准异常类:

int main()
{
	try {
		throw runtime_error("Runtime exception occurred!");
	}
	catch (const exception& e)
	{
		cout << "Caught standard exception: " << e.what() << endl;
	}

	return 0;
}

3.匹配原则

  • 异常抛出的对象的类型决定了应该匹配哪个 catch 的处理代码,该过程不会有隐式类型转换
void testException(int x)
{
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
}

int main()
{
	try {
		testException(0);
	}
	catch(const string& e)	//不会有隐式类型转换,不会匹配string的catch
	{
		cout << "Caught an exception(string): " << e << endl;
	}
	catch (const char* e)
	{
		cout << "Caught an exception: " << e << endl;
	}

	return 0;
}

在这里插入图片描述

  • 如果有多个类型匹配的 catch 的版本,则会去调用离抛出异常位置最近catch
void testException(int x)
{
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
}

void test1()
{
	try {
		testException(0);
	}
	catch (const char* e)	
	{
		cout << "(most near) Caught an exception: " << e << endl;
	}
}

int main()
{
	try {
		test1();
	}
	catch (const char* e)
	{
		cout << "Caught an exception: " << e << endl;
	}
	
	return 0;
}

在这里插入图片描述

  • throw 抛出异常对象后,会生成异常对象的拷贝,因为抛出的异常对象可能是临时对象。这个拷贝的临时异常对象会在被 catch 后销毁(类似于函数的传值返回)

  • catch(...) 可以捕获任意类型的异常对象,但不知道具体的异常错误。

int main()
{
	try {
		throw runtime_error("Runtime exception occurred!");
	}
	catch (...)
	{
		cout << "Unkonwn exception!" << endl;
	}

	return 0;
}

在这里插入图片描述

  • 实际上,抛出的异常对象类型实际上也不需要完全匹配,比如:可以抛出派生类对象用基类捕获。

函数调用链中的匹配原则

  1. 当异常在 try 代码块中 throw 抛出时,它会沿函数调用链向上传播,直到找到匹配的 catch 代码块而且此过程中,throw 后面的代码不再执行。

  2. 匹配到对应异常对象的类型的 catch 代码块后,沿着函数调用链销毁沿途的对象。

  3. 如果最后在 main 函数中没有匹配的 catch,程序会调用 terminate 函数,通常导致程序终止。

  4. 一般为了避免这种情况,需要用 catch(...) 防止异常没有捕获导致程序崩溃。
    在这里插入图片描述
    具体例子

void testException(int x)
{
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
}

void test1()
{
	try {
		testException(0);
	}
	catch (const string& e)	
	{
		cout << "(string) Caught an exception: " << e << endl;
	}
}

int main()
{
	try {
		test1();
	}
	catch (const char* e)
	{
		cout << "Caught an exception: " << e << endl;
	}
	catch (...)		//如果抛出的异常未经处理,程序会被terminate终止
	{
		cout << "Unkonwn exception!" << endl;
	}
	
	return 0;
}

在这里插入图片描述

异常的重新抛出

catch 代码块中,可以通过 throw 关键字将异常重新抛出,供更高层次的代码处理。

void testException(int x)
{
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
}

void test1()
{
	try {
		testException(0);
	}
	catch (const char* e)
	{
		cout << "(Inner catch) Caught an exception: " << e << endl;
		//将捕获到的异常重新抛出
		throw;
	}
}

int main()
{
	try {
		test1();
	}
	catch (const char* e)
	{
		cout << "(Outer catch) Caught an exception: " << e << endl;
	}
	catch (...)		//如果抛出的异常未经处理,程序会被terminate终止
	{
		cout << "Unkonwn exception!" << endl;
	}
	
	return 0;
}

在这里插入图片描述

异常安全

  • 构造函数最好不要抛异常,因为可能导致对象不完整初始化甚至完全没有初始化。
  • 析构函数也最好不要抛异常,否则可能造成资源泄漏。
void  testException(int x)
{
	int* array = new int[10];
	if (x == 0)
		throw "Division by zero error!"; //抛出 const char* 类型的异常

	cout << "x:" << x << endl;
	delete[] array;
}

int main()
{
	try
	{
		testException(0);
	}
	catch (const char* e)
	{
		cout << "Caught an exception: " << e << endl;
	}
	
	return 0;
}

这里由于 throw 抛异常提前退出了 testException 函数导致执行 delete[] array 导致了资源泄漏,这是非常危险的,一定要避免。

异常规范

  • 在函数的后面接 throw(type) ,表示这个函数可能抛出的所有的异常类型。
  • 函数后面接 throw() 表示函数不抛异常,在C++11中新增关键字 noexcept ,表示该函数不抛异常。
  • noexcept 会影响异常的捕获,确认函数不会加才使用。
// 这里表示这个函数会抛出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();

// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

C++标准库异常

C++ 标准库中提供了一组异常类,用于支持异常处理机制。这些异常类都继承自 exception,提供了统一的接口以便捕获和处理各种类型的异常。

核心异常类
exception 是描述标准库异常的基类,其接口为 virtual const char* what() const noexcept ,返回异常的描述信息。

  • bad_alloc :表示内存分配失败的异常,通常由 new 操作符抛出。
  • bad_cast :表示动态类型转换(dynamic_cast)失败的异常。
  • bad_typeid :在对空指针调用 typeid 时抛出。
  • bad_exception :如果异常对象在 throw 时不匹配声明的异常类型,可能会抛出此异常。

逻辑错误异常
logic_error 是一个逻辑错误的基类,表示程序中的逻辑问题,通常在运行时能够检测到。

  • domain_error :表示函数参数超出定义域的异常,比如尝试对负数求平方根。
  • invalid_argument :表示无效参数引发的异常,比如传递非法格式的字符串。
  • length_error :表示试图创建超出容器最大长度的对象,比如向 vector 添加过多元素。
  • out_of_range :表示访问容器中不存在的元素时抛出的异常,比如使用越界的索引访问 vector

运行时错误异常
runtime_error 是运行时错误的基类,表示在程序运行过程中发生的错误。

  • range_error :表示计算结果超出表示范围的异常。
  • overflow_error :表示算术运算发生上溢的异常。
  • underflow_error :表示算术运算发生下溢的异常。

其他异常

  • ios_base::failure :表示与输入/输出流相关的错误,比如文件读取失败。

拜拜,下期再见😏

摸鱼ing😴✨🎞
请添加图片描述


网站公告

今日签到

点亮在社区的每一天
去签到