十四天学会C++之第六天:异常处理

1. 异常的基本概念

  • 什么是异常,为什么需要异常处理。
  • C++中的异常处理机制,与传统错误处理方式的对比。

什么是异常?

异常是指在程序执行过程中发生的不正常情况,这些情况可能导致程序无法继续正常执行。这些异常情况包括但不限于:

  • 除数为零的除法操作。
  • 尝试访问不存在的内存地址。
  • 文件不存在或无法打开。
  • 用户输入的数据格式不正确。

传统的错误处理方式通常涉及到检查函数返回值或状态码,然后根据返回值或状态码采取相应的操作。这种方式可能会导致代码变得复杂,因为需要在每个可能出错的地方添加错误检查代码。

异常处理的优势在于它能够将错误处理代码从正常的代码路径中分离出来,使得代码更加清晰和可维护。当异常发生时,程序会跳转到相应的异常处理代码块,而不会继续执行出错的代码。

C++ 中的异常处理机制

C++ 提供了一套强大的异常处理机制,其中包括以下关键字:

  • try:用于标记可能会引发异常的代码块。
  • throw:用于引发异常,并将控制权传递给异常处理程序。
  • catch:用于捕获并处理异常。

示例-演示异常处理的基本结构:

cpp 复制代码
try {
    // 可能引发异常的代码
    int x = 10;
    int y = 0;
    if (y == 0) {
        throw "除数不能为零";
    }
    int result = x / y;
} catch (const char* error) {
    // 捕获并处理异常
    std::cerr << "错误消息: " << error << std::endl;
}

使用 try 标记了可能引发异常的代码块,在其中进行了除法操作。如果除数 y 为零,就会抛出一个异常,异常的类型是 const char* 类型的错误消息。

catch 块中,捕获这个异常,并输出了错误消息。这样,即使出现了异常,程序也能够正常终止,而不会导致崩溃。

2. try-catch块

  • 如何使用try-catch块捕获和处理异常。
  • try块和catch块的语法和作用。
  • 多重catch块的顺序和选择。

使用 try-catch 块捕获和处理异常

try-catch 块是一种异常处理机制,它允许程序员编写代码来处理可能发生的异常情况。下面是 try-catch 块的基本语法:

cpp 复制代码
try {
    // 可能引发异常的代码
} catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
} catch (...) {
    // 处理其他类型的异常
}
  • try 块:在 try 块中编写可能引发异常的代码。
  • catch 块:在 catch 块中编写处理异常的代码。每个 catch 块捕获一种异常类型,可以有多个 catch 块,每个块处理不同类型的异常。
  • ExceptionType:异常类型,指定要捕获和处理的异常类型。
  • e1e2 等:异常对象,用于访问异常的详细信息。

try-catch 块的作用

try-catch 块的作用在于,当 try 块中的代码引发异常时,程序会跳转到与异常类型匹配的 catch 块,并执行其中的代码。这允许我们在异常发生时采取适当的措施,而不会导致程序崩溃。

多个 catch 块按照它们出现的顺序依次检查异常类型,只有第一个匹配的 catch 块会被执行。如果没有匹配的 catch 块,程序将终止,并在控制台上显示异常信息。

多重 catch 块的顺序和选择

在使用多个 catch 块时,需要注意它们的顺序。C++ 将按照 catch 块出现的顺序来检查异常类型,因此应该将最具体的异常类型的 catch 块放在前面,最一般的异常类型的 catch 块放在后面。

cpp 复制代码
try {
    // 可能引发异常的代码
} catch (SpecificExceptionType e) {
    // 处理特定异常类型
} catch (GeneralExceptionType e) {
    // 处理通用异常类型
}

这样做的好处是,程序将首先尝试匹配最具体的异常类型,如果匹配成功,则不会再检查后面的 catch 块。如果没有匹配的 catch 块,才会执行最后一个通用异常类型的 catch 块。

3. 异常类

  • 异常类的概念和作用。
  • C++标准库中的异常类,如std::exception。
  • 创建自定义异常类以处理特定的异常情况。

异常类的概念和作用

异常类是 C++ 中的对象,用于表示异常情况。当程序执行过程中出现错误或不正常情况时,可以抛出(throw)一个异常类的实例,以通知异常处理机制发生了问题。异常类的作用在于:

  1. 标识异常类型:每个异常类都有一个唯一的类型,用于区分不同的异常情况。这有助于准确地捕获和处理特定类型的异常。

  2. 携带异常信息:异常类通常包含有关异常情况的详细信息,如错误消息、错误码等。这些信息可以帮助程序员诊断问题。

C++标准库中的异常类

C++标准库提供了一些常见的异常类,它们都派生自 std::exception 类。这些异常类包括:

  • std::runtime_error:用于表示在运行时发生的错误,通常是程序逻辑错误。
  • std::logic_error:用于表示逻辑错误,如断言失败。
  • std::domain_error:用于表示域错误,通常与数学计算相关。
  • std::invalid_argument:用于表示无效的参数。
  • std::length_error:用于表示长度错误,通常用于容器操作。
  • std::out_of_range:用于表示超出范围的错误,如数组下标越界。

创建自定义异常类

除了使用标准库中的异常类,C++ 允许程序员创建自定义异常类,以处理特定的异常情况。创建自定义异常类的步骤如下:

  1. 创建一个继承自 std::exception 的类。

  2. 在类中添加适当的成员变量和方法,以存储和提供异常信息。

  3. 可选地,重写 what() 方法,以返回异常的描述信息。

下面是一个示例,展示如何创建自定义异常类:

cpp 复制代码
#include <exception>
#include <string>

class MyException : public std::exception {
private:
    std::string errorMessage;

public:
    MyException(const std::string& message) : errorMessage(message) {}

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

在上面的示例中,MyException 类继承自 std::exception,并添加了一个成员变量 errorMessage 用于存储异常信息。重写了 what() 方法,以返回异常信息的 C 字符串表示。

使用自定义异常类时,可以像使用标准库异常类一样,抛出和捕获异常,如下所示:

cpp 复制代码
try {
    // 某些代码
    throw MyException("This is a custom exception.");
} catch (const MyException& e) {
    std::cout << "Caught an exception: " << e.what() << std::endl;
}

通过创建自定义异常类,程序员可以更好地组织和管理异常情况,并提供有用的错误信息来调试问题。

4. 异常规范

  • 异常规范的定义和用法。
  • 如何指定函数可能抛出的异常类型。
  • noexcept关键字的使用。

异常规范的定义和用法

使用 throw()

throw() 是一种异常规范的旧式写法,用于指定函数不会抛出任何异常。例如:

cpp 复制代码
int myFunction() throw() {
    // 函数体
}

在上面的示例中,throw() 表示 myFunction 不会抛出任何异常。如果它抛出异常,程序会终止。这种方式已经被弃用,不再推荐使用,因为它不能准确描述函数可能抛出的异常类型。

使用 noexcept

noexcept 是 C++11 引入的关键字,用于指定函数是否会抛出异常。它有两种形式:

  • noexcept:表示函数不会抛出异常。
  • noexcept(expression):表示函数根据 expression 的结果来决定是否抛出异常。

例如:

cpp 复制代码
int myFunction() noexcept {
    // 函数体
}

int anotherFunction() noexcept(true) {
    // 函数体
}

在上面的示例中,myFunctionanotherFunction 都使用了 noexcept 关键字,表示它们不会抛出异常。如果在 noexcept 函数中抛出了异常,程序会终止。

异常规范的注意事项

  • 异常规范在实际中的使用较少,因为它们不能精确指定函数可能抛出的异常类型,而且容易引入不必要的复杂性。

  • 在 C++11 及以后的标准中,更推荐使用异常安全的编程技巧和 try-catch 块来处理异常,而不是依赖异常规范。

  • 在函数声明中使用异常规范不是强制要求,而是一种附加信息。如果函数抛出了未在异常规范中列出的异常类型,程序仍会终止。

5. 示例和练习

  • 提供异常处理的示例代码,演示try-catch块的使用和自定义异常类的创建。
  • 给读者提供练习题,以加强对异常处理的理解和实践。

示例:使用try-catch块处理异常

cpp 复制代码
#include <iostream>

int divide(int x, int y) {
    if (y == 0) {
        throw std::runtime_error("除数不能为零!");
    }
    return x / y;
}

int main() {
    int a = 10;
    int b = 0;
    
    try {
        int result = divide(a, b);
        std::cout << "结果是:" << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "异常捕获:" << e.what() << std::endl;
    }

    return 0;
}

在上面的示例中,我们定义了一个 divide 函数,它用于计算两个整数的商。如果除数为零,它会抛出一个 std::runtime_error 异常。在 main 函数中,我们使用 try-catch 块捕获可能抛出的异常,以防止程序终止。

练习

问题1: 创建一个函数 int safeDivide(int x, int y),安全地处理除法,避免除以零的情况。

cpp 复制代码
#include <iostream>

int safeDivide(int x, int y) {
    if (y == 0) {
        std::cerr << "错误:除数不能为零!" << std::endl;
        return 0; // 返回一个合理的默认值,例如0
    }
    return x / y;
}

int main() {
    int a = 10;
    int b = 0;
    
    int result = safeDivide(a, b);
    
    std::cout << "结果是:" << result << std::endl;

    return 0;
}

代码中的 safeDivide 函数首先检查除数是否为零。如果除数为零,它将在标准错误流 (std::cerr) 中显示错误消息,并返回一个合理的默认值(这里是0)。这样可以避免抛出异常,程序不会终止。

问题2: 创建一个自定义异常类 FileOpenException,用于处理文件打开时可能出现的异常情况。

cpp 复制代码
#include <iostream>
#include <stdexcept>

class FileOpenException : public std::exception {
public:
    FileOpenException(const std::string& message) : errorMessage(message) {}

    const char* what() const noexcept override {
        return errorMessage.c_str();
    }

private:
    std::string errorMessage;
};

void openFile(const std::string& filename) {
    // 模拟文件打开失败的情况
    throw FileOpenException("无法打开文件: " + filename);
}

int main() {
    std::string filename = "example.txt";
    
    try {
        openFile(filename);
    } catch (const FileOpenException& e) {
        std::cerr << "文件打开失败:" << e.what() << std::endl;
    }

    return 0;
}

代码中,创建一个名为 FileOpenException 的自定义异常类,它继承自 std::exception。该异常类包含一个描述错误的字符串成员 errorMessage 和一个 what() 函数,用于返回错误消息。

openFile 函数中,模拟文件打开失败的情况,并抛出 FileOpenException 异常。在 main 函数中,我们使用 try-catch 块捕获可能抛出的异常,并在控制台上显示错误消息。

问题3: 编写一个程序,尝试打开一个不存在的文件,然后捕获可能抛出的 FileOpenException 异常,并在控制台上显示错误消息。

cpp 复制代码
#include <iostream>
#include <stdexcept>

class FileOpenException : public std::exception {
public:
    FileOpenException(const std::string& message) : errorMessage(message) {}

    const char* what() const noexcept override {
        return errorMessage.c_str();
    }

private:
    std::string errorMessage;
};

void openFile(const std::string& filename) {
    // 模拟文件打开失败的情况
    throw FileOpenException("无法打开文件: " + filename);
}

int main() {
    std::string filename = "nonexistent.txt";
    
    try {
        openFile(filename);
    } catch (const FileOpenException& e) {
        std::cerr << "文件打开失败:" << e.what() << std::endl;
    }

    return 0;
}

示例中,继续使用之前创建的 FileOpenException 自定义异常类。在 main 函数中,尝试打开一个不存在的文件,当抛出 FileOpenException 异常时,捕获它并在控制台上显示错误消息。

问题4: 创建一个函数 double calculateSquareRoot(double x),计算一个正数的平方根,处理负数情况。

cpp 复制代码
#include <iostream>
#include <cmath>
#include <stdexcept>

class NegativeNumberException : public std::exception {
public:
    NegativeNumberException(const std::string& message) : errorMessage(message) {}

    const char* what() const noexcept override {
        return errorMessage.c_str();
    }

private:
    std::string errorMessage;
};

double calculateSquareRoot(double x) {
    if (x < 0.0) {
        throw NegativeNumberException("负数没有实数平方根");
    }
    return std::sqrt(x);
}

int main() {
    double number = -4.0;
    
    try {
        double result = calculateSquareRoot(number);
        std::cout << "平方根:" << result << std::endl;
    } catch (const NegativeNumberException& e) {
        std::cerr << "错误:" << e.what() << std::endl;
    }

    return 0;
}

代码中,创建一个自定义异常类 NegativeNumberException,用于处理负数情况。calculateSquareRoot 函数首先检查输入值是否为负数,如果是,就抛出 NegativeNumberException 异常。在 main 函数中,我们测试了这个函数,包括正数和负数的情况。

问题5: 编写一个程序,调用 calculateSquareRoot 函数,测试它的行为,包括处理正数和负数的情况。

cpp 复制代码
#include <iostream>
#include <cmath>
#include <stdexcept>

class NegativeNumberException : public std::exception {
public:
    NegativeNumberException(const std::string& message) : errorMessage(message) {}

    const char* what() const noexcept override {
        return errorMessage.c_str();
    }

private:
    std::string errorMessage;
};

double calculateSquareRoot(double x) {
    if (x < 0.0) {
        throw NegativeNumberException("负数没有实数平方根");
    }
    return std::sqrt(x);
}

int main() {
    double positiveNumber = 25.0;
    double negativeNumber = -4.0;
    
    try {
        double result1 = calculateSquareRoot(positiveNumber);
        std::cout << "正数的平方根:" << result1 << std::endl;

        double result2 = calculateSquareRoot(negativeNumber);
        std::cout << "负数的平方根:" << result2 << std::endl;
    } catch (const NegativeNumberException& e) {
        std::cerr << "错误:" << e.what() << std::endl;
    }

    return 0;
}

main 函数中分别测试了 calculateSquareRoot 函数对于正数和负数的行为。当输入为正数时,它返回实数平方根;当输入为负数时,它抛出 NegativeNumberException 异常并显示错误消息。

相关推荐
一点媛艺1 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风1 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生2 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功2 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
pianmian12 小时前
python数据结构基础(7)
数据结构·算法
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
2401_850410832 小时前
文件系统和日志管理
linux·运维·服务器
老猿讲编程2 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp