一.文件指针
1.缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”; 2.每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等);3.这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。
下面是在 vs2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
代码语言:javascript
复制
struct _iobuf
{
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的编译器结构体的内容可能有些不一样,但都大同小异;
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。 一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面创建一个文件指针变量:
1.定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量);2.通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
例如:
二.文件的打开和关闭
1.文件的打开
打开文件我们需要使用到 fopen 函数;
让我们看看 fopen 在库函数中的声明:
1.需要头文件 <stdio.h>; 2.参数 const char *filename : 要打开的文件的文件名 ;3.参数 const char *mode :表示要打开的模式;4.返回值:返回一个文件指针,若文件打开失败则返回一个空指针;
注意(文件扩展名):有些小伙伴们打开文件的时候,认为自己的文件名是正确的,但编译器却显示没有这个文件,这个时候不要质疑电脑,电脑是不会出错的,遇到这种情况我们应检查文件管理器的文件扩展名是否开启,如果没有打开那打开就行了,你会发现有些文件名称发生了变化;
详情如图:
文件打开模式:
2.文件的关闭
关闭文件需要使用到函数 fclose ;
下面来看看 fclose 在库函数中的声明:
1.参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流;2.返回值:若文件关闭成功,则返回0; 若文件关闭失败,则返回EOF;
实例:
代码语言:javascript
复制
int main()
{
FILE* pf = fopen("test.txt", "w"); //以只写的方式打开文件
if (pf == NULL) //判断文件是否打开成功
{
perror("fopen"); //若打开失败,则显示错误信息
return 0;
}
//写文件
//.......
//关闭文件
fclose(pf);
pf = NULL; //将文件指针置空,防止野指针的使用
return 0;
}
三.文件的读写
1.读与写,输出与输入的概念
2.流的概念
可以看到流其实是一个极其抽象的概念,我们可以把它理解成C程序与外部设备进行交流的一个媒介;
3.字符的读与写 fgetc 与 fputc
字符的读取函数 fgetc :
1.参数 FILE *stream :这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流;2.返回值:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
字符的写入函数 fputc :
1.描述 :把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动;2.参数 int char :这是要被写入的字符。该字符以其ASCII 值进行传递;3.参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流;4.返回值:该函数以无符号 char 强制转换为 int 的形式(即字符的ASCII值)返回写入的字符,如果发生错误则返回 EOF。
实例:
代码语言:javascript
复制
int main()
{
FILE* pf = fopen("test.txt", "w"); //以只写的方式打开文件
if (pf == NULL)
{
perror("fopen");
return 0;
}
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++) //向文件写入26个小写英文字母
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
FILE *pfread = fopen("test.txt", "r"); //以只读的方式打开文件
if (pfread == NULL)
{
perror("fopen");
return 0;
}
while (ch!= EOF) //判断是否读取到文件末尾
{
ch = fgetc(pfread); //从文件中读取字符
printf("%c", ch); //打印读取道德字符
}
fclose(pfread);
pfread = NULL;
return 0;
}
打印结果:
4.文本行的读与写 fgets 与 fputs
文本行的读取 fgets :
1.描述: 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定;2.参数 char *str :这是指向一个字符数组的指针,该数组存储了要读取的字符串;3.参数 int n : 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度;4.参数 FILE *stream :这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流;5.返回值:如果成功,该函数返回相同的 str 参数;如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针;如果发生错误,返回一个空指针;
文本行的写入 fputs :
1.描述:把字符串写入到指定的流 stream 中,但不包括 '\0';2.参数 const char *str : 这是一个数组,包含了要写入的以 '\0' 终止的字符序列;3.参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流;4.返回值:该函数返回一个非负值,如果发生错误则返回 EOF。
实例:
代码语言:javascript
复制
int main()
{
char str[] = "abcdefg";
FILE* pf = fopen("test.txt", "w"); //以只写的方式打开文件
if (pf == NULL) //判断是否打开成功
{
perror("fopen");
return 0;
}
fputs(str, pf); //向文件中写入 str
fclose(pf);
pf = NULL;
FILE* pfread = fopen("test.txt", "r"); //以只读的方式打开文件
if (pfread == NULL) //判断时候打开成功
{
perror("fopen");
return 0;
}
char tmp[20] = { 0 };
printf("%s\n", fgets(tmp, 20, pfread)); //将读取到的字符串存入 tmp 中,并打印
fclose(pfread); //关闭文件
pfread = NULL;
return 0;
}
打印结果:
5.二进制的读与写 fread 与 fwrite
二进制的读取 fread :
1.描述:从给定流 stream 读取数据到 ptr 所指向的数组中;2.参数 void *ptr :这是指向带有最小尺寸 size*nmemb 字节的内存块的指针;3.参数 size_t size :这是要读取的每个元素的大小,以字节为单位;4.参数 size_t nmemb :这是元素的个数,每个元素的大小为 size 字节;5.参数 FILE *stream : 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流;6.返回值 :成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾,即如果返回值小于元素个数 ,则发生了错误或读到文件末尾。
二进制的写入 fwrite :
描述:把 ptr 所指向的数组中的数据写入到给定流 stream 中;各参数的意思和 fread 一致,只不过是把读取换成了写入;返回值:如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。
实例:
代码语言:javascript
复制
int main()
{
int a = 10000;
FILE* pfwrite = fopen("test.bin", "wb"); //以二进制写的方式打开文件
if (pfwrite == NULL)
{
perror("fopen");
return 0;
}
fwrite(&a, sizeof(int), 1, pfwrite); //写入文件
fclose(pfwrite);
pfwrite = NULL;
FILE* pfread = fopen("test.bin", "rb"); //以二进制读的方式打开文件
if (pfread == NULL)
{
perror("fopen");
return 0;
}
int b = 0;
fread(&b, sizeof(int), 1, pfread); //读取文件
printf("%d\n", b);
fclose(pfread);
pfread = NULL;
return 0;
}
程序运行起来成功打印了10000,但当我们打开记事本看这个文件时却是一个看不懂的符号; 这是因为我们是以二进制的方式写的文件,所以才会显示这样,我们可以用可以查看二进制文件的软件查看,例如 vs2022 就可以查看二进制文件;
四.文件结束的判定
1.被错误使用的 feof
首先牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。 而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。所以 feof 是用来判断文件是什么原因结束的。
2.一些判断文件结束的总结
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets ) 例如: fgetc 判断是否为 EOF . fgets 判断返回值是否为 NULL . 2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。 例如: fread判断返回值是否小于实际要读的个数。
例子:
代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
int main()
{
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);
fp=NULL;
return 0;
}
五.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。 如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。 缓冲区的大小根据C编译系统决定的。
实例:
代码语言:javascript
复制
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
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;
}
结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。