自学内容网 自学内容网

【C语言】文件操作

一、为什么要使用文件

        程序运行时使用的数据是存储在内存中的,程序运行结束后内存就回收了,数据也不存在了。有时,我们想让数据长久保存,就需要使用文件,存储在磁盘上。

二、什么是文件

        文件就是存放在外存上的文件。按功能分类,又分为两类:程序文件和数据文件。

(1)程序文件

        在Windows系统上,程序文件包括:源程序文件(后缀.c),目标文件(后缀.obj),可执行文件(后缀.exe)。

(2)数据文件

        文件内容是程序执行时使用的数据。在之前我们都是以终端作为输入输出的对象,本文讲述以数据文件作为输入输出的对象的方法。

(3)文件名

        文件名就是文件的唯一标识。文件名结构:文件路径+文件名主干+文件后缀。

        例如:D:code\test.txt

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

        数据文件按数据的组织形式,又分为文本文件和二进制文件。

        程序运行时,将数据存在内存中(二进制形式),当需要将内存中的数据输出到外存上的数据文件中时,如果直接放在文件里,那么这就是二进制文件;如果先转换成ASCII码的形式,再放在文件里,那么这就是文本文件

        例如:程序运行时,使用了一个 int 类型的数据10000,在内存中以二进制形式存储。现将它输出到文件中,如果直接以二进制形式输出,则文件占4字节;如果先转换为ASCII码形式再输出,则文件占5字节。如下图所示:

        运行以下代码,将 int 类型的数据1000 以二进制形式输出到 test.txt 文件,用二进制文件编辑器查看(内存中以小端方式存储):

        补充:VS上,二进制文件打开方式如下:

四、文件的打开和关闭

(1)流

        我们向不同的设备(屏幕、键盘、磁盘、U盘等)输入输出数据,需要进行不同的操作。为了方便程序员对各种设备进行操作,C语言抽象出了流的概念。我们只需要关心打开流,内存向流中读或写数据,至于各种设备与流之间是如何操作的,就是C语言底层解决的事情了。

(2)标准流

        我们在使用scanf、printf、perror等函数向显示器/键盘输出/输入数据时,没有打开流,也没有进行其它的底层操作,照样成功地实现了内存与外部设备的数据读写,这是为什么?

        这是因为,在程序启动的时候,C语言就会自动打开3个标准流

  • stdin: 标准输入流,大多环境中从键盘输入,scanf 就是从 stdin 中读数据。
  • stdout: 标准输出流,大多环境是输出到显示器,printf 就是向 stdout 中写数据。
  • stderr: 标准错误流,大多环境是输出到显示器,perror 就是向 stderr 中写数据。

        标准I/O流的类型就是 FILE*,称为文件指针,它可以维护流的各种操作。因此,打开文件实际上就是打开流

(3)文件指针

        进行文件打开操作之后,系统会自动在内存中开辟一个相应的文件信息区,并存放文件的相关信息(包括文件名、文件状态、文件大小等)。这些信息存放在一个结构体变量里,这个结构体类型叫做 FILE,由编译器定义。不同的C编译器,定义的 FILE 结构体类型有所不同,但大同小异。如下,VS2013 在头文件 <stdio.h> 定义的 FILE:

struct _iobuf {
    char* _ptr;
    int _cnt;
    char* _base;
    int _flag;
    int _file;
    int _charbuf;
    int _bufsiz;
    char* _tmpfname;
};
typedef struct _iobuf FILE;

        我们通常是创建一个 FILE 指针,指向 FILE 结构体,从而间接访问文件信息区的文件信息,如下图所示:

(4)文件的打开和关闭

        使用文件前,要打开文件;使用完后,要关闭文件。打开文件时,会返回一个 FILE 的指针,就建立了 FILE* 与文件的联系。

        在 ANSI C 中规定 fopen 函数打开文件,fclose 函数关闭文件,函数原型如下:

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

        对于 fopen:

  • filename 指向文件名字符串,mode 指向模式的字符串。
  • 打开成功,返回文件信息区起始地址;打开失败,返回NULL。
  • mode,所有打开模式如下:
文件打开模式含义如果指定文件不存在
“r”(只读)输入数据,打开一个文本文件出错
“w”(只写)输出数据,打开⼀个文本文件建立一个新文件
“a”(追加)向文本文件尾添加数据建立一个新文件
“rb”(只读)输入数据,打开一个二进制文件出错
“wb”(只写)输出数据,打开一个二进制文件建立一个新文件
“ab”(追加)向二进制文件尾添加数据建立一个新文件
“r+”(读写)读和写,打开一个文本文件出错
“w+”(读写)读和写,打开一个文本文件建立一个新文件
“a+”(读写)向文本文件尾进行读写建立一个新文件
“rb+”(读写)读和写,打开一个二进制文件出错
“wb+”(读写)读和写,打开一个二进制文件建立一个新文件
“ab+”(读写)向二进制文件尾进行读写建立一个新文件

        注意:

  • “r”、“rb”、“r+”、“rb+”:如果指定文件不存在,会报错。
  • “w”、“wb”、“w+”、“wb+”:如果指定文件不存在,会建立一个新的文件;如果指定文件存在,会覆盖原内容。
  • “a”、“ab”、“a+”、“ab+”:如果指定文件不存在,会建立一个新的文件;如果指定文件存在,会在原内容末尾追加内容。

        对于 fclose,文件关闭失败,返回 -1;文件关闭成功,返回 0。

        示例代码及运行结果如下,文件关闭前,pf 存储的地址:

        文件关闭后,pf存储的地址:

        所以 fclose 后,系统只会收回内存空间的使用权,但 pf 还是指向的原来的空间,避免 pf 成为野指针,在文件关闭后,要将 pf 置 NULL

        因文件原本不存在,"w" 模式在指定路径下创建了新文件:

五、文件的顺序读写

(1)文件的顺序读写函数

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

        所有输入/输出流表示,可以是标准流(stdin、stdout、stderr),也可以是其它的流,比如文件流。        

① fgetc

        函数原型:

int fgetc ( FILE * stream );

        参数:流的指针。

        函数返回值:

  • 读取成功:返回读取到的一个字符。
  • 读取到文件末尾而失败:返回EOF(-1),并设置 end-of-file 标记
  • 读取错误而失败:返回EOF(-1),并设置 error 标记。

        示例代码,读完整个文件:

② fputc

        函数原型:

int fputc ( int character, FILE * stream );

        参数:要写入的字符;流的指针。

        返回值:

  • 写入成功:返回写入的字符。
  • 写入失败:返回 EOF(-1),并设置 error 标记。

        示例代码,向文件写入字符 a~z:

③ fgets

        函数原型:

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

        参数:读取的字符串存入 str 指向的内存空间;读取的最大字符数量(包括终止符);流的指针。

        返回值:

  • 读取成功:返回 str。
  • 读取到文件尾,但读取到了字符:返回 str,并设置 end-of-file 标记
  • 读取到文件尾,并没有读取到任何字符:返回 null ,并设置 error 标记。
  • 发生错误:返回 null ,并设置 error 标记。

        示例代码,读完整个文件:

④ fputs

        函数原型:

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

        参数:str 指向要写入的字符串;流的指针。

        返回值:

  • 写入成功:返回一个非负值。
  • 写入失败:返回 EOF(-1),并并设置 error 标记。

        示例代码:

        运行结果:

⑤ fprintf

        函数原型:

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

        与 printf 函数对比:

int printf ( const char * format, ... );

         fprintf 的参数仅仅是多了一个 stream。

        返回值:

  • 成功:返回写入的字符个数。
  • 遇到错误:返回一个负数,并设置 error 标记。
  • 遇到编码错误(多字节字符):返回一个负数,并设置 errno 标记为 EILSEQ。

        示例代码:

        运行结果:

⑥ fscanf

        函数原型:

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

        相比 scanf 函数,参数仅仅多了一个 stream。

        返回值:与 scanf 函数相似,参考【C语言】数据类型、变量、操作符、printf、scanf详解_c语言布尔型scanf-CSDN博客

        示例代码及运行结果:

⑦ fwrite

        函数原型:

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

        参数:ptr 指向被写的数组;size 是每个元素的大小,单位字节;count 是元素个数;stream 是流。

        返回值:成功写入的元素个数。

        示例代码:

        运行结果(二进制编辑器打开):

⑧ fread

        函数原型:

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

        参数:ptr 指向要读取到的空间;元素大小;元素个数;流。

        返回值:成功读取到的元素个数。

        示例代码及运行结果:

        如果 fread 的返回值小于参数 count,那就意味着这是最后一次读了。

(2)函数的对比

① printf 和 scanf

printf -- 针对标准输出流(stdout)的,将数据以格式化的形式,输出到屏幕上。

scanf -- 针对标准输入流(stdin)的,从键盘上输入格式化的数据。

② fprintf 和 fscanf

fprintf -- 针对所有输出流的,格式化的输出函数。

fscanf -- 针对虽有输入流的,格式化输入函数。

③ sprintf 和 sscanf

sprintf -- 将格式化的数据转换成字符串。

sscanf -- 从字符串中提取出格式化的数据。

        函数原型:

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

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

       与 printf 和 scanf 相比,多了一个指向字符串的指针参数。

        示例代码及运行结果:

        应用场景:序列化(将数据结构转化为字节流或字符串等)和反序列化(序列化的逆操作)。

        反序列化:前端网页上的数据以字符串的形式传给后端,后端转换为结构体,就用到 sscanf。序列化:后端将结构体转为字符串展示到前端,用 sprintf。

六、文件的随机读写

(1)fseek

        函数原型:

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

        功能:根据文件内容的光标位置和偏移量来定位文件的位置。

        参数:流;offset 是偏移量,根据 origin 的光标位置计算;origin  是起始位置,有3个值:

  • SEEK_SET:文件的起始位置。
  • SEEK_CUR:光标的当前位置。
  • SEEK_END:文件的末尾位置。

        假如文件内容如下:

        读取 a 后,不想读取 b,而是读取 d。首先读取一个 a ,光标后移 1 位。如果 origin 是 SEEK_CUR ,光标在当前位置 a 后不动,则偏移量是 2;如果 origin 是 SEEK_SET,光标移到开头,则偏移量是 3;如果 origin 是 SEEK_END,光标移到 f 后,则偏移量是 -3。

        代码及运行结果如下:

(2)ftell

        函数原型:

long int ftell ( FILE * stream );

        功能:返回文件当前光标位置相对于文件起始位置的偏移量。

        示例代码:

        读取 d 后,光标移动到 d 后,相对于文件起始位置,偏移了 4 。

(3)rewind

        函数原型:

void rewind ( FILE * stream );

        功能:让光标位置回到文件的起始位置。

        示例代码及运行结果:

七、文件读取结束的判定

(1)feof 的错误使用

        feof 并不是用来判断文件是否读取结束的

        文件读取结束有两种原因:

  • 遇到文件末尾结束(正常结束)。
  • 文件读取失败结束。

         feof 和 ferror 是在已经知道文件读取结束的前提下,文件读取结束的原因:

  • feof:是否是遇到文件末尾而正常结束。
  • ferror:是否是文件读取发生错误而结束。

        feof 和 ferror 的原理:在文件读写的过程中,遇到文件末尾/错误,会设置文件末尾标记/错误标记。feof 就是检测文件末尾标记,而 ferror 就是检测错误标记。

(2)判断文件读取结束的方法

函数文件未读取结束回值文件读取结束返回值判断文件读取结束方法
fgetc读取到的字符ASCII码EOF(-1)判断返回值是否为EOF
fgets存储字符串的数组地址NULL判断返回值是否为NULL
fscanf读取到的参数个数0或EOF(-1)判断返回值是否小于要读的参数个数
fread读取到的元素个数0判断返回值是否小于要读取的个数

        示例代码 1 及运行结果(处理文本文件):

        示例代码 2 及运行结果(处理二进制文件):

八、文件缓冲区

        ANSIC 标准采用 “缓冲文件系统” 处理数据文件,即系统自动地在内存中,为程序中每一个正在使用的文件,除了开辟文件信息区外,还开辟一块“文件缓冲区”。内存到磁盘的数据输出/输入,都先要送到“缓存区”,缓冲区的数据放满了,再送到目的地。如下图所示:

        为什么要有缓冲区?

        如果没有缓冲区,并且需要读/写很多数据,但是每读/写一点数据,就需要调用操作系统把数据从硬盘读出或者写入硬盘,这样频繁打断操作系统,就无法全局地为计算机上的其它程序服务,从而导致计算机的效率变慢

        以下 3 种情况满足一条,就会从缓冲区读出/写入数据:

  • 缓冲区满。
  • 手动刷新缓冲区。
  • 关闭文件。

        测试代码:

//VS2019 WIN11环境测试
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");
    //注:fclose在关闭文件的时候,也会刷新缓冲区,因此又睡眠了10秒,防止是 fclose 刷新的
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}

        一开始,用 fputs 写数据到文件 test.txt,因为缓冲区没有满,所以此时文件中并没有内容。使用 fflush 手动刷新缓存区,就会把数据从缓冲区取出写入文件中,这时文件有内容了。

        结论:文件操作结束时,关闭文件非常重要,否则数据会丢失。


原文地址:https://blog.csdn.net/2401_86272648/article/details/142651741

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!