2023年6月12日 星期一

C/C++ 學習筆記 : 檔案處理

檔案以儲存形式來分有純文字檔 (text file) 與二進位檔 (binary file) 兩種, 文字檔以字元編碼 (ASCII 或 Unicode) 方式儲存資料, 可直接在螢幕輸出閱讀 (但比較占空間), 例如副檔名為 .txt, .log, .json, 或 .csv 者皆為文字檔, 也是程式專案中最常使用的檔案格式. 

而二進位檔是將記憶體中的資料以原始未經過編碼的 byte 形式原封不動儲存至檔案中, 需透過特定軟體解碼才能使用或閱讀, 例如編譯過的程式檔, 圖片, 影像, 以及音樂等均以二進位檔方式儲存 (因為這樣比較節省空間), 副檔名為 .dat, ,pdf, 或 .exe 等皆為二進位檔.  

本篇測試使用免費的線上開發平台 replit.com 執行程式碼, 此平台提供 Python, C, Java, C# 等程式語言的開發環境, 註冊使用者可將程式碼儲存在平台上或透過連結分享給別人, 參考 :



一. C 的檔案處理 : 

C 語言是以資料串流 (IO Stream) 方式來處理記憶體與周邊儲存裝置 (例如磁碟) 的資料傳輸, 但基於存取的效率考量, 檔案處理時程式並非直接對磁碟進行存取, 而是透過在記憶體中劃出的一塊緩衝區 (buffer) 當作串流的中介, 程式存取的是緩衝區存放的資料, 因此將資料寫入檔案時並非直接寫入磁碟, 而是先寫入緩衝區, 等關閉檔案時才真正寫入磁碟. C 語言標準函式庫中的 stdio 函式庫負責檔案的開啟, 讀寫, 以及關閉, 也會自動處理緩衝區的配置, 因此若程式要用到檔案輸出入時, 必須用 include 匯入 stdio.h 標頭檔.   


1. 開啟與關閉文字檔 :    

C 語言以文字串流 (text stream) 方式, 如同水管中的水流般一個字元一個字元地循序處理. C 語言的標準函式庫 stdio 提供檔案開啟, 讀寫, 與關閉等操作, 並會自動處理緩衝區. 使用標準函式庫處理檔案時必須先匯入 stdio.h 標頭檔 :

#include <stdio.h>

使用此函式庫開啟檔案時會傳回一個 FILE 型態的指標, 它會指向所開啟檔案的緩衝區, 因此處理檔案時要先宣告一個 FILE 型態的指標 :

FILE *fp;   

stdio 的檔案開啟與關閉函式 API 如下 :


 stdio 函式 說明
 FILE *fopen(char *filename, char *mode) 以指定模式開啟檔案, 成功傳回檔案指標, 失敗傳回 NULL.
 int fclose(FILE *fp) 傳入檔案指標關閉該檔案, 成功傳回 0, 失敗傳回 EOF


檔案操作結束務必呼叫 fclose() 並傳入檔案指標關閉檔案, 此函式會先將緩衝區內的資料寫回檔案後緩衝區記憶體. 

fclose(fp);     

fopen() 第二個參數為開啟模式字串, 可用的模式如下表 : 


 模式字串 說明
 r 以唯讀模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w 以覆蓋寫入模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a 以附加寫入模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 r+ 以讀寫模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w+ 以讀寫模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a+ 以附加讀寫模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 rb+ 以讀寫模式開啟已存在二進檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 wb+ 以讀寫模式開啟二進檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 ab+ 以附加讀寫模式開啟二進檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案


文字檔案的基本操作有讀取 (read only), 寫入 (write), 以及檔尾附加寫入 (append) 三種模式, 模式字串後面添加一個 "+" 表示可以更新檔案內容, 例如 r+ 與 w+ 都表示可讀寫檔案內容; 但 w+ 會先清除原來的內容, 而 r+ 則不會. 

fopen() 函式的第一參數為檔案名稱字串, 可以包含路徑, 在 Windows 系統下路徑之倒斜線必須用 \ 跳脫, 以下是絕對路徑的用法 : 

FILE *fp;                                           // 宣告指向檔案緩衝區的指標
fp=fopen("test.txt");                          // 開啟工作目錄下的 text.txt 檔 (目前目錄=C:\myfolder\)
fp=fopen("C:\\myfolder\\test.txt");   // 開啟指定目錄下的 text.txt 檔 
fp=fopen("C:/myfolder/test.txt");     // 開啟指定目錄下的 text.txt 檔

也可以使用相對路徑 :

fp=fopen("./myfolder/test.txt");     //目前目錄的子目錄 myfolder 下的 test.txt
fp=fopen("../myfolder/test.txt");    //目前目錄的上一層目錄 myfolder 下的 test.txt

fopen() 開啟檔案成功會傳回 FILE 型態的指標 (指向緩衝器位址), 開啟失敗則傳回 NULL, 個可利用傳回值判斷是否開檔成功, 例如 : 

if (fp==NULL) {
    printf("檔案開啟失敗\n");
    }
else {
    printf("檔案開啟成功\n");
    }

完整範例如下 :

/* 開啟和關閉文字檔案 test01.c */
#include <stdio.h>

int main() {
  FILE *fp;
  fp=fopen("helloworld.txt", "r");
  if (fp==NULL) {printf("檔案開啟失敗\n");}
  else {printf("檔案開啟成功\n");}
  fclose(fp);
  }

先在工作目錄下製作一個 helloworld.txt 檔案 (裡面有無內容均可), 執行程式結果會顯示檔案開啟成功; 然後用 rm 指令刪除 helloworld.txt 再次執行程式則顯示島案開啟失敗. 以下是在 replit.com 底下用 a.replit 執行的結果 : 




在 replit.com 執行 C 程式的方法參考下面這篇 :



2. 讀寫文字檔 :   

stdio 函式庫內建 fgets() 與 fputs() 函式來讀寫文字檔 : 


 讀寫文字檔的函式 說明
 char *fgets(char *str, int n, FILE *fp) 從指標 fp 讀取 n-1 個字元, 成功傳回 str 指標, 至檔尾傳回 NULL
 int fputs(char *str, FILE *fp) 將指標 str 字串寫入檔案 fp, 成功傳回非負整數, 失敗傳回 EOF


首先來測試用 fputs() 將字串寫入文字檔, 寫入文字檔可用 "w" 或 "a" 模式開檔, 用 "w" 模式開啟時若此檔案已存在, 則開啟成功後原本的內容會被清空; 若檔案不存在就建立一個空檔案, 總之用 "w" 模式建立的檔案, 其內容都是我們用 fputs() 等函式寫入的; 而 "a" 模式則是會保留原檔案內容 (如果檔案存在的話), 用 puts() 寫入的內容會附加在檔尾, 例如 :

/* 寫入文字檔 test02.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char *str1="Hello\n";       //用指標儲存字串
  char str2[10]="World\n";    //用陣列儲存字串
  fp=fopen("helloworld.txt", "w");  //以 w 模式開啟文字檔
  fputs(str1, fp);   //將字串寫入文字檔
  fputs(str2, fp);   //將字串寫入文字檔
  printf("文字檔寫入完成\n");
  fclose(fp);
  }

執行結果如下 : 




可見執行寫入後, helloworld.txt 內容為兩列的 Hello 與 Wordl. 如果將上面程式中的兩個 fputs() 註解掉重新執行程式, 則 helloworld.txt 檔案內容將變成空白 (開檔後沒有寫入). 

接下來可以用 fgets() 函式來讀取上面範例程式產生的 helloworld.txt 內容, 用 fgets() 讀檔首先必須宣告一個字元陣列來儲存讀到的內容, 其長度與 fgets() 的第二參數相同 :

char str[50];    //長度與 fgets() 的第二參數相同

然後使用 while 迴圈從緩衝區中固定讀取若干字元存到字元指標所指之位址, 直到 fgets() 傳回 NULL 表示已讀到檔尾結束迴圈, 語法如下 :

while (fgets(str, 50, fp) != NULL) {  //每次讀取 50-1=49 個字元 (扣掉字串尾 \0)
    .......
    }

完整範例如下 : 

/* 讀取文字檔 test03.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[50];  //讀取字串的暫存區, 長度與 fgets() 第二參數相同即可
  int count=0;   //統計讀了幾列
  fp=fopen("helloworld.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 50, fp) != NULL) {
      printf("%s", str);  //印出讀取到的字串 (列)
      count++;
      }
    printf("讀取了 %d 列\n", count);
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 :




下列範例使用 fgets() 與 fputs() 來複製檔案 : 

/* 複製文字檔 test04.c */
#include <stdio.h>

int main() {
  FILE *sfp, *dfp;  //宣告來源與目的檔案指標
  char str[20];   //暫存 fgets() 從緩衝區讀取之字串
  dfp=fopen("helloworld2.txt", "w");  //用 w 模式開啟目的檔
  sfp=fopen("helloworld.txt", "r");   //用 r 模式開啟來源檔
  if (sfp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 20, sfp) != NULL) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      fputs(str, dfp);  //將讀取之資料寫入目的檔緩衝區
      }
    fclose(sfp);   //關閉目的檔
    fclose(dfp);   //關閉目的檔
    }
  else {printf("檔案開啟失敗\n");}
  }

此例以上面的 helloworld.txt 為來源檔, 用 r 模式開啟檔案後傳回檔案指標 sfp; 以尚未建立的 helloworld2.txt 為目的檔, 用 w 模式開啟檔案後傳回檔案指標 dfp, 然後用 while 迴圈讀取來源檔 sfp 並將讀取之資料寫入目的檔直到檔尾 EOF 出現為止, 執行結果如下 :




可見原本不存在的 helloworld2.txt 在執行完程式後被建立了, 且內容與來源檔案 helloworld.txt 完全相同, 亦即完成了檔案複製動作. 


3. 格式化讀寫文字檔 :   

上面的範例中寫入檔案的均為字串, 而 fputs() 是專門用來將字串寫入檔案中的函式 (因為其第一參數為 char 類型), 如果要將整數用 fputs() 寫入檔案, 必須先用 stdio 函式庫的 sprintf() 函式將整數轉成字串, 其呼叫方式如下 : 

sprintf(字串變數, "%d", 整數變數);         //將整數變數轉為字串變數

如果要將浮點數轉成字串, 則可用 stdlib 函式庫的 gcvt() 函式, 呼叫方法如下 : 

char *字串變數=gcvt(浮點數變數, 總位數, 暫存字串變數);    //將浮點數變數轉為字串變數

注意, gcvt() 標頭檔定義於 stdlib, 須用 include 匯入 :

#include <stdlib.h>

例如 :

/* 用 sprintf() 死 gcvt() 將數值轉成字串 test05.c */
#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fp;
  fp=fopen("out.txt", "w");
  int i_price=123;       //整數
  float f_price=123.45;  //浮點數
  char i_price_str[10];  //用來儲存轉成字串的整數
  char c[10];            //gcvt() 用來暫存字串的字元陣列
  sprintf(i_price_str, "%d", i_price);    //將整數轉成字串
  char *f_price_str=gcvt(f_price, 5, c);  //將浮點數轉成字串
  fputs(i_price_str, fp); //將整數字串存入檔案
  fputs("\n", fp);        //存入換行字元
  fputs(f_price_str, fp); //將浮點數字串存入檔案
  fclose(fp);
  }

執行結果如下 : 




讀進儲存在檔案中的數值字串時也必須透過 strtoi(), atoi(), atof() 等函式轉成數值. 

參考 : 


另一個較簡單的方法是使用 fprintf() 與 fscanf() 函式, 如同 printf() 與 scanf() 為對標準 IO (螢幕與鍵盤) 進行格式化輸出入, C 語言的 stdio 也有對於檔案 IO 的格式化輸出入函式 fprintf() 與 fscanf(), 不論是獨或寫, 可以透過格式化字元自動轉換類型 :


 格式化讀寫文字檔函式 欄位2
 int fprintf(fp, 格式化字串, 變數群)  依格式化字串輸出到檔案, 成功傳回輸出字元, 失敗傳回 EOF
 int fscanf(fp, 格式化字串, 變數群)  依格式化字串從檔案讀取, 成功傳回讀取字元, 失敗傳回 EOF


格式化字串用法與 printf() 及 scanf() 相同, 常用的是 %c (字元), %s (字串), %d (整數), %f (浮點數) 等. 完整範例如下 : 

/* 格式化讀寫文字檔 test06.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[20];
  char name[20]="USB 讀卡機";  
  int count=10;  
  float price=21.5;
  fp=fopen("product.txt", "w");
  fprintf(fp, "品名: %s\n", name);
  fprintf(fp, "數量: %d\n", count);
  fprintf(fp, "價格: %f\n", price);
  fclose(fp);
  fp=fopen("product.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fscanf(fp, "%s", str) != EOF) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      }
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 : 




可見不論是字串或數值的檔案讀寫, 用 fprintf() 與 fscanf() 的格式化字串就能搞定, 不需要用其他各種函式轉來轉去. 

沒有留言 :