檔案以儲存形式來分有純文字檔 (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");
}
完整範例如下 :
#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() 寫入的內容會附加在檔尾, 例如 :
#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)
.......
}
完整範例如下 :
#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() 來複製檔案 :
#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 (浮點數) 等. 完整範例如下 :
#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() 的格式化字串就能搞定, 不需要用其他各種函式轉來轉去.