本篇将讲解C/C++中文件相关的基本知识和与文件操作相关的函数
目录
文件的概念
虽然大家都知道文件是什么,但还是概况一下:磁盘上的文件就是文件,但在程序设计中我们一般谈的文件有两种:程序文件和数据文件(从文件的功能角度分类的)
程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行文件(windows环境后缀为.exe)
数据文件
数据文件就是存放各种数据的文件,是程序文件读写数据的对象,文件的内容不一定是程序,但也可能是程序
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用
为了方便,文件标识常被称为文件名
文件名包括三部分:文件路径 + 文件名主干 + 文件后缀
例:c:\user\code\test.c,其中 "c:\user\code\" 是文件路径,"test" 是文件名主干,".c" 是文件后缀
二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件和二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII 字符的形式存储的文件就是文本文件
字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储
例:如有整数 10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用 5 个字节(每个字符⼀个字节),而二进制式输出,则在磁盘上只占4个字节(VS2019测试)
流和标准流
注意:关于流,打开文件方式的不同其实就是打开的流不同,你打开的什么流,就只能进行与其相关的操作(例,你用 "w" 的方式打开文本流,就只能对这个流进行写入操作(文件的打开的知识点在后面))
注意:同一个文件不能同时进行读写操作,因为就是不能同时对同一个文件的输入流和输出流进行赋值
流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作
理解:你可以把流当成一个中介,你要对一个文件或一个其他外部设备进行读写,只需要对相应的流进行读写,它会自动把你要进行的读写完成(不然每个外部设备的读写都需要进行不同的操作的话对于程序员过于难了)
标准流
当C语言程序启动时,默认打开了 3 个流,我们从键盘输入数据,向屏幕上输出数据时,都是通过这几个流输入输出的,这 3 个流就是标准流
stdin - 标准输入流,在大都数环境从键盘输入,scanf 函数就是从标准输入流中读取数据的
stdout - 标准输出流,大多数的环境中输出至显示器界面,printf 函数就是将信息输出到标准输出流中
stderr - 标准错误流,大多数环境中输出到显示器界面
stdin、stdout、stderr 三个流的类型是:FILE*,通常称为文件指针(这在下面会讲)
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等),这些信息是保存在⼀个结构体变量中的,该结构体类型是由系统声明的,取名FILE(类型重命名为 FILE 的)
每当打开一个文件的时候,系统会根据文件的情况自动创建一个 FILE 结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个 FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便
我们通过这样的一个指针变量就能访问到文件的文件信息区(结构体变量里存放的信息),通过访问信息区的信息就能访问到该文件的信息,也就是说,通过文件指针变量能够间接找到与它关联的文件
图例:
文件的打开和关闭
文件打开和关闭分别对应 fopen 函数 和 fclose 函数
fopen 函数
声明:FILE * fopen ( const char * filename, const char * mode );
用 mode 的方式打开文件名为 filename 的文件,并返回一个指针,这个指针指向一个根据文件信息创建的结构体,我们就可以通过这个指针维护文件(返回的指针就是文件指针,也就是流的类型,也可以变相理解就是流)
注意:你以什么方式打开文件,就相当于打开了什么类型的流,就只能进行什么样的操做
关于文件的打开模式,如下:
fclose 函数
声明:int fclose ( FILE * stream );
输入 fopen 所返回的 FILE 类型的指针,关闭与其有关系的流,也就是关闭了这个文件
例:
#include<stdio.h>
int main()
{
FILE* pf = fopen("text1.c", "w"); - 以写的形式打开文件 text1.c(相当于打开了文件 text1.c 的输出流)
//注意:如果 text1.c 不存在会自动创建一个,因为这是写入,若是读取则会报错
if (pf == NULL) - 判断文件打开是否成功
{
perror("write to text1.c"); - perror 的作用是打印括号里的字符串后,再打印" :"和 "错误信息"
return 1;
}
//向文件 text1.c 写入数据
// ...
fclose(pf); - 关闭文件
pf = NULL; - pf 置为空指针,避免野指针
return 0;
}
文件读写函数介绍
上面讲了如何打开/关闭文件,下面讲解对文件的读写
注意一个东西:读(取) == 输入,写(入) == 输出
顺序读写函数
顺序读写的意思就是,每读写一个数据,文件指针(光标)就会向后移动一个,读写都是一个一个按顺序来的
fgetc
声明:int fgetc ( FILE * stream );
从 stream 读取一个字符,返回它的 ASCII 码,读取失败返回 EOF,可循环(光标跟着顺序走)
fputc
声明:int fputc ( int character, FILE * stream );
往 stream 写入一个字符,返回这个字符的 ASCII 码,输出失败返回 EOF,可循环(光标跟着顺序走),对一个文件输出时,它会覆盖文件之前的数据
fgets
声明:char * fgets ( char * str, int num, FILE * stream );
从 stream 读取最多 num - 1 个字符到 strl 里,并返回 str 的指针,读取失败返回 NULL 指针,因为 fgets 读取字符串时将会在末尾预留一个位置给 '\0',所以读取的字符至少会比 num 少一个
fputs
声明:int fputs ( const char * str, FILE * stream );
往 stream 写入字符串 str,返回一个非负数,写入失败返回 EOF
fscanf
声明:int fscanf ( FILE * stream, const char * format, ... );
作用同和用法和 scanf 基本一致,不过多了个参数 stream 指定从哪个流里读取数据
fprintf
声明:int fprintf ( FILE * stream, const char * format, ... );
作用和用法同 printf 基本一致,不过多了个参数 stream 指定往哪个流里写入数据
fread
声明:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从 stream 读取 count 个字节大小 为 szie 的对象到 ptr 中,返回成功读取的对象数(出现错误或文件尾条件时,对象数可能小于 count),若 size 或 count 为零,则返回零且不进行其他动作。其原理是将每个对象解读为 unsigned char 数组,并对每个对象调用 size 次 fputc 以将那些 unsigned char 按顺序写入 stream
fwrite
声明:size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
从 ptr 向 stream 写入 count 个大小为 size 的对象,返回成功写入的对象数,若 size 或 count 为零,则返回零且不进行其他动作。其原理同 fread 一样
举一个 fscanf 和 fprintf 的例子:
随机读写函数
fseek
声明:int fseek ( FILE * stream, long int offset, int origin );
根据文件指针的位置和偏移量来定位文件指针
stream 是目标流,offset 是要偏移的量,origin 是开始偏移的起始位置(有三种)
SEEK_SET 是从文件开头开始
SEEK_CUR 是从当前文件指针(光标)开始
SEEK_END 是从文件末尾开始
注意: 文件开头光标是在第一个数据那,当前光标是在你最后读写的数据的后面一个位,文件末尾光标是在所以数据完后的后面一个位,因为光标读写完一个数据后会向后移动等待读写下一个数据,不要把光标的位置搞错了
ftell
声明:long int ftell ( FILE * stream );
返回文件指针相当于起始位置的偏移量,注:文件读取结束等时光标是在数据末尾后的空白上
rewind
声明:void rewind ( FILE * stream)
让文件指针的位置回到文件的起始位置
文件缓冲区
最后,在简单讲解下文件缓冲区,方便我们理解数据的传输过程
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为
程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓
冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
图例:
再介绍一个清除缓冲区的函数:fflush
fflush 函数
声明:int fflush ( FILE * stream );
刷新所指向的流的缓冲,如果 stream 是 null 指针,则刷新所有此类流,成功返回 0,失败返回 EOF
注意:当缓冲区被刷新时,其中的数据会直接传输到程序数据区(不然会等缓冲区满后才传输数据),fclose 函数使用时也会刷新缓冲区
例:如果使用文件读写函数时,如果去调试的话,会发现当文件读写函数运行后文件中数据不会改变,运行 fclose 函数后,文件才会被改变
再给大家补充个额外小知识:
像上面我的文件名是 " ./../text.txt ",这其实是相对文件的写法
相对文件位置:
. :表示当前目录
.. :表示上级目录
它们可以循环使用的,比如:./../../../ 表示当前文件目录的上级的上级的上级
而绝对位置就是我们平时用/看的那样:C:\code\Beginner\study
注意:文件目录中:/ 和 \ 是一样的,不过在程序中,\ 需要转义写作 \\ 表示单纯的 \ 才行,而 / 可以直接使用