目录
一.为什么使用文件?
如果没有⽂件,我们写的程序的/*数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失*/了,等再次运⾏程序,是看不到上次程序的数据的,如果要将/*数据进⾏持久化的保存*/,我们可以使⽤⽂件。
代码举例:
int main()
{
int n = 0;
printf("%d\n", n);
scanf("%d", &n);//20
printf("%d\n", n);
return 0;
}
程序运行的时候,对n的值输入20,程序结束下次再次运行起来的时候n的值还是为0。发现上次输入的值没有保存。
因为创建的n在内存,输入的数据也是在内存,内存的数据一但程序退出,数据就还给操作系统了。所以数据放在内存中是非常不安全,不稳定的。除非程序一直不结束,你数据就一直在。
所以想把/*数据进⾏持久化的保存*/或还想得到被改了之后的值 - 就可以使用文件。
二.什么是文件?
磁盘(硬盘)上的⽂件就是⽂件。//内存和硬盘要区分开,程序运行在内存,硬盘放的数据。
但是在程序设计中,我们⼀般谈的⽂件有两种:/*程序⽂件*/、/*数据⽂件*/(从⽂件功能的⻆度来分类的)。
1.程序文件
程序⽂件包括源程序⽂件(后缀为.c), ⽬标⽂件(windows环境后缀为.obj), 可执⾏程序(windows环境后缀为.exe)。
解析:程序文件是自己写的代码.c文件(源代码程序),经过编译会生成目标文件,可执行程序。
2.数据文件
⽂件的内容不⼀定是程序,而是程序运⾏时读写的数据,/*比如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件*/。
解析:程序运行起来会读a文件数据,最终写到b文件里面去,a文件和b文件就是数据文件。
过去所学的代码数据的输⼊输出都是以/*终端*/为对象的,即从键盘输入数据到终端,运⾏结果显⽰到显⽰器上。
有时候我们会把信息输出到磁盘上(保存到文件里面去)-- 一但把数据存储在文件里面去,用来存储数据的文件就是数据文件。
3.文件名
⼀个⽂件要有⼀个唯⼀的/*⽂件标识*/,以便⽤户识别和引⽤。
⽂件名包含3部分:⽂件路径 + ⽂件名主⼲ + ⽂件后缀
例如: c:\code\test.txt
c:\code\ -- 文件路径
test.txt -- ⽂件名主⼲
.txt -- ⽂件后缀
为了⽅便起⻅,⽂件标识常被称为⽂件名。
三.二进制文件 和 文本文件?
根据数据的组织形式,数据⽂件被称为/*⽂本⽂件*/或者/*⼆进制⽂件*/。
二进制文件:数据在内存中/*以二进制的形式存储*/,如果不加转换的输出到外存的文件。
含义:就是数据在内存怎么存储,在外存也怎么存储,是没有变化的。
比如:用文本编译器打开看是看不懂的。
⽂本⽂件:在外存上以ASCII码的形式存储,则需要在存储前转换,/*以ASCII字符的形式存储的⽂件。*/
比如用记事本(或文本编译器)打开的文件能直接看懂里面写的信息的文件。
文本编译器:能够读懂文本文件信息,像abcdef这种,但像二进制信息读不懂,会出现大量的乱码。
总结:字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
(1)举例
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
为什么10000占用5个字节?//因为会把10000的5位数分别当成一个字符 - 用于文本文件。
(2)举例
代码举例 - 以二进制形式把数据写在文件里面
int main()
{
int a = 10000;
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写二进制文件
fwrite(&a, sizeof(a), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
总结:
1.内存存储数据的形式直接写入文件就是二进制文件,是为了持续已久的保存数据。 - 直接看,看不懂
2.把数据每一位当成字符,再以ASCLL码值转换成二进制形式写入文件就是文本文件。 - 直接能看懂
3."test.txt"就是数据文件,我们是用程序文件操作数据文件要么向文件读数据,要么向文件写数据,专门存放数据或提供数据。
四. 文件的打开和关闭
流程:
1.打开文件
2.读写文件
3.关闭文件
1.流和标准流
(1) 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同。
比如:在硬盘里面写和在网络上写方式肯定不一样
比如:从硬盘上读和网络上拿数据方式也不一样。
理论上程序员要把各种各样外部设备'读'和'写'方式都搞清楚才能写代码。
为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符和数据的河。
对于程序员来说你要读数据你去'流'里面去读,你要像外部设备写数据,写到'流'里面去就可以了。
含义:流是抽象出来的概念是中间层,/*流对接不同的外部设备*/,具体怎么把流里面的数据写到文件和网络等,只需要流管理就行,不需要程序员管理。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
总结:
1.C程序针对⽂件、画⾯、键盘等外部设备的数据/*输⼊输出操作都是通过流操作的*/。
2.⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。不用关心不同设备如何操作。
3.是用流对接外部不同设备,比如要向文件读/写数据,只要打开文件相关的流就可以了。
4.流是内存里面的一块区域,专门在程序底层设计出来的东西。
5.做为程序员只需如何使用流,打开流,关闭流,流的底层实现如何对接外部设备不需要我们关心。
(2)标准流
代码举例
int main()
{
pirntf("hehe");//发现直接用pintf打印了,重来没打开什么流,就直接打印了?
int n = 10;
scanf("%d", &n);//直接用scanf就读了,也没有打开流,所以打开流是什么概念?
return 0;
}
问题:为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?是打开了我们不知道。
那是因为任何一个C语言程序写好之后运行起来,默认打开了3个流:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。默认底层打开这3个流,我们才能写数据。
stdin、stdout、stderr/* 三个流的类型是: FILE* */ ,通常称为/*⽂件指针*/。
很重要:C语⾔中,就是通过 FILE * 的⽂件指针来维护流的各种操作的,但指针文件流。
2.文件指针
缓冲⽂件系统中,关键的概念是/*“⽂件类型指针”*/,简称/*“⽂件指针”。*/
每个/*被使⽤的⽂件*/都在内存中开辟了⼀个相应的/*⽂件信息区*/,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。/*该结构体类型是由系统声明的,取名 FILE。 */
例如,VS2013编译环境提供的 stdio.h 头⽂件中对FILE有以下的⽂件类型申明:
不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异。但是关于结构体的细节不用了解。
每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节,这个结构体就是文件缓冲区。
⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。
举例:
data.txt - 文件,假设你要对这个文件进行处理,进行读写操作,当你在读写之前要打开文件。
1.打开文件 - 打开文件这个操作就会像内存申请一块区域(文件信息区),就会创建FILE结构体类型变量。、
2.根据当面打开data.txt文件的信息来填充FILE结构体里面相关的信息。
3.同时会给一个指向这个EILE结构体指针,/*FLIE*指针*/,用这个指针能找到这个文件信息区 - 文件信息区又跟data.txt文件相关的。
4.所以后期拿到FLIE*指针变量能找到文件信息区,文件信息区维护这个文件。
5.未来想操作文件,关注*FLIE指针就行,不许要知道文件信息区的细节,也不需要知道文件信息区又跟文件如何关联的。
总结:
1.FILE确实是个结构体,这个结构体就能描述文件的相关的信息。
2.当我想描述这些信息的时候,把这些信息存起来,利用这个FILE结构体类型,会在内存创建一个区域,这块区域就被称为文件信息区。
3.当打开一个文件想使用的时候,打开文件这个操作主动的会像内存申请一块区域创建⼀个FILE结构的变量,会根据文件当前信息把结构填充起来,不用关注细节。
4.⼀般都是通过⼀个FILE*的指针来维护这个FILE结构提体变量。
5.只要打开文件就会创建一个相关的文件信息区,不需要维护,后期操作文件的时候信息区也会跟着变化。
6.通关FILE*指针管理的中间区域(文件信息区)称为流。
问题:FILE*操作文件如何获得 文件指针变量?
解决:通过打开文件方式,获得一个文件指针。
3.文件的打开和关闭
⽂件在读写之前应该先/*打开⽂件*/,在使⽤结束之后应该/*关闭⽂件*/。
在编写程序的时候,/* 在打开⽂件的同时,都会返回⼀个FILE* */ 的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。
ANSIC 规定使⽤ /* fopen 函数来打开⽂件*/, /* fclose 来关闭⽂件。*/
fopen原型:
FILE* fopen(const char* filename, const char* mode);
参数:
const char* filename:指打开文件名。
const char* mode:打开方式 - 是进行读/写,是二进制的读还是二进制写。
返回类型:
FILE*:返回一个FLIE结构体类型的指针。
fclose原型:
int fclose(FILE* stream);
参数:
FILE* stream:FILE*类型的指针。
下⾯都是⽂件的打开模式:
代码举例:操作文件
(1)相对路径
int main()
{
//步骤:
//打开文件,为了写
//打开成功会返回地址,打开失败返回NULL
//所以要检查打开成功或失败
//.表示当前目录
//..表示上一级路径
FILE* pf = fopen("data.txt", "w");//直接写叫相对路径,相对于程序在的路径。
//解析含义
//1.使用fopen函数打开一个文件,会自动创建一个FILE结构体类型跟文件相关的文件信息区 - 流
//2.通过在流里读写数据,同时会fopen函数会返回FILE*,来维护文件信息区 - 流
if (pf == NULL)
{
perror("fppen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意:代码在那个路径,创建的文件默认在那个路径 - 相对路径。
(2)代码举例:绝对路径
int main()
{
FILE* pf = fopen("C:\\Users\\tyl\\Desktop\\fufu1.txt", "w");//这个是从跟上写的路径 - 绝对路径
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
"C:\\Users\\tyl\\Desktop" - 我的桌面路径。
(3)代码举例:.和..的使用,创建当面文件上一个路径
int main()
{
FILE* pf = fopen("./../fufu1.txt", "w");//含义是当前路径,上一级路径底下打开fufu1.txt
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这也是相对路径写法,这是一种相对于相对路径的写法。
"w"打开的方式特点:没有文件会新创建一个文件,如果文件存在并且里面有数据,代码执行过去会清理掉文件数据。
(4)知识点补充:
什么是输入输出?什么是读写?
1.自己写的程序,数据在内存上,假设你要从键盘/文件提供数据往程序内存放,这个动作是输入操作,也可以叫读操作。
2.当你想把数据写在文件/屏幕上,这时候你是把内存数据往文件或屏幕上输出,这个动作是输出操作,也可以叫写操作。
总结:当们想操作文件的时候,第一部你要打开文件,你要告诉我路径和打开方式是进行读还是写。
问题如何写?
五. 文件的顺序读写
顺序读写:写完一个紧接写下一个,一直往下写。
随机读写:写完一个,想跳着去读或跳着去写。
1.顺序读写函数介绍
(1)代码举例:fputc函数的使用(写)
fputc原型:
功能:一次写一个字符。
int fputc(int character, FILE * stream);
参数:
int character:参数是int,因为传字符是ASCII码值。
FILE* stream:文件流
含义是在文件流写数据,写字符。
方法1:
int main()
{
FILE* pf = fopen("fufu1.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, pf);
}
//关闭
fclose(pf);
pf = NULL;
return 0;
}
方法2:把26字母打印在屏幕上 - 用到标准输出流 (stdout)
int main()
{
int i = 0;
for (i = 0;i < 26;i++)
{
fputc('a' + i, stdout);
}
fputc('\n', stdout);
fputc('a', stdout);
return 0;
}
(2)代码举例 - fgetc函数的使用(读)
fgetc原型:
int fgetc(FILE * stream);
功能:一个读取一个字符。
参数:
FILE* stream:传的是个文件流,说明在流里面读数据
返回类型:读成功,返回字符的ASCII码值,读失败,返回EOF。
int main()
{
FILE* pf = fopen("fufu1.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
(3)代码举例 - 练习
题目 写一个代码,将fufu1.txt文件的内容,拷贝一份生成fufu2.txt。
分析:
1.从fufu1.txt中读取数据
2.写到fufu2.txt的文件中
int main()
{
//打开fufu1.txt进行读
FILE* pfread = fopen("fufu1.txt", "r");
if (pfread == NULL)
{
perror("fopen->fufu1.txt");
return 1;
}
//打开fufu2.txt进行写
FILE* pfwrite = fopen("fufu2.txt", "w");
if (pfwrite == NULL)
{
//如果fufu2.txt打开文件失败,也要关闭fufu1.txt文件
fclose(pfread);
pfread = NULL;
perror("fopen->fufu2.txt");
return 1;
}
//数据读写的(拷贝)
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
fputc(ch,pfwrite);
}
//关闭文件
fclose(pfread);
fclose(pfwrite);
pfread = NULL;
pfwrite = NULL;
return 0;
}}
文件拷贝底层原理。
(4)代码举例 - fputs函数的使用(写)
fputs原型:
int fputs(const char* str, FILE * stream);
参数:
const char* str:字符串
FILE* stream:流
含义:写一个字符串到流。
(5)代码举例 - fgets函数的使用(读)
fgets原型:
char* fgets(char* str, int num, FILE* stream);
参数:
char* str:从流里面读取,放到str指向的空间。
int num:最多读多少个,num - 1个,因为最后一个会放\0,得预留一个位置。
FILE* stream:从流里面读取。
知识点补充:
可变参数列表 - 用的非常少
在众多库函数里边只有 printf 和 scanf 这个2函数用到可变参数。
(6)代码举例 - fprintf函数的使用(写)
printf原型:
int printf(const char* format, ...);//可变参数
fprintf原型:
int fprintf(FILE* stream, const char* format, ...);
2个函数对比发现只有第一个参数不一样,只要会使用printf就会使用fprintf。
struct St
{
char name[20];
int age;
float score;
};
int main()
{
struct St s = { "zhangsan",20,90.5f };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//printf("%s %d %f", s.name, s.age, s.score);pirntf的使用
fprintf(pf,"%s %d %.1f", s.name, s.age, s.score);
//写在屏幕
fprintf(stdout, "%s %d %.1f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(7)代码举例 - fscanf函数的使用(读)
scanf原型:
int scanf(const char* format, ...);//可变参数
fscanf原型:
int fscanf(FILE* stream, const char* format, ...);
2个函数对比发现只有第一个参数不一样,只要会使用scanf就会使用fscanf。
struct St
{
char name[20];
int age;
float score;
};
int main()
{
struct St s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件 - 怎么放的怎么读,注意格式
//scanf("%s %d %f",s.name, &(s.age), &(s.score));scanf的使用,scanf后面要&地址,name是数组是首元素地址。
fscanf(pf, "%d %s %f", s.name, &(s.age), &(s.score));
fprintf(stdout, "%d %s %.f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意打印的时候可以.f控制长度,但读的时候不行。
总结:
1.fgetc和fputc,fgets和fputs,fscanf和fpirntf 这些函数都是以文本信息写到文件,直接能看懂的,读相反。
2.这些函数只是另一种输入输出的手段。
(8)代码举例 - fwrite函数的使用(写)
fwrite原型:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
参数:
const void* ptr:指针指向被写的数据元素,你要写那个数据,指针就指向那个地方,就是数据的起始地址。
size_t size:被写的元素的大小,单位是字节。
size_t count:一次写几个元素。
FILE* stream:写到流。
struct St
{
char name[20];
int age;
float score;
};
int main()
{
struct St s = { "zhangsan",20,95.5f };
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制形式 写文件
fwrite(&s, sizeof(s), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意:
1.字符串不管以文本形式还是以二进制形式在文本形式都是不变的。
2.对于整形浮点型他们ASCII码值不一样,导致文本形式查看,会看不懂。
(9)代码举例 - fread函数的使用(读)
fread原型:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
含义是:stream流里面的count的数据,count大小数据,读出来放到ptr指向的空间。
跟fwrite相反参数:一样,含义不一样。
struct St
{
char name[20];
int age;
float score;
};
int main()
{
struct St s = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制形式 读文件
fread(&s, sizeof(s), 1, pf);
//printf("%s %d %f\n", s.name, s.age, s.score);
//二进制形式 写在屏幕
fwrite(&s, sizeof(s), 1, stdout);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
六. 文件的随机读写
功能:文件指针(光标)来到想来到的位置,在合理的范围内想在哪里写就在哪里写,想在哪里读就在哪里读。
1.fseek
根据⽂件指针的位置和偏移量来定位 ⽂件指针(文件内容光标)。//光标也叫文件指针。
fseek原型:
int fseek(FILE* stream, long int offset, int origin);
参数:
FILE* stream:哪个流?
long int offset:偏移量。
int origin:有3个选择。
(1)SEEK_SET - Beginning of file //从文件起始位置算偏移量。
(2)SEEK_CUR - Current position of the file pointer//文件指针当前位置。
(3)SEEK_END - End of file*//文件末尾。
第3个参数是程序员用来选择位置计算想要光标要到位置偏移量。
代码举例:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
ch = fgetc(pf);
printf("%c\n", ch);//d
//SEEK_CUR 不想读到e,想回到起始位置a,偏移量-4,向前偏移4个字符
fseek(pf, -4, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//a
//SEEK_END 从文件末尾计算偏移量
fseek(pf, -6, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//a
//SEEK_SE 从文件起始位置计算偏移量
fseek(pf, -6, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);//a
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
总结:基于我们知道文件内容前提下。
2.ftell
功能:返回⽂件指针相对于起始位置的偏移量。含义:当你读写操作不知道文件指针(光标)在哪里了,从起始位置计算当前位置偏移量。
ftell原型:
long int ftell(FILE * stream);
(1)代码举例:ftell的使用
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
ch = fgetc(pf);
printf("%c\n", ch);//d
int n = ftell(pf);
printf("%d", n);
fclose(pf);
pf = NULL;
return 0;
}
(2)代码举例:ftell计算文件的大小
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//计算
fseek(pf, 0, SEEK_END);
int n = ftell(pf);
printf("%d\n", n);//6
fclose(pf);
pf = NULL;
return 0;
}
补充:也可以用于来计算文件的大小,偏移量就是字节。
3.rewind
功能:让⽂件指针的位置回到⽂件的起始位置。
rewind原型:
void rewind(FILE* stream);
代码举例:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
ch = fgetc(pf);
printf("%c\n", ch);//d
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);//a
fclose(pf);
pf = NULL;
return 0;
}
七. 文件读取结束的判定
我们在读取文件的时候,读一个写一个信息,总得判定文件读取文件结束,用的函数不一样判定的方法也不一样。
feof函数 — 在文件读取结束后,判断是否是因为遇到文件末尾而结束。
ferror函数 — 在文件读取结束后,判断是否是因为遇道错误结束。
1.被错误使用的 feof
牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。
feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:是否是遇到⽂件末尾结束。
(1)⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
• fgetc 判断返回值是否为 EOF .//不管读取失败结束,还是读取道末尾结束都会返回EOF。
• fgets 判断返回值是否为 NULL .//不管读取失败结束,还是读取道末尾结束都会返回NULL。
(2)⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
功能:fread判断返回值是否⼩于实际要读的个数。
fread原型:
size_t fread(void* ptr, size_t size, size_t count, FILE * stream);
参数:
FILE* stream:文件流
size_t count:实际要读的个数
size_t size:要读取的每个元素的大小,单位是字节。
void* ptr:指向的空间
含义:在文件里面读count的个数,size大小的数据放在ptr指向的空间。
返回值:真实读取到的个数。
代码举例:
(1)⽂本⽂件的例⼦:
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;//EXIT_FAILURE c语言定义好的值
}
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))//读取错误
puts("I/O error when reading");
else if (feof(fp))//文件末尾
puts("End of file reached successfully");
fclose(fp);
}
(2)⼆进制⽂件的例⼦:
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };//小数点后面没写默认是0
FILE* fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
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);
}
八. 文件缓冲区
ANSIC 标准采⽤/*“缓冲⽂件系统”*/处理的/*数据⽂件*/的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个/*正在使⽤的⽂件开辟⼀块“⽂件缓冲区”*/。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。
注意:缓冲区不用放满也能输入输出数据过去,只要刷新缓冲区就行,fflush函数可以刷新文件指针对应的缓冲区。
代码举例:
VS2022环境下
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘),刷新pf对应的缓冲区
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
此时文件什么都没有。
刷新了缓冲区,此时程序还没结束。
为什么存在缓存冲?
是为了效率考虑,当我们把数据写到硬盘上去,这个写硬盘的动作是谁完成的?
明面上看似fputs函数完成的,事实上fupts函数底层还会调用操作系统提供的接口,然后操作系统把我们数据写到硬盘。
//硬盘的操作是由操作系统完成的。
所以说C语言这些读写函数都会再次调用操作系统提供的接口,由操作系统帮我们真实的完成数据的读写。
这些函数在执行过程中会打断操作系统,频繁的打断不合适,所以放一块缓冲区,你先把要写的数据放在缓冲区,写满了或手动刷新操作系统在帮你写一次,这样不会被频繁的打断。
这⾥可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,//需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。
如果不做,可能导致数据的丢失。因为如果发生停电,缓冲区的数据还没写到硬盘上去的动作就断电了,数据没有及时写在硬盘等于丢了。如果及时在关闭文件或刷新缓冲区,就算停电了数据也不会丢。