• 【C语言】-文件操作



    前言

    我们在入门时敲代码的过程中,我们发现,当我们启动程序后输入的数据在下一次运行的时候就没有了。举个形象点的例子就是,我们玩小游戏时,妈妈叫我们去吃饭,这时候我们退出了游戏,下次登陆游戏后我们又要从第一关开始玩,这是很蛋疼的,那么为了解决这个问题,我们就引入了文件操作。


    1、文件是什么?

    文件我们一般可以分为程序文件数据文件,而我们在前言讨论的就是数据文件,我们在运行了一次程序之后,我们希望它记住我们上一次运行过程中输入的数据。我们需要它记住的文件即数据文件,而程序文件就是源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)

    那么我们一般为了区分不同的文件,我们会引入文件名。
    文件名包含3部分:文件路径+文件名主干+文件后缀。
    例如:c:\code\test.txt
    c:\code\就是路径
    test就是文件主干
    .txt就是文件后缀

    2、文件的打开和关闭

    为了让我们更好的加强对文件使用的严谨性,我们就不得不提到文件的打开和关闭
    文件在读写之前应该先打开文件
    文件在读写完之后应该关闭文件

    我们的C语言怎样和文件产生联系呢?答案是指针。我们通过指针找到文件进而访问到文件内容。也就是说我们通过文件指针找到与之关联的文件。
    那么这样的指针的类型自然而然就是文件。进而称之为文件指针。具体表现形式为

    FILE* pf

    言归正传,打开文件需要fopen函数,关闭文件需要fclose函数,具体表现形式为

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

    打开文件函数
    第一个参数是打开文件的名字,即你要打开哪个文件。
    第二个参数是你要打开的方式。例如你是要读还是要写,你是要二进制读还是要二进制写。
    这里笔者对打开方式进行了一个汇总:
    在这里插入图片描述
    我们可以结合实际情况的需要来选择打开文件的方式

    3、文件的顺序读写

    顺序读写是我们意思呢?
    假设这样一种情况,我们要将字符串“abcde”写入文件中,那么通过顺序读写,我们就会按顺序,先将a写入文件,再将b写入文件,一个一个按顺序来进行。
    能够实现顺序读写的函数有以下
    在这里插入图片描述
    我们可以根据我们具体情况选择我们需要的函数使用。

    在这里我们可以回想比较一下scanf和上述fscanf的区别。
    scanf是我们按照一定格式(例如,整型、浮点型)通过键盘(即标准输入流)输入数据。
    而fscanf就可以按照一定格式,从输入流(这里的输入流就包含了文件流、标准输入流)输入数据。
    也就是说,我们想要通过键盘输入数据时,scanf就也就可以满足我们的需求了,且用起来更加方便,当我们想要从文件读入数据时,scanf的局限性也就浮现了出来,这时候我们就需要fscanf来帮忙了。

    我们已经知道了顺序读写了,那么我们怎么把它运用到我们的开发当中呢
    笔者就不具体举例了,这里只表述出大体的思路:
    我们在退出程序前,我们可以把我们的数据提前写入一个文件当中。如何把数据写入文件当中就需要利用我们上述的函数。fopen、fprintf、fclose…。而我们在下一次进入程序的时候,我们需要在代码初始化阶段加入加载文件信息的代码。即把我们上一次保存在文件中的信息利用fopen、fscanf、fclose…来把数据写入到我们这次运行过程中来。

    4、文件的随机读写

    什么是文件的随机读写呢。我们主要是理解随机的含义,这里的随机并不是真正意义上的“随机”,并不是我们游戏抽奖概率上的“随机”。而是例如我们要对文件中字符串“abcdef”进行读取,我们可以不从字符’a‘开始读取,我们可以利用文件指针从字符‘b’开始读取。这就是我们所说的随机读取。这里笔者就介绍三个随机读写的函数

    4.1、fseek

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

    这个函数的功能是:根据文件指针的位置和偏移量来定位文件指针。
    也就是说我们通过这个函数,来控制文件指针指向哪里。

    第一个参数是我们的目标文件指针
    第二个参数是我们的偏移量
    第三个参数是我们初始文件指针的位置

    这里主要分析一下第三个参数。
    第三个参数有三种情况:
    情况一、SEEK_SET,当你把这个参数填入后,从文件开始位置(第一个字符)计算偏移量
    情况二、SEEK_END,当你把这个参数填入后,就是从文件末尾位置(最后一个字符)计算偏移量
    情况三、SEEK_CUR,当你把这个参数填入后,就是从文件当前位置开始计算偏移量。

    4.2、ftell

    long int ftell ( FILE * stream );

    这个函数的作用是返回文件指针相对于起始位置的偏移量。

    这里函数有时候计算出来的结果和人脑计算出来的结果有出入,并不是函数出错了,而是大部分时候是人脑出错了。这里有个知识点是:文件指针在读了一个字符之后会自动往后跳一个。我们往往忽略了这个过程,导致我们人脑计算时和函数计算时有出入。

    4.3、rewind

    void rewind ( FILE * stream );

    这个函数的作用是让文件指针回到起始位置。

    我们通过上述几个简单的库函数就可以实现一些简单的随机读写了

    5、文本文件和二进制文件

    我们观察到上述打开文件方式里有一个以二进制进行读写的方式。以此引出此处二进制文件的内容,也就是说文件不止有文本文件,也就是我们看得懂的例如“abcdefg”这种可以适合我们人脑阅读的文件,还有一种叫二进制文件。
    二进制文件与文本文件的区别就在于数据的组织形式不同。如果是以ASCII码值形式存储的,我们就叫文本文件,需要在存储之前转换。

    理解了文本文件和二进制文件的区别,笔者在这里解释一个现象:为什么我们二进制写入的文件用文本方式打开是一串乱码呢?例如:

    #include
    
    int main()
    {
    	FILE* pf = fopen("data.txt", "wb");
    	int a = 10000;
    	if (pf != NULL)
    	{
    		fwrite(&a, 4,1,pf);
    		fclose(pf);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这一段代码我们将变量a的值以二进制的方式写入。当我们打开记事本查看我们写入的数据时懵逼了。
    在这里插入图片描述
    这是个什么符号?我不是写入的10000吗,怎么变成了一个我从未见过的符号了。
    其实,是因为当我们以记事本打开时,它拿到的数据是10000的二进制序列,而它会把它当作文本进行解码。解码出来后的结果就是这个奇奇怪怪的符号。

    那么我们如何查看我们二进制写入的数据呢?
    如果你用的是vs的话,可以先把你的文件添加到vs里面,像笔者这样
    在这里插入图片描述

    可以在这里选择打开方式,在打开方式里面选择二进制编辑器
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    我们来解析一下这一串数字。
    首先前面8个0是地址,后面的10 27 00 00 就是我们写入的100000,这时候就产生疑问了,我不是写入的10000吗?怎么变成10 27 00 00 了?别急,看笔者一步一步分析。首先我们的vs采用的是小端存储,小端存储的数据是反过来的,变成我们阅读方式的数据就是00 00 10 27,离我们的10000似乎还有点距离。它其实目前还是16进制,我们把它变成2进制就是 00000000 00000000 00100111 00010000我们把这串二进制编码转换成十进制,刚好就是10000!

    6、关于feof用于文件结束判定的错误用法

    许多同学敲代码时,总喜欢用feof的返回值去判断文件是否结束,殊不知用错了feof的用法。许多同学以为feof函数的eof的意思是endof file(文件结束)就误以为这个函数是用来判断文件结束的。但其实它的真正用法是用于判断文件结束的原因。

    我们要牢记一点:判断文件结束不是用feof来判断的,而是当读取文件是文本文件时运用fgetc函数、fgets函数…的返回值,当fgetc函数读取结束时返回的是EOF,当fgets函数读取结束时返回的是NULL。当读取文件是二进制文件时运用的是fread函数,当它的返回值小于实际传入要求它读的数据的个数时即文件结束
    那么feof的正确用法是什么呢?
    以二进制文件读写为例子:

    #include 
    enum { SIZE = 5 };
    int main(void)
    {
        double a[SIZE] = {1.,2.,3.,4.,5.};
        FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
        fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
        fclose(fp);
        double b[SIZE];
        fp = fopen("test.bin","rb");
        size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
        if(ret_code == SIZE) {
            puts("Array read successfully, contents: ");
            for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
            putchar('\n');
       } else { // error handling
           if (feof(fp))
              printf("Error reading test.bin: unexpected end of file\n");
           else if (ferror(fp)) {
               perror("Error reading test.bin");
           }
       }
        fclose(fp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们可以看到,在代码末尾处,我们用feof判断文件是异常结束还是文件被读完了的正常结束。

    7、文件缓冲区

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

    简单来说就是:我们在数据写入时,系统会把数据存够再一并写入文件当中。

    那么为什么这样做呢?

    我们可以假想一个场景:假如小明同学品学兼优,小美同学每天就要找小明问问题,小美同学5分钟一个问题,5分钟一个问题。一产生问题就去找小明同学解答。这时候小明同学就不乐意了,告诉小美同学,能不能存一个小时问题再集中来问我呀?
    故事结束,我们可以把小明同学类比成我们的操作系统,操作系统如果频繁切换势必造成效率的低下。这时候操作系统就说:你把你要写入的数据给我存好,存满了我就一次性给你写入进去。

    可以说这样做的目的就是为了提高操作系统的工作效率。
    由此,我们可以呼应前面的内容,为什么我们要打开文件后一定要关闭文件呀?
    以为关闭文件的时候会刷新一次缓冲区,如果不关闭文件会导致缓冲区的数据还没来得及被操作系统写入就因为某种原因丢失了!

  • 相关阅读:
    如何模拟自然界生态系统中的食物链
    【解题报告】CF练一下题 | 难度CF2500左右
    Nuxt脚手架nuxi初始化失败原因&解决方法
    网络工程师 ---- 常见的查看命令
    最新版校园招聘进大厂系列----------(5)百度篇 -----未完待续
    聊聊Jasypt的StandardPBEByteEncryptor
    【毕业设计】Stm32 WIFI智能家居温湿度和烟雾检测系统 - 单片机 物联网 嵌入式
    C++ Reference: Standard C++ Library reference: C Library: cmath: hypot
    Jenkins 安装全攻略:从入门到精通
    Vue3多个弹窗同时出现解决思路
  • 原文地址:https://blog.csdn.net/flin666/article/details/127711135