目录
一.为什么使用文件
二.什么是文件
2.1程序文件
2.2数据文件
2.3文件名
三.文本文件和二进制文件
fwrite函数
fclose函数
四.文件的打开和关闭
4.1流和标准流
4.2文件指针
4.3文件的打开和关闭
五.文件的顺序读写
5.1文件的顺序读写函数
5.1.1fgetc函数
5.1.2fputc函数
5.1.3fgets函数
5.1.4fputs函数
5.2scanf和printf
5.2.1fscanf函数
5.2.2sprintf函数
六.文件的随机读写
6.1fseek函数
6.2ftell函数
6.3rewind函数
七.文件读取结束的判定
1.文本文件
2.二进制文件
八.文件缓冲区
一.为什么使用文件
如果没有文件,我们写的程序数据是储存在内存中的,一旦程序结束,内存回收,数据就没有了,再次运行程序,上次写的程序就消失了,如果我们要将数据持久化,我们就可以使用文件了。
二.什么是文件
在程序设计中,我们讨论的文件一般有两种:程序文件,数据文件(从文件的功能的角度来分类)
2.1程序文件
2.2数据文件
2.3文件名
⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。
文件名包含三个部分:文件路径 + 文件名主干 + 后缀名
为了⽅便起⻅,⽂件标识常被称为⽂件名。
三.文本文件和二进制文件
根据数据的形式,数据文件被分为文本文件或者二进制文件
二进制文件:数据在内存中以二进制的形式存储,不加以转换的输出到外存的文件 。
文本文件:数据在内存中以二进制的形式存储,要求在外存上以ASCII码的形式存储,ASCII字符的形式存储的文件就是文本文件。
如何判断一个数据在文件中是怎么存储的呢?
字符一律以ASCII码值的形式存储,数值型数据即可以用ASCII形式存储,也可以使用二进制的形式进行存储。
比如整数10000,如果以ASCII形式存储,则磁盘中占用5个字节(每一个字符一个字节),而以二进制的形式进行输出,则在磁盘上只占4个字节。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h>
int main()
{int a = 10000; FILE* pf = fopen("text.txt","wb");if (pf == NULL)//如果文件打开失败返回NULL;{perror("fopen");return 1;}fwrite(&a,4,1,pf);fclose(pf);pf = NULL;return 0;
}
fopen函数:以二进制的写(“wb”)的形式打开text.txt文件.
如果项目路径下没有这个文件,就会自动创建一个这样的文件.并且返回一个文件指针
fwrite函数
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
这个函数的功能就是将数据写入一个文件中。
- ptr指向一个数组.这个数组就是我们要写入的数据。
- count指的是我们需要写入的数组的元素个数。
- size指的是数组元素的大小。
- 我们需要写入的这个文件的文件指针。
- 返回值是成功写入的元素个数
fclose函数
在我们打开文件后,使用完成,就需要关闭这个文件,fclose这个函数是用于这个的,并且为了防止非法访问,我们还需要将文件指针置为空指针,避免野指针。
综上:
我们上面的代码就是打开了一个叫做text.txt的文件,并且以二进制的形式写入了一个数据a。
这个文件可在我们的项目路径下看到。
因为a是一个整形占四个字节,所以参数2是4。
正常来说,我们运行程序后,我们点击项目文件中的text.txt文件,我们发现我们无法看懂写了什么,是乱码。
这是因为这个文件是二进制的文件,在vs2022中,我们把这个文件添加到解决方案资源管理器中
点击完现有项,再把这个文件添加进去。
在打开方式中选择二进制编辑器
这样我们就打开了这个文件
前面的00000000不用管,后面的10 27 00 00 就是我们写入的数据,我们会发现这不是二进制的,其实是因为二进制的形式过于长了,所以采用十六进制,而且2个字符就是一个字节,更加方便。
我们存入的数据是10000,为什么显示10 27 00 00 呢?
我的这个机器是小端字节序存储,10000的二进制是00002710,所以int a的第一个字节是10 ,第二个字节是27,再进行写入数据的时候,是一个字节一个字节的写的,所以第一个是10,然后再是27.
四.文件的打开和关闭
4.1流和标准流
我们的程序的数据需要输出到各种外部的设备 ,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们把流想象成流淌着字符的河流
C程序针对文件,画面,键盘等数据的输入输出操作都是通过流操作的。
一般情况下,我们想要向流里写数据或者从流中读取数据,都是要打开流的,然后操作。
那为什么我们从键盘上输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序默认打开了3个流:
- stdin:标准输入流,在大多数情况下,从键盘输入,scanf函数就是从标准输入流中读取数据的
- stdout:标准输出流,在大多数情况下,输出至显示器界面,printf就是将信息输出到标准输出流中。
- srderr:标准错误流,大多数环境输出到显示器界面。
这三个流的类型都是FILE*,通常也叫做文件指针。
4.2文件指针
每当我们打开一个文件的时候,系统会自动根据文件的情况创建一个FILE的结构体变量,并且填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的文件指针来维护这个FILE的结构变量,这样使用起来更加方便。
FILE * pf;
这个pf的变量指向这个文件的文件信息区,通过这个文件的文件信息区就能够访问该文件,也就是说,通过文件指针变量能够间接找到与它相关联的文件了。
4.3文件的打开和关闭
在我们读写文件之前,我们应该打开文件,在使用结束后应该关闭该文件。
在编写程序的时候,我们使用fopen函数打开这个文件,这个函数会返回一个FILE*的指针变量指向这个文件,也相当于建立了指针和文件的关系。
ASCI C规定使用fopen函数打开文件,fclose函数来关闭文件。
FILE * fopen ( const char * filename, const char * mode );
filename就是文件名主干和后缀名,mode就是打开的方式
int fclose ( FILE * stream );
stream就是流,也就是文件指针。
mode表示文件的打开方式
文件的使用方式 | 含义 | 如果指定的文件不存在 |
"r" | 只读,为了读取数据,打开一个已经存在的文本文件 | 出错 |
"w" | 只写,为了写入数据,打开一个的文本文件 | 创建一个新的文件 |
"a" | 追加,在文件的末尾追加数据,打开一个文本文件 | 创建一个新的文件 |
"rb" | 只读,为了读取数据,打开一个二进制文件 | 出错 |
"wb" | 只写,为了写入数据,打开一个二进制文件 | 创建一个新的文件 |
"ab" | 追加,在二进制文件末尾添加数据 | 创建一个新的文件 |
"r+" | 读写,打开一个文本文件 | 出错 |
"w+" | 读写,打开一个文本文件 | 创建一个新的文件 |
"a+" | 读写,在文件的末尾进行读写 | 创建一个新的文件 |
"rb+" | 读写,打开了二进制文件 | 出错 |
"wb+" | 读写,打开一个二进制文件 | 创建一个新的文件 |
"ab+" | 读写,打开一个二进制文件 | 创建一个新的文件 |
- r是读(read),w是写(write),a是追加(append)
- 所有只能读的文件,如果不存在,就会报错。而写和追加就会创建新的文件
- b是二进制的意思(binary)
- 后面有+,就是既可以读又可以写。
实例代码:
#include<stdio.h>
int main()
{FILE* pf = fopen("text.txt","w");if (pf == NULL) {perror("fopen");return 1;};fputs("file open example",pf);fclose(pf);pf = NULL;return 0;
}
五.文件的顺序读写
5.1文件的顺序读写函数
观察下面这个代码:
//打开文件FILE* pf = fopen("text.txt","w");if (pf == NULL){perror("fopen");return 1;}//访问文件char arr[10] = "abcde";fwrite(arr,1,5,pf);//关闭文件fclose(pf);pf = NULL;
当我们打开这个txt文件,我们发现fwrite就向这个文件中写了五个字符abcde。
5.1.1fgetc函数
函数原型:
int fgetc ( FILE * stream );
这个函数的功能是从流中获得字符。返回又文件中的光标所指向的字符 。
在我们运行完上面的的代码后,我们再次运行以下代码.
int ch1 = fgetc(pf);int ch2 = fgetc(pf);int ch3 = fgetc(pf);printf("%c %c %c ", ch1, ch2, ch3);
我们发现输出结果是 a b c,为什么呢?
这是因为 指定流的内部文件位置指示器,最开始是在开头的,每一次调用函数后,它的位置会往后移动一个字符,所以会依次打印a b c。
当遇到文件末尾和读取失败的时候,这个函数会返回EOF.
如果需要打印完整个文件就可以采用循环的方式
int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c", ch);}
5.1.2fputc函数
函数原型:
int fputc ( int character, FILE * stream );
这个函数的功能就是将字符写到流中,并且使位置指示器前进一个字符。
//写文件fputc('a',pf);fputc('b',pf);fputc('c',pf);//读文件int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c", ch);}
在这个文件中显示的就是abc
在文件中打印26个字母:
for(char ch = 'a'; ch <= 'z';ch++){fputc(ch,pf);}
5.1.3fgets函数
fgetc是从流中得到字符,fgets就是从流中得到字符串
函数原型:
char * fgets ( char * str, int num, FILE * stream );
得到的字符串需要放在一个字符数组中,第一个参数就是数组名。
第二个参数是我们需要读取的字符个数。
第三个就是流
char str[26] = { 0 };fgets(str,26,pf);printf(str);
运行这个代码输出结果是
这是我们刚刚输入的26个字母。但是我们发现少了字符'z'。
通过调试我们发现
这个字符数组的最后一个字符不是'z',而是'\0'。
这是因为fgets这个函数会默认给最后一个字符补上'\0',所以如果我们想完整打印出26个字母,参数2就不应该是26而应该是27,因为字符串的末尾均是'\0',所以需留'\0'。
这个函数在进行读取的时候,在遇到第num-1 个字符,和新的一行,或者文件末尾,均会停止读取,但是不会影响函数功能。前面读取的字符不会收到影响。
注意:在遇到换行符时,这个符号仍然会被包含进这个数组中。
5.1.4fputs函数
这个函数和fputc函数很相似。
函数原型:
int fputs ( const char * str, FILE * stream );
写一个字符串到流中去。
参数1就是字符串。
fputs("hello world",pf);
运行这个代码,我们就可以在文件看到hello world.注意末尾的'\0'是不会被写到流中去的。
5.2scanf和printf
5.2.1fscanf函数
我们知道scanf是从键盘上读取格式化的数据
那么fscanf就是从文件中读取格式化的数据
int fscanf ( FILE * stream, const char * format, ... );
这个函数的函数原型与scanf几乎一模一样,只不过多了一个文件指针而已。
//定义一个结构体
struct stu
{char name[10];int age;int score;
};
在文件中输入张三 18 99
再运行以下代码
struct stu student;fscanf(pf, "%s %d %d", student.name,&(student.age),&(student.score));printf("%s %d %d ",student.name,student.age,student.score);
输出结果就是张三 18 99.
sscanf也是同理,从字符串中读取数据,本文只涉及文件相关内容,想要了解的读者,可以自行了解:sscanf - C++ Reference
5.2.2sprintf函数
函数原型:
int fprintf ( FILE * stream, const char * format, ... );
这个函数同理与printf函数类似,多了一个文件指针。从在屏幕上打印变成了在文件中打印
struct S s = { "李四",28,95};fprintf(pf, "%s %d %d\n", s.name, s.age, s.score);
这样我们就可以在文件中看到这个结构体的信息。当然其实也是可以在屏幕上打印的。
stdout就是标准输出流,把文件指针写成stdout就可以在屏幕上打印,起到和printf一样的效果。
同理前面的函数也可以使用
int main()
{fputc('a', stdout);return 0;
}
这样就会在屏幕上打印a.
六.文件的随机读写
6.1fseek函数
int fseek ( FILE * stream, long int offset, int origin );
这个函数的功能就是改变位置指示器的位置。
参数1是文件指针
参数2是偏移量
参数3有三种文件开头,文件末尾,和位置指示器当前位置
SEEK_SET就是文件开头,SEEK_END是文件末尾,SEEK_CUR是当前位置。
例:
FILE* pf = fopen("test.txt","wb");fputs("This is an apple.",pf);fseek(pf,9,SEEK_SET);fputs(" sam",pf);
这个代码的作用就是将位置指示器从文件开头偏移9个字符。
This is an apple.
偏移9个字符后,位置指示器的位置就到了第一个字符a的后面,然后打印" sam".
在我们写代码的过程中,我们快速并且准确的知道当前 位置指示器的位置,那怎么办呢?
这时候就需要用到第二个函数
6.2ftell函数
long int ftell ( FILE * stream );
这个函数会返回在流中的位置指示器相对于起始位置的偏移量。
#include <stdio.h>
int main()
{FILE* pFile;long size;pFile = fopen("test.txt", "rb");if (pFile == NULL)perror("Error opening file");else{fseek(pFile, 0, SEEK_END); size = ftell(pFile);fclose(pFile);printf("Size of myfile.txt: %ld bytes.\n", size);}return 0;
}
偏移量的值是可以为0也可以为负和正的,正就是向右移动,负就是向左,0是原位置。
这个代码计算了这个文件有多少个字节。
6.3rewind函数
void rewind ( FILE * stream );
这个函数会将流的位置设置到文件开头。
例子:
#include<stdio.h>
int main()
{int n;FILE* pFile;char buffer[27];pFile = fopen("myfile.txt", "w+");for (n = 'A'; n <= 'Z'; n++)fputc(n, pFile);rewind(pFile);fread(buffer, 1, 26, pFile);fclose(pFile);buffer[26] = '\0';printf(buffer);return 0;
}
使用fseek函数也可以达到相同的效果
fseek(pFile,0,SEEK_SET);
七.文件读取结束的判定
feof的作用是当文件读取结束的时候,判断文件读取结束的原因是不是遇到文件结尾而结束的。
ferror的作用是当文件读取结束的时候,判断文件读取结束的原因是不是发生错误而结束的。
1.文本文件
文本文件的读取结束,判断返回值是否为EOF(fgetc函数),判断是否等于NULL(fgets)
2.二进制文件
判断返回值是否小于实际要读取的个数(fread函数)
#include <stdio.h>
#include <stdlib.h>
int main(void)
{int c; // 注意:int,⾮char,要求处理EOFFILE* fp = fopen("test.txt", "r");if (!fp) {perror("File opening failed");return EXIT_FAILURE;}//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOFwhile ((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 <stdio.h>
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 handlingif (feof(fp))printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {perror("Error reading test.bin");}}fclose(fp);
}
八.文件缓冲区
#include <stdio.h>
#include <windows.h>
//VS2019 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;
}