C++ 异常处理:嵌套块与类对象的深入探索
1. 异常处理的基本概念
在程序运行过程中,异常是不可避免的。异常处理机制允许我们捕获和处理这些意外情况,从而增强程序的健壮性。在 C++ 中,我们可以使用
try
和
catch
块来实现异常处理。在不同的
try
块中调用函数时,对应的
catch
块会处理该函数抛出的特定类型的异常。我们可以根据程序的结构和操作需求,选择在最方便的层级处理异常。极端情况下,我们可以在
main()
函数中捕获程序中任何地方出现的异常,只需将
main()
函数的代码放在
try
块中,并添加合适的
catch
块。
2. 嵌套
try
块
try
块可以嵌套在另一个
try
块内部。每个
try
块都有自己的
catch
块集合,用于处理该块内部可能抛出的异常。
catch
块仅对其所属
try
块内抛出的异常起作用。以下是嵌套
try
块的处理流程:
graph TD;
A[外层 try 块] --> B{是否抛出异常};
B -- 是 --> C{异常类型};
C -- int --> D[外层 catch(int)];
C -- long --> E[外层 catch(long)];
A --> F[内层 try 块];
F --> G{是否抛出异常};
G -- 是 --> H{异常类型};
H -- int --> I[内层 catch(int)];
H -- long --> J{内层无匹配 catch};
J --> E;
下面是一个嵌套
try
块并在函数内抛出异常的示例代码:
// Ex15_02.cpp
// Throwing exceptions in nested try blocks
#include <iostream>
void throwIt(int i)
{
throw i; // Throws the parameter value
}
int main()
{
for(int i {} ; i <= 5 ; ++i)
{
try
{
std::cout << "outer try:\n";
if(i == 0)
throw i; // Throw int exception
if(i == 1)
throwIt(i); // Call the function that throws int
try
{ // Nested try block
std::cout << " inner try:\n";
if(i == 2)
throw static_cast<long>(i); // Throw long exception
if(i == 3)
throwIt(i); // Call the function that throws int
} // End nested try block
catch(int n)
{
std::cout << " Catch int for inner try. " << "Exception " << n << std::endl;
}
std::cout << "outer try:\n";
if(i == 4)
throw i; // Throw int
throwIt(i); // Call the function that throws int
}
catch(int n)
{
std::cout << "Catch int for outer try. " << "Exception " << n << std::endl;
}
catch(long n)
{
std::cout << "Catch long for outer try. " << "Exception " << n << std::endl;
}
}
}
该程序的输出如下:
outer try:
Catch int for outer try. Exception 0
outer try:
Catch int for outer try. Exception 1
outer try:
inner try:
Catch long for outer try. Exception 2
outer try:
inner try:
Catch int for inner try. Exception 3
outer try:
Catch int for outer try. Exception 3
outer try:
inner try:
outer try:
Catch int for outer try. Exception 4
outer try:
inner try:
outer try:
Catch int for outer try. Exception 5
代码解释:
-
throwIt()
函数用于抛出传入的参数值。如果在
try
块外部调用该函数,程序会立即终止,因为异常未被捕获,默认的终止处理程序将被调用。
- 所有异常都在
for
循环内抛出。通过检查循环变量
i
的值,我们可以决定何时抛出异常以及抛出何种类型的异常。
- 当
i
为 0 时,外层
try
块抛出
int
类型的异常,由外层对应的
catch
块捕获。
- 当
i
为 1 时,调用
throwIt()
函数抛出
int
类型的异常,同样由外层的
catch
块捕获。
- 当
i
为 2 时,内层
try
块抛出
long
类型的异常,由于内层没有匹配的
catch
块,该异常传播到外层,由外层的
catch(long)
块处理。
- 当
i
为 3 时,内层
try
块调用
throwIt()
函数抛出
int
类型的异常,由内层的
catch(int)
块捕获。之后,外层
try
块再次调用
throwIt()
函数抛出
int
类型的异常,由外层的
catch(int)
块捕获。
- 当
i
为 4 和 5 时,外层
try
块抛出
int
类型的异常,由外层的
catch(int)
块捕获。
3. 类对象作为异常
在 C++ 中,我们可以将任何类型的类对象作为异常抛出。通常,我们会定义特定的异常类来表示特定的问题。异常类对象通常包含一个描述问题的消息,可能还有某种错误代码。以下是一个简单的异常类定义:
// MyTroubles.h Exception class definition
#ifndef MYTROUBLES_H
#define MYTROUBLES_H
#include <string>
using std::string;
class Trouble
{
private:
string message;
public:
Trouble(string str = "There's a problem") : message {str} {}
string what() const {return message;}
};
#endif
下面是一个抛出异常类对象的示例代码:
// Ex15_03.cpp
// Throw an exception object
#include <iostream>
#include "MyTroubles.h"
int main()
{
for(int i {}; i < 2 ; ++i)
{
try
{
if(i == 0)
throw Trouble {};
else
throw Trouble {"Nobody knows the trouble I've seen..."};
}
catch(const Trouble& t)
{
std::cout << "Exception: " << t.what() << std::endl;
}
}
}
该程序的输出如下:
Exception: There's a problem
Exception: Nobody knows the trouble I've seen...
代码解释:
- 在
for
循环中,当
i
为 0 时,抛出一个使用默认构造函数创建的
Trouble
对象;当
i
为 1 时,抛出一个包含特定消息的
Trouble
对象。
-
catch
块的参数是一个引用,这是因为异常对象在抛出时会被复制,如果不使用引用,会进行不必要的二次复制。异常对象抛出时,首先会复制创建一个临时对象,原对象会因为退出
try
块而被销毁,复制的对象会传递给
catch
处理程序。
4. 匹配
catch
处理程序与异常
try
块后面的
catch
处理程序会按照代码中出现的顺序进行检查,第一个参数类型与异常类型匹配的处理程序将被执行。对于基本类型的异常,需要与
catch
块中的参数类型完全匹配;对于类对象异常,可能会进行隐式类型转换以匹配处理程序的参数类型。以下是匹配规则:
- 忽略
const
修饰符后,参数类型与异常类型相同。
- 参数类型是异常类的直接或间接基类,或者是异常类直接或间接基类的引用,忽略
const
修饰符。
- 异常和参数都是指针,并且异常类型可以隐式转换为参数类型,忽略
const
修饰符。
这些类型转换规则对
try
块的
catch
块顺序有影响。如果有多个处理同一类层次结构中不同异常类型的处理程序,最派生的类类型必须排在前面,最基类的类型排在最后。如果基类类型的处理程序排在派生类类型处理程序之前,那么基类处理程序将总是被选择来处理派生类异常,派生类类型的处理程序将永远不会被执行。
我们可以扩展之前的
MyTroubles.h
文件,添加更多的异常类:
// MyTroubles.h Exception classes
#ifndef MYTROUBLES_H
#define MYTROUBLES_H
#include <string>
using std::string;
class Trouble
{
private:
string message;
public:
Trouble(string str = "There's a problem") : message {str} {}
virtual ~Trouble(){} // Virtual destructor
virtual string what() const { return message; }
};
// Derived exception class
class MoreTrouble : public Trouble
{
public:
MoreTrouble(string str = "There's more trouble...") : Trouble {str} {}
};
// Derived exception class
class BigTrouble : public MoreTrouble
{
public:
BigTrouble(string str = "Really big trouble...") : MoreTrouble {str} {}
};
#endif
以下是抛出和捕获这些异常类对象的示例代码:
// Ex15_04.cpp
// Throwing and catching objects in a hierarchy
#include <iostream>
#include "MyTroubles.h"
int main()
{
Trouble trouble;
MoreTrouble moreTrouble;
BigTrouble bigTrouble;
for (int i {}; i < 7; ++i)
{
try
{
if (i == 3)
throw trouble;
else if (i == 5)
throw moreTrouble;
else if(i == 6)
throw bigTrouble;
}
catch (const BigTrouble& t)
{
std::cout << "BigTrouble object caught: " << t.what() << std::endl;
}
catch (const MoreTrouble& t)
{
std::cout << "MoreTrouble object caught: " << t.what() << std::endl;
}
catch (const Trouble& t)
{
std::cout << "Trouble object caught: " << t.what() << std::endl;
}
std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
}
}
该程序的输出如下:
End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
MoreTrouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
BigTrouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6
代码解释:
- 在
for
循环中,根据
i
的值抛出不同的异常对象。
- 每个
catch
块包含不同的消息,输出结果显示了哪个
catch
处理程序被选择来处理抛出的异常。
- 注意,
catch
块的参数类型都是引用,这是为了避免对异常对象进行不必要的复制。
5. 使用基类处理程序捕获派生类异常
由于派生类类型的异常可以隐式转换为基类类型,我们可以使用一个基类处理程序捕获之前示例中抛出的所有异常。以下是修改后的代码:
// Ex15_05.cpp
// Catching exceptions with a base class handler
#include <iostream>
#include "MyTroubles.h"
int main()
{
Trouble trouble;
MoreTrouble moreTrouble;
BigTrouble bigTrouble;
for (int i {}; i < 7; ++i)
{
try
{
if (i == 3)
throw trouble;
else if (i == 5)
throw moreTrouble;
else if(i == 6)
throw bigTrouble;
}
catch (const Trouble& t)
{
std::cout << "Trouble object caught: " << t.what() << std::endl;
}
std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
}
}
该程序的输出如下:
End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
Trouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
Trouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6
代码解释:
- 当
catch
块的参数是基类的引用时,它可以匹配任何派生类异常。虽然输出显示捕获的是
Trouble
对象,但实际上捕获的是派生类对象。
- 当异常通过引用传递时,其动态类型会被保留。我们可以使用
typeid()
运算符获取并显示异常对象的动态类型。修改
catch
块的代码如下:
catch(const Trouble& t)
{
std::cout << typeid(t).name() << " object caught: " << t.what() << std::endl;
}
修改后的程序输出如下:
End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
class Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
class MoreTrouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
class BigTrouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6
需要注意的是,有些编译器默认不启用运行时类型识别(RTTI),如果上述代码无法正常工作,需要检查编译器选项并启用该功能。如果将
catch
块的参数类型改为值传递,即
catch(Trouble t)
,则会丢失异常对象的动态类型信息。
通过以上内容,我们深入了解了 C++ 中异常处理的机制,包括嵌套
try
块的使用、类对象作为异常的处理以及如何匹配
catch
处理程序与异常。在实际编程中,合理使用异常处理可以提高程序的健壮性和可维护性。
C++ 异常处理:嵌套块与类对象的深入探索
6. 异常处理的最佳实践
在实际编程中,为了更好地利用 C++ 的异常处理机制,我们需要遵循一些最佳实践:
-
定义特定异常类
:如前面所述,定义特定的异常类来表示不同类型的问题,这样可以使异常处理更加清晰和易于维护。例如,在处理文件操作时,可以定义
FileOpenError
、
FileReadError
等异常类。
-
异常类层次结构
:使用继承来创建异常类的层次结构,最派生的类表示最具体的问题,基类表示更通用的问题。这样可以方便地使用基类处理程序捕获多个相关的异常。
-
catch
块顺序
:按照从最具体到最通用的顺序排列
catch
块,确保派生类异常首先被处理,避免基类处理程序掩盖派生类异常。
-
使用引用参数
:在
catch
块中使用引用参数,避免不必要的对象复制,同时保留异常对象的动态类型信息。
-
避免在异常类成员函数中抛出异常
:为了保持异常处理逻辑的可管理性,异常类的成员函数应避免抛出异常。
7. 异常处理的性能考虑
虽然异常处理为程序提供了强大的错误处理能力,但也会带来一定的性能开销。以下是一些性能相关的考虑:
-
异常抛出和捕获的开销
:抛出和捕获异常涉及到栈展开(stack unwinding)过程,即从异常抛出点开始,依次销毁栈上的对象,直到找到匹配的
catch
块。这个过程会消耗一定的时间和资源。
-
异常对象的复制
:异常对象在抛出时会被复制,这也会带来一定的性能开销。因此,建议在
catch
块中使用引用参数,避免额外的复制。
-
异常处理的频率
:频繁抛出和捕获异常会影响程序的性能。在设计程序时,应尽量避免不必要的异常处理,优先使用条件判断来处理可预见的错误。
8. 异常处理与资源管理
在异常处理过程中,资源管理是一个重要的问题。如果在异常抛出时,程序中分配的资源(如内存、文件句柄等)没有被正确释放,会导致资源泄漏。为了避免这种情况,可以使用 RAII(Resource Acquisition Is Initialization)技术。
RAII 是一种 C++ 编程技术,它利用对象的生命周期来管理资源。当对象创建时,资源被获取;当对象销毁时,资源被释放。以下是一个简单的 RAII 示例:
#include <iostream>
#include <fstream>
class FileHandler {
private:
std::fstream file;
public:
FileHandler(const std::string& filename) : file(filename, std::ios::in) {
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandler() {
if (file.is_open()) {
file.close();
}
}
std::fstream& getFile() {
return file;
}
};
int main() {
try {
FileHandler handler("example.txt");
// 使用文件进行操作
std::string line;
while (std::getline(handler.getFile(), line)) {
std::cout << line << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
在这个示例中,
FileHandler
类封装了文件操作,当对象创建时,文件被打开;当对象销毁时,文件被关闭。即使在操作过程中抛出异常,
FileHandler
对象的析构函数也会确保文件被正确关闭,避免资源泄漏。
9. 异常处理的总结
异常处理是 C++ 中一项重要的特性,它可以帮助我们更好地处理运行时错误,提高程序的健壮性和可维护性。以下是对本文内容的总结:
| 要点 | 描述 |
| ---- | ---- |
| 嵌套
try
块 | 可以在一个
try
块中嵌套另一个
try
块,每个
try
块有自己的
catch
块,异常会首先由内层
try
块的
catch
块处理,如果没有匹配的处理程序,会传播到外层
try
块。 |
| 类对象作为异常 | 可以将类对象作为异常抛出,通常定义特定的异常类来表示不同的问题,异常类对象可以包含错误消息和错误代码。 |
|
catch
处理程序匹配 |
catch
处理程序按照代码中出现的顺序进行检查,第一个参数类型与异常类型匹配的处理程序将被执行。对于类对象异常,可能会进行隐式类型转换。 |
| 基类处理程序捕获派生类异常 | 派生类异常可以隐式转换为基类类型,因此可以使用基类处理程序捕获多个相关的异常。 |
| 最佳实践 | 定义特定异常类、使用异常类层次结构、合理安排
catch
块顺序、使用引用参数、避免在异常类成员函数中抛出异常。 |
| 性能考虑 | 异常处理会带来一定的性能开销,应尽量避免不必要的异常处理。 |
| 资源管理 | 使用 RAII 技术确保在异常处理过程中资源被正确释放,避免资源泄漏。 |
通过合理使用异常处理机制,并遵循最佳实践,我们可以编写出更加健壮、可靠的 C++ 程序。在实际开发中,需要根据具体的应用场景和需求,灵活运用异常处理技术,平衡程序的健壮性和性能。
graph LR;
A[异常处理] --> B[嵌套 try 块];
A --> C[类对象异常];
A --> D[catch 匹配];
A --> E[基类捕获派生类];
A --> F[最佳实践];
A --> G[性能考虑];
A --> H[资源管理];
希望本文能帮助你深入理解 C++ 中的异常处理机制,并在实际编程中有效地运用它们。如果你有任何疑问或建议,欢迎在评论区留言。
超级会员免费看
112

被折叠的 条评论
为什么被折叠?



