← SEARCHABLE ARCHIVE
C / C++ · 2026-08-09

从 C++ 基础到掌握 — 第 15 集

即使发生异常也能安全地管理资源

INTERACTIVE READING MODE →

该博客上撰写的文章是出于教学目的而创建的。 另外,文章中的信息可能不正确,请谨慎。

如果读取文件并分配内存后中间代码出现问题,则必须清理已经获得的资源。 如果您为每个返回和异常路径手动编写清理代码,则很容易错过它。

如果出现异常

异常是向调用方传达当前位置无法处理的故障的一种方式。

Item LoadItem(int id)
{
    if (id <= 0)
    {
        throw std::invalid_argument("id must be positive");
    }

    return ReadItemFromFile(id);
}

try
{
    Item item = LoadItem(inputId);
}
catch (const std::exception& error)
{
    std::cerr << error.what() << '\n';
}

当执行 throw 时,会跳过当前行下面的代码并找到匹配的 catch。 在此过程中,已创建的本地对象的析构函数被调用。

并非所有失败都需要例外。 预期的结果,例如项目搜索失败,作为可选或布尔值可能会更明显。

使用 RAII 进行组织

通过将资源的生命周期与对象的生命周期联系起来,无需分离正常返回路径和异常路径。

std::string ReadConfig(const std::filesystem::path& path)
{
    std::ifstream file(path);
    if (!file)
    {
        throw std::runtime_error("config open failed");
    }

    std::string text(
        std::istreambuf_iterator<char>{file},
        std::istreambuf_iterator<char>{}
    );

    return text;
}

即使由于字符串创建在中间失败而发生异常,ifstream 的析构函数也会关闭该文件。 uniqueptr 用于动态对象,uniqueptr 用于锁std::lock_guard,容器内存的作用与向量相同。

void UpdateInventory()
{
    std::lock_guard<std::mutex> lock(inventoryMutex);
    RebuildInventory();
}

即使 RebuildInventory 中发生异常,锁析构函数也会释放互斥体。 它减少了直接配对锁定和解锁时可能发生的错误。

异常安全

您需要考虑当异常发生时您的函数保证什么状态。

  • 基本保证:不存在资源泄漏,对象保持可用。
  • 强保证:如果操作失败,保持调用前的状态。
  • 保证无异常:该函数不会抛出任何异常。

通过在临时对象中完成新状态并在最后替换它,可以轻松创建强有力的保证。

void Inventory::ReplaceItems(std::vector<Item> newItems)
{
    items.swap(newItems);
}

如果创建调用参数时失败,现有项目将不会更改。 在函数内部,通过 swap 完成的新列表替换了现有列表。

noexcept

对于不抛出异常的函数,noexcept可以显示。

Player(Player&& other) noexcept = default;
Player& operator=(Player&& other) noexcept = default;

向量决定了重新分配元素时是否可以安全地移动。 如果移动构造函数是noexcept,那么很可能会选择移动而不是复制。

不过内部可能会出现异常,但是如果加上noexcept的话,异常一出去的瞬间,std::terminate被称为。 它不是为了性能而盲目添加的关键字,而是承诺不会真正抛出异常。

异常安全的关键是不要编写大量的 catch。 资源由 RAII 对象处理,任务按顺序组织,即使失败也不会破坏现有状态。