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

『 C++ 』异常

2025/8/8 4:59:54 来源:https://blog.csdn.net/2202_75303754/article/details/140938768  浏览:    关键词:『 C++ 』异常

文章目录

    • 异常概念及使用
    • 自定义类型的异常
    • C++ 标准库的异常体系
    • 异常的重新抛出
    • 异常安全
    • 异常规范
    • 异常的优缺点


异常概念及使用

请添加图片描述

C语言常见的错误处理机制如下:

  • 返回值约定

    通过定义一些列的返回值以及其对应的错误信息表述,通过不同的返回值来查看当前函数是否与调用成功;

    通常情况下开发者需要检查返回值来判断是否发生错误;

    int main() {FILE* fp = fopen("log.txt", "r");if (fp == NULL) { // 返回值约定 如果返回空指针则表示打开文件错误cout << "fopen fail" << endl;}return 0;
    }
    /*输出结果为:$ ./test fopen fail
    */
    
  • errno变量

    C语言提供了一个全局变量errno,用于存储最近一次函数调用发生的错误代码;

    开发者可检查errno的值来获取更多的错误信息;

    int main() {FILE* fp = fopen("log.txt", "r");if (fp == NULL) {cout << strerror(errno) << endl;  // 使用strerror打印errno对应的错误信息}return 0;
    }
    /*输出结果为:$ ./test No such file or directory
    */
    
  • perror()函数

    这个函数可根据errno的值打印出一条错误信息以定位对应的错误;

    int main() {FILE* fp = fopen("log.txt", "r");if (fp == NULL) {perror("fopen fail\n"); // 使用perror打印最近的错误信息}return 0;
    }/*输出结果为:$ ./test fopen fail: No such file or directory*/
    
  • assert()

    assert()将直接终止程序,如内存错误,除零错误时;

异常是C++引入的一个错误处理机制,使用try-catch块来捕获和处理异常;

当函数抛出异常时控制流会转移到相应的catch块中处理;

通常使用throw抛异常,并使用try-catch来处理异常;

  • throw

    问题出现时,程序将使用throw抛一个异常;

    可抛出任意类型的异常,无论是内置类型还是自定义类型;

  • catch

    该关键字用于捕获异常,可以有多个catch进行捕获;

  • try

    try块中的代码标识将被激活特定异常,try块后可跟一个或是多个catch;

double Div(double num1, double num2) {double ret = 0;if (num2 == 0) {throw "Division by zero error";  // throw 抛异常} else {ret = num1 / num2;}cout << "the process executed sucess" << endl; // 捕获异常时不执行return ret;
}int main() {try {  // 捕获异常cout << Div(10, 0) << endl;cout << "No exception was caught" << endl; // 捕获异常时不执行} catch (const char* erostr) {  // 处理异常cout << erostr << endl;}
}

throw可抛任意类型的异常,其中当异常被抛出后,出现异常的后续代码将不会执行,程序将逐层释放栈帧并直接跳转至catch异常处理部分;

这个例子运行结果为:

$ ./test 
Division by zero error

程序的运行是以一个执行链式的存在,即在内存中不停的压栈与出栈;

通常情况下捕获异常时程序将跳转到最近的最匹配的catch处对异常进行处理,此处的匹配指的是当一个异常被抛出时必须被一个catch以相同类型的方式进行接收,类似于函数的传参;

当异常处理结束后程序将继续向后执行(已释放栈帧的函数内容后续不再执行);

double Div(double num1, double num2) {double ret = 0;if (num2 == 0) {throw "Division by zero error";  // throw 抛异常} else {ret = num1 / num2;}cout << "the process executed sucess" << endl;  // 捕获异常时不执行return ret;
}void func() {try  {  // func 捕获异常cout << Div(10, 0) << endl;cout << "No exception was caught" << endl;  // 捕获异常时不执行} catch (const char* erostr) {                // func 处理异常cout << "func : " << erostr << endl;}
}int main() {try {  // main 捕获异常func();} catch (const char* erostr) {  // main处理异常cout << "main : " << erostr << endl;}
}

在这个例子中func()函数与main()函数依次尝试捕获异常,当异常被抛出后执行流将跳转至最近且最匹配的栈帧中的catch进行异常处理;

这个例子的执行结果为:

$ ./test 
func : Division by zero error # 异常处理在func中进行# 同时抛出的异常已经在 func 中被处理
# main 函数中的 catch 在这次中不会再捕获到异常

当某个函数抛出一个异常时必须被捕获,若是一个异常被抛出没有被捕获,或者是没有以约定形式捕获(抛出了一个int类型的异常,但唯一的catchdouble类型),程序将直接终止;

double Div(double num1, double num2) {double ret = 0;if (num2 == 0) {throw "Division by zero error";  // throw 抛异常} else {ret = num1 / num2;}cout << "the process executed sucess" << endl;  // 捕获异常时不执行return ret;
}int main() {try { cout << Div(10, 0) << endl;cout << "No exception was caught" << endl;  } catch (int erostr) {   // 未按约定捕获异常            cout << "main : " << erostr << endl;}
}/*运行结果为:./test terminate called after throwing an instance of 'char const*'Aborted程序异常终止;
*/

自定义类型的异常

请添加图片描述

异常的抛出通常不使用单纯的内置类型,而是抛出一个自定义类型的异常;

通常这个自定义类型的异常为一个基类异常的派生类对象;

当抛异常抛出一个基类的派生类时无论是怎样的派生类,最终都可以被该基类的指针或是引用进行接收;

同时可在基类中以virtual关键字修饰一个虚函数,使派生类对该虚函数进行重写;

当基类对象的指针或是引用接收到一个派生类对象时可直接以多态的形式去调用其对应类型的虚函数从而使开发者明白具体在项目中的哪个模块抛出了异常出现了错误;

#include <iostream>
#include <string>using namespace std;// 基类:abnormalBase
class abnormalBase {
public:// 构造函数,初始化错误ID和错误信息abnormalBase(int id, const string& err) : erid_(id), errstr_(err) {}// 虚函数,用于输出错误信息virtual void what() {printf("the error id : %d\nwhat: %s\n", erid_, errstr_.c_str());}// 虚析构函数,确保派生类的析构函数被调用virtual ~abnormalBase() {}protected:int erid_;      // 错误IDstring errstr_; // 错误信息
};// 派生类:abnormalDeriver
class abnormalDeriver : public abnormalBase {
public:// 构造函数,调用基类构造函数abnormalDeriver(int id, const string& er) : abnormalBase(id, er) {}// 重写基类的what()函数,输出派生类特定的错误信息void what() override {printf("Derived class error id: %d\nwhat: %s\n", erid_, errstr_.c_str());}
};// 除法函数
double Div(double num1, double num2) {double ret = 0;// 检查除数是否为零if (num2 == 0) {// 创建异常对象并抛出abnormalDeriver* erptr = new abnormalDeriver(1, "Division by zero error");throw erptr; // 抛出异常指针} else {// 进行除法运算ret = num1 / num2;}cout << "the process executed successfully" << endl; // 捕获异常时不执行return ret; // 返回结果
}// 函数func,调用Div函数
void func() { Div(10, 0); // 这里会抛出异常
}// 主函数
int main() {try {func(); // 调用func,可能会抛出异常} catch (abnormalBase* er) { // 捕获异常er->what(); // 调用异常对象的what()方法输出错误信息delete er; // 释放异常对象的内存}return 0; // 程序结束
}

在这个例子中定义了一个名为abnormalBase的基类,并写了一个名为what的虚函数;

其中将析构函数设定为虚函数是确保其派生类在析构时能够在析构派生类部分后再析构基类部分从而达到完全清理的功能;

该基类派生了一个abnormalDeriver的类,该类重写了基类的虚函数,使得能够在调用该函数时达到多态效果;

同样以除零错误为例,当出现除零错误时将用new实例化出一个异常对象,并抛出异常对象的指针;

对应的执行流将跳转到最近且最匹配的catch部分并对异常进行处理,在main函数中以基类指针接收派生类对象指针,同时其基类虚函数被派生类重写,构成多态条件,该处可直接调用what函数查看异常的错误信息;

不同的项目对于异常的处理都可能不同,可能在有些项目中需要提示异常的错误信息,而有些项目中需要遇到异常时对异常操作重新进行若干次的重试(可能是该操作的资源未就绪所导致的异常)等等;

常见的异常处理策略为如下:

  • 错误提示

    在某些项目中异常信息需要及时反馈给用户使其能够知道具体错误发生的部分从而定位错误信息;

    通常通过日志记录或用户界面提示实现;

  • 重试机制

    对于一些操作(网络请求或文件访问),可能因为资源未就绪而导致异常;

    此时可设计重试机制,则在捕获到异常后自动尝试该操作若干次,若是仍不成功则再进行提示用户信息等操作;

  • 资源清理

    确保在捕获异常时,正确释放占用的资源以避免内存泄漏或资源锁定;

  • 不同策略结合

    在复杂项目中可结合多种策略,如先进性重试机制,当重试至若干次数时则进行资源清理并错误提示等;

  • 自定义异常类型

    可通过项目需求定义多个自定义异常类型以便细致地处理不同种类的错误;


C++ 标准库的异常体系

请添加图片描述

C++标准库提供了一套异常处理机制,允许程序在运行时捕获和处理错误;

C++标准库定义了一个基本的异常类std::exception,所有的标准异常类都继承自这个类,其中该类提供了一个what()虚函数,用于返回异常的描述信息;

同时C++标准库提供了一系列的标准异常类,常见的包括:

  • std::runtime_error

    表示运行时错误;

  • std::logic_error

    表示逻辑错误,如违反了程序的逻辑;

  • std::out_of_range

    表示访问超出范围的错误;

  • std::invalid_argument

    表示传递给函数的参数无效;

  • std::bad_alloc

    表示内存分配失败;

这些异常类可根据不同的错误类型进行捕获和处理;

#include <iostream>
#include <stdexcept> // 包含标准异常类using namespace std;// 除法函数
double Div(double num1, double num2) {if (num2 == 0) {throw runtime_error("Division by zero error");  // 抛出标准异常}cout << "The process executed successfully" << endl;  // 捕获异常时不执行return num1 / num2;
}int main() {try {cout << Div(10, 0) << endl; // 尝试除以零cout << "No exception was caught" << endl;} catch (const runtime_error& e) {cout << "main: " << e.what() << endl; // 捕获并处理异常}return 0; // 程序结束
}

在这个例子中调用Div函数,如果num2为零则抛出一个std::runtime_error异常;

这个例子的运行结果为:

$ ./test 
main: Division by zero error

C++标准库的异常体系通常较为复杂与混乱,不建议使用;


异常的重新抛出

请添加图片描述

异常的重新抛出指的是当一个函数抛出异常时首先让执行流跳跃至一个最近的且最匹配的catch处,在该catch中处理一些特定的操作后再将该异常重新抛出至较浅层的catch处对异常进行处理;

double Div(double num1, double num2) {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}void func() {try {cout << Div(10, 0) << endl;cout << "No exception was caught" << endl;} catch (const runtime_error& e) {  // 第一次捕获异常cout << "func get a abnormal " << endl;throw e;  // 将异常重新抛出}
}int main() {try {func();} catch (const runtime_error& e) {  // 最终捕获异常并对异常进行处理cout << "main handler a abonormal: " << e.what() << endl;}return 0;
}

在这个例子中调用了Div函数并在func函数中捕获了一场进行了一些特定处理,而后将异常重新抛出;

允许更高层次的代码(main函数)处理该异常;

这种机制允许在不同的层次上对异常进行处理,使得程序能够保持灵活性和可维护性;


异常安全

请添加图片描述

函数的调用可以类比于一个调用链,不停的在内存当中压栈以及释放;

当一个函数抛出一个异常时其执行流将直接跳转至最近且最匹配的catch处,同时逐层释放对应的栈帧;

此时该函数的栈结构以及对应的栈中的变量资源将被释放,但若是在该函数中在堆中开辟了空间时可能会导致执行流的跳转而未对该堆空间进行清理则会导致内存泄漏的问题,这种情况是异常安全的较为常见的异常安全问题;

class ObjectTest {public:ObjectTest() { cout << " ObjectTest()" << endl; }~ObjectTest() { cout << " ~ObjectTest()" << endl; }
};double Div(double num1, double num2) {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}void func() {ObjectTest *t1 = new ObjectTest;  // 在堆上实例化一个对象ObjectTest *t2 = new ObjectTest;  // 在堆上实例化另一个对象cout << Div(10, 0) << endl; // 这里会抛出异常// 当抛出一个异常时,执行流将直接跳跃至最近且最匹配的 catch 处// Div 函数若是抛出了异常则会直接跳跃至 main 函数中的 catch 处// 下面的 delete 清理语句则不会执行delete t1; // 不会执行delete t2; // 不会执行
}int main() {try {func(); // 调用 func,可能抛出异常} catch (const runtime_error &e) {cout << "main handler a abnormal: " << e.what() << endl;}return 0;
}

在这个例子中在堆中实例化了两个对象,在调用可能抛出异常的函数后对资源进行清理;

Div函数抛出异常后执行流将直接跳跃至main函数中的catch处,对应的func函数中的delete资源清理将不会被执行;

这个例子的运行结果为:

$ ./test ObjectTest()ObjectTest()# 只调用了构造函数 未调用析构函数 表示资源未正确清理

在这种情况下可使用异常的重新抛出对资源进行清理;

class ObjectTest {public:ObjectTest() { cout << " ObjectTest()" << endl; }~ObjectTest() { cout << " ~ObjectTest()" << endl; }
};double Div(double num1, double num2) {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}void func() {ObjectTest *t1 = new ObjectTest;ObjectTest *t2 = new ObjectTest;try {cout << Div(10, 0) << endl;} catch (const runtime_error &e) {delete t1;delete t2;cout << "the t1 and d2 deleted sucessful,and throw abnormal again" << endl;throw e;  // 对堆空间上的资源进行清理而后将异常重新抛出}
}int main() {try {func();} catch (const runtime_error &e) {cout << "main handler a abonormal: " << e.what() << endl;}return 0;
}

在这个例子中func函数在堆上实例化两个对象后再调用可能出现异常的函数时对对应异常进行捕获;

当异常被抛出时将先被func函数的catch捕获,此处并不作异常的处理,而是对无法自行清理的动态分配资源进行清理,而后将异常重新抛出;

最终异常会被main函数中的catch捕获并对异常进行处理;

这个例子的运行结果为:

$ ./test ObjectTest()ObjectTest()~ObjectTest()~ObjectTest()
the t1 and d2 deleted sucessful,and throw abnormal again
main handler a abonormal: Division by zero error# 析构函数被调用表示资源被正确清理

除了在newdelete之间抛出的异常导致内存泄漏所引发的异常安全以外,异常安全还有:

  • 在构造函数中抛异常

    通常不建议在构造函数中抛异常,构造函数主要完成对象的构造和初始化,若是在构造函数中抛异常可能导致对象不完整或是没有完全初始化;

  • 在析构函数中抛异常

    通常不建议在析构函数中抛异常,析构函数主要完成对象的资源清理工作,若是在析构函数中抛异常可能导致对象资源清理不完全导致内存泄漏;

  • 加锁解锁中抛异常

    加锁解锁的异常情况与new,delete的情况相似;

    在加锁与解锁中抛异常可能会导致死锁问题;

    同时这种情况都可以以RAII的方式处理该问题;


异常规范

请添加图片描述

异常规范是C++所引入的一个特性,用于声明一个函数可能抛出的异常类型,其基本语法为:

return_type function_name(parameters) throw(exception_type1, exception_type2, ...);

异常规范用于指定一个函数可以抛出的类型,可在函数声明中使用throw关键字后跟一个或多个异常类型;

若是保证这个函数将不会抛出任何异常可直接以return_type function_name(parameters) throw()的方式进行声明;

double Div(double num1, double num2)throw(runtime_error,int) {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}int main() {try {Div(10,0);} catch (const runtime_error &e) {cout << "main handler a abonormal: " << e.what() << endl;}return 0;
}

在这个例子中对Div函数声明了可能会抛出runtime_error类型与int类型的异常,但实际上并不会抛出int类型的异常,这是因为编译器无法确认该类型的异常是否一定会被抛出;

但通常throw关键字是一个强制性的约定,当一种类型的异常未被throw声明但仍抛出了这个异常,那么即使在调用链中存在对应异常类型的catch时,该异常也不会被这个catch给捕获,而是会被程序以调用std::unexpected()的形式接收;

double Div(double num1, double num2)throw() {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}int main() {try {Div(10,0);} catch (const runtime_error &e) {cout << "main handler a abonormal: " << e.what() << endl;}return 0;
}

在这个例子中Div函数声明了throw()关键字表示不会抛出任何异常,但实际上会抛出一个runtime_error类型的异常;

main函数中存在对应类型的catch捕获异常;

这个例子的运行结果为:

$ ./test 
terminate called after throwing an instance of 'std::runtime_error'what():  Division by zero error
Aborted

原因是当一个函数声明throw()但实际抛出异常时,程序不会进入任何catch块,这是因为编译器会假设这个函数不会抛出异常从而导致未定义行为;

这种情况下程序会被系统处理异常,通常表现为程序崩溃或终止;

C++11标准引入了noexcept关键字用来声明一个函数不会抛出任何异常,对应的该异常规范也逐渐开始被弃用,属于是一种过时的特性,;

  • noexcept 关键字

    在C++11标准中,引入了更为简洁和安全的机制来处理异常;

    该关键字通常声明一个函数不会抛出任何异常;

    void func() noexcept; // 表示该函数不会抛出异常
    

    该关键字的优势主要表现在于:

    • 性能

      使用noexcept使编译器可以进行更多的优化,因为该关键字可以假设此函数不会抛出任何异常;

    • 明确性

      noexcept的语义更加清晰,表示函数不会抛出异常而不需要列出所有可能的异常类型;

为了使得所有异常都被捕获可使用try{}catch(...){}来确保捕获了所有异常;

其中catch(...)则会捕获任何异常;

catch(...)可以捕获任何类型的异常并阻止他们向上传播;

double Div(double num1, double num2) {if (num2 == 0) {throw runtime_error("Division by zero error");}cout << "The process executed successfully" << endl;return num1 / num2;
}void func() {try {Div(10, 0);} catch (...) { // 捕获所有异常并阻止异常向上传播cout << "未知异常" << endl;}
}int main() {try {func();} catch (const runtime_error &e) {cout << "main handler a abonormal: " << e.what() << endl;}return 0;
}

在这个例子中Div函数将抛出一个异常,其中func函数将试图调用这个函数并以catch(...)的方式捕获该函数的异常,main函数将试图调用func函数,并catch(const runtime_error &e)捕获Div函数将会抛出的异常;

这个例子的运行结果为:

$ ./test 
未知异常

本质原因是异常被catch(...)所捕获并且阻止该异常向上传播导致main函数中的catch未正确捕获到异常;

通常catch(...)捕获应置于调用链的最上层(main函数),以确保所有的异常都被捕获;


异常的优缺点

请添加图片描述

  • 异常的优点

    • 清晰的错误处理逻辑

      异常处理使错误与正常逻辑分离,使得代码更易于阅读与维护;

    • 自动传播

      异常被抛出时将自动向上冒泡到调用栈中的上层函数,知道找到合适的catch块;

      这个机制避免了在每个函数中都需要检查错误并返回错误代码的繁琐;

    • 错误处理灵活

      使用异常处理可以针对不同类型的异常执行不同的处理逻辑;

      可通过定义多个catch块来处理不同类型的异常从而实现更加灵活和细致的错误处理;

    • 资源管理

      异常发生时可使用RAII(资源获取即初始化)模式来确保资源(内存,文件句柄等)的正确释放;

      即使发生异常,局部对象的析构函数仍会被调用从而避免资源泄露;

    • 简化的错误传递

      异常提供了一种简单的方式来传递错误信息,而不必通过返回值或全局变量;

      使得错误信息可以更加丰富与详细;

  • 缺点

    • 性能开销

      异常处理可能导致一定的性能开销,尤其是在异常频繁发生的情况下;

      虽然在正常情况下不会影响性能,但异常被抛出时堆栈展开和其他相关操作可能会变得十分昂贵;

    • 复杂性

      异常处理机制可能增加代码的复杂性,在多线程环境中异常的传播和处理可能会变得更加复杂;

      此外,过度使用异常可能导致代码难以理解和调试(执行流跳跃);

    • 难以追踪的异常

      异常的传播机制可能导致错误发生的地点与最终捕获异常的地点调用链相隔很远,使得定位和修复问题变得更加困难;

版权声明:

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

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