欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > C++——异常

C++——异常

2025/5/17 21:35:07 来源:https://blog.csdn.net/XLZ_44847/article/details/147342458  浏览:    关键词: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:用于捕获和指定的对象类型相同的异常。

double Div(int a, int b)
{int ret = 0;//throw 可以抛出一个异常,这个异常实际上是一个对象//后续会根据这个对象的类型决定匹配的catchif (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捕捉不支持隐式类型转换,但是对继承关系的基类和派生类之间的对象是存在例外的。允许抛出的派生类对象,而使用基类捕获

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

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则可以通过重新抛出将异常传递给更上层的函数进行处理。

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

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函数代码。

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语句块。

void fun()
{int* array1 = nullptr;int* array2 = nullptr;try {array1 = new int[10];try {array2 = new int[10];} catch (...) {// 如果array2分配失败,清理array1delete[] 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引入的智能指针。智能指针实际上就是为指针包装了一个类的壳子,这样指针就变为了一个局部对象,那么在函数栈帧销毁时自动调用析构函数,从而完成资源的自动释放。

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即可。如果不说明则表示可能会抛异常。

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

fun1() noexcept;//表示不会抛异常
fun2();//表示可能会抛异常

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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词