目录
在我们以前写代码的时候,数据都是在内存中存放的,一旦退出程序,数据就不见了,有没有一种方式能把数据写到硬盘上去呢?带着这个问题,我们一起来学习C语言中的文件操作
一.什么是文件?
磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。这就是数据文件,我们着重讨论数据文件。
二.文件的打开和关闭
1.文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
这是VS2013里面的FILE
在我们打开一个文件的时候会自动填充这些信息。也就是说FILE这个结构体类型描述了文件的各种信息。换句话说每打开一个文件,系统就会自动在内存中开辟一个结构体变量,这个结构体类型是FILE类型,这种类型是系统自带的,不是我们人为创建的,存放这种结构体类型的指针是FILE*类型的,通过这种指针可以维护这个结构体。在实际进行文件操作的时候,我们通常在操作这个FILE*类型的指针,这个指针指向了一个FILE类型的结构体,这个结构体里面有又某个文件的各种信息,通过这个结构体就维护了需要的文件。
2.fopen
他的两个参数分别是文件名和打开方式,都是char*类型的。打开方式有一张表,下面是三种常见打开方式
比如我要以只读的方式打开一个文件data.txt就可以写成
文件名和打开方式都要用双引号引起来。
fopen函数的返回值是当前打开的文件的文件信息区的地址。
以只读的形式打开文件成功会返回FILE*的指针变量指向该文件,打开失败返回NULL,因此在使用fopen函数以只读的形式的时候也应该检查返回值。通常这样写
如果是以写的形式打开文件,若当前代码所在路径中没有该文件,会自动创建一个data.txt的文件。
3.相对路径与绝对路径
像这种在使用fopen函数的时候直接给一个文件名的写法叫做给相对路径,表示fopen会去此代码所在路径的同一个文件夹中去找我们要打开的文件名。
如果我们想要打开一个不和当前代码所在文件夹中的文件,比如我要在桌面上打开一个文件,就不能给相对路径了,而是应该使用右键点属性,详细信息里面的路径,这个路径就是绝对路径。但是请注意,直接复制的绝对路径一般都有转义字符
像这样,我们应该再次转义一下
3.fclose
fclose的参数非常简单,就是要关闭的文件指针,而在关闭当前文件之后,pf并没有被置空,因此还要紧接着把pf置空。在使用的时候写成
4.流
流是一个高度抽象的概念。在C语言中我们所说的流可以认为是数据流,流分为输入流和输出流,可以认为流是一种中介,由于计算机的输入输出设备非常的多,他们的读写方式也各有不同,如果要掌握不同设备的读写方式,对于程序员的要求过高,于是我们写代码的时 候只需要写到‘流’上面,然后输入输出设备会从流当中拿取数据进行输入或者输出,至于流如何和外部设备进行交互,我们就不用管了。
而在我们使用printf,scanf进行输出或者输入的时候并没有打开所谓的流,这是因为在C语言程序运行起来之后就默认打开了三个流,分别是标准输入流stdin,标准输出流stdout,标准错误流stderr。因此我们才能直接使用标准输入输出函数来直接进行输入或者输出。实际上这三个标准流也是FILE*类型的。
但是当我们要进行文件操作,必须有三步,打开文件,读写文件,关闭文件,我们对文件进行读写,实际上就是写到文件流上,从文件流上读。这里所说的文件流,实际上就是使用fopen函数打开文件的时候返回的FILE*类型的指针。
也就是说流其实都是FILE*类型的。
三.文件的顺序读写
1.fputc
他的功能是写一个字符到某个流中去。
写一个字符进去,参数确实int类型,合适吗?当然合适,实际上我们写的这个c就是某个字符的ASCII码值。返回值也是int类型的,这是因为如果写入文件失败,会返回EOF也就是-1。
在我们打开一个以写的形式打开一个文件之后我们就要开始写文件了,如图
这样我们就在data.txt文件中按顺序写入了字符a,b,c
如果想要把26个字母写入这个文件,还可以这样写
如果想要写到屏幕上,我们可以这样写
这样的运行结果也说明了一个问题,fputc函数每写入一个字符之后指针就会往后走一步,跳过一个int类型,因为我们下一次输入的也是int类型。
2.fgetc
用来从某个流中读取一个字符。一次性只能读取一个字符,返回值是这个字符的ASCII码值,读取失败会返回EOF,因此返回值类型是int,同fputc一样,每次读取完一个字符之后,会使原本的指针往后走一步,跳过一个int类型。因为我们在写文件的时候是写的字符的ASCII码值,这个值是int类型的。
我们所说的读,写,输入,输出,其实都是针对当前程序来说的,如果是从键盘上打字,就是输入,或者叫读,如果是数据在屏幕上显示出来,就是输出,或者叫写,可以把当前程序类比成我们本人,我们读一本书,对于我们来说就是输入,我们写一篇博客,对于我们来说就是输出。
3.fputs
把一行字符写入到某个流当中去
当写入成功的时候规定返回的是一个大于零的数,写入失败的时候返回EOF
示例
注意fputs是不会自动换行的,如果想要两次输入的字符串在不同的行上,应该手动加一个\n。
4.fgets
从stream流中读取n-1个字符并存储到string开头的位置去,注意最多读取n-1个字符,且只能读一行,也就是说要么读够n-1个字符停下来,要么读到\n停下来
示例
这样就可以把原来pf指向文件中的数据前4个读到arr数组里面去。
上面的函数都是顺序读写的,也就是会自动改变FILE*类型的指针,连续调用是不会覆盖上一次写入的内容的,除非把这个文件关了重新打开,才会覆盖。
5.fprintf
功能:打印数据到某个流上面。类似于printf,只不过printf是把数据打印在屏幕上,也就是标准输出流,因此就可以省略前面的stream,如果我们要打印数据到文件流上,就要使用fprintf函数。既然是打印当前程序中的数据到一个文件流上面,那么对于当前程序来说就是输出,也就是写,因此在使用fprintf之前文件应该是以写的模式打开的。
使用示例
6.fscanf
功能:从某个流当中读一个数据,虽然是我们在写入东西,但对于当前程序来说就是在读,因此在使用fscanf之前文件应该是以读的模式打开的。
7.fwrite
在C语言中,fwrite函数用于将数据写入到文件中。它定义在stdio.h头文件中,该函数能够将数组中的数据以二进制的形式写入到指定的文件中。
fwrite函数的原型是:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明:
- const void *ptr:指向准备写入的数据的指针。通常是数组名。
- size_t size:每个数据项的大小(以字节为单位)。对于数组中的元素,这通常是sizeof每个元素的返回值。
- size_t nmemb:要写入的数据项的总数。
- FILE *stream:指向FILE对象的指针,该FILE对象标识了要将数据写入的文件。这个FILE对象应该是通过调用fopen函数打开的。
返回值:fwrite函数返回成功写入的数据项数目,这个数目应该等同于nmemb,除非遇到了错误或者文件末尾。
例如,假设你有一个结构体数组,你想要将这个数组写入一个文件,你可以这样使用fwrite函数:
请注意,fwrite函数执行的是无格式输出,适用于二进制文件的写入。对于文本文件,如果需要写入字符串或者格式化文本,通常使用fprintf或fputs之类的函数。使用fwrite写入的数据可以通过fread函数读取。请确保应正确处理文件的打开和关闭以及错误检查。
8.fread
在 C 语言中,fread 函数用于从文件中读取数据。它被定义在 stdio.h 头文件中,并且常用于读取以二进制格式保存的文件。
fread 函数的原型是:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明:
- void *ptr:这是一个无类型指针,指向内存的起始地址,这个内存区域用来存放所读取的数据。
- size_t size:要读取的每个数据项的字节数,它通常是某种类型的 sizeof。
- size_t nmemb:要读取的数据项的个数。
- FILE *stream:指向 FILE 对象的指针,这个文件对象表示一个已经打开的文件。
返回值:fread 函数返回实际读取的数据项的个数,如果遇到文件结束或读取错误,这个值可能会小于 nmemb。
下面是一个使用 fread 函数的例子:
在上面的例子中,我们尝试从 myfile.bin 文件中读取 10 个 MyStruct 类型的数据项到数组 records 中。fread 函数返回成功读取的数据项数目。如果读取的数据项少于请求的数量,可能是因为文件中的数据项不足或者发生了读取错误。经常需要检查返回值以确保数据正确读取。
同样,当使用 fread 和 fwrite 进行数据读写时,程序员需要确保数据的字节顺序和对齐方式在不同的硬件和操作系统之间保持一致,否则可能导致跨平台的兼容问题。
四.文件的随机读写
1.fseek
在 C 语言中,fseek 函数提供了一个在打开的文件内进行字节偏移的方法,允许你改变文件位置指示器的位置。它定义在 stdio.h 头文件中。
fseek 函数的原型是:
int fseek(FILE *stream, long int offset, int whence);
参数说明:
- FILE *stream:指向 FILE 对象的指针,代表一个已经打开的文件。
- long int offset:相对于 whence 参数定位的偏移量,单位为字节。
- int whence:这是一个起始位置,它指定了从文件的哪个位置开始进行偏移。它可以取以下常量之一:
- SEEK_SET:文件开头
- SEEK_CUR:当前的文件位置
- SEEK_END:文件末尾
返回值:如果成功,fseek 函数返回0;如果失败,返回非0值。
fseek 函数可以用来移动文件内的读/写位置,以访问文件中不同的部分,或者是跳过某些部分。这对于处理大小较大的二进制文件或者需要随机访问的文件尤其有用。
下面是一个使用 fseek 函数的例子:
在例子中,fseek 将文件位置指示器移动到文件开头后的 100 个字节的位置。之后可以执行读取或写入操作。需要注意,使用 fseek 在文本模式下打开的文件中,跳转操作可能不可靠,特别是在不同系统中,因为文本模式文件中的换行符可能会被转换,导致实际偏移量与预期不符。对于需要精确位置控制的操作,建议使用二进制模式打开文件。
2.ftell
ftell 函数用于获取当前的读写位置相对于文件开头的字节偏移量。这个位置被称为文件位置指示器。ftell 通常与 fseek 函数一起使用,以确定文件中当前的操作位置或者保存这个位置以便之后可以返回到它。
ftell 函数的原型定义在 stdio.h 头文件中:
long int ftell(FILE *stream);
参数说明:
- FILE *stream:指向 FILE 对象的指针,代表一个已经打开的文件。
返回值:如果成功,ftell 函数返回从文件开头到当前文件位置指示器的偏移量(以字节为单位)。如果失败,返回-1L,并设置错误指示器。
下面是一个使用 ftell 函数的例子:
在上面的例子中,fseek 首先将文件位置指示器移动到文件的末尾,然后 ftell 获取当前的位置,也就是文件的总字节大小。之后,再一次使用 fseek 把位置指示器回到文件的开头,以便后续的读操作。在例子的最后,文件通过 fclose 函数关闭。
ftell 在二进制模式下打开的文件中特别有用,因为二进制模式下的文件读写操作通常需要精确地掌握数据的字节位置。此外,在文本模式的文件中使用 ftell 获取的位置值,仅对当前系统有效,并不能保证在不同系统间通用,因为不同的操作系统可能对文本文件中的行结束字符进行不同的处理。
3.rewind
rewind 函数是 C 语言标准库中的一个文件操作函数,用于将文件位置指示器设置到文件的开头,并且清除错误指示器。该函数不返回任何值,可用于快速重置文件的读写位置到开始处,无须考虑返回值。
rewind 函数的原型定义在头文件 stdio.h 中:
void rewind(FILE *stream);
参数说明:
- FILE *stream:指向 FILE 对象的指针,代表一个已经打开的文件。
由于 rewind 不返回任何值,使用它时不能直接检查是否成功。不过,如果需要检查在 rewind 调用后文件位置是否真的移到了开头,可以使用 ftell 函数来验证。同时,与 fseek 函数设置为 fseek(stream, 0L, SEEK_SET) 相比,rewind 还会清除文件流的错误和文件结束指示器。
下面是一个使用 rewind 函数的例子:
在这个例子中,无论先前文件位置指示器在哪里,rewind 函数都将其重置为文件的开头。这样就可以无需考虑当前位置就重新开始读文件。
五.文本文件与二进制文件
通俗来讲如果打开一个文件,我们能够看懂其中的内容,那么这个文件就是文本文件,如果我们看不懂,这个文件就是一个二进制文件。但是这种说法实际上是不准确的,应该说数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
六.文件结束的判定
如果是判断文本文件是否结束,可以通过查看fgetc的返回值是否为EOF,fgets的返回值是否为NULL
如果是判断二进制文件是否结束,可以通过查看fread的返回值,看看是否小于我们要求读的个数。
但是注意,绝对不能使用feof来判断文件是否已经结束,虽然他长得很像用于这种功能,但是实际上他的真正应用场景是:当我们已经知道了文件读取结束,用feof来判断文件读取结束是否是因为文件尾结束。
七、文件缓冲区
文件缓冲区可以被理解为暂时存储数据的内存区域,它作为程序与硬件之间的缓冲层,主要存在于计算机的RAM中。在文件读写操作中,文件缓冲区的运用非常关键,由以下几个方面体现了其存在的意义:
- 提高性能
:文件操作,尤其是对硬盘等存储设备的操作,相较于内存访问要慢得多。文件缓冲区通过减少实际的磁盘I/O操作次数以及优化读写操作的大小来提升性能。数据可以在内存中累积到一定量后再一次性写入硬盘,或在一次操作中从硬盘读取大量数据到内存,减少访问硬盘的次数。
- 减少系统调用
:每次磁盘I/O操作都伴随着系统调用和上下文切换,这些都需要消耗CPU资源。文件缓冲区可以将多次I/O操作聚集成一次系统调用,减少系统调用的开销。
- 数据整合和分离
:缓冲区可以用来整合输出数据和分离输入数据,方便程序对数据进行管理。例如,程序可能一次只生成少量输出,而缓冲区可以将这些小量输出积累起来,在达到一定量时一并发送出去,从而更高效。
- 支持异步操作
:缓冲区允许程序继续执行,而数据在后台被载入或写出,不必等待每个读/写操作完成。这对于非阻塞I/O和多线程/多任务系统特别有用。
- 提升数据处理效率
:在许多情况下,处理一大块数据比处理许多小块数据更高效,因为可以减少处理的次数。
- 补偿速度不匹配
:计算机中的组件工作速度不尽相同,例如CPU速度通常比磁盘驱动器速度快。缓冲区有助于补偿这些组件之间的速度差异,减少快速组件在等待慢速组件时的空闲时间。
文件缓冲区的类型主要有两种:全缓冲(full buffering,数据填满缓冲区后才进行实际I/O操作)、行缓冲(line buffering,每次处理一行数据)和无缓冲(no buffering,数据直接从源读取或直接写入目的地)。
在C语言中,标准I/O库如 stdio.h 提供的函数大多使用缓冲区。例如,stdout 通常是行缓冲的,而磁盘文件通常是全缓冲的。可以使用 setvbuf 函数来设置缓冲区的类型和大小。
缓冲区的使用虽然提高了效率,但也可能带来一些问题,比如程序崩溃可能导致缓冲区中的数据未被写入硬盘而丢失。为此,重要操作后调用如 fflush 这样的函数显得尤为重要,以确保缓冲区中的数据及时被正确地保存到硬盘上。