目录
- 一、文件的作用
- 二、什么是文件
- 三、文件的打开和关闭
-
- [3.1 文件指针](#3.1 文件指针)
- [3.2 文件打开函数-fopen](#3.2 文件打开函数-fopen)
- [3.3 文件关闭函数 - fclose](#3.3 文件关闭函数 - fclose)
- 3.4文件操作正确流程
- 四、文件的顺序读写
-
- [4.1 字符输入输出函数 - fgetc和fputc](#4.1 字符输入输出函数 - fgetc和fputc)
- [4.2文本行输入输出函数 - fgets和fputs](#4.2文本行输入输出函数 - fgets和fputs)
- [4.3格式化输入输出函数 - fscanf和fprintf](#4.3格式化输入输出函数 - fscanf和fprintf)
- [4.4 二进制输入输出函数 - fread和fwrite](#4.4 二进制输入输出函数 - fread和fwrite)
- 五、文件的随机读写
-
- [5.1 fseek函数](#5.1 fseek函数)
- [5.2 ftell函数](#5.2 ftell函数)
- [5.3 rewind函数](#5.3 rewind函数)
- 六、文件读取结束的判定
-
- [6.1 ferror函数](#6.1 ferror函数)
- [6.2 feof函数](#6.2 feof函数)
- [6.3 ferror函数和feof函数搭配使用](#6.3 ferror函数和feof函数搭配使用)
- 七、文件缓冲区
一、文件的作用
当我们在写了一些程序例:通讯录,当通讯录运行起来可以对其进行增删查改,此时数据是存在内存中,当程序退出通讯录中的数据也不在了,没法保存起来,等下次再次运行通讯录程序时又得重新录入数据,这样显然效率是非常低下且很难受的。
我们预期想把一些数据保存下来,程序下次运行时这些数据依然存在,只有我们自己删除数据时,数据才不存在,这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上就做到了数据的持久化。
二、什么是文件
文件是计算机文件,属于文件的一种,与普通文件的载体不同,计算机文件是以计算机硬盘为载体存储在计算机上的信息集合。
在程序设计中,我们一般关注的文件有两类,即程序文件和数据文件。
程序文件: 包括源程序文件(后缀为.c),可执行程序(windows环境后缀为.exe)。
数据文件: 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本篇围绕数据文件展开
文件名
一个文件要有一个唯一的文件标识 ,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
为了方便起见,文件标识常被称为文件名。
文件类型
根据数据的组织形式,数据文件被称为二进制文件或文本文件。
二进制文件: 数据在内存中以二进制的形式进行存储,如果不加转换直接输出到外存,就是二进制文件。
文本文件: 如果要求在外存上以ASCII码的形式存储,则需要在存储前进行转换。以ASCII码的形式进行存储的文件就是文本文件,就是我们能看懂的文件。
字符一律以ASCII码值进行存储;数值型数据既可以以ASCII码值进行存储,也可以以二进制的形式进行存储。
如:整数10000,以ASCII值的形式输出到磁盘,那么它将在磁盘中占用5个字节(一个字符一个字节);而如果以二进制的形式进行输出,那么它只在磁盘中占用4个字节(一个整型大小即可存储):
字符1的ASCII码是49,字符0的ASCII是48所以就有了如上序列
三、文件的打开和关闭
3.1 文件指针
缓冲文件系统中,关键的概念是"文件类型指针 ",简称"文件指针 "。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
例如
c
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
定义了一个_iobuf结构体,最后对结构体重命名为FILE,该结构体就是用来描述文件信息的。
3.2 文件打开函数-fopen
文件的两个简单操作:在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系,通过指针访问文件的内容。
函数功能
c
FILE *fopen( const char *filename, const char *mode );
该函数的功能就是打开一个文件,函数的第一个参数打开文件的文件名,第二个参数是打开该文件的形式。
打开一个文件时,系统会为该文件创建一个文件信息区,该函数调用完毕后,如果打开该文件成功,那么返回指向该文件信息区的指针(FILE*类型);如果打开文件失败,那么返回一个空指针(NULL)。
文件的打开形式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb+"(读写) | 为了读和写打开一个二进制文件 | 出错 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
![](https://i-blog.csdnimg.cn/direct/3bcbf12e6c9f490ea925f8956253ef5d.png)
如:1. 以文本形式打开一个名叫data.txt的文件,对其进行只读操作,那么打开文件时应该这样写:
c
FILE* pf = fopen("data.txt", "r");
注:若data.txt文件不存在则打开文件失败,fopen函数会返回一个空指针。
如:2. 以二进制形式打开一个名叫data.bin的文件,对其进行写操作,那么打开文件时应该这样写:
c
FILE* pf = fopen("data.bin", "wb");
注:若data.bin文件存在则清空文件原内容,在进行我们的写操作,若不存在则自动创建该文件并进行写操作
- 检测fopen返回值的有效性
如果文件打开成功,fopen函数会返回指向文件信息区的指针,否则fopen函数会返回一个空指针。所以当使用接收fopen函数的返回值的指针前,我们必须检测其有效性,否则可能非法访问内存。
c
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;//失败返回
}
相对路径与绝对路径
填写fopen函数的第一个参数的时候,如果我们要打开的文件与我们正在运行的源代码在同级目录下(以打开data.txt文件为例)
c
FILE* pf = fopen("data.txt", "r"); //同级
如果我们想要打开的文件与当前运行的源代码不在同一级目录有两种方法:相对路径和绝对路径。
- 相对路径
当待打开的文件不在正在运行的源代码时:
c
FILE* pf = fopen("../data.txt", "r"); //上一级
FILE* pf = fopen("../../data.txt", "r"); //上上级,可类推
FILE* pf = fopen("Debug/data.txt", "r"); //下一级
注:这里data.txt文件在Debug文件内,Debug文件与正在运行的源代码在同级目录下。
- 绝对路径
绝对路径就直接写出目标文件的完整路径
c
FILE* pf = fopen("D:\\code\\File_test\\File_test\\data.txt", "r");
注:文件的路径原本为"D:\code\File_test\File_test\data.txt,但是为了防止字符串中的'\'及其后面的字符被整体视为为转义字符,所以需要在每个'\'后面再加一个'\'。
3.3 文件关闭函数 - fclose
与动态开辟内存空间时一样,当我们打开文件时,会在内存中开辟一块空间,如果我们打开该文件后不关闭,那么这个空间会一直存在,一直占用那块内存空间,导致内存泄漏,所以当我们对一个文件的操作结束时,一定要记住将该文件关闭,这就需要用到fclose函数来关闭文件。
c
int fclose( FILE *stream );
关闭一个文件直接将该文件的文件指针传入fclose函数即可,fclose函数如果关闭文件成功会返回0。与free函数一样,当我们调用完fclose函数将文件关闭后,我们也要将指向文件信息区的指针置空,避免该指针变成野指针。
c
fclose(pf); //关闭pf指向的文件
pf = NULL; //及时置空
3.4文件操作正确流程
c
#include <stdio.h>
#include <string.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL) //检测指针有效性
{
perror("fopen"); //错误提醒
return 1; //失败返回
}
//对文件进行一系列操作
//...
//关闭文件
fclose(pf); //关闭pf指向的文件
pf = NULL; //及时置空
return 0;
}
四、文件的顺序读写
功能 | 函数名 | 适用 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输入 | fwrite | 文件 |
任何一个C语言程序运行时,默认打开三个流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)
其中stdin对应的是键盘,stdout和stderr对应的是屏幕,它们的类型都是FILE*
4.1 字符输入输出函数 - fgetc和fputc
- fputc
c
int fputc( int c, FILE *stream );
fputc函数的第一个参数是待输出的字符,第二个参数该字符输出的位置,即fputc函数的功能是将一个字符输出到指定的位置。该函数调用完毕会返回用户传入的字符。
如:将小写字母a~z写入到data.txt文件中
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写入26个字母
for (char ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf); //关闭文件
pf = NULL; //置空
return 0;
}
- fgetc
c
int fgetc( FILE *stream );
fgetc函数只有一个参数,即你要读取的文件的文件指针。fgets函数的功能就是从指定位置读取一个字符。该函数调用成功会返回读取到的的字符;若读取文件时发生错误,或是已经读取到文件末尾,则返回EOF。
如:将文件data.txt文件中的内容全部读取,并打印到屏幕上
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//将文件内容循环打印到屏幕
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
printf("\n");
fclose(pf); //关闭文件
pf = NULL; //置空
return 0;
}
4.2文本行输入输出函数 - fgets和fputs
- fputs函数
c
int fputs( const char *string, FILE *stream );
fputs函数的第一个参数是待输出的字符串,第二个参数该字符串输出的位置,即fputs函数的功能是将一个字符串输出到指定的位置,该函数调用成功会返回一个非负值;若输出时发生错误,则返回EOF。
如:将字符串"hello world!"写入到data.txt文件中
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1; //文件打开失败,失败返回
}
//将该字符串写入文件
char arr[] = "hello world!";
fputs(arr, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
- fgets函数
c
char *fgets( char *string, int n, FILE *stream );
fgets函数的第三个参数是你要读取的文件的文件指针,第二个参数是你要读取的字符个数(也可以说是字节个数),第一个参数是你所读取到的数据的储存位置。fgets函数的功能就是从指定位置读取指定字符个数的数据储存到指定位置。该函数调用成功会返回用于储存数据的位置的地址,如果读取过程中发生错误,或是读取到了文件末尾,则返回一个空指针(NULL)。
如:从data.txt文件中读取数据
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;//文件打开失败,失败返回
}
//对文件进行读取字符串操作
char arr[20] = { 0 };
fgets(arr, 15, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgets函数读取字符的过程中会出现两种情况:
- 在fgets函数读取到指定字符数之前,若读取到换行符('\n'),则停止读取,读取的字符包含换行符。
- 直到fgets函数读取到第n-1个字符时都没有遇到换行符('\n'),则返回读取到的n-1个字符,并在末尾加上一个空字符一同返回(共n个字符),最多读取n-1个字符。
4.3格式化输入输出函数 - fscanf和fprintf
- fprintf函数
c
int fprintf( FILE *stream, const char *format, argument,...);
第三个参数的三个点是可变参数列表,接收参数可多可少
c
int printf( const char *format, argument,... );
对比这两函数除了文件指针其它都是一样的,所以我们可以模仿我们常用的库函数printf使用
如:将一个结构体类型的变量信息输出到data.txt文件中
c
struct S
{
char* name;
int age;
char *sex;
};
int main()
{
struct S s = { "张三",18,"man" };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以格式化输出
fprintf(pf, "%s, %d, %s", s.name, s.age, s.sex);
fclose(pf);
pf = NULL;
return 0;
}
可见和我们使用库函数printf是差不多的
- fscanf函数
c
int fscanf( FILE *stream, const char *format, argument,... );
int scanf( const char *format,argument,... );
同样这两也类似
c
struct S
{
char* name;
int age;
char *sex;
};
int main()
{
struct S s={0};
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以格式化输入
fscanf(pf, "%s %s %d", s.name, s.sex, &(s.age));
printf("%s, %s, %d", s.name, s.sex, s.age);
return 0;
}
4.4 二进制输入输出函数 - fread和fwrite
- fwrite函数
c
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
fwrite函数的第一个参数是输出数据的位置,第二个参数是要输出数据的元素个数,第三个参数是每个元素的大小,第四个参数是数据输出的目标位置。该函数调用完后,会返回实际写入目标位置的元素个数,当输出时发生错误或是待输出数据元素个数小于要求输出的元素个数时,会返回一个小于count的数。
fwrite函数的功能就是将buffer位置的,每个元素大小为size的,count个元素,以二进制的形式输出到stream位置。
如:将数组arr中的10个元素以二进制的形式输出到data.txt文件中去。
c
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//对文件以二进制形式进行写入操作
int arr[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
fwrite(arr, sizeof(int), 10, pf);
fclose(pf);
pf = NULL;
return 0;
}
- fread函数
fread函数的第一个参数是接收数据的位置,第二个参数是要读取的每个元素的大小,第三个参数是要读取的元素个数,第四个参数是读取数据的位置。函数调用完会返回实际读取的元素个数,若在读取过程中发生错误或是在未读取到指定元素个数时读取到文件末尾,则返回一个小于count的数。
c
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
fread函数的功能就是从stream位置,以二进制的形式读取count个每个元素大小为size的数据,到buffer位置。
如:将刚才用fwrite函数输出到data.txt文件的数据读取出来
c
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;//文件打开失败,失败返回
}
//对文件以二进制形式进行读取操作
int arr[10] = { 0 };
fread(arr, sizeof(int), 10, pf);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]); //将arr中的内容打印出来,看是否读取成功
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
五、文件的随机读写
引入:
如:data.txt文件中的内容是"abcdef",执行完以下代码后
c
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//用字符输入函数读取文件信息
int ch = fgetc(pf);
printf("%c\n", ch); //观察第一次读取到的字符
ch = fgetc(pf);
printf("%c\n", ch); //观察第二次读取到的字符
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
第一次读取字符时,指针位于文件开头,读取到了第一个字符以后,指针就会自动向后移动,指向后面的内容:
还未进行读取操作时,文件指针指向文件信息区的起始位置:
读取'a'字符以后,文件指针自动指向到下一个字符:
当再次进行读取操作时,就读取到的是字符'b'了,文件指针再次后移指向字符'c'
那么,在读取了字符'a'和字符'b'后,我们如何再次读取到字符'a',或是不读取下一个字符'c',而读取其后的某一指定位置的数据呢?
5.1 fseek函数
c
int fseek( FILE *stream, long offset, int origin );
fseek函数的第一个参数既是要移动位置的文件指针,第三个参数是"初始位置"(并非文件信息区的起始位置),第二个参数是文件指针经操作后相对于这个"起始位置"的偏移量,单位为字节。fseek函数如果调用成功,则返回0;若调用失败,则返回一个非0的值。
关于fseek函数的第三个参数,有以下三种形式:
参数形式 | 代表意义 |
---|---|
SEEK_CUR | 文件指针的当前位置 |
SEEK_SET | 文件开头 |
SEEK_END | 文件末尾 |
如:用fseek函数,对上述例题中pf指针进行操作,使其下一次读取字符时能读取到字符'a',则有以下三种写法:
1.让文件指针相对于其当前位置向前偏移2个字节
c
fseek(pf, -2, SEEK_CUR);//调整文件指针位置
ch = fgetc(pf); //读取到字符'a'
printf("%c\n", ch);
2.让文件指针相对于文件开头偏移0个字节(即指向文件开头)
c
fseek(pf, 0, SEEK_SET);//调整文件指针位置
ch = fgetc(pf); //读取到字符'a'
printf("%c\n", ch);
3.让文件指针相对于文件末尾向前偏移6个字节
c
fseek(pf, -6, SEEK_END);//调整文件指针位置
ch = fgetc(pf); //读取到字符'a'
printf("%c\n", ch);
5.2 ftell函数
为了更好地明确文件指针位于什么位置,于是出现了ftell函数,它用于计算当前文件指针相对于起始位置的偏移量。
c
long ftell( FILE *stream );
ftell函数的参数是一个文件指针。ftell函数调用成功,则返回文件指针相对于起始位置的偏移量;若调用失败,则返回 - 1。
例如,我们要计算上述例题中,读取了两个字符后,pf相对于起始位置的偏移量,我们可以这样计算:
c
long Offset = ftell(pf);
5.3 rewind函数
c
void rewind( FILE *stream );
rewind函数的作用是让传入的文件指针返回文件的起始位置。例如,在上述例题中,我们可以直接让文件指针返回文件的起始位置,这样就能读取到字符'a':
c
rewind(pf); //调整文件指针位置
ch = fgetc(pf); //读取到字符'a'
printf("%c\n", ch);
场景:用fseek函数与ftell函数求文件大小
求一个文件的大小其实很简单,只需要将文件指针移到该文件末尾,然后求文件指针相对于起始位置的偏移量即可
c
fseek(pf, 0, SEEK_END);//将文件指针置于文件末尾
int FileLen = ftell(pf);//求文件指针相对于文件起始位置的偏移量
fseek(pf, 0, SEEK_SET);//将文件指针放回文件开头
六、文件读取结束的判定
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
6.1 ferror函数
c
int ferror( FILE *stream );
ferror函数的功能就是判断使用某一文件指针的过程中,是否发生错误,若使用时没有发生错误,则ferror函数返回0;否则,ferror函数将返回一个非零的值。调用ferror函数时,我们只需将待检查的文件指针传入即可。
c
if (ferror)
{
printf("文件指针使用时,发生错误\n");
}
6.2 feof函数
c
int feof( FILE *stream );
feof函数的功能也是判断使用某一文件指针的过程中,是否读取到文件末尾,若使用时没有读取到文件末尾,则feof函数返回0;否则,feof函数将返回一个非零的值。调用feof函数时,也只需将待检查的文件指针传入即可。
c
if (feof(pf))
{
printf("文件指针使用时,读取到文件末尾\n");
}
6.3 ferror函数和feof函数搭配使用
如:文件data.txt文件中的数据为"abcdef",要用fgetc函数读取data.txt文件中的数据,当数据读取完之后就可以用ferror函数和feof函数,来判断最后一次fgetc函数调用失败的原因:
c
#include <stdio.h>
#include <string.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;//文件打开失败,失败返回
}
//读取文件中的数据
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
if (ferror(pf))
{
printf("文件指针使用时,发生错误而结束\n");
}
else if (feof(pf))
{
printf("文件指针使用时,读取到文件末尾而结束\n");
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
七、文件缓冲区
文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间。使用文件缓冲区可减少读取硬盘的次数。
文件缓冲系统: 是指系统自动地在内存中为程序中每一个正在使用的文件开辟开辟一块"文件缓冲区"。从内存向磁盘输出的数据会先送到内存中的缓冲区,待缓冲区装满后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,待缓冲区装满后,再从缓冲区逐个地将数据送到程序数据区。缓冲区的大小是根据C编译系统决定。
例如过节朋友找你玩,你在家里打扫卫生时,有人嗑瓜子、吃橘子、吃糖果后将这些垃圾一个一个扔在地上,你可以选择地上一有垃圾你就去收拾和他们都吃完走后,你一块收拾这些垃圾。这里收拾垃圾的你当作是操作系统,一点一点的垃圾就好比需要操作系统传输的信息,而你等朋友们离开后再去清理垃圾就好比缓冲区的工作机制。
总而言之,缓冲区的存在大大提高了操作系统的效率。