1.為什么使用文件
我們前面學(xué)習(xí)結(jié)構(gòu)體時(shí),寫通訊錄的程序,當(dāng)通訊錄運(yùn)行起來(lái)的時(shí)候,可以給通訊錄中增加、刪除數(shù)據(jù),此時(shí)數(shù)據(jù)是存放在內(nèi)存中,當(dāng)程序退出的時(shí)候,通訊錄中的數(shù)據(jù)自然就不存在了,等下次運(yùn)行通訊錄程序的時(shí)候,數(shù)據(jù)又得重新錄入,如果使用這樣的通訊錄就很難受。
我們?cè)谙爰热皇峭ㄓ嶄浘蛻?yīng)該把信息記錄下來(lái),只有我們自己選擇刪除數(shù)據(jù)的時(shí)候,數(shù)據(jù)才不復(fù)存在。這就涉及到了數(shù)據(jù)持久化的問(wèn)題,我們一般數(shù)據(jù)持久化的方法有,把數(shù)據(jù)存放在磁盤文件、存放到數(shù)據(jù)庫(kù)等方式。
使用文件我們可以將數(shù)據(jù)直接存放在電腦的硬盤上,做到了數(shù)據(jù)的持久化。
2. 什么是文件
磁盤上的文件是文件
但是在程序設(shè)計(jì)中,我們一般談的文件有兩種:程序文件、數(shù)據(jù)文件(從文件功能的角度來(lái)分類的)。
2.1 程序文件
包括源程序文件(后綴為.c),目標(biāo)文件(windows環(huán)境后綴為.obj),可執(zhí)行程序(windows環(huán)境后綴為.exe)。
2.2 數(shù)據(jù)文件
文件的內(nèi)容不一定是程序,而是程序運(yùn)行時(shí)讀寫的數(shù)據(jù),比如程序運(yùn)行需要從中讀取數(shù)據(jù)的文件,或者輸出內(nèi)容的文件。
在之前的文章所處理數(shù)據(jù)的輸入輸出都是以終端為對(duì)象的,即從終端的鍵盤輸入數(shù)據(jù),運(yùn)行結(jié)果顯示到顯示器上。其實(shí)有時(shí)候我們會(huì)把信息輸出到磁盤上,當(dāng)需要的時(shí)候再?gòu)拇疟P上把數(shù)據(jù)讀取到內(nèi)存中使用,這里處理的就是磁盤上文件。
2.3 文件名
一個(gè)文件要有一個(gè)唯一的文件標(biāo)識(shí),以便用戶識(shí)別和引用。
文件名包含3部分:文件路徑+文件名主干+文件后綴
例如: c:\code\test.txt
為了方便起見(jiàn),文件標(biāo)識(shí)常被稱為文件名。
3. 文件的打開(kāi)和關(guān)閉
3.1 文件指針
緩沖文件系統(tǒng)中,關(guān)鍵的概念是“文件類型指針”,簡(jiǎn)稱“文件指針”。
每個(gè)被使用的文件都在內(nèi)存中開(kāi)辟了一個(gè)相應(yīng)的文件信息區(qū),用來(lái)存放文件的相關(guān)信息(如文件的名字,文件狀態(tài)及文件當(dāng)前的位置等)。這些信息是保存在一個(gè)結(jié)構(gòu)體變量中的。該結(jié)構(gòu)體類型是由系統(tǒng)聲明的,取名FILE.
例如,VS2019編譯環(huán)境提供的 stdio.h 頭文件中有以下的文件類型申明:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的C編譯器的FILE類型包含的內(nèi)容不完全相同,但是大同小異。
每當(dāng)打開(kāi)一個(gè)文件的時(shí)候,系統(tǒng)會(huì)根據(jù)文件的情況自動(dòng)創(chuàng)建一個(gè)FILE結(jié)構(gòu)的變量,并填充其中的信息,使用者不必關(guān)心細(xì)節(jié)。
一般都是通過(guò)一個(gè)FILE的指針來(lái)維護(hù)這個(gè)FILE結(jié)構(gòu)的變量,這樣使用起來(lái)更加方便。
下面我們可以創(chuàng)建一個(gè)FILE*的指針變量:
FILE* pf;//文件指針變量
定義pf是一個(gè)指向FILE類型數(shù)據(jù)的指針變量。可以使pf指向某個(gè)文件的文件信息區(qū)(是一個(gè)結(jié)構(gòu)體變量)。通過(guò)該文件信息區(qū)中的信息就能夠訪問(wèn)該文件。也就是說(shuō),通過(guò)文件指針變量能夠找到與它關(guān)聯(lián)的文件。
3.2 文件的打開(kāi)和關(guān)閉
文件在讀寫之前應(yīng)該先打開(kāi)文件,在使用結(jié)束之后應(yīng)該關(guān)閉文件。
在編寫程序的時(shí)候,在打開(kāi)文件的同時(shí),都會(huì)返回一個(gè)FILE*的指針變量指向該文件,也相當(dāng)于建立了指針和文件的關(guān)系。
ANSIC 規(guī)定使用fopen函數(shù)來(lái)打開(kāi)文件,fclose來(lái)關(guān)閉文件。
//打開(kāi)文件
FILE * fopen ( const char * filename, const char * mode );
//關(guān)閉文件
int fclose ( FILE * stream );
文件使用方式 | 含義 | 如果指定文件不存在 |
---|---|---|
“r”(只讀) | 為了輸入數(shù)據(jù),打開(kāi)一個(gè)已經(jīng)存在的文本文件 | 出錯(cuò) |
“w”(只寫) | 為了輸出數(shù)據(jù),打開(kāi)一個(gè)文本文件 | 建立一個(gè)新的文件 |
“a”(追加) | 向文本文件尾添加數(shù)據(jù) | 建立一個(gè)新的文件 |
“rb”(只讀) | 為了輸入數(shù)據(jù),打開(kāi)一個(gè)二進(jìn)制文件 | 出錯(cuò) |
“wb”(只寫) | 為了輸出數(shù)據(jù),打開(kāi)一個(gè)二進(jìn)制文件 | 建立一個(gè)新的文件 |
“ab”(追加) | 向一個(gè)二進(jìn)制文件尾添加數(shù)據(jù) | 建立一個(gè)新的文件 |
“r+”(讀寫) | 為了讀和寫,打開(kāi)一個(gè)文本文件 | 出錯(cuò) |
“w+”(讀寫) | 為了讀和寫,建議一個(gè)新的文件 | 建立一個(gè)新的文件 |
“a+”(讀寫) | 打開(kāi)一個(gè)文件,在文件尾進(jìn)行讀寫 | 建立一個(gè)新的文件 |
“rb+”(讀寫) | 為了讀和寫打開(kāi)一個(gè)二進(jìn)制文件 | 出錯(cuò) |
“wb+”(讀寫) | 為了讀和寫,新建一個(gè)新的二進(jìn)制文件 | 建立一個(gè)新的文件 |
“ab+”(讀寫) | 打開(kāi)一個(gè)二進(jìn)制文件,在文件尾進(jìn)行讀和寫 | 建立一個(gè)新的文件 |
示例:
//相對(duì)路徑
FILE* pf = fopen("..\\Debug\\data.txt", "w");
//絕對(duì)路徑
//C:\Users\Administrator\Desktop\data.txt
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\data.txt", "w");
#include <stdio.h>
int main()
{
FILE* pFile;
//打開(kāi)文件
pFile = fopen("myfile.txt", "w");
//文件操作
if (pFile != NULL)
{
fputs("fopen example", pFile);
//關(guān)閉文件
fclose(pFile);
}
return 0;
}
4. 文件的順序讀寫
4.1 順序讀寫函數(shù)介紹
功能 | 函數(shù)名 | 適用于 |
---|---|---|
字符輸入函數(shù) | fgetc | 所有輸入流 |
字符輸出函數(shù) | fputc | 所有輸出流 |
文本行輸入函數(shù) | fgets | 所有輸入流 |
文本行輸出函數(shù) | fputs | 所有輸出流 |
格式化輸入函數(shù) | fscanf | 所有輸入流 |
格式化輸出函數(shù) | fprintf | 所有輸出流 |
二進(jìn)制輸入 | fread | 文件 |
二進(jìn)制輸出 | fwrite | 文件 |
舉例:
fgetc
#include <stdio.h>
int main()
{
//讀文件
int ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
ch = fgetc(stdin);
printf("%c\n", ch);
return 0;
}
fputc
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//寫文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a'+i, stdout);
}
//關(guān)閉文件
fclose(pf);
pf = NULL;
return 0;
}
fputs
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//寫文件 - 寫一行
fputs("hello world\n", pf);
fputs("hello vs2022\n", pf);
//關(guān)閉文件
fclose(pf);
pf = NULL;
return 0;
}
fgets
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//讀文件 - 讀一行
char arr[30] = { 0 };
fgets(arr, 30, pf);
printf("%s", arr);
fgets(arr, 30, pf);
printf("%s", arr);
//關(guān)閉文件
fclose(pf);
pf = NULL;
return 0;
}
下面我們對(duì)比
printf/fprintf/sprintf
scanf/fscanf/sscanf
分別有什么不同的作用
printf
int printf ( const char * format, ... );
將按格式指向的 C 字符串寫入標(biāo)準(zhǔn)輸出 (stdout)。如果 format 包含格式說(shuō)明符(以 % 開(kāi)頭的子序列),則格式后面的其他參數(shù)將被格式化并插入到生成的字符串中,替換其各自的說(shuō)明符。
返回值
成功后,將返回寫入的字符總數(shù)。
如果發(fā)生寫入錯(cuò)誤,則設(shè)置錯(cuò)誤指示器(ferror)并返回負(fù)數(shù)。
如果在寫入寬字符時(shí)發(fā)生多字節(jié)字符編碼錯(cuò)誤,errno 將設(shè)置為 EILSEQ 并返回負(fù)數(shù)
fprintf
int fprintf ( FILE * stream, const char * format, ... );
將按格式指向流的 C 字符串寫入流。如果 format 包含格式說(shuō)明符(以 % 開(kāi)頭的子序列),則格式后面的其他參數(shù)將被格式化并插入到生成的字符串中,替換其各自的說(shuō)明符。
在 format 參數(shù)之后,函數(shù)至少需要與格式指定的一樣多的其他參數(shù)。
返回值
成功后,將返回寫入的字符總數(shù)。
如果發(fā)生寫入錯(cuò)誤,則設(shè)置錯(cuò)誤指示器(ferror)并返回負(fù)數(shù)。
如果在寫入寬字符時(shí)發(fā)生多字節(jié)字符編碼錯(cuò)誤,errno 將設(shè)置為 EILSEQ 并返回負(fù)數(shù)。
示例:
struct S
{
int a;
float s;
};
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//寫文件
struct S s = { 100, 3.14f };
fprintf(pf, "%d %f", s.a, s.s);
//關(guān)閉文件
fclose(pf);
pf = NULL;
return 0;
}
scanf
int scanf ( const char * format, ... );
從 stdin 讀取數(shù)據(jù),并根據(jù)參數(shù)格式將它們存儲(chǔ)到附加參數(shù)所指向的位置。
其他參數(shù)應(yīng)指向格式字符串中相應(yīng)格式說(shuō)明符指定的類型的已分配對(duì)象。
返回值
成功后,該函數(shù)返回成功填充的參數(shù)列表的項(xiàng)數(shù)。此計(jì)數(shù)可以與預(yù)期的項(xiàng)目數(shù)匹配,也可以由于匹配失敗、讀取錯(cuò)誤或文件末尾的到達(dá)而減少(甚至為零)。
如果發(fā)生讀取錯(cuò)誤或在讀取時(shí)到達(dá)文件末尾,則會(huì)設(shè)置正確的指示器(feof 或 ferror)。并且,如果在成功讀取任何數(shù)據(jù)之前發(fā)生任一情況,則返回 EOF。
如果在解釋寬字符時(shí)發(fā)生編碼錯(cuò)誤,該函數(shù)會(huì)將 errno 設(shè)置為 EILSEQ。
fscanf
int fscanf ( FILE * stream, const char * format, ... );
從流中讀取數(shù)據(jù),并根據(jù)參數(shù)格式將其存儲(chǔ)到附加參數(shù)指向的位置。
其他參數(shù)應(yīng)指向格式字符串中相應(yīng)格式說(shuō)明符指定的類型的已分配對(duì)象。
返回值
成功后,該函數(shù)返回成功填充的參數(shù)列表的項(xiàng)數(shù)。此計(jì)數(shù)可以與預(yù)期的項(xiàng)目數(shù)匹配,也可以由于匹配失敗、讀取錯(cuò)誤或文件末尾的到達(dá)而減少(甚至為零)。
如果發(fā)生讀取錯(cuò)誤或在讀取時(shí)到達(dá)文件末尾,則會(huì)設(shè)置正確的指示器(feof 或 ferror)。并且,如果在成功讀取任何數(shù)據(jù)之前發(fā)生任一情況,則返回 EOF。
如果在解釋寬字符時(shí)發(fā)生編碼錯(cuò)誤,該函數(shù)會(huì)將 errno 設(shè)置為 EILSEQ。
示例:
//文件是由上面的fprintf示例生成
struct S
{
int a;
float s;
};
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//寫文件
struct S s = {0};
fscanf(pf, "%d %f", &(s.a), &(s.s));
printf("%d %f", s.a, s.s);
//關(guān)閉文件
fclose(pf);
pf = NULL;
return 0;
}
sprintf
int sprintf ( char * str, const char * format, ... );
使用與在 printf 上使用格式時(shí)打印的文本相同的文本組成字符串,但內(nèi)容不是打印,而是作為 C 字符串存儲(chǔ)在 str 指向的緩沖區(qū)中。
緩沖區(qū)的大小應(yīng)足夠大,以包含整個(gè)生成的字符串。
終止空字符會(huì)自動(dòng)追加到內(nèi)容之后。
在 format 參數(shù)之后,該函數(shù)至少需要與格式所需的其他參數(shù)一樣多。
返回值
成功后,將返回寫入的字符總數(shù)。此計(jì)數(shù)不包括自動(dòng)追加在字符串末尾的其他 null 字符。
失敗時(shí),返回負(fù)數(shù)。
sscanf
int sscanf ( const char * s, const char * format, ...);
從 s 讀取數(shù)據(jù)并根據(jù)參數(shù)格式將它們存儲(chǔ)到附加參數(shù)給出的位置,就像使用 scanf 一樣,但從 s 而不是標(biāo)準(zhǔn)輸入 (stdin) 讀取。
其他參數(shù)應(yīng)指向格式字符串中相應(yīng)格式說(shuō)明符指定的類型的已分配對(duì)象。
返回值
成功后,該函數(shù)返回參數(shù)列表中成功填充的項(xiàng)數(shù)。此計(jì)數(shù)可以與預(yù)期的項(xiàng)目數(shù)匹配,或者在匹配失敗的情況下更少(甚至為零)。
如果在成功解釋任何數(shù)據(jù)之前輸入失敗,則返回 EOF。
示例:
struct S
{
int a;
float s;
char str[10];
};
int main()
{
char arr[30] = { 0 };
struct S s = { 100, 3.14f, "hehe" };
struct S tmp = {0};
sprintf(arr, "%d %f %s", s.a, s.s, s.str);
sscanf(arr, "%d %f %s", &(tmp.a), &(tmp.s), tmp.str);
printf("%d %f %s\n", tmp.a, tmp.s, tmp.str);
return 0;
}
fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
將計(jì)數(shù)元素?cái)?shù)組(每個(gè)元素的大小為字節(jié))從 ptr 指向的內(nèi)存塊寫入流中的當(dāng)前位置。
流的位置指示器按寫入的總字節(jié)數(shù)前進(jìn)。
在內(nèi)部,該函數(shù)解釋所指向的塊,就好像它是一個(gè)類型的元素?cái)?shù)組,并按順序?qū)懭胨鼈儯秃孟駷槊總€(gè)字節(jié)調(diào)用一樣。
返回值
返回成功寫入的元素總數(shù)。
如果此數(shù)字與 count 參數(shù)不同,則寫入錯(cuò)誤阻止函數(shù)完成。在這種情況下,將為流設(shè)置錯(cuò)誤指示器(ferror)。
如果大小或計(jì)數(shù)為零,則該函數(shù)返回零,錯(cuò)誤指示器保持不變。
size_t 是無(wú)符號(hào)整數(shù)類型。
示例:
struct S
{
int a;
float s;
char str[10];
};
int main()
{
struct S s = { 99, 6.18f, "bit" };
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//寫文件
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
因?yàn)槭且远M(jìn)制編碼形式存入,所以使用文本編輯器打開(kāi)是亂碼
我們可以將其添加到vs
這里每個(gè)字節(jié)的十六進(jìn)制對(duì)應(yīng)的ASCII碼值即為對(duì)應(yīng)字符。
fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
從流中讀取計(jì)數(shù)元素?cái)?shù)組,每個(gè)元素的大小為字節(jié),并將它們存儲(chǔ)在 ptr 指定的內(nèi)存塊中。
流的位置指示器按讀取的總字節(jié)數(shù)前進(jìn)。
如果成功,則讀取的總字節(jié)數(shù)為(大小*計(jì)數(shù))。
返回值
返回成功讀取的元素總數(shù)。
如果此數(shù)字與 count 參數(shù)不同,則表示讀取時(shí)發(fā)生讀取錯(cuò)誤或到達(dá)文件末尾。在這兩種情況下,都會(huì)設(shè)置正確的指標(biāo),可以分別用 ferror 和 feof 進(jìn)行檢查。
如果大小或計(jì)數(shù)為零,則該函數(shù)返回零,并且流狀態(tài)和 ptr 指向的內(nèi)容保持不變。
size_t 是無(wú)符號(hào)整數(shù)類型。
示例:
struct S
{
int a;
float s;
char str[10];
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//讀文件
fread(&s, sizeof(struct S), 1, pf);
printf("%d %f %s\n", s.a, s.s, s.str);
fclose(pf);
pf = NULL;
return 0;
}
5. 文件的隨機(jī)讀寫
5.1 fseek
根據(jù)文件指針的位置和偏移量來(lái)定位文件指針。
int fseek ( FILE * stream, long int offset, int origin );
將與流關(guān)聯(lián)的位置指示器設(shè)置為新位置。
對(duì)于以二進(jìn)制模式打開(kāi)的流,新位置是通過(guò)向源指定的參考位置添加偏移量來(lái)定義的。
對(duì)于以文本模式打開(kāi)的流,偏移量應(yīng)為零或先前調(diào)用 ftel 返回的值,并且源必須SEEK_SET。
如果使用這些參數(shù)的其他值調(diào)用函數(shù),則支持取決于特定的系統(tǒng)和庫(kù)實(shí)現(xiàn)(不可移植)。
成功調(diào)用此函數(shù)后,流的文件結(jié)束內(nèi)部指示器將被清除,并且之前對(duì)此流的 ungetc 調(diào)用的所有效果都將被刪除。
在開(kāi)放進(jìn)行更新(讀+寫)的流上,對(duì) fseek 的調(diào)用允許在讀取和寫入之間切換。
示例:
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile);
fseek(pFile, 9, SEEK_SET);
fputs(" sam", pFile);
fclose(pFile);
return 0;
}
int main()
{
FILE* pf = fopen("example.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//讀文件
//定位文件指針到pf
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
5.2 ftell
返回文件指針相對(duì)于起始位置的偏移量
long int ftell ( FILE * stream );
返回流的位置指示器的當(dāng)前值。
對(duì)于二進(jìn)制流,這是從文件開(kāi)頭開(kāi)始的字節(jié)數(shù)。
對(duì)于文本流,數(shù)值可能沒(méi)有意義,但仍可用于稍后使用 fseek 將位置恢復(fù)到相同位置(如果有使用 ungetc 放回的字符仍在等待讀取,則行為未定義)。
返回值
成功后,返回倉(cāng)位指標(biāo)的當(dāng)前值。
失敗時(shí),返回 -1L,并將 errno 設(shè)置為特定于系統(tǒng)的正值。
示例:
int main()
{
FILE* pf = fopen("example.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//讀文件
//定位文件指針到pf
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
int pos = ftell(pf);
printf("%d\n", pos);
fclose(pf);
pf = NULL;
return 0;
}
5.3 rewind
讓文件指針的位置回到文件的起始位置
void rewind ( FILE * stream );
將與流關(guān)聯(lián)的位置指示器設(shè)置為文件的開(kāi)頭。
成功調(diào)用此函數(shù)后,將清除與流關(guān)聯(lián)的文件結(jié)束和錯(cuò)誤內(nèi)部指示器,并且將丟棄以前對(duì)此流上 ungetc 的調(diào)用的所有效果。
在打開(kāi)進(jìn)行更新(讀+寫)的流上,對(duì)倒帶的調(diào)用允許在讀取和寫入之間切換。
示例:
int main()
{
FILE* pf = fopen("example.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//讀文件
//定位文件指針到pf
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
6. 文本文件和二進(jìn)制文件
根據(jù)數(shù)據(jù)的組織形式,數(shù)據(jù)文件被稱為文本文件或者二進(jìn)制文件。
數(shù)據(jù)在內(nèi)存中以二進(jìn)制的形式存儲(chǔ),如果不加轉(zhuǎn)換的輸出到外存,就是二進(jìn)制文件。
如果要求在外存上以ASCII碼的形式存儲(chǔ),則需要在存儲(chǔ)前轉(zhuǎn)換。以ASCII字符的形式存儲(chǔ)的文件就是文本文件。
一個(gè)數(shù)據(jù)在內(nèi)存中是怎么存儲(chǔ)的呢?
字符一律以ASCII形式存儲(chǔ),數(shù)值型數(shù)據(jù)既可以用ASCII形式存儲(chǔ),也可以使用二進(jìn)制形式存儲(chǔ)。
如有整數(shù)10000,如果以ASCII碼的形式輸出到磁盤,則磁盤中占用5個(gè)字節(jié)(每個(gè)字符一個(gè)字節(jié)),而二進(jìn)制形式輸出,則在磁盤上只占4個(gè)字節(jié)(VS2022測(cè)試)。
7. 文件讀取結(jié)束的判定
7.1 被錯(cuò)誤使用的feof
牢記:在文件讀取過(guò)程中,不能用feof函數(shù)的返回值直接來(lái)判斷文件的是否結(jié)束。
feof 的作用是:當(dāng)文件讀取結(jié)束的時(shí)候,判斷是讀取結(jié)束的原因是否是:遇到文件尾結(jié)束。
- 文本文件讀取是否結(jié)束,判斷返回值是否為 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判斷是否為 EOF .
fgets 判斷返回值是否為 NULL . - 二進(jìn)制文件的讀取結(jié)束判斷,判斷返回值是否小于實(shí)際要讀的個(gè)數(shù)。
例如:
fread判斷返回值是否小于實(shí)際要讀的個(gè)數(shù)。
正確使用示例:
文本文件:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int c; // 注意:int,非char,要求處理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 當(dāng)讀取失敗的時(shí)候或者遇到文件結(jié)束的時(shí)候,都會(huì)返回EOF
while ((c = fgetc(fp)) != EOF) // 標(biāo)準(zhǔn)C I/O讀取文件循環(huán)
{
putchar(c);
}
//判斷是什么原因結(jié)束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
return 0;
}
二進(jìn)制文件:
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必須用二進(jìn)制模式
fwrite(a, sizeof * a, SIZE, fp); // 寫 double 的數(shù)組
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 讀 double 的數(shù)組
if (ret_code == SIZE) {
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
8. 文件緩沖區(qū)
ANSIC 標(biāo)準(zhǔn)采用“緩沖文件系統(tǒng)”處理的數(shù)據(jù)文件的,所謂緩沖文件系統(tǒng)是指系統(tǒng)自動(dòng)地在內(nèi)存中為程序中每一個(gè)正在使用的文件開(kāi)辟一塊“文件緩沖區(qū)”。從內(nèi)存向磁盤輸出數(shù)據(jù)會(huì)先送到內(nèi)存中的緩沖區(qū),裝滿緩沖區(qū)后才一起送到磁盤上。如果從磁盤向計(jì)算機(jī)讀入數(shù)據(jù),則從磁盤文件中讀取數(shù)據(jù)輸入到內(nèi)存緩沖區(qū)(充滿緩沖區(qū)),然后再?gòu)木彌_區(qū)逐個(gè)地將數(shù)據(jù)送到程序數(shù)據(jù)區(qū)(程序變量等)。緩沖區(qū)的大小根據(jù)C編譯系統(tǒng)決定的。
#include <windows.h>
//VS2022 WIN11環(huán)境測(cè)試
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先將代碼放在輸出緩沖區(qū)
printf("睡眠10秒-已經(jīng)寫數(shù)據(jù)了,打開(kāi)test.txt文件,發(fā)現(xiàn)文件沒(méi)有內(nèi)容\n");
Sleep(10000);
printf("刷新緩沖區(qū)\n");
fflush(pf);//刷新緩沖區(qū)時(shí),才將輸出緩沖區(qū)的數(shù)據(jù)寫到文件(磁盤)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此時(shí),再次打開(kāi)test.txt文件,文件有內(nèi)容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在關(guān)閉文件的時(shí)候,也會(huì)刷新緩沖區(qū)
pf = NULL;
return 0;
}
這里可以得出一個(gè)結(jié)論:
因?yàn)橛芯彌_區(qū)的存在,C語(yǔ)言在操作文件的時(shí)候,需要做刷新緩沖區(qū)或者在文件操作結(jié)束的時(shí)候關(guān)閉文件。
如果不做,可能導(dǎo)致讀寫文件的問(wèn)題。
9.通訊錄改造
我們之前的文章寫道動(dòng)態(tài)版的通訊錄,但是在程序退出時(shí),隨著程序的終止,通訊錄數(shù)據(jù)也隨著清除了,那么我們根據(jù)這一章節(jié)的文件操作知識(shí),是否有辦法將我們的文件保存下來(lái)呢,在重新進(jìn)入文件時(shí),又能否讀取保存的通訊錄呢,下面我們就開(kāi)始改造通訊錄吧!
9.1 保存通訊錄到本地
首先我們?cè)黾雍瘮?shù)SaveCon到頭文件聲明,并在contact.c文件中定義
void SaveCon(Con* cont)
{
FILE* pf = fopen("contact.dat", "wb");
if (pf == NULL)
{
perror("fopen");
}
int i = 0;
for (i = 0; i < cont->sz; i++)
{
fwrite(cont->data + i, sizeof(peo), 1, pf);
}
fclose(pf);
pf = NULL;
}
首先這里是打開(kāi)condact.dat文件進(jìn)行讀寫,判斷是否成功,成功后逐字符進(jìn)行寫入,最后關(guān)閉文件,指針置空。
主函數(shù)switch語(yǔ)句中EXIT選項(xiàng)調(diào)用
示例:
我們添加4個(gè)聯(lián)系人
9.2 讀取本地通訊錄
我們只需在初始化函數(shù)InitCon最后調(diào)用一個(gè)讀取函數(shù)LoadCon即可,接下來(lái)我們編寫LoadCon函數(shù)
int CheckCapacity(Con* cont);
void LoadCon(Con* cont)
{
FILE* pf = fopen("contact.dat", "rb");
if (pf == NULL)
{
perror("fopen");
return;
}
peo tmp = { 0 };
while (fread(&tmp, sizeof(peo), 1, pf))
{
if (0 == CheckCapacity(cont))
return;
cont->data[cont->sz] = tmp;
cont->sz++;
}
fclose(pf);
pf = NULL;
}
首先我們打開(kāi)保存的文件,建立臨時(shí)的peo,逐個(gè)讀取文件內(nèi)容,逐個(gè)復(fù)制到cont->data內(nèi),最后關(guān)閉文件,指針置空,需要注意的是,這里我們使用了CheckCapacity函數(shù),所以要在前面加上聲明,否則他會(huì)找不到此函數(shù)。
9.3 通訊錄最終版全部代碼
contact.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#define NAME_MAX 10
#define SEX_MAX 6
#define ADDR_MAX 20
#define TELE_MAX 12
#define N 3
#define INC 2
typedef struct people
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tele[TELE_MAX];
char addr[ADDR_MAX];
}peo;
typedef struct Contact
{
peo* data;
int capacity;
int sz;
}Con;
enum
{
EXIT,
ADD,
DEL,
MOD,
SEARCH,
SHOW,
DISTORY,
QSORT
};
void InitCon(Con* cont);
void add(Con* cont);
void show(const Con* cont);
int FindName(const Con* cont, const char* name);
void del(Con* cont);
void mod(Con* cont);
void sel(const Con* cont);
void Distory(Con* cont);
int cmp_stu_by_name(const void* p1, const void* p2);
void DistoryCon(Con* cont);
void SaveCon(Con* cont);
contact.c
#include "contact.h"
int CheckCapacity(Con* cont);
void LoadCon(Con* cont)
{
FILE* pf = fopen("contact.dat", "rb");
if (pf == NULL)
{
perror("fopen");
return;
}
peo tmp = { 0 };
while (fread(&tmp, sizeof(peo), 1, pf))
{
if (0 == CheckCapacity(cont))
return;
cont->data[cont->sz] = tmp;
cont->sz++;
}
fclose(pf);
pf = NULL;
}
void InitCon(Con* cont)
{
assert(cont);
cont->data = (peo*)malloc(N * sizeof(peo));
if (cont->data == NULL)
{
perror("malloc");
return;
}
cont->sz = 0;
cont->capacity = N;
LoadCon(cont);
}
int CheckCapacity(Con* cont)
{
if (cont->sz == cont->capacity)
{
peo* tmp = (peo*)realloc(cont->data, (cont->capacity + INC) * sizeof(peo));
if (tmp == NULL)
{
perror("relloc");
return 0;
}
else
{
cont->data = tmp;
cont->capacity += INC;
printf("增容成功");
return 1;
}
}
return 1;
}
void add(Con* cont)
{
assert(cont);
if (0 == CheckCapacity(cont))
return;
printf("請(qǐng)輸入要添加聯(lián)系人的信息:\n");
printf("請(qǐng)輸入姓名:");
scanf("%s", cont->data[cont->sz].name);
printf("請(qǐng)輸入性別:");
scanf("%s", cont->data[cont->sz].sex);
printf("請(qǐng)輸入年齡:");
scanf("%d", &cont->data[cont->sz].age);
printf("請(qǐng)輸入電話:");
scanf("%s", cont->data[cont->sz].tele);
printf("請(qǐng)輸入地址:");
scanf("%s", cont->data[cont->sz].addr);
cont->sz++;
printf("添加成功\n");
}
void show(const Con* cont)
{
assert(cont);
int i = 0;
printf(" 通訊錄\n");
printf(" +---------------------------------------------------------------+\n");
printf(" |姓名 性別 年齡 電話 地址 |\n");
for (i = 0; i < cont->sz; i++)
{
printf(" |%-10s %-5s %-5d %-11s %-15s |\n", cont->data[i].name, cont->data[i].sex, cont->data[i].age, cont->data[i].tele, cont->data[i].addr);
}
printf(" +---------------------------------------------------------------+\n");
}
int FindName(const Con* cont, const char* name)//查找人函數(shù)
{
assert(cont && name);//防止傳入空指針
int i = 0;
for (i = 0; i < cont->sz; i++)
{
if (!strcmp(cont->data[i].name, name))//查找通訊錄是否有這個(gè)名字
{
return i;//有就返回下標(biāo)
}
}
return -1;//沒(méi)有返回一個(gè)負(fù)數(shù).
}
void del(Con* cont)//刪除聯(lián)系人函數(shù)
{
assert(cont);//防止傳入空指針
int i = 0;
char name[NAME_MAX];
printf("請(qǐng)輸入需要?jiǎng)h除的聯(lián)系人的姓名:\n");
scanf("%s", name);
i = FindName(cont, name);
if (i == -1)
{
printf("通訊錄中沒(méi)有該聯(lián)系人,刪除失敗\n");
return;
}
for (; i < cont->sz - 1; i++)//注意這里sz要-1,因?yàn)橄旅嬗玫搅薸+1下標(biāo)
{
cont->data[i] = cont->data[i + 1];
}
cont->sz--;
printf("刪除成功,姓名為%s的聯(lián)系人已刪除\n", name);
}
void mod(Con* cont)//修改聯(lián)系人函數(shù)
{
assert(cont);//防止傳入空指針
int ret = 0;
char name[NAME_MAX];
printf("請(qǐng)輸入要修改的聯(lián)系人的姓名:\n");
scanf("%s", name);
ret = FindName(cont, name);
if (ret == -1)
{
printf("通訊錄中沒(méi)有該聯(lián)系人,修改失敗\n");
return;
}
printf("請(qǐng)輸入修改后聯(lián)系人的信息:\n");
printf("請(qǐng)輸入姓名:");
scanf("%s", cont->data[ret].name);
printf("請(qǐng)輸入性別:");
scanf("%s", cont->data[ret].sex);
printf("請(qǐng)輸入年齡:");
scanf("%d", &cont->data[ret].age);
printf("請(qǐng)輸入電話:");
scanf("%s", cont->data[ret].tele);
printf("請(qǐng)輸入地址:");
scanf("%s", cont->data[ret].addr);
printf("修改成功.\n");
}
void sel(const Con* cont)//查詢聯(lián)系人函數(shù)
{
char name[NAME_MAX];
printf("請(qǐng)輸入要查詢的聯(lián)系人的名字:\n");
scanf("%s", name);
int ret = FindName(cont, name);
if (ret == -1)
{
printf("通訊錄中沒(méi)有該聯(lián)系人,查詢失敗.\n");
return;
}
printf("查詢成功,該聯(lián)系信息如下:\n");
printf(" +---------------------------------------------------------------+\n");
printf(" |姓名 性別 年齡 電話 地址 |\n");
printf(" |%-10s %-5s %-5d %-11s %-15s |\n", cont->data[ret].name, cont->data[ret].sex, cont->data[ret].age, cont->data[ret].tele, cont->data[ret].addr);
printf(" +---------------------------------------------------------------+\n");
}
void Distory(Con* cont)
{
assert(cont);
cont->sz = 0;
printf("全部聯(lián)系人刪除成功!\n");
}
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((peo*)p1)->name, ((peo*)p2)->name);
}
void DistoryCon(Con* cont)
{
free(cont->data);
cont->data = NULL;
cont->capacity = 0;
cont->sz = 0;
}
void SaveCon(Con* cont)
{
FILE* pf = fopen("contact.dat", "wb");
if (pf == NULL)
{
perror("fopen");
}
int i = 0;
for (i = 0; i < cont->sz; i++)
{
fwrite(cont->data + i, sizeof(peo), 1, pf);
}
fclose(pf);
pf = NULL;
}
main.c
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-568702.html
#include"contact.h"
void menu()
{
printf("\n 歡迎使用通訊錄:\n");
printf(" +-------------------------------------------------------------+\n");
printf(" | 1.添加聯(lián)系人 2.刪除聯(lián)系人 |\n");
printf(" | 3.修改聯(lián)系人 4.查詢聯(lián)系人 |\n");
printf(" | 5.展示通訊錄 6.刪除全部聯(lián)系人 |\n");
printf(" | 7.排序聯(lián)系人 0.退出通訊錄 |\n");
printf(" +-------------------------------------------------------------+\n");
printf("請(qǐng)選擇:");
}
int main()
{
int input = 0;
Con cont;
InitCon(&cont);
while (1) {
do
{
menu();
scanf("%d", &input);
switch (input)
{
case EXIT:
SaveCon(&cont);
DistoryCon(&cont);
printf("退出通訊錄");
break;
case ADD:
add(&cont);
break;
case DEL:
del(&cont);
break;
case MOD:
mod(&cont);
break;
case SEARCH:
sel(&cont);
break;
case SHOW:
show(&cont);
break;
case DISTORY:
Distory(&cont);
break;
case QSORT:
qsort(cont.data, cont.sz, sizeof(peo), cmp_stu_by_name);
printf("排序成功\n");
break;
default:
printf("輸入錯(cuò)誤");
break;
}
} while (input);
break;
}
return 0;
}
結(jié)語(yǔ)
有興趣的小伙伴可以關(guān)注作者,如果覺(jué)得內(nèi)容不錯(cuò),請(qǐng)給個(gè)一鍵三連吧,蟹蟹你喲?。?!
制作不易,如有不正之處敬請(qǐng)指出
感謝大家的來(lái)訪,UU們的觀看是我堅(jiān)持下去的動(dòng)力
在時(shí)間的催化劑下,讓我們彼此都成為更優(yōu)秀的人吧??!!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-568702.html
到了這里,關(guān)于C語(yǔ)言進(jìn)階之文件操作及改造通訊錄的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!