往期回顾:
C++ 入门13:异常处理
一、前言
在前面文章的学习中,我们学习了类和对象的基础知识、构造函数、拷贝构造函数、静态成员、常量成员、运算符重载、友元函数、友元类、继承和派生类、虚函数和多态以及模板。今天,我们将学习 C++ 中的异常处理。异常处理是 C++ 提供的一种处理程序运行时错误的机制,它使得程序可以优雅地处理错误,而不是简单地崩溃。是的,就是优雅的处理代码,通过异常处理机制,我们可以实现即使程序有不可执行的错误却依旧能正常运行。
二、异常处理
2.1、异常处理的重要性
异常处理的重要性在于它允许程序以一种结构化和可预测的方式处理错误,而不是简单地终止执行。这不仅提高了程序的健壮性和稳定性,还使得调试和维护变得更加容易。通过异常处理,开发者可以更加专注于业务逻辑的实现,而不必担心错误处理细节,因为异常处理机制会负责捕获和处理这些错误。
2.2、 什么是异常处理?
异常处理是编程中一种不可或缺的错误管理机制,它允许程序在运行时优雅地响应和处理错误或异常情况,从而避免程序因未处理的错误而突然崩溃。这种机制对于开发稳定、可靠和用户友好的软件至关重要。在C++中,异常处理通过一组特定的关键字和语法结构来实现,主要包括try
、catch
和throw
,它们共同协作以捕获、传递和处理异常。
(1)try 块
try
块是异常处理机制的核心,它标识了一段可能抛出异常的代码区域。当try
块内的代码执行时,如果发生了异常情况(即抛出了一个异常),那么执行将立即停止,并跳转到紧随其后的catch
块(如果有匹配的异常类型)进行异常处理。如果没有匹配的catch
块,异常将按调用栈向上传播,直到找到相应的处理代码或最终由程序运行时环境捕获并处理(通常是终止程序)。
(2)catch 块
catch
块紧随try
块之后,用于捕获并处理try
块中抛出的异常。catch
块可以指定它想要捕获的异常类型,这允许程序对不同类型的异常采取不同的处理策略。如果有多个catch
块,它们将按照在代码中出现的顺序进行匹配,一旦找到匹配的异常类型,就会执行该catch
块中的代码,并忽略后续的catch
块。如果try
块中的代码没有抛出异常,那么catch
块将被完全跳过。
(3)throw 表达式
throw
关键字用于抛出一个异常。它后面可以跟任何类型的表达式,但通常是一个与错误或异常情况相关的对象。当throw
语句执行时,它会立即停止当前函数的执行,并开始查找最近的try
块。一旦找到,控制权就会转移到第一个匹配该异常类型的catch
块。如果没有找到匹配的catch
块,异常将继续向上传播,直到被捕获或程序终止。
2.3、 异常处理的基本语法
(1)异常处理的基本结构
try {
// 可能抛出异常的代码
} catch (异常类型 变量名) {
// 处理异常的代码
}
当 try
块中的代码抛出一个异常时,程序会立即跳转到 catch
块,并执行其中的代码。如果没有异常发生,catch
块的代码将不会被执行。
示例:
我们定义一个简单的示例,演示如何捕获和处理除零错误。
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 0;
int result;
try {
if (b == 0) {
throw "Division by zero!";
}
result = a / b;
cout << "Result: " << result << endl;
} catch (const char* e) {
cout << "Error: " << e << endl;
}
return 0;
}
在这个示例中,如果 b
为 0,则抛出一个字符串异常 "Division by zero!",并在 catch
块中捕获和处理这个异常。
2.4、 抛出和捕获不同类型的异常
C++允许抛出的异常几乎可以是任何类型的数据,包括基本数据类型(如int
、float
、char
等)、用户定义的类对象、指针,甚至是函数指针和成员函数指针。然而,尽管技术上可行,但并非所有类型都适合用作异常类型。
(1)基本数据类型
虽然可以抛出基本数据类型的异常,但这种方式通常不推荐。原因之一是它们不提供足够的上下文或错误信息,使得捕获和处理这些异常时难以确定错误的具体原因。此外,对于基本数据类型,编译器无法提供类型安全的异常处理,因为多个不同类型的异常可能都会被同一个catch
块捕获(除非使用多个catch
块分别捕获每种类型,但这会降低代码的清晰度和可维护性)。
(2)类对象
相比之下,使用类对象作为异常类型更为常见和推荐。类对象可以封装更丰富的错误信息,包括错误代码、错误消息、发生错误的位置等。这种封装使得异常处理更加灵活和强大。
-
自定义异常类 :开发者可以定义自己的异常类,这些类通常继承自标准库中的异常基类(如
std::exception
),从而继承一些基本的异常处理功能(如获取异常描述信息的what()
方法)。通过定义自己的异常类,开发者可以添加额外的成员变量和方法,以支持更具体的错误信息和处理逻辑。 -
类型安全的异常处理 :由于类对象具有明确的类型,因此可以使用C++的类型系统来确保异常处理的类型安全性。这意味着只有与
catch
块中指定的类型相匹配的异常才会被捕获,从而避免了错误的异常处理。 -
封装和继承:通过类的封装和继承特性,开发者可以构建异常处理的层次结构。基类异常可以代表一般性的错误,而派生类异常可以表示更具体的错误情况。这种结构使得异常处理更加模块化和可扩展。
-
易于调试和维护:由于类对象可以包含丰富的错误信息和上下文,因此它们对于调试和维护异常处理代码非常有用。当异常发生时,开发者可以轻松地获取到有关错误的所有相关信息,从而更快地定位问题并采取相应的修复措施。
示例:
我们自定义一个 DivideByZeroException
类,用于表示除零错误,并在代码中使用它。
#include <iostream>
#include <string>
using namespace std;
class DivideByZeroException {
private:
string message;
public:
DivideByZeroException(string msg) : message(msg) {}
string getMessage() const {
return message;
}
};
int main() {
int a = 10;
int b = 0;
int result;
try {
if (b == 0) {
throw DivideByZeroException("Division by zero!");
}
result = a / b;
cout << "Result: " << result << endl;
} catch (DivideByZeroException& e) {
cout << "Error: " << e.getMessage() << endl;
}
return 0;
}
在这个示例中,我们定义了一个 DivideByZeroException
类,并在除零错误发生时抛出该异常。catch
块捕获这个异常并输出错误信息。
2.5、多个 catch
块
在C++中,try
块可以伴随多个catch
块,这种设计允许我们的程序针对不同类型的异常进行精确的处理。每个catch
块都关联着一个特定的异常类型(或类型派生自的基类),当try
块中的代码抛出异常时,程序会按照catch
块声明的顺序逐一检查它们,寻找能够处理该异常的第一个catch
块。一旦找到匹配的catch
块,就会执行该块中的代码,并忽略所有后续的catch
块,即使它们也可能匹配相同的异常类型(由于C++的异常处理机制是基于第一个匹配的原则)。
多个 catch
块的机制使我们对异常处理可以更灵活性。我们可以根据需要,定义多个异常类,并为每种异常类型编写相应的处理逻辑。通过组合使用多个catch
块,可以确保程序能够针对各种可能的错误情况做出恰当的响应。
注意:
虽然catch
块可以有多个,但通常建议将它们组织得尽可能简洁明了,避免过于复杂的嵌套和重复的逻辑。 此外,为了捕获所有未明确处理的异常,可以添加一个捕获...
(省略号)的catch
块作为最后的备选,这可以捕获所有类型的异常,但应谨慎使用,因为它可能会隐藏潜在的编程错误。
而且实际开发中不会使用太多的。
示例:
我们扩展前面的示例,添加一个 catch
块,用于捕获整数类型的异常。
#include <iostream>
#include <string>
using namespace std;
class DivideByZeroException {
private:
string message;
public:
DivideByZeroException(string msg) : message(msg) {}
string getMessage() const {
return message;
}
};
int main() {
int a = 10;
int b = 0;
int result;
try {
if (b == 0) {
throw DivideByZeroException("Division by zero!");
}
result = a / b;
cout << "Result: " << result << endl;
} catch (DivideByZeroException& e) {
cout << "Error: " << e.getMessage() << endl;
} catch (int e) {
cout << "Integer error: " << e << endl;
}
return 0;
}
在这个示例中,如果抛出 DivideByZeroException
类型的异常,将会被第一个 catch
块捕获。如果抛出整数类型的异常,将会被第二个 catch
块捕获。
2.6、异常的再抛出
在某些情况下,catch
块中的代码可能无法完全处理当前捕获的异常,或者它可能认为该异常应该由更高层的代码来处理。在这种情况下,catch
块可以使用throw
关键字重新抛出异常。重新抛出的异常可以被更外层的try-catch
结构捕获并处理。
(1)两种方式:
|------------------|------------------------------------------------------------------------------------------|
| 不带表达式的throw: | 简单地使用throw;
语句(注意末尾的分号)可以重新抛出当前捕获的异常。这种方式保留了异常的原始类型和值,以及任何与之关联的堆栈跟踪信息(如果编译器和运行时环境支持的话)。 |
| 带表达式的throw: | 虽然不太常见,但也可以使用throw
后跟一个表达式来重新抛出一个新的异常。这通常用于在捕获并处理了一个异常后,基于该异常的信息构造并抛出一个新的异常。 |
重新抛出异常是一种重要的异常处理策略,它允许我们在异常处理链中传递异常,直到找到能够妥善处理该异常的代码为止。这种机制允许我们将异常处理的责任委托给更高层的代码,而不必在每个可能抛出异常的点都编写完整的错误处理逻辑。
注意:
同样的,过度使用异常再抛出可能会导致异常处理逻辑的复杂化,特别是在涉及多层嵌套try-catch
结构的情况下。很多时候其实我们是需要确保异常处理逻辑清晰、简洁且易于理解的。所以大家谨慎使用。
示例:
我们演示如何在 catch
块中重新抛出异常。
#include <iostream>
#include <string>
using namespace std;
class DivideByZeroException {
private:
string message;
public:
DivideByZeroException(string msg) : message(msg) {}
string getMessage() const {
return message;
}
};
void divide(int a, int b) {
try {
if (b == 0) {
throw DivideByZeroException("Division by zero!");
}
int result = a / b;
cout << "Result: " << result << endl;
} catch (DivideByZeroException& e) {
cout << "Caught in divide function: " << e.getMessage() << endl;
throw; // 重新抛出异常
}
}
int main() {
int a = 10;
int b = 0;
try {
divide(a, b);
} catch (DivideByZeroException& e) {
cout << "Caught in main function: " << e.getMessage() << endl;
}
return 0;
}
在这个示例中,当 divide
函数中的异常被捕获后,异常被重新抛出,并在 main
函数中再次被捕获和处理。
以上就是 C++ 程序的异常处理的基础知识点了。包括异常处理的基本语法、抛出和捕获不同类型的异常、多个 catch
块以及异常的再抛出。异常处理是 C++ 中的一种非常重要的机制,基本上在开发中都会用到的,大家多看看,一定要掌握。
都看到这里了,点个赞再走呗朋友~
加油吧,预祝大家变得更强!