欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 深入理解 C++ 异常处理机制

深入理解 C++ 异常处理机制

2025/2/21 3:24:16 来源:https://blog.csdn.net/Dreaming_TI/article/details/144127463  浏览:    关键词:深入理解 C++ 异常处理机制

文章目录

  • 前言
    • 一、什么是异常?
    • 二、C++ 异常处理的核心概念
      • 1. throw:抛出异常
      • 2. try 块:异常检测区域
      • 3. `catch` 块:异常处理
      • 4. 异常的传播
      • 5. 异常的安全性
    • 三、C++ 异常的基本语法
      • 1. 单一异常类型捕获
      • 2. 捕获多个异常类型
      • 3. 捕获所有类型的异常
      • 4. 重新抛出异常
    • 四、标准异常类
      • 示例:使用标准异常类
    • 五、C++ 异常处理的注意事项
    • 六、自定义异常类
    • 七、总结
  • 一些问题
    • 程序异常终止方式有哪些?
    • 传统错误处理机制
    • setjmp 与 longjmp
      • 1. setjmp
      • 2. longjmp

前言

C++ 中,异常(Exception)指程序运行过程中发生的异常情况,可能导致程序流程的中断。**异常处理机制允许程序在出现错误时采取适当的行动,而不需要完全依赖于错误代码或中断程序的正常流。**异常处理不仅提高了程序的健壮性,也使得错误处理更加结构化和易于维护。

下面我们会详细介绍 C++ 异常的基础概念、语法、机制、常见使用方式以及相关的实践:


一、什么是异常?

在程序执行过程中,某些不可预见的状况可能会导致程序无法继续正常运行。通常这些事件可以分为两类:

  • 逻辑错误:如数组越界、空指针解引用等。
  • 运行时错误:如文件未找到、网络连接中断、内存分配失败等。

C++ 异常处理机制的 核心思想 是当程序发生错误时,程序不再继续执行错误代码,而是跳转到一个异常处理代码块来处理错误或恢复程序执行。这种方式通过 throwtrycatch 关键字来实现。


二、C++ 异常处理的核心概念

1. throw:抛出异常

throw 用于抛出一个异常,表示程序在执行中发现了一个错误。它将错误信息传递给异常处理机制。抛出的异常对象可以是任何类型的对象,通常情况下是异常类的实例。

throw std::runtime_error("An error occurred");

上面的代码抛出了一个 std::runtime_error 类型的异常,异常的具体描述是 "An error occurred"


2. try 块:异常检测区域

try 块包含程序中可能会抛出异常的代码。程序会在执行 try 块中的代码时监控异常的发生。如果发生了异常,控制流将立即跳转到相应的 catch 块。

try {// 可能会抛出异常的代码int a = 10;if (a == 10) {throw std::runtime_error("Number is 10");}
} catch (const std::exception& e) {std::cout << "Exception caught: " << e.what() << std::endl;
}

3. catch 块:异常处理

catch 块用于捕获并处理从 try 块中抛出的异常。一个 try 块可以有多个 catch 块,它们用于捕获不同类型的异常。catch 的参数类型必须与抛出的异常类型匹配,或者是其基类类型。

try {throw std::runtime_error("An error occurred");
} catch (const std::runtime_error& e) {std::cout << "Runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {std::cout << "Some other exception: " << e.what() << std::endl;
}

4. 异常的传播

当一个异常被抛出后,它会沿着调用栈向外传播,直到遇到匹配的 catch 块为止。如果没有找到匹配的 catch 块,程序将终止并打印出异常信息。


5. 异常的安全性

C++ 提供了两种类型的异常安全性保证:

  • 基本保证:如果抛出异常,程序保持在某个一致的状态,某些操作可能会回滚,但其他的操作将不会影响程序的正确性。
  • 强保证:如果抛出异常,程序保证维持一致的状态,操作会以原子方式进行,要么完全成功,要么完全失败。

三、C++ 异常的基本语法

1. 单一异常类型捕获

在异常处理代码中,catch 块可以捕获特定类型的异常对象。

try {throw 42;  // 抛出整数类型的异常
} catch (int e) {  // 捕获整数类型的异常std::cout << "Caught an integer exception: " << e << std::endl;
}

2. 捕获多个异常类型

可以在同一个 try 块中使用多个 catch 块来捕获不同类型的异常,按顺序匹配。

try {throw std::out_of_range("Index out of range");
} catch (const std::out_of_range& e) {std::cout << "Caught an out_of_range exception: " << e.what() << std::endl;
} catch (const std::exception& e) {std::cout << "Caught a general exception: " << e.what() << std::endl;
}

3. 捕获所有类型的异常

可以使用 catch(...) 来捕获所有类型的异常,这对于调试或异常日志记录很有用。

try {throw 3.14;
} catch (...) {std::cout << "Caught an unknown exception" << std::endl;
}

4. 重新抛出异常

如果在 catch 块中处理了异常后,决定将异常重新抛出,可以使用 throw; 来重新抛出当前捕获的异常。

try {try {throw std::out_of_range("Out of range");} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;throw;  // 重新抛出当前异常}
} catch (const std::exception& e) {std::cout << "Caught rethrown exception: " << e.what() << std::endl;
}

四、标准异常类

C++ 标准库提供了多种常见的异常类,都是从 std::exception 派生的。以下是几种常见的异常类:

  • std::exception:所有标准异常的基类。
  • std::runtime_error:运行时错误,通常用于程序运行过程中检测到的错误。
  • std::logic_error:逻辑错误,表示代码错误,如非法参数。
  • std::out_of_range:数组下标越界等错误。
  • std::invalid_argument:无效的函数参数。

示例:使用标准异常类

#include <iostream>
#include <stdexcept>void testException() {throw std::out_of_range("Index out of range");
}int main() {try {testException();} catch (const std::out_of_range& e) {std::cout << "Caught exception: " << e.what() << std::endl;}return 0;
}

五、C++ 异常处理的注意事项

  1. 避免异常滥用:异常不应作为普通的控制流机制。它应仅用于处理程序不可恢复的错误或异常情况。

  2. 异常处理要及时:尽量在错误发生的最小范围内处理异常,避免将异常传播到不必要的层次。

  3. 保持异常的语义清晰:抛出异常时,提供足够的信息(例如异常类型、错误消息等)帮助调试和定位问题。

  4. 保证资源管理的正确性:在异常发生时,要确保资源(如内存、文件句柄、数据库连接等)能够被正确释放。可以使用 RAII(资源获取即初始化)模式来管理资源,避免泄漏。

  5. 异常安全:编写代码时,要确保异常安全。特别是在处理容器和智能指针等复杂数据结构时,要避免内存泄漏和数据损坏。


六、自定义异常类

在实际的项目开发中,我们可以自定义异常类用于后续的异常处理。

我们封装一个类 继承自 std::exception,也可以进行多层次的继承编写,比如对于下面的自定义异常类:

#include <iostream>
#include <exception>
#include <string>// 基础自定义异常类
class MyBaseException : public std::exception {
private:std::string message;public:MyBaseException(const std::string& msg) : message(msg) {}const char* what() const noexcept override {return message.c_str();}
};// 子类:特定类型的异常
class MyDerivedException : public MyBaseException {
private:int errorCode;public:MyDerivedException(const std::string& msg, int code): MyBaseException(msg), errorCode(code) {}int getErrorCode() const {return errorCode;}
};// 一个函数,抛出不同类型的异常
void riskyFunction(bool throwDerived) {if (throwDerived) {throw MyDerivedException("抛出派生类异常", 2002);} else {throw MyBaseException("抛出基类异常");}
}int main() {try {riskyFunction(true);  // 传递 true,抛出派生类异常} catch (const MyDerivedException& e) {  // 捕获派生类异常std::cout << "捕获到派生类异常: " << e.what() << " with error code: " << e.getErrorCode() << std::endl;} catch (const MyBaseException& e) {  // 捕获基类异常std::cout << "捕获到基类异常: " << e.what() << std::endl;}return 0;
}

七、总结

C++ 异常处理提供了一种结构化的方式来处理程序中的错误,它让代码更加健壮和易于维护。通过合理使用 throwtrycatch,可以让程序在发生错误时避免崩溃,而是采取适当的措施进行恢复或报告错误。

然而,异常处理也有其开销和潜在问题,开发者在使用时需要权衡性能和健壮性,遵循最佳实践,避免滥用异常处理机制。

掌握 C++ 异常的处理机制不仅是编写高质量代码的必要条件,也是提高程序稳定性和可维护性的关键技能。


一些问题

程序异常终止方式有哪些?

对于程序的异常终止方式,我们总结出下表:

异常终止方式描述
程序崩溃(Crash)程序由于未处理的错误或致命问题完全停止运行,可能出现操作系统级的错误提示。
断言失败(Assertion Failure)程序通过断言验证条件,若条件不满足则抛出错误,导致程序终止。
未捕获的异常(Uncaught Exception)程序抛出异常但没有捕获机制时,程序会终止。
栈溢出(Stack Overflow)递归调用过深或调用栈空间不足时,程序因栈溢出而异常终止。
分段错误(Segmentation Fault)程序访问不允许的内存区域,导致操作系统终止程序。
资源耗尽程序消耗过多系统资源,导致无法分配更多资源,操作系统强制终止。
非法指令(Illegal Instruction)程序执行不合法的机器指令时,处理器触发异常终止程序。
外部干预(外部信号或中断)程序被操作系统或用户通过外部命令中断或终止,如Ctrl+C或kill命令。

传统错误处理机制

同样的,我们通过下表列出一些传统的错误处理机制:

错误处理机制描述优点缺点示例代码
返回值检查通过函数返回值来表示成功或错误状态。简单直接,易于实现。容易忽略返回值,代码不易维护和阅读。FILE *file = fopen("data.txt", "r"); if (file == NULL) { perror("Error opening file"); }
错误码函数返回特定的整数错误码,表示错误类型。易于实现,返回值可以传递具体的错误类型。错误码定义和管理可能变得复杂,易出现遗漏检查。if (someFunction() == -1) { perror("Function failed"); }
全局错误状态使用全局变量(如errno)记录程序的错误状态。方便多个函数共享错误信息。不利于多线程,可能导致状态混乱。if (someFunction() == -1) { perror("Global error occurred"); }
函数返回状态函数返回特定状态代码,调用者需要检查该状态。简单,适用于小规模项目。需要大量的状态码定义和检查,容易遗漏。char *buffer = malloc(100); if (buffer == NULL) { printf("Memory allocation failed\n"); }
日志记录通过记录日志文件追踪错误信息,便于后续分析。便于追踪错误信息,适合调试和后期分析。错误处理延迟,程序流程不易即时响应错误。if (someFunction() == -1) { fprintf(stderr, "Error: %s\n", strerror(errno)); }
断言在程序开发阶段使用断言来验证假设条件,失败则程序终止。有助于调试,能快速发现逻辑错误。生产环境中可能被禁用,程序会因断言失败中断。assert(x > 0); // 如果x <= 0,程序会终止
退出程序在发生严重错误时通过exit()abort()函数终止程序。直接终止程序,避免错误影响。无法恢复错误,可能导致资源泄露。if (malloc_failed) { exit(1); // 退出程序 }

setjmp 与 longjmp

setjmplongjmp 是 C 语言中用于非局部跳转的两个函数。常用于在程序中执行异常处理或跳出深层嵌套的函数调用。它们提供了一个机制,可以从一个函数返回到另一个函数的特定位置,跳过函数的正常返回流程。

1. setjmp

setjmp 用于设置一个“跳转点”,并保存当前的程序状态。调用 setjmp 后,程序会记录下当前的执行状态,包括程序的栈信息、寄存器状态等。

函数原型

int setjmp(jmp_buf env);
  • envjmp_buf 类型的变量,它保存了当前的执行环境。这是一个平台相关的类型,通常会包含栈、寄存器等信息,用于后续跳转时恢复程序的执行状态。

  • 返回值setjmp 返回两次:

    • 第一次调用时,返回值为 0
    • 第二次调用(当 longjmp 被调用时),返回值为非零值,通常是由 longjmp 传递的返回值。

2. longjmp

longjmp 用于从 setjmp 所设置的“跳转点”返回。它通过修改 setjmp 保存的执行环境,恢复到 setjmp 调用时的状态,并且从 setjmp 返回时会返回非零值。

函数原型

void longjmp(jmp_buf env, int val);
  • envjmp_buf 类型的变量,它保存了程序跳转时的执行环境。这应该是与 setjmp 调用中的 env 对应的变量。

  • vallongjmp 会将这个值作为 setjmp 的返回值返回。如果 val0setjmp 会返回 1,否则 setjmp 会返回 val

用法示例

下面是一个简单的例子,展示了 setjmplongjmp 的基本使用方法:

#include <stdio.h>
#include <setjmp.h>jmp_buf env;  // 用于保存跳转状态的变量void func() {printf("In func(), before longjmp\n");longjmp(env, 42);  // 跳转回 setjmp 并传递返回值 42printf("In func(), after longjmp (this will not be executed)\n");
}int main() {int val = setjmp(env);  // 设置跳转点并保存执行状态if (val == 0) {// 第一次调用 setjmp,执行正常逻辑printf("In main(), before calling func\n");func();  // 调用 func(),里面会触发 longjmpprintf("In main(), after calling func (this will not be executed)\n");} else {// 第二次调用 setjmp,说明程序通过 longjmp 跳转回来了printf("In main(), after longjmp, returned value: %d\n", val);}return 0;
}

代码解释

  • 第一次调用 setjmp:在 main 函数中调用 setjmp(env) 时,程序会正常执行。当 setjmp 被调用时,它返回值为 0,程序继续向下执行并调用 func() 函数。

  • longjmp 被调用:在 func() 函数中,调用了 longjmp(env, 42),这会导致程序跳回到 setjmp 调用的地方,并将 42 作为返回值传递给 setjmp

  • 第二次返回 setjmp:由于 longjmp 的调用,setjmp 返回非零值(即 42),因此程序跳到 else 分支,输出 In main(), after longjmp, returned value: 42

应用 与 注意事项

  1. 异常处理setjmplongjmp 可以用于实现非局部的错误处理或异常机制。longjmp 可以让程序跳回到程序之前保存的状态,而不需要通过常规的函数返回。

  2. 避免深层嵌套的函数调用setjmplongjmp 也可以用于避免深层嵌套的函数调用,允许程序从更深层的嵌套中跳出。

  3. 栈的完整性longjmp 跳转时不会执行跳转点到跳转后的代码之间的代码,因此在使用 setjmplongjmp 时需要小心栈的完整性,避免破坏局部变量的状态。

  4. 不可用于局部变量的清理longjmp 跳转时不会调用局部变量的析构函数,因此不能用于清理资源(例如关闭文件、释放内存等)。如果需要清理资源,应使用 try-catch 等机制,或者确保在跳转前手动清理。

  5. setjmplongjmp 的限制:它们可能对调试和程序的可维护性产生负面影响,通常不推荐广泛使用。现代 C++ 中,可以通过异常机制(try-catch)来替代 setjmp/longjmp 的使用。

总结

  • setjmp 用于保存当前的执行状态,并设置一个跳转点。
  • longjmp 用于从 setjmp 设置的跳转点恢复,并跳回到 setjmp,并且返回值可以指定一个非零值来表明程序的跳转。
  • setjmplongjmp 是一种底层的控制流机制,常用于处理特殊的异常情况,但需要小心使用,以免影响程序的稳定性和可维护性。

版权声明:

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

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

热搜词