目录
前言:
为什么使用文件?
文件是什么?
文件名:
数据文件类型:
数据在文件中的存储
流
什么是流?
文件指针
文件的打开与关闭
写文件:
读文件:
文件读写顺序:
文件函数:
fputc:
fgetc: 编辑
fputs:编辑
fgets: 编辑
fprintf:
fscanf:
sscanf 和 sprintf:
fwrite:
fread:
fseek:
ftell: 编辑
rewind:
ferror: 编辑
feof:
总结:
这其中内核空间用户代码是不能读写的专门留给操作系统内核去使用。
但是这一篇我们来讲文件(上方内存图意义不明,哈哈,权当复习)。
文件是用来存放数据的,但是真正当我们进入工作以后这些数据是不会放在文件当中的,这些数据会放在数据库当中。所以文件这种操作一般用的比较少,但是我们还是要掌握,我们有时可能会使用配置文件,所以还是至少要了解一些。
为什么使用文件?我们写的程序的数据是存储在电脑内存中的,如果程序退出,内存回收,数据就丢失了,等再次运行程序,就要重新开始,看不到上次程序的数据,如果要将数据进行持久化的保存,我们可以使用文件。
磁盘(硬盘)上的文件是文件。但在程序设计中,文件这里一般分为两种文件:
- 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。数据文件
文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取的文件,或者输出内容的文件。
这里我们讨论数据文件,之前所处理数据的输入输出都是以终端为对象的,即从终端设备的键盘输入数据,运行结果显示到显示器上。但有时候我们会把信息输出到磁盘上,当需要的时候在从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
一个文件要有一个唯一的文件标识,以便用户识别和使用。
文件名包含三个部分:文件路径+文件名主干+文件后缀。
例如:c:\code\test.txt
我们已经知道文件分为两种,程序文件和数据文件,那么其实根据数据的组成形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存就是二进制文件。比如我们打开一个不是以txt文件结尾并且也不是对应打开方式的文件,就会遇到以下情况:
是不是很熟悉,接下来我就解释一下是为什么。
一个数据在文件中是怎么存储的呢?字符一律以ASCII形式存储;数值类型数据既可以用ASCII形式存储,也可以用二进制形式存储。
比如整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
以上截图先不要看代码,是我们以二进制方式将10000写入文件,直接打开都是以文本文件打开的(就是读取内存,根据ASCII码转换为对应字符)。
此时就需要用到二进制文件来读取了。
int main()
{
int a = 10000;
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写入文件中
fclose(pf);//关闭
pf = NULL;
return 0;
}
此时我们在原文件上面添加现有项,将桌面上的文件添加到VS中。
此时我们先看10000在VS中的存储: (关于大小端知识,可以看这一篇文章:大端和小端存储模式-CSDN博客 不影响阅读)
此时二进制读取就是正确的(墙面的0可以暂时忽略,编译器原因):
此时就会理解数据文件,内容不一定是程序,而是程序运行是读写的数据。可以发现以上我们使用VS的程序文件操作一个后缀为txt的文件,所以可以得出结论:程序文件是来操作数据文件的。
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同外部设备的输入输出操作各不相同,为方便程序员对各种设备进行方便操作,我们抽象出了流的概念。
百度结果:流是个抽象的概念,是对输入输出设备的抽象。
我们可以把流想象成流淌着字符的河。C程序针对文件、画面、键盘等数据输入输出都是通过流操作的。一般情况下,我们要想向流里写数据,或者从流中读取数据, 都是打开流,然后操作的。
接下来我来给出我的理解(不对请大佬在评论区讲解):就是我们可以把流想象成控制数据流动的方向,我们可以通过指定流来控制数据的流向。
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?因为C语言程序在启动的时候,默认就打开了3个流。
- stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout - 标准输出流,大多数的环境种输出至显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开的三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr三个流的类型是:FILE*,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护各种流的操作的。
缓冲文件系统中,关键概念是“文件类型指针”,简称“文件指针”。
每一个被打开的文件,就会有一个跟它相关的文件信息区。只要打开文件就会创建文件信息区(如文件的名字,文件状态寄文件当前位置)。其实这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明,取名FILE。
我们来看C语言的定义:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同C编译器的FILE类型包含的内容不完全相同,但大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中信息,我们使用并不用关注细节。
一般是通过一个FILE的指针来维护FILE结构的变量,这样使用起来更加方便。下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过文件信息区中的信息就能够访问该文件。也就是说,通过指针变量能够间接找到与它关联的文件。
我们是用文件就需要先打开文件,之后使用完以后关闭文件。C规定使用fopen来打开文件,flose来关闭文件。我们先来看fopen的声明。
可以发现返回的是文件指针所以我们定义一个文件指针类型来接收,我们主要观察mode,上面写给定的模式mode打开文件,这里就需要逐个讲解了。
插一嘴,因为fopen打开失败文件会返回NULL,所以和动态内存函数一样,我们要先判断是否打开成功,之后再进行正常功能的使用:
if (pf == NULL) { perror("fopen"); //使用perror函数来打印错误内容 //等同于printf + strerror 函数 return 1; }
模式为w,如果没有则创建文件,有则在文件中写数据:
int main()
{
//打开文件,为了写
//如果文件打开失败,会返回空指针
FILE* pf = fopen("data.txt", "w");
//所以检查一下
if (pf == NULL)
{
perror("fopen");
return 1;
}
//关闭文件
fclose(pf);
//因为fclose没有能力将其置空
//所以还需要手动置空,防止野指针出现
pf = NULL;
return 0;
}
关闭文件就直接使用fclose即可,我们不再赘述,文件指针使用规律和动态内存函数相似,详情请看动态内存函数-CSDN博客
这里就又会有人问道:文件名不是路径名+文件名+后缀名吗?为啥这只有文件名和后缀名?其实这里是分为绝对路径和相对路径的。
- 绝对路径:大家多学过地理,知道西经东经,南纬北纬,一经一纬就可以确定一个地点。再比如,我直接说新乡,你肯定不知道是哪里。但是我说河南省新乡市新乡县就会知道大致位置,这就是绝对路径。
- 相对路径:就是相对于当前在哪里。比如我说503,你肯定又懵了,但是是我的老板,就知道503在哪里。我必须说成上海xx酒店503,说成绝对位置,你就会知道是哪里了。
此时就以写的方式,在当前的路径下,写了一个data.txt的文件,如果有,则直接往里面写,如果没有就创建一个。
此时我们将刚才的文件删除掉,并直接以读的方式编辑(因为没有该文件,所以报错)。
当我们使用绝对路径时,为了方式转义,我们使用双斜杠来使转义失效(就是我上面的一串代码)。
FILE* pf = fopen("C:\\Users\\Administrator\\Desktop\\test.txt", "w");
相对路径的使用中,还有很多有趣操作,比如:
int main()
{
//打开文件,为了写
//如果文件打开失败,会返回空指针
//. 表示当前目录
//.. 表示上一级目录
FILE* pf = fopen("./../data1.txt", "w");
//此时就是在当前文件的上一级目录写一个文件
//所以检查一下
if (pf == NULL)
{
perror("fopen");
return 1;
}
//关闭文件
fclose(pf);
//因为fclose没有能力将其置空
//所以还需要手动置空,防止野指针出现
pf = NULL;
return 0;
}
此时就在当前文件的上一级文件创建了一个新的data1.txt文件 。注意以上我们用的是相对路径,所以可以不用双斜杠使其转义失效;但是用绝对路径时就需要使用双斜杠。
我们还是使用相对路径,在该路径下生成的data中编辑。此时我们在我们创建好的文件中写入数据。
之后关闭,并在执行一次代码(注:此时我们在文件中已经写入了内容),并打开文件观察结果:
发现刚才写入的值消失了。 所以会发现我们直接在文件中写入数据还是不会保留结果的,所以我们就需要使用文件函数来对文件写入数据。
为了使用C语言来编写文件,C语言给出了很多关于操作文件的函数(这样才能保留)。
打开文件后,接下来我们就来讲解这些函数的使用。
写入文件字符函数。
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
当然,我们也可以使用循环往里面写我们利用循环写入26个音文字母,并在控制台打印结果,此时就需要用到标准输出流(就像我前面解释的那样,暂时可以理解为数据流向了控制台,没有流向文件,博主水平太次,但大致确实是这个意思,以后会了会详细讲解):
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
写文件
//fputc('a', pf);
//fputc('b', pf);
//fputc('c', pf);
//fputc('d', pf);
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, stdout);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
此时我们打开文件并以读的方式,我们先项文件中写入一些数据,之后读取文件中的数据。
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);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里其实是根据光标来判断起始位置的。 第一次读完以后光标会移动,按顺序往后面跑。
之后我们利用这两个函数进行拷贝文件(注:文件data1.txt中已经有数据):
//写一个代码,完成将data1.txt文件的内容,拷贝生成data2.txt文件
//从data1.txt中读取数据
//写到data2.txt文件中
int main()
{
FILE* pfread = fopen("data1.txt", "r");
if (pfread == NULL)
{
perror("fopen->data1.txt");
return 1;
}
FILE* pfwrite = fopen("data2.txt", "w");
if (pfwrite == NULL)
{
//第二个文件打开失败
//关闭第一个文件
fclose(pfread);
pfread = NULL;
perror("fopen->data2.txt");
return 1;
}
//拷贝
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
//EOF是文件结束标志
fputc(ch, pfwrite);
}
fclose(pfread);
fclose(pfwrite);
pfread = NULL;
pfwrite = NULL;
return 0;
}
成功拷贝文件。
这里还是以写文件的形式,虽然有返回值,但是我们可以不进行接收。不同于fputc,它是一次性写入整个字符。
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("pf");
return 1;
}
//写文件 - 写一行
fputs("abcdef", pf);
fputs("abcdef", pf);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
此时我们进行换行操作:
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("pf");
return 1;
}
//写文件 - 写一行
fputs("abcdef\n", pf);
fputs("abcdef\n", pf);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
此时我们就需要进行读文件的操作了。比如此时文件中数据是abcdefabcdef。之后我们将其拷贝到字符串中(这里注意观察n的值,只是复制了n-1个,最后一个是\0)。
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("pf");
return 1;
}
//读取
char arr[20] = "xxxxxxxxxxxxx";
//char * fgets(char *str, int n, FILE *stream)
//从指定流(文件)中读取 n - 1 个数据
//并把它存储在str中
fgets(arr, 10, pf);//此时将arr改变
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
小总结:请允许我这里先总结一下,以免防止搞混。我们发现,我们可以把文件当做控制台,fgets函数类似gets函数的使用,获取字符串;fputs函数类似puts函数的使用,输出字符串。
输出当然要输出到控制台中,但此时控制台是文件,所以就相当于写文件。
获取字符串需要有现成的字符串, 所以就是从控制台中拿数据,所以就是读文件。
这里我们可以发现,它仅仅比printf函数多了一个f,很明显就是针对文件的。我们来看它和printf函数的差别:
可以发现,就多出了一个指定方向的流,后面的…是参数列表,我们可以先不讨论。既然前面是针对的流,所以我们把它指定到文件中去。
fprintf顾名思义,根据我们以上的总结,就是写文件使用到(因为把数据输出到文件中去,所以是写文件)。这里我们使结构体,并写入文件:
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 90.5f };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
return 1;
}
//写文件
//类比 - printf
//printf("%s %d %f\n", s.name, s.age, s.score);
fprintf(pf, "%s %d %.2f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
和fprintf函数一样。
fscanf就是从文件中读数据, 之后指定方向输出,比如下方我们结合fprintf函数使用。
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 90.5f };
//此时为读文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 1;
}
//读文件
//类比 - scanf
//scanf("%s %d %f\n", s.name, &(s.age), &(s.score));
fscanf(pf, "%s %d %f\n", s.name, &(s.age), &(s.score));
//相当于从文件当中写到控制台里面去
//打印
fprintf(stdout, "%s %d %.2f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
注意以上是将fprintf指定的输出流到控制台中。 所以可以发现,我们可以通过fscanf和fprintf函数实现scanf和printf函数。因为scanf和printf是针对标准输入流和标准输出流的,而fscanf和fprintf是可以选择所有的流。
其实这两个函数和fscanf/fprintf函数一样,它们是关于字符串的使用。我们先来看sprintf函数:
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 100,3.14f,"abcdef" };
char buf[1024] = { 0 };
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
//就是把那些输入的字符串格式化的输入到buf中
//有能力把结构体的数据转化为字符串
printf("%s\n", buf);
return 0;
}
再看格式化输入sscanf,sscanf函数需要传入一个字符串指针,并让这个指针接收这些内容,它会读取格式化的数据到一个新的变量中。:
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 100,3.14f,"abcdef" };
struct S t = { 0 };
char buf[1024] = { 0 };
//把格式化的数据转换成字符串存储到buf中
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
//从buf中读取格式化的数据到tmp中
sscanf(buf, "%d %f %s", &(t.n), &(t.score), &(t.arr));
printf("%d %f %s", t.n, t.score, t.arr);
return 0;
}
这个函数被要求要写入个数,和大小,并且只适用于文件。因为是以二进制方式写入文件,所以我们打开文件要使用“wb”方式。
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 90.5f };
FILE* pf = fopen("data.txt", "wb");
//以二进制写文件
if (pf == NULL)
{
return 1;
}
//二进制形式写文件
fwrite(&s, sizeof(s), 1, pf);//一个元素
fclose(pf);
pf = NULL;
return 0;
}
前面我们将解数据文件中有提到过,字符存储不受影响,只有针对数值才会出现乱码,此时也就验证了结果。
既然以二进制的方式写入了数据,那么使用二进制的方式读数据对应起来就不会出错了,所以要使用fread函数(二进制方式输出)。
不难发现,它和fwrite韩式的使用很相似,但是可以理解为是从后向前的,因为先指定从哪个流中读取数据,之后放到哪里。
接下来我们就以二进制方式写入,并以二进制方式读取。
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 90.5f };
FILE* pf = fopen("data.txt", "rb");
//以二进制读文件
if (pf == NULL)
{
return 1;
}
//二进制形式写文件
fread(&s, sizeof(s), 1, pf);//一个元素
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
这样就不会出现问题。
我们每次打开文件,前面都会有一个光标(可以理解为文件指针)。
还记得我们之前使用的fgetc函数吗?它每次读取都会使文件之真相后挪一个位置。那么我们如何想读取一个固定位置的字符呢?就可以使用这个函数,相当于定义了光标的位置。
我们可以看它的具体使用:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 0;
}
//1.定位文件指针
//fseek 一共文件指针到指定的位置
// int fseek(FILE* stream,long offset,int origin);
// 偏移量 文件指针当前位置
fseek(pf, 2, SEEK_CUR);
//2.读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
我们再来看使用文件末位置的定位:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
fseek(pf, -2, SEEK_END);
int a=fgetc(pf);
printf("%c\n", a);
fclose(pf);
pf = NULL;
return 0;
}
ftell函数,这个函数可以告诉你当前文件指针相对于起始文件指针的偏移量为多少,它返回的值是整形,只有一个参数是文件指针。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//定位文件指针
//long int ftell(FILE* stream);
//ftell告诉你当前指针相对于文件起始位置的偏移量
fseek(pf, -2, SEEK_END);
int pos = ftell(pf);//返回文件指针相对于起始位置的偏移量
printf("%d\n", pos);
fclose(pf);
pf = NULL;
return 0;
}
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//定位文件指针
//long int ftell(FILE* stream);
//ftell告诉你当前指针相对于文件起始位置的偏移量
//fseek(pf, -2, SEEK_END);
int pos = ftell(pf);//返回文件指针相对于起始位置的偏移量
printf("%d\n", pos);
fclose(pf);
pf = NULL;
return 0;
}
因为我们使用fgetc等函数会改变指针指向,所以相对偏移量也会改变。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//定位文件指针
//long int ftell(FILE* stream);
//ftell告诉你当前指针相对于文件起始位置的偏移量
//fseek(pf, -2, SEEK_END);
fgetc(pf);//因为使用完fgetc函数后文件指针会向后移动一个字节
int pos = ftell(pf);//返回文件指针相对于起始位置的偏移量
printf("%d\n", pos);
fclose(pf);
pf = NULL;
return 0;
}
我们可以通过fseek函数和ftell函数结合使用,得知文件的总大小:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 0, SEEK_END);
//把光标(文件指针)移动到最后
int sz = ftell(pf);
//由此得知文件有多少字节
printf("%d\n", sz);
fclose(pf);
pf = NULL;
return 0;
}
当我们一直读取一直写入时,忘记了光标在哪里,或者想将光标重新定义到起始位置,就可用到rewind函数,使光标回到起始位置。
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 0;
}
int ch=fgetc(pf);
printf("%c\n", ch);
//void rewind(FILE* stream)
rewind(pf);//让文件指针的位置回到文件的起始位置
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
还记得strerror函数吗?strerror它返回的是char*,这个是整数,但是功能是一样的,可以当做检查当前流的错误信息,我们一般配合feof使用,接下来我们讲解。
当我们是用完文件函数时,文件直接就关闭了,但是文件关闭到底是为什么关闭的?是因为遇到了文件的结束标志EOF(-1)关闭的,还是其他意外情况?此时就出现了feof函数,判断文件到底是什么原因关闭的。
遇到结束标志返回非零值,否则为0,所以我们可以判断。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("hehe");
return 0;
}//读文件
int ch = 0;
while ((ch = fgetc(pf))!=EOF)
{
putchar(ch);
}
if (ferror(pf))
{
printf("error\n");
}
else if (feof(pf))
{//在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束
//而是用于当文件读取结束的时候,判断读取失败结束,还是遇到文件尾结束
//如果pf最后就是-1,feof就会返回一个为非零的数,就说明文件就是因为文件结束而结束的,不是文件读取失败而结束的
printf("end of file\n");
}
fclose(pf);
pf = NULL;
return 0;
}
文件我们实际生活使用的比较少,但是也需要了解,而且有些地方也容易搞混。所以要多加积累。
终于到这一步了,真的很辛苦,这一篇我很想逃避,因为博主真的不是非常了解这一章,但是还是要迎难而上,我接下来会进行JAVA的学习,希望大家多加支持,不足之处请在评论区指出,感谢各位。