C 語言檔案處理 (File I/O)

為了永久儲存資料(例如儲存遊戲存檔、記錄 log),我們需要使用檔案。C 語言透過 FILE 結構與一系列的標準函式庫來處理檔案讀寫。

基本檔案操作

1. 開啟檔案 (fopen)

// FILE *fopen(const char *filename, const char *mode);
FILE *fp = fopen("data.txt", "r");

操作模式 (Mode)

  • "r" (Read):讀取。檔案必須存在,否則回傳 NULL。
  • "w" (Write):寫入。檔案若不存在會建立;若存在會清空 (Truncate) 內容。
  • "a" (Append):追加。寫入的資料會接在原本內容後面。
  • "r+":讀寫模式(檔案須存在)。
  • "w+":讀寫模式(會清空檔案)。
永遠檢查 fopen 是否成功。如果 fpNULL,代表開檔失敗(可能權限不足、硬碟滿了或檔案不存在)。

2. 關閉檔案 (fclose)

fclose(fp);

操作完檔案後,一定要關閉,確保資料完整寫入硬碟緩衝區並釋放系統資源 (File Descriptor)。

文字檔讀寫 (Text I/O)

適合處理人類可讀的資料 (txt, csv, config)。

寫入 (fprintf)

用法跟 printf 幾乎一樣。

fprintf(fp, "Score: %d\n", 100);

讀取 (fscanf vs fgets)

  • fscanf:適合讀取有固定格式的資料。
    int score;
    fscanf(fp, "Score: %d", &score);
    
  • fgets:適合讀取一整行文字(更安全,推薦)。
    char buffer[100];
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }
    

二進位檔讀寫 (Binary I/O)

如果要儲存圖片、聲音、或是結構 (Struct) 資料,應該使用二進位模式。這樣可以確保資料原封不動地寫入,不會受到換行符號 (\n vs \r\n) 轉換的影響。

模式字串要加上 b,例如 "rb", "wb"

寫入 (fwrite)

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

讀取 (fread)

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

範例:儲存陣列到檔案

#include <stdio.h>

int main() {
    int data[] = {10, 20, 30, 40, 50};
    
    // 1. 寫入二進位檔
    FILE *fp = fopen("data.bin", "wb"); // 注意 "wb"
    if (fp != NULL) {
        // 寫入 5 個 int
        fwrite(data, sizeof(int), 5, fp);
        fclose(fp);
    }

    // 2. 讀取二進位檔
    int read_data[5];
    fp = fopen("data.bin", "rb"); // 注意 "rb"
    if (fp != NULL) {
        // 讀取回來
        fread(read_data, sizeof(int), 5, fp);
        fclose(fp);
    }

    return 0;
}

標準串流 (Standard Streams)

其實 printfscanf 只是特例,它們分別是對以下標準檔案指標操作:

  • stdin (Standard Input):標準輸入(通常是鍵盤)。
  • stdout (Standard Output):標準輸出(通常是螢幕)。
  • stderr (Standard Error):標準錯誤輸出(通常也是螢幕)。

這意味著你可以用 fprintf(stdout, "Hello") 來取代 printf("Hello")

技巧:發生錯誤時,請將錯誤訊息印到 stderr,而不是 stdout。這樣使用者在使用管線 (Pipeline) 重導向輸出時,錯誤訊息才不會被導到檔案裡。

if (fp == NULL) {
    fprintf(stderr, "Fatal Error: Cannot open config file!\n");
    return 1;
}

最佳實踐

  1. 總是檢查 fopen 的回傳值
  2. 使用 perrorstrerror:當開檔失敗時,這些函式可以告訴你「為什麼」失敗(例如:No such file or directory)。
    #include <errno.h>
    #include <string.h>
    // ...
    if (fp == NULL) {
        fprintf(stderr, "Error opening file: %s\n", strerror(errno));
    }
    
  3. 文字檔用文字模式,二進位檔用二進位模式:雖然在 Linux 上沒差,但在 Windows 上混用會導致換行符號錯亂。

掌握檔案處理,你的程式就能夠持久化資料,與作業系統進行更深度的互動。