本文涵蓋:**標準 I/O 效能基準測試分析、二進制 I/O (fread/fwrite)、流定位 (ftell/fseek vs fgetpos/fsetpos)、格式化 I/O 族函數、標準 I/O 實現細節 (FILE 結構、文件描述符關聯)、臨時檔案建立 (tmpnam/tmpfile/tempnam)**。配合原書圖表數據深度解讀。
| 版本 | 用戶 CPU | 系統 CPU | 時鐘時間 | 說明 |
|---|---|---|---|---|
| read/write BUFSIZ=1 | 高 | 高 | **最慢** | 300 萬次系統呼叫 |
| read/write BUFSIZ=4096 | 低 | 低 | **最快 (基準)** | 360 次系統呼叫 |
| getc/putc (巨集) | 中高 | 低 | ~2x 基準 | 300 萬次巨集展開,無系統呼叫開銷 |
| fgetc/fputc (函數) | 中高 | 低 | ~2x 基準 | 300 萬次函數呼叫 |
| fgets/fputs | 中 | 低 | ~1x 基準 | 3 萬次函數呼叫 (按行),memcpy 優化 |
標準 I/O 為處理**非文本資料** (結構體、陣列、二進制檔案) 提供:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// 返回:成功讀寫的**項目數** (nmemb),非位元組數
// 1. 讀寫基本型別陣列
float data[100];
fwrite(&data[2], sizeof(float), 4, fp); // 寫入第 3~6 個元素 (索引 2,3,4,5)
// 2. 讀寫結構體 (整體序列化)
struct Record {
int id;
char name[32];
double score;
} rec;
fwrite(&rec, sizeof(rec), 1, fp); // size=sizeof(rec), nmemb=1
long ftell(FILE *fp); // 返回相對檔案開頭的位元組偏移量,錯誤 -1L
int fseek(FILE *fp, long offset, int whence); // whence: SEEK_SET/CUR/END
void rewind(FILE *fp); // 等同 fseek(fp, 0, SEEK_SET) + 清除錯誤標誌
int fgetpos(FILE *fp, fpos_t *pos); // 將當前位置存入不透明物件 pos
int fsetpos(FILE *fp, const fpos_t *pos); // 還原位置
// fpos_t 可為結構體/長整數,足以表示 64 位甚至更大偏移
int printf(const char *format, ...); // → stdout
int fprintf(FILE *fp, const char *format, ...); // → 指定流
int sprintf(char *buf, const char *format, ...); // → 字串緩衝區 (危險:無長度檢查)
int snprintf(char *buf, size_t n, const char *format, ...); // 安全版 (C99)
// 可變參數列表版 (v-前綴)
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *fp, const char *format, va_list ap);
int vsprintf(char *buf, const char *format, va_list ap);
int vsnprintf(char *buf, size_t n, const char *format, va_list ap);
關鍵點:`sprintf` **極易造成緩衝區溢位**,**嚴禁使用**,永遠用 `snprintf`。返回值為**寫入字元數 (不含結尾 `\0`)**,截斷時返回**本應寫入的總字元數** (C99 規範)。
int scanf(const char *format, ...); // ← stdin
int fscanf(FILE *fp, const char *format, ...); // ← 指定流
int sscanf(const char *buf, const char *format, ...); // ← 字串
// 返回值:成功賦值的**項目數**,EOF 表示輸入失敗/結束
// 掃描集: %[^\\n] 讀取整行, %*d 跳過整數不賦值
%[flags][width][.precision][length]specifier
flags: - (左對齊) + (正負號) 空格 (正數前空格) # (替代形式) 0 (前導零)
width: 最小欄寬 (可為 * 由參數指定)
precision: .n 小數位數/字串最大長度 (可為 *)
length: hh (char) h (short) l (long/double) ll (long long) L (long double) j (intmax_t) z (size_t) t (ptrdiff_t)
specifier: d/i (有號十進制) u (無號十進制) o (八進制) x/X (十六進制) f/F (定點) e/E (科學) g/G (自動) a/A (十六進制浮點) c (字元) s (字串) p (指標) n (寫入已輸出字元數) % (字面 %)
用戶程式
│
▼
FILE *fp (指向 _IO_FILE / struct __sFILE 等實現結構)
│
├── int _fileno (文件描述符 fd) ←→ 核心檔案表 → inode → 磁碟
├── unsigned char *_IO_buf_base (緩衝區基址)
├── size_t _IO_buf_size (緩衝區大小)
├── unsigned char *_IO_read_ptr / _IO_write_ptr (讀/寫指標)
├── _IO_read_end / _IO_write_end (緩衝區邊界)
├── short _flags (緩衝模式: _IO_UNBUFFERED, _IO_LINE_BUF, _IO_NO_READS...)
└── 鎖 (執行緒同步)
│
▼
標準 I/O 函數 (fread, fprintf, fseek...)
│
▼ (緩衝區滿/刷新/無緩衝/行緩衝遇換行)
系統呼叫 (read, write, lseek, close)
int fileno(FILE *fp); // 返回關聯的文件描述符
// 用途: 需要混用系統 IO 與標準 IO 時 (如: 將 socket fd 封裝為 FILE*, 或對 fd 進行 fcntl/epoll 操作)
int setvbuf(FILE *fp, char *buf, int mode, size_t size);
// mode: _IOFBF (全緩衝) | _IOLBF (行緩衝) | _IONBF (無緩衝)
// buf=NULL, size=0 → 標準庫自動分配 (通常 BUFSIZ=8192 或 st_blksize)
// 必須在**打開流後、任何操作前**呼叫
| 函數 | 返回值 | 特點 | 安全性 | 適用場景 |
|---|---|---|---|---|
tmpnam(char *s) | 路徑字串指標 | 只產生**唯一路徑名**,**不建立檔案**。需自行 `fopen`。最多 `TMP_MAX` 次調用。 | 低 (TOCTOU 競態:產生名→開檔間隙可被搶佔) | 需預知路徑名、跨進程共享臨時檔 |
tmpfile(void) | FILE* (二進制 wb+) | **原子性建立並打開**唯一臨時檔。**關閉或程式結束時自動刪除**。無路徑名洩露。 | **最高** (推薦) | 大多數臨時資料處理、單進程內部使用 |
tempnam(const char *dir, const char *pfx) | 路徑字串指標 (malloc 分配,需 free) | 可指定目錄 (`dir`/`TMPDIR`/`P_tmpdir`/`/tmp`) 與前綴。不建立檔案。 | 低 (同 tmpnam),且需記得 free 返回指標 | 需控制臨時檔存放位置、命名規則 |
// mkstemp: 原子性建立唯一檔案並返回 fd (O_RDWR | O_CREAT | O_EXCL, 0600)
char template[] = "/tmp/myapp_XXXXXX";
int fd = mkstemp(template); // template 被修改為實際檔名
FILE *fp = fdopen(fd, "w+");
// 使用完畢:
fclose(fp); // 或 close(fd) + unlink(template)
// mkdtemp: 建立唯一目錄
char dtemplate[] = "/tmp/myapp_XXXXXX";
char *dir = mkdtemp(dtemplate);
總結: APUE 第 5 章下半部揭示了標準 I/O 庫「用戶空間緩衝」的本質:**以空間換時間,以複雜度換通用性**。理解 `fread/fwrite` 的二進制語義限制、掌握 `fgetpos/fsetpos` 的大檔案/跨平台優勢、警惕 `sprintf` 的緩衝區溢位、**優先使用 `tmpfile`/`mkstemp` 建立臨時資源**,是寫出健壯、高效、可移植 C 程式的關鍵。建議配合 `strace -e trace=read,write,lseek ./your_program` 觀察實際系統呼叫行為,驗證緩衝策略效果。