C語言-檔案I/O

簡介

儲存在變數的資料只是暫時的,想要在程式結束時儲存資料就必須靠檔案

這個章節要介紹的是如何操作檔案

fopen,fclose

如果要處理檔案要用fopen來開啟檔案,fopen函數的原型宣告如下:

1
FILE* fopen (const char*, const char*);

fopen的第一個參數是檔案名稱(路徑),第二個參數是打開檔案的方式

以下表格是檔案打開的方式

模式 說明
r 開啟檔案進行唯讀,若檔案不存在,則傳回NULL
w 開啟檔案進行唯寫,若檔案不存在,則建立新檔,若檔案存在則將之刪除,再建立新檔
a 開啟檔案進行附加,若檔案存在,則資料從檔案尾端寫入,若檔案不存在則建立新檔
rb 以二進位模式開啟檔案進行唯讀
wb 以二進位模式開啟檔案進行唯寫
ab 以二進位模式開啟檔案進行附加
r+ 開啟檔案進行讀寫,若檔案不存在,則建立新檔,若檔案存在,資料將從檔案開頭進行覆寫
w+ 開啟檔案進行讀寫,若檔案不存在,則建立新檔,若檔案存在則覆寫原有的資料
a+ 開啟檔案進行附加、讀取,若檔案不存在則建立新檔,若檔案存在,則資料從檔案尾端寫入
r+b 以二進位方式開啟檔案進行讀寫
w+b 以二進位方式開啟檔案進行讀寫
a+b 以二進位方式開啟檔案進行附加、讀取

fopen會使用緩衝區來減少I/O,以提高效率,所以在讀寫的過程中其實是對緩衝區做讀寫

使用檔案的好習慣就是不使用時就要使用fclose關閉,參數為檔案指標,以下為原型宣告

1
int fclose(FILE *fp);

如果檔案正常關閉則回傳0,不正常關閉則回傳非0的數字

fgetc,fputc,fgets,fputs

開啟檔案後可以用fgetc讀取檔案的一個字元,用fputc輸出一個字元

函數宣告原型如下

1
2
int fgetc(FILE* fp);
int fputc(int ch, FILE *fp);

fgetc傳檔案指標進去就可以取得該檔案的一個字元,直到檔案結尾EOF(End Of File)

可以像下面這樣判斷檔案結尾

1
2
3
4
5
6
7
8
char ch;
ch = fgetc(file);
while( ch != EOF ) {
...
...
...
ch = fgetc(file);
}

fputc傳一個字元跟檔案指標就能寫一個字元進去

而如果想一次處理一行字串就要使用fgets、fputs,函數宣告原型如下

1
2
char* fgets(char *str, int length, FILE *fp);
int fputs(char *str, FILE *fp);

fgets第一個參數是要儲存的字串位址,第二個是要讀幾個字,由於最後一個字要是’\0’

所以真正的長度為length-1,第三個參數是檔案指標

fputs第一個參數是要輸出的字串,第二個參數是要輸出到哪個檔案

以下程式是示範用fgetc,fputc複製檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
int main() {
FILE *file1 = fopen("source.txt", "r");
if(!file1) {
puts("來源檔案開啟失敗");
return 1;
}
FILE *file2 = fopen("target.txt", "w");
if(!file2) {
puts("目的檔案開啟失敗");
return 1;
}
char ch;
ch = fgetc(file1);
while(ch != EOF) {
fputc(ch, file2);
ch = fgetc(file1);
}
fclose(file1);
fclose(file2);
return 0;
}

以下程式是示範用fgets,fputs複製檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
int main() {
FILE *file1 = fopen("source.txt", "r");
if(!file1) {
puts("來源檔案開啟失敗");
return 1;
}
FILE *file2 = fopen("target.txt", "w");
if(!file2) {
puts("目的檔案開啟失敗");
return 1;
}
char str[50];
while(fgets(str, 50, file1) != NULL) {
fputs(str, file2);
}
fclose(file1);
fclose(file2);
return 0;
}

fscanf,fprintf

檔案操作也可以做格式化,以下是原型宣告

1
2
int fprintf(FILE *fp, char *formatstr, arg1, arg2, ...);
int fscanf(FILE *fp, char *formatstr, arg1, arg2, ...);

除了第一個參數要給檔案指標以外,其餘跟scanf,printf一樣

以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
int main() {
char ch;
FILE *file = fopen("test.txt", "w");
if(!file) {
puts("無法寫入檔案");
return 1;
}
fprintf(file, "%s\t%d\r\n", "AAA", 100);
fprintf(file, "%s\t%d\r\n", "BBB", 90);
fprintf(file, "%s\t%d\r\n", "CCC", 80);
fclose(file);
file = fopen("test.txt", "r");;
if(!file) {
puts("無法讀入檔案");
return 1;
}
char name[10];
int score;
puts("名字\t分數");
while(fscanf(file, "%s\t%d", name, &score) != EOF) {
printf("%s\t%d\n", name, score);
}
fclose(file);
return 0;
}

fread,fwrite

使用二進位模式讀寫檔案就需要用fread,fwrite

讀寫二進位檔案都是使用位元組(Byte)為單位的區塊(Block),以下為函數原型宣告

1
2
int fread(char *buffer, int size, int count, FILE *fp);
int fwrite(char *buffer, int size, int count, FILE *fp);

fread的意思是將count個元素從fp檔案中讀出到buffer,每個元素大小為size位元組

fwrite的意思是從buffer寫入count個元素到fp檔案中,每個元素大小為size位元組

以下為複製檔案的範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
int main() {
FILE *file1 = fopen("source.txt", "rb");
FILE *file2 = fopen("target.txt", "wb");
if(!file1) {
puts("檔案讀入失敗");
return 1;
}
if(!file2) {
puts("檔案輸出失敗");
return 1;
}
char ch;
while(!feof(file1)) {
fread(&ch, sizeof(char), 1, file1);
if(!feof(file1)) {
fwrite(&ch, sizeof(char), 1, file2);
}
}
fclose(file1);
fclose(file2);
return 0;
}

fseek

開啟檔案的時候,會有一個指標指向檔案開頭,每次讀取幾個字元,就會往後位移

C語言提供fseek來位移這個指標,方便我們要從任一地方讀取檔案

以下為fseek的函數原型宣告

1
int fseek(FILE *fp, long offset, int mode);

第一個參數是檔案,第二個參數為偏移量,第三個參數是從哪裡位移

以下是位移模式的說明

位移模式 說明
SEEK_SET 檔案開頭
SEEK_CUR 目前游標所在位置
SEEK_END 檔案結尾

這種檔案操作稱為非循序的檔案存取,一般會使用二進位模式搭配自定的資料結構

以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
struct Employee{
char name[30]; // 名字
int age; //年齡
char gender; // 性別,'M' or 'F'
double salary; // 薪水
};
void printPrompt();
int main() {
// 初始化
FILE *file = fopen("data.bin", "wb");
int count;
printf("要建立幾筆資料? ");
scanf("%d", &count);
struct Employee employee = {"",0,'\0',0.0};
int i;
for(i = 0; i < count; i++)
fwrite( &employee, sizeof(employee), 1, file);
fclose(file);
// 讀取
count = 0;
file = fopen("data.bin", "r+b");
if(!file) {
puts("無法讀取檔案");
return 1;
}
while(1) {
fread(&employee, sizeof(employee), 1, file);
if(!feof(file)) {
count++;
}
else {
break;
}
}
rewind(file); // 等同於 fseek(file, 0, SEEK_SET);
printf("輸入序號(1-%d)\n", count);
puts("輸入0離開");
int id;
while(1) {
printf("\n序號? ");
scanf("%d", &id);
if(id == 0) {
break;
}
printf("輸入名字 年齡 性別 薪水\n=> ");
scanf("%s %d %c %lf", employee.name, &(employee.age), &(employee.gender), &(employee.salary));
fseek(file, (id - 1) * sizeof(employee), SEEK_SET);
fwrite(&employee, sizeof(employee), 1, file);
printf("輸入序號(1-%d)\n", count);
puts("輸入0離開");
}
fclose(file);
return 0;
}

執行結果

要建立幾筆資料? 5
現在開始輸入資料
輸入序號(1-5)
輸入0離開

序號? 1
輸入名字 年齡 性別 薪水
=> Gundam 20 M 81000
輸入序號(1-5)
輸入0離開

序號? 5
輸入名字 年齡 性別 薪水
=> Box 20 M 22000
輸入序號(1-5)
輸入0離開

序號? 0
現在輸出所有資料
名字    年齡    性別    薪水
Gundam  20      M       81000.00
        0       0       0.00
        0       0       0.00
        0       0       0.00
Box     20      M       22000.00

freopen

freopen跟fopen不一樣的地方是fopen是打開檔案

freopen是重新導向串流(stream)直到程式結束為止,以下是函數原型宣告

1
FILE * freopen ( const char * filename, const char * mode, FILE * stream );

第一個參數是檔案名稱,第二個是檔案模式,第三個則是要重新導向的串流

串流分為stdin(標準輸入)、stdout(標準輸出)、stderr(標準錯誤)

以下是範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main() {
// 把檔案導向到標準輸入
freopen("source.txt","r",stdin);
// 把標準輸出重新導向到檔案
freopen("target.txt","w",stdout);
char str[50];
while(gets(str) != NULL) {
puts(str);
}
return 0;
}

參考

  1. C語言
  2. 未格式化檔案 I/O
  3. 格式化檔案 I/O
  4. 二進位檔案 I/O
  5. 資料流游標
  6. 隨機存取檔案