【C语言】文件操作

发布于:2024-04-26 ⋅ 阅读:(23) ⋅ 点赞:(0)

建议学完结构体和指针再来看

文件一般分为两种:1.程序文件 2.数据文件

文件名:文件路径+文件名主干+文件后缀
eg:C:\code\test.txt

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE

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

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

文件的打开与关闭

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

实例:

int main(){
    FILE* fp=NULL;
    fp=fopen("test.txt","w");//打开文件
    //test.txt在程序文件夹中可以直接这样写(相对路径),而绝对路径是完整的路径名
    //w是只写,r是只读
    //之间可以对文件进行操作
    fclose(fp);//关闭文件
    return 0;
}

文件的打开方式

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

文件的读写方式的函数

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

以下内容来源于菜鸟教程

fseek

C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

即根据文件指针的位置和偏移量来定位文件指针。

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • offset – 这是相对 whence 的偏移量,以字节为单位。
  • whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量 描述
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾
  • SEEK_SET,即从文件的最前端处开始向后偏移。
  • SEEK_CUR,即当前文件指针的偏移处开始向后偏移。
  • SEEK_END,即从文件的最末尾处开始向前偏移,当然在偏移数一定要为负数才能读取文件中的内容。

返回值

如果成功,则该函数返回零,否则返回非零值。

实例0

#include <stdio.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ( "example.txt" , "wb" );
  fputs ( "This is an apple." , pFile );
  fseek ( pFile , 9 , SEEK_SET );
  fputs ( " sam" , pFile );
  fclose ( pFile );
  return 0; 
}

fwrite

C 库函数 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)ptr 所指向的数组中的数据写入到给定流 stream 中。

参数

  • ptr – 这是指向要被写入的元素数组的指针。
  • size – 这是要被写入的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

返回值

如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。

fread

C 库函数 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) 从给定流 stream 读取数据到 ptr 所指向的数组中。

参数

  • ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
  • size – 这是要读取的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

返回值

成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。

实例1

#include <stdio.h>
#include <string.h>
int main()
{
   FILE *fp;
   char c[] = "This is runoob";
   char buffer[20];
   /* 打开文件用于读写 */
   fp = fopen("file.txt", "w+");
   /* 写入数据到文件 */
   fwrite(c, strlen(c) + 1, 1, fp);
   /* 查找文件的开头 */
   fseek(fp, 0, SEEK_SET);
   /* 读取并显示数据 */
   fread(buffer, strlen(c)+1, 1, fp);
   printf("%s\n", buffer);
   fclose(fp);
   return(0);
}

fputc

C 库函数 int fputc(int char, FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。

参数

  • char – 这是要被写入的字符。该字符以其对应的 int 值进行传递。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流。

返回值

如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。

实例2

#include <stdio.h>
int main ()
{
   FILE *fp;
   int ch;
   fp = fopen("file.txt", "w+");
   for( ch = 33 ; ch <= 100; ch++ )
   {
      fputc(ch, fp);
   }
   fclose(fp);
   return(0);
}

fgetc

C 库函数 int fgetc(FILE *stream) 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流。

返回值

该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。

实例3

#include <stdio.h>

int main ()
{
   FILE *fp;
   int c;
   int n = 0;
  
   fp = fopen("file.txt","r");
   if(fp == NULL) 
   {
      perror("打开文件时发生错误");
      return(-1);
   }
   do
   {
      c = fgetc(fp);
      if( feof(fp) )
      {
          break ;
      }
      printf("%c", c);
   }while(1);

   fclose(fp);
   return(0);
}

fputs

C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符。

参数

  • str – 这是一个数组,包含了要写入的以空字符终止的字符序列。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。

返回值

该函数返回一个非负值,如果发生错误则返回 EOF。

实例4

#include <stdio.h>
int main ()
{
   FILE *fp;
   fp = fopen("file.txt", "w+");
   fputs("这是 C 语言。", fp);
   fputs("这是一种系统程序设计语言。", fp);
   fclose(fp);
   return(0);
}

fgets

C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

参数

  • str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  • n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

返回值

如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。

如果发生错误,返回一个空指针。

实例5

#include <stdio.h>
int main()
{
   FILE *fp;
   char str[60];
   /* 打开用于读取的文件 */
   fp = fopen("file.txt" , "r");
   if(fp == NULL) {
      perror("打开文件时发生错误");
      return(-1);
   }
   if( fgets (str, 60, fp)!=NULL ) {
      /* 向标准输出 stdout 写入内容 */
      puts(str);
   }
   fclose(fp);
   return(0);
}

fprintf

C 库函数 int fprintf(FILE *stream, const char *format, …) 发送格式化输出到流 stream 中。

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了要被写入到流 stream 中的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier,具体讲解如下:
specifier(说明符) 输出
c 字符
d 或 i 有符号十进制整数
e 使用 e 字符的科学科学记数法(尾数和指数)
E 使用 E 字符的科学科学记数法(尾数和指数)
f 十进制浮点数
g 自动选择 %e 或 %f 中合适的表示法
G 自动选择 %E 或 %f 中合适的表示法
o 有符号八进制
s 字符的字符串
u 无符号十进制整数
x 无符号十六进制整数
X 无符号十六进制整数(大写字母)
p 指针地址
n 无输出
% 字符
flags(标识) 描述
- 在给定的字段宽度内左对齐,默认是右对齐(参见 width 子说明符)。
+ 强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
(space) 如果没有写入任何符号,则在该值前面插入一个空格。
# 与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。 与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。 与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0 在指定填充 padding 的数字左边放置零(0),而不是空格(参见 width 子说明符)。
width(宽度) 描述
(number) 要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。
* 宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
.precision(精度) 描述
.number 对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。 对于 e、E 和 f 说明符:要在小数点后输出的小数位数。 对于 g 和 G 说明符:要输出的最大有效位数。 对于 s: 要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。 对于 c 类型:没有任何影响。 当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.* 精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
length(长度) 描述
h 参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。
l 参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。
L 参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

返回值

如果成功,则返回写入的字符总数,否则返回一个负数。

实例6

#include <stdio.h>
#include <stdlib.h>
int main()
{
   FILE * fp;
   fp = fopen ("file.txt", "w+");
   fprintf(fp, "%s %s %s %d", "We", "are", "in", 2024);
   fclose(fp);
   return(0);
}

fscanf

C 库函数 int fscanf(FILE *stream, const char *format, …) 从流 stream 读取格式化输入。

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符format 说明符
    format 说明符形式为 [=%[*][width][modifiers]type=],具体讲解如下:
参数 描述
* 这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。
width 这指定了在当前读取操作中读取的最大字符数。
modifiers 为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g)
type 一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。

fscanf 类型说明符:

类型 合格的输入 参数的类型
c 单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。 char *
d 十进制整数:数字前面的 + 或 - 号是可选的。 int *
e,E,f,g,G 浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4 float *
o 八进制整数。 int *
s 字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。 char *
u 无符号的十进制整数。 unsigned int *
x,X 十六进制整数。 int *
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

返回值

如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

实例7

#include <stdio.h>
#include <stdlib.h>
int main()
{
   char str1[10], str2[10], str3[10];
   int year;
   FILE * fp;

   fp = fopen ("file.txt", "w+");
   fputs("We are in 2100", fp);
   
   rewind(fp);
   fscanf(fp, "%s %s %s %d", str1, str2, str3, &year);
   
   printf("Read String1 |%s|\n", str1 );
   printf("Read String2 |%s|\n", str2 );
   printf("Read String3 |%s|\n", str3 );
   printf("Read Integer |%d|\n", year );

   fclose(fp);
   return(0);
}

feof

C 库函数 int feof(FILE *stream) 测试给定流 stream 的文件结束标识符。

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。

返回值

当设置了与流关联的文件结束标识符时,该函数返回一个非零值,否则返回零。

实例8

#include <stdio.h>
int main ()
{
   FILE *fp;
   int c;
  
   fp = fopen("file.txt","r");
   if(fp == NULL) 
   {
      perror("打开文件时发生错误");
      return(-1);
   }
   while(1)
   {
      c = fgetc(fp);
      if( feof(fp) )
      { 
          break ;
      }
      printf("%c", c);
   }
   fclose(fp);
   return(0);
}

ftell

C 库函数 long int ftell(FILE *stream) 返回给定流 stream 的当前文件位置。

即返回文件指针相对于起始位置的偏移量

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。

返回值

该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。

实例9

#include <stdio.h>
int main ()
{
   FILE *fp;
   int len;
   fp = fopen("file.txt", "r");
   if( fp == NULL ) 
   {
      perror ("打开文件错误");
      return(-1);
   }
   fseek(fp, 0, SEEK_END);

   len = ftell(fp);
   fclose(fp);
   printf("file.txt 的总大小 = %d 字节\n", len);
   return(0);
}

rewind

C 库函数 void rewind(FILE *stream) 设置文件位置为给定流 stream 的文件的开头。

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

实例10

#include <stdio.h>
int main ()
{
  int n;
  FILE * pFile;
  char buffer [27];
  pFile = fopen ("myfile.txt","w+");
  for ( n='A' ; n<='Z' ; n++)
    fputc ( n, pFile);
  rewind (pFile);
  fread (buffer,1,26,pFile);
  fclose (pFile);
  buffer[26]='\0';
  puts (buffer);
  return 0; 
}

文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。(如整数10000,需要以ASCII码输出到磁盘上,则在磁盘中的存储形式就是10000)。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

再用整数10000举例。如果以二进制的形式输出到磁盘上,则在磁盘上是以二进制的形式存储。但是我们到文件底下去看二进制形式的文本时,都是乱码无法看懂(但机器能够看懂)。此时我们再将该文本文件移到编译器(VS2019)中。而编译器内有一个二进制编辑器能够将该乱码翻译为二进制数显示出来。详细步骤如下:

#include <stdio.h>
int main()
{
 int a = 10000;
 FILE* pf = fopen("test.txt", "wb");
 fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
 fclose(pf);
 pf = NULL;
 return 0; 
}

二进制编辑器

此时我们在编译器中打开该文本:

打开结果

是什么原因让10000用二进制的形式存储变为了10 27 00 00呢?原因是我们先将10000的二进制序列写出来,为:00000000 00000000 00100111 00010000,每四位则为一个16进制数字。则结果为00 00 27 10,但是我们的编译器是以小端的形式存储的。即数据的低位存储到内存的低地址中,数据的高位存储到高地址中。则存储的形式就为:10 27 00 00

文件读取结束的判定

feof函数的错误使用

在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。(feof函数是判断结束过程而不是判断结束的结果)

文本文件读取是否结束,判断返回值是否为 EOF(getc)或者NULL(fgets)
例如:

  • fgetc 判断是否为 EOF .
  • fgets 判断返回值是否为 NULL.

二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:

fread判断返回值是否小于实际要读的个数。

文件文本中正确使用feof函数的例子:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if(!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;
   }
 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
   { 
       putchar(c);
   }
 //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

二进制文件中正确使用feof函数的例子:

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

文件缓冲区

说到文件缓冲区,我们就自然而然想到输入缓冲区,即当一个字符一个字符从键盘上输入时,并不是直接输入到磁盘内,而是先放到输入缓冲区,而当输入缓冲区内的字符放满后,文件缓冲区才向磁盘内输入字符。

文件缓冲区也是一样的道理。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

测试代码:

#include <stdio.h>
#include <windows.h>
//VS2013 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; 
}

我们可以测试一下这个代码,在程序第一个到fgets函数处时,立刻去打开test.txt文本文件,我们会发现里面没有内容,而我们用刷新文件缓冲区的fflush函数时再次打开test.txt文本文件时,会发现里面已经有输入的内容。则能够证实的确有文件缓冲区的存在。

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

参考博文:

https://blog.csdn.net/ZJRUIII/article/details/120552735