C++——异常

1. C语言错误处理机制

我们在曾经介绍过C语言下的错误码。错误码我们过去经常见到,错误码通常是指errno变量中的值,它表示特定操作(如系统调用或库函数)发生错误的原因。errno是一个全局变量,当出现错误时会自动将错误码存储在errno中,不同的值代表着不同的错误信息。我们可以通过perror和strerror来查看错误信息。

perror:void perror(const char *s); 打印输入的参数字符串+此时errno对应的错误信息

strerror:char *strerror(int errnum); 打印指定错误码(传入参数)的错误信息

对于退出码,它是在进程结束后返回的一个退出状态信息,表示程序的执行结果,一般约定0为成功,而非0为出现错误,至于退出码和原因的对应关系没有固定的要求。

除了错误码之外,C语言下的终止程序也是一种处理方案,比如assert断言、内存错误等,只不过这种处理方式十分粗暴。

C++采取异常的方式处理错误,当一个函数发现错误后,可以选择抛出一个异常,这个异常就会顺着调用链传递,找到能够处理这个异常的模块。

2. 异常的使用方法

为了使用C++的异常,引入三个关键字:

throw:它的作用是抛出异常。

try:在try语句块中的代码会被"监视",判断其中的代码在执行中是否发生了异常。如果没有异常发生,那么相安无事。一旦发生了异常,则会停止块中的代码执行,转而搜索catch块,寻找匹配的catch来处理异常。

catch:用于捕获和指定的对象类型相同的异常。

cpp 复制代码
double Div(int a, int b)
{
	int ret = 0;
	//throw 可以抛出一个异常,这个异常实际上是一个对象
	//后续会根据这个对象的类型决定匹配的catch
	if (b == 0) throw "Devision by zero"; //抛出一个const chat*类型的异常
	//首先检查throw是否在try块的内部
	// 在try中:查找接收的类型和异常对象类型匹配catch
	// 不在try中或在try中却没有匹配的catch:直接结束当前函数,返回上一层函数中继续判断是否位于try中并进行catch匹配
	else ret = double(a) / double(b);
	
	cout << "div return" << endl;
	return ret;
}
void fun()
{
	int a, b;
	cin >> a >> b;
	cout << Div(a, b) << endl;

	cout << "fun return" << endl;
}

//输入1 0:
// Div函数中:if分支为真,抛出一个const char*的异常。由于throw不在try块内,所以直接结束当前函数,返回到fun函数中捕获异常
// fun函数中:抛出异常导致退出Div栈帧后返回fun函数的调用Div函数处,同样不在try内,直接结束当前函数,返回到main函数中捕获异常
// main函数中:对于fun的调用在try中,因此进行catch匹配,并且成功匹配到了const char*类型,进入处理异常,异常处理结束后继续main函数的执行
int main()
{
	try
	{
		fun();
	}
	catch (const int num) //匹配int型异常对象
	{
		cout << num << endl;
	}
	catch (const char* message) //匹配const char*型异常对象
	{
		cout << message << endl;
	}
	//catch匹配成功则进入处理异常,并且在处理完异常后继续后续代码执行
	//main函数仍未成功匹配,则终止程序
	catch (...)//可以捕获任意类型的异常
	{
		cout << "unknown exception" << endl;
	}
	
	cout << "main return" << endl;

	//和函数的返回值一样,异常对象作为函数的局部对象想要被调用链其他函数捕获,就需要对其拷贝构造来传递
	//当然在C++11中,这一步拷贝构造传递完全可以由移动构造来完成

	return 0;
}

在这个例子中,如果输入1 0:

Div函数中:if分支为真,抛出一个const char*的异常。由于throw不在try块内,所以直接结束当前函数,返回到fun函数中捕获异常;

fun函数中:抛出异常导致退出Div栈帧后返回fun函数的调用Div函数处,同样不在try内,直接结束当前函数,返回到main函数中捕获异常;

main函数中:对于fun的调用在try中,因此进行catch匹配,并且成功匹配到了const char*类型,进入处理异常,异常处理结束后继续main函数的执行。

使用要点

①throw用于可以抛出一个异常,这个异常实际上是一个具体的对象(如int类型对象、string类型对象等),后续的catch会根据这个对象的类型决定匹配的catch。

②抛出的异常如果在当前函数无法处理(包括没有位于try块中,或没有匹配的catch),那么异常对象会沿着函数的调用链向上传递。即直接结束当前函数,将异常带到调用这个函数的地方去找处理方案。

具体来说,throw抛出异常后如果在try中,则在后续的catch中查找接收的类型和异常对象类型匹配catch,如果找到了匹配的catch,则根据catch块的代码处理异常。

如果throw抛出时不在try中,或在try中却没有匹配的catch,那么则直接结束当前函数,返回上一层函数中继续判断是否位于try中并进行catch匹配。

③异常匹配catch会选择类型严格匹配距离最近的,即异常对象的匹配不支持隐式类型转换,并且如果函数a调用函数b,函数b调用函数c,c抛出的异常在b和a中都可以被捕捉,但是会优先被b捕捉处理,所以不再会去a中试图catch。

④如果catch匹配成功则进入处理异常,并且在处理完异常后继续本函数后续代码执行。后续代码即为try-catch语句块之后的代码。

⑤如果直到main函数,异常仍未成功匹配,则终止程序并报错。

catch (...)可以捕获任意类型的异常

⑦和函数的返回值一样,异常对象也需要在函数之间传递。异常对象时函数的局部对象,如果想要被调用链其他函数捕获,就需要对其拷贝构造来传递,这和函数返回值一模一样。当然在C++11中,这一步拷贝构造传递完全可以由移动构造来完成。

⑧虽然异常对象的catch捕捉不支持隐式类型转换,但是对继承关系的基类和派生类之间的对象是存在例外的。允许抛出的派生类对象,而使用基类捕获

以下给出一个基类捕获派生类异常的例子,实际上这也是异常体系的基本定义方式。

cpp 复制代码
class Exception
{
public:
	Exception(const string& errormessage, int id)
		:_ErrorMessage(errormessage)
		, _id(id)
	{}

	virtual string show() const //虚函数用于实现多态
	{
		return _ErrorMessage;
	}
	
protected:
	string _ErrorMessage; //错误信息描述
	int _id; //错误id
};

class SqlException :public Exception
{
public:
	SqlException(const string& errormessage, int id, const string& sql)
		:Exception(errormessage,id) //构造基类部分
		,_sql(sql)
	{}

	virtual string show() const //重写
	{
		string ret = "SqlException:";
		ret += _ErrorMessage;
		ret += "->";
		ret += _sql;
		return ret;
	}
private:
	const string _sql;
};

class HttpException :public Exception
{
public:
	HttpException(const string& errormessage, int id, const string& http)
		:Exception(errormessage,id) //构造基类部分
		,_http(http)
	{}

	virtual string show() const //重写
	{
		string ret = "HttpException:";
		ret += _ErrorMessage;
		ret += "->";
		ret += _http;
		return ret;
	}
private:
	const string _http;
};

class CacheException :public Exception
{
public:
	CacheException(const string& errormessage, int id)
		:Exception(errormessage,id) //构造基类部分
	{}

	virtual string show() const //重写
	{
		string ret = "CacheException:";
		ret += _ErrorMessage;
		return ret;
	}
};

void SqlServe()
{
	srand(time(0));
	if (rand() % 11 == 0)
	{
		throw SqlException("权限不足", 101, "select * from student where name=\'张三\'");
	}
	cout << "success" << endl;
}
void CacheServe()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw CacheException("权限不足", 111);
	}
	if (rand() % 5 == 0)
	{
		throw CacheException("数据缺失", 112);
	}
	SqlServe();
}
void HttpServe()
{
	srand(time(0));
	if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 131, "get");
	}
	if (rand() % 3 == 0)
	{
		throw HttpException("资源缺失", 132, "post");
	}
	CacheServe();
}

int main()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try
		{
			HttpServe();
		}
		catch (const Exception& ex) //只捕获父类
		{
			cout << ex.show() << endl; //多态
		}
		catch (...)
		{
			cout << "unknown exception" << endl;
		}
	}
	return 0;
}

3. 异常的注意事项

3.1 异常重新抛出

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

异常在接收后重新抛出对于有资源申请的函数模块有很大意义。在某些情况下,并不需要在本函数处理异常,但是由于本函数申请了资源,出现异常直接结束函数会导致资源泄露。所以出现异常就需要捕获,成功捕获后释放资源,再将异常重新抛出。

cpp 复制代码
double Div(int a, int b)
{
	int ret = 0;
	if (b == 0) throw "Devision by zero"; 
	else ret = double(a) / double(b);
	return ret;
}
void fun()
{
	int* array = new int[10];

	try {
		int a, b;
		cin >> a>> b;
		cout << Div(a, b) << endl;
	}
	catch (const int num)
	{
		throw num;//重新抛出num异常对象
	}
	catch (...)
	{
		//某些情况并不需要在本函数处理异常,但是出现异常直接结束函数会导致资源泄露
		//所以出现异常就需要捕获,成功捕获后释放资源,再将异常重新抛出
		cout << "delete []" << array << endl;
		delete[] array;
		throw; //异常的重新抛出,接收什么抛出什么
	}
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		fun();
	}
	catch (const char* message)
	{
		cout << message << endl;
	}
	catch (...)
	{
		cout << "unknown exception" << endl;
	}
	
	return 0;
}

3.2 new异常的处理

考虑以下修改后的fun函数代码。

cpp 复制代码
void fun()
{
	int* array1 = new int[10];
	int* array2 = new int[10];

	try {
		int a, b;
		cin >> a>> b;
		cout << Div(a, b) << endl;
	}
	catch (...)
	{
		delete[] array1;
        delete[] array2;
		throw;
	}
	delete[] array1;
	delete[] array2;
}

new也是会抛异常的,当array1申请资源失败后,需要直接抛异常;当array2申请资源失败后则需要先释放array1的资源,然后再抛异常。会发现仅仅对于两个连续的new,如果我们完善的考虑异常的问题,需要不小的try-catch语句块。

cpp 复制代码
void fun()
{
    int* array1 = nullptr;
    int* array2 = nullptr;
    
    try 
    {
        array1 = new int[10];
        try 
        {
            array2 = new int[10];
        } 
        catch (...) 
        {
            // 如果array2分配失败,清理array1
            delete[] array1;
            throw;
        }
    }
    catch (...) 
    {
        // array1分配失败
        throw;
    }

	try 
    {
		int a, b;
		cin >> a>> b;
		cout << Div(a, b) << endl;
	}
	catch (...)
	{
		delete[] array1;
        delete[] array2;
		throw;
    }
	delete[] array1;
    delete[] array2;

}

对于这种问题,最好的解决方案是C++11引入的智能指针。智能指针实际上就是为指针包装了一个类的壳子,这样指针就变为了一个局部对象,那么在函数栈帧销毁时自动调用析构函数,从而完成资源的自动释放。

cpp 复制代码
template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete(_ptr);
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator*()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

/
//fun()函数内:

	A* a1 = new A;
	A* a2 = new A;

    //①C++库的智能指针
	unique_ptr<A>  a1 = new A;
	unique_ptr<A>  a2 = new A;
    //②自己包装的简单的智能指针
	SmartPtr<A> p1=(new A);
	SmartPtr<A> p2=(new A);

3.3 异常的其他要点

①构造函数、析构函数完成的是对象的初始化和资源清理过程,所以尽量避免在构造函数和析构函数中抛异常,防止对象未完全初始化和资源泄露的问题。

②在C++98中,规定了异常的规格说明:

如果函数内会出现异常,则需要在函数后使用throw(异常类型)来列出所有可能的异常类型。

如果函数内不会出现异常,则使用throw()。

但是这套异常规格说明自C++11起被noexcept取代,C++17 移除动态异常规范(只保留 throw() 作为 noexcept 的旧式写法)。

所以在C++11下如果函数不会抛异常,则使用noexcept即可。如果不说明则表示可能会抛异常。

③异常可以方便我们更加清晰地展示错误信息,更加准确的定位错误位置,更加方便的完成错误处理。但是其也使得代码执行顺序大幅度跳转,并且非常容易引起内存泄漏和死锁等问题,需要我们多加注意。

cpp 复制代码
fun1() noexcept;//表示不会抛异常
fun2();//表示可能会抛异常

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

相关推荐
ghost1436 分钟前
C#学习第17天:序列化和反序列化
开发语言·学习·c#
愚润求学9 分钟前
【数据结构】红黑树
数据结构·c++·笔记
難釋懷41 分钟前
bash的特性-bash中的引号
开发语言·chrome·bash
Hello eveybody2 小时前
C++按位与(&)、按位或(|)和按位异或(^)
开发语言·c++
6v6-博客2 小时前
2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?
java·开发语言·php
Baoing_2 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
被AI抢饭碗的人2 小时前
c++:c++中的输入输出(二)
开发语言·c++
lqqjuly2 小时前
C++ 面向对象关键语法详解:override、虚函数、转发调用和数组引用传参-策略模式
开发语言·c++
EstrangedZ2 小时前
vcpkg缓存问题研究
c语言·c++·缓存·cmake·vcpkg
喵~来学编程啦2 小时前
【模块化编程】Python文件路径检查、跳转模块
开发语言·python