← 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 物件處理,任務依序組織,即使失敗也不會破壞現有狀態。