4、文件读写

C中的文件

C语言具有操作文件的能力,比如打开文件、读取和追加数据、插入和删除数据、关闭文件、删除文件等。

在操作系统中除了常见的文本文件以及二进制文件,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。例如:

常见硬件设备对应的文件

文件 硬件设备
stdin 标准输入文件,一般指键盘;scanf()、getchar() 等函数默认从 stdin 获取输入。
stdout 标准输出文件,一般指显示器;printf()、putchar() 等函数默认向 stdout 输出数据。
stderr 标准错误文件,一般指显示器;perror() 等函数默认向 stderr 输出数据。
stdprn 标准打印文件,一般指打印机。

操作文件的正确流程为:打开文件 --> 读写文件 --> 关闭文件。文件在进行读写操作之前要先打开,使用完毕要关闭。

所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。

在C语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读取一整行,还可以读取若干个字节。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。

文件流

所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。数据在文件和内存之间传递的过程叫做文件流,类似水从一个地方流动到另一个地方。数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。

文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。

输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。

我们可以说,打开文件就是打开了一个流。

打开文件

//以只写方式打开一个当前路径下test.txt文本文件
//fp 通常被称为文件指针。
FILE *fp = fopen("./test.txt","w");

判断是否打开成功

打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,

int main(){
   FILE *fp = NULL;
   if((fp = fopne("./test.txt","w")) == NULL){
      printf("File open failed");
   }
    
   return 0;
}

打开方式

读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。

整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:

控制读写权限的字符串
打开方式 说明
"r" 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。
"w" 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a" 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
"r+" 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。
"w+" 以“写入/更新”方式打开文件,相当于wr+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a+" 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
控制读写方式的字符串
打开方式 说明
"t" 文本文件。如果不写,默认为"t"
"b" 二进制文件。

关闭文件

读写文件

以字符

以字符形式读写文件时,每次可以从文件中读取一个字符,或者向文件中写入一个字符。主要使用两个函数,分别是 fgetc() 和 fputc()。

fgetc 是 file get char 的缩写,意思是从指定的文件中读取一个字符。

#include<stdio.h>

int main(){
   //打开文件
   FILE *fp = NULL;
   if((fp = fopen("./test.txt","r")) == NULL){
      printf("File open failed");
      return 0;
   }
   //按照字符读取文件
   char c;
   while((c = fgetc(fp)) != EOF){
      printf("%c",c);
   }
   //关闭文件
   fclose(fp);
   return 0;
}

EOF

文件位置指针

fputc 是 file output char 的所以,意思是向指定的文件中写入一个字符。

注意

#include<stdio.h>

int main(){
   //打开文件
   FILE *fp = NULL;
   if((fp = fopen("./test.txt","w")) == NULL){
      printf("File open failed");
      return 0;
   }
   //从键盘获取字符,并写入文件,回车结束
   char c;
   while((c = getchar()) != '\n'){
      fputc(c,fp);
   }
    
   fclose(fp);
   return 0;
}

以字符串

fgetc() 和 fputc() 函数每次只能读写一个字符,速度较慢;实际开发中往往是每次读写一个字符串或者一个数据块,这样能明显提高效率,以字符串读主要是用两个函数:fgets、fputs

fgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中

注意

#include<stdio.h>

#define NUM 100

int main(){
   FILE *fp = NULL;
   if((fp = fopen("./test.txt","r")) == NULL){
      printf("File open failed");
      return 0;
   }

   char c[NUM];
   //逐行读取
   while(fgets(c,999,fp) != NULL){
      printf("%s",c);
   }

   fclose(fp);
   return 0;
}

fputs() 函数用来向指定的文件写入一个字符串

#include<stdio.h>

int main(){
   FILE *fp = NULL;
   if((fp = fopen("./test.txt","w")) == NULL){
      printf("File open failed");
      return 0;
   }

   char str[] = "Hello World\nHello C Language";
   //写入文件
   fputs(str,fp);

   fclose(fp);
   return 0;
}

以数据块(二进制)

以二进制读取数据,通常是数组或结构体。如果希望读取多行内容,需要使用 fread() 函数;相应地写入函数为 fwrite()。

fread() 函数用来从指定文件中读取块数据。所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。

原型:size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );

fwrite() 函数用来向文件中写入块数据

原型:size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );

说明

参数

返回值

对数组进行读写

#include<stdio.h>

int main(){
   int age[] = {18,22,55,89};

   FILE *fp = NULL;
   if((fp = fopen("./age","wb")) == NULL){
      printf("File Open Failed");
      return 0;
   }

   //将数组写入文件,每个数据块为sizeof(int)字节,一共sizeof(age) / sizeof(int)个数据块
   fwrite(age,sizeof(int),sizeof(age) / sizeof(int),fp);

   fclose(fp);

   return 0;
}
#include<stdio.h>

int main(){

   FILE *fp = NULL;
   if((fp = fopen("./age","rb")) == NULL){
      printf("File Open Failed");
      return 0;
   }

   int age[4];
   fread(age,sizeof(int),4,fp);

   for(int i = 0;i < 4;i++){
      printf("%d\n",age[i]);
   }

   fclose(fp);

   return 0;
}

对结构体进行读写

#include<stdio.h>

typedef struct Stu{
   int age;
   char *name;
}Stu;

int main(){

   FILE *fp = NULL;
   if((fp = fopen("./stu","wb")) == NULL){
      printf("File Open Failed");
      return 0;
   }

   Stu s = {18,"lucy"};
   //将一个结构体变量的值写入文件,数据块的大小就是一个结构体的大小,只写入一个数据块
   //如果是结构体数组,那么第三个参数就是数组长度
   fwrite(&s,sizeof(Stu),1,fp);
   
   fclose(fp);
   return 0;
}
#include<stdio.h>

typedef struct Stu{
   int age;
   char name[50];
}Stu;

int main(){

   FILE *fp = NULL;
   if((fp = fopen("./stu","rb")) == NULL){
      printf("File Open Failed");
      return 0;
   }

   Stu s;
   int i = fread(&s,sizeof(Stu),1,fp);
   printf("姓名:%s,年龄:%d",s.name,s.age);
   
   fclose(fp);
   return 0;
}

格式化读写文件

fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。

函数原型

int fscanf ( FILE *fp, char * format, ... );
int fprintf ( FILE *fp, char * format, ... );

fprintf() 返回成功写入的字符的个数,失败则返回负数。

fscanf() 返回参数列表中被成功赋值的参数个数。

例如,将Stu结构体变量写入文件中

#include<stdio.h>

typedef struct Stu{
    char name[20];
    int age;
}Stu;

int main(){
    Stu s = {"lucy",18};

    //打开文件
    FILE *fp;
    if((fp = fopen("./stu.txt","w")) == NULL){
        printf("File opne failed");
    }

    //以规定格式写入
    fprintf(fp, "%s %d\n",s.name,s.age);

    fclose(fp);
    return 0;
}

此时,stu.txt文件内容为

lucy 18

然后,按照格式读取就可以了

#include<stdio.h>

typedef struct Stu{
    char name[20];
    int age;
}Stu;

int main(){
    FILE *fp;
    if((fp = fopen("./stu.txt","r")) == NULL){
        printf("File open failed");
    }

    Stu s;

    //从文件中按照固定格式读取
    int i = fscanf(fp,"%s %d\n",&s.name,&s.age);

    //打印
    printf("name->%s||age->%d",s.name,s.age);

    fclose(fp);
    return 0;
}

name->lucy||age->18

移动文件位置指针

在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。

实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。

rewind()

函数原型:void rewind ( FILE *fp );

该函数的作用相当于重置文件位置指针,将指针移到文件开头

fseek()

函数原型:int fseek ( FILE *fp, long offset, int origin );

起始点 常量名 常量值
文件开头 SEEK_SET 0
当前位置 SEEK_CUR 1
文件末尾 SEEK_END 2

例如,把位置指针移动到离文件开头100个字节处

fseek(fp, 100, 0);

注意:值得说明的是,fseek() 一般用于二进制文件,在文本文件中由于要进行字节和字符转换,计算的位置有时会出错。