PREREQ P7

檔案例外

先備知識 · 語法與互動
ifstream|ofstream|getline|try / catch|throw|at()
向下捲動開始
📌 本頁使用方式(先備知識 · 語法與互動)

先讀語法與例子,再操作互動圖解並完成四選一自測。忘記寫法時回看速查表,最後用中英詞彙卡複習。

CONTENTS · 內容目錄
PART 00 · 檔案與錯誤

讓資料留下來,並處理走不通的路

本頁先讀寫文字檔,再學習開檔失敗、界線檢查與例外傳遞。檔案範例可以獨立執行,不需其他教材資料。這些做法也適用於一般程式的輸入驗證與錯誤回報。

PART 01 · 檔案

讀檔與寫檔:三個型別、一個模式

檔案串流物件會管理它開啟的檔案。物件生命週期結束時,會執行稱為解構式(destructor)的收尾函式。先理解「物件負責管理資源」即可;本頁使用標準類別,不需要先自行撰寫解構式。

ofstream 寫、ifstream 讀,用法跟 coutcin 一模一樣 —— 因為它們是同一家族。

寫進去再讀回來
#include <iostream> #include <fstream> #include <string> using namespace std; int main() { ofstream out("notes.txt"); // 開檔寫入,不存在就建立 if (!out.is_open()) { cout << "開檔失敗" << endl; return 1; } out << "READ" << endl << "CHECK" << endl << "SEND" << endl; out.close(); // 也可以讓它離開範圍自動關 ifstream in("notes.txt"); // 開檔讀取 string line; int n = 0; while (getline(in, line)) { // 逐行讀到檔尾 cout << ++n << ": " << line << endl; } return 0; }
預期輸出
1: READ
2: CHECK
3: SEND

三個重點:① 檔案串流需 <fstream>。② getline 回傳串流參考,while 依讀取狀態判斷是否繼續;最後一行沒有換行也可成功讀取。③ 物件正常離開作用域時解構式會關檔,但本例要在 out 還活著時立刻讀同一檔案,所以先明確 close,確保寫入完成。

寫法意思
ifstream in("a.txt");開檔讀取
ofstream out("a.txt");開檔寫入,清空原內容
ofstream out("a.txt", ios::app);附加到結尾
getline(in, line)讀取一行,回傳串流;作條件時反映這次讀取是否成功
in >> word讀一個「詞」,以空白切開

順便:字串也能當「檔案」讀 —— stringstream

stringstream 是這一家族的第三個成員:把字串當成 cin 來讀,>> 一樣自動以空白切詞、照變數型別轉換。跟 P1istringstream 模擬鍵盤輸入是同一家,只是 stringstream 讀寫都能做。

將一行用品記錄逐詞讀出
#include <iostream> #include <sstream> #include <string> using namespace std; int main() { stringstream input("pencil blue 12"); string token; while (input >> token) { cout << "[" << token << "] "; } cout << endl; }
預期輸出
[pencil] [blue] [12] 

stringstream 讓字串也能用串流的 >> 規則逐項解析,並依目標型別轉換。獨立程式應明確 #include <sstream>,不要依賴特定環境自動引入。

PART 02 · 開檔失敗

檔案開啟失敗時,串流會進入失敗狀態

開一個不存在的檔案
#include <iostream> #include <fstream> using namespace std; int main() { ifstream in("no_such_file.txt"); cout << "is_open() = " << in.is_open() << endl; cout << "檔案不存在時 ifstream 不會丟例外,只是進入失敗狀態。" << endl; cout << "所以開檔之後一定要自己檢查。" << endl; return 0; }
預期輸出
is_open() = 0
檔案不存在時 ifstream 不會丟例外,只是進入失敗狀態。
所以開檔之後一定要自己檢查。

它不丟例外、不終止、什麼都不說,只是把串流設成失敗狀態。你如果不檢查就直接讀,會讀到零筆資料然後難以判斷原因。所以 ifstream 開完一定要接一句 if (!in.is_open())

✅ 相對路徑是相對於「執行時的工作目錄」

檔案讀取失敗時,先確認工作目錄、檔名及開檔狀態,再檢查資料格式。相對路徑以執行時的工作目錄為基準。

PART 03 · 例外

例外不是「壞掉」,是「這條路走不通,換你決定」

例外處理的骨架只有兩個關鍵字:try 圈出可能失敗的程式,catch 接住失敗。

下面先看語法骨架:risky() 代表可能失敗的函式,SpecificError 代表要捕捉的例外型別,兩者都是占位名稱,不是標準函式或可直接執行的程式。後面的完整範例會換成 readAge 與 invalid_argument。

try { // 可能失敗的那幾行,範圍越小越好找錯 risky(); } catch (const SpecificError& e) { // 只接住你預期會發生的那一種 cout << "拋出例外時: " << e.what() << endl; } // 接住之後程式繼續往下跑
C++Python備註
try { } catch (…) { }try: / except:概念相同
throw X();raise X()主動拋出
e.what()str(e)取得訊息
沒有對應finally:把資源綁定到物件生命週期,由解構式收尾:RAII(Resource Acquisition Is Initialization)
沒有對應else:C++ 沒有這個區塊
⚠️ 不要寫 catch (...) 把所有東西一起接住

catch (...) 接住所有傳到此處的 C++ 例外,但不能處理一般的未定義行為或記憶體存取錯誤。它可用於清理後重新拋出,或在程式邊界統一回報;若接住後不記錄、不處理,就可能掩蓋問題。

同一段程式,餵不同的輸入就會走出兩條完全不同的路。按下面兩顆 ▶ 各跑一次,注意例外那條路上 readAge 的框是什麼時候消失的

#include <iostream> #include <stdexcept> using namespace std; int readAge() { int age; cin >> age; if (age < 0) { throw invalid_argument("年齡不能是負數"); } return age; } int main() { try { int age = readAge(); cout << "年齡:" << age << endl; } catch (const invalid_argument& e) { cout << "輸入有問題:" << e.what() << endl; } cout << "程式繼續往下跑" << endl; return 0; }
選一條路開始。同一份程式碼,合法輸入與負數輸入走的路完全不同。
速度
堆疊解開 UNWIND

例外不是throw 直接跳到 catch。它會沿著呼叫鏈往回走,把中間每一個函式的框架逐一撤掉,撤掉時該框架裡區域物件的解構式都會被呼叫 —— 這叫 stack unwinding。所以檔案串流這類物件不必手動收尾,例外飛過去的時候它們也會自己關好。

型別要對得上 MATCH

catch 只接住型別相符的例外。丟 invalid_argument、只寫 catch (const out_of_range& e),例外就會穿過去繼續往上飛;一路到 main 外面都沒人接,程式直接 terminate 終止。父類別接得住子類別,所以 catch (const exception& e) 是最寬的那一層,要放最後面。

PART 04 · at() 與 []

兩個下標寫法,一個檢查範圍一個不檢查 查錯時用得到

同一個越界存取,兩種下場
#include <iostream> #include <vector> #include <stdexcept> using namespace std; int main() { vector<int> v = {10, 20, 30}; // 錯誤示範:v[10] 越界,行為未定義,因此不執行。 try { cout << "trying at: "; int value = v.at(10); cout << value << endl; } catch (const out_of_range& e) { cout << "caught out_of_range" << endl; cout << "details: " << e.what() << endl; } cout << "continue" << endl; }
預期輸出
trying at: caught out_of_range
details: (由實作提供的錯誤描述)
continue

vector 的 v[10] 不符合有效索引前提,屬未定義行為,所以只留在註解中。實際執行的是 v.at(10):它丟出 out_of_range,catch 接住後可繼續執行。what() 訊息依實作而異;trying at: 已在拋出之前輸出,例外不會回復先前的副作用。

常見標準例外什麼時候發生
out_of_rangevector::at()string::at() 越界
invalid_argumentstoi("abc") 這種轉不了的輸入
bad_allocnew 要不到記憶體
runtime_error通用的執行期錯誤,自訂例外常繼承它
🎯 什麼時候用哪個

不確定索引時,at() 的範圍檢查能讓錯誤較早被發現。已確認合法且需要控制檢查成本時可用 [];兩者差別是介面契約,不是「所有程式都該用某一個」。string 的 size() 位置另可讀結尾空字元,不能把它和 vector 的界線完全混同。

PART 05 · 主動拋出

用 throw 主動回報輸入問題

函式無法接受輸入時,可以透過例外回報。本例遇到負數就拋出 invalid_argument;正常路徑只回傳已通過檢查的數量。

負數數量應立即拒絕
#include <iostream> #include <stdexcept> using namespace std; int checkedQuantity(int n) { if (n < 0) throw invalid_argument("數量不可為負數"); return n; } int main() { try { cout << checkedQuantity(-1) << endl; } catch (const exception& e) { cout << e.what() << endl; } }
預期輸出
數量不可為負數

先用標準的 invalid_argument 表達輸入不合法,throw 會離開目前正常路徑並尋找符合的 catch。等學過類別、繼承與模板後,P9 保留完整自訂例外與草稿箱例子

💡 為什麼不回傳一個特殊值就好

特殊回傳值是否合適,取決於函式契約。例如本頁數量只允許非負值,便可另約定以 -1 表示錯誤;但若函式回傳溫度,-1 也可能是正常資料。採特殊值時,呼叫端必須檢查;採例外時,則由相符的 catch 處理。這裡使用 invalid_argument,讓正常回傳值只表達有效數量。

PART 06 · 沒人接

例外沒有人接住會怎樣

未捕捉例外的執行結果
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v = {1, 2, 3}; cout << "接下來這行會丟例外,而且沒有人接:" << endl; cout << v.at(99) << endl; cout << "這一行永遠不會被執行" << endl; return 0; }
預期輸出
接下來這行會丟例外,而且沒有人接:

[執行結束碼 -6]
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 99) >= this->size() (which is 3)

程式會終止,後面的正常敘述不再執行。這裡的 terminate 診斷與結束碼是特定環境的示例;不能只憑 -6 判定是未捕捉例外或 assert,因為其他情況也可能呼叫 abort。定位原因時應一起閱讀例外型別、訊息與發生位置。

✅ 沒接住其實常常是對的

在有能力處理或轉譯錯誤的位置使用 catch。若目前這一層無法處理,可讓例外繼續傳遞,交由呼叫端或程式邊界統一回報。保留錯誤資訊,避免無聲忽略後繼續使用不完整的結果。

EX · 隨堂練習

兩題確認觀念

EXERCISE 1 · 開檔失敗

ifstream in("data.txt"); 檔案不存在時會發生什麼?

(A) 丟出一個例外
(B) 安靜地進入失敗狀態,要自己用 is_open() 檢查
(C) 自動建立一個空檔案
(D) 程式必定立刻終止
EXERCISE 2 · 該用哪個下標

你在 debug 一個索引算錯的排序程式。應該把 v[i] 暫時換成什麼?

(A) v.at(i)
(B) v.front()
(C) v.data()[i]
(D) v[i]
REFERENCE · 速查表

P7 速查表

#include <fstream> #include <iostream> #include <string> #include <vector> #include <stdexcept> using namespace std; // 以下是放在函式內的獨立語法片段。 ifstream in("data.txt"); if (!in.is_open()) { /* 回報開檔失敗 */ } string line; while (getline(in, line)) { /* 逐行處理 */ } ofstream out("out.txt", ios::app); // 附加模式 out << "一行內容" << endl; vector<int> values = {10, 20}; try { cout << values.at(5); } catch (const out_of_range& e) { cout << e.what(); } // 主動回報錯誤,仍使用本頁已介紹的標準例外。 throw invalid_argument("數量不可為負數");

自訂例外需要類別與繼承語法,完整寫法請接著讀 P9 的自訂例外。本頁速查先整理已學過的標準例外。

ifstream 開完一定要檢查 is_open()
② 相對路徑是相對於執行時的工作目錄
③ debug 時把 [] 換成 at()
④ 在適當的位置處理例外;無法處理時,保留資訊並向呼叫端傳遞。

延伸閱讀

資源看什麼
cppreference · fstream開檔模式與串流狀態的完整說明。
cppreference · Exceptions標準例外的完整階層。
本站程式唯一示範例外處理的地方。
QUIZ · 自我檢測

自我檢測:檔案與例外 隨堂自測 · 6 題

每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。

Q1.以下程式印出什麼?
vector<int> v = {10, 20, 30};
try {
    cout << v.at(10);
} catch (const out_of_range& e) {
    cout << "接住了";
}
cout << "|繼續";
Q2.ifstream in("data.txt"); 而 data.txt 不存在時會發生什麼?
Q3.假設檔案開啟、寫入與讀取皆成功,以下片段印出什麼?
ofstream out("log.txt");   // log.txt 原本有 100 行
out << "新的一行" << endl;
out.close();

ifstream in("log.txt");
string line;
int n = 0;
while (getline(in, line)) n++;
cout << n;
Q4.你在 debug 一個索引算錯的程式。把 v[i] 換成 v.at(i) 有什麼好處?
Q5.接住 const runtime_error& e 後,e.what() 的用途是什麼?
Q6.一個例外沒有被任何 catch 接住,會發生什麼?
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面

先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。