Learning Objectives
- 用 ofstream 建立檔案並寫入,分辨覆寫與 ios::app 附加
- 用 ifstream 開啟檔案,並檢查是否開啟成功
- 用 >> 逐筆讀取、用 getline 逐行讀取,寫出讀到檔案結尾就停的迴圈
- 說明 close 的作用,以及檔案物件結束時會自動關檔
- 讀取 CSV 格式的資料,用 stringstream 依逗號拆出欄位
為什麼要讀寫檔案
到目前為止,程式的資料都放在變數裡;程式一結束,資料就不見了。要讓資料留下來,例如成績紀錄、設定值、遊戲存檔, 就要寫進檔案;要處理別人準備好的大量資料,例如一份 CSV 成績表,就要從檔案讀進來。
C++ 用檔案串流(file stream)讀寫檔案,標頭檔是 <fstream>。它的用法和你熟悉的 cout、cin 幾乎一樣:
| 類別 | 用途 | 類比 |
|---|---|---|
ofstream |
寫入檔案(output file stream) | 像 cout,用 << |
ifstream |
讀取檔案(input file stream) | 像 cin,用 >> 或 getline |
fstream |
同一個檔案又讀又寫 |
ofstream out("todo.txt"); // 開啟(建立)檔案
out << "買牛奶" << endl; // 和 cout 一樣的寫法
out.close(); // 關閉檔案
本課的程式在一個沙盒資料夾裡執行,範例會先放好要讀的檔案;程式寫出的檔案,會在輸出區多一個分頁顯示它的內容。
ofstream 寫檔與 ios::app 附加
右邊的變數區會顯示每個檔案串流連到哪個檔案,以及目前是「開啟中」還是「已關閉」。播完之後切到輸出區的檔案分頁:todo.txt 有三行(第三行是附加上去的),study.txt 是迴圈寫出來的。
開檔模式與檢查
開啟檔案時可以用第二個參數指定模式(定義在 ios 裡,可以用 | 組合):
| 模式 | 意思 |
|---|---|
ios::out |
寫入(ofstream 的預設) |
ios::in |
讀取(ifstream 的預設) |
ios::trunc |
開啟時清空原本的內容(ofstream 預設就會清空) |
ios::app |
附加:每次寫入都接到檔案結尾 |
ios::binary |
二進位模式,不轉換換行字元 |
開檔不一定成功:要讀的檔案不存在、檔名打錯、沒有權限都會失敗。失敗時不會丟出例外,只是串流變成失敗狀態, 接下來的讀寫全部沒有作用。所以開檔之後要檢查:
ifstream in("scores.txt");
if (!in.is_open()) { ... } // 寫法一:is_open()
if (!in) { ... } // 寫法二:串流放在條件裡,失敗時是 false
檔案路徑:只寫檔名時,是相對於程式執行時的「工作目錄」,不一定是程式碼所在的資料夾。在 Windows 上寫完整路徑時,
反斜線要寫兩次("C:\\data\\a.txt"),或改用斜線 "C:/data/a.txt"。
用 >> 逐筆讀到檔案結尾
scores.txt 每行有一個名字和一個分數。第 16 行的 while (in >> name >> score) 每一圈讀一個名字和一個分數;讀到檔案結尾、讀不到東西時,條件變成 false,迴圈結束。注意 >> 會自動跳過空白和換行。
getline 逐行讀取
notes.txt 不存在,所以第 8 行開檔失敗,變數區會顯示「開啟失敗」。poem.txt 的第一行中間有一個空白:用 getline 讀時整行完整讀進來;最後用 >> 讀,只讀到空白之前。
想一想:寫入與讀取
下面的程式會印出什麼?
ofstream a("memo.txt");
a << "A";
a.close();
ofstream b("memo.txt");
b << "B";
b.close();
ifstream in("memo.txt");
string s;
in >> s;
cout << s;
檔案 data.txt 的內容是:
hello world
second line
下面的程式會印出什麼?
ifstream in("data.txt");
string line, word;
getline(in, line);
in >> word;
cout << line << "|" << word;
ifstream in("nofile.txt"); 而這個檔案不存在,會發生什麼事?
讀到檔案結尾的正確寫法
讀取檔案最常用的兩種迴圈:
while (in >> x) { ... } // 一筆一筆讀,以空白或換行分隔
while (getline(in, line)) { ... } // 一行一行讀
讀取動作本身就是條件:讀成功是 true,讀到檔案結尾(或格式不符,例如要讀 int 卻遇到文字)就是 false。
初學者常寫成下面這樣,最後一筆常常會被處理兩次:
while (!in.eof()) { // 不建議
in >> x;
sum += x; // 最後一次 in >> x 失敗了,x 還是上一筆的值
}
eof() 要等到「已經讀過頭」才會變成 true,所以判斷的時間點晚了一步。用讀取動作當條件就沒有這個問題。
close() 要不要寫? 檔案串流物件被解構時(例如離開所在的函式)會自動關檔。明確寫 close() 的好處是:
寫入的資料立刻存進檔案,同一個程式接著要用另一個串流讀這個檔案時,才讀得到完整內容;也可以讓同一個串流物件再 open() 另一個檔案。
CSV 與 stringstream
CSV(comma-separated values)是最常見的表格資料格式,試算表軟體都能匯出:每一行是一筆資料,欄位之間用逗號分隔,第一行通常是標題。
name,chinese,math
Amy,85,92
>> 以空白分隔,沒辦法直接拆逗號。做法分兩步:
- 用
getline(in, line)讀一整行 - 把這一行放進
stringstream(標頭檔<sstream>),它是「把字串當成串流」的類別,再用getline(ss, field, ',')一次讀到逗號為止
getline 的第三個參數是分隔字元,預設是換行 '\n'。拆出來的欄位都是 string,數字要用 stoi(整數)或 stod(浮點數)轉換。
讀取 CSV 並計算總分
每一圈先讀一行到 line,再建立 stringstream ss 拆成三個欄位。注意變數區裡 chinese、math 帶引號,表示它們是字串,要到第 18 行才轉成數字。
想一想:逐行讀取與拆欄位
下面的程式會印出什麼?
stringstream ss("蘋果,,香蕉");
string field;
int count = 0;
while (getline(ss, field, ',')) {
count++;
}
cout << count;
檔案 nums.txt 的內容是 3 5 x 7(一行)。下面的程式會印出什麼?
ifstream in("nums.txt");
int n, sum = 0;
while (in >> n) {
sum += n;
}
cout << sum;
動手寫:購物清單小計
沙盒裡已經有 cart.csv:第一行是標題,之後每行是「品名,單價,數量」。完成迴圈,印出每個品項的小計(單價乘數量),最後印出合計。
item,price,qty
筆記本,45,3
原子筆,15,4
便當,80,1
重點回顧
- 檔案串流在
<fstream>:ofstream寫、ifstream讀,用法和cout、cin一樣 ofstream預設清空原本的內容;要接在後面寫,用ios::app- 開檔失敗不會丟出例外,開檔後用
is_open()或if (!in)檢查 >>以空白分隔逐筆讀,getline逐行讀(保留行內的空白);用while (in >> x)、while (getline(in, line))讀到檔案結尾,不要用while (!in.eof())close()把資料寫進檔案並結束連線;串流物件解構時也會自動關檔- CSV:
getline讀一行,放進stringstream,再用getline(ss, field, ',')拆欄位,數字用stoi、stod轉換