全文目录
引言
在上一篇文章中我们了解了文件的打开与关闭、文件的顺序读写的相关函数。
其实有一些内容是没有介绍到的。
比如流的相关知识;比如当读写结束后原因的判断,到底是因为读写到了末尾,还是读写错误;比如二进制文件与文本文件;比如文件的随机读写等一些相关内容。
在本文中就会一一介绍:
简单了解流
上一篇文章中其实提到过这个概念。我们提到过fgetc、fputc、fgets、fputs、fscanf、fprintf这些文件读写函数是适用于任何输入输出流的;而fread、fwrite只适用于文件流。并且提到了标准输入流(键盘)与标准输出流(屏幕)。
标准输出流的文件指针为stdout,类型为FILE*;标准输入流,它的文件指针为stdin,类型为FILE*。也就是说,这两个文件指针是可以作为参数,用fscanf与fprintf达到scanf与printf同样的效果的:
#include<stdio.h>
int main()
{
int n = 0;
fscanf(stdin, "%d", &n);
fprintf(stdout, "%d\n", n);
return 0;
}
FILE*不是文件指针吗,为什么流的类型也是FILE*?
其实上篇文章的表述是不是很清楚的。
我们平时讲的流,类似于水流、气流,就是物体在介质中的移动。例如水的移动就是水流,空气的移动就是气流。而我们C语言中提到的流其实就是数据的移动。
即,有方向的字节序列。
流向内存的数据称为输入流;流出内存的数据称为输出流。
我们可以将数据从键盘输入(标准输入流);输出到屏幕(标准输出流);也可以从硬盘上的文件中输入/输出;也可以输出到网络、打印机等流。
而为了统一对各种硬件的操作、简化接口,不同的硬件设备都被看成一个文件。对这些文件的操作等同于对硬盘中文件的操作,都要通过FILE*型的文件指针才能找到相应的流类。
文件指针是在打开文件之后,系统自动在内存中开辟的一块空间的指针。所以我们的C程序需要先通过函数打开相应的文件后再进行读写。
并且每一个C程序在运行时都会默认打开标准输出文件(stdout)、标准输入文件(stdin)、标准错误文件(stderr),也就是标准输出流、标准输入流与标准错误流。这样,我们才能在实现从键盘输入、输出到屏幕以及报错之前不需要人为打开文件。
到此,我们把上篇文章留的坑填了一下,接下来继续介绍剩下的内容。
文件的随机读写
之前我们介绍了文件顺序读写的一些函数:fgetc、fputc、fgets、fputs、fscanf、fprintf、fread、fwrite。这些函数在读或写完一个字符或字符串之后,其文件中的标识符就会向前移动相应的字节。这也就使在使用这些文件的时候只能够实现将文件中的内容从第一个字符读或写到最后一个字符。当需要读写的内容是乱序的时候就需要一些其他的函数:
fseek
fseek库函数可以实现改变流中标识符的位置
函数声明
fseek在头文件stdio.h中声明
它有三个参数:
第一个参数类型为FILE*。表示要改变标识符的流类;
第二个参数类型为long int。表示要将标识符移动offest个字符;
第三个参数类型为int。表示从什么位置移动offest个字节。它有三个固定的常量参数:SEEK_SET、SEEK_CUR、SEEK_END。分别表示从流的起始位置移动;从当前位置移动;从流的末尾位置移动。
需要注意的是:C标准是允许库不实现SEET_END的,所以使用SEET_END的代码不具有真正的可移植性。
返回值为int。如果移动成功,返回0;否则返回一个非0的值。
当在"w"打开文件的状态下使用fseek时,重新输入的字符会替换原字符。
函数使用
例如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
fputs("abcdefgh", pf);
fseek(pf, 1, SEEK_SET);
fputs("ijklmn",pf);
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
在这段代码中,先fopen以"w"打开文件,返回值赋给文件指针pf,并if判断是否打开成功。
然后再使用fputs,以常量字符串为参数,在文件中写入abcdefgh。
然后再fseek,将该文件中的标识符移动到起始位置的下一个字符。
再fputs写入ijklmn。
我们打开这个文本文件时就会发现,从a后面的6个字符被替换为ijklmn。
读取时也是同样的:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s[20] = { 0 };
fgets(s, 5, pf);
printf("%s\n", s);
fseek(pf, 1, SEEK_SET);
fgets(s, 5, pf);
printf("%s\n", s);
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
这段代码中,第一次打印是从起始位置开始读取4('\0’会占一个)个字符;第二次打印是从起始位置后的位置开始,读取4个字符。
ftell
ftell库函数可以告诉我们标识符现在的位置
函数声明
frell在头文件stdio.h中声明:
它只有一个参数,参数类型为FILE*。表示需要知道标识符位置的流的文件指针。
返回值类型为long int。返回获取到的标识符的位置(从起始位置算起的第几个)。
函数使用
例如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s[20] = { 0 };
fgets(s, 5, pf);
printf("%s\n", s);
printf("%d\n", ftell(pf));
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
在这段代码中,"r"打开test.txt文件,并判断是否打开成功。
再从文件中读取4个字符放在字符串s中,并打印。
然后使用ftell告知我们标识符的位置。前面已经读取了4个字符,所以现在标识符的位置就在第4个字符后。
rewind
rewind库函数可以将标识符移动到文件的起始位置
函数声明
rewind在头文件stdio.h中声明:
它只有一个参数,参数类型为FILE*。表示需要将标识符位置移动到起始位置的流的文件指针。
空返回值。
成功调用此函数后,与该流相关的文件结尾和错误内部指示符都将被清除。
函数使用
例如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s[20] = { 0 };
fgets(s, 5, pf);
printf("%s\n", s);
rewind(pf);
fgets(s, 5, pf);
printf("%s\n", s);
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
原本在读取4个字符后,标识符的位置应该在第4个字符后。这时使用rewind将标识符位置移动到起始位置,再次读取时,读取到的依然是前四个字符。
文件读取结束后的判定
之前我们提到,在对文件进行读写时,读写结束时有两种情况:文件读取到末尾或文件读取错误。读取结束后,我们可以使用一些库函数去获取文件读取结束的原因:
feof与ferror
feof可以判断文件是否由于读取完毕而结束。
文件在读取完毕后,系统会设置一种end-of-file标志,表示文件已经读写完毕。feof函数就是通过检查这个标志来判断文件是否读写完毕的;
而当文件读取错误时,系统会设置一种错误标志,表示文件读写出错。ferror函数就是通过检查这个错误表示来判断文件是否读写出错的。
需要注意的是,当我们通过fseek或rewind等向前移动标识符的位置的时候,这个eof标志与错误标志就会被删除。
函数声明
feof与ferror在头文件stdio.h中声明
feof有一个参数,参数类型为FILE*。表示要查询的文件的文件指针。
返回值类型为int。当查找到eof标志时,返回一个非0的值;否则返回0。
ferror有一个参数,参数类型为FILE*。表示要查询的文件的文件指针。
返回值类型为int。当查找到错误标志时,返回一个非0的值;否则返回0。
函数使用
例如:
首先,我们在文件中写入数据:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int nums[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int i = 0;
while (i < 10 && fwrite(nums + i, sizeof(int), 1, pf) == 1)
{
i++;
}
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
这段代码中,以"wb"的方式打开test.txt文件,并写入了1到10的数据。由于是二进制写入,所以在以文本文件打开时是无法读取的。
然后我们就读取这个文件中的数据,并判断是由于什么原因结束读取:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int n = 0;
while (fread(&n, sizeof(int), 1, pf) == 1)
{
printf("%d ", n);
}
if (feof(pf))
{
printf("文件已读取完毕\n");
}
else
{
if (ferror(pf))
{
printf("文件读取出错\n");
}
}
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
在这段代码中:
我们以"rb"的方式打开test.txt文件,读取我们刚刚写进去的内容。
在读取结束后退出循环时,我们使用库函数feof判断是否由于读取完毕结束。然后else中判断是否由于出错了而读取结束。
文本文件与二进制文件
我们在用"wb"新建一个二进制文件后,使用fwrite写入的内容是无法在文本文件中查看的,就像我们上面写的代码。
所以接下来就了解一下文本文件与二进制文件。
根据数据的组织形式,文件分为文本文件与二进制文件:
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;
如果在外存中存储前,将数据转换为ASCII码的形式存储,就是文本文件。
对于字符数据来说,存储时当然是以ASCII码的形式存储的,也就是说对于字符数据是不存在文本文件与二进制文件的区别的:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char c = 'a';
fwrite(&c, sizeof(char), 1, pf);
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
以二进制的形式写入,依旧可以看出来是a;
但对于整型数据:
我们可以将这个数据的二进制直接存储,这就是二进制文件;
而如果将组成这个整型数据的每一位都看成字符,并分别以ASCII码的形式存储,就是文本文件。
例如10000这个数据:
当我们以二进制文件的形式存入文件时,它的二进制补码等于原码,为:00000000 00000000 00100111 00010000。用十六进制表示就是10 27 00 00(小段存储)。
当然,我们使用记事本的方式打开是无法观察的,我们可以在编译器中以二进制的方式打开:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int c = 10000;
fwrite(&c, sizeof(int), 1, pf);
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
当我们以文本文件的形式存入文件时,就是将’1’、‘0’、‘0’、‘0’、'0’这四个字符的ASCII码值存入文件。即00110001 00110000 00110000 00110000 00110000。用十六进制表示就是31 30 30 30 30 。
当然,这种情况下以记事本打开是可以观察到10000的,我们还是在编译器中看一下这个数据的二进制数:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int c = 10000;
fprintf(pf, "%d", c);
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
文件缓冲区
ANSIC标准是采用“缓冲文件系统”处理数据文件的。
缓冲文件系统是指,系统自动为每个打开的文件,在内存中开辟一块文件缓冲区。在从内存向文件中输出、或从文件向内存中输入时,数据会先传到文件缓冲区中。
缓冲区的大小是由环境决定的。
当文件缓冲区充满或刷新缓冲区时,缓冲区中的数据才被传到文件或程序数据区中(变量中)。
我们在关闭文件时,也会刷新缓冲区,使数据传输到文件或程序数据区中。所以,我们在对文件读写后需要关闭文件,确保数据依旧成功传输。
总结
这篇文章中,我们了解了流、文件的随机读写、文件读写结束后的判定、文本文件与二进制文件、文件缓冲区的相关知识。
通过这两篇文章的介绍,相信大家对文件操作已经理解了。接下来就需要一些实战练习。
在接下来的几天中,将会更新三个C语言小实战练习:三子棋、扫雷、通讯录。通过这三个小练习,大家一定会对到现在所学到的知识理解更加清楚。希望大家持续关注哦。
最后,如果对本文有任何问题,欢迎在评论区进行讨论
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦