35、异常处理

厨子大约 6 分钟C++C++基础编程程序厨

异常处理是一项重要的特性,它允许程序在运行时遇到错误条件时能够优雅地恢复或终止执行,而不是简单地崩溃。

C++通过trycatchthrow三个关键字来实现异常处理机制。本文会详细介绍。

基本概念

定义

异常是指在程序执行过程中发生的、不符合程序正常流程的事件。在C++中,异常通常是由于某些错误条件触发的,如除以零、数组越界、内存分配失败等。

关键字

  • try:用于标记可能会抛出异常的代码块,称为保护代码。
  • throw:当检测到异常条件时,使用throw关键字抛出一个异常。throw后面可以跟任意表达式,它的类型决定了抛出的异常类型。
  • catch:用于捕获并处理异常。catch块紧跟在try块之后,并指定了要捕获的异常类型。

用法

下面是一个简单的例子,演示了如何使用trythrowcatch来处理除以零的异常:

#include <iostream>
using namespace std;

double division(int a, int b) {
    if (b == 0) {
        throw "Division by zero!"; // 抛出异常
    }
    return (a / b);
}

int main() {
    int x = 50;
    int y = 0;
    double z = 0;
    try {
        z = division(x, y); // 可能抛出异常的代码
        cout << z << endl;
    } catch (const char* msg) { // 捕获并处理异常
        cerr << msg << endl;
    }
    return 0;
}

在这个例子中,如果y为零,division函数将抛出一个字符串异常,该异常在main函数的catch块中被捕获并处理。

一个try块可以跟随多个catch块,来捕获多种不同类型的异常:

#include <iostream>
#include <stdexcept> // 包含标准异常类
using namespace std; // 为了演示方便,项目中不建议这样使用

void testFunction() {
    throw runtime_error("Runtime error occurred!");
}

int main() {
    try {
        testFunction(); // 可能抛出异常的函数
    } catch (const logic_error& e) {
        cout << "Caught a logic_error: " << e.what() << endl;
    } catch (const runtime_error& e) {
        cout << "Caught a runtime_error: " << e.what() << endl;
    } catch (...) { // 捕获所有其他类型的异常
        cout << "Caught an unknown exception" << endl;
    }
    return 0;
}

testFunction抛出了一个runtime_error异常,该异常在第二个catch块中被捕获并处理。最后一个catch块使用省略号...来捕获所有其他类型的异常。

C++标准异常类

C++标准库提供了一系列预定义的异常类,这些类都继承自std::exception基类。

使用标准异常类可以使代码更加清晰。

层次结构

如图

暂时无法在飞书文档外展示此内容

  • std::exception:所有标准异常的基类。
  • std::bad_alloc:内存分配失败时抛出。
  • std::bad_cast:动态类型转换失败时抛出。
  • std::bad_typeid:使用typeid运算符失败时抛出。
  • std::bad_exception:在函数声明中使用了异常规格,但抛出了未列出的异常时抛出(C++11已弃用)。
  • std::logic_error:逻辑错误异常基类,包括:
    • std::domain_error:数学域错误,如sqrt(-1)。
    • std::invalid_argument:无效参数错误。
    • std::length_error:超出允许长度的错误。
    • std::out_of_range:范围错误,如访问vector的非法索引。
  • std::runtime_error:运行时错误异常基类,包括:
    • std::overflow_error:上溢错误。
    • std::range_error:范围错误(与std::out_of_range不同,用于其他情况)。
    • std::underflow_error:下溢错误。

示例

#include <iostream>
#include <stdexcept>
using namespace std;

void testLogicError() {
    throw invalid_argument("Invalid argument error!");
}

void testRuntimeError() {
    throw out_of_range("Out of range error!");
}

int main() {
    try {
        testLogicError(); // 抛出逻辑错误异常
    } catch (const logic_error& e) {
        cout << "Caught a logic_error: " << e.what() << endl;
    }

    try {
        testRuntimeError(); // 抛出运行时错误异常
    } catch (const runtime_error& e) {
        cout << "Caught a runtime_error: " << e.what() << endl;
    }

    return 0;
}

testLogicError函数抛出了一个invalid_argument异常,而testRuntimeError函数抛出了一个out_of_range异常。这两个异常分别在对应的catch块中被捕获并处理。

自定义异常类

虽然C++标准库提供了丰富的异常类,但在某些情况下,开发者可能需要定义自己的异常类。

我们可以通过继承std::exception基类并重载what方法来实现。

示例

#include <iostream>
#include <exception>
#include <string>
using namespace std;

class MyException : public exception {
public:
    MyException(const string& message) : message_(message) {}

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

private:
    string message_;
};

void testCustomException() {
    throw MyException("Custom exception occurred!");
}

int main() {
    try {
        testCustomException(); // 抛出自定义异常
    } catch (const MyException& e) {
        cout << "Caught a MyException: " << e.what() << endl;
    } catch (const exception& e) {
        cout << "Caught an unknown exception: " << e.what() << endl;
    }

    return 0;
}

MyException类继承自std::exception并重载了what方法。testCustomException函数抛出了一个MyException异常,该异常在main函数的catch块中被捕获并处理。

注意,这里还添加了一个捕获所有其他std::exception子类的catch块,确保能够捕获所有未知异常。

noexcept

从C++11开始,推荐使用noexcept关键字来声明函数不抛出任何异常:

void func() noexcept; // 声明函数不抛出任何异常

如果func函数在执行过程中抛出了异常,程序会直接终止。noexcept关键字还可以用于提高性能,因为编译器可以优化不抛出异常的函数调用。

如果你看过gcc源码,你会发现,基本上通篇都是noexcept

noexcept会告诉编译器,它修饰的函数不会产生异常(exception),这有利于编译器做更多的优化。

C++的异常处理是在运行时检测的,而不是在编译时检测,为了运行时检测,编译器应该会做些额外的操作,如果能够通过noexcept明确的告诉编译器这个函数不会抛出异常,编译器应该会做一些优化。

验证函数是否**noexcept****?**

noexcept还可以当作运算符,它可以传入参数,来验证某个函数是否是noexcept

void may_throw();
void no_throw() noexcept;
int main() {
  noexcept(may_throw()); // false
  noexcept(no_throw()); // true
}

什么时候使用**noexcept****?**

  • 移动构造函数,移动赋值函数,建议使用noexcept修饰,因为搭配标准库使用时,noexcept作用巨大,它可以优先移动而非拷贝,推荐阅读 move_if_noexceptopen in new window
  • 而析构函数默认就是noexcept的,不需要显式指定noexcept
  • 在明确确认某个函数不会产生exception时,可以使用noexcept

推荐看看这几个noexcept相关的文档:

异常安全性

异常安全性是指程序在遇到异常时仍然能够保持正确的状态,不会出现资源泄露或者数据不一致等问题。

因为C++允许程序在执行过程中抛出异常,这可能导致程序的控制流发生变化,如果资源管理不当,就可能出现资源泄露或程序崩溃等问题。