【C语言】文件操作
文件操作-目录
一、为什么使用文件
我们在写程序时,例如写通讯录,联系人的数据都是放在内存中的,如果退出程序,这些数据也随之消失,下次运行通讯录时,又得重新录入数据,很是麻烦。
为了避免数据消失,使数据持久化,我们可以将数据存入磁盘文件,也可以将数据上传至数据库。
使用文件我们就可以将数据存入电脑磁盘,实现数据的持久化。
二、什么是文件
文件就是磁盘中的文件,如果以文件功能来分类,文件可以分为程序文件和数据文件
程序文件
包括源程序文件(.c),可执行程序(.exe),目标文件(.obj)
数据文件
存放数据的文件,数据是指程序运行时进行读写的数据。数据文件可以存放程序需要读取的数据,也可以存放程序输出的数据。
文件名
为了方便识别和引用,每个文件都有唯一的文件标识
文件标识包括三部分:文件路径+文件名主干+文件后缀
方便起见,文件标识常被称为文件名
三、文件的打开和关闭
文件指针
在对文件进行操作时,需要用到文件指针变量,简称文件指针
每打开一个文件,系统都会在内存中开辟一个文件信息区,用来存放文件相关的信息。这些信息存放在一个结构体当中,这个结构体由系统声明,取名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;
打开文件时,系统会自动根据文件信息创建一个FILE结构变量,并自动填充其中的内容
为了方便对文件内容进行操作,我们一般创建一个文件指针来维护相对应的FILE结构变量
例如,定义一个FILE数据类型的指针变量
FILE* pf;//文件指针变量
使pf指向一个FILE结构体变量,该结构体变量存放着文件信息。
这样我们就可以通过文件中的信息访问文件,就是说通过文件指针变量可以访问与它相关联的文件
文件的打开与关闭
就像我们在冰箱中放/取东西,先打开冰箱门,放/取东西,关闭冰箱门
在对文件进行操作时,我们需要先打开文件,再进行相应操作,操作完毕后还需关闭文件
打开文件时,系统会自动开辟一个文件信息区并返回一个FILE*指针,指向该文件,建立指针与文件的关系
ANSIC规定,使用fopen函数打开文件,fclose关闭文件,它们的头文件是<stdio.h>
//打开文件
FILE* pf = fopen(const char * filename, const char * mode);
//关闭文件
fclose(pf);
pf = NULL;
fopen
参数 const char * filename 是要打开的文件的文件名,const char * mode 是打开方式
打开文件成功,该函数返回指向打开的文件的指针;打开失败,返回空指针 NULL
打开方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写,打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,建立一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
fclose
关闭文件,需要将要关闭的文件的指针传给 fclose
关闭文件成功,返回 0 ;关闭失败,则返回 EOF
例如:打开文件“data.txt”,打开方式为“w(只写)”
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
//如果打开失败
if (pf == NULL)
{
perror("fopen");//打印错误信息
return 1;
}
//把字符串"abcde"放入文件
fputs("abcde", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
四、文件的顺序读写
程序按照顺序进行输入和输出数据的操作就是文件的顺序读写
这里的输入和输出是站在内存的角度来说的
外部向内存输送数据就是输入,也就是读文件
内存向外部输送数据就是输出,也就是写文件
顺序读写函数
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
以上函数多适用于“流”,那“流”是什么呢?
流
流是一个高度抽象的概念
我们知道水流,里面流淌的是水
而这里的流是数据流,里面流淌的是数据
数据的输出或输入并不是直接与外部建立联系,而是通过流来进行,流相当于一个中转站
程序员想进行输出或者输入操作时,只需将数据传输给流,剩下的步骤交给流来做
例如:scanf 和 printf
在使用这两个函数时,我们直接就操作了,似乎并没有去进行与流相关的操作
其实不然,在C语言程序运行时,会自动打开三个流:标准输入流 stdin
,标准输出流 stdout
,标准错误流 stderr
在使用 scanf 时,标准输入流从键盘读取数据,然后流将数据输送给程序
使用 printf 时,程序将数据输送给标准输出流,而后流将数据输送给屏幕
在了解流这一概念后,我们再来看看下面的顺序读写函数的使用
fgetc 字符输入
一次读取一个字符
读取成功,返回该字符的 ASCII 码值
如果将文件里的字符都读取完了,就会返回 EOF,并且在流上设置一个标记,此标记可以被函数 feof 识别
如果读取失败,也会返回 EOF,并且在流上设置标记,而该标记可以被函数 ferror 识别通过以上两种标记,我们就可以判断文件读取是因为什么而结束的
下面我们来具体应用一下 fgetc
先在代码路径下创建 “data.txt” 文件,随便输入一些数据
打开文件 “data.txt”,打开方式为 “r”
FILE* pf = fopen("data.txt", "r");
//如果打开失败
if (pf == NULL)
{
perror("fopen");//打印错误
return 1;
}
读取字符并打印
char c = 0;
printf("%c ", fgetc(pf)); //a
printf("%c ", fgetc(pf)); //b
printf("%c ", fgetc(pf)); //c
printf("%c ", fgetc(pf)); //d
printf("%c ", fgetc(pf)); //e
关闭文件
fclose(pf);
pf = NULL;
运行结果:成功读取并打印了字符
可是为什么当我们调用一次函数读取了 a 之后,再次调用函数时会读取 b 呢?
我们打开文件时,鼠标的光标默认在最前面的位置,进行删除或增加字符时,光标都会变动位置
我们猜测流里存在一个类似光标的东西,用来记录位置,当输入或是输出后,这个光标就会挪动
fputc 字符输出
就是将字符写出到文件中
写出成功,返回写出的字符的ASCII码值
写出失败,返回 EOF,并在流上设置标记(ferror)
应用
此时我们要写出数据到文件,这个文件可有可无
如果没有这个文件,在打开此文件时,就会创建一个
打开文件,打开方式为 “w”
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror(fopen);
return 1;
}
写出字符到文件
fputc('h', pf);
fputc('e', pf);
fputc('l', pf);
fputc('l', pf);
fputc('o', pf);
关闭文件
fclose(pf);
pf == NULL;
运行结果:“data.txt”内的内容
fgets 文本行输入
该函数可以按行读取字符,并以字符串的形式存入str
参数
str:指向字符数组的指针,这个字符数组用来存放读取的字符
num:限制要读取的字符的个数,包括换行符。例如,num = 100,那么读取 99 字符就停止,在99字符末尾追加换行符
stream: 指向输入流的指针
该函数会在以下情况停止:
读到换行符,并且将换行符也存入str
读取了 num-1 个字符
读取到文件末尾
返回值
读取成功,返回 str
读取到文件末尾,在流上设置结束标记(feof)。如果还没读到任何字符就遇到文件末尾,就返回空指针NULL,str指向内容不变
发生读取错误,在流上设置错误标记(ferror),并返回空指针NIULL,str指向的内容可能发生改变
应用
“data.txt”文件:
打开文件,打开方式“r”
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
读取文本行,用字符数组 str 来接收,顺便打印看看效果
char str[] = "xxxxxxxxx";
fgets(str, 100, pf); //这里一行并没有 100 个字符,所以读到换行就停止
printf("%s", str);
关闭文件
fclose(pf);
pf = NULL;
运行效果:
成功读取了 “hello” 并且追加了 “\n”
fputs 文本行输出
将 str 指向的字符串写出到文件,从第一个字符开始,直到遇到终止空字符,终止空字符不会被写出
返回值
写出成功,返回一个非负的值
写出失败,返回 EOF,并在流上设置结束标识(ferror)
应用
打开文件“data.txt”,打开方式“w”
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
输出字符数组 “hello world!” 到 “data.txt”
char str[] = "hello world!";
fputs(str, pf);
关闭文件
fclose(pf);
pf = NULL;
运行效果,“data.txt”内容:
fprintf 格式化输出
与 printf 的差别就是,printf只能选择标准输出流,而 fprintf 不仅可以选择标准输出流,还可以选择其他输出流
下面来看看如何使用:
打开文件,打开模式“w”
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
声明结构体并初始化
struct stu
{
float f;
char c;
int i;
}s = {3.14, 'a', 19};
输出数据,可以看到,与 printf 用法相同,只是参数多了一个流pf
fprintf(pf, "%f %c %d", s.f, s.c, s.i);
关闭文件
fclose(pf);
pf = NULL;
运行结果,“data.txt”内容:
fscanf 格式化输入
同样的,fscanf 比scanf 更为强大,可以选择自己的输入流,并不局限于标准输入流
应用
我们把上文的结构体变量初始化为0,用来接收“data.txt”的内容
struct stu
{
float f;
char c;
int i;
}s = { 0 };
打开文件,模式为“r”
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
读取数据,也是和 scanf 的用法相同,不过是多了个流pf
fscanf(pf, "%f %c %d", &s.f, &s.c, &s.i);
关闭文件
fclose(pf);
pf = NULL;
运行效果:
fwrite 二进制输出
上文介绍的输出函数,输出的数据都是ASCII字符形式,而二进制输出函数输出的数据是二进制形式
以ptr为起始位置,向后 count 个数据,每个数据字节大小是 size,输出到 stream
应用
打开文件,打开模式 “wb”
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
创建整型数组 arr1 ,将其中的数据输出到 “data.txt”
arr1 共有元素 10 个,每个元素大小是 4 字节
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
fwrite(arr1, 4, 10, pf);
关闭文件
fclose(pf);
pf = NULL;
运行结果,“data.txt”内容:
存放的是二进制数据,看不懂
我们可以用 fread 将数据读入,看看是不是和我们输出的数据一致
fread 二进制输入
和 fwrite 的参数相同,只不过是将流里的数据放到 ptr 指向的空间中
应用
打开文件,打开模式 “rb”
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
创建整型数组 arr2,fread 读取数据并打印
int arr2[10] = { 0 };
fread(arr2, 4, 10, pf);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
关闭文件
fclose(pf);
pf = NULL;
运行结果:和我们输出的结果一致
五、文件的随机读写
这里的随机读写不是指随便乱写,而是指可以读写任意位置
下文的文件指针我们可以理解为光标
fseek
根据文件指针的位置和偏移量来定位指针
offset 代表偏移量,可正可负
origin 表示文件指针的起始位置
SEEK_SET 表示文件的起始位置
SEEK_CUR 表示文件指针当前的位置
SEEK_END 表示文件的末尾
应用
就以输入字符为例,“data.txt”内容:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取字符
fclose(pf);
pf = NULL;
return 0;
}
先来读取 4 个字符
//读取字符
char ch = 0;
//a
ch = fgetc(pf);
printf("%c\n", ch);
//b
ch = fgetc(pf);
printf("%c\n", ch);
//c
ch = fgetc(pf);
printf("%c\n", ch);
//d
ch = fgetc(pf);
printf("%c\n", ch);
此时光标的位置:
如果想重新读 a ,有三种方法:
起始位置+偏移量
当前位置+偏移量
末尾位置+偏移量
起始位置+偏移量
fseek(pf, 0, SEEK_SET);
//a
ch = fgetc(pf);
printf("%c\n", ch);
当前位置+偏移量
光标目前在 e 的位置,需要向前偏移 4 个单位才可以回到 a,偏移量为 -4
fseek(pf, -4, SEEK_CUR);
//a
ch = fgetc(pf);
printf("%c\n", ch);
末尾位置+偏移量
如果光标在末尾,需要向前偏移 6 个单位才可以回到 a,偏移量为 -6
fseek(pf, -6, SEEK_END);
//a
ch = fgetc(pf);
printf("%c\n", ch);
三种方法都有效:
ftell
返回文件指针相对于起始位置的偏移量
该函数比较容易理解,还是以上面的代码为例
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取字符
char ch = 0;
//a
ch = fgetc(pf);
printf("%c\n", ch);
//b
ch = fgetc(pf);
printf("%c\n", ch);
//c
ch = fgetc(pf);
printf("%c\n", ch);
//d
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
此时读取完d,光标位置:
文件指针相对于起始位置的偏移量为 4
ftell 打印
printf("偏移量为%d",ftell(pf));
运行:
rewind
让文件指针的位置回到起始位置
还是上文代码
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取字符
char ch = 0;
//a
ch = fgetc(pf);
printf("%c\n", ch);
//b
ch = fgetc(pf);
printf("%c\n", ch);
//c
ch = fgetc(pf);
printf("%c\n", ch);
//d
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
调用 rewind 使文件指针回到 a
rewind(pf);
//a
ch = fgetc(pf);
printf("%c\n", ch);
运行:
以上三个函数可以使我们在读写文件过程中实现随机读写,无论调用哪个函数,怎样调用某个函数,都需要我们对文件的内容足够了解
六、文本文件和二进制文件
根据数据的组织形式,数据文件可以被分为文本文件和二进制文件
在内存中,数据都是以二进制形式存储的
如果在数据输出前进行转换,以ASCII码值的形式存储到文件中,那就是文本文件;如果不进行转换,直接以二进制形式存储到文件中,那就是二进制文件
数据在内存中,字符数据一律以ASCII码值形式存储,而整型数据既可以用ASCII码值形式存储,也可以用二进制形式存储
我们来测试一下,将整型10000以二进制形式存储到文件中
int main()
{
int a = 10000;
//打开
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制的形式写到文件中
fwrite(&a, 4, 1, pf);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
输出成功,但我们并不能看懂这里面存储的数据是什么,我们用vs打开查看一下
打开后,可以看到一组数字:
00000000
不用管,10 27 00 00
才是我们需要看的
这组数字是不是10000,我们来计算验证:
10000的二进制形式
在内存中实际是以小端形式存储
为了方便查看,将二进制转换为十六进制,每4个二进制位转换为1个十六进制位
计算结果与文件中存储的数据一致
七、文件读取结束的判定
被错误使用的feof
注意:feof并不是用来检测文件读取是否结束的,而是用来检测文件读取结束的原因的
feof的真正作用:当文件读取结束后,检测读取结束是否是因为:遇到文件末尾而结束
如何检测文件读取是否结束
文本文件
文本文件读取是否结束,可以判断返回值是否为EOF或NULL
fgetc:当读取结束时会返回EOF
fgets:读取结束会返回NULL
二进制文件
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
例如fread
fread要求读取 count 个 size 字节大小的数据
如果读取到 count 个数据,就会返回 count
如果没有读取到 count 个数据,就会返回实际读取到的数据个数
例如,一个二进制文件中有 6 个元素,我们要求 fread 一次读取 5 个元素
第一次调用 fread,成功读取 5 个元素,返回值为 5
第二次调用,只读取到一个元素,返回值为 1
应用
我们以 fgetc 为例,读取 “data.txt”中的数据并打印
创建整型变量c,来接收 fgetc 的返回值,fgetc的返回值是读取到的字符的ASCII码值,属于整型
只要 c 不为EOF,就继续读取,以此条件进行循环
int main()
{
//打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取
int c = 0;//用来接收fgetc的返回值
while ((c = fgetc(pf)) != EOF)
{
putchar(c);
}
//关闭
fclose(pf);
pf = NULL;
return 0;
}
结果:
feof 和 ferror 的使用
feof:如果文件读取是因为读取到文件末尾而结束,返回非零值,否则就返回零
ferror:如果文件读取是因为读取失败而结束,就返回非零值,否则返回零
//结束原因
if (feof(pf))
printf("结束原因:读取到文件末尾\n");
else if (ferror(pf))
printf("结束原因:读取失败\n");
还是上面的代码,区别就是:在读取结束之后,关闭文件之前,打印一个信息来提示文件读取结束的原因
int main()
{
//打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取
int c = 0;//用来接收fgetc的返回值
while ((c = fgetc(pf)) != EOF)
{
putchar(c);
}
printf("\n");
//结束原因
if (feof(pf))
printf("结束原因:读取到文件末尾\n");
else if (ferror(pf))
printf("结束原因:读取失败\n");
//关闭
fclose(pf);
pf = NULL;
return 0;
}
结果:
八、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”
内存输出数据时并不是只要输出了就会即时将数据存到文件中,而是将数据暂存到一个文件缓冲区,达到一定条件后才会将数据存到文件中,内存读取数据同理:
文件缓冲区输出/输入数据的条件:
1.缓冲区被充满时
2.关闭文件时
3.刷新缓冲区时
达成以上任一条件,缓冲区都会进行数据输出/输入
函数 fflush 可以刷新缓冲区,我们可以通过此函数来粗略观测一下文件缓冲的过程
首先打开文件,将数据放入输出缓冲区
#include <windows.h>
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将数据放入输出缓冲区
fputs("abcdef", pf);
return 0;
}
设置休眠十秒,打印提示信息,让我们打开“data.txt”文件查看数据有没有存进去
#include <windows.h>
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//将数据放入输出缓冲区
fputs("abcdef", pf);
//提示信息
printf("睡眠10秒,已经写出数据,请打开data.txt文件查看\n");
//睡眠十秒
sleep(10000);
return 0;
}
查看完之后,刷新缓冲区,休眠十秒,再打开文件查看
#include <windows.h>
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);
//提示信息
printf("睡眠10秒,已经写出数据,请打开data.txt文件查看\n");
//睡眠十秒
sleep(10000);
printf("刷新缓冲区\n");
//刷新缓冲区
fflush(pf);
//提示信息
printf("睡眠10秒,已经刷新缓冲区,请打开data.txt文件查看\n");
//睡眠十秒
sleep(10000);
return 0;
}
关闭文件
#include <windows.h>
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);
//提示信息
printf("睡眠10秒,已经写出数据,请打开data.txt文件查看\n");
//睡眠十秒
Sleep(10000);
printf("刷新缓冲区\n");
//刷新缓冲区
fflush(pf);
//提示信息
printf("睡眠10秒,已经刷新缓冲区,请打开data.txt文件查看\n");
//睡眠十秒
Sleep(10000);
//关闭文件时也会刷新缓冲区
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
结束,再见:D