超级详尽的”文件的操作“

1. 为什么使用文件?

我们使用电脑时,进行的操作都是在内存上完成的。但是内存是临时的,一旦你退出程序,内存回收,数据就丢失了,再次运行程序,就看不到上次程序的数据了。如果想要数据达到持久化保存的目的,我们可以使用文件。


2. 什么是文件?

你电脑磁盘上的文件就是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。

2.1 程序文件

程序文件包括源程序文件(后缀为.c),目标文件(Windows环境下后缀是.obj),可执行程序(windows环境后缀为.exe)。

2.2 数据文件 

文件的内容还可以是程序运行时读写的数据,可以是程序运行需要从中读取数据的文件,或者是输出内容的文件。

本篇文章讲的是数据文件。

2.3 文件名

⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。
⽂件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了⽅便起⻅,⽂件标识常被称为文件名


3. ⼆进制文件和文本文件?

根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件。数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存,就是⼆进制⽂件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是本文件。
一个数据在内存中是怎么存储的?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节(VS2019测试)。


4. 文件的打开和关闭

4.1 流和标准流

4.1.1 流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。

4.1.2 标准流

那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?那是因为C语⾔程序在启动的时候,默认打开了3个流。

  • stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
  • stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
  • stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。

这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为⽂件指针
C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。

4.2 文件指针

缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”。
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名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;

不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异。每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节。⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。

下⾯我们可以创建⼀个FILE*的指针变量:

FILE* pf;//⽂件指针变量

 定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(是⼀个结构体变量)。通过该⽂件信息区中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件。如下图:

4.3 文件的打开和关闭

⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件。
在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。ANSIC 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。

//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );

mode表⽰⽂件的打开模式,下⾯都是⽂件的打开模式:

 文件使用方式  含义如果指定文件不存在
   “r”(只读)为了输入数据,打开一个已经存在的文本文件出错
  “w”(只写)为了输入数据,打开一个文本文件建立一个新的文件
  “a”(追加)向文本文件尾添加数据建立一个新的文件
  “rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输入数据,打开一个二进制文件建立一个新的文件
 “ab”(追加)向一个二进制文件尾添加数据建立一个新的文件
 “r+”(读写)为了读和写,打开一个文本文件出错
 “w+”(读写)为了读和写,建立一个新的文件建立一个新的文件
 “a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
 “rb+”(读写)为了读和写,打开一个二进制文件出错
 “wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
 “ab+”(读写)打开一个二进制文件,在文件的尾进行读和写建立一个新的文件

实例代码:


5. 文件的顺序读写       

5.1 顺序读写函数介绍

     函数名                  功能               适用于
    fgetc字符输入函数         所有输入流

    fputc

字符输出函数           所有输出流
    fgets文本行输入函数         所有输入流
    fputs文本行输出函数         所有输出流
    fscanf格式化输入函数         所有输入流
    fprintf格式化输出函数         所有输出流
    fread二进制输入             文件
    fwrite二进制输出             文件

上面说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输入流(如文件输入流);所有输出流⼀般指适用于标准输出流和其他输出流(如文件输出流)。

5.1.1 fgetc和fputc

如何使用第一个函数fgetc呢?建议去cplusplus网站直接搜索你想用的函数,就能找到这个函数的用法。fgetc如下图:

返回类型是int,参数是文件指针FILE*,因为char类型也是整型家族,字符存储形式就是ASCII码值,所以不用疑惑。完整的写一遍:

打开文件之后,需要判断pf是否为空指针,因为有可能接受失败,如果是空指针,可以打印出错误信息,并终止程序,返回1表示程序异常。接下来就是创建一个字符变量来接收fgetc读取到的字符。最后关闭文件,还要记得把pf赋值为空指针,防止pf成为野指针。

但是,如果想要有多个字符读取,需要重复上面的操作,十分繁琐。再深入了解fgetc,可以知道fgetc接收失败时会返EOF,因此可以写一个while循环。

fputc函数原型如下,第一个参数是字符。实例如下:

5.1.2  fgets和fputs

fgets和fputs与fgetc和fputc类似的,两个函数原型如下:

 fgets要注意的是:

  • 读取字符时,将读取到的字符串存储到str中,直到读取(num-1)个字符,或者到达换行符或文件结束符,便停止读取。

  • 换行符使fgets停止读取,但它被函数认为是一个有效字符,并包含在复制到str的字符串中

  • 在复制到str的字符之后,将自动追加一个终止null字符。

  • 请注意,fgets与gets有很大的不同:fgets不仅接受流参数,而且允许指定str的最大长度,并在字符串中包含任何结束换行符。

fputs函数注意项:

  • 将由str指向的C字符串写入流。
  • 函数从指定的地址(str)开始复制,直到到达结束的空字符('\0')。这个终止的空字符不会复制到流中。
  • 注意,fputs与puts的不同之处不仅在于可以指定目标流,而且fputs不会写入额外的字符,而puts会自动在末尾附加一个换行符。

5.1.3 fscanf和fprintf

我们先比较printf和fprintf这两个函数的原型,如下图。你会发现fprintf只多一个参数文件指针FILE*。fprintf要指定一个流,将格式化的数据写入这个流中。而printf的流规定是标准输出流stdout,通常是屏幕和终端,或其他输出显示的设备。

 

再比较scanf和fscanf这两个函数的原型,如下图。你会发现fsanf多一个文件指针参数,fsanf要指定从一个流中读取格式化的数据。而scanf函数,规定从标准输入流stdin,通常是从键盘或其它设备读取数据。

 我们先创建一个结构体变量,然后将结构体变量的数据格式化写入到test.txt这个文件中,之后再读取test.txt的数据,格式化到指定的流中。

第一部分:

第二部分:

5.1.4 fread和fwrite

之前的函数指针文本文件,这两个函数针对的是二进制文件。二进制文件一般机器可以识别,认识读不懂的。函数原型如下:

 这两个函数的第二个参数,是一个数据所占的字节,第三个参数是写入几个这样的数据。

实例如下:

5.2 对比一组函数

  • scanf/fscanf/sscanf
  • printf/fprintf/sprintf

我们上面分析过scanf和fscanf,printf和fprintf的区别。我们先观察另外两个函数的原型,如下图:

ssprintf是把指定格式化的数据转换成字符串,放到第一个参数str中。sscanf是把一个流中字符串转换成格式化的数据,实例如下:


6. 文件的随机读写

6.1 fseek

根据⽂件指针的位置和偏移量来定位⽂件指针。

int fseek ( FILE * stream, long int offset, int origin );

origin指的是从哪个位置开始读取数据,有三种分别是SEEK_SET,SEEK_CUR和SEEK_END。SEEK_SET是文件内容的开头,SEEK_END是文件内容的末尾,SEEK_CUR指的是文件指针此时指向的位置。

此时调整文件指针偏移量,打印出c,代码如下:

6.2 ftell

返回⽂件指针相对于起始位置的偏移量。函数原型如下:

long int ftell ( FILE * stream );

 实例如下:

6.3 rewind

让⽂件指针的位置回到⽂件的起始位置。函数原型如下:

void rewind ( FILE * stream );

实例如下:


7. 文件读取结束的判定

feof注意事项:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。         feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。

1. ⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )               例如:

  • fgetc 判断是否为 EOF 。
  • fgets 判断返回值是否为 NULL。

2. ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。

  • fread判断返回值是否⼩于实际要读的个数。

文本文件的例子:


8. 文件缓冲区

ANSIC 标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为
程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。

 可以通过下面的例子来感受缓冲区的存在。

 这⾥可以得出⼀个结论:因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件的问题。


总结

文件操作这块内容没有很大的难度,拍你想实操应用的方面,需要我们熟悉里面函数的用法就好了。并且内容比较多,更需要耐心的学习。因此不能光看,多敲代码才是王道。多多重复,百炼成钢!

创作不易,如果喜欢这篇文章的话,请留下你的三连哦,你的支持是我创作最大的的动力!!!

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值