C语言--文件操作
第16讲:文件操作
1. 为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
2. 什么是文件?
磁盘(硬盘)上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端键盘输入数据,运行结果显示在显示器上。
其实有时候我们会把信息输出到磁盘(磁盘的文件中)上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
文件路径:c:\code\
文件名主干:test
文件后缀:.txt
为了方便起见,文件标识常被称为文件名。
3. 二进制文件和文本文件?
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节。
测试代码:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");//wb:⼆进制的形式写到⽂件中
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
在VS上打开二进制文件:
1000在二进制文件中(显示数据在内存中的存储形式(以字节为单位的16进制格式并按照小端字节排序的形式)):(从00000000之后才是数据内容)

由于0000 0000 0000 0000 0010 0111 0001 0000在内存中以小端字节序的存储方式存储,所以在内存中的存储形式就是10 27 00 00
注意:因为计算机在内存中以字节为单位以16进制为格式存放数据,一个字节为8个比特位,所以这里的小端存储也是以字节为单位逆着排序,而一个字节内部的顺序并不发生调换,所以二进制中的10 27进过小端存储的调整后并没有变成01 72
4. 文件的打开和关闭
4.1 流和标准流
4.1.1 流
我们程序的数据需要输出到各种外部设备(文件、光盘、软盘、U盘···),也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对不同的外部设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
stdin
- 标准输入流,在大多数的环境中从键盘输入,scanf
函数就是从标准输入流中读取数据。stdout
- 标准输出流,大多数的环境中输出至显示器界面,printf
函数就是将信息输出到标准输出流中。stderr
- 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr三个流的类型是:FILE
,通常和你为文件指针。
C语言中,就是通过FILE*
的文件指针来维护流的各种操作的。
4.2 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件(包括外部设备)都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名 FILE
。
例如,VS2013编译环境提供的stdio.h
头文件中有以下的文件类型申明:
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指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
每一个文件都有一个属于自己的文件信息区,文件放在硬盘中,而文件信息区在内存中。
比如:
4.3 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用fopen
函数来打开文件,fclose
来关闭文件。
//打开⽂件
/*
参数:filename:文件名
mode:打开方式
返回值:文件信息区的地址(如果打开文件成功返回有效指针,打开文件失败则返回NULL)
*/
FILE * fopen ( const char * filename, const char * );
//关闭⽂件
int fclose ( FILE * stream );
下面对于“读”“写”的对象,应该有所区分:
mode表示文件的打开模式,下面都是文件的打开模式:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r” (只读) | 为了输入数据,打开一个文本文件 | 出错 |
“w” (只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件夹 |
“a” (追加) | 向文本文件尾部添加数据 | 建立一个新的文件夹 |
“rb” (只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb” (只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件夹 |
“ab” (追加) | 向一个二进制文件尾部添加数据 | 建立一个新的文件夹 |
“r+” (读写) | 为了读取和写,打开一个文本文件 | 出错 |
“w+” (读写) | 为了读取和写,建立一个新的文本文件 | 建立一个新的文件夹 |
“a+” (读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件夹 |
“rb+” (读写) | 为了读取和写,打开一个二进制文件 | 出错 |
“wb+” (读写) | 为了读取和写,建立一个新的二进制文件 | 建立一个新的文件夹 |
“ab+” (读写) | 打开一个二进制文件,在文件尾进行读写和写 | 建立一个新的文件夹 |
补充:
- “W”:如果文件名存在,会清空文件中的内容。如果文件名不存在,会新建一个此文件名的文件。
- 如果需要找电脑中别的位置的文件,需要写详细文件的路径,并注意转义字符。
C:\Users\Administrator\Desktop\test.txt
(绝对路径).\\..\\..\\test.txt
(相对路径)- .表示当前路径
- …表示上一级路径
- 上面的路径含义为:当前路径底下的上一级路径的上一级路径中的test.txt文件
实例代码:
/* fopen fclose example */
#include <stdio.h>
int main ()
{
FILE * pFile;
//打开⽂件
pFile = fopen ("myfile.txt","w");
//⽂件操作
if (pFile!=NULL)
{
fputs ("fopen example",pFile);
//关闭⽂件
fclose (pFile);
pFile = NULL;
}
return 0;
}
5. 文件的顺序读写
在文件中按照顺序读写里面的数据。
5.1 顺序读写函数介绍
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数(读字符) | 所有输入流 |
fputc | 字符输出函数(写字符) | 所有输出流 |
fgets | 文本行输入函数(读字符串) | 所有输入流 |
fputs | 文本行输出函数(写字符串) | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
补充:
- 上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。
- 上面函数中前6个读写的都是文本信息,并且均适用于所有输出/入流;最后两个读写的是二进制信息,但是仅适用于文件输出/入流。
5.1.1 fgetc和fputc
fgetc函数是在文件顺序读写中用于将文件中的字符一个一个读出的函数。
int fgetc(FILE *stream);
在使用fgect函数读取文件的时候会遇到两种情况:
- 文件未到末尾,成功读取文件,将读取到的字符的ASCII码值返回
- 文件未到末尾,读取文件失败,返回EOF(end of file(文件的结束标志))即为-1,便于程序员检查
代码示例:
//代码1:将项目路径底下的test.txt文件中前四位读取出来
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件的内容(事先向文件中输入abcdef)
printf("%c\n", fgetc(pf));//a
printf("%c\n", fgetc(pf));//b
printf("%c\n", fgetc(pf));//c
printf("%c\n", fgetc(pf));//d
//关闭文件
fclose(pf);
return 0;
}
//代码2::将项目路径底下的test.txt文件中a~z全读取出来
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件内容
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fputs函数是在文件顺序读写中用于在文件中一个字符一个字符写入文件的函数。
int fputc(int character, FILE *stream);
在使用fputs函数的时候也存在两种情况:
- 文件写入成功,则返回字符的ASCII码值
- 文件写入失败,则返回EOF
代码示例:
//代码:向项目路径底下的test.txt文件中写入a~z
int main()
{
FILE*pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
**补充:**标准输入/出流的使用
stdin为标准输入流,但是以读的形式打开文件就是文件输入流。
stdout为标准输出流,但是以写的形式打开文件就是文件输出流。
int main()
{
int ch = fgetc(stdin);//从键盘(标准输入流)上读取
fputc(ch, stdout);//将字符输出(写)到屏幕(标准输出流)
return 0;
}
5.1.2 fgets和fputs
fgets函数原型如下:
char *fgets(char *str, int num, FILE *stream);
- **函数功能:**从指定的文件流中获取字符串,并将其存储到指定位置
- 参数:
- str:用于存储所读到的字符串的位置
- num:要读取的字符串的字符个数
- stream:所要读取的文件的文件指针
- 返回值:
- 调用成功会返回用于储存数据的位置的起始地址
- 读取过程中发生错误,或是读取到了文件末尾,则返回一个空指针(NULL)
代码示例:
//读出文件test.txt中的10个字符到数组arr中去,并打印出来
int main()
{
//1. 打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2. 读文件
char arr[20] = "xxxxxxxxxxxxx";//用于区分是不是fgetc函数读出的字符
fgets(arr, 10, pf);
printf("%s\n", arr);//在控制台上仅仅显示前9个字符
//3. 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意:fgets函数读取字符的过程中会出现两种情况:
- 在fgets函数读取到换行符(\n)时,则停止读取,此时带回的字符串中包含换行符,并在换行符后再加上一个空字符(\0)。
- 在fgets函数读取到num-1个字符都没有读取到\n时,则读取到num-1个字符,并在末尾加上一个空字符一同返回(共n个字符)。
fputs函数原型如下:
int fputs(const char *str, FILE *stream);
- **函数功能:**写字符串到指定的文件流中去
- 参数:
- str:要写入文件的字符串数组的首地址
- stream:要写入的文件的文件流
- 返回值:
- 函数调用成功,输出成功返回一个非负值
- 函数调用失败,输出时发现错误返回EOF(-1)
代码示例:
//在项目路径底下的test.txt文件中输入“I am a student”"Are you OK??"
int main()
{
//1. 打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2. 写文件
fputs("I am a student\n", pf);
fputs("are you ok??", pf);
//3. 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
同样以上两个函数fgets和fputs也是适用于所有数据流,可以用键盘输入或者在屏幕上输出。
代码示例:
//在控制台上用键盘输入什么,敲回车之后控制台就会显示什么
int main()
{
char arr[20] = { 0 };
fgets(arr, 20, stdin);
fputs(arr, stdout);
return 0;
}
5.1.3 printf
/fprintf
/sprintf
-
printf函数原型:
int printf(const char *format, ...);
-
fprintf函数原型:
int fprintf(FILE *stream, const char *format, ...);
-
sprintf函数原型:
int sprintf(char *str, const char *format, ...);
①、经过观察前两个函数可以看出函数的参数部分只有首个参数不同,因为首个参数表示对某种流进行操作。printf默认操作的是stdout,fprintf操作的是所有输出流,所以在使用fprintf的时候只需要在使用printf函数的基础上加上一个参数即可。同时因为所有输出流包含标准输出流stdout,也就说明printf就是fprintf的一种。
代码示例:
//将下面的结构体分别打印在test.txt文件中和屏幕上
int main()
{
struct S s = { "lisi", 18, 88.0f };
//1. 打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2. 写文件
fprintf(pf,"%s %d %.1f", s.name, s.age, s.score); //在文件中打印数据
printf("%s %d %f", name, age, score); //在屏幕上打印数据
//3. 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
观察可知sprintf的第一个参数是一个字符指针。而因为printf函数是将后面格式化的数据打印在屏幕上,fprintf函数是将后面格式化的数据写入文件或者其他流中去,同理sprintf函数是将后面格式化的数据写入str指针指向的字符串中去。所以不管sprintf函数后面的格式化数据为任何类型(int/float/结构体···)都会被放进字符串中去,可以理解为将格式化的数据转化成字符串。
在后续的使用中,若遇到需要将许多各种乱七八糟数据类型的数据转化为字符类型可以使用函数sprintf。
代码示例:
struct S
{
char name[20];
int age;
float score;
};
int main()
{
char arr[100] = { 0 };
struct S s = { "wangwu", 23, 66.6f };
//临时变量
struct S tmp = { 0 };
//将s中的各个数据转换成字符串,存放在arr中
sprintf(arr, "%s %d %f", s.name, s.age, s.score);
//printf("%s\n", arr);
//从字符串arr中提取格式化的数据,存放在tmp中
sscanf(arr, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
②、同时这三个函数的参数最后都有一个**…(可变参数列表)**,表示此函数的参数数量是并不固定的是可变的,如下
printf("hehe\n"); //1个参数
printf("%d", 100); //1个参数
printf("%d %c", 100, 'x'); //3个参数
printf("%d %c %f", 100, 'x', 3.14f); //4个参数
5.1.4 scanf
/fscanf
/sscanf
-
scanf函数原型:
int scanf(const char *format, ...);
-
fscanf函数原型:
int fscanf(FILE *stream, const char *format, ...);
-
sscanf函数原型:
int sscanf(const char *s, const char *format, ...);
①、先看前两个函数可以看出函数的参数部分只有首个参数不同,因为首个参数表示对某种流进行操作。scanf默认操作的是stdin,fscanf操作的是所有输入流;所以在使用fscanf的时候只需要在使用scanf函数的基础上加上一个参数即可。
代码示例:
//分别从键盘和和文件中读取数据,并将其显示在屏幕上
int main()
{
struct S s = {0};
//1. 打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2. 读文件
//从键盘中获取信息,放入结构体s的各个成员中
scanf("%s %d %f", s.name, &(s.age), &(s.score));
//从文件中读取信息,存放到s的各个成员中
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
//下面两种方法均可将数据打印在屏幕上
printf("%s %d %.1f\n", s.name, s.age, s.score);
fprintf(stdout, "%s %d %.1f\n", s.name, s.age, s.score);//直接对标准输出流操作
//3. 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
经过了上面sprintf函数的介绍,可以知道sscanf函数是一个相反的过程,他会将字符串arr中提取格式化的数据放在程序指向的对应空间去。
代码示例:
//先将结构体s中的格式化数据放入字符串arr中,再从arr中提取格式化的数据放入指定空间中,并显示在屏幕上
struct S
{
char name[20];
int age;
float score;
};
int main()
{
char arr[100] = { 0 };
struct S s = { "wangwu", 23, 66.6f };
//临时变量
struct S tmp = { 0 };
//将s中的各个数据转换成字符串,存放在arr中
sprintf(arr, "%s %d %f", s.name, s.age, s.score);
//printf("%s\n", arr);
//从字符串arr中提取格式化的数据,存放在tmp中
sscanf(arr, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
总结:
scanf/printf 针对标准输入流/标准输出流的**格式化(在输入和输出数据时需要指定格式)**输入/输出函数
fscanf/fprintf 针对所有输入流/所有输出流的格式化输入/输出函数(可以针对文件也可以针对标准流)
sscanf 将格式化的数据写到字符串中,也可以理解为将格式化的数据转换成字符串
sprintf 从字符串中提取格式化的数据,也可以理解为将字符串转换成格式化的数据
5.1.5 fread和fwrite
fwrite函数原型如下:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
- **函数功能:**将数据从内存写入指定的文件流中(以二进制的形式将数据写进文件流中)
- 参数:
- ptr:要写入文件的数据的指针
- size:要写入文件的数据的字节大小
- count:要写入的元素的数量
- stream:表示文件流,指定了将数据写入的目标文件
- **返回值:**写入成功的元素数量
- 情况一:写入失败,返回值将小于count。如果发生错误或者到达文件末尾,会返回实际写入的元素数。
- 情况二:写入成功,返回值等于count。
代码示例:
//代码1:将结构体中的数据以二进制的形式写入文件中
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "cuihua", 25, 88.8f };
//以二进制的形式写到文件中
//1. 打开文件
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.写文件
fwrite(&s, sizeof(struct S), 1, pf);
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
**补充:**在代码运行成功后,打开“test.txt”文件,会发现里面会出现乱码,这是因为我们的数据是以二进制的形式写入文件,所以使用文本编辑是没有办法看懂的。
fread函数原型如下:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
这个函数和上面的fwrite函数十分相似,fwtite函数是把ptr中大小为size的count个数据写入到stream。
所以fread函数是读出steam中的count个大小为size的数据存放到ptr这块空间中。同样fread函数的返回值也是成功读取的元素个数。
代码示例:
//代码2:将已经通过代码2写入文件的二进制数据,在读取出打印在屏幕上
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {0};
//读取二进制的信息写到文件中
//1. 打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
//打印数据看是否成功读取
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
**补充:**代码成功运行之后可以将上面的结构体数据打印在屏幕上,这样也就可以读出代码2写到文件中的二进制数据。
6. 文件的随机读写
在打开文件时,光标始终留在第一位如果这时候进行读写的部分并不是程序员想要操作的部分,如果要实现跳过部分文件内容,不按照文件中数据的排列顺序进行读写就需要就需要将文件光标移动到想要操作的数据前在进行读写,这其中就涉及下面文件的随机读写中的内容。
6.1 fseek
fseek函数原型如下:
int fseek(FILE *stream, long int offset, int origin);
- **函数功能:**根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。
- 参数:
- stream:需要操作的文件流
- offset:想要操作的光标位置距origin的偏移量(向右偏为正,向左偏为正)
- origin:起始位置
- SEEK_SET:文件的起始位置
- SEEK_CUR:文件指针当前的位置
- SEEK_END:文件的末尾
- 返回值:
- 返回值为 0:表示成功,文件位置指针已成功移动到新的位置。
- 返回值为非 0 值:表示失败,文件位置指针未能正确移动,具体失败的原因可以通过
errno
进行检查。
代码示例:
//在文件中操作,在"This is an apple."改为输出“This is a sample.”
/* fseek example */
#include <stdio.h>
int main ()
{
//1. 打开文件
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
//2. 写文件
fputs ( "This is an apple." , pFile );//放入数据
fseek ( pFile , 9 , SEEK_SET );//移动文件光标
fputs ( " sam" , pFile );//操作数据
//3. 关闭文件
fclose ( pFile );
pFile = NULL;
return 0;
}
6.2 ftell
ftell函数原型如下:
long int ftell(FILE *stream);
- **函数功能:**返回文件指针相对于起始位置的偏移量。
- 参数:
- stream:需要操作的文件流
- 返回值:
- 返回值为 -1:表示发生错误。
- 返回值为非 0 值:正常情况,返回值是文件当前位置相对于文件开头的字节数,即输出文件指针相较于文件起始位置的偏移量。
例子:
//算出文件中的的数据有多少个字节
/* ftell example : getting size of a file */
#include <stdio.h>
int main ()
{
//1. 打开文件
FILE * pFile;
long size;
pFile = fopen ("myfile.txt","rb");
if (pFile==NULL)
perror ("Error opening file");
else
{
//2. 读文件
fseek (pFile, 0, SEEK_END); //直接定位到文件末尾
size=ftell (pFile);
//3. 关闭文件
fclose (pFile);
pFile = NULL;
printf ("Size of myfile.txt: %ld bytes.\n",size);
}
return 0;
}
6.3 rewind
rewind函数原型如下:
void rewind(FILE *stream);
- **函数功能:**让文件指针的位置重新定位,回到文件的起始位置。
- 参数:
- stream:需要操作的文件流
- **返回值:**无
例子:
//将A~Z的二进制形式显示在屏幕上
/* rewind example */
#include <stdio.h>
int main ()
{
int n;
FILE * pFile;
char buffer [27];
//1. 打开文件
pFile = fopen ("myfile.txt","w+");
//2. 操作文件
for ( n='A' ; n<='Z' ; n++)
{
fputc ( n, pFile);
}
rewind (pFile);
fread (buffer,1,26,pFile);
//将写入文件的A~Z,以二进制的形式读出并存放到数组buffer中去
fclose (pFile);
pFile = NULL;
buffer[26]='\0';
printf(buffer);
return 0;
}
7. 文件读取结束的判定
7.1 被错误使用的 feof
牢记:在文件读取过程中,不能用 feof
函数的返回值直接来判断文件是否结束。
feof
的作用是:当文件读取结束时,判断是读取结束的原因是否是:遇到文件尾结束。
feof函数原型如下:
int feof(FILE *stream);
- 返回值:
- 返回值为非零值:表示文件已经到达末尾。具体来说,如果文件流的当前位置已经到达文件的末尾,
feof()
会返回一个非零值(通常是1)。 - 返回值为0:表示文件并未到达末尾。也就是说,文件指针并不在文件的末尾,仍然可以继续读取数据。
- 返回值为非零值:表示文件已经到达末尾。具体来说,如果文件流的当前位置已经到达文件的末尾,
例子:
- 利用feof函数检测文本文件读取是否结束,可以判断返回值是否为
EOF
(fgetc
),或者NULL
(fgets
)
例如:
fgetc
判断是否为EOF(表示出现错误)
fgets
判断返回值是否为NULL(表示出现错误)
- 利用feof函数检测二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
- fread判断返回值是否小于实际要读的个数(出现读取错误时会出现这种情况)。
文本文件的例子:
//读取文件"test.txt"中的内容并显示在屏幕上,并附上最后读取结束的原因
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
//1. 打开文件
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
//2. 操作文件
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环 读取文件中的内容
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp)) //检测是否是文件IO的错误
{
puts("I/O error when reading");
}
else if (feof(fp)) //检测是否读取到文件末尾
{
puts("End of file reached successfully");
}
//3. 关闭文件
fclose(fp);
fp = NULL;
}
在第22行引出了一个新的函数ferror函数,函数原型如下:
int ferror(FILE *stream);
- **函数功能:**用于检查文件流是否发生了错误。
- 参数:
- stream:要操作的文件流
- 返回值:
- 返回值为非零值:表示在文件操作过程中发生了错误。
- 返回值为0:表示没有发生错误,文件流操作正常。
根据以上对ferror函数的介绍可以得知,在上述代码进行文件错误类型的判读中可以知道,当代码运行起来的时候,可以根据在屏幕上返回的错误原因得出读取文件结束的原因是什么。
如果是文件IO出现错误则ferror函数会返回一个非零值,此时在屏幕上会显示错误原因为**“I/O error when reading"如果是已经读取到文件末尾则feof函数会返回一个非零值,此时会在屏幕上显示错误原因为"End of file reached successfully”**。
二进制文件的例子:
//将1.0 2.0 3.0 4.0 5.0以二进制形式存储到文件"test.bin"中去,并再以二进制的形式读取这个文件,如果正确读取将读取的内容打印到屏幕上,如果读取错误将错误原因打印到屏幕上
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};//1. = 1.0
//1. 打开文件
FILE *fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
//2. 操作文件
fwrite(a, sizeof *a, SIZE, fp);
//将来自于a这个空间的大小为(sizeof *a)的SIZE个数据写入到fp指向的文件流中去
//3. 关闭文件
fclose(fp);
fp = NULL;
double b[SIZE];
//1. 打开文件
fp = fopen("test.bin","rb");
//2. 操作文件
size_t ret_code = fread(b, sizeof *b, SIZE, fp);
// 将fp指向的文件流中的大小为(sizeof *a)的SIZE个数据读取到空间b中
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)) //文件IO的错误
{
perror("Error reading test.bin");
}
}
//3. 关闭文件
fclose(fp);
fp = NULL;
}
8. 文件缓冲区
ANSI C标准采用**“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”**。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
**文件缓冲区存在的意义:**因为每次程序数据区和硬盘之间进行数据交换都需要调用操作系统的接口,如果每交换一个数据就调用一次这样频繁的调用会增加操作系统的负担,并且使得操作系统没有办法处理其他程序。因为引用了数据缓冲区,假设每交换五个数据再调用一次操作系统的接口进行数据传输,这样就可以使得操作系统有时间去处理其他程序,增加操作系统的效率。
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);//单位为ms,即10s
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者文件操作结束的时候关闭文件。如果不做,可能到导致读写文件的问题。
9. 文件操作的练习
9.1 两个文件之间的拷贝
通过文件操作将文件“test1.txt”的内容拷贝到“test2.txt”中去。
#include <stdio.h>
int main()
{
//1. 打开文件
FILE* pRead = fopen("test1.txt","r");
if(pRead == NULL)
{
perror("fopen\n");
return 1;
}
FILE* pWrite = fopen("test2.txt","w")
{
perror("fopen\n");
fclose(pRead);
return 1;
}
//2. 操作文件
while((ch = fgetc(pRead)) != EOF)//读取文本
{
fputc(ch,pWrite);//拷贝文本
}
//3. 关闭文件
fclose(pRead);
pRead = NULL;
fclose(pWrite)
pWrite = NULL;
return 0;
}