好久没有更新C语言学习的博客了,今天带来的是文件部分的知识点!
之前学习过通讯录的代码实现,可以给通讯录中增加、删除联系人。但是这个通讯录在你exe文件关闭的同时就被销毁了,它的内容并不能顺延到下一次打开这个通讯录,这对我们的使用产生了不便。
而文件可以帮助我们实现数据的持久化:将数据保存在磁盘文件中,下次打开通讯录的时候,之前保存的联系人不会消失。
文件就是存放在磁盘上的带特定格式的数据。
在程序设计中,一般讨论两种文件:程序文件、数据文件
如.c
,目标文件.obj/.o
,可执行文件.exe
这篇博客我们了解的是数据文件
文件名包含3个部分:文件路径+文件名主干+文件后缀
如:c:\code\test.txt
文件标识常被称为文件名
在文件操作中,非常重要的一个知识点就是文件类型指针
,简称文件指针
每个文件在开辟的时候都有一个对于的文件信息区,用于保存文件的名字、状态、当前的位置等相关信息。这些信息保存在了一个结构体中,该结构体系统声明为FILE
不同的C语言编译器都有不同的FILE类型,但是大同小异。
打开一个文件的时候,系统会根据文件的内容,自动创建FILE结构体变量,并填充它的信息。
我们需要使用文件的时候,就可以通过一个FILE类型的指针来访问这个结构体变量
文件在读写之前需要打开文件,使用结束后需要关闭文件
这一点和动态内存管理很相似
ANSIC规定用fopen函数来打开文件,fclose来关闭文件。
打开文件的同时,会返回一个FILE*
的指针变量指向该文件。
关闭文件后,文件指针就变成了野指针,需要置为NULL防止错误调用
fopen函数打开文件失败,会返回空指针
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));//用该函数打印错误信息
return 0;
}
//1.读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));//用该函数打印错误信息
return 0;
}
//2.写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
strerror函数在这篇博客里面有讲解点我
通过这张表格,我们可以了解一下文件使用方式的不同类型
用w写入的时候,会覆盖原本已有内容。如果需要在已有内容后面追加,需要使用a
这里还有一个小知识点:C语言程序,运行的时候会默认打开3个流
在执行输入输出操作的时候,之前我们是直接将内存中的数据printf打印到屏幕上
现在我们可以通过文件指针,将数据输入到标准输出流,达到类似printf的效果
上述代码中,用到了fputc
函数,这个函数的作用是将一个字符输入到文件中
下表列出了一些我们会用到的文件函数
fputc函数:向文件中写入单个字符
fgetc函数:从文件中读取单个字符
可以看到,我们把刚刚文件中写入的字符全部打印出来了
将一个文件的内容拷贝到另外一个文件中
int main()
{
//实现一个代码将data.txt 拷贝一份 生成data2.txt
FILE* pr = fopen("data.txt", "r");
if (pr == NULL)
{
printf("open for reading: %s\n", strerror(errno));
return 0;
}
FILE* pw = fopen("data2.txt", "w");
if (pw == NULL)
{
printf("open for writting: %s\n", strerror(errno));
fclose(pr);
pr = NULL;
return 0;
}
//拷贝文件
int ch = 0;
while ((ch = fgetc(pr)) != EOF)
{
fputc(ch, pw);
}
fclose(pr);
pr = NULL;
fclose(pw);
pw = NULL;
return 0;
}
fputs函数:将字符串写入到文件中
//写一行
#include
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("hello world\n", pf);
fputs("hehe\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
运行代码,可以看到两行字符串已经被写入到了项目路径下的data.txt文件中
fgets函数:从文件中读取规定长度的字符串
该函数在使用的时候具有第3个参数,用于限制读取字符串的长度
读文件-读一行
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
char buf[1000] = {0};
//读文件
fgets(buf, 3, pf);
printf("%s\n", buf);
fgets(buf, 3, pf);
printf("%s\n", buf);
fclose(pf);
pf = NULL;
return 0;
}
运行程序,可以看到我们设置的是3,却只读取了2个字符出来
将buf[2]
更改为1,调试查看
可以看到,在执行第一个fgets
函数后,原本的1被写入成了\0
这就证实:fgets函数在读取字符的时候,会以\0
作为结尾
如果我们需要读取3个字符,就需要将限制设置为4
这里的“格式化”指的是结构体这种具有特定格式的数据内容
fprintf函数:将格式化数据写入文件中
#include
//……
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三", 20, 95.5 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写格式化的数据
fprintf(pf, "%s %d %lf", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
fscanf函数:从文件中读取格式化数据,存放到对应结构体变量s中
使用该函数的时候需要使用**“rb”,“wb”**来打开文件
fwrite(s, sizeof(struct Stu), 2, pf);
//s 来源
//sizeof 需要写入元素的大小
//2 需要写入元素的个数
//pf 写入的目标文件指针
以下是写入结构体变量的例子
struct Stu
{
char name[20];
int age;
double d;
};
//二进制的写
int main()
{
struct Stu s[2] = { {"张三", 20, 95.5} , {"lisi", 16, 66.5}};
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//按照二进制的方式写文件
fwrite(s, sizeof(struct Stu), 2, pf);
fclose(pf);
pf = NULL;
return 0;
}
可以看到,此时写入的数据已经部分成了乱码。这时候它的内容已经是用二进制存放的了,txt阅读器无法正确读出这些数据
二进制的读取就是复现这一步,将文本中的二进制数据以特定格式读取出来,并放入对应变量
fread(s, sizeof(struct Stu), 2, pf);
//s 存放文件内容的变量
//sizeof 需要读取元素的大小
//2 需要读取元素的个数
//pf 读取的目标文件指针
这两个函数比较特殊,它们的作用是将文件里面的格式化数据(如结构体)以字符串的形式拷贝到字符数组里面
见下图
https://cplusplus.com/reference/cstdio/fseek/?kw=fseek
该函数的作用是:将文件指针移动到相对于某个位置的特定偏移量的位置
听起来有点绕口,举例说明就知道了
给定一个字符串“abcdef”
每次使用一次fgetc,文件指针就会往后进一位。使用两次,文件指针指向的是字符c
如果我们需要指向f,就让指针
我们可以用该函数,定位文件指针,将其更改到我们需要的位置,进行字符替换等操作
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//定位文件指针
fseek(pf, -2, SEEK_END);
fputc('#', pf);//将当前字符替换成#
fclose(pf);
pf = NULL;
return 0;
}
返回文件指针当前的偏移量(相对于文件开头)
https://cplusplus.com/reference/cstdio/rewind/?kw=rewind
让文件指针的位置回到文件的起始位置
fseek(pf, 0, SEEK_SET);
//rewind函数与该fseek函数操作等价
//但是rewind更方便
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
int ret = ftell(pf);
printf("%d\n", ret);//2
rewind(pf);
//fseek(pf, 0, SEEK_SET);
ret = ftell(pf);
printf("%d\n", ret);//0
fclose(pf);
pf = NULL;
return 0;
}
我们现在已经知道了fread/fwrite函数可以实现二进制的输入输出,它们是怎么具体实现的呢?
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
在内存中,字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
对于数字10000,可以用下面两种方式存储
这时候使用二进制的方式,就能节省空间
用如下代码,将10000以二进制方式写入文件中
在VS中我们可以以特定打开方式二进制编辑器
打开test.txt
文档
可以看到10000是以二进制码的形式存放在文件中的
这里涉及到了大小端的问题点我
在一般情况下,我们可以直接判断fgets/fgetc
的返回值来判断文件是否读取结束。但这两个函数在文件读取错误的时候,也会返回和文件读取结束
一样的结果,此时就需要判断文件是因为什么情况结束的了
此时就需要用到两个函数,ferror和feof
ferror函数:判断文件是否出现了读取错误。
// 来自https://cplusplus.com/reference/cstdio/ferror/
int ferror ( FILE * stream );
在函数描述里面可以看到,该函数会判断传入的文件流最近的一次文件读写是否出现了错误。其判断的是文件流中的errror indicator
错误描述符。
同时在函数释义里面,提到了错误会被clearerr/rewind/freopen
这三个函数清除
此函数用于判断文件流是否读取到了文件末尾
// https://cplusplus.com/reference/cstdio/feof/
int feof ( FILE * stream );
在函数的描述处,提到了流的内部指针可能指向的是文件结尾。但在尝试读取文件结尾之前,不一定会设置EOF指示符。这句话不是很好理解,看下图
指针只有读取了文件末尾,才能知道自己已经走到了文件末尾,从而设置EOF标识符
feof/ferror
的先后顺序并没有特定的要求,因为它们两个的判断功能是不同的
依照读取退出的不同情况,可以分为下面几种类型
理论上而言,我们应该先用feof判断:因为读取到文件尾退出是没有问题的;
只有feof判断没有到文件尾部,而读取函数又退出了,才需要我们用ferror进行错误的判断。
if(feof(fp))
printf("EOF reach");
else if(ferror(fp))
printf("error while readfile");
实际上,这两个函数的返回值是互斥的
所以,这两个函数的先后顺序并不会相互影响
上课时,老师告诉我们,并不推荐在读取文件过程中,使用feof函数的返回值来判断文件是否读取结束,如下
while(!feof(fp))//用feof的返回值判断文件是否读取结束
{
c = fgetc(fp);
printf("%c",c);
}
fgetc
函数本身的返回值就拥有此功能,更推荐在fgetc的循环结束后,用feof函数判断是读取失败结束,还是遇到文件结尾正常结束
//1. 文件打开操作
//2. fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
printf("\n");
//3. 判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
但在我查阅资料的时候,看到C语言中文网站上使用了feof来判断文件是否读取结束。我感觉C语言中文网里面说的并没有啥问题,单看feof的函数作用,也确实能用来判断文件是否读取完毕。
后来经过询问,得知老师的意思是:不能只用
feof来判断文件是否正常读取结束。必须要将feof和ferror结合起来。包括上面提供的示例代码中步骤3也是同时使用了ferror和feof!
文本文件读取是否结束,判断读取函数fgetc/fgets
的返回值
退出读取文件的循环后,用feof和ferror判断是否出现了错误
二进制文件的读取结束,则需要判断返回值是否小于实际要读的个数:
文件中的数据不足SZIE字节
文件读取遇到错误
再次说明,feof和ferror使用的先后顺序没有明确区别和硬性要求,我们只需要根据自己的场景,给错误判断加上对应的提示信息即可!
ANSIC 标准采用“缓冲文件系统”处理数据文件。
所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
这就和git一样,是先将需要push的文件放入缓存区,确认文件无误后再push到远程仓库中
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小是C编译系统(编译器)决定的。
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文 件。 如果不做,可能导致读写文件的问题。
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
运行程序,通过sleep函数暂停程序,可以看到刚开始字符串并没有存入文件中
而是先写入输入缓存区,刷新缓存区后,才写入txt文件
#include
#include
int main()
{
while (1)
{
printf("hehe\n");
//在linux环境中,不带'\n'的时候,并不会打印(没有刷新缓存区)
//而在VS环境中,带不带都会正常打印
Sleep(1000);//linux环境中,sleep函数的参数,单位是秒(VS是毫秒)
// linux环境下,sleep函数需要小写,VS下是Sleep
}
return 0;
}
在Linux环境下(树莓派)测试这个代码
可以看到,去掉\n
后,代码并不会打印hehe
编译的时候,遇到报错,但是程序依旧编译出来了
implicit declaration of function ‘sleep’
CSDN查了查,发现是需要引用头文件#include
重新编译,没有报错了(此处hehe已经加了\n
,程序正常打印)
文件章节的内容非常丰富,你学费了吗!
大多数内容还是需要我们多多操作来熟悉它的真正作用
如果内容有误,还请大佬无情指正!