【C进阶】—— 一篇文章带你学会C语言的文件操作(二)

简介: 【C进阶】—— 一篇文章带你学会C语言的文件操作(二)

5. scanf/fscanf/sscanf printf/fprintf/sprintf 两组函数对比

C语言中有这样两组函数:


scanf,fscanf,sscanf

printf/fprintf/sprintf

大家看它们是不是长的很像啊,那它们之间有什么区别和不同吗?

相信scanf和printf大家应该都比较熟悉了,那fscanf和fprintf其实在上面的内容中我们也学习了,现在应该就剩下sscanf 和sprintf 大家比较陌生了。


下面我们就一起来学习一下:

5.1 sprintf

我们先来学习一下sprintf 是干什么的?

我们已经知道fprintf是将格式化的数据写入文件流。

对比一下它们。还是很相似的。

140c383e0f374897ad4f8e8907b4aee5.png

只有一个参数不同,fprintf是将格式化的数据写入文件流,所以它的第一个参数是文件指针。

4172b22e26c04a56a9385b0b550425bb.png

sprintf 的作用其实是将格式化的数据写入字符串,所以第一个参数是一个字符指针,它可以指向一个字符数组,字符数组是可以存放一个字符串的。

ae737e14bf224ca3a7886d8d7b9109a2.png

其余细节和fprintf也是比较一样的。

那就写个代码练习练习:

我们尝试把一个结构体数据格式化的写入到一个字符串中,并打印出来看看:

#include <stdio.h>
struct S
{
  char name[20];
  int age;
  float score;
};
int main()
{
  char buf[100] = { 0 };
  struct S s = { "zhangsan",20,95.5f };
  sprintf(buf, "%s %d %f", s.name, s.age, s.score);
  printf("%s", buf);
  return 0;
}

那就是这样一段代码,将结构体变量s中的数据,格式化的写入到字符数组buf中,我们运行看看:

5c6d0c5e8a00445298f04278f9ad1baa.png

就打印出来了。

那能不能把字符串里的内容再还原到一个结构体变量中呢?

当然可以。用sscanf

5.2 sscanf

我们可以再来对比一下sscanffscanf

57e6d3d4e4294d7fadc726482bda6c90.png

它们还是第一个参数不同:

fscanf是从流中读取格式化数据,参数是文件指针。

sscanf是从字符串中读取格式化数据,所以参数还是字符指针

66c250c81caa4a568b9f99ba204dd38d.png

好,那我们接着刚才上面写的代码,把写入字符串的数据再还原到一个结构体变量中。

int main()
{
  char buf[100] = { 0 };
  struct S s = { "zhangsan",20,95.5f };
  sprintf(buf, "%s %d %f\n", s.name, s.age, s.score);
  printf("%s", buf);
  struct S tmp = { 0 };
  sscanf(buf,"%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
  printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
  return 0;
}

结构体变量tmp 我们初始化为0,然后把从字符串buf中读取的格式化数据放到tmp 打印出来

两次打印结果应该是一样的:

7f1a0b629bb74453b590d2918c6c81be.png

没有问题。

5.3 总结

那现在我们来对这几个函数:


scanf/fscanf/sscanf printf/fprintf/sprintf

做一个小小的总结.


scanf和printf

scanf:从标准输入流(stdin )读取格式化数据。

stdin是标准输入,一般指键盘输入到缓冲区里的东西

printf:将格式化数据打印到标准输出流(stdout)

标准输出流是应用程序输出的默认目标。在大多数系统中,默认情况下,它通常定向到文本控制台(通常在屏幕上)。


fscanf和fprintf

fscanf:从流(文件/stdin)中读取格式化数据

fprintf:将格式化数据写入流(文件/stdout)

它们两个适用于所有的输入输出流。


sscanf 和sprintf

sscanf :从字符串中读取格式化数据

sprintf :将格式化数据写入字符串


相信现在大家就对这几个函数有所认识了。


6. 文件的随机读写

我们上面刚刚讲过了文件的顺序读写。


为什么叫顺序读写呢?

大家有没有发现,我们上面讲的那几个函数,在读写文件数据的时候,要么是从文件起始位置开始,一次读取一个字符,如果再读的话就从上次的位置继续往后再读一个;要么是一次读取一个字符串,依次往后读…。

那说到底,都是在顺序读取文件。


那么,如果我们在读取文件时,不想按照顺序读呢?

如果我们想对文件进行随机读取,想从哪个位置开始读就从哪个位置读,能不能做到呢?

当然可以。

接下来,我们就来学习一下,如何对文件进行随机读写。

6.1 fseek

首先我们来看一个函数叫做——fseek

那它是干什么的,怎么用呢?

93323d8e949a4b539d88daed818a3596.png

它可以重新定位流位置指示器,将与流关联的位置指示器设置为新位置。

什么意思呢?

我们打开一个文件,与该文件关联的位置指示器是默认指向文件开头的,所以我们读取文件也默认是从开头进行的。

那fseek呢,就可以重新定位与文件关联的这个位置指示器,那这样的话,我们想从哪个位置读写文件,就可以通过fseek把位置指示器定位到我们想要的位置,这样,就可以实现对文件的随机读写了。

fseek3个参数,我们来了解一下:

第一个参数FILE * stream还是接收文件对应的文件指针。

第二个参数long int offset,接收相对于参考位置的偏移量。

第三个参数int origin,用于指定用作偏移参考的位置。

origin可以有三个取值:

a2584cbb68cd4ac6bf5f3ba9d3e2ef29.png

文件开头,文件末尾和文件位置指示器的当前位置。

第二个参数offset就是接收相对这三个位置的偏移量。

看一下返回值:

526e70df9481413084369af280367d46.png

然后我们就练习一下吧。

eea6a9a019844266a886756322619fe4.png

现在电脑上有一个文本文件test.txt,我们先在就对它进行一个随机读写。

首先我们想直接第一次就读取到字符d,怎么做:

  1. 以文件开头a的位置(SEEK_SET)为参考位置,读从文件开头向后偏移量为3的位置。

那要这样写:

#include <stdio.h>
int main()
{
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    perror("fopen()");
    return 1;
  }
  fseek(pf, 3, SEEK_SET);
  int ch = fgetc(pf);
  printf("%c\n", ch);
  fclose(pf);
  pf = NULL;
  return 0;
}

7653bb510d5a4ef1bcb8b56fb656efdb.png

运行代码:

7cbcc54fecec449d85cb755be59f8009.png

一次就读到d了。

  1. 从文件末尾SEEK_END向前偏移量为3的位置
  fseek(pf, -3, SEEK_END);
  int ch = fgetc(pf);
  printf("%c\n", ch);

这里注意从后向前偏移要写成负数。

f9652a41513a4a2593446f97ea59f1c1.png

那如果我们上去先顺序读取一个字符,然后我们想跳到d的位置读取d,怎么搞呢

  1. 先读一个字符,那现在指向第二个字符b的位置,从当前位置SEEK_CUR 向后偏移量为2的位置就是d
  int ch = fgetc(pf);
  fseek(pf, 2, SEEK_CUR);
  ch = fgetc(pf);
  printf("%c\n", ch);

ce4b3bb6c20241cd9d4695255a4d83ea.png

也可以。

6.2 ftell

那通过上面的学习我们知道有时候想使用fseek实现对文件的随机读写,是需要知道当前位置指示器的位置的。

那有没有什么方法可以快速获取当前位置指示器的位置,不需要我们自己再去计算呢?

当然有。

函数ftell 就是专门来干这件事情的。

我们来看一下:

03240c58c10f4e88b6fbc7a5a932581e.png

它的作用就是返回流的位置指示器的当前值(或者说当前位置指示器相对于起始位置的偏移量)。

0e608431734d43a89285c83fdeafa48a.png

怎么用呢?


它只有一个参数FILE * stream,接收对应的文件指针,就可以返回当前位置指示器相对于起始位置的偏移量。


那我们接着上面的代码,我们刚才读取了d,那现在位置指示器应该指向d的后面,也就是e的位置。


文件中放的是abcdef,那e相对于起始位置a的偏移量应该是4。

那我们现在就用ftell 验证一下,我们打印一下ftell 的返回值,看是不是4:

e1dd2a30d56147578a343ddb7600e8d5.png

通过ftell ,我们很容易就能知道当前位置指示器的位置。

6.3 rewind

然后我们再来看一个函数——rewind

那它的作用是什么呢?

783badb4fc554682873441c53c009bd7.png

它的作用就是:不管当前文件的位置指示器指到了哪个位置,使用rewind就可以让位置指示器直接回到起始位置,指向文件开头。

19a306c0faaa4bb1986ad4d630e8649f.png

只有一个参数,还是接收文件对应的文件指针,无返回值。

那我们现在就来练习一下:

还是上面那个文件,通过之前的操作,我们知道它现在是指向e的位置了,那我们就调用一次rewind,然后在读一次,如果读到的是a,就证明位置指示器在rewind的作用下回到起始位置了。

  rewind(pf);
  ch = fgetc(pf);
  printf("%c\n", ch);

9c777b29e52d47d89295e3dd4aece06a.png

是a。


7. 文本文件和二进制文件

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


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

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。


一个数据在内存中是怎么存储的呢?


字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

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

a6629c5388ce4cf3a2fe3303bbd4adcf.png我们可以测试一下:

我们现在就把10000以二进制的形式存到文件中。

看看是什么样的:

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

运行代码,生成test.txt,我们打开看看:

d78b3a6781a2414bbb0190b5fa4bdae4.png

我们 直接看到的并不是10000,而是一个乱码,因为我们是以二进制的形式存进去的。

有没有什么方法可以查看呢?

有的,我们可以借助vs查看

07e3a7e194f149a6a53e30759afec4d3.png

将这个文件添加到vs2022中;

6fb817684b2f44dd80db949d1a5b2287.png

右键选择打开方式,以二进制编辑器的方式打开

c6c177d4b10e4fd69a41234eff3dc697.png

d373eb91c23b496499cbde68318f530a.png

我们知道vs上是小端存储模式,变成00 00 27 10 是不是跟我们上面分析的一样。
证明就是二进制的存储。

8. 文件读取结束的判定

我们先来看一个函数:

8.1 feof

c2757acdf1ae4f4a9b993486abb23b6a.png

c7fe449b0e2243caaf10a228144de565.png

这个函数是用来干什么的呢?


注意:函数feof 不是用来判断文件是否读取结束的。

而是应用于当文件读取结束的时候,判断是读取失败导致结束,还是遇到文件尾结束。

feof 只有一个参数,接收一个文件指针,判读该文件读取结束时是由于哪种原因导致的结束。


那如何判断是哪种原因导致的结束呢?


我们看到feof 的返回值是int。

如果文件是因为读取到了文件尾而结束的,feof 将返回一个非零值;

否则,将返回0。


我们可以来练习一下:308bbdaca87343b98c00b074edb6720c.png

还是这个文件,我们搞个循环,把它的内容读完,然后用feof 判断一下,看返回值是不是非0值(返回非0就表示读到文件尾的正常结束)。

int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "r");
  if (NULL == pf)
  {
    printf("fopen");
    return 1;
  }
  int ch = 0;
  while((ch = fgetc(pf))!=EOF)
  {
    printf("%c\n", ch);
  }
  printf("%d\n", feof(pf));
  //关闭文件
  fclose(pf);
  pf = NULL;
  return 0;
}

77edeb77fff4447a84498f9efdec69c9.png

是1,非0值,说明文件是读取到文件尾结束的。

大家看这样可以吗?

  int i = 0;
  for (i = 0; i < 6; i++)
  {
    int ch = fgetc(pf);
    printf("%c\n", ch);
  }
  printf("%d\n", feof(pf));

文件中总共6个字符,循环6次是不是就读到文件尾了啊,那我们看一下结果吧:

59956411c4064179802281ad900d02ff.png

诶~,我们看到前面abcdef都打印出来了,和上面一样,但是feof(pf)的返回值却是0,为什么呢?

这里注意:

如果用for循环的话需要循环7次,因为第6次我们是读到了f,并不是文件的结束标志,所以需要在读一次,读取7次才读到文件结尾。

bc6c6e4ede844b62b2a88cd875e7dd3f.png

这次就是非0值1了。

不过我们发现这样好像多打印出来了一个空格,那按照第上面的分析第7次就应该读到文件结束标志了(文件尾指示器),那就是EOF嘛(本质是一个-1)。

而我们把EOF以%c的形式打印:

6a37fb5697bc4629993e0bb0366ab65f.png

就是一个空格。

所以:

文件结束标志(文件尾指示器)应该是在最后一个字符后面。

8.2 如何判断文件是否读取结束

那说到底函数feof 不是用来判断文件是否读取结束的,那我们应该如何去正确判断一个文件是否读取结束呢?


正确的方法是:我们要根据相关函数的返回值来判断文件是否读取结束。

其实它们的返回值我们在学习这些函数的时候也提过。

8.2.1 文本文件

文本文件读取是否结束,判断其返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )或是否小于指定数据个数(fscanf )

对于fgetc来说:


我们看一下它的返回值是啥:

80e64ded293d474f8957c13e4ac1f496.png

那我们就可以通过判断fgetc 的返回值是否为EOF来判断文件是否读取结束。(当然这里我们看到如果发生其它读取错误,也会返回EOF,那这种情况我们是不是可以使用feof 再进行判断)

fgets 呢?

490051128b784921ab8f9e5ef1c377ba.png

对于fgets 来说,读取文件结束返回空指针,所以我们可以通过判断其返回值是否为空指针来判断文件是否读取结束。

还有fscanf

b5c3b372bf0c4368b5ffba90b8f6973f.png

判断返回值是否小于指定的数据个数来判断是否是读取结束。2.我是文本 蓝色

正确的使用示范:

#include <stdlib.h>
int main() 
{
  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);
  fp = NULL;
}

大家可以看看这段代码。

e2e862c77894455e9404d544edd38cee.png

这里面用了一个ferror 我们没有说。

ferror 其实是判断是否发生错误的,如果发生读取错误,则ferror 返回非0值。

5d057051f28c4af59adb09172dc9df3b.png

8.2.2 二进制文件

二进制文件的读取结束判断,判断(fread)返回值是否不同于(可能小于count)实际要读的个数

来看一下fread的返回值:

9b3f3b7fe2d4436c9d2f44eca1f73a27.png

所以对于fread我们可以通过判断其返回值是否小于实际要读的个数(count)来判断文件是否读取结束。

正确示范:

enum { SIZE = 5 };
int main() 
{
  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");
  if (!fp) {
    perror("File opening failed");
    return EXIT_FAILURE;
  }
  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);
  fp = NULL;
}

大家可以仔细读一下这段代码。

9. 文件缓冲区

接下来我们再来了解一个知识叫做“文件缓冲区”。


ANSIC 标准采用“缓冲文件系统”来处理数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

从内存向磁盘输出的数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。

如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区至充满缓冲区后,再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

16d818b8309a421dba766205f0965ed8.png

当然:

虽然说的是要把缓冲区装满才会开始传输数据(这样做可以提高效率),但是我们也可以根据自己的需求去刷新缓冲区,比如函数fflush 就可以强制刷新缓冲区,另外,遇到\n(行缓冲)和关闭文件时也会自动刷新缓冲区。

下面有一段代码,可以让我们感受一下缓冲区的存在:

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

我们运行代码,abcdef不会直接写入文件,而是先放在缓冲区,当我们使用fflush强制刷新缓冲区后,才将输出缓冲区的数据写到文件(磁盘),然后文件中才有内容。

我们运行代码给大家看一下:

运行之前该文件还未创建:

fae118f298ad4dddbc6b990f894df039.png

现在我们运行:

bde1c707dfd74e05a4cbccfd549893ed.png

此时打开文件,里面还没有内容,因为还没有刷新缓冲区

刷新后,我们再打开文件,发现里面就有内容了。

fc9636f926d14ee8bbe05ecaeace6b5b.png

这里可以得出一个结论:

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

好了,那到这里,C语言文件操作的内容就全部讲完了。希望对大家有帮助,也欢迎大家指正!!!

c2631f9f2d4b46fc8a97b25a30ca6ffa.png

目录
相关文章
|
5天前
|
存储 小程序 C语言
【C语言程序设计——文件】文件操作(头歌实践教学平台习题)【合集】
本文介绍了C语言中的文件操作,分为两个关卡。第1关任务是将键盘输入的字符(以#结束)存入`file1.txt`并显示输出;第2关任务是从键盘输入若干行文本(每行不超过80个字符,用-1作为结束标志),写入`file2.txt`后再读取并显示。文中详细讲解了文件的打开、读取(使用`fgetc()`和`fgets()`)、写入(使用`fputc()`和`fputs()`)及关闭操作,并提供了示例代码和测试说明。
22 5
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
60 9
|
1月前
|
存储 数据管理 C语言
C 语言中的文件操作:数据持久化的关键桥梁
C语言中的文件操作是实现数据持久化的重要手段,通过 fopen、fclose、fread、fwrite 等函数,可以实现对文件的创建、读写和关闭,构建程序与外部数据存储之间的桥梁。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
161 3
|
2月前
|
存储 C语言
【c语言】玩转文件操作
本文介绍了C语言中文件操作的基础知识,包括文件的打开和关闭、文件的顺序读写、文件的随机读写以及文件读取结束的判定。详细讲解了`fopen`、`fclose`、`fseek`、`ftell`、`rewind`等函数的使用方法,并通过示例代码展示了如何进行文件的读写操作。最后,还介绍了如何判断文件读取结束的原因,帮助读者更好地理解和应用文件操作技术。
62 2
|
3月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
93 7
|
3月前
|
存储 编译器 C语言
如何在 C 语言中判断文件缓冲区是否需要刷新?
在C语言中,可以通过检查文件流的内部状态或使用`fflush`函数尝试刷新缓冲区来判断文件缓冲区是否需要刷新。通常,当缓冲区满、遇到换行符或显式调用`fflush`时,缓冲区会自动刷新。
|
3月前
|
存储 编译器 C语言
C语言:文件缓冲区刷新方式有几种
C语言中文件缓冲区的刷新方式主要包括三种:自动刷新(如遇到换行符或缓冲区满)、显式调用 fflush() 函数强制刷新、以及关闭文件时自动刷新。这些方法确保数据及时写入文件。
|
3月前
|
存储 C语言
简述C语言文件操作
简述C语言文件操作
15 0
|
3月前
|
存储 文件存储 C语言
深入C语言:文件操作实现局外影响程序
深入C语言:文件操作实现局外影响程序