深入探究 C++ 异常处理
1. 引言
在程序执行过程中,异常是问题出现的一种指示。异常处理能够让我们创建可以解决(或处理)异常的应用程序。在很多情况下,这使得程序可以像没有遇到问题一样继续执行。通过异常处理,我们能够编写健壮且容错的程序,这些程序可以处理问题并继续执行,或者优雅地终止。
下面是学习异常处理的一些目标:
- 使用
try
、
catch
和
throw
分别检测、处理和指示异常。
- 声明新的异常类。
- 了解栈展开如何使在一个作用域中未捕获的异常能在另一个作用域中被捕获。
- 处理新的失败情况。
- 使用
unique_ptr
防止内存泄漏。
- 理解标准异常层次结构。
2. 示例:处理除零异常
我们通过一个简单的示例来理解异常处理,该示例处理函数尝试除零时可能发生的异常。
2.1 商函数的实现
定义了一个名为
quotient
的函数,它接收用户输入的两个整数,并将第一个
int
参数除以第二个
int
参数。在进行除法之前,函数将第一个
int
参数的值转换为
double
类型,第二个
int
参数的值会隐式提升为
double
类型进行计算,最终返回一个
double
结果。
// perform division and throw DivideByZeroException object if
// divide-by-zero exception occurs
double quotient( int numerator, int denominator )
{
// throw DivideByZeroException if trying to divide by zero
if ( denominator == 0 )
throw DivideByZeroException(); // terminate function
// return division result
return static_cast< double >( numerator ) / denominator;
} // end function quotient
2.2 定义异常类
为了表示除零异常,我们定义了
DivideByZeroException
类,它是标准库类
runtime_error
的派生类。
runtime_error
是
exception
的派生类,而
exception
是 C++ 标准库中异常的标准基类。
// Fig. 22.1: DivideByZeroException.h
// Class DivideByZeroException definition.
#include <stdexcept> // stdexcept header contains runtime_error
// DivideByZeroException objects should be thrown by functions
// upon detecting division-by-zero exceptions
class DivideByZeroException : public std::runtime_error
{
public:
// constructor specifies default error message
DivideByZeroException()
: std::runtime_error( "attempted to divide by zero" ) {}
}; // end class DivideByZeroException
2.3 异常处理的演示
在
main
函数中,我们使用异常处理来包裹可能抛出
DivideByZeroException
的代码,并处理该异常。
// Fig. 22.2: fig22_02.cpp
// Example that throws exceptions on
// attempts to divide by zero.
#include <iostream>
#include "DivideByZeroException.h" // DivideByZeroException class
using namespace std;
int main()
{
int number1; // user-specified numerator
int number2; // user-specified denominator
cout << "Enter two integers (end-of-file to end): ";
// enable user to enter two integers to divide
while ( cin >> number1 >> number2 )
{
// try block contains code that might throw exception
// and code that will not execute if an exception occurs
try
{
double result = quotient( number1, number2 );
cout << "The quotient is: " << result << endl;
} // end try
catch ( DivideByZeroException ÷ByZeroException )
{
cout << "Exception occurred: "
<< divideByZeroException.what() << endl;
} // end catch
cout << "\nEnter two integers (end-of-file to end): ";
} // end while
cout << endl;
} // end main
2.4 代码结构分析
-
try块 :try块(第 32 - 36 行)包裹了对quotient函数的调用和显示除法结果的语句。如果quotient函数抛出异常,try块内后续的语句将不会执行。 -
catch处理程序 :catch处理程序(第 37 - 41 行)必须紧跟在try块之后。异常参数应声明为对catch处理程序能处理的异常类型的引用,这样可以避免复制异常对象,并能正确捕获派生类异常。当try块中发生异常时,程序会寻找第一个类型匹配的catch处理程序。
以下是程序执行流程的 mermaid 流程图:
graph TD;
A[开始] --> B[提示用户输入两个整数];
B --> C{输入是否有效};
C -- 是 --> D[调用 quotient 函数];
D -- 分母不为 0 --> E[输出商];
D -- 分母为 0 --> F[抛出 DivideByZeroException];
F --> G[进入 catch 处理程序];
G --> H[输出错误信息];
E --> I[提示用户再次输入];
H --> I;
C -- 否 --> J[结束];
I --> C;
3. 异常处理的终止模型
如果
try
块中的语句导致异常发生,
try
块会立即终止。然后,程序会搜索第一个能处理该异常类型的
catch
处理程序。当找到匹配的
catch
处理程序时,其中的代码会执行。当
catch
处理程序处理完异常后,异常被视为已处理,
catch
处理程序内定义的局部变量(包括
catch
参数)将超出作用域。程序控制不会返回到异常发生的点(即抛出点),而是从
try
块后面最后一个
catch
处理程序之后的第一条语句继续执行,这就是异常处理的终止模型。
4. 重新抛出异常
函数可能会使用资源(如文件),并且在发生异常时希望释放该资源。异常处理程序在接收到异常后,可以释放资源,然后通过
throw;
语句重新抛出异常,通知调用者发生了异常。
以下是重新抛出异常的示例代码:
// Fig. 22.3: fig22_03.cpp
// Rethrowing an exception.
#include <iostream>
#include <exception>
using namespace std;
// throw an exception and catch it in the same function
void throwException()
{
// throw exception
try
{
cout << "Function throwException throws an exception\n";
throw exception(); // generate exception
}
catch ( exception & )
{
cout << "Exception handled in function throwException"
<< "\nFunction throwException rethrows exception";
throw; // rethrow exception for further processing
}
cout << "This also should not print\n";
}
int main()
{
// execute try block
try
{
cout << "\nmain invokes function throwException\n";
throwException();
cout << "This should not print\n";
}
catch ( exception & )
{
cout << "\n\nException handled in main\n";
}
cout << "Program control continues after catch in main\n";
}
在这个示例中,
throwException
函数中的
try
块抛出一个
exception
实例,其
catch
处理程序捕获该异常,打印错误信息并重新抛出异常。
main
函数中的
catch
处理程序捕获重新抛出的异常并打印错误信息。
表格:常见编程错误和预防提示
| 错误类型 | 描述 | 预防提示 |
| ---- | ---- | ---- |
| 语法错误 | 在
try
块和其对应的
catch
处理程序之间或
catch
处理程序之间放置代码 | 确保代码结构正确,
catch
处理程序紧跟
try
块 |
| 语法错误 | 每个
catch
处理程序只能有一个参数,指定逗号分隔的异常参数列表是语法错误 | 只声明一个异常参数 |
| 编译错误 | 在单个
try
块后面的多个
catch
处理程序中捕获相同类型的异常 | 避免重复捕获相同类型的异常 |
| 逻辑错误 | 假设异常处理后控制会返回到抛出点之后的第一条语句 | 理解异常处理的终止模型 |
| 程序终止 | 在
catch
处理程序外执行空
throw
语句会放弃异常处理并立即终止程序 | 仅在
catch
处理程序内使用空
throw
语句 |
5. 栈展开
如果
try
块中发生的异常没有匹配的
catch
处理程序,或者异常发生在不在
try
块中的语句中,包含该语句的函数将立即终止,程序会尝试在调用函数中找到一个封闭的
try
块,这个过程称为栈展开。
下面通过一个简单的调用栈示例来说明栈展开的过程:
| 调用栈层级 | 函数 | 操作 |
| ---- | ---- | ---- |
| 1 |
main
| 调用
funcA
|
| 2 |
funcA
| 调用
funcB
|
| 3 |
funcB
| 抛出异常 |
当
funcB
抛出异常且自身没有合适的
catch
处理程序时,
funcB
终止,控制返回
funcA
。如果
funcA
也没有合适的
catch
处理程序,
funcA
终止,控制返回
main
。直到找到匹配的
catch
处理程序或者程序终止。
以下是栈展开过程的 mermaid 流程图:
graph TD;
A[funcB 抛出异常] --> B{funcB 有匹配的 catch 处理程序?};
B -- 否 --> C{funcA 有匹配的 catch 处理程序?};
C -- 否 --> D{main 有匹配的 catch 处理程序?};
D -- 否 --> E[程序终止];
B -- 是 --> F[在 funcB 处理异常];
C -- 是 --> G[在 funcA 处理异常];
D -- 是 --> H[在 main 处理异常];
F --> I[继续执行];
G --> I;
H --> I;
6. 何时使用异常处理
- 提高代码健壮性 :异常处理可以让程序在遇到问题时继续执行,而不是崩溃。例如在文件操作中,如果文件打开失败,程序可以捕获异常并进行相应处理,而不是直接终止。
- 统一错误处理 :在大型项目中,使用异常处理可以提供统一的错误处理机制,方便团队协作。
- 分离错误处理逻辑 :将错误处理逻辑从正常业务逻辑中分离出来,使代码更清晰易读。
7. 构造函数、析构函数与异常处理
在构造函数中,如果发生异常,对象的构造过程会被中断,已经构造的成员对象和基类子对象会被正确析构。在析构函数中,通常不应该抛出异常,因为如果析构函数抛出异常且没有被捕获,程序会调用
std::terminate
终止。
以下是构造函数中异常处理的示例代码:
#include <iostream>
#include <stdexcept>
class MyClass {
public:
MyClass(int value) {
if (value < 0) {
throw std::invalid_argument("Value must be non-negative");
}
// 正常构造逻辑
std::cout << "Object constructed successfully" << std::endl;
}
~MyClass() {
std::cout << "Object destroyed" << std::endl;
}
};
int main() {
try {
MyClass obj(-1);
} catch (const std::invalid_argument& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
8. 异常与继承
异常类可以通过继承形成层次结构。派生类异常可以被基类异常的
catch
处理程序捕获。在设计异常类时,合理使用继承可以提高代码的可维护性和扩展性。
以下是异常继承的示例代码:
#include <iostream>
#include <stdexcept>
class BaseException : public std::runtime_error {
public:
BaseException(const std::string& message) : std::runtime_error(message) {}
};
class DerivedException : public BaseException {
public:
DerivedException(const std::string& message) : BaseException(message) {}
};
void throwDerivedException() {
throw DerivedException("Derived exception occurred");
}
int main() {
try {
throwDerivedException();
} catch (const BaseException& e) {
std::cout << "Caught base exception: " << e.what() << std::endl;
}
return 0;
}
9. 处理
new
失败
当
operator new
无法为对象分配内存时,会抛出
std::bad_alloc
异常。我们可以捕获这个异常并进行相应处理。
以下是处理
new
失败的示例代码:
#include <iostream>
#include <new>
int main() {
try {
while (true) {
char* ptr = new char[1024 * 1024]; // 尝试分配 1MB 内存
}
} catch (const std::bad_alloc& e) {
std::cout << "Memory allocation failed: " << e.what() << std::endl;
}
return 0;
}
10.
unique_ptr
与动态内存分配
unique_ptr
是 C++ 标准库中的智能指针,它可以自动管理动态分配的内存,避免内存泄漏。当
unique_ptr
对象超出作用域时,它会自动释放所指向的内存。
以下是使用
unique_ptr
的示例代码:
#include <iostream>
#include <memory>
void useUniquePtr() {
std::unique_ptr<int> ptr(new int(42));
std::cout << "Value: " << *ptr << std::endl;
// 当 ptr 超出作用域时,内存会自动释放
}
int main() {
useUniquePtr();
return 0;
}
11. 标准库异常层次结构
C++ 标准库定义了一个异常类的层次结构,
std::exception
是所有标准异常类的基类。常见的派生类包括
std::runtime_error
、
std::logic_error
等。
以下是标准库异常层次结构的简单表格:
| 基类 | 派生类 | 描述 |
| ---- | ---- | ---- |
|
std::exception
|
std::runtime_error
| 表示运行时错误 |
|
std::exception
|
std::logic_error
| 表示逻辑错误 |
|
std::runtime_error
|
std::overflow_error
| 表示溢出错误 |
|
std::runtime_error
|
std::underflow_error
| 表示下溢错误 |
12. 总结
通过本文,我们深入学习了 C++ 异常处理的各个方面,包括异常的检测、处理、重新抛出,以及栈展开、构造函数和析构函数中的异常处理等。同时,我们还了解了标准库异常层次结构和如何使用
unique_ptr
防止内存泄漏。合理使用异常处理可以让我们编写更健壮、容错的 C++ 程序。
在实际编程中,我们应该根据具体情况选择合适的异常处理策略,将异常处理融入到系统设计中,提高代码的质量和可维护性。
超级会员免费看
1300

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



