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关闭文件)。如果不做,可能导致读写文件的问题。

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

相关推荐
破茧蝴蝶5 小时前
Python分布式爬虫完整教程
开发语言·python
狗凯之家源码网5 小时前
全新 PHP 轻量客服聊天室源码深度评测
开发语言·php·聊天室系统
远离UE45 小时前
UE5 Substrate Thin Film 源码拆析
开发语言·javascript·ue5
keyipatience6 小时前
JavaScript学习(1)书写位置,变量、数据类型、运算符
开发语言·前端·javascript·css·学习
栈帧之外6 小时前
【C++】STL之set
开发语言·c++
catchadmin7 小时前
免费可商用 PHP 管理后台 CatchAdmin V5.4.1 发布,新增 MCP 支持
开发语言·php
jimy18 小时前
c++的 Type Traits (二、更详细版本)
开发语言·c++
狗凯之家源码网9 小时前
纯 PHP+MySQL 应用市场系统源码实测!
开发语言·mysql·php
dishugj9 小时前
系统架构风险点、敏感点和权衡点的概念和区分
java·开发语言
Zwawa9 小时前
先把轮子转稳,才不会烧心:编码器速度环 PID 怎么接回 PWM
c语言·单片机·嵌入式