在 C 語言中,「判斷檔案是否存在」看似簡單,實則有**檔案 vs 目錄**、**權限檢查 vs 純存在性**、**跨平台相容性**等細微差別。本文系統梳理三大主流方法,附帶錯誤碼對照與最佳實踐建議。
#include <unistd.h> // Linux/macOS
// #include <io.h> // Windows (MSVC)
int access(const char *pathname, int mode);
| 宏常數 | 數值 | 檢查內容 | 適用場景 |
|---|---|---|---|
F_OK | 0 | **純存在性**(檔案或目錄) | 最常用,不關心權限 |
R_OK | 4 | 讀取權限 | 需確認可讀才打開 |
W_OK | 2 | 寫入權限 | 需確認可寫才打開 |
X_OK | 1 | 執行/搜尋權限 | 檢查可執行檔/目錄可進入 |
常見 `errno`:
| 錯誤碼 | 含義 | 典型原因 |
|---|---|---|
ENOENT | 檔案/目錄不存在 | 路徑錯誤、尚未建立 |
EACCES | 權限不足 | 無讀取目錄權限、檔案權限不符 mode |
ENAMETOOLONG | 路徑過長 | 超過 `PATH_MAX` |
ELOOP | 符號連結過多 | 遞迴符號連結 |
EROFS | 唯讀檔案系統 | 檢查 `W_OK` 時觸發 |
#include <stdio.h>
#include <unistd.h> // Windows 用 io.h
int main() {
const char *path = "/etc/passwd";
// 1. 純存在性檢查
if (access(path, F_OK) == 0) {
printf("存在: %s\n", path);
} else {
printf("不存在: %s (errno=%d: %s)\n", path, errno, strerror(errno));
}
// 2. 同時檢查讀寫權限
if (access(path, R_OK | W_OK) == 0) {
printf("可讀可寫\n");
}
return 0;
}
Time-Of-Check to Time-Of-Use:
if (access("config.ini", W_OK) == 0) {
// 攻擊者在這裡刪除/替換檔案、修改權限
FILE *fp = fopen("config.ini", "w"); // 可能失敗或寫入錯誤檔案
}
對策: **不要用 access 預檢權限再 fopen**。直接嘗試 `fopen`,失敗再處理錯誤。`access` 僅適合「純存在性檢查」或「非關鍵路徑的權限預判」。
FILE *fopen(const char *pathname, const char *mode);
| mode | 檔案不存在時 | 檔案存在時 | 適合判斷存在性? |
|---|---|---|---|
"r" / "rb" | 返回 NULL ✓ | 返回檔案指標 ✓ | **是**(唯讀,無副作用) |
"w" / "wb" | **建立新檔** ✗ | **截斷為 0** ✗ | **否**(破壞性) |
"a" / "ab" | **建立新檔** ✗ | 定位到檔尾 ✗ | **否**(建立副作用) |
"r+" / "rb+" | 返回 NULL ✓ | 可讀寫,不截斷 ✓ | 可用,但需關閉 |
int file_exists(const char *path) {
FILE *fp = fopen(path, "rb"); // 唯讀二進制,最安全
if (fp) {
fclose(fp);
return 1;
}
return 0; // 可能是不存在、無權限、是目錄等
}
#include <sys/stat.h>
// Linux/macOS: int stat(const char *path, struct stat *buf);
// Windows: int _stat(const char *path, struct _stat *buf);
#include <sys/stat.h>
#include <stdio.h>
int is_regular_file(const char *path) {
struct stat st;
if (stat(path, &st) != 0) return 0; // 不存在或無權限訪問
return S_ISREG(st.st_mode); // 是否為一般檔案
}
int main() {
printf("/etc/passwd: %s\n", is_regular_file("/etc/passwd") ? "一般檔案" : "其他/不存在");
printf("/etc: %s\n", is_regular_file("/etc") ? "一般檔案" : "目錄/其他");
printf("/nonexist: %s\n", is_regular_file("/nonexist") ? "一般檔案" : "不存在");
return 0;
}
| 維度 | access() | fopen("rb") | stat() |
|---|---|---|---|
| **標準** | POSIX.1 | C89 (ISO C) | POSIX.1 |
| **跨平台** | Linux/macOS/Windows(io.h) | **全平台** | Linux/macOS/Windows(_stat) |
| **純存在性檢查** | ✓ 最佳 | ✓ 可用 | ✓ 可用 |
| **權限檢查** | ✓ 詳細 (R/W/X) | ✗ 無法區分 | ✓ 透過 st_mode |
| **檔案類型判斷** | ✗ | ✗ | ✓ 完整 (S_IS*) |
| **目錄檢查** | ✓ (F_OK) | ✗ | ✓ (S_ISDIR) |
| **副作用** | 無 | 無 (用 "rb") | 無 |
| **TOCTOU 風險** | 高 (權限檢查時) | 低 (直接嘗試) | 中 |
| **效能** | 較快 | 較慢 (開關檔) | 中等 |
// 跨平台封裝範例
#ifdef _WIN32
#include <io.h>
#define F_OK 0
#define access _access
#define stat _stat
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#else
#include <unistd.h>
#include <sys/stat.h>
#endif
bool file_exists(const char *path) { return access(path, F_OK) == 0; }
bool is_regular_file(const char *path) { struct stat st; return stat(path, &st)==0 && S_ISREG(st.st_mode); }
總結: 「判斷檔案存在」沒有銀彈,關鍵在於**後續意圖**。單純存在性用 `access(F_OK)`;即將操作檔案直接 `fopen` 處理錯誤;需要豐富屬性用 `stat()`。切記:**access 預檢權限再操作是反模式**,除非你在寫「檢查工具」而非「業務邏輯」。