首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

APUE 讀書筆記:標準 I/O 庫 (上) —— 流與 FILE、緩衝機制、打開流、讀寫流、逐行 I/O

     編輯:關於C語言

前言:《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`**。
  • `+` (讀寫) 限制:
    1. 輸出後接輸入,**必須中間插 `fflush`/`fseek`/`fsetpos`/`rewind`**
    2. 輸入後接輸出,**必須中間插 `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 關鍵行為 (必須掌握)

  1. **保留 `\n`**:讀到換行符會存入緩衝區 (若空間夠),後接 `\0`。
    → 判斷是否讀完整行:檢查 `buf[strlen(buf)-1] == '\n'`。
  2. **行過長截斷**:若行長 ≥ n,只讀 n-1 字元,**不含 `\n`**,下次 `fgets` 續讀該行剩餘部分。
    → 處理超長行需迴圈拼接。
  3. **返回值**:成功返回 `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 實現細節與臨時檔案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved