跳到主要内容

C++ throw语句

什么是throw语句

在C++编程中,throw语句是异常处理机制的核心组成部分,它用于在程序执行过程中引发(或抛出)一个异常。当程序遇到无法处理的情况时,可以使用throw语句中断正常的程序流程,并将控制权转交给异常处理代码。

异常处理允许我们将"异常情况的检测"与"异常情况的处理"分离,这使得代码更加清晰、易于维护。

throw语句的基本语法

throw语句的基本语法非常简单:

cpp
throw 表达式;

这里的"表达式"可以是任何类型的值:整数、字符串、对象,甚至是自定义的异常类对象。

工作原理

当执行到throw语句时,会发生以下步骤:

  1. 程序立即停止当前函数的执行
  2. 开始查找匹配的catch
  3. 如果在当前函数中找到匹配的catch块,则执行该块中的代码
  4. 如果当前函数中没有匹配的catch块,程序会沿着调用链向上查找
  5. 如果最终没有找到匹配的catch块,程序将调用std::terminate()函数终止运行

基本使用示例

让我们看一个简单的示例,展示如何使用throw语句:

cpp
#include <iostream>

double divide(double a, double b) {
if (b == 0) {
throw "除数不能为零!"; // 抛出字符串类型的异常
}
return a / b;
}

int main() {
try {
double result = divide(10, 2);
std::cout << "结果: " << result << std::endl;

result = divide(20, 0); // 这里会抛出异常
std::cout << "这行代码不会执行" << std::endl;
}
catch (const char* message) {
std::cout << "捕获到异常: " << message << std::endl;
}

std::cout << "程序继续执行..." << std::endl;

return 0;
}

输出结果:

结果: 5
捕获到异常: 除数不能为零!
程序继续执行...

在这个例子中:

  • divide函数在遇到除数为零时抛出一个字符串异常
  • main函数中的try块尝试执行可能抛出异常的代码
  • 当异常发生时,程序跳过try块中剩余的代码,直接执行匹配的catch
  • catch块处理完异常后,程序继续执行catch块之后的代码

抛出不同类型的异常

C++允许抛出几乎任何类型的异常:

cpp
#include <iostream>
#include <string>
#include <stdexcept>

void test_exceptions() {
int choice;
std::cout << "输入一个数字(1-4)来选择要抛出的异常类型: ";
std::cin >> choice;

switch(choice) {
case 1:
throw 42; // 整数类型异常
case 2:
throw "C风格字符串异常"; // 字符串字面量异常
case 3:
throw std::string("std::string异常"); // 标准字符串异常
case 4:
throw std::runtime_error("标准异常"); // 标准库异常
default:
std::cout << "没有抛出异常" << std::endl;
}
}

int main() {
try {
test_exceptions();
}
catch (int e) {
std::cout << "捕获到整数异常: " << e << std::endl;
}
catch (const char* e) {
std::cout << "捕获到C风格字符串异常: " << e << std::endl;
}
catch (const std::string& e) {
std::cout << "捕获到std::string异常: " << e << std::endl;
}
catch (const std::exception& e) {
std::cout << "捕获到标准异常: " << e.what() << std::endl;
}
catch (...) {
std::cout << "捕获到未知类型的异常" << std::endl;
}

return 0;
}
提示

上面的代码中使用了catch(...),这是一个通用的异常捕获器,可以捕获任何类型的异常。建议将它放在所有catch块的最后,作为最后的防线。

使用自定义异常类

在实际开发中,通常会创建自定义异常类来提供更多的上下文信息:

cpp
#include <iostream>
#include <string>
#include <exception>

// 自定义异常类
class MathException : public std::exception {
private:
std::string m_message;

public:
MathException(const std::string& message) : m_message(message) {}

// 重写what()方法以返回异常信息
const char* what() const noexcept override {
return m_message.c_str();
}
};

double divide(double a, double b) {
if (b == 0) {
throw MathException("数学错误:除数不能为零!");
}
return a / b;
}

int main() {
try {
std::cout << divide(10, 0) << std::endl;
}
catch (const MathException& e) {
std::cout << "捕获到数学异常: " << e.what() << std::endl;
}
catch (const std::exception& e) {
std::cout << "捕获到标准异常: " << e.what() << std::endl;
}

return 0;
}

输出结果:

捕获到数学异常: 数学错误:除数不能为零!

重新抛出异常

在某些情况下,您可能希望在catch块中执行一些操作后重新抛出异常以便由外层函数处理:

cpp
#include <iostream>
#include <exception>

void function_b() {
std::cout << "进入function_b" << std::endl;
throw std::runtime_error("function_b中发生异常");
}

void function_a() {
std::cout << "进入function_a" << std::endl;
try {
function_b();
}
catch (const std::exception& e) {
std::cout << "function_a捕获到异常: " << e.what() << std::endl;
std::cout << "在function_a中执行一些清理工作..." << std::endl;
throw; // 重新抛出当前异常
}
}

int main() {
try {
function_a();
}
catch (const std::exception& e) {
std::cout << "main捕获到异常: " << e.what() << std::endl;
}

return 0;
}

输出结果:

进入function_a
进入function_b
function_a捕获到异常: function_b中发生异常
在function_a中执行一些清理工作...
main捕获到异常: function_b中发生异常

使用throw;语句(不带任何表达式)可以重新抛出当前捕获的异常,保持异常对象的原始类型和信息。

throw表达式与noexcept说明符

C++11引入了noexcept说明符,用于指定函数不会抛出异常:

cpp
void safe_function() noexcept {
// 这个函数承诺不会抛出异常
// 如果抛出了异常,程序会立即调用std::terminate()
}

void might_throw() {
// 这个函数可能会抛出异常
throw std::runtime_error("出错了");
}

如果一个被标记为noexcept的函数抛出了异常,程序会直接调用std::terminate()终止运行,不会尝试异常处理。

实际应用场景

文件操作中的异常处理

cpp
#include <iostream>
#include <fstream>
#include <string>

void read_file(const std::string& filename) {
std::ifstream file(filename);

if (!file.is_open()) {
throw std::runtime_error("无法打开文件: " + filename);
}

std::string line;
std::cout << "文件内容: " << std::endl;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}

file.close();
}

int main() {
try {
read_file("存在的文件.txt");
read_file("不存在的文件.txt");
}
catch (const std::exception& e) {
std::cout << "错误: " << e.what() << std::endl;
}

return 0;
}

资源管理中的异常处理

cpp
#include <iostream>
#include <memory>
#include <vector>

class Resource {
public:
Resource(int id) : m_id(id) {
std::cout << "Resource " << m_id << " 已分配" << std::endl;
}

~Resource() {
std::cout << "Resource " << m_id << " 已释放" << std::endl;
}

void use() {
std::cout << "使用 Resource " << m_id << std::endl;
}

private:
int m_id;
};

void function_with_resources() {
// 使用智能指针自动管理资源
auto resource1 = std::make_unique<Resource>(1);

try {
auto resource2 = std::make_unique<Resource>(2);

std::cout << "执行可能抛出异常的操作" << std::endl;
throw std::runtime_error("操作失败");

// 这行代码不会执行
std::cout << "操作成功完成" << std::endl;
}
catch (const std::exception& e) {
std::cout << "捕获到异常: " << e.what() << std::endl;
}

// 无论异常是否发生,resource1和resource2都会被正确释放
std::cout << "function_with_resources 函数结束" << std::endl;
}

int main() {
function_with_resources();
return 0;
}

输出结果:

Resource 1 已分配
Resource 2 已分配
执行可能抛出异常的操作
捕获到异常: 操作失败
Resource 2 已释放
function_with_resources 函数结束
Resource 1 已释放

这个例子展示了如何使用智能指针在发生异常时正确释放资源,避免内存泄漏。

最佳实践

使用异常时,请遵循以下最佳实践:

  1. 只在异常情况下使用异常:异常应该用于处理非预期的、错误的情况,而不是用于常规的控制流程。

  2. 选择合适的异常类型

    • 使用标准异常类(如std::runtime_errorstd::logic_error等)
    • 或者创建从std::exception继承的自定义异常类
  3. 明确异常安全性:确保代码在发生异常时不会泄露资源或留下对象处于无效状态。

  4. 使用RAII(资源获取即初始化):通过智能指针和其他RAII技术确保资源在异常发生时能正确释放。

  5. 在文档中说明函数的异常规范:让其他开发者知道您的函数可能会抛出哪些类型的异常。

警告

过度使用异常可能导致代码难以理解和维护。在性能关键的代码段中,异常处理也可能引入额外的开销。

总结

本教程介绍了C++中throw语句的用法和工作原理:

  1. throw语句用于在程序中抛出异常,中断正常的程序流程
  2. 异常可以是任何类型:基本类型、字符串、对象或自定义异常类
  3. 抛出的异常会沿着调用栈向上传播,直到找到匹配的catch
  4. 重新抛出异常可以使用不带表达式的throw;语句
  5. 异常处理应该与资源管理结合使用,确保在发生异常时正确释放资源

掌握throw语句和异常处理机制可以帮助您编写更健壮的C++程序,正确处理各种错误情况。

练习

  1. 创建一个简单的计算器程序,使用异常处理来处理除零错误和无效输入。

  2. 实现一个自定义异常类层次结构,包含基类ApplicationError和派生类NetworkErrorFileErrorInputError,并在适当的情况下抛出这些异常。

  3. 编写一个函数,该函数在内部分配动态内存,确保即使在发生异常时也不会发生内存泄漏。

  4. 修改下面的代码,使其能正确处理所有可能的异常并避免资源泄漏:

cpp
void process_data(const std::string& filename) {
int* data = new int[1000];
std::ifstream file(filename);
// 读取和处理数据的代码
// ...
delete[] data;
file.close();
}

进一步阅读