目录
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
磁盘(硬盘)上的文件是文件。
但是在程序设计中,我们⼀般谈的⽂件有两种:程序文件、数据文件(从文件功能的角度度来分类的)。
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,⽂件标识常被称为文件名。
根据数据的组织形式,数据文件被称为⼆进制文件和文本文件?
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的文件中,就是⼆进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
⼀个数据在文件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节。
用二进制写入,用文本文件不能打开,只能使用二进制的形式打开
- #include
-
- int main() {
- int a = 1000;
- FILE* pf = fdopen("test.txt", "wb");
- fwrite(&a, 4, 1, pf); //找到a的地址写入,写入四个字节,写入一次,找到关联的文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
流-->一个媒介,连接程序和外部设备
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作,最后关闭流
那为什么我们从键盘输入数据(在C语言中),向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
stdout - 标准输出流,⼤多数的环境中输出到显示器界面,printf函数就是将信息输出到标准输出流中。
stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针。
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名 FILE.
例如,VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
struct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE;
每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下⾯我们可以创建⼀个FILE*的指针变量:
FILE* pf;//⽂件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变量)。通过该⽂件信息区中的信息就能够访问该文件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建立了指针和文件的关系。
ANSI C 规定使用 fopen 函数来打开文件, fclose 来关闭文件。
文件操作,文件中有路径的话 最好加上两个斜杠
E:\C\Code\Cproject\practice6.17\practice6.17(绝对路径)
如果用相对路径, " . " 表示当前路径 ". ."表示上一级路径
那我们要访问该路径的上一级路径的文件时, " .\ \ ..\ \ test.txt "
- int main() {
- //1.打开文件
- //打开成功返回一个有效的指针
- //打开失败,返回NULL(判断文件指针是否为空)
- FILE* pf = fopen("test.txt", "w"); //D:\\WeGameApps\\
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.写文件操作
- //...
-
- //关闭文件
- fclose(pf);
-
- }
文件按照顺序去读写,光标
在C语言中,EOF(End of File)是一个特殊的标记,用于表示文件的结尾。当我们读取文件或者输入数据时,EOF可以帮助我们确定何时到达了文件的末尾。本文将详细介绍EOF的意义和用法,并提供一些示例代码以帮助理解。
EOF的意义 EOF在C语言中用于标识文件的结尾。当我们从文件中读取数据时,EOF可以告诉我们何时已经读取完了文件中的所有内容。这对于文件的处理非常重要,因为它允许我们在读取文件时判断何时停止读取并继续执行其他操作。
EOF的用法 在C语言中,我们可以使用
EOF
常量来表示文件的结尾。EOF
的值是一个特殊的整数常量,通常被定义为-1。当我们从文件中读取数据时,EOF
常量可以与读取的字符进行比较,以确定是否已经到达了文件的结尾。
如下代码从文件中读取所有字符 等于EOF就是到了文件结尾,结束循环
while((ch = fgetc(pf)) != EOF)
写字符操作,一个个字符写入 该函数有两个参数 ---> fputc(i,pf); // i表示要写入的内容 pf 文件指针流
- int main() {
- //1.打开文件
- //打开成功返回一个有效的指针
- //打开失败,返回NULL(判断文件指针是否为空)
- FILE* pf = fopen("test.txt", "w");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.写文件操作
- int i = 0;
- for (int i = 'a'; i <= 'z'; i++) {
- fputc(i,pf); // i表示要写入的内容 pf 文件指针流
- }
-
- //关闭文件
- fclose(pf);
-
- }
读字符函数, fgetc(pf), 只有一个参数(文件指针流)
- int main() {
- //1.打开文件
- //打开成功返回一个有效的指针
- //打开失败,返回NULL(判断文件指针是否为空)
- FILE* pf = fopen("test.txt", "r");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.读文件操作
- int ch = fgetc(pf); //读取到一个字符赋值给ch 然后光标就往下
- printf("%c\n", ch);
- ch = fgetc(pf); //再读取一个 赋值给ch 光标再往下
- printf("%c\n", ch);
- ch = fgetc(pf);
- printf("%c\n", ch);
-
- //关闭文件
- fclose(pf);
- pf = NULL;
- }
另外一种读 操作 使用EOF
- int main() {
- //1.打开文件
- //打开成功返回一个有效的指针
- //打开失败,返回NULL(判断文件指针是否为空)
- FILE* pf = fopen("test.txt", "r");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.读文件操作
- int ch = 0;
- while ((ch = fgetc(pf)) != EOF)
- {
- printf("%c", ch);
- }
-
- //关闭文件
- fclose(pf);
-
- }
在上面我们所提到的标准流 stdin和stdout也可以用上面的读写函数,他们适合所有的输入输出流
实现在屏幕输入一个字符,在屏幕输出一个字符
- int main() {
- int ch = fgetc(stdin);
- fputc(ch, stdout);
- return 0;
- }
int fputs ( const char * str, FILE * stream );
文本输入函数, 第一个参数是输入一个字符串,第二个参数是文件指针流
- int main() {
- //1.打开文件
- FILE* pf = fopen("test.txt", "w");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- fputs("i am a man", pf);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
char * fgets ( char * str, int num, FILE * stream );
第一个参数用一个字符数组存储读到的字符串,第二个参数,最多读到多少个字符,第三个参数文件指针流
注意:在读字符的时候,实际读到的只是(num-1)个字符因为包含了一个\0,或者遇到换行的时候也会终止读取
- int main() {
- //1.打开文件
- FILE* pf = fopen("test.txt", "r");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- char arr[20] = { 0 };
- fgets(arr, 5, pf);
- printf("%s", arr);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
同样以上两个函数同样适合所有的输入输出流,以下实现在屏幕输入和输出字符串
- int main() {
- char arr[20] = {0};
- fgets(arr, 10, stdin);
- fputs(arr, stdout);
- }
int fprintf ( FILE * stream, const char * format, ... )
格式化输出函数(所有类型都可以) ,适合所有输出流,该函数和printf函数多了个文件指针流,
- int main() {
- char name[20] = "aaa";
- int age = 20;
- float score = 95.7f;
- //1.打开文件
- FILE* pf = fopen("test.txt", "w");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- fprintf(pf, "%s %d %.1f", name, age, score);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
我们也可以把数据放到结构体里
- struct S
- {
- char name[20];
- int age;
- float score;
- };
-
-
- int main() {
- struct S s = { "lisi",18,96.2f };
- //1.打开文件
- FILE* pf = fopen("test.txt", "w");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
int fscanf ( FILE * stream, const char * format, ... );
要先把数据读出放到一个结构体中,再输出到屏幕
- struct S
- {
- char name[20];
- int age;
- float score;
- };
-
-
- int main() {
- struct S s = { 0 };
- //1.打开文件
- FILE* pf = fopen("test.txt", "r"); //读文件
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- //name不用加&符号,因为本来name就是数组首元素地址
- //把文件中的数据读出来后放到结构体中
- fscanf(pf, "%s %d %.1f", s.name, &(s.age), &(s.score));
- //再把结构体的数据读出
- printf(" %s %d %.1f", s.name, s.age, s.score);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
第一个参数,取需要写入文件的数据的地址,第二个参数该数据的大小,第三个参数该数据的个数,
以二进制的形式输出到文件中
- struct S
- {
- char name[20];
- int age;
- float score;
- };
- int main() {
- struct S s = { "lisi",18,96.2f };
- //1.打开文件 二进制形式写入
- FILE* pf = fopen("test.txt", "wb");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- fwrite(&s, sizeof(struct S), 1, pf);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
-
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
将文件中以二进制的形式读出放到ptr指向的空间里
- struct S
- {
- char name[20];
- int age;
- float score;
- };
- int main() {
- struct S s = { "lisi",18,96.2f };
- //1.打开文件 二进制形式写入
- FILE* pf = fopen("test.txt", "rb");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.操作文件
- fread(&s, sizeof(struct S), 1, pf);
- printf("%s %d %f", s.name, s.age, s.score);
- //3.关闭文件
- fclose(pf);
- pf = NULL;
- return 0;
- }
-
scanf/fscanf/sscanf
printf/fprintf/sprintf
这里重点介绍sscanf函数和sprintf函数
1.sprintf函数,将其他类型的数据转化成字符串
- struct S
- {
- char name[20];
- int age;
- float score;
- };
-
- int main() {
- char arr[100] = {0};
- struct S s = { "aa",17,96.2f};
- sprintf(arr, "%s %d %f", s.name, s.age, s.score);
- printf("%s\n", arr);
- return 0;
- }
2.sscanf函数 将字符串转化成其他类型的数据
- struct S
- {
- char name[20];
- int age;
- float score;
- };
-
- int main() {
- char arr[100] = {0};
- struct S s = { "aa",17,96.2f};
- //临时变量
- struct S tmp = { 0 };
-
- sprintf(arr, "%s %d %f", s.name, s.age, s.score);
- /*printf("%s\n", arr);*/
- sscanf(arr, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
- printf("%s %d %f", tmp.name, tmp.age, tmp.score);
- return 0;
- }
根据⽂件指针的位置和偏移量来定位⽂件指针(⽂件内容的光标)。
int fseek ( FILE * stream, long int offset, int origin );
offsetof --- 计算结构体成员相比于起始位置的偏移量
我们看下面代码 如果想单独读字符e就会比较麻烦,我们可以用fseek函数
- int main() {
- FILE* pf = fopen("test.txt", "rb");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.读文件
- int ch = fgetc(pf); //读取到一个字符赋值给ch 然后光标就往下
- printf("%c\n", ch);
- ch = fgetc(pf); //再读取一个 赋值给ch 光标再往下
- printf("%c\n", ch);
- ch = fgetc(pf);
- printf("%c\n", ch);
-
- fclose(pf);
- pf = NULL;
- return 0;
- }
fseek函数,第三个参数可以有三种选项
SEEK_SET | 文件开头位置 |
---|---|
SEEK_CUR | 文件指针指向的当前位置 |
SEEK_END | 文件末尾 |
- int main() {
- FILE* pf = fopen("test.txt", "rb");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.读文件
- int ch = fgetc(pf); //读取到一个字符赋值给ch 然后光标就往下
- printf("%c\n", ch);
- ch = fgetc(pf); //再读取一个 赋值给ch 光标再往下
- printf("%c\n", ch);
- fseek(pf, 2, SEEK_CUR);//在当前位置偏移两个位置找到e
- ch = fgetc(pf);
- printf("%c\n", ch);
- fclose(pf);
- pf = NULL;
- return 0;
- }
返回文件指针相对于起始位置的偏移量
我们看下面代码,读完e之后往下一光标,输出偏移量为5
- int main() {
- FILE* pf = fopen("test.txt", "rb");
- if (pf == NULL) {
- perror("fopen");
- return 1;
- }
- //2.读文件
- int ch = fgetc(pf); //读取到一个字符赋值给ch 然后光标就往下
- printf("%c\n", ch);
- ch = fgetc(pf); //再读取一个 赋值给ch 光标再往下
- printf("%c\n", ch);
- fseek(pf, 2, SEEK_CUR);
- ch = fgetc(pf);
- printf("%c\n", ch);
- //输出文件指针相比于文件的起始位置的偏移量
- printf("%d", ftell(pf));
- fclose(pf);
- pf = NULL;
- return 0;
- }
让文件指针的位置回到文件的起始
void rewind ( FILE * stream );
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
因为文件结束有两种,1.遇到文件结尾,2.遇到错误
feof 的作用是:当文件读取结束的时候,再判断是读取结束的原因是否是:遇到文件尾结束。
feof的返回值,如果没有设置就默认返回0(还没到文件末尾),返回1的话代表已经到达文件末尾
1.文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
例如:fread判断返回值是否小于实际要读的个数。
文本文件例子:
- include
- #include
- int main(void)
- {
- int c; // 注意:int,⾮char,要求处理EOF
- FILE* fp = fopen("test.txt", "r");
- if(!fp) {
- perror("File opening failed");
- return EXIT_FAILURE;
- }
- //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);
- }
二进制的例子
- #include
- enum { SIZE = 5 };
- int main(void)
- {
- double a[SIZE] = { 1.,2.,3.,4.,5. };
- 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);
- }
ferro函数,如果返回一个非零的整数值,说明读取出问题;返回一个0则表示没有问题
小练习:写一个程序将一个txt文件的内容复制到另外一个txt文件
- int main() {
- //打开文件
- FILE* pfread = fopen("test.txt", "r");
- if (pfread == NULL) {
- perror("fopen\n");
- return 1;
- }
- FILE* pfwrite = fopen("test2.txt","w");
- if (pfwrite == NULL) {
- perror("fopen\n");
- fclose(pfread); //如果打开文件操作出错,则关闭pfread流
- return 1;
- }
- //读或写文件
- int ch = 0; //返回值要为int类型
- while ((ch = fgetc(pfread)) != EOF) {
- fputc(ch, pfwrite); //ch表示要写入的字符
- }
- //关闭文件
- fclose(pfread);
- pfread = NULL;
- fclose(pfwrite);
- pfwrite = NULL;
- }
ANSIC 标准采用“缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使⽤的⽂件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘文件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
简单来说,就是不能一直让操作系统处理数据,先把数据存起来,再一次过处理,而不是每发一次就处理一次
- #include
- #include
- //VS2022 WIN11环境测试
- int main()
- {
- FILE*pf = fopen("test.txt", "w");
- fputs("abcdef", pf);//先将代码放在输出缓冲区
- printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
- Sleep(10000);
- printf("刷新缓冲区\n");
- fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
- //注:fflush 在⾼版本的VS上不能使⽤了
- printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
- Sleep(10000);
- fclose(pf);
- //注:fclose在关闭⽂件的时候,也会刷新缓冲区
- pf = NULL;
- return 0;
- }
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题,防止信息丢失