C++基礎教程面向對象(學習筆記(80))

異常危險和缺點

與幾乎所有有益的東西一樣,異常也存在一些潛在的缺點。本文並不是全面的,只是指出在使用異常(或決定是否使用它們)時應該考慮的一些主要問題。

清理資源

使用異常時新程序員遇到的最大問題之一是在發生異常時清理資源的問題。請考慮以下示例:

try
{
    openFile(filename);
    writeFile(filename, data);
    closeFile(filename);
}
catch (FileException &exception)
{
    cerr << "Failed to write to file: " << exception.what() << std::endl;
}

如果WriteFile()失敗並拋出FileException會發生什麼?此時,我們已經打開了文件,現在控制流跳轉到FileException處理程序,該處理程序打印錯誤並退出。請注意,該文件從未關閉!此示例應重寫如下:

try
{
    openFile(filename);
    writeFile(filename, data);
    closeFile(filename);
}
catch (FileException &exception)
{
    // 確保文件關閉
    closeFile(filename);
    // 然後寫入錯誤
    cerr << "Failed to write to file: " << exception.what() << std::endl;
}

在處理動態分配的內存時,這種錯誤通常以另一種形式出現:

try
{
    Person *john = new Person("John", 18, PERSON_MALE);
    processPerson(john);
    delete john;
}
catch (PersonException &exception)
{
    cerr << "Failed to process person: " << exception.what() << '\n';
}

如果processPerson()拋出異常,控制流將跳轉到catch處理程序。結果,john永遠不會被取消分配!這個例子比前一個更棘手 - 因爲john是try塊的本地例子,當try塊退出時它超出了範圍。這意味着異常處理程序根本無法訪問john(它已被銷燬),因此無法釋放內存。

但是,有兩種相對簡單的方法可以解決這個問題。首先,在try塊之外聲明john,這樣當try塊退出時它不會超出範圍:

Person *john = NULL;
try
{
    john = new Person("John", 18, PERSON_MALE);
    processPerson(john);
    delete john;
}
catch (PersonException &exception)
{
    delete john;
    cerr << "Failed to process person: " << exception.what() << '\n';
}

因爲john是在try塊之外聲明的,所以在try塊和catch處理程序中都可以訪問它。這意味着catch處理程序可以正確地進行清理。

第二種方法是使用一個類的局部變量,該類知道如何在超出範圍時清理自身(通常稱爲“智能指針”。標準庫提供了一個名爲std :: unique_ptr的類,可用於此目的 .std :: unique_ptr是一個包含指針的模板類,當它超出範圍時會釋放它。)

#include <memory>; // std::unique_ptr
 
try
{
    Person *john = new Person("John", 18, PERSON_MALE);
    unique_ptr<Person> upJohn(john); // upJohn now owns john
 
    ProcessPerson(john);
 
    // 當upJohn超出範圍時,它將刪除john
}
catch (PersonException &exception)
{
    cerr << "Failed to process person: " << exception.what() << '\n';
}

我們將在下一章中詳細討論智能指針。

異常和破壞

與構造函數不同,拋出異常可能是表明對象創建不成功的有用方法,因此絕不應在析構函數中拋出異常。

在堆棧展開過程中從析構函數拋出異常時會發生此問題。如果發生這種情況,則編譯器處於不知道是繼續堆棧展開過程還是處理新異常的情況。最終結果是您的程序將立即終止。

因此,最好的做法是完全避免在析構函數中使用異常。請將消息寫入日誌文件。

性能問題

異常確實需要支付較低的性能價格。它們會增加可執行文件的大小,並且由於必須執行額外的檢查,它們也可能導致它運行得更慢。但是,異常的主要性能損失發生在實際拋出異常時。在這種情況下,必須展開堆棧並找到適當的異常處理程序,這是一個相對昂貴的操作。

需要注意的是,一些現代計算機體系結構支持稱爲零成本異常的異常模型。零成本異常(如果支持)在非錯誤情況下沒有額外的運行時成本(我們最關心的是性能)。但是,在發現異常的情況下,它們會收到到更大的成本。

那我什麼時候應該使用異常?

如果滿足以下所有條件,則最好使用異常處理:

#正在處理的錯誤可能很少發生。
#錯誤很嚴重,否則執行無法繼續。
#無法在發生錯誤的地方處理錯誤。
#沒有一種好的替代方法可以將錯誤代碼返回給調用者。
作爲一個例子,讓我們考慮你編寫了一個函數,希望用戶傳入磁盤上文件名的情況。您的函數將打開此文件,讀取一些數據,關閉文件,並將一些結果傳回給調用者。現在,假設用戶傳入了一個不存在的文件名或一個空字符串。這是一個異常的好候選人嗎?

在這種情況下,上面的前兩個點很容易滿足 - 這不會經常發生,並且當它沒有任何數據可用時,你的函數無法計算結果。該函數也無法處理錯誤 - 重新提示用戶輸入新文件名不是函數的作用,這可能甚至不合適,具體取決於程序的設計方式。第四個點是關鍵 - 是否有一種很好的替代方法可以將錯誤代碼返回給調用者?這取決於您的計劃的細節。如果是這樣(例如,您可以返回空指針或狀態代碼來指示失敗),那可能是更好的選擇。如果沒有,那麼異常是合理的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章