← 11/26|分離編譯與命名空間(Ch 11) | 回總覽 | 12/10|繼承(Ch 14) →
對應課本習題:Ch12: 2, 3, 5
這幾題各要用到什麼(動手前先看)
主課的上機考幾乎就是這些題目,所以每一題都自己寫過。下表是每題需要的東西(我的歸納,不是題目本文)與本系列對應的練習:
| 課本題號 | 要用到的東西 | 先練 |
|---|---|---|
| Ch12-2 | 檔案裡一堆 double,算平均印到螢幕 |
Q1 |
| Ch12-3 | 已排序的檔案算中位數:先讀一遍數個數、關檔重開再讀到中間;再延伸算四分位 | Q9 |
| Ch12-5 | 讀整數檔檢查是不是等差數列,符合的寫到輸出檔、遇到不符就停 | Q6、Q8 |
這週要會什麼
1 | ifstream / ofstream → 檢查開檔 → 讀到檔尾 → 追加模式 → get / put 逐字元 → 對齊輸出 → getline + stringstream 拆欄位 |
串流(stream)的概念
白話說:串流就是「資料流動的水管」。cin 是從鍵盤流進來的水管,cout 是流向螢幕的水管。檔案 I/O 做的事只有一件——把水管的另一端接到檔案上,語法完全一樣。
| 類別 | 用途 | 類比 |
|---|---|---|
ifstream |
從檔案讀(input file stream) | 像 cin |
ofstream |
往檔案寫(output file stream) | 像 cout |
都要 #include <fstream>。(還有可讀可寫的 fstream,這學期用不到。)
「語法完全一樣」不是比喻:ofstream 就是一種 ostream,所以你在 11/12 自己重載的 operator<<(ostream& os, const Vec2& v),拿去寫檔案 fout << v; 完全不用改(ifstream 與 istream 同理)。「是一種」的意思 12/10 講繼承時會說清楚。
讀檔
1 |
|
input.txt 內容與輸出:
1 | input.txt: |
cerr就是 09/17 提過的標準錯誤輸出:一樣印到螢幕,但不緩衝、專門用來印錯誤訊息。if (!fin)檢查開檔是否成功。沒檢查就直接讀的話,while (fin >> x)一圈都不會跑,上面那支會若無其事地印出sum = 0——看起來像個正常答案,你根本不會發現檔案壓根沒打開。開不起來最常見的原因不是程式寫錯,是檔案不在你執行./a.out的那個資料夾——程式裡的"input.txt"找的是「目前工作目錄」底下的檔案;先ls確認兩個在一起再重跑。
為什麼串流可以當條件?
兩件事。(1)fin >> x的回傳值是fin自己,所以>>才能一路串下去寫成fin >> a >> b——這就是 11/12 你親手寫過的「operator>>要回傳istream&」。(2) 串流身上記著一個狀態(上一次讀寫成功了沒),被放進if/while的括號裡時會自動變成true(狀態正常)或false(讀失敗、到檔尾、格式不合)。
所以while (fin >> x)讀作「這次真的讀到一個 int 就繼續」,!fin讀作「串流狀態不正常」。這就是 09/24 說「串流能當條件,原理 12/03 再講」的那件事。
另一種寫法是先宣告再開檔:
1 | ifstream fin; |
fin.fail() 問的是「上一個動作失敗了嗎」,在開檔檢查上跟 !fin 同一件事,挑一種寫就好(課本兩種都有)。分開寫真正的用途是同一個串流要換檔案重開(先 close() 再 open());檔名等程式跑起來才決定並不需要分開寫——後面〈檔名由使用者輸入決定〉那支就是直接 ifstream fin(filename);。
寫檔
1 |
|
這支程式螢幕上不會有輸出,但會產生 output.txt:
1 | Hello, file! |
ofstream fout("output.txt");檔案不存在會自動新建(這點跟ifstream相反),存在的話預設會清空(本來的內容就沒了)。所以ofstream開檔失敗通常是資料夾沒有寫入權限,不是檔案不見。fout.close();:寫完要在同一支程式裡再讀回來時一定要先close(),否則資料可能還卡在緩衝區裡、讀到的是空檔;單純寫完就結束程式可以不寫。- 想接在後面寫,用追加模式:
1 | ofstream fout("log.txt", ios::app); // append |
ios 是所有串流共同的祖先類別,:: 就是 10/15 的「屬於」,所以 ios::app =「ios 裡那個叫 app(append)的旗標」。常用的只有三個:ios::in(讀)、ios::out(寫,預設清空)、ios::app(接在後面寫);ios::binary 是給圖片影音用的,這學期用不到。要一次指定好幾種模式就用 | 串起來,例如 ofstream fout("log.txt", ios::out | ios::app);。這個 | 跟 09/17 的邏輯 ||(兩根)不是同一個東西,這學期照抄就好。
讀到檔尾的正確寫法
1 | // ✅ 正確:把讀取動作本身當條件(前面求和那支用的 while (fin >> x) 就是這招) |
❌ 那段拿上面的 input.txt 去跑,實際印出:
1 | 3 7 11 20 20 |
原因:文字檔最後幾乎一定有一個換行。讀完 20 之後 eof() 還是 false,於是迴圈又跑一圈——這次 fin >> x 只吃到換行就碰到檔尾、讀取失敗,x 沒被改到,最後一筆就被印了兩次。請一律用 while (fin >> x) 或 while (getline(fin, line))。
混用
>>和getline一樣會中招:fin >> id;之後直接getline(fin, line)會讀到空字串,因為>>把數字後面的換行留在管子裡。解法跟 11/12 的cin版一模一樣——中間插一行fin.ignore(numeric_limits<streamsize>::max(), '\n');(需#include <limits>),把那一行剩下的東西全丟掉。
逐字元讀寫
下面這支把 input.txt 一個字元不漏地抄進 output.txt,順便把小寫轉大寫(toupper 要 #include <cctype>):
1 |
|
input.txt 是 Hello, World! 時,output.txt 就是 HELLO, WORLD!。
fin >> c會跳過空白;fin.get(c)不會。要原封不動處理檔案內容就用get/put。fin.ignore(n, ch):最多丟掉 n 個字元、遇見ch就停——就是 11/12 的cin.ignore(...),前面說的換行殘留就靠它清掉。fin.peek():偷看下一個字元但不取走(這學期用不到,知道有就好)。
格式化輸出(<iomanip>)
只有 setw(n) 只影響下一個輸出,其他都會一直生效到你改掉為止。
| 操作子 | 作用 |
|---|---|
setw(n) |
設定欄寬 |
setfill(c) |
補位字元 |
setprecision(n) |
搭配 fixed =小數點後 n 位;沒有 fixed 時是有效位數 n 位(10/22 那個 1.2e+03 的坑) |
fixed |
固定小數點表示法 |
left / right |
靠左 / 靠右對齊 |
showpoint |
強制顯示小數點 |
1 | cout << left << setw(10) << "Name" << right << setw(8) << "Score" << '\n'; |
輸出:
1 | Name Score |
做出這種對齊的表格是實驗課很常見的要求。
檔名由使用者輸入決定
檔名不一定要寫死在程式裡:
1 |
|
課本寫的是舊寫法
fin.open(filename.c_str());(C11 以前的ifstream只吃 C 風格字串),兩種在 Ubuntu 的 g 都能編。
隨機存取:seekg / tellg
串流內部記著「現在讀到第幾個 byte」,tellg() 問位置、seekg() 跳位置,可以用來量檔案大小或倒回檔頭重讀。實驗課題目幾乎都是順序讀寫,知道有這兩個函式就好。
stringstream:把字串當串流用
需要 #include <sstream>。命名規則跟檔案串流一模一樣:istringstream(從字串讀,像 ifstream)、ostringstream(往字串寫,像 ofstream)、stringstream(可讀可寫)。
什麼時候需要它?檔案每一列的欄位數如果不固定(有人考三科、有人考五科),fin >> a >> b >> c 沒辦法知道哪裡換行。做法是:先用 getline 抓一整列,再把這個字串包成串流慢慢拆。
1 |
|
grades.txt 內容與輸出:
1 | grades.txt: |
欄位改用逗號之類的符號隔開時,getline 的第三個參數可以自訂分隔字元(簡單的逗號分隔格式這樣就夠了;正式的 CSV 還允許用引號包住逗號,得另外處理):
1 | istringstream iss("Ann,90,85"); |
反過來,ostringstream 可以把數字組成字串:
1 | ostringstream oss; |
本週重點回顧
- 開檔後一定要
if (!fin);開不起來通常不是程式錯,是檔案沒跟a.out放在同一個資料夾。 - 讀到檔尾用
while (fin >> x)或while (getline(fin, line)),永遠不要用eof();>>之後要接getline記得先fin.ignore(numeric_limits<streamsize>::max(), '\n')。 - 要保留空白與換行就用
get/put,>>會把空白吃掉。 - 每列欄位數不固定 →
getline抓整列 +istringstream拆欄位;逗號分隔就用三參數getline。
本週練習題
以下題目請先
cd到你放程式的資料夾,再用nano input.txt建一個測試檔。
Q1. 檔案求和
讀取 input.txt 中任意數量的整數,把總和與平均寫進 output.txt。
1 | input.txt: |
參考解答
1 |
|
Q2. 檔案複製(相當於 cp)
把 input.txt 一字不差地複製成 copy.txt(含空白與換行)。
參考解答
1 |
|
如果改用 while (fin >> c),所有空白與換行都會消失——這題就是在考 >> 與 get 的差別。
Q3. 成績檔排序
scores.txt 每一列是「姓名 分數」。讀進來後依分數由高到低排序,輸出到 sorted.txt,並在螢幕印出平均分數。
1 | scores.txt: |
參考解答
1 |
|
Q4. 簡化版 CSV
data.csv 每列格式為 姓名,國文,英文,數學,欄位裡保證沒有逗號、引號和換行(正式的 CSV 允許 "Wang, Ann",90,85,95 這種用引號包住逗號的欄位,那要另外處理,本題不考慮)。讀入後輸出每個人的總分與平均,並對齊成表格。
1 | data.csv: |
參考解答
1 |
|
拆欄位用的就是前面講的三參數 getline;n > 0 是在防「這一列一個分數都沒有」時除以 0。
Q5. 詞頻統計
讀入 text.txt,統計每個單字出現次數(忽略大小寫與標點),輸出出現次數最多的前三名。同票時輸出順序不拘;單字不足三個就有幾個印幾個。
1 | text.txt: |
參考解答
1 |
|
這題用「線性搜尋 + vector」是 $O(n^2)$,資料量大會很慢。學過 map 之後會快很多,但那是這學期範圍外的東西,實驗課用這個寫法就夠。
實驗課題型加練
Q6–Q8 的題型取自歷年考古題與去年實驗課用過的資料檔格式(去年第 11 週實驗課只有講義沒有練習題;今年的投影片還沒出),Q9 對應課本勾選題。檔案題在實驗課的考試很常見:讀一個題目附的 .txt(每行固定幾欄,或用分號分隔),算完寫到另一個檔。題目不是我原創的:練的東西跟那些題一樣,但題目敘述、資料檔內容、輸出格式和解答都是我自己重寫的,不是原題。三種基本形狀各練一題。
Q6. 讀檔算最大公因數
gcd.txt 每行兩個正整數,例如:
1 | 12 18 |
讀到檔尾為止,用輾轉相除法(10/01 Q9 那個函式)算每一行的最大公因數並印出;檔案打不開要印錯誤訊息。
1 | 輸出: |
參考解答
1 |
|
while (fin >> a >> b) 就是本篇「讀到檔尾的正確寫法」那節的形狀:一次讀兩個、讀不滿就停,不用 eof()。函式本身是 10/01 Q9 那份,換的只有資料來源——把「讀鍵盤」換成「讀檔案」,程式其他部分一行都不用動,這是串流設計的用意。
Q7. 分號分隔的菜單檔
menu.txt 每行是「代號;品名;單價」,品名含空白:
1 | A01;Iced Latte;120 |
讀入後對齊印成表格(代號靠左佔 5 格、品名靠左佔 16 格、單價靠右佔 4 格),最後印平均單價(小數一位)。提示:getline(fin, s, ';') 的第三個參數是「讀到哪個字元為止」,讀完會把那個分號吃掉。
1 | 輸出: |
參考解答
1 |
|
一行三欄就 getline 三次:前兩次以 ';' 為界、最後一次讀到行尾(預設以換行為界)。三個 getline 用 && 串成 while 的條件,任何一欄讀不到就代表檔案結束。單價讀進來是字串,stoi 轉成整數才能加總。left/right 決定 setw 補的空白放哪一邊(預設是靠右、空白補在左邊),設了之後會一直有效,所以印完兩個靠左的欄位要切回 right 再印數字。這也是 12/10 車輛檔那題的讀檔部分,先在這裡練熟。
Q8. 成績報表與直方圖(多檔輸出)
grades.txt 每行是「學號 小考次數 各次小考分數… 期中上機分數」,小考次數每人不同(至少一次),最多 100 人:
1 | B113040012 4 72 85 90 66 58 |
假設配分是:小考平均佔 30%、期中上機佔 30%、期末上機佔 40%。算每人的目前總分(小考平均 × 0.3 + 期中 × 0.3,所以期末考前最多 60 分),再算「期末上機至少要考幾分才能到 60 分及格」——已經及格印 0,考滿分也救不回來印 -1。把表格寫進 report.txt,並依目前總分每 10 分一級畫直方圖(一人一顆星)寫進 histogram.txt;螢幕只印一行摘要。
1 | 螢幕輸出: wrote report.txt and histogram.txt (5 students) |
參考解答
1 |
|
「小考次數每人不同」是這題的核心:先讀 count,再用 for 讀那麼多個,最後才讀期中——格式由檔案的欄位決定,不能假設每行一樣長。輸出檔跟 cout 用法完全一樣,setw、fixed 都能用;直方圖那格用 static_cast<int>(current) / 10 決定落在哪一級。「至少要考幾分」是把 60 - current 除以期末的權重 0.4 反推回來,超過 100 就代表不可能。這個題型出自一份舊的期末考考古題(原版還會再加上搜尋與排序),能寫到這裡就有一半分數了。
Q9. 讀兩遍算中位數(不用 vector)
sorted.txt 裡是一串已排好序的數字。不把它們存進陣列或 vector,直接算中位數:先讀一遍數有幾個,close() 後重新開檔,再讀到正中間那個(偶數個就取中間兩個的平均),印出個數與中位數(一位小數)。
1 | sorted.txt: 3 7 11 15 20 24 |
參考解答
1 |
|
讀到檔尾之後串流就停在「失敗」狀態,close() 再 open() 才能從頭再讀一次——這就是正文說「分開寫 open 真正的用途」。中間位置的算法:6 個取第 3、4 個,5 個取第 3 個(索引從 0 起算是 count / 2)。
說些什麼吧!