C语言·文件操作

1. 为什么使用文件

        如果没有文件,我们写的程序的数据都是储存在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将程序进行持久化的保存,我们就可以使用文件。其实也可以理解成游戏的本地存档

2. 什么是文件

        磁盘(硬盘)上的文件文件就是文件

        但是在程序设计中,我们一般能谈到两种文件:程序文件、数据文件

2.1 程序文件

        程序文件包含源程序文件(.c),目标文件(.obj),可执行程序(.exe)

2.2 数据文件

        这种文件的内容是程序在运行的时候读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件

        本节讨论的就是数据文件

        在之前的编写中,处理数据的输入输出都是以终端为对象的,即从输入终端键盘上输入数据,运行结果到显示器上输出。然而这节我们将要学习如何把数据输出到磁盘上,当需要的时候在从磁盘上把数据读到内存中使用,这样就是在处理磁盘上的文件

2.3 文件名

        一个文件要有一个唯一的文件标识,以便于用户的识别和引用,这个东西就叫做文件名

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

C语言·文件操作_第1张图片

3. 二进制文件和文本文件

        众所周知,数据在内存中是以二进制的形式存储的,如果不加转换的把这些数据输出到硬盘中的文件,这种文件就叫二进制文件。如果在输出数据的过程中,将二进制数据转换成了ASCII码值的形式,存储到了硬盘中的文件中,那么这种文件就叫文本文件

        比如我现在要输出一个整形十进制数字10000,它在内存中是以二进制00000000 00000000 00100111 00010000的形式存储的,现在我直接把这一串二进制信息放到一个文件中,那么这个文件就叫做二进制文件,这些信息占4个字节。如果我把10000当成一个字符串,然后一个字符一个字符的转成ASCII码,再存到文件中,那么这个文件就叫做文本文件,这些信息占5个字节

4. 文件的打开和关闭

4.1 流和标准流

4.1.1 流

        我们程序的数据需要从各种外设中输入,也要输出到各种外设中去,不同的外设输入和输出的操作各不相同,我们不能把所有外设的操作方式都包含到程序中去,所以为了方便程序员对各种设备进行操作,我们抽象出了流的概念,我们可以把流想象成流淌着的数据河。

        C程序针对文件、画面、键盘等数据的输入输出操作都是通过流获取或输出的。一般情况下,我们想要从流里面读数据,或向流里面写数据,都要先打开流,然后再操作

4.1.2 标准流

        那为什么我们在用printf scanf函数输入输出数据的时候并没有打开流呢?

        那是因为C语言程序在启动的时候,默认打开了3个流

        stdin 标准输入流,在大多数环境从键盘输入,scanf函数就是从标准输入流中读取数据。

        stdout 标准输出流,大多数环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中

        stderr 标准错误流,大多数环境中输出到显示器界面

        stdin stdout strerr 三个流的类型数 FILE* ,通常称为文件指针。C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的

4.2文件指针

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

        每个被使用的文件都在内存中开辟了一块相应的文件信息区,用来存放文件的相关信息(如文件名,文件状态,文件当前的位置等),这些信息都被保存在一个结构体变量中。该结构体类型是由系统命名的,叫做FILE

        vs2013中可以看到在stdio.h的头文件中,有如下的文件类型声明

                C语言·文件操作_第2张图片

        不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异

        每当打开⼀个文件的时候,系统会根据文件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节

        我们一般通过FILE的指针来维护FILE结构中的内容,下面我们创建一个FILE*的指针变量

        

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

4.3 文件的打开和关闭

        文件在读写之前应该先打开文件,在使用结束之后应该关闭文件

        在编写程序时,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系

        ANSI C规定使用fopen函数打开文件,fclose函数来关闭文件

                        FILE * fopen ( const char * filename, const char * mode );

                官网资料:fopen - C++ Reference

                        int fclose ( FILE * stream );

                官网资料:fclose - C++ Reference

        关闭文件函数 fclose 的参数 stream 是指在打开文件时生成的指针,通过这个指针就可以关闭这个文件了

        打开文件函数 fopen 的第一个参数 filename 是文件名,这个文件名有3种书写方式

        第一种:直接写文件名主干和文件名后缀,比如:data.txt ,这样的话就是打开当前目录(位置)下的data.txt文件,当前目录的位置就是这个源文件的位置

        第二种:绝对位置,将文件路径、文件主干、文件后缀都写的清清楚楚明明白白,比如:c:\code\data.txt ,这样就是在你指定的路径下打开这个文件

        第三种:相对位置,文件路经用 ./(当前目录)  ../(上一级目录) 这种符号填充,比如:./../../data.txt ,这样就是在源文件位置的目录的上一级目录的上一级目录下打开这个文件

        fopen函数的第二个参数mode表示文件的打开模式,下面是各种的文件的打开模式:

文件使用方式                                        含义                                                    如果指定文件不存在
"r"(只读)                         输入到程序中,打开一个已经存在的文本文件                     报错
"w"(只写)    输出到文件中,打开一个文本文件,如果文件中有内容会全部清空   建立一个新的文件
"a"(追加)                        向文本文件尾添加数据                                                    建立一个新的文件
"rb"(二进制只读)            为了输入数据,打开一个二进制文件                                    报错
"wb"(二进制只写)      为了输出数据,打开一个二进制文件,有内容就清空         建立一个新的文件
"ab"(二进制追加)            向一个二进制文件尾添加数据                                        建立一个新的文件
"r+"(读和更新)                打开一个文件用于更新                                                          报错
"w+"(写和更新)               建立一个空文件用于更新,有内容就清空                      建立一个新的文件
"a+"(追加和更新)      打开文件进行更新,所有输出操作都在文件末尾写入数据     建立一个新的文件
"rb+"(二进制只读和更新)      操作同上,但是操作二进制文件                                      报错
"wb+"(二进制只写和更新)      操作同上,但是操作二进制文件                             建立一个新的文件
"ab+"(二进制追加和更新)      操作同上,但是操作二进制文件                              建立一个新的文件

下面我们尝试打开和关闭一个文件

C语言·文件操作_第3张图片

C语言·文件操作_第4张图片

        可以观察到,这段代码确实让我的源文件路径下生成了一个data.txt文件

5. 文件的顺序读写

5.1 顺序读写函数介绍

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

        官网资料: (stdio.h) - C++ Reference

        现在我们尝试给文件里面写点东西

C语言·文件操作_第5张图片

        运行程序之后我们发现文件中确实写进去了5个字符,那么现在我们尝试把刚刚写进文件里的那些字母打印到屏幕上C语言·文件操作_第6张图片

        好的,我们成功了,文件中的内容被打印出来了。

        这些操作都是属于在循序的读写,这些字符被按顺序的一个一个的输出到文件中,或一个一个的输入到程序中再输出到屏幕上。那么顺序是什么,其实就是光标的位置。写入一个字符之后光标会向后移动一格,那么下一个字符就会写入到在光标位置,然后光标再往后移动一格,如此顺序的写下去。当从文件中读走数据输入到程序中时也是这个过程,读一个字符,光标向后移动一格,然后再从光标位置读一个字符,光标再向后移动

C语言·文件操作_第7张图片

其实官网上已经把这些函数都说的很明白了,这里我就浅讲一下其他函数吧:

                        char* fgets(char* str, int num, FILE* stream);

        fgets函数从流当中获取一行字符,字符数不超过num-1个,并将他们作为C字符串存到str数组中,也就是说会在数组中预留一个\0,这也解释了为什么这个函数最多只能读num-1个字符。最后返回数组str首元素地址

C语言·文件操作_第8张图片

        观察这段代码,即使我输入了一行6个字符(要算上\n),但是实际放到数组中的只有4个,因为这个函数自动补的那个\0被算进去了

                        int fputs ( const char * str, FILE * stream );

        fputs函数将str指向的C字符串写入流,直到\0停止,但是不会把\0写到流中。最后返回一个非负值(这个值没啥意义)

                        int fscanf ( FILE * stream, const char * format, ... );

                        int fprintf ( FILE * stream, const char * format, ... );

        这两个函数的用法和scanf printf的用法几乎一致,就是在最前面加了一个流参数而已

        现在我们尝试用这两个函数操作一下,我事先把需要用到的数据先放到了文件里

                        C语言·文件操作_第9张图片

C语言·文件操作_第10张图片

        好的,这段代码先是用 fscanf 函数把数据从文件中提取到结构体变量 s 中,然后用 fprintf 函数把结构体变量 s 中的数据放到标准输出流中打印到了屏幕上

                        size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

                        size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

        他俩作为二进制家族的读写,第一个参数 ptr 是要操作的变量的地址,第二个参数 size 是一个元素的大小,第三个参数 count 是指有几个元素,第四个参数还是流。最后返回成功读入或写出的元素总数

        我们还是用张三那个数据为题用这两个函数操作一下

C语言·文件操作_第11张图片

        首先要将文件打开成二进制只写形式,然后用二进制输出函数把数据存放在文件中,现在打开文件看看,只有zhangsan可以正常看懂,但是后面的数据就都是乱码了,这是因为记事本是文本文件,只能读懂文本信息,读不懂二进制信息,所以后两个数据记事本是翻译不对的,但是因为字符的文本数据和二进制数据是一致的,都能追溯到ASCII码上,所说文本文件可以把以二进制形式存储的字符数据翻译出来

        虽然说我们肉眼认为这时的数据变成了一堆乱码,但是数据还是存放在了这里,我们只需要用到能读懂二进制数据的工具把它们翻译一下就行了,这时 fread 闪亮登场

C语言·文件操作_第12张图片

        我们将二进制的数据存到变量s中,变量在内存中的信息本来就是以二进制的形式存在的,所以说现在程序完全步入正轨了,然后再用一个fprintf函数把结构体s中的内容都打印出来。好的,现在可以看出结果完全符合预期,很完美

5.2 对比一组函数

        scanf/fscanf/sscanf

        printf/fprintf/sprintf

        根据之前的学习,我们知道了:

        scanf - 针对标准输入流(键盘)的格式化输入函数

        printf - 针对标准输出流(屏幕)的格式化输出函数

        fscanf - 针对任何输入流的格式化输入函数

        fprintf - 针对任何输出流的格式化输出函数

        下面我们看一下那两个新函数

                        int sscanf ( const char * s, const char * format, ...);

        从一个字符串中读取一个格式化的数据

        官网资料:sscanf - C++ Reference

                        int sprintf ( char * str, const char * format, ... );

        把一个格式化的数据放到字符串中去

        官网资料:sprintf - C++ Reference

        下面我们尝试让张三再来解释一下这两个函数怎么用

C语言·文件操作_第13张图片

        我们发现sprintf sscanf两个函数完成了对于字符串的格式化操作

6. 文件的随机读写

        在顺序读写中我们已经了解到了文件操作的精髓了,就是文件的光标位置。当一进入文件的时候光标指向着文件内容的开头,如果是顺序操作的话,就是按顺序一点一点的把光标往后移,光标移动到哪里就操作的哪里。而文件的随机读写事实上就是对于光标位置的控制,我此时想操作哪里,就控制光标移动到哪里,而不是让它按顺序一点一点往后挪。

6.1 fseek

        根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)

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

        第一个参数流就不多介绍了。第二个参数是光标的偏移量,针对origin位置的偏移量,正数向后偏移,负数向前偏移。第三个参数origin是光标起始位置,起始位置有三种选择:SEEK_SET 文件的起始位置、 SEEK_CUR 光标当前的位置、 SEEK_END 文件结束位置

        官网资料:fseek - C++ Reference

6.2 ftell

        用于返回当前光标相对于文件起始位置的偏移量

                        long int ftell ( FILE * stream );

        这个参数流也无需多言了,输入流参数之后就可以知道现在的光标在当前流的位置,值得注意的是它的返回值类型是一个long int型的,不要在获取返回值的时候因为变量类型设置错误导致截断了数据发生错误

        官网资料:ftell - C++ Reference

6.3 rewind

        让文件指针的位置回到文件的起始位置

                        void rewind ( FILE * stream );

        这个没啥好注意的,很简单的一个函数,让光标回到文件开始的位置

        官网资料:rewind - C++ Reference

7.文件结束的判定

7.1 feof

        判断那些输入类型函数(fgetc、fgets、fread)是否是遇见文件末尾结束的

                        int ferror ( FILE * stream );

        这个函数的返回值如果是遇见文件末尾结束的返回非0数,如果不是返回0

        官网资料:feof - C++ Reference

7.2 ferror

        有一个与它功能相似的函数ferror,是用于判断是否是发现错误而返回的

                        int ferror ( FILE * stream );

        这个函数发现是当遇见错误的话就返回非零数,否则返回0

        官网资料:ferror - C++ Reference

        下面我们尝试一下这两个函数

C语言·文件操作_第14张图片

        最终结果就是成功完成了读取任务,文件读到结尾结束的,中间并没有出现错误

8. 文件缓冲区

        ANSIC  标准采用“缓冲文件系统”处理文件数据的,所谓缓冲文件系统就是系统自动的在内存中为程序中每一个正在运行的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到输出缓冲区中,当输出缓冲区中装满了之后再一并送到磁盘中去。同理,如果磁盘向计算机中读数据,也是先都读到输入缓冲区中,当读完或者读满了之后,再逐个将数据送到程序数据区(程序变量)。缓冲区的大小是根据编译器决定的。

C语言·文件操作_第15张图片

        下面我们观察一段代码,这段代码很好的展示了输出缓冲区的存在

C语言·文件操作_第16张图片

C语言·文件操作_第17张图片

        Sleep函数我们之前已经使用过了,含义是延时多少毫秒,需要引用头文件windows.h

        这段代码中fputs函数将数据都先放在了输出缓冲区当中,所以在第一个Sleep期间文件中并没有abcdef,但是当用fflush手动刷新文件缓冲区了之后,abcdef就出现了

你可能感兴趣的:(C语言学习之旅,c语言,开发语言)