首先,C语言是可以操作文件的,虽然并不是很常用
1,为什么使用文件
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用⽂件。
2,什么是文件
我们一般谈的文件按功能分类有两种:程序文件,数据文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows 环境后缀为.exe)
⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或 者输出内容的⽂件。这样的文件就是数据文件。
一般的我们处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显示到显示器上。这也称为标准输入输出。
但有时我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处 理的就是磁盘上⽂件。
⼀个⽂件要有⼀个唯⼀的文件标识,以便用户识别和引⽤。
⽂件名包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀
例如: c:\code\test.txt 。为了⽅便起见,⽂件标识常被称为⽂件名。
下面我们讨论的主要都是数据文件。
3,二进制文件和文本文件
根据数据的组织形式,数据⽂件被分为**⽂本⽂件和⼆进制⽂件**。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是**⼆进制⽂件**。
还有一种判断方法,打开一个文件,你看得懂的就是文本文件,看不懂的就是二进制文件:
左边就是文本文件,右边就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是文本⽂件。
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
如整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节。
4,文件的打开和关闭
4.1,流和标准流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出 操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流 想象成流淌着字符的河。C程序针对⽂件、画面、键盘等的数据输⼊输出操作都是通过流操作的。 ⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
一般读写文件时,都是1,先打开和文件相关的流,2,再读写文件,3,最后关闭和文件相关的流
C语⾔程序在启动的时候,默认打开了3个标准流:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出 流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作就是因为默认打开了这三个流。
stdin、stdout、stderr 三个流的类型是: FILE *,通常称为⽂件指针。 C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。
4.2,文件指针
缓冲⽂件系统中,关键的概念是"⽂件类型指针",简称"⽂件指针"。
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名 字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系 统声明的,取名 FILE.
不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异。
每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信 息,使⽤者不必关⼼细节。 ⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。
VS2022中FILE的定义如下:
cpp
#define _FILE_DEFINED
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
4.3,文件的打开和关闭
⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件。 在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了 指针和⽂件的关系。
ANSI C 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。
fopen打开文件失败时会返回空指针NULL
mode表示文件打开模式,有:
|-------------|--------------------------|---------------|
| ⽂件使⽤⽅式 | 含义 | 如果指定⽂件不存在 |
| "r"(只读) | 为了输⼊数据,打开⼀个已经存在的⽂本⽂件 | 出错 |
| "w"(只写) | 为了输出数据,打开⼀个⽂本⽂件 | 建⽴⼀个新的⽂件 |
| "a"(追加) | 向⽂本⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
| "rb"(只读) | 为了输⼊数据,打开⼀个⼆进制⽂件 | 出错 |
| "wb"(只写) | 为了输出数据,打开⼀个⼆进制⽂件 | 建⽴⼀个新的⽂件 |
| "ab"(追加) | 向⼀个⼆进制⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
| "r+"(读写) | 为了读和写,打开⼀个⽂本⽂件 | 出错 |
| "w+"(读写) | 为了读和写,建议⼀个新的⽂件 | 建⽴⼀个新的⽂件 |
| "a+"(读写) | 打开⼀个⽂件,在⽂件尾进⾏读写 | 建⽴⼀个新的⽂件 |
| "rb+"(读写) | 为了读和写打开⼀个⼆进制⽂件 | 出错 |
| "wb+"(读 写) | 为了读和写,新建⼀个新的⼆进制⽂件 | 建⽴⼀个新的⽂件 |
| "ab+"(读 写) | 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 | 建⽴⼀个新的⽂件 |
补充:以写的形式打开文件,如果文件存在,会清空文件
当前目录下没有该文件,就报错了
使用写的方式打开就没有报错了,而且在当前目录下给我创建了一个文件
不仅可以打开当前目录下的,还可以打开其它目录下的,就是用绝对路径,相对路径
什么是绝对路径,什么是相对路径,绝对路径和相对路径的区别?-CSDN博客
至于 / 和 \ ,在C语言里都可以,只不过 使用 \ 要注意转义字符
5,文件的顺序读取
顺序读取函数有:
|-------------|-------------|-----------|
| 函数名 | 功能 | 适⽤于 |
| fgetc | 字符输⼊函数 | 所有输⼊流 |
| fputc | 字符输出函数 | 所有输出流 |
| fgets | ⽂本⾏输⼊函数 | 所有输⼊流 |
| fputs | ⽂本⾏输出函数 | 所有输出流 |
| fscanf | 格式化输⼊函数 | 所有输⼊流 |
| fprintf | 格式化输出函数 | 所有输出流 |
| fread | ⼆进制输⼊ | ⽂件输⼊流 |
| fwrite | ⼆进制输出 | ⽂件输出流 |
上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀ 般指适⽤于标准输出流和其他输出流(如⽂件输出流)。
5.1,fputc和fgetc
fputc把字符写到文件中,如图:,为什么可以把这些字母全部写到文件里呢而不会覆盖呢,因为它们是顺序读取函数,每写入一个字符后,光标都会向后移动一位,多所以就能够全部写入并全部读取了,
注意,fgetc的返回值如果读取成功就返回读取到的字符,读取失败时会返回EOF,也就是-1,所以返回值类型是int,如果用char,EOF就无法接收。所以用int更好。
因为是针对所用输入输出流,所以也可以从键盘上读取,再写到显示屏上
5.2,fputs和fgets
fputs,和fgets都是读取一行
虽然fputs是读取一行,但是使用多个fputs写到文件中并不会换行,要换行还要自己加上\n
而fgets是从文件中读取,把它存储到一个字符数组中,num是最大读取个数
当num没有文件中字符多时,就读取num个字符,但要注意,这num个字符中包括了\0,
所以实际读取的字符个数是num-1个,
当num大于这一行字符个数时,就把这一行全部读取出,末尾再加一个\0
fgets一次读取一行,第二次调用fgets读取同一个文件才读取第二行
同样的,fgets和fputs也可以从标准输入输出流中读取,这里就不再演示了
5.3,fscanf和fprintf
可以发现,fprintf和printf的参数很像,所以使用fprintf时就把它想成printf,只不过在前面加一个文件指针,当然,如果这个指针是stdout,那么就是打印到显示屏上,
fscanf也与scanf很像,就不再赘述了
5.4,fread和fwrite
前面的函数都是读写到文本文件中,而接下来的两个是读写到二进制文件中,
fwrite就是把ptr指向的内容中count个大小为size的数据写到文件中,fread就刚好相反
5.5,对比:scanf fscanf sscanf , printf fprintf sprintf
scanf是针对stdin的格式化输入函数
printf是针对stdout的格式化输出函数
fscanf是针对所有输入流的输入函数
fprintf是针对所有输出流的输出函数
sscanf是从字符串中格式化提取数据
sprintf是从把数据格式化转化成字符串
6,文件的随机读取
上面的顺序读取是一个一个向后读取,那有没有什么办法能够跳跃式的读取,想读哪里就读取哪里,这就是文件的随机读取了。
6.1,fseek
根据⽂件指针的位置和偏移量来定位⽂件指针(⽂件内容的光标)
第二个参数是偏移量,第三个参数是源头即参考点,即相对于参考点的偏移量
origin有三种取值,分别是文件开头,光标当前位置,和文件末尾
顺序读取是读取到了ab,
那如果我们想第二个读取e,该怎么做呢。我们就使用fseek来修改光标的位置,e相对于文件开头的偏移量是4,可以理解为数组下标
可以看到此时就能够读取到e了。
同样的使用另外两种origin也可以达到这样的效果
要注意使用SEEK_END时,是向左偏移,偏移量就是负的
6.2,ftell
返回⽂件指针相对于起始位置的偏移量
还是这个例子,e的偏移量是4,读完e后指针后移,此时偏移量就是5了
6.3,rewind
让⽂件指针的位置回到⽂件的起始位置
rewind之后光标就回到了起始位置,再去读取就又从第一个开始读了
7,文件读取结束的判定
文件读取结束有两种情况,一种是正常的读取到了文件末尾,另一种是读取时发生错误导致读取结束,
在文件读取结束后,要判断文件是那种原因导致读取结束就要用到两个函数------feof,ferror。
我们知道EOF是文件读取结束的的标志,但feof使用时是来判断文件读取时是否遇到了文件末尾
ferror是来判断是否是发生了错误而导致文件读取结束。
文件读取时都会设置标记1/0,来判断是否遇到了文件结尾(1),以及是否发生了错误(1),注意是两个不同的标记,分别判断。
那么如何判断文件是否读取结束呢?
这就要利用到读取文件的函数的返回值了,
- ⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets ) 例如:
• fgetc 判断是否为 EOF.
•fgets 判断返回值是否为 NULL .
- ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。 例如:
• fread判断返回值是否⼩于实际要读的个数。
这就是读取到文件末尾了
这是因为以写到方式打开却偏要去读,所以就发生错误了
8,文件缓冲区
ANSI C 标准采⽤"缓冲⽂件系统" 处理数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程 序中每⼀个正在使⽤的⽂件开辟⼀块"⽂件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲 区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊ 到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲 区的大小根据C编译系统决定的。
从缓冲区写到硬盘上有三种情况:
1,写满缓冲区
2,主动刷新缓冲区
3,fclose------刷新缓冲区
验证缓冲区代码:
cpp
#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);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}