
在 C++ 编程中,错误处理是一个至关重要的方面。当程序运行时,可能会遇到各种意外情况,如文件打开失败、内存分配不足、数组越界等。如果不妥善处理这些错误,程序可能会崩溃,导致数据丢失或产生不可预测的结果。C++ 提供了异常处理机制,通过 try 块、catch 块和 throw 语句,我们可以更优雅地处理这些错误,提高程序的健壮性和可维护性。
异常是程序在执行过程中遇到的错误或异常情况。例如,当我们尝试除以零、访问不存在的文件或数组越界时,就会产生异常。异常可以分为两类:同步异常和异步异常。同步异常是由程序的特定操作引发的,如除以零;而异步异常则是由外部事件引发的,如硬件故障或用户中断。
在传统的C风格错误处理中,我们通常使用返回值或错误码来处理异常情况。这种方式存在三个主要问题:
C++异常机制通过以下特性解决这些问题:
C++ 异常处理的基本结构由 try 块、catch 块和 throw 语句组成。
try {
// 可能抛出异常的代码
throw SomeException();
}
catch (const SomeException& e) {
// 处理特定异常
}
catch (...) {
// 处理所有其他异常
}try 块:包含可能会抛出异常的代码。当 try 块中的代码抛出异常时,程序会立即停止执行 try 块中的剩余代码,并跳转到相应的 catch 块中进行异常处理。catch 块:用于捕获和处理异常。每个 catch 块都有一个异常类型参数,用于指定该 catch 块可以捕获的异常类型。当 try 块中抛出的异常类型与某个 catch 块的异常类型匹配时,该 catch 块会被执行。throw 语句:用于抛出异常。可以在程序中使用 throw 语句手动抛出异常,也可以使用 C++ 标准库提供的异常类来抛出异常。① throw表达式
void processFile(const string& filename) {
ifstream file(filename);
if (!file) {
throw runtime_error("文件打开失败: " + filename);
}
// 文件处理逻辑...
}②try-catch块
try {
processFile("data.txt");
}
catch (const runtime_error& e) {
cerr << "错误: " << e.what() << endl;
}栈展开过程:
安全等级 | 描述 |
|---|---|
基本保证 | 资源不泄漏,对象保持有效状态 |
强保证 | 操作要么完全成功,要么无影响 |
无异常保证 | 承诺不抛出任何异常 |
// 强保证示例:事务性操作
void transaction() {
auto temp = resource; // 创建副本
modify(temp); // 修改副本
swap(resource, temp); // 原子性交换
}try 块和 catch 块的使用try-catch 示例下面是一个简单的 try-catch 示例,演示如何处理除以零的异常:
#include <iostream>
int main() {
int numerator = 10;
int denominator = 0;
try {
if (denominator == 0) {
throw "Division by zero!";
}
int result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
} catch (const char* error) {
std::cerr << "Error: " << error << std::endl;
}
return 0;
}
在 try 块中检查分母是否为零。如果分母为零,使用 throw 语句抛出一个字符串常量 "Division by zero!"。然后,程序会跳转到 catch 块中,该 catch 块的异常类型为 const char*,用于捕获字符串常量类型的异常。在 catch 块中,输出错误信息。
catch 块的使用可以在一个 try 块后面使用多个 catch 块,每个 catch 块用于捕获不同类型的异常。下面是一个示例:
#include <iostream>
#include <stdexcept> // 包含标准异常类
int main() {
int numerator = 10;
int denominator = 0;
try {
if (denominator == 0) {
throw std::runtime_error("Division by zero!");
}
int result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what() << std::endl;
} catch (const char* error) {
std::cerr << "Error: " << error << std::endl;
}
return 0;
}
抛出了一个 std::runtime_error 类型的异常。std::runtime_error 是 C++ 标准库提供的一个异常类,用于表示运行时错误。使用两个 catch 块来捕获不同类型的异常,第一个 catch 块用于捕获 std::runtime_error 类型的异常,第二个 catch 块用于捕获字符串常量类型的异常。
catch 块的匹配规则当 try 块中抛出异常时,程序会按照 catch 块的顺序依次检查每个 catch 块的异常类型是否与抛出的异常类型匹配。匹配规则如下:
std::runtime_error,那么可以被 catch (const std::runtime_error& e) 或 catch (const std::exception& e) 捕获,因为 std::runtime_error 是 std::exception 的派生类。catch 块,程序会执行该 catch 块中的代码,并跳过其他 catch 块。catch 块,程序会调用 std::terminate() 函数,终止程序的执行。catch 块的参数传递catch 块的参数可以是值传递、引用传递或指针传递。通常建议使用引用传递,因为引用传递可以避免对象的复制,提高性能。下面是一个使用引用传递的示例:
#include <iostream>
class MyException {
public:
MyException(const std::string& message) : message_(message) {}
const std::string& what() const { return message_; }
private:
std::string message_;
};
int main() {
try {
throw MyException("This is a custom exception!");
} catch (const MyException& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
定义了一个自定义的异常类 MyException,并在 try 块中抛出了一个 MyException 对象。在 catch 块中,使用引用传递来捕获异常对象,并调用 what() 方法输出异常信息。
throw 语句的使用可以在程序中使用 throw 语句手动抛出异常。throw 语句的语法如下:
throw expression; 其中,expression 可以是任意类型的表达式,通常是一个异常对象。下面是一个手动抛出异常的示例:
#include <iostream>
#include <stdexcept> // 包含标准异常类
void divide(int numerator, int denominator) {
if (denominator == 0) {
throw std::invalid_argument("Denominator cannot be zero!");
}
int result = numerator / denominator;
std::cout << "Result: " << result << std::endl;
}
int main() {
try {
divide(10, 0);
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
divide 函数用于进行除法运算。在函数中,检查分母是否为零,如果为零,则抛出一个 std::invalid_argument 类型的异常。在 main 函数中,调用 divide 函数,并使用 try-catch 块来捕获和处理异常。
在 catch 块中,可以使用 throw 语句重新抛出异常。重新抛出异常可以将异常传递给上一层的 try-catch 块进行处理。下面是一个重新抛出异常的示例:
#include <iostream>
#include <stdexcept> // 包含标准异常类
void func() {
try {
throw std::runtime_error("Something went wrong!");
} catch (const std::runtime_error& e) {
std::cerr << "Caught exception in func(): " << e.what() << std::endl;
throw; // 重新抛出异常
}
}
int main() {
try {
func();
} catch (const std::runtime_error& e) {
std::cerr << "Caught exception in main(): " << e.what() << std::endl;
}
return 0;
}
func 函数中抛出了一个 std::runtime_error 类型的异常,并在 catch 块中重新抛出该异常。在 main 函数中,再次捕获该异常,并输出异常信息。
exception
├── logic_error
│ ├── invalid_argument
│ ├── domain_error
│ ├── length_error
│ └── out_of_range
├── runtime_error
│ ├── range_error
│ ├── overflow_error
│ └── underflow_error
└── bad_allocC++ 标准库提供了一系列的异常类,这些异常类构成了一个继承层次结构。顶层的异常类是 std::exception,它是所有标准异常类的基类。std::exception 类定义了一个纯虚函数 what(),用于返回异常信息。
以下是一些常见的标准异常类:
std::logic_error:表示逻辑错误,如无效的参数、越界访问等。std::runtime_error:表示运行时错误,如内存分配失败、文件打开失败等。std::bad_alloc:表示内存分配失败。std::out_of_range:表示越界访问。可以直接使用 C++ 标准库提供的异常类来抛出和捕获异常。下面是一个使用 std::out_of_range 异常类的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
try {
int value = vec.at(10); // 越界访问
std::cout << "Value: " << value << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
使用 std::vector 的 at() 方法访问向量的元素。如果访问的索引越界,at() 方法会抛出一个 std::out_of_range 类型的异常。使用 try-catch 块来捕获和处理该异常。
除了使用 C++ 标准库提供的异常类,还可以自定义异常类。自定义异常类通常继承自 std::exception 或其派生类,并实现 what() 方法。下面是一个自定义异常类的示例:
#include <iostream>
#include <stdexcept> // 包含标准异常类
#include <string>
class MyException : public std::exception {
public:
MyException(const std::string& message) : message_(message) {}
const char* what() const noexcept override {
return message_.c_str();
}
private:
std::string message_;
};
void doSomething() {
throw MyException("This is a custom exception!");
}
int main() {
try {
doSomething();
} catch (const MyException& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
定义了一个自定义异常类 MyException,它继承自 std::exception。在 MyException 类中,实现了 what() 方法,用于返回异常信息。在 doSomething 函数中,抛出了一个 MyException 对象。在 main 函数中,使用 try-catch 块来捕获和处理该异常。
自定义异常类可以使代码更加清晰和易于维护。通过自定义异常类,可以为不同的错误情况定义不同的异常类型,从而使异常处理更加精确。例如,可以为文件操作定义一个 FileException 类,为数据库操作定义一个 DatabaseException 类,这样在捕获异常时,可以根据异常类型来区分不同的错误情况。
异常处理是一种比较昂贵的操作,因为它涉及到栈展开和对象析构等操作。因此,我们应该避免在性能敏感的代码中过度使用异常。对于一些可以通过简单的错误检查来处理的情况,应该优先使用条件判断语句。
在编写代码时,我们应该考虑异常安全性。异常安全性是指在异常发生时,程序能够保持数据的一致性和完整性。为了实现异常安全性,可以使用 RAII(资源获取即初始化)技术,确保资源在对象的生命周期内被正确管理。例如,使用 std::unique_ptr 和 std::shared_ptr 来管理动态分配的内存,使用 std::lock_guard 来管理互斥锁。
在处理异常时,我们应该考虑异常的传递和记录。对于一些无法在当前函数中处理的异常,应该将其传递给上一层的调用者进行处理。同时,应该记录异常信息,以便在调试和维护时能够快速定位问题。可以使用日志库来记录异常信息,如 spdlog 或 glog。
void riskyFunction() {
throw runtime_error("未处理异常");
}
int main() {
riskyFunction();
// 程序终止,输出terminate called after throwing an instance of...
}解决方案:使用全局异常捕获
int main() try {
riskyFunction();
}
catch (...) {
cerr << "未知异常" << endl;
}void process() {
int* arr = new int[100];
throw exception();
delete[] arr; // 永远不会执行
}解决方案:使用智能指针
void safeProcess() {
auto arr = make_unique<int[]>(100);
throw exception();
// 自动释放内存
}是否可恢复? --> 是 --> 使用异常
|
否 --> 使用断言/终止程序noexcept标记不会抛出异常的函数
std::make_shared/std::make_unique
std::terminate
通过合理运用异常处理机制,可以构建出更健壮、更易维护的C++应用程序。理解异常处理的底层原理,结合现代C++特性,能够帮助我们在保证程序可靠性的同时,兼顾代码的执行效率。
希望这篇博客能够帮助你更好地理解 C++ 中的 try 块和异常处理。如果你有任何疑问或建议,欢迎在评论区留言。
using声明在模板编程中有着重要应用,如定义模板类型别名等。