【手把手带你进阶】C语言:文件操作


哈喽!大家好呀~好久不见啦!

回头一看,发现关于C语言的内容我们已经讲得差不多了,还剩下文件操作和程序的编译这两部分。

其中文件的操作比较简单,在工作中用得也比较少,虽然如此,我们也不能不学它呀~毕竟这也是C语言的一部分,不学它好像不太完整,而且万一以后要用到怎么办呢?所以我们还是老老实实地把文件的操作过一遍吧!

为什么使用文件

在之前编写的程序中,我们会发现在程序运行的过程中,我们当下输入的信息在程序结束之后就不再保存了,当下一次再运行程序的时候,上次输入的信息就没有任何记录。

如果我们希望每次运行程序时输入的信息都被保存下来,这时候我们就可以把信息写入文件中,因为文件是存储在磁盘上的,所以程序结束后,程序运行的内存还给操作系统了,但是文件里面的内容却不会被抹去(除非我们自己执行了删除操作),从而达到保存信息的目的。

所以我们说,使用文件可以让我们将数据直接存放在电脑的硬盘上,从而做到数据的持久化。

什么是文件

磁盘上的文件都是文件。

我们打开电脑C盘、D盘或者E盘,我们可见的里面存放的所有东西都是文件。
在这里插入图片描述
而在程序设计中,我们一般从文件功能的角度将文件分为两类:程序文件和数据文件。

程序文件

我们平常码代码得到的后缀为.c(源程序文件) / .obj (Windows环境下的目标文件)/ .exe(Windows环境下的可执行程序)等的文件都称为程序文件。

数据文件

而我们运行程序的时候,可能需要把一些信息写到文件里面或者从文件中读取数据。这些文件用来存储程序运行时读写的数据的文件就是数据文件,后缀为.txt。

那么今天本文讨论的都是数据文件。

在之前的学习中,我们总是把信息通过键盘输入,再输出到屏幕上。而今天,我们操作的对象是文件,则我们是把信息输出到磁盘上,或者是把数据从磁盘上读取到内存中使用,所以这里处理的就是磁盘上的文件。

文件名

那么一个文件到底是怎么描述的呢?

当然是借助于文件名了。

一个文件要有一个唯一文件标识,以便于我们来识别和引用。

而我们通常利用文件名来对文件进行识别和引用。
所以我们会发现同一路径下的两个文件名是不能相同的。

一个文件的文件名包含3个部分:文件路径+文件名主干+文件后缀

例如:c:\myCode\test.txt
c:\myCode - 文件路径 - 表示在C盘底下的一个叫myCode的文件夹里面的文件
test - 文件名主干
.txt - 文件名后缀

为了方便,通常我们就把文件标识成文文件名。

文件的打开和关闭

文件指针

首先我们要对文件进行定位,这时候我们需要借助文件指针。

这是缓冲文件系统中一个关键的概念,称为“文件类型指针”,简称为“文件指针”。

当我们操作文件的时候,我们首先要打开文件,而当我们打开文件的时候,系统就在内存中开辟了一个相应的文件信息区,用来存放诸如文件名、文件状态、文件当前位置等的信息,这些信息都被保存在一个结构体变量中,由系统进行声明,取名为FILE。

所以只要我们打开文件,内存中就会相应地生成一个文件信息区,从而把文件和程序关联上。

以下给出VS2013环境下的文件结构体。
在这里插入图片描述
不同编译器中的FILE类型包含的内容不完全相同,实现的方式也未必一样,但是基本上是大同小异的。

我们作为文件的使用者,不必关心文件信息区中的文件是如何填充的。

程序一般通过一个FILE指针来维护这个FILE结构的变量,而我们通过这个FILE指针来找到文件信息区,通过文件信息区把我们和文件关联起来,从而对文件进行操作。
在这里插入图片描述

文件的打开和关闭

那么知道了文件指针之后,我们应该如何打开文件呢?

实际上,在编写程序的时候,我们打开文件的同时,系统都会返回一个FILE*的指针,该指针指向该文件,由此建立了指针和文件的关联。

那么我们到底怎么打开文件呢?
C语言给我们提供了一个叫fopen的函数,用来打开文件。

我们可以cplusplus网站上查一下这个函数。
在这里插入图片描述

filename - 文件名 - 用于找到文件
mode - 文件打开模式 - 用于决定对文件是进行读还是写等等操作

在网页下方我们可以看到fopen函数给我们提供的打开模式有以下几种:
在这里插入图片描述

下面给出更全的文件使用方式:
在这里插入图片描述
使用如下:
在这里插入图片描述
下面我们再来看看fopen指针的返回值:
在这里插入图片描述
所以当我们用指针接收fopen的返回值后,要先检查一下是否打开成功。
在这里插入图片描述
那么打开文件之后,我们就要对文件进行操作,操作完文件之后,要关闭文件。

而关闭文件用到的函数是fclose。
在这里插入图片描述
在这里插入图片描述
那么我们现在运行一下这个程序。由于我在代码的路径下并没有创建这个文件,所以当我运行程序的时候,屏幕上会打印出错误信息。
在这里插入图片描述
现在我在文件源程序文件的目录下创建一个data.txt文件。
在这里插入图片描述
然后再运行程序。
在这里插入图片描述
程序没有报错,说明文件打开成功。

那么这时候如果我们把data.txt文件放到别的路径下,程序还能不能打开它呢?

当然可以,但是我们要向程序提供文件的路径。

假如我把文件放到桌面上。
在这里插入图片描述
这时候只需要右键将文件的路径复制一下。
在这里插入图片描述
再粘贴到fopen的第一个参数位置 - 即文件名处。
在这里插入图片描述

注意要避免路径中的斜杠和字符结合形成转义字符,所以我们要在有斜杠的地方再加上一个斜杠,防止转义。
在这里插入图片描述
这时候我们再次运行代码,程序依然没有任何错误。
在这里插入图片描述
说明文件打开成功。

注意,如果我们只提供了文件的主干名给fopen,则相当于是提供了相对路径,而如果是把文件路径传给了fopen,则是提供了绝对路径。

大家可以看到,上面的表格中最后有一栏,如果指定的文件不存在。
其中有的函数会出错,而有的函数会创建一个新的文件。

在这里插入图片描述
在这段代码中,我要在桌面打开一个名为abc的txt文件,用写的方式进行打开,但是桌面并没有这个文件,当我运行程序之后,程序并没有报错。
在这里插入图片描述
再回到桌面,我们可以看到,桌面上创建了一个名为abc的txt文件。
在这里插入图片描述

文件的顺序读写

当我们了解了如何打开和关闭文件之后,接下来我们就要看看到底应该如何来读写文件。

当我们再程序中要把数据写到文件当中去时,我们把写文件的操作称为输出,相应地,我们把从文件中读取数据到内存中称为输入。

在这里插入图片描述

明白了以上逻辑之后,我们依次来看下面这些文件读写函数:

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

接下来我们就用这两个函数来对文件分别就进行写和读。

fputc 字符输出函数

首先我们去cplusplus网站上看一下fputc这个函数应该怎么用。
在这里插入图片描述

character - 要被写进去的字符
strea - 指向文件信息区结构体的指针

下面我们用代码实现。

在这里插入图片描述
运行程序,可以看到控制台中没有任何内容。
在这里插入图片描述
我们再打开桌面的data.txt文件,发现里面被写入了“hei”这三个字符。
在这里插入图片描述
由此,我们可以想见,每次写完一个字符之后,pf指针都应该向后挪了一位,这样我们再次调用fputc的时候,才能把字符往后写进去。

在这里插入图片描述
字符输出函数是适用于所有输出流的,即它既可以用于文件的输出,也可以用于标准输出。

那么输出流、输入流是什么呢?
在C语言中,存在着一个叫流的概念。而流是一个高度抽象的概念,我们可以把它和水流进行一个比拟。
我们把文件、屏幕、网络、光盘、软盘(早期)这些称为外部设备,而我们在往这些外部设备中写数据的时候要一个一个地去对应研究,比较麻烦,这时候C语言就抽象出来了一个流,由流来负责把数据输出到这些外部设备中,而我们程序员只需要把数据交给流就好了,这样极大地为程序员提供了便利。
而实际上,我们上面写的代码中,pf相当于是一个指向文件流的指针,我们是通过文件流把数据写到文件中。

另外,只要我们运行了C的程序,就默认打开了3个流:

标准输出流 - stdout
标准输入流 - stdin
标准错误流 - stderr
而上述三个流的类型都是FILE*

只要打开了这三个流,我们打开程序之后就可以直接进行数据的输入或者输出了。

而上述fputc函数是适用于所有输出流的,即我们既可以把数据输出到文件中,也可输出到屏幕上等等。

下面我们通过程序来看看。
在这里插入图片描述

fgetc 字符输入函数

下面我们来看看fgetc函数。

在这里插入图片描述
根据cplusplus网站给出的指引,fgetc函数的参数是FILE*类型的指针,返回的是一个int类型的值,说明我们应该把文件的指针pf传给fgetc,然后用一个整型变量来接收读到的字符的ASCII值。

在这里插入图片描述
由上图中的多次读取,每次都能读到下一个字符,说明每读完一个字符,文件的指针都会向后移动一位。

在这里插入图片描述
下面我们通过标准输入流来读取数据,并打印到屏幕上:
在这里插入图片描述

fputs 文本行输出函数

在这里插入图片描述
fputs的参数是一个字符指针和一个文件类型的指针,可以猜到我们应该是把要写进文件的字符串的指针和指向文件的指针传给函数。

下面看代码:
在这里插入图片描述

现在打开abc.txt文件,可以看到被写入的字符串。
在这里插入图片描述

fgets 文本行输入函数

在这里插入图片描述
刚才的fputs函数是把数据写入文件中,那么fgets应该是从文件中读取字符串,所以第一个参数应该是指向一个字符数组,这个数组用来存放读取到的字符串,第三个参数是指向文件的指针,中间的参数int num应该是每次读取的最大字符数。

下面看代码和程序运行结果:
在这里插入图片描述
从上图程序运行结果中,我们看到虽然我们希望读5个字符,但是fgets函数实际上只读了4个字符,并且在第五个字符处放了一个’\0’,并且下一次读字符的时候,fgets是从上一次读取的最后一个字符的下一个位置开始读取。
在这里插入图片描述
如果我们把读取的最大字符数改为20。
在这里插入图片描述

fprintf 格式化输出函数

在这里插入图片描述
大家有没有发现,这个函数和printf函数长得很像,所我们可以对标一下pritf函数。
在这里插入图片描述
我们可以看到,fpritf的参数比pritf多了一个FILE*的指针,其他都一样,那么下面我们来试着用一下fprintf函数。

在这里插入图片描述
注意:格式化输出其实就是按照某种格式进行输出,但是虽然我们是按照给定的格式输出的,在实际写进文件的过程中,是把数据转化成字符写进文件中的,即我们看到的data.txt文件中的内容其实是一个个字符。

fscanf 格式化输入函数

同理,我们也将fscanf和scanf函数对比着看。
在这里插入图片描述
在这里插入图片描述
我们可以发现,fscanf也是比scanf多了一个FILE*的指针。

所以其实反过来读文件也是类似的方法,只是这次我们是把读到的信息放到变量s中去。

在这里插入图片描述
以上我们用到的函数都是以文本的形式对数据进行输入和输出的,那么我们除了以文本的形式读写数据之外,C语言还提供了二进制的读写方法,下面我们来看一下两个函数。

fwrite 二进制输出函数

在这里插入图片描述
如果不清楚函数的各个参数具体指什么,我们就往下翻翻指引。
在这里插入图片描述
我们接下来还是用代码试一试。
在这里插入图片描述
我们会发现,以二进制形式写文件之后,我们是看不懂文件中的字符的,但是我们可以用二进制输入函数来帮我读取(并翻译)这些数据。

fread 二进制输入函数

在这里插入图片描述
可以看到,读函数和写函数的参数类型是一样的, 我们还是直接看代码。

在这里插入图片描述

比较下面三个输入输出函数

函数名功能函数名功能
printf适用于格式化(屏幕) 输出scanf适用于格式化(键盘)输入
fprintf适用于所有流(键盘/文件)的输出fscanf适用于所有流(键盘/文件)的输入
sprintf把格式化数据转化为相应的字符串sscanf把字符串转化为对应格式的数据

下面讲一下sprint和sscanf函数。

我们还是先到cplusplus网站上分别查一下这两个函数。
在这里插入图片描述
在这里插入图片描述
我们可以看到,这两个函数的参数是一样的,只是scanf的char*前面加了const修饰,这是因为sscanf是把字符串转化成格式化的数据,说明字符串是不能比改变的,sprintf则是将格式化的数据放到一个字符指针中,即把数据转化成一个字符串的形式。

在这里插入图片描述

文件的随机读写

上面我们介绍的函数都是按照顺序从前到后进行读写的,我们称为顺序读写。那么如果我想从文件的中间输入或者输出某个数据,这时就涉及到了文件的随机读写。

下面我们依次来看一下这些函数。

fseek

首先打开桌面上的abc.txt文件。我们看到里面有两行字符串,当我们在程序中打开文件的时候,pf默认是指向文件内容中的第一个位置,现在我们希望直接读到中间的’o’字符,我们应该怎么做呢?

在这里插入图片描述
这时候我们就要借助fseek函数啦!

老规矩,还是去cplusplus网站上看一下这个函数的使用指引吧。
在这里插入图片描述
从上图我们可以看到,fseek函数就是用来定位文件指针的。它实际上就是根据文件指针的位置和偏移量来进定位。

函数中的三个参数分别是文件指针,偏移量和起始位置。
在这里插入图片描述
其中SEEK_SET是指文件的开头位置,SEEK_CUR是文件指针当前的位置,SEEK_END是指文件末尾的位置。

当我们刚打开文件的时候,不对文件进行任何操作,这时候文件指针指向文件的起始位置,则这时SEEK_SET和SEEK_CUR一样。

在这里插入图片描述

在这里插入图片描述
如果这时候我们希望的是直接读后面的‘o’,我们就可以利用fseek和指针的偏移量来实现。
在这里插入图片描述
接下来如果现在我们希望下一次往前读‘e’,则偏移量应为多少呢?

答案是-4,因为当我们读完’o’之后,指针又向后走了一步,所以如果要读‘e’,则偏移量为4。

在这里插入图片描述
所以,利用fseek函数,我们就可以通过指针的偏移量来根据我们的需要读取特定位置的数据,即做到随机读取。

但是有时候我们可能并不知道,或者不便于得到指针的偏移量,所以下面我们需要了解帮助我们或者指针偏移量的函数。

ftell

ftell可以告诉我们文件指针相对于起始位置的偏移量。

在这里插入图片描述
下面直接上代码。
在这里插入图片描述

rewind

如果我们已经读了很多内容,文件指针已经走了很远之后,我们希望文件指针恢复到起始位置,这时候就要用到rewind函数。

在这里插入图片描述
我们还是直接上代码。

在这里插入图片描述
我们可以发现,当我们掌握了以上代码之后,我们就可以根据自己的需要读取自己想要的读取的数据。

提问:那么明白了以上函数之后,我们好像看到文件的操作就是通过文件指针的偏移量来进行,那我们可以不通过fopen这些函数而之间用指针和偏移量来对文件直接进行操作吗?

答:fopen是C语言提供给我们的一个函数,但是它为什么可以调用存储在硬盘上的文件呢?
这是因为fopen在内部实现的时候其实是调用了操作系统提供给我们的接口,一般称为系统调用。这样我们才可以通过操作系统来操作文件。

文本文件和二进制文件

我们知道,数据在内存中是以二进制的形式存储的,如果对其不加转换地输出到外存(硬盘)中,这样的文件就是二进制文件。

而如果在存储到外存中之前,数据被转换成ASCII码的形式,则这些以ASCII字符形式存储的文件被称为文本文件。

所以根据文件的组织形式,数据文件被分为文本文件和二进制文件。

我们可以看到,当我们打开一个文本文件,里面存储的信息是我们可以读懂的。
在这里插入图片描述
而如果我们用记事本的形式打开一个exe文件,会发现这是一些我们看不懂的字符,说明它是以二进制的形式存储的,即是一个二进制文件。
在这里插入图片描述

那么一个数据在内存中是怎么存储的呢?

首先,字符是用ASCII形式存储的,而数值型数据在内存中既可以用ASCII的形式存储,也可以用二进制的形式存储。

假设我们有一个整数10000,如果这个数以ASCII的形式输出到磁盘中,则在磁盘中占5个字节(10000共5个字符),而如果以二进制的形式存储,则在磁盘中占4个字节(一个int类型的大小)。

在这里插入图片描述

下面我们用代码将10000以二进制的形式写到文件中去。

在这里插入图片描述
打开data文件之后我们会看到一个不明含义的符号,但是强大的VS编辑器给我们提供了以二进制视角读懂文件的功能,具体应该怎么做呢?我们往下看。

首先进入VS2019,在源文件中把data.txt添加进去。
在这里插入图片描述
在这里插入图片描述
点击确定之后,会看到如下界面:在这里插入图片描述
由此我们可以看到当我们以二进制的形式写文件的时候,它确实是以二进制的形式直接写到文件当中去的。

文件结束的判定

那么我们如何知道一个文件是否读取结束了呢?

前面我们一共讲了11个有关文件读写的函数,而其中有关文件读取的有如下几个:

fgetc
fgets
fscanf
fwrite

下面我们去cplusplus中查看这些函数的返回值。

首先我们看fgetc这个函数的返回值。
在这里插入图片描述
当函数读取成功的时候,fgetc会返回读取到的字符的ASCII码值。如果文件读取失败,则会返回EOF,区别是如果是因为文件结束而读取失败的,文件会设置feof(文件流的指标),如果是因为某些错误而读取失败的,则会设置ferror(错误指示器)。

所以,我们可以通过判断fgetc函数的返回值是否是EOF来判断文件是否读取结束。
在这里插入图片描述
下面我们再看看fgets的返回值是什么。
在这里插入图片描述
所以,我们可以通过函数的返回值是否是NULL来判定文件是否读取结束。

在这里插入图片描述
最后是fscanf和fread函数。
在这里插入图片描述
在这里插入图片描述
fscanf和fread都是返回成功读取到的数据个数,我们可以通过是否少于我们希望读取到的个数来判断文件读取是否结束。

那么当一个文件读取结束之后,我们应该如何判断它是因为错误而结束的还是遇到文件尾结束的呢?

这时候我们就可以用到ferror和feof这两个函数,并且在前面对每个函数返回值的查阅中,我们都看到了对于遇到错误和文件末尾结束时是如何设置它们的。

下面我们举两个栗子~
在这里插入图片描述
在这里插入图片描述
所以总的来说:
feof是用来判断文件是否遇到文件末尾而结束。
ferror是用来判断文件是否遇到错误结束的。

文件缓冲区

标准C中采用“缓冲文件系统”来处理数据文件。

缓冲文件系统是指系统自动地在内存中为程序中的每一个正在使用的文件开辟一块“文件缓冲区”。
从内存写入磁盘的数据会先送到内存缓冲区中,待缓冲区装满之后才一起送到磁盘上。如果是从磁盘读取数据到内存中,则是先将读取的数据装到输入缓冲区,然后再从缓冲区把数据逐个送到程序数据区中。
文件缓冲区的大小根据C编译系统决定。

在这里插入图片描述
下面我们通过一段代码来测试一下。
在这里插入图片描述
文件中没有内容,说明内容写在输入缓冲区中。
在这里插入图片描述
说明内容已经写到文件中了。

注意两个问题:

  1. fflush在一些高版本的VS中可能不能使用。
  2. 刷新缓冲区之后,仍然要睡眠20s,这是因为当我们关闭文件的时候,程序也会刷新缓冲区,所以我们要停留十秒以让我们看到刷新缓冲区的效果。

同时这也进一步说明为什么我们写完文件要关闭文件,因为关闭文件时会刷新缓冲区,这样可以确保我们结束程序的时候数据确实写到文件中去了。

以上就是C语言中关于文件的操作详解啦~

其实对于文件的操作在工作中的使用并不多,但是作为一名程序员,这些基本的操作我们怎么能不知道呢?所以还是要好好掌握,万一哪一天用上了呢?

好啦!C语言的进阶只剩下最后一个部分啦~

博主尽量快点更新,大家一起期待吧!
(顺便给博主点赞加个油吧!!)

本文的代码已经上传到Gitee啦~欢迎取阅哦!

https://gitee.com/fang-qiuhui/my-code/blob/my_code/blog_2021_10_26_file/blog_2021_10_26_file.c

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值