文章目录
- 前言
- 一、什么是异常?
- 二、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++ 异常处理机制的 核心思想 是当程序发生错误时,程序不再继续执行错误代码,而是跳转到一个异常处理代码块来处理错误或恢复程序执行。这种方式通过 throw
、try
和 catch
关键字来实现。
二、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++ 异常处理的注意事项
-
避免异常滥用:异常不应作为普通的控制流机制。它应仅用于处理程序不可恢复的错误或异常情况。
-
异常处理要及时:尽量在错误发生的最小范围内处理异常,避免将异常传播到不必要的层次。
-
保持异常的语义清晰:抛出异常时,提供足够的信息(例如异常类型、错误消息等)帮助调试和定位问题。
-
保证资源管理的正确性:在异常发生时,要确保资源(如内存、文件句柄、数据库连接等)能够被正确释放。可以使用 RAII(资源获取即初始化)模式来管理资源,避免泄漏。
-
异常安全:编写代码时,要确保异常安全。特别是在处理容器和智能指针等复杂数据结构时,要避免内存泄漏和数据损坏。
六、自定义异常类
在实际的项目开发中,我们可以自定义异常类用于后续的异常处理。
我们封装一个类 继承自 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++ 异常处理提供了一种结构化的方式来处理程序中的错误,它让代码更加健壮和易于维护。通过合理使用 throw
、try
和 catch
,可以让程序在发生错误时避免崩溃,而是采取适当的措施进行恢复或报告错误。
然而,异常处理也有其开销和潜在问题,开发者在使用时需要权衡性能和健壮性,遵循最佳实践,避免滥用异常处理机制。
掌握 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
setjmp
和 longjmp
是 C 语言中用于非局部跳转的两个函数。常用于在程序中执行异常处理或跳出深层嵌套的函数调用。它们提供了一个机制,可以从一个函数返回到另一个函数的特定位置,跳过函数的正常返回流程。
1. setjmp
setjmp
用于设置一个“跳转点”,并保存当前的程序状态。调用 setjmp
后,程序会记录下当前的执行状态,包括程序的栈信息、寄存器状态等。
函数原型
int setjmp(jmp_buf env);
-
env
:jmp_buf
类型的变量,它保存了当前的执行环境。这是一个平台相关的类型,通常会包含栈、寄存器等信息,用于后续跳转时恢复程序的执行状态。 -
返回值:
setjmp
返回两次:- 第一次调用时,返回值为
0
。 - 第二次调用(当
longjmp
被调用时),返回值为非零值,通常是由longjmp
传递的返回值。
- 第一次调用时,返回值为
2. longjmp
longjmp
用于从 setjmp
所设置的“跳转点”返回。它通过修改 setjmp
保存的执行环境,恢复到 setjmp
调用时的状态,并且从 setjmp
返回时会返回非零值。
函数原型
void longjmp(jmp_buf env, int val);
-
env
:jmp_buf
类型的变量,它保存了程序跳转时的执行环境。这应该是与setjmp
调用中的env
对应的变量。 -
val
:longjmp
会将这个值作为setjmp
的返回值返回。如果val
为0
,setjmp
会返回1
,否则setjmp
会返回val
。
用法示例
下面是一个简单的例子,展示了 setjmp
和 longjmp
的基本使用方法:
#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
。
应用 与 注意事项
-
异常处理:
setjmp
和longjmp
可以用于实现非局部的错误处理或异常机制。longjmp
可以让程序跳回到程序之前保存的状态,而不需要通过常规的函数返回。 -
避免深层嵌套的函数调用:
setjmp
和longjmp
也可以用于避免深层嵌套的函数调用,允许程序从更深层的嵌套中跳出。 -
栈的完整性:
longjmp
跳转时不会执行跳转点到跳转后的代码之间的代码,因此在使用setjmp
和longjmp
时需要小心栈的完整性,避免破坏局部变量的状态。 -
不可用于局部变量的清理:
longjmp
跳转时不会调用局部变量的析构函数,因此不能用于清理资源(例如关闭文件、释放内存等)。如果需要清理资源,应使用try-catch
等机制,或者确保在跳转前手动清理。 -
setjmp
和longjmp
的限制:它们可能对调试和程序的可维护性产生负面影响,通常不推荐广泛使用。现代 C++ 中,可以通过异常机制(try-catch
)来替代setjmp
/longjmp
的使用。
总结
setjmp
用于保存当前的执行状态,并设置一个跳转点。longjmp
用于从setjmp
设置的跳转点恢复,并跳回到setjmp
,并且返回值可以指定一个非零值来表明程序的跳转。setjmp
和longjmp
是一种底层的控制流机制,常用于处理特殊的异常情况,但需要小心使用,以免影响程序的稳定性和可维护性。