C语言中的文件操作
目录
一、为什么使用文件?
二、什么是文件?
2.1 程序文件
2.2 数据文件
2.3 文件名
三、二进制文件和文本文件
四、文件的打开和关闭
4.1 流和标准流
4.1.1 流
4.1.2 标准流
4.2 文件指针
4.3 文件的打开和关闭
五、文件的顺序读写
5.1 顺序读写函数介绍
5.1.1 fputc的使用
5.1.2 fgetc的使用
5.1.3 fputs的使用
5.1.4 fgets的使用
5.1.5 fprintf的使用
5.1.6 fscanf的使用
5.1.7 fwrite的使用
5.1.8 fread的使用
5.2 对比一组函数
六、文件的随机读写
6.1 fseek
6.2 ftell
6.3 rewind
七、文件读取结束的判定
7.1 被错误使用的feof
八、文件缓冲区
一、为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
二、什么是文件?
在程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
2.2 数据文件
文件的内容不⼀定是程序,⽽是程序运行时读写的数据,比如程序运⾏需要从中读取数据的文件,或者输出内容的⽂件。
本章讨论的是数据⽂件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输⼊数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上⽂件。
2.3 文件名
一个文件要有⼀个唯一的文件标识,以便用户识别和引用。
⽂件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,⽂件标识常被称为文件名。
三、二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者⼆进制文件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
⼀个数据在文件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使用⼆进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
测试代码如下所示:
# include <stdio.h>int main (){int a = 10000 ;FILE* pf = fopen( "test.txt" , "wb" );fwrite(&a, 4 , 1 , pf); // ⼆进制的形式写到⽂件中fclose(pf);pf = NULL ;return 0 ;}
在VS上打开⼆进制⽂件:
vs上打开二进制的方法
10000在二进制文件中
四、文件的打开和关闭
4.1 流和标准流
4.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对⽂件、画面、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。我们下面所使用的fopen就是打开一个文件流。
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 来关闭⽂件。
// 打开⽂件FILE * fopen ( const char * filename, const char * mode );// 关闭⽂件int fclose ( FILE * stream )
fopen需要两个参数,第一个参数是文件名,第二个参数是文件的打开方式。
如果打开成功就会返回与这个文件相关的文件信息区的指针,如果打开失败就会返回NULL,所以我们在打开文件后,需要额外在判断一下返回的文件指针是否为NULL;
fclose需要一个参数,就是文件指针。
文件使用方式 | 含义 | 如果文件不存在 |
---|---|---|
“r”(只读) | 为了输⼊数据,打开⼀个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开了一个文本文件 | 建立一个新文件 |
“a”(追加) | 向文本文件末尾添加数据 | 建立一个新文件 |
“rb”(只读) | 为了输⼊数据,打开⼀个二进制文本文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新文件 |
“ab”(追加) | 向一个二进制文件末尾添加数据 | 建立一个新文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新文件 |
实例代码:
# 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.1 顺序读写函数介绍
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输入流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输入流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输入流 |
frend | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
5.1.1 fputc的使用
fputc的函数原型如下:
int fputc ( int character, FILE * stream );
将character字符写入到文件流stream中,如下所示:我们将字符abc写到文件中。
函数的返回值是写入的字符,如果写入失败,就会返回EOF
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "w");if (pf == NULL) {perror("fopen:");return 1;}//写文件fputc('a', pf);fputc('b', pf);fputc('c', pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
这样我们就会把字符abc写到文件中了,我们查看文件,文件中有了abc这三个字符了。
现在我们想要将26个英文字符都存到文件中,如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "w");if (pf == NULL) {perror("fopen:");return 1;}//写文件char ch = 'a';while (ch <= 'z') {fputc(ch, pf);ch = ch + 1;}//关闭文件fclose(pf);pf = NULL;return 0;
}
我们查看文件就可以看见26个英文字母了。
5.1.2 fgetc的使用
fgetc的函数原型如下:
int fgetc ( FILE * stream );
从文件流中读取一个字符,如果成功读取字符的话,返回的是字符的ASCII值,如果读取遇到文件末尾,或者读取失败的时候就会返回EOF(-1)。
如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件char ch;ch = fgetc(pf);printf("%c ", ch);ch = fgetc(pf);printf("%c ", ch);ch = fgetc(pf);printf("%c ", ch);//关闭文件fclose(pf);pf = NULL;return 0;
}
那我们如何把上面存取的26个字符都读出来呢?如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件char ch;while ((ch=fgetc(pf)) != EOF) {printf("%c", ch);}//关闭文件fclose(pf);pf = NULL;return 0;
}
运行结果如下:
5.1.3 fputs的使用
fputs函数原型如下:
int fputs ( const char * str, FILE * stream );
这个函数是将一个str所指向的字符串或者一个字符串常量写到stream文件流中,如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "w");if (pf == NULL) {perror("fopen:");return 1;}//写文件fputs("hello world", pf);fputs("hello bit", pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
我们使用两次fputs函数,会将“hello world”和“hello bit”写到文件中,那现在我们猜一下文件中是一行还是两行呢?答案如下所示:
为什么文件中不是两行,而是一行呢?
原因是:我们在写入hello world后,光标会停在d的后面,下一次写就会从光标的位置开始写,如果我们想要每一次写字符串都新起一行,可以在字符串后面加一个\n换行符,如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "w");if (pf == NULL) {perror("fopen:");return 1;}//写文件fputs("hello world\n", pf);fputs("hello bit\n", pf);//关闭文件fclose(pf);pf = NULL;return 0;
}
文件中显示如下:
5.1.4 fgets的使用
fgets函数原型如下:
char * fgets ( char * str, int num, FILE * stream );
fgets有三个参数,它是指,从stream流中读取数据,读取最多num个字符,读到str所指向的内存中。
注意1:最多读取num个字节,但是最后一个包’\0’,所以相当于最多读取num-1个字节。如实例1:
注意2:如果文件中的数据小于num字符,他就只会读取这一行,\n也会被读进去,但是第二行是不会被读的。如实例2
注意3:如果读取成功,返回是是str地址,如果读取遇到文件末尾或者遇到错误,就会返回NULL.
实例1如下所示:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件char arr[10] = { 0 };fgets(arr, 10, pf);printf("%s", arr);//关闭文件fclose(pf);pf = NULL;return 0;
}
输出结果如下:
我们最多读取10个字符,但是实际上只读取九个字符,第10个字符是‘\0’。
实例2如下所示:现在文件中的内容如下所示:
我们读取10个字符:
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件char arr[20] ="xxxxxxxxxxxxxxxxxxx";fgets(arr, 10, pf);printf("%s", arr);//关闭文件fclose(pf);pf = NULL;return 0;
}
内存显示如下所示:
我们要读取10个字符,但是第一行只有5个字符,我们就会将换行符\n读进来,在加一个\0.
现在我们想要将文件中的多行数据都读出来,那应该怎么办呢:文件结构如下所示:
代码如下所示:
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件char arr[20] = {0};while (fgets(arr, 20, pf) != NULL) {printf("%s", arr);}//关闭文件fclose(pf);pf = NULL;return 0;
}
输出结果如下所示:
那么问题来了,如果我们的第一行很长呢?超过20个字符,还会按照文件的格式输出出来吗?答案是可以的。如下所示:
文件结构如下:
使用上面的代码输出如下:
也是按照文件的格式输出出来的,这是为什么呢?
原因:程序第一次读取20个字符,它就会读取19个字符,再加一个\0,第二次读取就会从第20个字符开始读,如果读取的字符没有换行符,输出的时候就不会换行,而当我们读取到换行符的时候,读取就结束了,下一行的内容是不会被读取的。 下一次读取就从下一行开始。
5.1.5 fprintf的使用
fprintf的函数原型如下:
int fprintf ( FILE * stream, const char * format, ... );
printf的函数原型如下:
int printf ( const char * format, ... );
我们可以看到这两个函数非常相似,只是fprintf多了一个文件流,所以我们在使用fprintf的时候可以参考printf。如下所示:
#include<stdio.h>
struct S {char name[20];int age;float score;
};
int main() {struct S s = { "zhangsan",10,78.0f };//想要把s中的数据存到文件中FILE* pf=fopen("text.txt", "w");if (pf == NULL) {perror("fopen:");return 1;}//写文件fprintf(pf,"%s %d %f", s.name, s.age, s.score);fclose(pf);pf = NULL;return 0;
}
文件内容如下所示:
5.1.6 fscanf的使用
fscanf的函数原型如下:
int fscanf ( FILE * stream, const char * format, ... );
scanf的函数原型如下:
int scanf ( const char * format, ... );
我们可以看到这两个函数也非常相似,只是fscanf多了一个文件流,所以我们在使用fscanf的时候可以参考scanf。如下所示:
#include<stdio.h>
struct S {char name[20];int age;float score;
};
int main() {struct S s = { 0 };//想要从文件text.txt中读取数据到s中FILE* pf=fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//读文件fscanf(pf,"%s %d %f", s.name, &s.age, &s.score);//打印到屏幕上printf("%s %d %f", s.name, s.age, s.score);fclose(pf);pf = NULL;return 0;
}
5.1.7 fwrite的使用
fwrite函数的原型如下:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
ptr--Pointer to the array of elements to be written, converted to a const void*.
ptr是指指向数组元素的指针
size--Size in bytes of each element to be written
size是指我要写的数据多少个字节
count--Number of elements, each one with a size of size bytes
count是指我要写多少个数据
stream--Pointer to a FILE object that specifies an output stream
如下所示:
#include<stdio.h>
int main() {int arr[] = { 1,2,3,4,5 };FILE* pf = fopen("text.txt", "wb");if (pf == NULL) {perror("fopen");return 1;}//写数据fwrite(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);//以二进制形式写//关闭文件fclose(pf);pf = NULL;return 0;
}
5.1.8 fread的使用
fread函数原型如下:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
ptr--Pointer to a block of memory with a size of at least (size*count) bytes, converted to a void*.
从流中读取数据放到ptr所指向的空间里面
size--Size, in bytes, of each element to be read
size是指一个数据多大
count--Number of elements, each one with a size of size bytes
count是指要读多少个数据
stream--Pointer to a FILE object that specifies an input stream.
如下所示:
#include<stdio.h>
int main() {int arr[5] = { 0 };FILE* pf = fopen("text.txt", "rb");if (pf == NULL) {perror("fopen");return 1;}fread(arr, sizeof(arr[0]), 5, pf);for (int i = 0; i < 5; i++) {printf("%d ", arr[i]);}fclose(pf);pf = NULL;return 0;
}
运行结果如下:
5.2 对比一组函数
scanf----从标准输入流上读取格式化的数据
fscanf----从指定的输出流上读取格式化的数据
sscanf----在字符串中读取格式化的数据
fscanf的函数原型如下:
int fscanf ( FILE * stream, const char * format, ... );
scanf的函数原型如下:
int scanf ( const char * format, ... );
sscanf的函数原型如下:
int sscanf ( const char * s, const char * format, ...);
printf----把数据以格式化的形式打印在标准输出流上
fprintf----把数据以格式化的形式打印在指定的输出流上
sprintf----把格式化的数据转换成字符串
fprintf的函数原型如下:
int fprintf ( FILE * stream, const char * format, ... );
printf的函数原型如下:
int printf ( const char * format, ... );
sprintf的函数原型如下:
int sprintf ( char * str, const char * format, ... );
使用如下:
#include<stdio.h>
struct S {char name[20];int age;float score;
};
int main() {char buf[200] = { 0 };struct S s = { "zhangsan",10,78.0f };sprintf(buf,"%s %d %f", s.name, s.age, s.score);printf("1以字符串的形式打印%s\n", buf);struct S t = { 0 };sscanf(buf,"%s %d %f", t.name, &(t.age), &(t.score));printf("2按照格式打印 %s %d %f", s.name, s.age, s.score);return 0;
}
输入如下:
六、文件的随机读写
6.1 fseek
int fseek ( FILE * stream, long int offset, int origin );
offset:偏移量
origin:起始位置,有三个值可以选择。
SEEK_SET | Beginning of file ---文件的起始位置 |
SEEK_CUR | Current position of the file pointer ---文件指针的当前位置 |
SEEK_END | End of file * ---文件末尾 |
如下所示:
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL){perror("fopen:");return 1;}//读文件char ch;ch = fgetc(pf);printf("%c ", ch);fseek(pf, 4, SEEK_CUR);ch = fgetc(pf);printf("%c", ch);fclose(pf);pf = NULL;return 0;
}
6.2 ftell
返回⽂件指针相对于起始位置的偏移量。
函数原型如下:
long int ftell ( FILE * stream );
6.3 rewind
让⽂件指针的位置回到⽂件的起始位置
函数原型如下:
void rewind ( FILE * stream );
七、文件读取结束的判定
7.1 被错误使用的feof
牢记:在⽂件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof 的作用是:当⽂件读取结束的时候,判断读取结束的原因是否是:遇到⽂件尾结束。
文件现在读取结束了,但是是什么原因读取结束的呢?
1.有可能是遇到文件末尾----使用feof判定
2.读取的时候发生了错误----使用ferror判定
打开一个流的时候,这个流上有两个标记值:1:是否遇到文件末尾。2:是否发生错误。
1.⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
- fgetc 判断是否为 EOF .
- fgets 判断返回值是否为 NULL .
2.⼆进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}char ch = 0;while ((ch = fgetc(pf)) != EOF) {printf("%c ", ch);}//判断是什么原因到时读取文件结束if (feof(pf)) {printf("遇到文件末尾正常结束");}else if (ferror(pf)) {perror("fgetc:");}fclose(pf);pf = NULL;return 0;
}
输出如下所示:
文件读取发生错误实例:我以读的方式打开文件。现在向文件里面写数据
#include<stdio.h>
int main() {FILE* pf = fopen("text.txt", "r");if (pf == NULL) {perror("fopen:");return 1;}//写文件char ch = 0;for (ch = 'a'; ch <= 'z'; ch++) {fputc(ch, pf);}//判断是什么原因到时读取文件结束if (feof(pf)) {printf("遇到文件末尾正常结束");}else if (ferror(pf)) {perror("fgetc:");}fclose(pf);pf = NULL;return 0;
}
输出如下所示:
八、文件缓冲区

#include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
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;
}