前言:《UNIX 環境高級程式設計》(APUE) 第 5 章標準 I/O 庫上半部核心內容
本文系統梳理:**流與 FILE 物件抽象、三種緩衝模式 (全/行/無) 及控制 API、打開流的三大函數與 15 種模式字串、讀寫流的三層級 API (字元/行/二進制)、逐行 I/O 陷阱 (fgets vs gets、fputs vs puts)**。夯實 C 語言檔案操作根基。
一、流與 FILE 物件:標準 I/O 的核心抽象
系統 I/O vs 標準 I/O
| 維度 | 系統 I/O (Unix I/O) | 標準 I/O (Stdio) |
| 核心物件 | 文件描述符 `int fd` (小非負整數) | **流** `FILE *fp` (指向結構體指標) |
| 緩衝 | **無用戶空間緩衝** (核心有頁面快取) | **用戶空間緩衝** (核心: 減少系統呼叫) |
| API 風格 | 底層、原子、無格式 (`read/write/lseek`) | 高層、格式化、豐富 (`fprintf/fscanf/fgets`) |
| 可移植性 | POSIX / Unix-like | **ISO C 標準**,全平台 (Windows, 嵌入式...) |
| 標準流 | `0=STDIN_FILENO, 1=STDOUT_FILENO, 2=STDERR_FILENO` | `stdin, stdout, stderr` (定義在 ``) |
FILE 結構體內部 (概念模型)
typedef struct {
int fd; // 底層文件描述符
char *buf; // 緩衝區基址
size_t buf_size; // 緩衝區大小
size_t buf_used; // 緩衝區已用字元數
char *read_ptr; // 讀取指標
char *write_ptr; // 寫入指標
unsigned short flags; // _IOFBF/_IOLBF/_IONBF, _IOERR, _IOEOF...
// ... 鎖、定位狀態、轉換狀態等
} FILE;
關鍵點:`FILE*` 稱為**檔案指標** (file pointer),實為**不透明物件指標**,應用程式**不可直接解引用存取成員**,只能透過標準 I/O 函數操作。
二、緩衝機制:標準 I/O 效能的關鍵
三種緩衝模式
| 模式 | 觸發實際 I/O 條件 | 典型場景 | 預設對象 |
| 全緩衝 (_IOFBF) | 緩衝區**填滿** (通常 4KB/8KB),或呼叫 `fflush()`,或正常關閉 | 磁碟檔案、管道、Socket (非終端) | 非終端流 |
| 行緩衝 (_IOLBF) | 遇到**換行符 `\n`**,或緩衝區填滿,或需從無緩衝/行緩衝輸入流讀取時 (刷新關聯輸出流) | 終端輸入輸出 (`stdin`, `stdout` 連接終端) | 終端流 |
| 無緩衝 (_IONBF) | **每次 I/O 操作** 直接系統呼叫 | 標準錯誤 `stderr`、即時日志、信號處理中輸出 | `stderr` |
ANSI C 強制要求
- `stdin` / `stdout`:**僅當不涉及互動式設備時** 才為全緩衝 (即重導向到檔案/管道時)。
- `stderr`:**絕不**為全緩衝 (通常無緩衝),保證錯誤資訊即時顯示。
緩衝控制 API
#include <stdio.h>
// 簡易版: 僅切換開/關 或 指定自有緩衝區 (必須 BUFSIZ 大小)
void setbuf(FILE *fp, char *buf); // buf=NULL → 無緩衝
// 完整版: 精確指定模式、緩衝區、大小
int setvbuf(FILE *fp, char *buf, int mode, size_t size);
// mode: _IOFBF (全) | _IOLBF (行) | _IONBF (無)
// buf=NULL, size=0 → 標準庫自動分配 (通常 BUFSIZ 或 st_blksize)
// 必須在**打開流後、任何讀寫操作前**呼叫
強制刷新
int fflush(FILE *fp); // fp=NULL → 刷新**所有輸出流**
// 返回: 成功 0, 失敗 EOF (設定 errno)
// 場景: 互動式提示無換行、關閉前確保資料落盤、fork 前刷新避免雙重輸出
三、打開流:fopen / freopen / fdopen
三大函數差異
| 函數 | 用途 | 典型場景 |
| `fopen(path, mode)` | 打開路徑對應檔案 | 最常用 |
| `freopen(path, mode, fp)` | **在現有流 fp 上** 打開新檔案 (先關閉 fp) | 重導向標準流:`freopen("log.txt", "w", stdout)` |
| `fdopen(fd, mode)` | **將現有文件描述符 fd 封裝為 FILE*** | Socket、管道、`dup`/`fcntl` 獲得的 fd |
ISO C 規定的 15 種模式字串 (核心 6 種 + b/+ 組合)
| 模式 | 讀 | 寫 | 建立 | 截斷 | 定位 | 二進制 |
| `r` / `rb` | ✓ | ✗ | ✗ | ✗ | 開頭 | b |
| `w` / `wb` | ✗ | ✓ | ✓ | ✓ | 開頭 | b |
| `a` / `ab` | ✗ | ✓ | ✓ | ✗ | 檔尾 (每次寫入前 seek end) | b |
| `r+` / `rb+` / `r+b` | ✓ | ✓ | ✗ | ✗ | 開頭 | b |
| `w+` / `wb+` / `w+b` | ✓ | ✓ | ✓ | ✓ | 開頭 | b |
| `a+` / `ab+` / `a+b` | ✓ | ✓ | ✓ | ✗ | 檔尾 (讀從開頭,寫前 seek end) | b |
關鍵細節
- `b` (二進制) 模式:UNIX/Linux **無實際作用** (不轉換 `\n`),但 **Windows 會抑制 `\n` ⇄ `\r\n` 轉換**。**跨平台程式務必加 `b`**。
- `+` (讀寫) 限制:
- 輸出後接輸入,**必須中間插 `fflush`/`fseek`/`fsetpos`/`rewind`**
- 輸入後接輸出,**必須中間插 `fseek`/`fsetpos`/`rewind`**,除非輸入遇到 EOF
- 追加模式 `a`/`a+` 原子性**:多進程同時 `fopen(..., "a")` 寫入,**每次寫入前自動 `lseek(fd, 0, SEEK_END)`**,保證資料不覆蓋、順序寫入 (POSIX 保證)。
- 建立檔案權限**:`w`/`a` 建立新檔時,權限為 `0666 & ~umask` (通常 `0644`)。無法在 `fopen` 指定自定權限,需 `open` + `fdopen` 或 `chmod` 後處理。
關閉流
int fclose(FILE *fp);
// 動作: 刷新輸出緩衝區 → 釋放輸入緩衝區 → 關閉底層 fd → 釋放 FILE 結構體 (若標準庫分配)
// 返回: 成功 0, 失敗 EOF
// 程式正常結束 (exit/return from main) 會自動 fclose 所有流
四、讀寫流:三層級 API 選擇指南
層級 1:逐字元 I/O (最基礎)
int getc(FILE *fp); // 通常為巨集,快,參數無副作用
int fgetc(FILE *fp); // 函數,可取地址傳遞,稍慢
int getchar(void); // = getc(stdin)
int putc(int c, FILE *fp); // 巨集
int fputc(int c, FILE *fp); // 函數
int putchar(int c); // = putc(c, stdout)
// 返回: 成功 = unsigned char 轉 int (0~255),失敗/EOF = EOF (-1)
// **不可存入 char 變數** (無法區分 0xFF 和 EOF)
字元推回:ungetc
int ungetc(int c, FILE *fp);
// 將字元推回流,下次讀取返回該字元
// 限制: 每次調用**最多推回 1 字元** (標準僅保證 1),不能推回 EOF
// 用途: 詞法分析「前瞻一字元」、解析器回溯
錯誤與 EOF 區分
int ferror(FILE *fp); // 檢查錯誤標誌
int feof(FILE *fp); // 檢查 EOF 標誌
void clearerr(FILE *fp); // 清除兩者
// FILE 內部維護兩獨立標誌:_IOERR, _IOEOF
層級 2:逐行 I/O (文本處理主力)
char *fgets(char *buf, int n, FILE *fp); // 讀取至 \n 或 n-1 字元,保留 \n,補 \0
char *gets(char *buf); // **已棄用 (C11 移除)**:無長度限制 → 緩衝區溢位,**嚴禁使用**
int fputs(const char *s, FILE *fp); // 寫入字串,**不寫入結尾 \0**,不自動加 \n
int puts(const char *s); // 寫入字串到 stdout,**自動加 \n** (不寫 \0)
fgets 關鍵行為 (必須掌握)
- **保留 `\n`**:讀到換行符會存入緩衝區 (若空間夠),後接 `\0`。
→ 判斷是否讀完整行:檢查 `buf[strlen(buf)-1] == '\n'`。
- **行過長截斷**:若行長 ≥ n,只讀 n-1 字元,**不含 `\n`**,下次 `fgets` 續讀該行剩餘部分。
→ 處理超長行需迴圈拼接。
- **返回值**:成功返回 `buf`,EOF/錯誤返回 `NULL`。需配合 `feof`/`ferror` 區分。
層級 3:二進制 I/O (fread/fwrite) —— 見「下篇」
五、常見陷阱與最佳實踐總結
| 陷阱 | 錯誤代碼 | 正確做法 | 原因 |
| 用 `gets` | `gets(buf);` | `fgets(buf, sizeof(buf), stdin);` | 緩衝區溢位漏洞,C11 已移除 |
| 忽略 `fgets` 保留 `\n` | `if (strcmp(buf, "quit") == 0)` | `buf[strcspn(buf, "\n")] = 0;` 去除尾部換行 | `buf` 含 `\n`,比對失敗 |
| `scanf` 後直接 `fgets` | `scanf("%d", &n); fgets(buf, ...);` | 中間加 `while(getchar()!='\n');` 清理緩衝區 | 殘留 `\n` 導致 `fgets` 讀到空行 |
| 用 `sprintf` | `sprintf(buf, "%s", src);` | `snprintf(buf, sizeof(buf), "%s", src);` | 緩衝區溢位風險 |
| 二進制檔未加 `b` 模式 | `fopen("data.bin", "w");` | `fopen("data.bin", "wb");` | Windows 上 `\n` 被轉換損壞資料 |
| `r+` 模式讀寫切換無定位 | `fprintf(fp, "x"); fscanf(fp, "%d", &n);` | 中間加 `fseek(fp, 0, SEEK_CUR);` 或 `fflush(fp)` | 標準要求切換方向必須定位/刷新 |
| 未檢查 `fopen` 返回值 | `FILE *fp = fopen(...); fprintf(fp, ...);` | `if (!fp) { perror(...); exit(1); }` | NULL 解引用導致崩潰 |
總結: 標準 I/O 庫以 **`FILE*` 封裝 `fd` + 用戶空間緩衝** 為核心,提供**可移植、高效、功能豐富**的檔案操作介面。掌握 **三種緩衝模式觸發條件**、**15 種模式字串語義差異**、**逐行 I/O 保留 `\n` 與截斷行為**、**`r+/w+/a+` 切換限制**、**`gets`/`sprintf` 禁用**,是寫出正確、安全、高效 C 檔案操作代碼的基石。下篇將深入二進制 I/O、流定位、格式化 I/O 實現細節與臨時檔案。