C语言

14. C语言进阶--文件操作

Posted on 2022-02-11,20 min read

6. 文件操作

6.1 为什么使用文件

在程序中,整形、浮点型、数组、结构体等,随着程序的终止,数据的生命也随之结束。那如何将数据保存呢?C语言中,通过printf函数可以将数据输出到控制台。同样,C语言也提供了一些列函数可以将数据输出到文件中,即将数据存放在硬盘上,做到数据的持久化。此外,还可以将数据存储在数据库中。

6.2 什么是文件

文件:一组相关数据的有序集合。在程序设计中,按照文件功能可将文件划分为程序文件和数据文件。

6.2.1 程序文件

源程序文件(后缀名.c),目标文件(windows环境后缀名为.obj),可执行程序(windows环境后缀名为.exe)。

6.2.2 数据文件

程序运行时,读取或写入数据的文件。

6.3 文件的打开和关闭

6.3.1 文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名,文件状态、位置等)。这些信息保存在一个结构体变量中。该结构体类型由系统声明,名为**FILE**。通过调用fopen函数,系统创建FILE结构体变量,且可以返回一个FILE指针。

// 创建一个 FILE* 指针变量
FILE* pf;

// pf是要给FILE结构体指针,指向一个FILE结构体变量,该变量中包含文件的相关信息
// 所以通过pf可以找到这些信息,并通过这些信息操作该文件

6.3.2 文件打开和关闭

操作文件的流程为:打开文件 → 操作文件 → 关闭文件。ANSIC规定使用fopen函数打开文件,fclose关闭文件。

// fopen函数原型
FILE * fopen ( const char * filename, const char * mode );
// 参数:filename 文件名 (相对路径或绝对路径)
//      mode 打开模式
// 返回值:FILE指针 用来操作打开的文件

// fclose函数原型
int fclose ( FILE * stream );
// 参数:FILE指针
// 返回值:0 -- 成功关闭文件
//       EOF -- 关闭文件失败

mode(文件的打开模式)

模式 描述
r 打开一个已有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。
w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模

补充:如果处理二进制文件,应使用 "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b", 替代表格中的打开模式。

6.4 文件的顺序读写

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

6.4.1 流

是与磁盘或其它外围设备关联的数据的源或目的地,是一个抽象的概念。I/O设备是流的源头和目的地,将数据的输入和输出看作是数据的流入和流出。在Unix/Linux中,文本流和二进制流是相同的,但在Windows中,稍有差异。

  • 文本流:由文本行组成的序列,每一行包含0个或多个字符,并以\n结尾。在某些环境中, 可能需要将文本流转换为其它表示形式(例如把\n映射成回车符\r和换行符\r),或从其它表示形式转换为文本流。
  • 二进制流:由未经处理的字节构成的序列,这些字节记录着内部数据, 并具有下列性质:如果在同一系统中写入二进制流,然后再读取该二进制流,则读出和写入 的内容完全相同。

在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备都被映射成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。

文件 硬件设备
stdin 标准输入设备(键盘);scanf()getchar() 等函数从 stdin 获取输入。
stdout 标准输出设备(显示器);printf()putchar() 等函数向 stdio 输出数据。
stderr 标准错误输出设备(显示器);perror() 等函数向 stderr 输出数据。
stdprn 标准打印设备(打印机)。
stdaux 标准辅助输入输出设备(异步串行口)。

程序开始执行时,默认会打开 stdinstdoutstderr三个文件,所以我们使用 scanf()printf() 等函数时就不需要再使用 fopen() 显式打开这些文件。

打开一个流,将把该流与一个文件或设备连接起来,关闭流将断开这种连接,打开一个文件将返回一个指向FILE结构体类型的指针,该指针记录了控制该流的所有必要信息。

6.4.2 EOF

EOF是C语言中定义在stdio.h头文件中的常量。是文本文件结束的标志。

#define EOF (-1)

6.4.3 fgetc和fputc

  • 函数原型
// 函数原型
// fgetc
int fgetc ( FILE * stream );
// 功能:从流中获取字符
// 参数:输入流的FILE*指针
// 返回值:获取成功 -- 返回读取到的字符,并转换为整形,即字符对应的ASCII码值
//        获取失败 -- 返回EOF,即-1 并设置错误指示ferror


// fputc
int fputc ( int character, FILE * stream );
// 功能:向流中写入一个字符
// 参数:character -- 需要写入字符的ASCII码值
//      stream    -- 输出流
// 返回值:写入成功 -- 返回写入的字符,并转换为整形,即字符对应的ASCII码值
//        写入失败 -- 返回EOF,即-1 并设置错误指示ferror
  • 实例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
  FILE* pf = fopen("test.txt", "w");
  if (pf == NULL)
  {
    perror("fopen");
    return 1;
  }
  for (int i = '0'; i < '9'; i++)
  {
    fputc(i, pf);
  }
  fclose(pf);
  pf = NULL;
  return 0;
}


6.4.4 fgets和fputs

  • 函数原型
// 函数原型
// fgets
char * fgets ( char * str, int num, FILE * stream );
// 功能:从流中获取字符串
// 参数:str -- 将获取的字符拷贝到字符数组str中
//      num -- 拷贝到str中的最大字符数(包括 null 终止字符)也就是说最多从流中获取num-1个字符
//      stream -- 输出流
// 返回值:获取成功 -- 返回str
//        获取失败 -- 返回EOF 


// fputs
int fputs ( const char * str, FILE * stream );
// 功能:向流中写入字符串
// 参数: str -- C字符串
//       FILE -- 输入流
// 返回值:写入成功 -- 返回非负数
//        写入失败 -- 返回EOF
  • 实例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>


int main()
{
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    perror("fopen");
    return 1;
  }
  char arr[8] = { 0 };
  fgets(arr, 5, pf);
  printf("%s\n", arr);
  fclose(pf);
  pf = NULL;
  return 0;
}


6.4.5 fscanf和fprintf

  • 函数原型
// 函数原型
// fscanf
int fscanf ( FILE * stream, const char * format, ... );
// 功能:从流中获取格式化数据,遇到第一个空格和换行符时,会停止读取
// 使用方法类似于scanf 只不过第一个参数是输入流
// 返回值是获取的字符个数

// fprintf
int fprintf ( FILE * stream, const char * format, ... );
// 功能:向流中写入格式化数据
// 使用方法类似于printf 只不过第一个参数是输出流
// 返回值是获取的字符个数
  • 实例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

struct S
{
  char c;
  int i;
  float f;
} s = {'a', 0, 0.0f};

int main()
{
  FILE* pf = fopen("test.txt", "a");
  if (pf == NULL)
  {
    perror("fopen");
    return 1;
  }
  fprintf(pf, "%c %d %f", s.c, s.i, s.f);
  fclose(pf);
  pf = NULL;
  return 0;
}


6.4.6 fread和fwrite

  • 函数原型
// 函数原型
// fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
// 功能:从流中读取数据块
// 参数:ptr -- 至少是size×count字节的内存块的指针 并转换为空指针类型
//      size -- 读取每个元素的大小 单位 byte
//      count -- 读取元素个数 
//      stream -- 输入流
// 返回值:成功读取元素个数


// fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
// 功能:从流中读取数据块
// 参数:ptr -- 被写入的元素数组的指针 并转换为空指针类型
//      size -- 读取每个元素的大小 单位 byte
//      count -- 读取元素个数 
//      stream -- 输出流
// 返回值:成功读取元素个数

  • 实例1

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    
    struct S
    {
      char c;
      int i;
      float f;
    } s = { 'a', 1, 3.14 };
    
    int main()
    {
      FILE* pf = fopen("test.txt", "w+");
      if (pf == NULL)
      {
        perror("fopen");
        return 1;
      }
      fwrite(&s, sizeof(struct S), 1, pf);
      fclose(pf);
      pf = NULL;
      return 0;
    }
    
    


  • 实例2

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    
    struct S
    {
      char c;
      int i;
      float f;
    } s;
    
    int main()
    {
      FILE* pf = fopen("test.txt", "r");
      if (pf == NULL)
      {
        perror("fopen");
        return 1;
      }
      fread(&s, sizeof(struct S), 1, pf);
      printf("%c %d %f\n", s.c, s.i, s.f);
      fclose(pf);
      pf = NULL;
      return 0;
    }
    


6.5.7 函数对比

  • scanf — 针对标准输入流的格式化输入(通常空白字符截至) — stdin
  • fscanf — 针对所有输入流的格式化输入
  • sscanf — 从字符串中读取格式化数据
  • printf — 针对标准输出流的格式化输出 — stdout
  • fprintf — 针对所有输出流的格式化输出
  • sprintf — 将格式化数据转换为从字符串

6.5.8 scanf

// 函数原型 int scanf ( const char * format, ... );
// 参数:
//   format -- C字符串 包含一个或多个空格字符、非空格字符 和 format 说明符
//     format 说明符格式 -- [=%[*][width][modifiers]type=]
//                        * -- 可选 表示数据从流 stream 中读取 但被忽视 即它不存储在对应的参数中
//                    width -- 当前读取操作中读取的最大字符数
//                modifiers -- 长度格式符为l和h l->长整型数据(如%ld)和双精度浮点数(如%lf; ;h->短整型数据
//                     type -- 一个字符 指定要被读取的数据类型以及数据读取方式
// 返回值:
//   如果成功 返回成功匹配和赋值的个数
//   如果到达文件末尾或发生读错误 返回 EOF
// 另外:
// scanf读出的数据使用变量地址接收 而不是变量名
  • scanf类型说明符
类型 合格的输入 参数的类型
%a、%A 读入一个浮点值(仅 C99 有效) float *
%c 单个字符;如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置,在末尾不会追加空字符 char *
%d 十进制整数 int *
%e、%E、%f、%F、%g、%G 浮点数 float *
%i 十进制、八进制、十六进制整数 int *
%o 八进制整数 int *
%s 字符串 直到空格字符 空格字符:是空白、换行和制表符 char *
%u 无符号的十进制整数。 unsigned int *
%x、%X 十六进制整数 int *
%p 指针
%[] 扫描字符集合
%% 读 % 符号

scanf用来从标准输入读取字符串,通常遇到空白字符结束。可以在format参数中使用%[ ] ,作用是读取一个字符集合,[ ]中输入字符集合,支持正则表达式。比如:"%[a-z] 读取 az 的字符集合;"%[^\n]" 读取非换行符的字符集合。

// 通常 使用scanf只能读一个单词 如下
int main()
{
  char s[100];
  scanf("%s", s);
  return 0;
}
// 输入:hello world
// 输出:hello


// 使用scanf读取一行字符串 代码如下

int main()
{
  char s[100];
  scanf("%[^\n]", s);
  return 0;
}
// 输入:hello world
// 输出:hello world

fscanf 同理。此外,可以还可以使用 gets 从标准输入获取一行字符串。

6.5 文件的随机读写

6.5.1 fseek

设置流stream的文件位置为从origin 开始的偏移offset

  • 函数原型

    int fseek ( FILE * stream, long int offset, int origin );
    // 参数:stream -- 流
    //      offset -- 相对 origin 的偏移量 
    //                正数->向右偏移 
    //                负数->向左偏移
    //      origin -- 添加偏移 offset 的位置 可取的值如下
    //                SEEK_SET 文件开头
    //                SEEK_CUR 当前位置
    //                SEEK_END 文件末尾
    // 返回值:成功 -- 返回0
    //        失败 -- 返回非0
    
  • 实例

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    int main()
    {
      FILE* pf = fopen("test.txt", "w");
      if (pf == NULL)
      {
        perror("fopen");
        return 1;
      }
      fputs("I like C.\n", pf);
      fseek(pf, -4, SEEK_END);
      fputs("golang.\n", pf);
      fclose(pf);
      pf = NULL;
      return 0;
    }
    
    


6.5.2 ftell

获取流中当前文件位置

  • 函数原型

    long int ftell ( FILE * stream );
    // 参数:stream -- 流
    // 返回值:成功 -- 当前位置值
    //        失败 -- -1L 全局变量 errno 被设置为一个正值 
    
  • 实例

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    int main()
    {
      FILE* pf = fopen("test.txt", "w");
      if (pf == NULL)
      {
        perror("fopen");
        return 1;
      }
      fputs("I like C.\n", pf);
      printf("%d\n", ftell(pf));
      fseek(pf, -4, SEEK_END);
      printf("%d\n", ftell(pf));
      fputs("golang.\n", pf);
      printf("%d\n", ftell(pf));
      fclose(pf);
      pf = NULL;
      return 0;
    }
    


6.5.3 rewind

设置流的文件位置为流的开头

  • 函数原型

    void rewind ( FILE * stream );
    
    
  • 实例

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    
    
    int main()
    {
      FILE* pf = fopen("test.txt", "w");
      if (pf == NULL)
      {
        perror("fopen");
        return 1;
      }
      fputs("I like C.\n", pf);
      rewind(pf);
      fputs("I like golang.\n", pf);
      fclose(pf);
      pf = NULL;
      return 0;
    }
    
    


6.6 文本文件和二进制文件

广义的二进制文件即指文件,由文件在外部设备的存放形式为二进制而得名。狭义上,文件分为二进制文件和文本文件。

6.6.1 文本文件

文本文件是以ASCII码方式(也称文本方式)存储的文件,更确切地说,英文、数字等字符存储的是ASCII码,而汉字存储的是机内码。文本文件中除了存储文件有效字符信息(包括能用ASCII码字符表示的回车、换行等信息)外,不能存储其他任何信息。可以用任何文字处理程序阅读的简单文本文件。文本文件是指一种容器,而纯文本是指一种内容。文本文件可以包含纯文本。文本文件存在计算机系统中,通常在文本文件最后一行放置文件结束标志。文本文件的编码基于字符定长,译码相对要容易一些。

6.6.2 二进制文件

二进制文件指包含在 ASCII及扩展 ASCII 字符中编写的数据或程序指令的文件是除文本文件以外的文件。二进制文件编码是变长的,灵活利用率要高,而译码要难一些,不同的二进制文件译码方式是不同的。这些文件含有特殊的格式及计算机代码。

6.6.3 更多

https://www.jianshu.com/p/af0b4f8b030e

6.6.4 例如

例如整形1的存储,以二进制形式存储时,只要将1转换为二进制00000000000000000000000000000001,共4字节。以ASCII码形式存储,即文本形式存储,先将整形1当作字符'1'转换为ASCII码值49,再将49转换为二进制00110001,公共1字节。虽然1以二进制存储占用的字节数大于以ASCII存储占用的字节数,但是当数值大于一定值后,使用二进制存储是明显节省空间的。另外,ASCII形式,需要的转换的次数多于二进制形式,所以效率不如二进制形式存储...

6.7 文件读取结束的判定

6.7.1 feof

测试给定流 stream 的文件结束标识符。在文件读取过程中,不能使用feof函数的返回值来判断文件是否结束,因为fgetc(或者fgets)函数返回 EOF 并不一定就表示文件结束,读取文件出错时也会返回 EOF。因此,feof应用于当文件读取结束后,判断是读取失败结束,还是遇到文件尾结束。

  • 函数原型

    int feof(FILE *stream)
    // 设置了与流关联的文件结束标识符时 返回一个非零值
    

    此外,当文件内部的位置指针指向文件结束时,并不会立即设置FILE结构中的文件结束标识符,只有再执行一次读文件操作,才会设置结束标识符,然后调用feof()函数才会返回为真。

6.7.2 ferror

测试给定流 stream 的错误标识符。

  • 函数原型
    int ferror(FILE *stream)
    // 设置了与流关联的错误标识符,该函数返回一个非零值
    

6.7.4 fclear

清除给定流 stream 的文件结束和错误标识符

  • 函数原型
    void clearerr(FILE *stream)
    // 这不会失败 且不会设置外部变量 errno 
    // 但是如果它检测到它的参数不是一个有效的流 则返回 -1
    // 并设置 errno 为 EBADF
    

6.7.3 判断文件读取结束

  • fgetc — 读取结束,返回EOF;正常读取,返回读取到的字符的ASCII码值
  • fgets — 读取结束,返回NULL;正常读取,返回存放字符串内存空间的起始地址
  • fread — 读取结束,读取到的完整元素的个数小于指定元素的个数,则是最后一次读取;正常读取,返回实际读取到的完整元素的个数

6.8 文件缓冲区

C语言打开文件时,先将文件内容载入缓冲区(缓存),并返回一个指向FILE结构体的指针,接下来对文件的操作,都映射成对缓冲区的操作,只有当强制刷新缓冲区、关闭文件或程序运行结束时,才将缓冲区中的内容更新到文件。ANSIC标准采用“缓冲文件系统”处理数据文件,缓冲文件系统指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定。缓冲区的存在,一定程度上减少程序与磁盘进行的I/O操作次数,提高程序运行效率。

6.8.1 实例

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

int main()
{
  FILE* pf = fopen("test.txt", "w");
  fputs("hello", pf); // 写入数据

  printf("睡眠中...\n");
  Sleep(10000); // 睡眠10s 查看test.txt文件内容 并没有数据

  printf("刷新缓冲区\n");
  fflush(pf); // 刷新缓冲区,将缓冲区中的代码写入文件(磁盘)

  printf("睡眠中...\n");
  Sleep(10000); // 睡眠10s 查看test.txt文件内容 已经有数据

  fclose(pf);
  // 注:关闭文件 退出程序 也会自动刷新缓冲区
  pf = NULL;
  return 0;
}

6.8.2 fflush

刷新缓冲区。

  • 函数原型
    int fflush(FILE *stream)
    
    // 参数:stream -- FILE对象指针 FILE对象指定一个缓冲流
    // 返回值:如果成功该函数返回零值
    //        如果发生错误 返回 EOF 且设置错误标识符(即 feof)
    
    

下一篇: 13. C语言进阶--动态内存管理→