程式的資料若只存在記憶體,程式一結束就煙消雲散。
本單元學習 C 標準庫的檔案 I/O:開檔、讀寫(字元/行/格式化/二進位)、
定位、錯誤處理。所有範例都自己先建立測試檔(寫到 /tmp/)再讀取,
不依賴任何外部檔案,直接編譯執行即可看到完整結果。
學習目標
- 理解
FILE *串流的概念,熟記fopen模式表 - 會用四個層次讀寫檔案:字元(
fgetc/fputc)、行(fgets/fputs)、 格式化(fprintf/fscanf)、二進位(fread/fwrite) - 會用
fseek/ftell/rewind隨機存取 - 掌握錯誤處理三件套:
ferror/feof/perror,以及stderr與exit - 完成三個實作:檔案複製、行數統計、CSV 讀寫
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 檔案存取(fopen/getc/putc) | §7.5 File Access |
| 錯誤處理(stderr 與 exit) | §7.6 Error Handling |
| 行的輸入輸出 | §7.7 Line Input and Output |
| 完整函式規格 | Appendix B1 |
觀念講解
1. FILE * 與串流
fopen 回傳 FILE *——一個指向「串流狀態」(緩衝區、目前位置、錯誤旗標……)的指標。
之後所有操作都透過這個指標。程式啟動時自動有三條串流:
| 串流 | 用途 | 預設對象 |
|---|---|---|
stdin |
標準輸入 | 鍵盤 |
stdout |
標準輸出 | 螢幕(有緩衝) |
stderr |
標準錯誤 | 螢幕(通常不緩衝,錯誤訊息不會被吞) |
2. fopen 模式表
FILE *fp = fopen("/tmp/data.txt", "r");
if (fp == NULL) { /* 開檔可能失敗:檔案不存在、沒權限… */
perror("fopen"); /* 印出系統錯誤原因 */
exit(EXIT_FAILURE);
}
/* ... */
fclose(fp); /* 用完必關:釋放資源、沖出緩衝 */
| 模式 | 意義 | 檔案不存在時 | 原內容 |
|---|---|---|---|
"r" |
唯讀 | 失敗(NULL) | 保留 |
"w" |
唯寫 | 建立新檔 | 整個清空! |
"a" |
附加寫 | 建立新檔 | 保留,寫入永遠在檔尾 |
"r+" |
讀寫 | 失敗(NULL) | 保留 |
"w+" |
讀寫 | 建立新檔 | 整個清空! |
"a+" |
讀+附加寫 | 建立新檔 | 保留 |
加 b(如 "rb"、"wb") |
二進位模式 | 同上 | Unix/macOS 無差別;為可攜性二進位檔一律加 b |
3. 四個層次的讀寫
字元層:int fgetc(FILE *) 回傳字元或 EOF——注意回傳型別是 int,
必須用 int 變數接才能區分 EOF 與合法位元組:
int c; /* 不是 char! */
while ((c = fgetc(in)) != EOF)
fputc(c, out);
行層:fgets 讀進至多 n−1 個字元並保留換行、保證補 '\0';讀到檔尾回傳 NULL:
char line[256];
while (fgets(line, sizeof line, fp) != NULL)
fputs(line, stdout);
格式化層:fprintf/fscanf 就是 printf/scanf 的檔案版:
fprintf(fp, "%s,%d,%.2f\n", name, age, score);
while (fscanf(fp, "%31[^,],%d,%lf\n", name, &age, &score) == 3) { ... }
二進位層:fwrite/fread 直接搬記憶體位元組,適合結構陣列:
fwrite(records, sizeof records[0], n, fp); /* 寫 n 筆 */
size_t got = fread(buf, sizeof buf[0], n, fp); /* 回傳實際讀到的筆數 */
4. 定位:fseek / ftell / rewind
fseek(fp, 0L, SEEK_END); /* 跳到檔尾 */
long size = ftell(fp); /* 目前位置 = 檔案大小(bytes) */
rewind(fp); /* 回到開頭(= fseek(fp, 0, SEEK_SET)) */
fseek(fp, (long)(i * sizeof(struct rec)), SEEK_SET); /* 隨機跳到第 i 筆 */
SEEK_SET(從頭)、SEEK_CUR(從目前位置)、SEEK_END(從檔尾)。
5. 錯誤處理
| 工具 | 用途 |
|---|---|
| 回傳值檢查 | fopen→NULL、fgets→NULL、fread→短筆數…第一道防線 |
feof(fp) |
讀取失敗後判斷:是「到檔尾了」嗎? |
ferror(fp) |
讀取失敗後判斷:是「I/O 錯誤」嗎? |
perror("msg") |
印出 msg: 系統錯誤原因(根據 errno)到 stderr |
exit(EXIT_FAILURE) |
結束程式並回報失敗;會沖出緩衝、關閉串流 |
正確的讀迴圈收尾:
while ((c = fgetc(fp)) != EOF) { ... }
if (ferror(fp)) fprintf(stderr, "read error\n");
else /* feof(fp) */ ; /* 正常讀完 */
6. stderr 與 exit
錯誤訊息一律印到 stderr:它不經緩衝、且與正常輸出分流
(./prog > out.txt 時錯誤仍會顯示在螢幕)。
exit(status) 可在任何函式中結束程式;main 中 return n 等價於 exit(n)。
範例程式
| 檔案 | 內容 |
|---|---|
01_text_io_basics.c |
建立測試檔後示範:fputc/fgetc、fputs/fgets、fprintf、行數字數統計(wc 雛形)、feof/ferror |
02_binary_seek.c |
結構陣列的 fwrite/fread、fseek/ftell/rewind 隨機存取第 i 筆、量檔案大小 |
03_copy_and_csv.c |
檔案複製(含錯誤處理的完整版)、CSV 寫入與逐欄解析(fgets+strtok) |
每支程式都會先把測試檔寫進 /tmp/,再讀回展示,無需準備任何外部檔案:
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_text_io_basics.c
/tmp/demo
常見錯誤與陷阱
1. 不檢查 fopen 的回傳值
FILE *fp = fopen("nonexistent.txt", "r");
fgets(line, sizeof line, fp); /* fp 是 NULL -> 當掉 */
修正:開檔後立刻 if (fp == NULL) { perror("fopen"); exit(EXIT_FAILURE); }。
2. 用 char 接 fgetc 的回傳值
char c;
while ((c = fgetc(fp)) != EOF) ... /* char 裝不下 EOF 與全部 256 種位元組 */
平台上 char 為無號時迴圈永不結束;為有號時位元組 0xFF 會被誤判成 EOF。
修正:int c;。
3. 忘記 fclose(或以為它只是禮貌)
寫入是有緩衝的:不 fclose(或 fflush),最後一批資料可能根本沒落地。
而且行程能同時開啟的檔案數有限。規則:一個 fopen 對一個 fclose。
4. 用 "w" 開啟想保留的檔案
FILE *fp = fopen("precious.log", "w"); /* 內容瞬間被清空! */
要附加用 "a";要讀改用 "r+"。"w" 是「砍掉重練」。
5. 二進位資料用文字模式與跨平台直寫結構
- 二進位檔開檔忘了
b:在 Windows 上換行會被翻譯,資料毀損(macOS 無差別但要養成習慣)。 - 直接
fwrite結構再到別的機器fread:endianness 與 padding 都可能不同, 可攜格式要逐欄位序列化。
練習題
- 寫程式統計一個文字檔的字元數、單字數、行數(自己先產生測試檔),對照
wc的輸出。 - 寫
int file_size(const char *path):用 fseek/ftell 回傳檔案大小,失敗回傳 -1。 - 寫「加上行號輸出」程式:讀文字檔,每行前面印
%6d:(cat -n 的雛形)。 - 寫學生成績的二進位資料庫:
struct student陣列 fwrite 到檔案, 再用 fseek 直接讀出「第 k 筆」而不讀整個檔案。 - 擴充 CSV 範例:支援欄位內含空白,並把解析結果依某一欄排序後另存新 CSV。
- (進階)實作簡單的
grep:mygrep 關鍵字 檔案——逐行讀取, 含關鍵字的行印出行號與內容(用strstr)。
延伸閱讀
- K&R 2nd Edition,§7.5–7.7、Appendix B1(本單元主軸)
- K&R §8.1–8.4:系統呼叫層的 I/O(open/read/write,無緩衝)——理解 stdio 底下的世界
man fopen、man fread、man fseek(macOS)- 恭喜完成中階教程!接下來可挑戰進階主題:鏈結串列與樹(K&R §6.5–6.6)、 函式指標(§5.11)、自製 malloc(§8.7)