C语言基础14——文件操作。文本文件或二进制文件读写。通讯录的改造。文件缓冲区讲解

本文详细讲解了文件的使用原理、打开方式、流的概念、顺序读写操作,以及字符与文本文件的处理,涉及fputc、fgetc、fgets、fputs和格式化函数的使用。通过实例演示如何改造通讯录,实现文件的随机读写、判定文件类型和使用缓冲区。
摘要由CSDN通过智能技术生成

目录

为什么使用文件?

什么是文件?

文件的打开和关闭

文件指针

文件的打开和关闭

文件的打开方式

重定义文件

文件流

文件的顺序读写

以字符形式读写文本文件

fputc()函数

fgetc()函数

以字符串形式读写文本文件

fputs()函数

fgets()函数

格式化读写文件

fprintf()函数

fscanf()函数

以数据块的形式读写文件

fwrite()函数

fread()函数

三种输入/输出函数的比较

改造通讯录

文件的随机读写

fseek()函数

ftell()函数

rewind()函数

文本文件和二进制文件

文件读取结束的判定

文本文件的判定

二进制判定示例

文件缓冲区

gets、getchar、缓冲区讲解

gets()函数

getchar()函数——只适用于标准输入流

缓冲区详解

练习


为什么使用文件?

我们在学习结构体的时候,学习了通讯录,运行后,可以在其中增加/删除数据。运行时,数据存放在内存中;运行结束,内存释放。等我们下次再次运行的时候,就需要再次输入数据,很麻烦。

既然是通讯录,就应该把数据保存下来。也就是说要把数据持久化,一般数据持久化的方法:把数据存放在硬盘文件、存放到数据库中等方式。

使用文件,可以将数据存放到电脑硬盘中,做到了数据的持久化。

什么是文件?

硬盘上的文件就是文件。但是在程序设计中,文件一般有两种:程序文件、数据文件。(从文件功能角度划分)。

之前我们数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到控制台上。

有时候我们可以把信息输出到硬盘上,当需要的时候再从硬盘上把数据读取到内存中使用,这里就是处理硬盘文件

  • 程序文件

    包括源程序文件(.c)、目标文件(windows环境下后缀为.obj)、可执行程序(win中后缀为.exe)

  • 数据文件

    文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

  • 文件名

    //一个文件要有一个唯一的表示,以便用户识别和引用。
    //文件包含三部分:文件路径+文件名+文件后缀
    // 例如:D:\code\test.txt
    //为了方便起见,文件标识常被命名为文件名。
    

文件的打开和关闭

文件指针

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

    每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,如:文件名、文件状态、文件当前位置。

  • 这些信息是保存在一个结构体变量中的。该结构体是由系统声明的,取名FILE

    CLion编译环境提供的stdio.h头文件中就有以下的文件类型申明

    struct _iobuf {
    #ifdef _UCRT
        void *_Placeholder;
    #else
        char *_ptr;
        int _cnt;
        char *_base;
        int _flag;
        int _file;
        int _charbuf;
        int _bufsiz;
        char *_tmpfname;
    #endif
    };
    typedef struct _iobuf FILE;
    
  • 不同编译器的FILE类型包含的内容不完全相同,但是大同小异。

    每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个FILE类型的指针来维护这个FILE结构体的变量,这样使用起来更方便。

    在这里插入图片描述

  • 创建一个FILE*类型的指针:

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

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

    在这里插入图片描述

文件的打开和关闭

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

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

ANSIC规定使用fopen()函数来打开文件,fclose()来关闭文件

/*
 * fopen()函数
 * 函数原型:FILE* fopen(const char* filename,const char* mode);
 * 作用:
 * - 打开对应文件名的文件,并将其与一个流相关联,该流可以在以后的操作中通过返回的FILE指针来表示。
 * - 流允许的操作以及如何执行这些操作由mode参数定义。
 *   如果已知返回的流不引用交互式设备,则默认情况下返回的流是完全缓冲的。返回的指针可以通过调用fclose或freopen与文件解除关联。
 *   所有打开的文件都会在程序正常终止时自动关闭。运行环境至少支持FOPEN_MAX个文件同时打开。
 * 参数:
 * - filename:C字符串,包含要打开的文件名。其值应遵循运行环境的文件命名规范,并且可以包含路径(如果系统支持)
 * - mode:C字符串,包含文件的访问模式。
 *   # "r" 打开文件进行输入操作。该文件必须存在
 *   # "w" 为输出操作创建一个空文件。如果同路径同名文件已经存在,则清空该文件内容,将其视为新的空文件。
 *   # "a" 打开文件,在文件末尾输出。输出操作总是在文件末尾写入输入。忽略重新定位操作(fseek、fsetpos、rewind)
 *     如果文件不存在,则创建文件。
 *   # "r+" 打开一个文件进行更新(输入和输出)。该文件必须存在。
 *   # "w+" 创建一个空文件并打开进行更新(输入和输出)。如果同路径同名文件已经存在,则清空该文件内容,将其视为新的空文件。
 *   # "a+" 打开一个文件进行更新(输入和输出),所有输出操作都在文件末尾写入数据。
 *     重新定位操作(fseek、fsetpos、rewind)会影响下一个输入操作,但输出操作会将位置移回文件末尾。
 *     入宫文件不存在,则创建该文件。
 *   注意:
 *   # 使用上面的模式说明符,文件将作为文本文件打开。如果要以二进制文件的形式打开文件,则模式字符串必须包含b字符。
 *    这个附加的b字符可以附加在字符串的末尾,从而形成复合模式:"rb"、"wb"、"ab"、"r+b"、"w+b"、"a+b"
 *    或插入字母和"+"之间表示混合模式:"rb+"、"wb+"、"ab+"
 * - 文本文件时包含文本序列的文件。根据程序运行环境,在文本模式下的输入/输出操作中可能会发生一些特殊字符串转换,以使其适应系统特定的文本文件格式。
 *   尽管在某些环境中不会发生转换并且文本文件和二进制文件的处理方式相同,但使用适当的模式可以提高可移植性。
 * - 对于更新而打开的文件(包含"+"),允许进行输入输出操作,在写入后的读取前,流应该被刷新(fflush)或重新定位(fseek、fsetpos、rewind)
 *   在读取操作之后的写入操作之前(当该文件到达文件末尾时),应该重定位流(fseek、fsetpos、rewind)
 *
 * 返回值:
 * - 如果文件成功打开,则返回一个指向FILE对象的指针,该对象可用于在将来的操作中识别流。
 * - 否则返回一个空指针。并且在大多数库实现中,设置全局变量errno来标识错误。
 *
 *
 * fclose()函数
 * 函数原型:int fclose(FILE* stream);
 * 作用:关闭与流关联的文件并取消关联。
 * - 与流关联的所有内部缓冲区都与它解除关联并刷新:任何未写入的输出缓冲区的内容都会被写入,而任何未读的输入缓冲区的内容都会被丢弃。
 *   即使调用失败,作为参数传递的流也将不再与文件及其缓冲区相关联。
 *
 *  参数:stream,指向 要关闭流的FILE对象 的指针,
 *  返回值:如果流成功关闭,就返回0,如果失败,则返回EOF。
 */

#include <stdio.h>

int main()
{
    //CLion的默认路径,在该项目下的cmake-build-debug文件夹中创建
    //FILE* pf = fopen("test.txt","w");

    //如果我们想要指定别的路径,则需要在文件名前加上路径。
    //注意:防止路径的\被当成转义字符,需要写成双斜杠。D:\Projects\test.txt变为D:\\Projects\\test.txt
    //FILE* pf = fopen("D:\\Projects\\test.txt","w");

    FILE* pf = fopen("test.dat","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写文件

    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

文件的打开方式

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

重定义文件

/*
 * - 在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作
 *   通常,把显示器称为标准输出文件,printf就是向这个文件中输出数据;
 *   而键盘称为标准输入文件,scanf就是从这个文件读取数据。
 *
 * 常见硬件设备所对应的文件:
 * - stdin:标准输入流,一般指键盘。scanf()、getchar()等函数默认从stdin获取输入
 * - stdout:标准输出流,一般指显示器。printf()、putchar()等函数默认向stdout输出数据
 * - stderr:标准错误流,一般指显示器。perror()等函数默认向stderr输出数据
 * - stdprn:标准打印文件,一般指打印机。
 *
 * - 我们不去探讨硬件设备是如何被映射成文件的,只需要记住,在C语言中硬件设备可以看成文件。
 *   有些输入输出函数不需要你指明到底读写哪个文件,系统已经为它们设置了默认的文件。
 *   当然你也可以更改,例如让 printf 向磁盘上的文件输出数据。
 *
 * 操作文件的正确流程:打开 ——> 读写 ——> 关闭
 *
 * - 所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。
 *   关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。
 * - 在C语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读取一整行,还可以读取若干个字节。
 *   文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。
 */

文件流

/*
 * 流是一个抽象的概念
 * - 我们运行程序,要输入输出数据到屏幕、硬盘、U盘、光盘、网络、软盘上等等。硬件不同,其数据的交互方式就不同
 *   而我们运行的程序,要想输入输出到不同的硬件上,就需要写不同的代码,太麻烦了。所以C语言在程序与外部设备之间,抽象出一个流的概念。
 * - 我们要与外部数据进行交互时,我们只需要与流进行交互,再由流与外部设备进行交互即可。我们只需要把数据放入流中即可。
 *
 * 文件流
 * - 在《载入内存,让程序运行起来》一文中提到,所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。
 *   数据在文件和内存之间传递的过程叫做文件流,类似水从一个地方流动到另一个地方。
 *   数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。
 *
 * - 文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。
 *   我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。
 *   相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。 注意:是相对于程序(内存)而言的,到内存中,就是输入流;从内存中出去,就是输出流。
 * - 输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。
 *   几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。
 *   通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。
 * - 我们可以说,打开文件就是打开了一个流。
 *
 * C语言程序,只要开始运行,就默认打开三个流:
 * - stdin:标准输入流,对应键盘。scanf()、getchar()等函数默认从stdin获取输入
 * - sdtout:标准输出流,对应屏幕。printf()、putchar()等函数默认向stdout输出数据
 * - stderr:标准错误流,对应屏幕。perror()等函数默认向stderr输出数据
 */

文件的顺序读写

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

    从硬盘中 ————————> 内存中,叫做:读取/输入
    从内存中 ————————> 硬盘中,叫做:写入/输出

    注意:相对于内存:到内存中,就是输入流;从内存中出去,就是输出流。

    ​ 相对于硬盘:从内存中出去,就是读取;到硬盘中,就是写入。

以字符形式读写文本文件

fputc()函数

/*
 * fputc()函数 fputc是:file output char的意思。
 * 函数原型:int fputc(int character,FILE* stream);
 * 作用:向指定流/文件中写入一个字符。每写入一个字符,将文件内部位置标识符向后移动一个字符。
 * 参数:
 * - char:要写入字符的int提升,写入时,该值在内部转换为无符号字符。
 * - stream:指向标识输出流的FILE对象的指针。
 * 返回值:写入成功,就返回写入的字符;写入错误,则返回EOF并设置错误指示符ferror。
 */

#include <stdio.h>

int main()
{
    FILE* pf = fopen("test.dat","w");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写入文件
    fputc('h',pf);
    fputc('e',pf);
    fputc('l',pf);
    fputc('l',pf);
    fputc('o',pf);
    //写入到标准输出流stdout,也就是打印到控制台。
    fputc('h',stdout);
    fputc('e',stdout);
    fputc('h',stdout);
    fputc('e',stdout);
    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

fgetc()函数

/*
 * fgetc()函数 —— fgetc是file get char的缩写,意思是:从指定文件中读取一个字符。
 * 函数原型:int fgetc(FILE* stream)
 * 作用:返回指定的流的内部文件位置指示符当前指向的字符,然后内部文件位置指示器前进到下一个字符。
 * - 在文件内部有一个位置指示器,用来指向当前读写到的位置,也就是读写到第几个字节。
 *   在文件打开时,该指示器总是指向文件的第一个字节。使用fgetc()函数后,该指针回向后移动一个字符/字节
 *   所以可以连续多次使用fgetc()读取多个字符。
 * - 注意:这个文件内存的位置指示器与C语言中的指针不是一回事。
 *   位置指示器(位置指针)仅仅是一个标志,表示文件读写到的位置,也就是读写到几个字节,它不表示地址。
 *   文件每读写一次,位置指针就会移动一次,它不需要在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。
 *
 * 参数:
 * - stream:指向标识输入流的FILE对象的指针。
 *
 * 返回值:
 * - 返回值类型是int,以适应表示失败的特殊值EOF。
 * - 读取成功,返回读取的字符(提升为int值)。
 * - 如果位置指示符位于文件末尾,则该函数返回EOF,并设置流的EOF指示器(feof)
 *   如果发生其他读取错误,该函数也会返回EOF,但会设置其错误指示器(ferror)
 * - EOF 是end of file 的缩写,表示文件末尾,是在stdio.h中定义的宏,其值往往是一个负数:-1。
 *   fgetc()的返回值类型值所以是int,就是为了容纳这个负数(char不可能是负数)
 *   EOF也不绝对是-1,也可以是其他负数,还要看编译器的实现。
 */
#include <stdio.h>

int main()
{
    //从文件中读取数据
    FILE* pf = fopen("test.dat","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读文件   
    int ret = fgetc(pf);
    printf("%c\n",ret);
    ret = fgetc(pf);
    printf("%c\n",ret);
    ret = fgetc(pf);
    printf("%c\n",ret);

    //关闭文件
    fclose(pf);
    pf = NULL;

    //fgetc()从标准输入流读取信息
    printf("请输入信息,以供读取:");
    int ret1 = fgetc(stdin);
    printf("%c\n",ret1);
    ret1 = fgetc(stdin);
    printf("%c\n",ret1);
    ret1 = fgetc(stdin);
    printf("%c\n",ret1);

    return 0;
}

以字符串形式读写文本文件

fputs()函数

/*
 * fputs()函数
 * 函数原型:int fputs(const char* str,FILE* stream);
 * 作用:将字符串写入流。
 * - 将str指向的C字符串写入流,该函数从指定的地址(str)开始复制,直到到达终止空字符'\0' 。
 *   此终止空字符不会复制到流中。
 * - 注意:fputs与puts的不同不仅在于可以指定目标流。
 *   fputs()不会写入额外的字符 ,而puts()会自动在末尾加上一个换行符。
 *
 * 参数:
 * - str:要写入流的字符串。
 * - stream:指向表示输出流的FILE对象的指针。
 *
 * 返回值:
 * - 写入成功,返回一个非负值。
 * - 发生错误,该函数返回EOF并设置错误指示符EOF
 */
#include <stdio.h>

int main()
{
    FILE* pf =fopen("test.dat","w");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写文件:按照行(字符串)来写。
    //如果写入文件的内容需要换行,那么我们需要自己加上换行符。如果不加换行,则写入文件的内容都是在一起的。
    fputs("dear dog\n",pf);
    fputs("you are very good\n",pf);

    //关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

fgets()函数

/*
 * fgets()函数
 * 函数原型:char* fgets(char* str,int num,FILE* stream);
 * 作用:从流中读取字符并将他们作为字符串,存储到str中,直到读取到(num-1)个字符或到达换行符或文件结尾,以先发生者为准。
 * - 换行符使fgets停止读取,但它被视为有效字符并包含在复制到str的字符串中。
 * - 在复制到str的字符之后会自动附加一个终止空字符。读取到的字符串会在末尾自动添加\0,n个字符是包括\0的。
 *   也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。
 *
 * 注意:
 * - fgets()与gets()完全不同。fgets()接收流参数,但也允许指定str的最大大小,并在字符串中包含任何结束的换行符。
 *   gets()会忽略换行符。
 * - 需要重点说明的是:在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。
 *   这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。
 *   在C语言中,没有按行读取文件的函数,我们借助该函数,将n的值设置地足够大,每次就可以读取到一行数据。
 *
 * 参数:
 * - str:这是指向一个字符数组的指针,该数组存储要读取的字符串。
 * - n:要复制到str中的最大字符数(包括终止空字符)。通常是使用以str传递进来的数组的长度。
 * - stream:指向标识输入流的FILE对象的指针。stdin可用作从标准输入读取的参数。
 *
 * 返回值:
 * - 函数调用成功,返回str。
 * - 如果在尝试读取字符时,遇到文件结尾,则设置EOF指示符(feof)。
 *   如果在读取任何字符之前发生这种情况,则返回的是空指针(str内容保持不变)
 * - 如果发生读取错误,将设置错误指示符(ferror)并返回空指针(但str指向的内容可能已经更改)
 */

#include <stdio.h>

int main()
{
    char arr[10] = {0};
    FILE* pf = fopen("test.dat","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }

    //读文件
    fgets(arr,10,pf);
    printf("%s\n",arr);

    fgets(arr,7,pf);
    printf("%s",arr);

    //关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

格式化读写文件

fscanf()函数 与 fprintf() 函数与之前使用的 scanf()函数 与printf()函数 功能类似,都是格式化读写函数,两者的区别在于fsacnf()与fprintf() 的读写对象不是键盘与显示器,而是磁盘文件。

fprintf()函数

/*
 * fprintf()函数
 * 函数原型:int fprint(FILE* stream,const char* format,...);
 * 作用:格式化输出到指定流中。
 * - 如果format包含格式说明符(以%开头的子序列),则format后面的附加到参数将被格式化并插入到结果字符串中,替换各自的说明符。
 * - 在format参数之后,该函数需要至少与format指定的一样多的附加参数。
 *
 * 参数:
 * - stream:指向标识输出流的FILE对象的指针。
 *   如果将stream设置为 stdout,那么 fprintf() 函数将会向显示器输出内容,与 printf 的作用相同。
 * - format:包含要写入到流中的文本。可以选择包含嵌入的格式说明符,这些说明符被后续附加参数中指定的值替换,并根据请求进行格式化。
 * 返回值:
 * - 调用成功,返回写入的字符总数。
 * - 如果发生写入错误,则设置错误指示符(ferror)并返回负数。
 *   如果在写入宽字节时发生多字节字符编码错误,则将errno设置为EILSEQ并返回一个负数。
 */
#include <stdio.h>

struct S
{
    char arr[10];
    int num;
    float sc;
};

int main()
{
    struct S s = {"abcdef",10,5.5f};
    struct S s1 = {"abcdee",12,6.5f};
    //对格式化的数据进行写文件
    FILE* pf = fopen("test.dat","w");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写文件
    fprintf(pf,"%s %d %f",s.arr,s.num,s.sc);
    fprintf(pf,"%s %d %f",s1.arr,s1.num,s1.sc);
    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

fscanf()函数

/*
 * fscanf()函数
 * 函数原型:int fscanf(FILE* stream,const char* format,...);
 * 作用:从流中读取数据,并根据参数格式将他们存储到附加参数指向的位置。附加参数应指向已分配的对象,其类型由格式字符串中的相应格式说明符确定。
 * 参数:
 * - stream:指向FILE对象的指针,该对象标识要从中读取数据的输入流。
 *   如果将stream设置为stdin,那么fscanf()函数将会从键盘读取数据,与scanf()的作用相同。
 * - format:包含一系列字符的字符串,这些字符控制如何处理从流中提取的字符。
 *
 * 返回值:
 * - 成功时,该函数返回参数列表中成功匹配的个数。
 *   由于匹配失败、读取错误或达到文件末尾,此计数即可与预期的项目数匹配或更少(甚至为0)
 * - 如果发生读取错误或读取时达到文件末尾,则会设置正确的指示符(feof或ferror)。
 *   并且,如果其中一种情况发生在任何数据成功读取之前,则返回EOF。
 * - 如果在解释宽字符时发生编码错误,该函数会将errno设置为EILSEQ。
 */
#include <stdio.h>

struct S
{
    char arr[10];
    int num;
    float sc;
};

int main()
{
    struct S s = {0};
    struct S s1 = {0};
    //对格式化的数据进行写文件
    FILE* pf = fopen("test.dat","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读文件
    //s.arr arr是数组名,本身就是地址。而s.num是int类型,所以需要取地址。
    //因为.的优先级更高,所以也可以不加()。
    fscanf(pf,"%s %d %f",s.arr,&s.num,&s.sc);
    fscanf(pf,"%s %d %f",s1.arr,&(s1.num),&(s1.sc));

    //打印
    printf("%s %d %f\n",s.arr,s.num,s.sc);
    //使用fprintf()函数在标准输出流输出。
    fprintf(stdout,"%s %d %f",s1.arr,s1.num,s1.sc);
    
    //printf是对fprintf()的封装

    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

以数据块的形式读写文件

fgets()有局限性:每次最多只能从文件中读取一行内容,因为fgets()遇到换行符就结束读取。

如果希望读取多行数据,就需要使用fred()函数;相应的写入函数为fwrite()。

fwrite()函数

/*
 * fwrite()函数
 * 函数原型:size_t fwrite(const void* ptr,size_t size,size_t count,FILE* stream);
 * 作用:
 * - 将count个元素的数组从ptr指向的内存块中写入到stream流中。每个元素的大小为size
 *   流的位置指示器按写入的总字节数向后移动。
 * - 在内部,该函数将ptr指向的内存块解释为unsigned char类型的size*count元素数组
 *   并将它们顺序写入流,就像对每个字节调用fputc一样。
 *
 * 参数:
 * - ptr:指向要写入的元素数组的指针,转换为const void*。
 * - size:要读取的每个元素的大小,以字节为单位。size_t是无符号整型。
 * - count:元素个数,每个元素的大小为size字节。
 * - stream:指向指定输出流的FILE对象的指针。
 *
 * 返回值:
 * - 返回成功读写的块数,即count。
 * - 如果此数字与count参数不同,则写入错误会阻止函数完成。在这种情况下,将为流设置错误指示符(ferror)。 
 *   如果返回值小于0,则发生了写入错误,可以用ferror()函数检测。
 * - 如果size或count为0,则该函数返回值0,并且错误指示符保持不变。
 */
#include <stdio.h>

struct S
{
    char arr[10];
    int num;
    float sc;
};

int main()
{
    struct S s = {"abcdef",10,5.5f};
    struct S s1 = {"abcdee",12,6.5f};
    //对格式化的数据进行写文件
    FILE* pf = fopen("test.dat","w");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写文件
    fwrite(&s,sizeof(struct S),1,pf);
    fwrite(&s1,sizeof(struct S),1,pf);

    /*
     * 在查看文件时,我们发现是一堆乱码。
     * - 但是可以看出字符串,因为字符串以二进制形式与以文本形式写进去是相同的。
     * - 但是整数与浮点数,以二进制形式和以字符形式写进去,是完全不同的。
     * 虽然我们看不懂,但是以fwrite()写进去的,用fread()读取就可以显示了。
     */

    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

fread()函数

/*
 * - fread() 函数用来从指定文件中读取块数据。
 *   所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。
 *
 * fread()函数
 * 函数原型:size_t fread(void* ptr,size_t size,size_t count,FILE* stream);
 * 作用:
 * - 从流中读取数据块。从流中读取一个count元素数组,每个元素大小为size字节,并将他们存储到ptr指定的内存块中。
 *   流的位置指示符按读取的总字节数向下一个移动。如果成功,则读取的总字节数=size*count
 *
 * 参数:
 * - ptr:指向大小至少为size*count字节的内存块的指针,转换为void*
 * - size:要读取的每个元素的大小,以字节为单位。size_t是无符号整型。
 * - count:元素个数,每个元素的大小为size字节。
 * - stream:指向指定输入流的FILE对象的指针。
 *
 * 返回值:
 * - 返回成功读取的元素总数。
 * - 如果如果返回值与count不同,则说明发生错误,或读取时到达文件末尾,此时返回值小于0。这两种情况下,都设置了正确的指示符,可以分别使用ferror和feof进行检查。
 * - 如果size或count为零,则函数返回零,并且流状态和ptr指向的内容都保持不变。
 */
#include <stdio.h>

struct S
{
    char arr[10];
    int num;
    float sc;
};

int main()
{
    struct S s = {0};
    struct S s1 = {0};
    //对格式化的数据进行写文件
    FILE* pf = fopen("test.dat","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读文件
    fread(&s,sizeof(struct S),1,pf);
    fread(&s1,sizeof(struct S),1,pf);

    //打印
    printf("%s %d %f\n",s.arr,s.num,s.sc);//abcdef 10 5.500000
    printf("%s %d %f\n",s1.arr,s1.num,s1.sc);//abcdee 12 6.500000

    //关闭文件
    fclose(pf);
    pf = NULL;
    return 0;
}

三种输入/输出函数的比较

/*
 *  scanf:标准输入流的格式化输入 - stdin
 * fscanf:所有输入流的格式化输入 - stdin/文件
 * sscanf:从一个字符串中读取一个格式化数据。
 *
 *  printf:标准输出流的格式化输出 - stdout
 * fprintf:所有输出流的格式化输出 - stdout/文件
 * sprintf:把一个格式化的数据,转换成字符串。
 */

/*
 * sprintf()函数
 * 函数原型:int sprintf(char* str,const cahr* format,...);
 * 作用:将格式化数据输出到str指向的字符串。
 * - 如果在sprintf上使用format,则使用相同的文本组成一个字符串,但是不打印,而是将内容作为C字符串存储到str指向的空间。
 *   str指向的空间,应该足够大,以包含整个结果字符串。内容后会自动附加一个终止空字符。
 * - 在format参数之后,该函数至少需要与format中指定字符的一样多的参数
 *
 * 参数:
 * - str:指向存储结果字符串的字符数组的指针,这个字符数组也被叫做缓冲区。数组应足够大,以包含结果字符串。
 * - format:包含样式字符串的C字符串。
 * - 附加参数:根据格式字符串,该函数需要一系列附加参数,每个参数都包含一个值,用于替换格式字符串中的格式说明符。
 *   这些参数的数量,至少应与格式说明符中指定的数量一样多。该函数会忽略其他参数。
 *
 * 返回值:
 * - 成功时,返回写入的字符总数。此计数不包括自动追加在字符串末尾的额外空字符。
 * - 失败时,返回一个负数。
 *
 */
/*
 * sscanf()函数
 * 函数原型:int sscanf(const char* s,const char* format,...);
 * 作用:
 * - 从字符串s中读取数据,并根据参数格式将他们存储到附加参数给定的位置。
 *   就像使用了scanf一样,但是sscanf不是从标准输入(stdin)读取,而是从s字符串中读取
 * - 附加参数应指向已分配的对象,其类型由格式字符串中的相应格式说明符指定。
 *
 * 参数:
 * - str:字符串,函数检索的数据源。
 * - format:包含格式字符串的字符串。
 * - 附加参数:根据格式字符串,该函数需要一系列附加参数,每个参数都包含一个指向已分配存储的指针,
 *   根据格式字符串的指定解释,以适当的类型存储。这些参数的数量,至少应与格式说明符中指定的数量一样多。该函数会忽略其他参数。
 *
 * 返回值:
 * - 成功时,该函数返回参数列表中成功匹配和赋值的个数。
 *   如果失败,此计数比预期数更小(甚至为0)。如果要判断,可以将该计数与预期数进行比较
 * - 如果到达文件末尾或发生错误:在成功解释任何数据之前输入失败,则返回EOF。
 *
 */

#include <stdio.h>

struct S
{
    char arr[10];
    int age;
    float f;
};

int main()
{
    //注意这里的字符串中不能有空格。否则不可以正常输出。
    //因为在输入时,sscanf是以空格来区分参数的,读取时就会发生错误。
    struct S s = {"hello",20,5.5f};
    struct S tmp = {0};
    char buf[100] = {0};
    //sprintf把一个格式化的数据,转换成字符串
    sprintf(buf,"%s %d %f",s.arr,s.age,s.f);
    printf("%s\n",buf);

    //从buf字符串中还原出结构体
    sscanf(buf,"%s %d %f",tmp.arr,&(tmp.age),&(tmp.f));
    printf("%s %d %f",tmp.arr,tmp.age,tmp.f);

    return 0;
}

改造通讯录

contact.h

//类型定义、函数声明。
#ifndef FIRST_CONTACT_H
#define FIRST_CONTACT_H

//头文件引入
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


//常量的定义
#define MAX_NAME 20
#define MAX_SEX 10
#define MAX_TELE 12
#define MAX_ADDR 30

#define DEF_SZ 3
#define INC_SZ 2

//类型定义
typedef struct PeoInfo
{
    //将其定义为常量,修改时可以在定义的位置修改。
    char name[MAX_NAME];
    char sex[MAX_SEX];
    int age;
    char tele[MAX_TELE];
    char addr[MAX_ADDR];
}PeoInfo;  //重命名该结构体类型为PeoInFo

//因为我们为通讯录添加人的时候,需要知道往哪里添加,所以这里我们将其再次封装。
typedef struct Contact
{
    PeoInfo* data; //指向动态申请的空间,用来存放练习人的信息
    //PeoInfo* data = malloc(3*size0f(PeoInfo));
    int sz;//当前通讯录中有多少人
    int capacity; //记录当前通讯录的最大容量是几人
}Contact;//重命名该结构体类型为Contact


//————————函数声明
//初始化通讯录
void InitContact(Contact* pc);
//添加联系人
void AddContact(Contact* pc);
//打印通讯录
void PrintContact(const Contact* pc);
//删除联系人
void DelContact(Contact* pc);
//查找
void SearchContact(Contact* pc);
//修改
void ModContact(Contact* pc);
//销毁通讯录
void DestoryContact(Contact* pc);
//保存通讯录信息
void SaveContact(Contact* pc);
//加载通讯录信息
void LoadContact(Contact* pc);

#endif //FIRST_CONTACT_H

contact.c

//函数实现

#include "contact.h"

//初始化通讯录函数的实现
void InitContact(Contact* pc)
{
    pc->data = (PeoInfo*)malloc(DEF_SZ* sizeof(PeoInfo));
    if(pc->data == NULL)
    {
        perror("InitContact");
        return;
    }
    pc->sz = 0;
    pc->capacity = DEF_SZ;

    //加载文件
    LoadContact(pc);

}

//增容函数
void checkCapacity(Contact* pc)
{
    if(pc->sz == pc->capacity)
    {
        //扩容后的大小:(当前最大容纳几人+2)*每人所占大小
        PeoInfo* ptr = (PeoInfo*)realloc(pc->data,(pc->capacity+INC_SZ)* sizeof(PeoInfo));
        if(ptr != NULL)
        {
            pc->data = ptr;
            pc->capacity += INC_SZ;
            printf("== 通讯录容量不够,已进行扩容 ==\n");
        }
        else
        {
            perror("AddContact");
            printf("通讯录扩容失败");
            return;
        }
    }
}
//添加联系人
void AddContact(Contact* pc)
{
    //判断通讯录是否满了,满了就扩容
    //调用增容函数checkCapacity();
    checkCapacity(pc);
    //添加一个人的信息
    //当前sz是几,表示有几条信息。我们为data数组下表为sz的元素添加信息。
    //因为name、sex、tele、addr都是数组,而数组名就是地址,所以不用再取地址。
    //而age是int类型,所以要整体括起来取其地址。
    printf("请输入名字:");
    scanf("%s",pc->data[pc->sz].name);
    printf("请输入性别:");
    scanf("%s",pc->data[pc->sz].sex);
    printf("请输入年龄:");
    scanf("%d",&(pc->data[pc->sz].age));
    printf("请输入电话:");
    scanf("%s",pc->data[pc->sz].tele);
    printf("请输入地址:");
    scanf("%s",pc->data[pc->sz].addr);

    //添加之后,sz++,此时通讯录中就添加了一条信息
    pc->sz++;
    printf("== 添加成功 == \n");
}

//打印:肯定不会修改,所以加const修饰
void PrintContact(const Contact* pc)
{
    int i;
    //printf("   姓名 — 性别 — 年龄 — 电话 — 地址\n");
    //%10s表示要打印的字符串如果不满10个字符,不满的部分则会在其前面以空白补充。可以说是“右对齐”这种方式
    //%-10s表示要打印的字符串如果不满10个字符,不满的部分则会在其后面以空白补充。可以说是“左对齐”这种方式
    printf("   %-10s  %-4s  %-4s  %-12s  %-20s\n","姓名","性别","年龄","电话","地址");

    for(i=0; i<pc->sz ; i++)
    {
        printf("%d. %-10s   %-3s   %-3d %-12s %-20s\n",i+1,pc->data[i].name,pc->data[i].sex,pc->data[i].age,pc->data[i].tele,pc->data[i].addr);
    }
}

//通过名字查找,并返回其在数组中的下标。加static表示这个函数只能在本文件中使用。
static int FindByName(Contact* pc,char name[])
{
    int i = 0;
    for(i=0 ; i<pc->sz ; i++)
    {
        if(strcmp(pc->data[i].name,name) == 0)
        {
            return i;
        }
    }
    //找不到返回-1
    return -1;
}

//删除联系人
void DelContact(Contact* pc)
{
    if(pc->sz == 0)
    {
        printf("通讯录为空\n");
        return;
    }
    char name[MAX_NAME] = {0};
    printf("请输入要删除的联系人姓名:");
    scanf("%s",name);

    //查找要删除的人
    //没有则结束方法
    int pos = FindByName(pc,name);
    if(pos == -1)
    {
        printf("通讯录中不存在此联系人\n");
        return;
    }

    //删除这个人,假设sz是1000,则pos=999时,999+1=1000,下标为1000,数组发生了越界
    //所以我们让sz-1,这样数组就不会越界了。但是最后一个元素删除不了了
    int i = 0;
    for(i=pos ; i<pc->sz-1 ; i++)
    {
        //用下一个元素将这个元素覆盖。
        pc->data[i] = pc->data[i+1];
    }

    //我们如果要删除最后一个元素,则需要另外删除。
    //以上只能删除下标为:0~sz-1(不包括sz-1)的任意一条数据,但是下标为sz-1处的数据没办法删除。所以我们手动删除。
    if(pos == pc->sz-1)//当pos时最后一条数据的时候
    {
        //pc->data是数组首元素的地址,+pc->sz-1就是指向最后一个元素的指针
        //将其后的一个结构体大小的数据置为0。
        memset((pc->data+pc->sz-1),0,sizeof(PeoInfo));
    }

    //每次删除后,有效数据数sz-1。所以也可以不做对最后一个元素的删除。
    //就算不删除最后一个元素,每次执行后sz都减去1,这样即使最后一个元素没有删除,但是sz-1了,这个元素不显示了,也跟删除一样。
    //等到下次要添加数据的时候,会直接添加sz的位置,就把最后一个元素覆盖掉了。
    pc->sz--;
    printf("== 删除成功 ==\n");
}

//查找指定联系人
void SearchContact(Contact* pc)
{
    char name[MAX_NAME] = {0};
    printf("请输入要查找人的姓名:");
    scanf("%s",name);

    //查找
    int pos = FindByName(pc,name);
    if(pos == -1)
    {
        printf("通讯录中不存在此联系人\n");
    }
    else
    {
        printf("   %-10s  %-4s  %-4s  %-12s  %-20s\n","姓名","性别","年龄","电话","地址");
        printf("%d. %-10s   %-3s   %-3d %-12s %-20s\n",pos+1,pc->data[pos].name,
               pc->data[pos].sex,pc->data[pos].age,pc->data[pos].tele,pc->data[pos].addr);
    }
}

//修改
void ModContact(Contact* pc)
{
    char name[MAX_NAME] = {0};
    printf("请输入要修改的联系人的姓名:");
    scanf("%s",name);

    //查找
    int pos = FindByName(pc,name);
    if(pos == -1)
    {
        printf("通讯录中不存在此联系人\n");
    }
    else
    {
        //因为是按照名字修改,所以我们就不改名字了
//        printf("请输入名字:");
//        scanf("%s",pc->data[pc->sz].name);
        printf("修改性别为:");
        scanf("%s",pc->data[pos].sex);
        printf("修改年龄为:");
        scanf("%d",&(pc->data[pos].age));
        printf("修改电话为:");
        scanf("%s",pc->data[pos].tele);
        printf("修改地址为:");
        scanf("%s",pc->data[pos].addr);
        printf("== 修改成功 ==");
    }
}

//销毁通讯录
void DestoryContact(Contact* pc)
{
free(pc->data);
pc->data = NULL;
pc->sz = 0;
pc->capacity = 0;
}

//保存通讯录信息
void SaveContact(Contact* pc)
{
    FILE* pf = fopen("contact.dat","w");
    if(pf == NULL)
    {
        perror("saveContact");
        return;
    }
    //写文件
    int i;
    for(i=0 ; i<pc->sz ; i++)
    {
        fwrite(pc->data+i,sizeof(PeoInfo),1,pf);
    }
    //关闭
    fclose(pf);
    pf = NULL;
}

//加载文件中数据
void LoadContact(Contact* pc)
{
    FILE* pf = fopen("contact.dat","r");
    if(pf == NULL)
    {
        perror("LoadContact");
        return;
    }
    //读文件。
    PeoInfo tmp = {0};
    //fread()函数返回成功读的元素个数。
    //因为我们是一个一个读取,所以如果读取完了,则下一次读取就返回0,因为已经读取完了,就没有读取到,没有读取到就返回0,此时循环停止
    while(fread(&tmp, sizeof(PeoInfo),1,pf))
    {
        //通讯录默认大小是3,而我们的数据文件中的数据条数可能是大于3的,所以我们在这里判断通讯录是否满了
        checkCapacity(pc);
        //将其放入通讯录中
        pc->data[pc->sz] = tmp;
        //放入后sz++
        pc->sz++;
    }
    //关闭文件
    fclose(pf);
    pf=NULL;
}

test.c

//测试通讯录的模块
/*
 * 版本3:
 * - 当通讯录退出时,把信息写入到文件。
 * - 当程序运行时,加载文件中的信息到通讯录中
 */

#include "contact.h"

void menu()
{
    printf("--------------------------------------------------\n");
    printf("----------1. add     2. del    3. mod-------------\n");
    printf("----------4. search  5. sort   6.print------------\n");
    printf("------------------   0. exit   -------------------\n");
    printf("--------------------------------------------------\n");
    printf("请选择:");
}

//使用枚举
enum Option
{
    //从0开始递增1。与菜单上的选项相对应。
    EXIT,
    ADD,
    DEL,
    MOD,
    SEARCH,
    SORT,
    PRINT
};

int main()
{
    int input = 0;
    //创建通讯录
    Contact con;

    //调用函数对通讯录进行初始化。
    //为data在堆上申请一块连续的空间
    //sz=0
    //将capacity初始化为当前data指向的空间快的最大容量。
    //并且初始化时,访问数据文件,将保存的通讯录信息读取出来。
    InitContact(&con);

    do
    {
        menu();
        scanf("%d",&input);
        switch(input)
        {
            case ADD:
                //为通讯录中添加联系人。因为是要为已经创建好的通讯录添加,所以肯定是传址调用。
                AddContact(&con);
                break;
            case DEL:
                DelContact(&con);
                break;
            case MOD:
                ModContact(&con);
                break;
            case SEARCH:
                SearchContact(&con);
                break;
            case SORT:
                //排序自己实现。
                //因为没有一个比较适合排序的选项,所以不再实现
                break;
            case PRINT:
                PrintContact(&con);
                break;
            case EXIT:
                //保存通讯录中的信息
                SaveContact(&con);
                //销毁通讯录 —— 释放动态开辟的内存
                DestoryContact(&con);
                printf("程序退出,通讯录已销毁");
                break;
            default:
                printf("输入错误请重新输入。\n");
                break;
        }
    }while(input);
    return 0;
}

文件的随机读写

fseek()函数

/*
 * fseek函数()
 * 函数原型:int fseek(FILE* stream,long int offset,int origin);
 * 作用:设置stream的文件位置为给定的偏移offset。新的位置是通过将offset添加到由origin指定的参考位置来定义的。
 *
 * 参数:
 * - stream:指向标识流的FILE对象的指针。
 * - offset:也就是要移动的字节数。之所以为long类型,是希望移动的范围更大,能处理的文件更大。
 *   offset为正时,向后移动;offset为负时,向前移动。
 * - origin:
 *   # SEEK_SET:文件开头 常量值:0
 *   # SEEK_CUR:文件指针的当前位置  常量值:1
 *   # SEEK_END:文件结束。 常量值:2
 *
 * 注意:fseek() 一般用于二进制文件,在文本文件中由于要进行转换,计算的位置有时会出错。
 *
 * 返回值:
 * - 函数调用成功,返回0。否则返回非零值。
 *   如果发生读取或写入错误,则会设置错误提示符(ferror)。
 */
#include <stdio.h>

int main()
{
    FILE* pf = fopen("test.txt","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读取文件
    int ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,1,SEEK_CUR);
    ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,-2,SEEK_END);
    ch = fgetc(pf);
    printf("%c\n",ch);
    return 0;
}

ftell()函数


/*
 * ftell()函数
 * 函数原型:long int ftell(FILE* stream);
 * 作用:返回stream的位置指示器在文件中的当前位置。
 * 参数:stream:指向标识流的FILE对象的指针。
 * 返回值:成功,返回位置指示器的当前值。如果发生错误,则返回-1L,全局变量errno被设置为系统特定的正值。
 */

#include <stdio.h>

int main()
{
    FILE* pf = fopen("test.txt","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读取文件
    int ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,1,SEEK_CUR);
    ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,-3,SEEK_END);
    ch = fgetc(pf);
    printf("%c\n",ch);

    int ret = ftell(pf);
    printf("%d\n",ret);

    return 0;
}

rewind()函数

/*
 * rewind()函数
 * 函数原型:void rewind(FILE* stream);
 * 作用:将stream中位置指示器设置为文件的开头。设置为开头后,与stream关联的文件结尾/错误内部提示符将被清楚。
 * 参数:stream——指向标识流的FILE对象的指针
 */
#include <stdio.h>

int main()
{
    FILE* pf = fopen("test.txt","r");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读取文件
    int ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,1,SEEK_CUR);
    ch = fgetc(pf);
    printf("%c\n",ch);

    fseek(pf,-3,SEEK_END);
    ch = fgetc(pf);
    printf("%c\n",ch);

    int ret = ftell(pf);
    printf("%d\n",ret);

    //让文件返回起始位置
    rewind(pf);
    ch = fgetc(pf);
    printf("%c\n",ch);

    return 0;
}

文本文件和二进制文件

在这里插入图片描述

/*
 * 根据数据的组织形式,数据文件被称为:文本文件或二进制文件。
 * - 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
 * - 如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储,就是文本文件。
 *
 * 一个数据在内存中是怎么存储的呢?
 * - 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式进行存储,也可以用二进制形式存储。
 * - 如有正数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节)
 *   而以二进制形式输出,在磁盘上只占四个字节。
 */
#include <stdio.h>

int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt","wb");
    if(pf == NULL)
    {
        perror("fopen");
        return 1;
    }

    //以二进制形式写入
    fwrite(&a,sizeof(int),1,pf);
    //写入后,直接打开是:' 。将文件test.txt用二进制编译器打开。发现其中是:0000 0000 10 27 00 00 ,是采用小端存储
    //10000的二进制是:00000000 00000000 00100111 00010000转换为十六进制就是:00 00 27 10
    //倒着存放在内存中就是:10 27 00 00
    return 0;
}

文件读取结束的判定

/*
 * 在文件读取的过程中,不能使用feof()函数的返回值直接用来判断文件是否结束
 * - 该函数用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件结尾结束。
 *
 * 那如何判断文件是否读取结束呢?
 * 文本文件是否读取结束的判断:
 * - 例如:fgetc函数在读取结束的时候,会返回EOF。正常读取的时候,返回读取到的字符的ASCII码
 *   所以我们判断fgetc函数是否读取结束时,就判断它的返回值是否为EOF,如果是返回EOF,则说明读取结束。
 * - 而fgets()函数:读取结束时返回NULL。正常读取时,返回存放字符串的空间的起始地址。
 *   所以判断fgets()是否解读结束时,接判断其返回值是否为NULL。如果是返回NULL,则说明文件读取结束。
 *
 * 二进制文本文件读取结束的判断:
 * - fread()函数在读取时,返回实际读取的完整元素的个数。
 *   如果我们读取到的元素的个数 < 指定读取的元素个数,则说明是最后一次读取。此时就说明文件读取结束了。
 */

/*
 * ferror()函数
 * 函数原型:int ferror(FILE* stream);
 * 作用:测试为stream设置的错误标识符
 * 参数:stream,指向标识流的FILE对象的指针。
 * 返回值:如果被设置了错误指示符,则是非0的值。否则,返回0。
 *
 * feof()函数
 * 函数原型:int feof(FILE* stream);
 * 作用:检查是否为stream设置了文件结束标识符。如果设置了则返回一个非0的值。
 * 参数:指向标识流的FILE对象的指针
 * 返回值:当设置了与流关联的文件结束标识符后,该函数返回一个非0值,否则返回0。
 */

文本文件的判定

//将test.txt拷贝到test2.txt
#include <stdio.h>

int main() {
    FILE *pfread = fopen("test.txt", "r");
    if (pfread == NULL) {
        perror("fopen");
        return 1;
    }
    FILE *pfwrite = fopen("test2.txt", "w");
    if (pfwrite == NULL) {
        fclose(pfread);
        pfread = NULL;
        return 1;
    }

    //读取文件
    int ch = 0;
    while ((ch = fgetc(pfread)) != EOF) {
        fputc(ch, pfwrite);
    }
    //读取完后判断是因为什么而停止的
    if (feof(pfread))
    {
        printf("遇到结束符,文件读取结束");
    }
    else if(ferror(pfread))
    {
        printf("读取出错结束");
    }

    //关闭文件
    fclose(pfwrite);
    pfwrite = NULL;
    fclose(pfread);
    pfread = NULL;
    return 0;
}

二进制判定示例

#include <stdio.h>

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);
}

文件缓冲区

在这里插入图片描述

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

#include <stdio.h>
#include <windows.h>
//WIN10环境测试
int main()
{
    FILE*pf = fopen("test.txt", "w");

    fputs("abcdef", pf);//先将代码放在输出缓冲区
    printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
    Sleep(10000);
    printf("刷新缓冲区\n");
    fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)

    //注:fflush 在高版本的VS上不能使用了
    printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
    Sleep(10000);
    fclose(pf);
    //注:fclose在关闭文件的时候,也会刷新缓冲区
    pf = NULL;
    return 0;
}

gets、getchar、缓冲区讲解

gets()函数

/*
 * char * gets(char * str);
 * 功能:从标准输入读取字符并将它们作为C字符串存储到str中,直到达到换行符或文件结尾。
 *      如果读取到换行符,则不会将其复制到str中,而是在复制到str的字符之后自动附加一个字符串结尾符。
 * 参数:str —— 这是指向一个字符数组的指针,该数组中存储了C字符串。
 * 返回值:
 * - 成功,函数返回str;
 * - 如果到达文件结尾还未读取到任何字符,则返回NULL,并且str的内容保持不变
 * - 如果发生读取错误,则设置错误提示符(ferror),并返回空指针,但str指向的内容可能已经更改。
 */

getchar()函数——只适用于标准输入流

/*
 *  int getchar(void)函数 —— 作用:输入一个字符。通常用于接收空格/换行符
 *  返回值:该函数以无符号char强制类型转换为int的形式返回读取的字符,如果达到文件末尾或发生错误,则会返回(-1)EOF
 *         返回字符对应的ASCII码
 *
 *  int putchar(int char) —— 作用:输出一个字符。char是要输出的参数。
 *  返回值:该函数以无符号char强制转换为int的形式返回写入的字符,如果发生错误则会返回(-1)EOF
 *         参照ASCII表,将char对应的字符返回。
 *
 *注意:
 *  - 当程序调用getchar()时,等待用户按键,用户输入的字符被存放在键盘缓冲器中,直到用户按回车为止(这个回车字符也存放在缓冲区中)。
 *    用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。
 *  - getchar()函数不仅可以从输入设备获取一个可显示的字符,而且可以获得屏幕上无法显示的字符,如:回车换行/空格等
 *  - getchar()函数的返回值是:用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕
 *  - 如果用户在按回车之前输入了不止一个字符,其他字符也会保留在键盘缓冲区中,等待后续getchar()调用读取
 *    也就是说:后续的getchar调用不会等待用户按键,而是直接读取缓冲区中的字符,直到缓冲区中的字符读取完之后,才等待用户按键
 *
 *主要用法:
 *  - 清空回车符,这种情况一般发生在寻魂中涉及到输入的情况。
 *  - 某些编译平台(IDE)在运行程序时,并没有在程序程序运行后给别人看结果的时间。这时在程序最后加上getchar(),就可以造成程序的暂停。
 *  - 使用getchar();时,输入完字符,要按回车才能读取进去
 *    使用getch();时,在键盘上按一个字符马上就被读取进去,不用按回车,因此可以作为“按任意键继续”的执行语句。
 */

/*
 * - getch()函数
 *   getch与getchar基本功能相同,差别是:getch直接从键盘获取键值,不等待用户按回车,只要用户按一个键,getch就立刻返回
 *   getch返回值是用户输入的字符的ASCII码,出错返回-1。
 *   - 输入的字符不会会现在屏幕上。getch函数常用于程序调试中。在调试时,在关键位置显示有关的结果以待查看。
 *     然后用getch函数暂停函数运行,当按任意键后程序继续运行。
 *   - getch()是非缓冲输入函数,就是不能用getch()来接收缓冲区已存在的字符。
 *
 * - getche()函数
 *   这个函数与前两上类似,功能也相近,都是输入一个字符,返回值同样是输入字符的ASCII码。
 *   但不同的是,此函数在输入后立即从控制台取字符,不以回车为结束(带回显)。
 *
 */
#include <stdio.h>

int main()
{
    int ch = 0;
    /*
     * - 虽然getchar()是用于输入一个字符。
     *   但如果用户在按回车之前输入了不止一个字符,其他字符也会保留在键盘缓冲区中,等待后续getchar()调用读取。
     *   也就是说:后续的getchar调用不会等待用户按键,而是直接读取缓冲区中的字符,直到缓冲区中的字符读取完之后,才等待用户按键
     *
     * 程序运行后,等待键盘输入。输入一串字符后,循环读取这串字符,按顺序输出。
     * 如输入abc并回车,实际输入:abc\n
     * 则会输出  97——a----     98——b----     99——c----     10——
                ----
     * 这里10——后面换行才输出了----,就说明了:\n也被存储到了缓冲区中,其对应的ASCII码是:10。
     *
     */
    while ((ch=getchar()) != EOF)
    {
        //getchar()函数的返回值是其字符对应的ASCII码
        printf("%d——",ch);

        //putchar()函数的返回值是其传入变量对应的字符。
        putchar(ch);
        printf("----\t");
    }
}

缓冲区详解

/*
 * 键入缓冲区——所有从键盘输入的数据,不管是字符还是数字,都是先存储在内存的缓存区,叫做"键盘输入缓冲区",简称"输入缓冲区"或"输入流"
 * 当我们调用的函数需要我们用键盘输入时,我们输入的数据都会被依次存入缓冲区。
 * - 但是只有当按下回车之后,scanf函数才会进入缓冲区取数据,所取数据的个数取决于scanf的"输入参数"的个数。
 *   所以不在于怎么输入。可以存一个取一个,也可以一次性全部存进去,然后一个一个取。
 *
 * 使用%d 和 %c  读取缓存的差别:
 * - 对于%d,在缓冲区中,空格、回车、tab键都只是分隔符,不会被sacnf当成数据使用。%d碰见他们就跳过,取下一个数据。
 *   遇到字符,会直接退出,不再取其中的数据,就算还有变量也不会再取,而是直接为没有取到数据的变量赋值为0.
 * - 对于%c,空格、回车、tab键都会被当成数据输出给scanf取用。
 * - %s,也是只会取出数据。不会取空格、回车、tab键
 */


/*
 * 运行程序运行,我们输入:123456回车,却直接弹出了请确认密码(Y/N):确认失败。
 * 原因:我们输入的数据存储到缓冲区中是:123456\n  (\n是我们的回车,也会存储到缓存区中)
 * - scnaf函数执行,用%s读取缓存区,只会取数据123456,把\n剩在了缓存区中。
 * - 接下来调用getchar()获取我们键盘输入时,因为缓存区中还有数据,会先将其中的数据读取完之后才会等待用户继续输入。
 *   这里的\n被getchar()获取走了,赋给了ch变量,而'\n'!='Y',所以没有等待我们输入,直接输出了“确认失败”
 */

//#include <stdio.h>
//int main()
//{
//    //定义字符数组,长度为20   初始化其中所有元素为0
//    char password[20] = {0};
//    printf("请输入密码:");
//    //%s表示输入一个字符串
//    scanf("%s",password);
//    printf("请确认密码(Y/N):");
//    int ch = getchar();
//    if(ch == 'Y')
//    {
//        printf("确认成功\n");
//    }
//    else
//    {
//        printf("确认失败\n");
//    }
//    return 0;
//}

/*
 * - 解决方法:
 *   在int ch = getchar();语句前加上一个getchar();,用来取走缓冲区中的'\n'
 *   然后等到执行int ch = getchar();时,就是等待我们输入Y/N了。
 * - 注意:
 *   我们使用getchar()取走了缓存区中的‘\n’。但是int ch = getchar();语句执行,也需要回车才会进去取这个字符
 *   这时需要注意的是:与scanf函数取数据一样,getchar()函数取数据之后,也会把其产生的\n遗留在缓冲区中
 */

//#include <stdio.h>
//int main()
//{
//    char password[20] = {0};
//    printf("请输入密码:");
//    scanf("%s",password);
//    printf("请确认密码(Y/N):");
//    //在%c取字符之前,使用getchar()吸收遗留的\n
//    getchar();
//    int ch = getchar();
//    if(ch == 'Y')
//    {
//        printf("确认成功\n");
//    }
//    else
//    {
//        printf("确认失败\n");
//    }
//    return 0;
//}

/*
 * !思考:如果有多个scanf给int型变量赋值,那么每个scanf都会遗留一个回车,那么这时候是不是有几个scanf就需要几个getchar()呢?
 * - 不需要,仍然只需要一个getchar()就可以。
 *   当scanf用%d或%s取缓冲区数据的时候,如果遇到空格、回车、tab键,会跳过去。这里的跳过是指:释放掉了。
 *   也就是说,scanf用%d或%s取缓冲区数据,碰到空格、回车、tab键,就会释放掉,不会再存在于缓存区中。
 * - 所以加入有三个scanf给int型变量赋值,那么第一个把\n留在了缓冲区,第二个scanf取值时会释放掉第一个scanf遗留的回车。
 *   第三个scanf取值时,会释放掉第二个scanf遗留的回车。而第三个遗留的回车,我们用一个getchar()就可以释放掉。
 *   也就是说:混充去中永远不可能遗留多个回车。
 *
 * 结论:
 * 当我们输入一串数据:有字符、数字、空格
 * - scanf用%d获取其中的数据,只会取其中的数字。
 *   如果遇到其中的空格、回车、tab键,则会将其当成分隔符。
 *   如果遇到其中的字符,则会直接退出,不再取数据。
 * - scanf用%c取数据
 *   任何数据都被当成一个字符。
 * - 所以如果要从输入流中取一个字符,但是在之前我们使用过scanf,那么此时就必须先使用getchar()吸收回车。
 *   否则取到的就不是我们需要的字符了,而是scanf遗留在输入流中的回车。
 * - 如果你要从输入流中取的不是字符,就不需要getchar()吸收回车了。
 * - 但是在实际编程中,程序往往很长。我们很难预测到下一次到缓存区中取数据是%d、%c、gets()或是fgets()
 *   所以为了避免忘记吸收回车,习惯上scanf后面都加上getchar()。
 */

/*  - 运行程序,输入数据:123a456
 *    scanf以%d取数据,取到a发现如果输入的是字符,则会直接退出,不取数据。
 *    a被赋值123,而b、c没有取下值,则会赋值为0
 *  - 剩下的一串数据(a456以及scanf遗留的'\n')仍然会存储在缓存区中。
 *    这时剩下的数据以5个getchar()进行回收
 */

//#include <stdio.h>
//int main()
//{
//    int a = 0;
//    int b = 0;
//    int c = 0;
//    scanf("%d %d %d",&a,&b,&c);
//    printf("%d %d %d\n",a,b,c);
//    int ch = 0;
//    while ((ch=getchar()) != EOF)
//    {
//        printf("%d——",ch);
//        putchar(ch);
//        printf("----\t");
//    }
//    return 0;
//}

/*
 * fflush(stdin)方法
 * - 前面介绍了使用 getchar() 吸收回车的方法,而fflush()方法,作用就是直接将输入缓冲区全部清空。
 * - 清空缓冲区只需加一句 fflush(stdin) 即可。fflush 是包含在文件 stdio.h 中的函数。stdin 是“标准输入”的意思。
 *   std 即 standard(标准),in 即 input(输入),合起来就是标准输入。fflush(stdin) 的功能是:清空输入缓冲区。
 *
 *   fflush 一般用于清除用户前面遗留的垃圾数据,提高代码的健壮性。因为如果是自己编程的话,一般都会按要求输入。
 *   但对于用户而言,难免会有一些误操作,多输入了一些其他没有用的字符,如果程序中不对此进行处理的话可能会导致程序瘫痪。
 *   所以编程时一定要考虑到各种情况,提高代码的健壮性和容错性。使用 fflush() 就可以将用户输入的垃圾数据全部清除。
 */
//#include <stdio.h>
//int main()
//{
//    int a = 0;
//    printf("请输入一串字符和数字(请以数字开头):");
//    scanf("%d",&a);
//    printf("您输入的字符中,已存储的有效数字为:%d\n",a);
//    fflush(stdin);
//    printf("已经调用ffluh方法清空缓存区,请输入一个字符:");
//    int ch = getchar();
//    printf("%c",ch);
//
//    return 0;
//}

/*
 * - getchar()的高级用法 ———— while (getchar() != '\n');
 *   它可以完全代替 fflush(stdion) 来清空缓冲区。不管用户输入多少个没用的字符,他最后都得按回车,而且只能按一次。
 *   只要他按了回车那么回车之前的字符就都会被 getchar() 取出来。只要 getchar() 取出来的不是回车('\n') 那么就会一直取,直到将用户输入的垃圾字符全部取完为止。
 *
 * - while (getchar() != '\n');语句执行
 *   循环执行getchar语句,会依次读取缓冲区的字符,这样所有缓冲区的字符都读入程序并依次被getchar()给吸收了,条件成立,执行空语句; 然后一直循环。
 *   直到读取了缓冲区中的'\n'后,条件不成立,循环终止。虽然条件不成立,但是getchar()函数已经调用了,已经取走了其中'\n',这样就达到了清空缓冲区的效果。
 *

 */

//#include <stdio.h>
//int main()
//{
//    int a = 0;
//    printf("请输入一串字符和数字(请以数字开头):");
//    scanf("%d",&a);
//    printf("您输入的字符中,已存储的有效数字为:%d\n",a);
//    while (getchar() != '\n');
//    printf("高级运用getchar() 清空缓存区,请输入一个字符:");
//    int ch = getchar();
//    printf("%c",ch);
//    return 0;
//}

练习

  • 文件名及路径说法错误的是:

    文件名中有一些禁止使用的字符。

    文件名中一定包含后缀名。 (错误)——文件名可以不加后缀名

    文件的后缀名决定了文件的默认打开方式。

    文件路径指的是从盘符到该文件所经历的路径中各符号名的集合。

  • 下列说法不正确的是

    scanf和printf是针对标准输入、输出流的格式化输入、输出语句。

    fscanf和fprintf是针对所有输入、输出流的格式化输入、输出语句。

    sscanf是从字符串读取格式化的数据。

    sprintf是把格式化的数据写入到输出流当中。(错误)——是把一个格式化的数据,转换为字符串。

  • feof()函数描述不正确的是

    feof()函数是用来判断文件是否读取结束的。 (错误)—— 该函数用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件结尾结束。他不是判断结束的条件,而是在读取结束之后,判断是什么原因结束的

    feof函数是在文件读取结束的时候,检测是否是因为遇到了文件结束符标志EOF而读取结束的。

    读取文本判断是否结束时,fgetc()看返回值是否为EOF,fgets()看返回值是否为NULL

    二进制文本文件判断读取结束,看实际读取个数是否小于要求读取的个数。
    输入。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值