文件 I/O 是程序與持久化存儲、外設交互的橋樑。本文系統梳理文件分類、標準流、緩衝機制、FILE 結構體本質、打開模式語義、核心 API 族(文本/二進制、隨機訪問)、工程級錯誤處理模式,構建 C 文件操作完整知識體系。
| 類別 | 示例 | 特點 |
|---|---|---|
| 流文件 | 磁盤文件、管道、Socket | 字節序列,順序/隨機訪問 |
| 設備文件 | stdin/stdout/stderr、串口、終端、/dev/null | 內核抽象的字符/塊設備接口 |
FILE *stdin = fdopen(0, "r"); // 標準輸入,默認鍵盤 FILE *stdout = fdopen(1, "w"); // 標準輸出,默認終端 FILE *stderr = fdopen(2, "w"); // 標準錯誤,默認終端(無緩衝或行緩衝)
關鍵區別: stdout 默認行緩衝(遇 \n 刷新),stderr 默認無緩衝(即時輸出),重定向到文件時均變為全緩衝。
| 條件 | 觸發機制 | 代碼示例 |
|---|---|---|
| 遇換行符 | 行緩衝模式下自動 | printf("hello\n"); |
| 程序正常退出 | exit()/return from main 觸發 fcloseall | return 0; |
調用 fflush() | 手動強制刷新 | fflush(stdout); |
| 緩衝區滿 | 默認 8192 字節(BUFSIZ) | 大量 printf 無 \n 時自動觸發 |
FILE 結構體本質(Linux glibc 實現節選)struct _IO_FILE {
int _flags; // 標誌位:_IO_MAGIC, _IO_USER_BUF, _IO_LINE_BUF...
char *_IO_read_ptr; // 讀緩衝區當前指針
char *_IO_read_end; // 讀緩衝區末端
char *_IO_read_base; // 讀緩衝區基址
char *_IO_write_base; // 寫緩衝區基址
char *_IO_write_ptr; // 寫緩衝區當前指針
char *_IO_write_end; // 寫緩衝區末端
char *_IO_buf_base; // 緩衝區基址(統一)
char *_IO_buf_end; // 緩衝區末端
// ... 文件描述符、鎖、跳轉表等
};
typedef struct _IO_FILE FILE;
工程啟示: FILE* 是不透明指針,絕不直接解引用訪問內部字段,僅通過標準 API 操作。
fopen 打開模式完整語義表| 模式 | 讀 | 寫 | 創建 | 截斷 | 定位 | 備註 |
|---|---|---|---|---|---|---|
r | ✓ | ✗ | ✗ | ✗ | 開頭 | 文件必須存在 |
w | ✗ | ✓ | ✓ | ✓ | 開頭 | 清空現有內容 |
a | ✗ | ✓ | ✓ | ✗ | 末尾 | 追加寫入 |
r+ | ✓ | ✓ | ✗ | ✗ | 開頭 | 讀寫不截斷 |
w+ | ✓ | ✓ | ✓ | ✓ | 開頭 | 清空後讀寫 |
a+ | ✓ | ✓ | ✓ | ✗ | 末尾 | 追加讀寫 |
rb/wb/ab/... | 同以上,但二進制模式(無 \n↔\r\n 轉換) | |||||
Windows 額外模式: t 文本模式(默認),b 二進制模式。跨平台代碼務必顯式指定 b 避免換行符轉換差異。
fgets/fputschar *fgets(char *s, int size, FILE *stream); // 讀一行含 \n
int fputs(const char *s, FILE *stream); // 寫一行不加 \n
// 文件拷貝範例
while (fgets(buf, sizeof(buf), fp_src))
fputs(buf, fp_dst);
fgetc/fputcint fgetc(FILE *stream); // 返回 unsigned char 轉 int,EOF=-1 表錯誤/結束 int fputc(int c, FILE *stream); // 寫低 8 位字符 // 字符統計範例 int ch, cnt = 0; while ((ch = fgetc(fp)) != EOF) cnt++;
fprintf/fscanffprintf(fp, "%d %s %.2f\n", id, name, score); fscanf(fp, "%d %s %f", &id, name, &score);
fread/fwrite(高性能、結構體持久化首選)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);
// 結構體數組寫入
struct Student { int id; char name[20]; float gpa; } stu[100];
fwrite(stu, sizeof(struct Student), 100, fp);
// 讀取返回實際項數,0 表錯誤/EOF 需配合 feof/ferror 區分
fseek/ftell/rewindint fseek(FILE *stream, long offset, int whence);
// whence: SEEK_SET(開頭) SEEK_CUR(當前) SEEK_END(末尾)
long ftell(FILE *stream); // 返回當前偏移量
void rewind(FILE *stream); // 等價 fseek(stream, 0, SEEK_SET) + 清錯誤標誌
// 文件擴展技巧:fseek 到超過文件大小位置,再寫入 → 文件擴大(空洞文件)
fseek(fp, 1000000, SEEK_SET);
fputc('x', fp); // 文件變 1MB+1 字節
feof/ferror/clearerr// 標準讀取循環模式
while (fread(&item, size, 1, fp) == 1) {
process(item);
}
if (ferror(fp)) handle_error();
else if (feof(fp)) handle_eof();
FILE *safe_fopen(const char *path, const char *mode) {
FILE *fp = fopen(path, mode);
if (!fp) {
fprintf(stderr, "FATAL: fopen('%s','%s') failed: %s\n",
path, mode, strerror(errno));
exit(EXIT_FAILURE);
}
return fp;
}
// 使用
FILE *fp = safe_fopen("data.bin", "rb");
// ... 操作 ...
if (fclose(fp) != 0) { // fclose 也會 fflush,可能失敗
perror("fclose failed");
}
ulimit -n 1024(軟限制),硬限制 4096/1048576ulimit -n 65535 或修改 /etc/security/limits.confgetrlimit(RLIMIT_NOFILE, &rlim)"rb"/"wb" 避免換行符轉換驚喜fopen/fread/fwrite/fseek/fcloseferror/feof 區分錯誤與 EOF:fread 返回 0 可能是兩者fseeko/ftello(off_t 64位)tmpfile()/mkstemp() 自動清理C 文件操作看似 API 繁多,實則圍繞「緩衝管理、定位控制、錯誤語義」三大核心。建立「流=緩衝+文件描述符」心智模型,熟練掌握 fread/fwrite 高性能二進制通道與 fseek/ftell 隨機訪問組合拳,即可駕馭 95% 文件處理場景。