Home
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

4

Local AI

--

Uedu Code

--

Taipei AQI 57 · Taichung AQI 74 · Tainan AQI 63 · Kaohsiung AQI 64
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 12

例外處理

throw、try、catch 與標準例外類別
Interaction實作 20 minute(s)

Learning Objectives

  • 說明為什麼需要例外:把「發生錯誤」和「正常結果」分開處理
  • 用 throw 丟出例外,用 try/catch 接住,追蹤程式跳躍的路線
  • 依型別安排多個 catch,知道 catch (...) 與「子類別先、父類別後」的順序
  • 使用標準例外類別與 what(),處理 vector::at 與 stoi 丟出的例外
  • 自訂例外類別,說明例外往外傳遞時區域物件會被解構,以及沒有被接住時程式會結束

為什麼需要例外

程式執行時總會遇到「不該發生、但真的發生了」的情況:除數是 0、使用者輸入的不是數字、索引超出範圍、檔案打不開。 傳統的做法是讓函式回傳一個特別的值代表失敗,例如找不到就回傳 -1:

int pos = findStudent("Amy");
if (pos == -1) { /* 處理找不到 */ }

這個做法有兩個麻煩:呼叫端很容易忘了檢查,錯誤就被默默忽略;而且有些函式的每個回傳值都是合法結果(例如除法), 根本挑不出一個值來代表錯誤。

例外(exception)是 C++ 提供的另一條路:發生錯誤的地方用 throw 丟出一個例外物件,函式立刻停止; 程式沿著呼叫的路線往外找,找到能處理它的 catch 就跳過去執行。錯誤處理的程式集中在 catch,正常流程的程式就不會被一堆 if 打斷。

try {
    // 可能出錯的程式
} catch (const runtime_error& e) {
    // 出錯時要做的事,e.what() 是錯誤訊息
}

本課的動畫會把例外發生的那一步標示出來,接著你會看到程式「跳」到 catch,中間沒執行的程式碼全部被跳過。

throw 與 try/catch

divide(10, 4) 正常回傳;divide(5, 0) 在第 8 行丟出例外。注意例外發生之後,divide 的框架立刻消失,第 10 行的 return 和第 17 行都沒有執行,下一步直接到第 18 行的 catch。

例外的流程規則

寫法 意思
throw 運算式; 丟出例外。可以丟任何型別:throw 404;、throw string("...");、throw runtime_error("...");
try { ... } 把可能丟出例外的程式包起來;沒有包在 try 裡的例外無法在這裡被接住
catch (型別 名稱) { ... } 例外的型別相符時執行;一個 try 後面可以接好幾個 catch
catch (...) 三個點:接住任何型別的例外,一定要放在最後

流程有三個重點:

  1. throw 執行後,同一個區塊裡後面的程式全部跳過,函式也不會回傳值。
  2. 程式從 try 後面的第一個 catch 開始由上往下比對型別,只會進入第一個相符的 catch,其他 catch 不執行。
  3. catch 執行完,程式接著執行整個 try/catch 之後的程式,不會回到 throw 的地方。

catch 比對型別時很嚴格:throw 'x';(char)不會被 catch (int n) 接住,throw 3.14; 也不會;一般的型別轉換在這裡不算數。 主要的例外是繼承:子類別的例外物件可以被父類別的 catch 接住,後面會用到。

習慣寫法:丟出時用值(throw runtime_error("...")),接住時用 const 參考(catch (const runtime_error& e)), 避免複製例外物件,也能正確處理子類別。

多個 catch:依型別比對

迴圈讓 code 從 0 跑到 3,每一輪 check 丟出不同型別的例外(0 不丟)。觀察每次例外發生之後,程式跳到哪一個 catch:int 跳到第 17 行,string 跳到第 19 行,double 沒有對應的 catch,由第 21 行的 catch (...) 接住。

想一想:例外的流程

1.

下面的程式會印出什麼?

try {
    cout << "A";
    throw 1;
    cout << "B";
} catch (int n) {
    cout << "C";
}
cout << "D";
2.

下面的程式會印出什麼?

try {
    throw 'x';
} catch (int n) {
    cout << "int";
} catch (...) {
    cout << "其他";
}
3.

out_of_range 是 exception 的子類別。下面的程式會印出什麼?

try {
    throw out_of_range("x");
} catch (const exception& e) {
    cout << "A";
} catch (const out_of_range& e) {
    cout << "B";
}
cout << "C";
第 1 次作答

標準例外類別

標準函式庫定義了一組例外類別,全部繼承自 std::exception(標頭檔 <exception>);常用的幾個在 <stdexcept>:

類別 用途 誰會丟出
exception 所有標準例外的父類別
logic_error 程式邏輯上就能避免的錯誤
invalid_argument 引數不合法 stoi("abc")
out_of_range 超出範圍 vector::at、string::at、stoi("99999999999")
runtime_error 執行時才會發現的錯誤 自己丟出時最常用
bad_alloc new 要不到記憶體(在 <new>) new

invalid_argument、out_of_range 是 logic_error 的子類別。每個例外物件都有成員函式 what(),回傳一段描述錯誤的文字; 建立時傳進去的字串就是 what() 的內容,例如 runtime_error("除數不能是 0")。

[] 和 at() 的差別:v[5] 不檢查範圍,超出範圍是未定義行為;v.at(5) 會檢查,超出範圍時丟出 out_of_range,可以用 catch 處理。

stoi(<string>,C++11)把字串轉成 int:開頭不是數字時丟出 invalid_argument,數字超出 int 能表示的範圍時丟出 out_of_range。 注意 stoi 只看開頭,stoi("12abc") 會得到 12,不會丟出例外。

vector::at 與 stoi 丟出的例外

第 12 行的 scores.at(5) 超出範圍,例外訊息是 g++ 的標準函式庫(libstdc++)產生的。後半段的迴圈把三個字串交給 stoi:第一個成功,第二個丟出 invalid_argument,第三個丟出 out_of_range,各自跳到對應的 catch。

自訂例外與堆疊解開

自己的程式也可以定義例外類別,讓 catch 依錯誤種類分開處理。最常見的寫法是繼承標準例外:

// 寫法一:繼承 exception,覆寫 what()
class NotEnoughMoney : public exception {
public:
    const char* what() const noexcept override { return "餘額不足"; }
};

// 寫法二:繼承 runtime_error,把訊息交給父類別的建構子
class NotEnoughMoney : public runtime_error {
public:
    NotEnoughMoney() : runtime_error("餘額不足") {}
};

noexcept(C++11)表示這個函式保證不會丟出例外;what() 在標準例外裡就是這樣宣告的,覆寫時要寫一樣。 繼承之後,catch (const exception& e) 也能接住它,e.what() 會呼叫覆寫後的版本。

例外丟出之後,如果目前的函式沒有相符的 catch,例外會往外傳遞給呼叫它的函式,一路找下去。 被經過的每個函式都會結束,它們的區域物件會依建立的相反順序被解構,這個過程叫做堆疊解開(stack unwinding)。 所以 vector、string、ofstream 這些會在解構子裡釋放資源的物件,即使發生例外也不會漏掉清理。

例外往外傳遞,沿路解構區域物件

main 呼叫 shop,shop 呼叫 withdraw,右邊會疊出三層框架。第 23 行丟出例外後,withdraw 和 shop 都沒有 catch,注意框架一層層消失之前,各自的 Tracker 解構子都被呼叫了(先 withdraw 的、再 shop 的)。第 24、30 行都沒有執行。

沒有被接住的例外

如果例外一路往外傳到 main 都沒有相符的 catch,程式會呼叫 std::terminate() 結束,預設的做法是呼叫 abort() 立刻中止。 用 g++ 編譯的程式在 Linux 上會印出類似這樣的訊息:

terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
Aborted (core dumped)

第一行告訴你例外的型別,第二行是 what() 的內容。這種情況下區域物件會不會被解構,C++ 標準交給各家實作決定,不要依賴它。

沒有 catch:程式結束

這段程式沒有 try/catch。播到最後一步,看輸出區的錯誤訊息,第 10 行的「結束」不會印出來。

想一想:標準例外與傳遞

1.

下面的程式會印出什麼?

try {
    cout << stoi("12abc") + 1;
} catch (const invalid_argument& e) {
    cout << "錯誤";
}
2.

下面的程式會印出什麼?

struct Box {
    char tag;
    Box(char t) : tag(t) {}
    ~Box() { cout << tag; }
};

void inner() { Box b('2'); throw 0; }
void outer() { Box a('1'); inner(); }

int main() {
    try { outer(); } catch (int) { cout << "C"; }
}
3.

vector<int> v(3); 之後,v[5] = 1; 和 v.at(5) = 1; 有什麼不同?

第 1 次作答

動手寫:檢查年齡輸入

完成 parseAge 與 main 迴圈:stoi 遇到不是數字的字串會自己丟出 invalid_argument;你要在年齡小於 0 或大於 150 時丟出 out_of_range("年齡不合理")。main 要分別接住兩種例外,輸出要和預期相同。

你的輸出
預期輸出

重點回顧

  • throw 丟出例外,所在的區塊和函式立刻停止;try 包住可能出錯的程式,catch 依型別接住例外
  • catch 由上往下比對,只進入第一個相符的;catch (...) 接住任何型別,放在最後;子類別的 catch 要放在父類別前面
  • 丟出時用值,接住時用 const 參考:catch (const runtime_error& e),e.what() 取得錯誤訊息
  • 標準例外都繼承 std::exception;vector::at 超出範圍丟出 out_of_range,stoi 會丟出 invalid_argument 或 out_of_range
  • 自訂例外可以繼承 exception 覆寫 what(),或繼承 runtime_error 傳入訊息
  • 例外往外傳遞時,經過的函式依序結束,區域物件被解構(堆疊解開);一直沒有被接住時,程式呼叫 std::terminate() 結束
這一課完成了!
Next lesson:檔案操作
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。