C语言:详解文件操作

C语言:详解文件操作

1.文件相关知识
2.文件的打开和关闭
3.文件的顺序读写
4.文件的随机读写
5.文件读取结束判定
6.文件缓冲区

1.文件相关知识

什么是文件?

磁盘(硬盘)上的文件是文件。

在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。

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

本期讨论的是数据文件。

一个文件要有一个唯一的文件标识,文件名包含3部分:文件路径 + 文件名主干 + 文件后缀,例如:c:\code\test.txt 。最后一个 \ 和 . 之间的是文件主干名,最后一个 \ 之前的内容(包括最后一个 \ )为文件路径,剩下的内容和点号 . 为文件后缀。

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

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

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

流和标准流

程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了帮助程序员对各种设备进行方便的操作,我们抽象出了的概念,我们可以把流想象成流淌着字符的河。

C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。

C语言程序在启动的时候,默认打开了3个流:

  • stdin :标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
  • stdout :标准输出流,在大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
  • stderr :标准错误流,在大多数环境中输出到显示器界面。

stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为文件指针。C语言中,就是通过 FILE* 的文件指针来维护流的各种操作。

文件指针

每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息保存在一个结构体变量中,该结构体类型由系统声明,取名 FILE

下面创建一个FILE*类型的指针变量pf。

c 复制代码
FILE* pf;  //文件指针变量

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

2.文件的打开和关闭

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

fopen函数用来打开文件,函数原型如下:

c 复制代码
FILE* fopen(const char* filename,const char* mode);
  • 参数filename为文件名,用字符串表示。
  • 参数mode为文件打开模式,用字符串表示。

读和写的意义如图所示:

文件打开模式如下:

注意:对于"w",若打开的文件存在且已有信息,则清空原有信息。

fclose函数用来关闭文件,函数原型如下:

c 复制代码
int fclose(FILE* stream);

示例:打开一个文件,然后关闭文件。

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","w");
   if(pf==NULL)  //检查
   {
      perror("fopen");  
      //如果pf为NULL,就会打印 fopen:错误信息 ,perror的参数是一个字符串
      return 1;
   }
   fclose(pf);  //关闭文件
   pf=NULL;  //把pf置为NULL,避免成为野指针。
   return 0;
}

程序运行后就会自动创建test.txt文件,按如下方法找到test.txt文件。

接下来点击 "x" 退出,按下图操作。



3.文件的顺序读写

顺序读写函数介绍

上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。

fputc函数

fputc函数可将字符输出到文件中,函数原型如下:

c 复制代码
int fputc(int ch,FILE* stream);
  • 参数ch传字符或ASCII码值

示例:在test.txt文件上写字母a~z。

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","w");
   if(pf==NULL)  //检查
   {
      perror("fopen");
      return 1;
   }
   char ch=0;
   for(ch='a';ch<='z';ch++)  //写入字母a~z
      fputc(ch,pf);
   fclose(pf);
   pf=NULL;
   return 0;
}

程序运行结束后,我们用记事本打开文件,就能看到文件被写入字母a~z。

fputc函数的第二个参数是文件指针类型的stream,因为fputc适用于所有输出流,所以我们将上述代码中fputc的参数pf改为stdout,即可把字母a~z输出到屏幕。

c 复制代码
#include<stdio.h>
int main()
{
   char ch=0;
   for(ch='a';ch<='z';ch++)
      fputc(ch,stdout)
   return 0;
}
fgetc函数

fgetc函数可以从文件中读取字符,函数原型如下:

c 复制代码
int fgetc(FILE* stream);
  • 若成功读取字符,返回字符的ASCII码值。
  • 若读取遇到文件末尾,或读取失败,返回EOF(即为 -1)。

示例:把test.txt中的字母a~z显示到屏幕上。

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","r");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   int ch=0;
   while((ch=fgetc(pf))!=EOF)  //如果能正常读取,循环继续,直到读到文件末尾
      printf("%c",ch);
   fclose(pf);
   pf=NULL;
   return 0;
}   

fgetc也可以把参数pf改为stdin,这样就能从键盘输入字符,类似scanf。

c 复制代码
#include<stdio.h>
int main()
{
   int ch=0;
   ch=fgetc(stdin);
   fputc(ch, stdout);   
   return 0;
}
fputs函数

fputs函数可以向文件输出一个字符串,函数原型如下:

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","w");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   fputs("haha\n",pf);
   fputs("hehe\n",pf);
   fclose(pf);
   pf=NULL;
   return 0;
}

可以看到,test.txt文件被写入了haha和hehe,由于\n的作用,所以有3行。

同样地,我们把参数pf改为stdout,就能把字符串输出到屏幕。

c 复制代码
#include<stdio.h>
int main()
{
   char str[]="abcdefg";
   fputs(str,stdout);
   return 0;
}
fgets函数

fgets函数能从文件中最多读取一行字符,但不会换行读,函数原型如下:

c 复制代码
char* fgets(char* str,int num,FILE* stream);
  • 参数num表示最多读num个字符。
  • 读取成功返回字符串地址,读取失败或遇到文件末尾,返回NULL。

示例:将test.txt中的haha、hehe打印到屏幕上。

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","r");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   char str[100];
   while(fgets(str,30,pf)!=NULL)
      printf("%s",str);
   fclose(pf);
   pf=NULL;
   return 0;
}

同样的,我们把参数pf改为stdin,就能在键盘上输入。

c 复制代码
#include<stdio.h>
int main()
{
   char str[100];
   fgets(str,5,stdin);
   printf("%s\n",str);
   return 0;
}

因为最多只读5个字符,且包含 '\0' ,所以打印结果如下:

fprintf函数

fprintf函数是格式化输出函数,函数原型如下:

c 复制代码
int fprintf(FILE* stream,const char* format,...);

示例:把结构体的数据写到文件中。

c 复制代码
#include<stdio.h>
struct S
{
   char name[20];
   int age;
   float score;
};
int main()
{
   struct S s={"张三",20,65.5f};
   FILE* pf=fopen("test.txt","w");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   fprintf(pf,"%s %d %f",s.name,s.age,s.score);
   fclose(pf);
   pf=NULL;
   return 0;
}

可以看到,fprintf函数与printf类似,只是fprintf多了一个参数,我们同样可以通过对文件类型参数的调整来使数据输出到屏幕。

c 复制代码
#include<stdio.h>
int main()
{
   int arr[10]={0};
   for(int i=0;i<10;i++)
   {
      fprintf(stdout,"%d ",arr[i]);
   }
   return 0;
}
fscanf函数

fscanf函数为格式化输入函数,函数原型如下:

c 复制代码
int fscanf(FILE* stream,const char* format,...);

示例:把test.txt文件中的数据读入结构体变量s中。

c 复制代码
#include<stdio.h>
struct S
{
   char name[20];
   int age;
   float score;
};
int main()
{
   struct S s={0};
   FILE* pf=fopen("test.txt","r");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   fscanf(pf,"%s %d %f",s.name,&(s.age),&(s.score));
   printf("%s %d %f\n",s.name,s.age,s.score);
   fclose(pf);
   pf=NULL;
   return 0;
}

fscanf也与scanf类似,不同之处就在于fscanf函数参数多了一个文件类型指针,我们通过这个指针也可以实现通过键盘输入数据。

c 复制代码
#include<stdio.h>
int main()
{
   int n=0;
   fscanf(stdin,"%d",&n);
   fprintf(stdout,"%d",n);
   return 0;
}
fwrite函数

fwrite函数为二进制输出函数,返回成功读取的元素个数,只适用于文件输出流,函数原型如下:

c 复制代码
size_t fwrite(const void* p,size_t size,size_t count,FILE* stream);
  • 例如p指向数组,每个元素占size个字节,共count个元素。

示例:把arr的数据写到文件test.txt中。

c 复制代码
#include<stdio.h>
int main()
{
   int arr[]={1,2,3,4,5};
   int len=sizeof(arr)/sizeof(arr[0]);
   FILE* pf=fopen("test.txt","wb");
   if(pf=NULL)
   {
      perror("fopen");
      return 1;
   }
   fwrite(arr,sizeof(int),len,pf);
   fclose(pf);
   pf=NULL;
   return 0;
}

由于文件的打开模式是"wb",所以test.txt文件里的内容是无法读懂的。

fread函数

fread函数是二进制输入函数,读到多少个元素就返回数字多少,只适用于文件输入流,函数原型如下:

c 复制代码
size_t fread(void* p,size _t size,size_t count,FILE* stream);
  • size为元素所占字节数,count为元素个数。

示例:把test.txt文件中的二进制数据读到arr。

c 复制代码
#include<stdio.h>
int main()
{
   int arr[5]={0};
   FILE* pf=fopen("test.txt","rb");
   if(pf==NULL)
   {
      perror("fopen");
      return 0;
   }
   int i=0;
   fread(arr, sizeof(int), 5, pf);
   for (int i = 0;i < 5;i++)
   {
       printf("%d ",arr[i]);
   }
   fclose(pf);
   pf=NULL;
   return 0;
}

4.文件的随机读写

fseek函数

fseek可根据文件中的光标的位置来读写文件。函数原型如下:

c 复制代码
int fseek(FILE* stream,long int,offset,int origin);
  • 参数offset为目标位置到起始位置偏移量。
  • 参数origin为起始位置,文件起始位置为SEEK_SET,光标当前位置为SEEK_CUR,文件末尾为SEEK_END。

示例:

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","w");  //先用"w"把之前文件中的内容清空
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   char str[]="abcdefghij";  //往文件中写入字母a~j
   fputs(str,pf);
   fclose(pf);  //关闭文件,刷新缓冲区接下来才能正常操作
   pf=fopen("test.txt","r");  //再次以"r"打开文件
   fseek(pf,4,SEEK_SET);  //光标从文件起始位置(向右)偏移4,向左偏移则为负数
   int ch=fgetc(pf);
   printf("%c\n",ch);
   fclose(pf);
   pf=NULL;
   return 0;
}


ftell函数

ftell函数能返回光标相对起始位置的偏移量,函数原型如下:

c 复制代码
long int ftell(FILE* stream);

示例:

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","r");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   for (int i=0;i<5;i++)  //读取5个字符
   {
      fgetc(pf);
   }
   int ret=ftell(pf);
   printf("%d\n",ret);
   fclose(pf);
   pf=NULL;
   return 0;
}
rewind函数

rewind函数能让文件指针的位置回到文件的起始位置,函数原型如下:

c 复制代码
void rewind(FILE* stream);

示例:

c 复制代码
#include<stdio.h>
int main()
{
   FILE* pf=fopen("test.txt","r");
   if(pf==NULL)
   {
      perror("fopen");
      return 1;
   }
   for (int i=0;i<5;i++)  //移动光标
   {
      fgetc(pf);
   }
   int ret=ftell(pf);
   printf("此时光标与起始位置的距离为%d\n",ret);
   rewind(pf);
   ret=ftell(pf);
   printf("此时光标与起始位置的距离为%d\n",ret);
   fclose(pf);
   pf=NULL;
   return 0;
}

5.文件读取结束判定

文件读取结束的原因可能是 遇到文件末尾 或 读取出错。

文本文件读取是否结束:

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

⼆进制文件的读取是否结束:

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

其他判断方法:

  • feof函数,返回值非0,则遇到文件末尾。
c 复制代码
int feof(FILE* stream);
  • ferror函数,错误标记被设置返回非0,否则返回0。
c 复制代码
int ferror(FILE* stream);

6.文件缓冲区

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

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

如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

缓冲区的大小根据C编译系统决定的。

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

拙作一篇,望诸位同道不吝斧正。

相关推荐
TalkU浩克几秒前
C++中使用Essentia实现STFT/ISTFT
开发语言·c++·音频·istft·stft·essentia
awonw13 分钟前
[python][flask]flask静态资源
开发语言·python·flask
Chef_Chen20 分钟前
从0开始学习R语言--Day57--SCAD模型
开发语言·学习·r语言
医工交叉实验工坊25 分钟前
R 语言绘制六种精美热图:转录组数据可视化实践(基于 pheatmap 包)
开发语言·信息可视化·r语言
小关会打代码30 分钟前
Python编程进阶知识之第五课处理数据(matplotlib)
开发语言·python·机器学习·matplotlib·绘图
小比卡丘31 分钟前
【C++进阶】第7课—红黑树
java·开发语言·c++
超浪的晨1 小时前
Java 单元测试详解:从入门到实战,彻底掌握 JUnit 5 + Mockito + Spring Boot 测试技巧
java·开发语言·后端·学习·单元测试·个人开发
不断努力的根号七1 小时前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt
赵英英俊1 小时前
Python day24
开发语言·python
Harbor Lau1 小时前
多线程插入保证事务的一致性,亲测可用方式一实测
java·开发语言