目录
函数使用③( fscanf 和 fprintf - 格式化 )
fseek函数 - 根据文件指针的位置和偏移量来定位文件指针
例子1 - 把test.txt文件拷贝一份,生成test2.txt
注
本笔记参考B站 鹏哥C语言
为什么使用文件
在之前所写的代码中,我们写入的数据只有在程序允许时才是有效的。一旦程序结束,向内存申请的空间全部还给操作系统,这时写入的数据也就无效了。
于是就会存在一种想法,就是把数据持久化。把数据持久化的方法有:①把数据存放在磁盘文件、②存放到数据库等方式。
使用文件可以把数据直接放在电脑的硬盘上,实现了数据的持久化。
什么是文件
硬盘上的文件就是文件。
但是在程序设计中,一般谈到的文件就两种:程序文件、数据文件(从文件功能的角度进行分类)。
程序文件
包括
源程序文件(后缀为.c)
目标文件(Windows环境下后缀为.obj)
可执行文件(Windows环境下后缀为.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如:程序运行需要从中读取数据的文件,或者输出内容的文件。
之前我们处理数据时,数据的输入和输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示在显示器上。
文件名
一个文件要有唯一的文件标识,以便于用户识别和引用。
文件名包含3个部分:文件路径 + 文件名主干 + 文件后缀
例如:D:\code\Project_3\23-1.c
方便起见,文件标识常被称为文件名。
什么是流?
流,是一个高度抽象的概念。
程序就向“流”内输入数据,再由“流”把数据输入到各个外部设备。
ps:
C语言程序,只要运行起来,就默认打开了3个流(类型都是FILE*):
- stdin - 标准输入流 - 键盘
- stdout - 标准输出流 - 屏幕
- stderr - 标准错误流 - 屏幕
文件的打开和关闭
文件指针
缓存文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(入文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。
例如(编译环境为VS2022,在头文件stdio.h中可以找到这种申明):
(此处可能是笔者没有找到正确的申明)
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,使用起来更方便。
创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使用pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
文件的打开和关闭
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。
fopen函数 - 打开文件(在读写之前)
主动创建文件信息区,并且返回这个文件信息区的起始地址。如果遇到错误,返回一个NULL指针。
注意 参数mode - 文件使用方式:
- "r" - read(只读):打开文件,进行读取。如果文件不存在或者无法找到,fopen函数执行失败。
- "w" - write(只写):创建一个空文件,进行写入。如果已经存在同名文件,则删除原本文件内容并认为该文件是新的空文件。
- "a" - append(追加):打开文件,从文件的末尾开始写入并进行扩展。重新定位操作(fseek、fsetpos、rewind)将被忽略。 如果文件不存在,则创建该文件。
- "r+" - read/update(读写):打开一个文件,进行更新(用于读取和写入)。 该文件必须存在。
- "w+" - write/update(读写):创建一个空文件,打开以进行更新(用于读取和写入)。 如果已存在同名文件,则删除原本文件内容并认为该文件是新的空文件。
- "a+" - append/update(读写):打开一个文件进行更新(读取和写入),所有读取操作都在文件末尾写入数据。 重新定位操作(fseek、fsetpos、rewind)会影响下一个读取操作,但写入操作会将位置移回文件末尾。 如果文件不存在,则创建该文件。
剩下的一些文件使用方式:
文件使用方式 含义 如果指定文件不存在 "rb"(只读) 打开一个二进制文件,进行读取 出错 "wb"(只写) 打开一个二进制文件,进行写入 创建一个新文件 "ab"(追加) 向一个二进制文件末尾添加数据 出错 "rb+"(读写) 打开一个二进制文件,进行读和写 出错 "wb+"(读写) 新建一个二进制文件,进行读和写 创建一个新文件 "ab+"(读写) 打开一个二进制文件,在文件末尾进行读写 创建一个新文件
flose函数 - 关闭文件(在使用结束之后)
参数是指向要关闭的文件信息区的指针。
如果成功关闭文件,返回 0;如果关闭失败,返回EOF。
一个进程或者一个程序可以打开文件的数量是有限的,这是一种资源。如果只打开文件而不关闭文件 ,最后就无法打开文件了。
使用例
int main()
{
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件操作
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
如果以"w"的模式进行佛彭,而所查找的文件不存在,则:
文件被创建。
如果以"r"的模式进行fopen,而所查找的文件不存在(注意路径问题),此时会发生:
这可能是因为在上面的代码中没有写明文件路径,所以程序只在程序所在工程内部寻找 test.dat 文件(相对路径),如果写成:
进行运行是否可行?答案是不可行。注意转义字符!
应该写成:
这就是绝对路径。
文件的顺序读写
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
函数使用①( fputc 和 fgetc - 字符)
fputc函数 的使用
这个函数会按照执行顺序依次往指定的流内写入字符,或者写入到 stdout(标准输出流)中。
参数character 是要写入的字符。
参数stream 是要写入的文件信息区所在的地址。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.dat", "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;
return 0;
}
程序运行完毕后打开 test.dat 观察结果:
发现已经写入数据。
如果再次运行程序,并且只运行
FILE* pf = fopen("test.dat", "w");
会发现:
程序内部的信息已经消失了,这是因为我们是通过"w"这个方式使用文件的。
向屏幕输出字符:
fputc('H', stdout); fputc('e', stdout); fputc('l', stdout); fputc('l', stdout); fputc('o', stdout);
此时代码运行,屏幕上出现对应字符
fputc函数指定的流决定了字符输出向哪里。
fgetc函数 的使用
从指定的流中依次读取字符。
- 如果读取成功,返回被读取字符的ASCII值;
- 如果读取失败,返回EOF(-1)。
先在要打开的流内输入一些数据。
代码(从文件中读取数据)
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ret = fgetc(pf);
printf("%c\n", ret);
ret = fgetc(pf);
printf("%c\n", ret);
ret = fgetc(pf);
printf("%c\n", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行:
读取了刚刚放入 test.dat 内的前3个字符。
如果从 stdin(标准输入流)中读取数据:
int main() { int ret = fgetc(stdin); printf("%c\n", ret); ret = fgetc(stdin); printf("%c\n", ret); ret = fgetc(stdin); printf("%c\n", ret); return 0; }
运行程序:
输出了用户输入字符的前3个。
如果文件读取到最后,没有字符了,会发生什么?
当读取 test.dat 内的第7个字符时,ret值变为 -1
说明文件读取结束。
函数使用②( fputs 和 fgets - 文本行)
fputs函数 的使用
将字符串写入文件或者流中。
提问:下面数据会以什么样的形式被写入文件中?
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件 - 按照行来写
fputs("abcdef", pf);
fputs("ghijklmn", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
打印结果:
文件中的数据被集中到了一行。
那如果加上'\n'会怎么样?
fputs("abcdef\n", pf);
fputs("ghijklmn\n", pf);
打印结果:
数据换行了。
注意:数据写入文件时,都是已文本(ASCII值)的形式被写入的。
fgets函数 的使用
从流(stream)中读取数据,放到字符串(str)中。
- num参数 是可以读取字符的最大个数。(ps:如果n = 100,则读取99个字符,剩下的一个是'\0')
在 test.dat 中放入数据
#include<stdio.h>
int main()
{
char arr[10] = { 0 };
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件 - 按照行来写
fgets(arr, 4, pf);
printf("%s\n", arr);
fgets(arr, 4, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
打印结果:
注意,数组空间的最后还有一个'\0'。fgets函数每次都是继续往后读取。
函数使用③( fscanf 和 fprintf - 格式化 )
先看fprintf函数
函数格式里的 "…" 是可变参数,参数的个数是可以发生变化的。
再看fprintf函数
和printf函数相比,fprintf函数仅仅多了 stream参数。
代码
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcdef", 10, 10.5f };
//对格式化的数据进行写文件
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
执行代码,再打开 test.dat 文件
再看fscanf函数
同样的,fscanf函数也是只比scanf函数多了一个stream参数
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { 0 };
//对格式化的数据进行写文件
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//取文件
fscanf(pf, "%s %d %f", s.arr, &s.num, &s.sc);
//打印
printf("%s %d %f", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
原本 test.dat 内存放:
执行程序,控制台展示:
fprintf(stdout, "%s %d %f", s.arr, s.num, s.sc); 这样写可以达到同样的效果。
函数使用④( fwrite 和 fread - 二进制 )
fwrite函数 的使用
将数据写入流(stream)中。
- ptr参数:要写入的数据的首地址;
- size参数:一个元素的大小;
- count参数:写入数据的最大个数。
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcdef", 10, 10.5f };
//二进制的形式写入
FILE* pf = fopen("test,dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(s), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
执行代码,打开 test.dat 文件:
使用VS Code打开
使用记事本(文本编辑器)打开
- 为什么会出现无法看懂的数据?因为fwrite函数是以二进制的方式写入的。
- 那为什么"abcdef"可以被看懂?因为字符串以二进制的形式写进去和以文本的形式写进去,写入的内容是一样的。
那要怎样看懂这些代码呢?
fread函数 的使用
与fwrite函数类似,fread函数是从流(stream)中读取个数为count,大小为size的数据,把数据放入ptr中。
代码
#include<stdio.h>
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcdef", 10, 10.5f };
//二进制的形式读
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
执行代码,发现之前通过fwrite写入的乱码可以被fread函数读取:
注意
在 fread函数 返回值的描述中,存在着这样一句话:返回成功读取的元素的总个数。
- 如果放入8个元素,而每次读取5个元素,则 fread函数 第一次会返回 5,第二次却会返回 3。
通过返回值,我们可以确定 fread函数 是否读完了所指向的文件。
函数使用⑤( sscanf 和 sprintf )
接下来看具体分析:
同样的,和scanf函数进行对比
发现,相比scanf函数,sscanf函数只多了一个参数 - s
参数s:即字符串s,sscanf函数就是从标准输入读取数据,格式化后,根据附加的参数format,把格式化后的数据附加到对应的位置上。
---
同样的,相比printf函数,sprintf函数只多出了参数str。
sprintf函数是把格式化的数据写到一个字符串内部。
使用例(sprintf函数)
struct S
{
char arr[10];
int age;
float f;
};
int main()
{
struct S s = { "Hello", 20, 5.5 };
char buffer[100] = { 0 };
//把 格式化的数据 转换成 字符串
sprintf(buffer, "%s %d %f", s.arr, s.age, s.f);
printf("%s\n", buffer);
return 0;
}
打印结果:
此处使用了字符串来接收格式化后的数据,所以数据被格式化成了字符串的形式。
既然如此,类似地,是否可以把这样一个字符串还原成结构体呢?
使用例(sscanf)
struct S
{
char arr[10];
int age;
float f;
};
int main()
{
struct S s = { "Hello", 20, 5.5 };
struct S tmp = { 0 };
char buffer[100] = { 0 };
//把 结构体s 的数据转换成 字符串buffer
sprintf(buffer, "%s %d %f", s.arr, s.age, s.f);
printf("%s\n", buffer);
//从字符串buffer中还原出一个结构体数据
sscanf(buffer, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.f));
printf("%s %d %f\n", tmp.arr, tmp.age, tmp.f);
return 0;
}
打印结果:
注意:虽然两行数据打印结果一样,但是打印形式是不一样的。(上面一行是一整个字符串,下面一行是分开的结构体数据)
文件的随机读写
fseek函数 - 根据文件指针的位置和偏移量来定位文件指针
- stream参数 — 对应的流;
- offset参数 — 就是偏移量(相对于起始位置origin而言);
- origin参数 — 起始位置。
origin参数有对应的3个值,分别为:
SEEK_SET 文件的起始位置 SEEK_CUR 当前文件指针的位置 SEEK_END 文件的末尾 offset参数对应的偏移量就是相对于这些参数而言的。
使用例
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, -1, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在启动程序之前,先新建 test.txt ,往该文件内放入 abcdef 。
打印结果:
分析:
以此类推,当fseek函数的 offset参数 值为 2 时,得到:
通过fseek函数,可以自由的调度文件指针。
ftell函数 - 返回文件指针相对于起始位置的偏移量
使用例
(接续 fseek函数 的例子)
#inlcude<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//读取文件指针位置
int ret = ftell(pf);
printf("%d\n", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
打印结果:
ftell函数可以用来计算文件指针偏移量的多少。
在之前的例子中,test.txt 内存放的是 abcdef ,文件指针的偏移量 = 1 + 2 + 1 + 1 = 5。
rewind函数 - 让文件指针的位置回到文件的起始位置
使用例
(依旧是将 fseek函数 的例子拿来用)
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
//让文件指针回到起始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
打印结果:
最后一次打印,依旧拿取了 字符a ,可以发现,此时文件指针已经回到了起始位置。
文本文件和二进制文件
根据数据的组织方式,数据文件被称为文本文件和二进制文件。
- 二进制文件:数据在内存中以二进制的形式存储,如果不加转换地输出到外存,就会产生二进制文件。
- 文本文件:如果要求在外存上以ASCII码的形式进行存储,则需要在存储之前进行转换。以ASCII字符的形式存储的文件就是文本文件。
数据在文件中的存储方式:
字符在内存中一律以ASCII码的形式进行存储,数值型数据既可以用ASCII值的形式存储,也可以使用二进制的形式存储。
例如:整数10000,如果以ASCII值的形式输出到磁盘上,则这个整数在磁盘中占用5个字节(每个字符对应1个字节),而如果以二进制的形式输出,则在磁盘上占4个字节(VS2013测试)。
使用例
#include<stdio.h> int main() { int a = 10000; FILE* pf = fopen("test.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } fwrite(&a, sizeof(int), 1, pf); fclose(pf); pf == NULL; return 0; }
打开 test.txt ,观察输出结果
发现无法看懂二进制文件,这里使用VS2022编辑器打开:
其中10 27 00 00 就是10000通过二进制方式写入文件的数据,可以使用计算器计算:
补齐这串数字:
00000000 0000000 00100111 00010000
转为十进制
00 00 27 10
比较 10 27 00 00 ,发现这是通过倒序存入的。
文件读取结束的判定
被错误使用的 feof
注:在文件读取的过程中,不能把feof函数的返回值直接用于判断文件的结束与否。
feof函数应该被应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件末尾结束。
1. 文本文件读取是否结束,判断返回值是否为 EOF(fgetc),或者 NULL(fgets)。
例如:
-
fgetc函数,读取结束时,返回EOF。(正常读取时,返回字符的ASCII值)
-
fgets函数,读取结束时,返回NULL。(正常读取时,返回存放字符串的空间的首地址)
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
-
fread函数,读取结束时,返回实际读取到的完整元素的个数。如果发现 读取到的完整元素的个数 < 指定的元素个数,这次读取就算最后一次读取。
例子1 - 把test.txt文件拷贝一份,生成test2.txt
int main()
{
FILE* pfread = fopen("test.txt", "r");
if (pfread == NULL)
{
perror("fopen");
return 1;
}
FILE* pfwrite = fopen("test2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);
pfread == NULL;
perror("fopen");
return 1;
}
//文件打开成功,读写文件
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
//写文件
fputc(ch, pfwrite);
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
先在 test.txt 内存入数据:
执行程序,成功,出现 test2.txt :
如果要判断文件是否遇到结束标志,可以:
feof函数的正确使用
如果是遇到文件末尾结束的情况,feof函数会返回一个非0的值。否则,返回0。
代码参考自 cplusplus.com
#include<stdio.h>
int main()
{
FILE* pFile;
int n = 0;
pFile = fopen("test.txt", "rb");
if (pFile == NULL)
perror("fopen");
else
{
while (fgetc(pFile) != EOF)
{
++n;
}
if (feof(pFile))
{
puts("遇到文件结束标志,文件正常结束。");
printf("总字节数为: %d\n", n);
}
else puts("没有遇到文件结束标志。");
fclose(pFile);
}
return 0;
}
文件缓冲区
ANSIC标准采用“文件缓冲区”处理数据文件。所谓的缓存文件系统是指系统自动地在内存中为程序中的每一个正在使用的文件开辟一块“文件缓冲区”。
- 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
- 如果磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,(充满缓冲区)然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定。
为什么存在文件缓冲区?
如果不存在文件缓冲区,那么程序数据区内的数据将会直接往硬盘内输出数据。此时,输出一个字符,就要往硬盘上写一个字符,输出一个,就要写入一个。总是需要“写入”这个操作。
但是写入操作是需要代价的,这个操作的完成需要通过操作系统,而操作系统往往不会在同一时间不进行任何工作,而单单执行写入的指令。更多的时候,操作系统需要停下目前正在进行的工作,转而执行新提出的写入操作。
如果操作系统被这样频繁地打断,那么操作系统可以进行的工作任务就会十分有限。这回导致效率十分低下。
例子(注:fflush在高版本下的VS似乎无法使用,但是笔者的VS2022还可以正常运行。)
#include<stdio.h>
#include<Windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放到缓冲区
printf("睡眠10秒——已经写了数据,打开test.txt文件,发现文件内没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,缓冲区内的数据才被写入文件(磁盘)内
printf("再次睡眠10秒——此时再次打开test.txt文件,文件内有内容\n");
Sleep(10000);
fclose(pf);//注意:fclose函数在关闭文件时,也会刷新缓冲区
pf = NULL;
return 0;
}
执行程序:
只要刷新,数据就会写入。