目录
一.为什么使用文件
如果没有文件,我们写的程序数据是储存在内存中的,一旦程序结束,内存回收,数据就没有了,再次运行程序,上次写的程序就消失了,如果我们要将数据持久化,我们就可以使用文件了。
二.什么是文件
在程序设计中,我们讨论的文件一般有两种:程序文件,数据文件(从文件的功能的角度来分类)
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,要求处理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 <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 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);
}
八.文件缓冲区
#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;
}