一. 什么是文件

1. 文件基本概念

广义上磁盘上的文件都是文件。但在程序设计中我们把文件分为两类:程序文件、数据文件。

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

数据文件:文件的内容不一定是程序,还可能是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

下面我们讨论的是数据文件。

2. 文件标识

一个文件要有一个唯一的文件标识,以便用户识别和引用。其中文件标识包括三个部分:文件路径 + 文件名 + 文件后缀。
在这里插入图片描述

3. 文件类型

根据数据的组织形式,数据文件又分为为文本文件二进制文件

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

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

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

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

4. 文件控制块

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

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。每当打开一个文件的时候,操作系统会根据文件的情况自动为该文件创建一个FILE类型的结构体变量,并填充其中的信息,使用者不必关心具体的实现细节
在这里插入图片描述

C将文件的内容存储到顺序字节流里,其实就是一块缓冲区,这个字节流以文件结束符EOF(end of file)作为结束标志。在文件控制块中有一个指针 _ptr 指向这个字节流的起始位置,相当于数组名;还有一个位置指示器 _charbuf 标识字节流当前的位置,相当于数组的下标。

二. 文件操作的函数

前面已经介绍了文件相关的基本知识和概念。那么在语言层面上(比如C语言),如何对一个文件进行打开、关闭、读写数据的操作呢?C语言提供了一系列文件操作相关的库函数,包含在头文件stdio.h里。

1. 文件的打开与关闭

1.1 文件打开函数 — fopen

FILE* fopen ( const char* filename, const char* mode );

参数介绍:

  • filename:如果我们要打开的文件与我们当前正在运行的源代码在同级目录下,那么可直接写文件名字。不在同级目录下的话需要写明文件所在的路径(绝对路径或相对路径都可以)。
  • mode:文件打开方式,常用的包括以下几种。
打开方式含义
“r”read:打开文件进行输入操作。该文件必须存在。
“w”write:为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。
“rb”read:以二进制形式打开文件,进行输入操作。该文件必须存在。
“wb”write:以二进制形式打开文件,为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。

功能:以特定的方式打开文件。操作系统会根据第一个参数的路径找到或创建该文件,并新建一个存储该文件信息的文件控制块,最后返回文件控制块的地址。注意:文件刚打开时,位置指示器指向最开始位置。

返回值

  • 打开成功:返回该文件的文件控制块的地址。
  • 打开失败:返回空指针,即NULL。

函数使用举例

在当前路径下创建一个新文件log.txt,因为事先不存在这个文件,所以我们采用"w"模式打开。

// 1、打开文件
FILE* pf = fopen("log.txt", "w");    
if(pf == NULL)    
{    
  printf("open error\n");    
  return 1;    
}    
// 2、对文件进行一系列操作...    
// 3、关闭文件
return 0;   

运行结果:
在这里插入图片描述

1.2 文件关闭函数 — fclose

就像malloc动态开辟空间,使用完毕之后需要手动free释放掉这块空间一样的道理。我们通过fopen函数打开一个文件,操作系统为我们动态创建了这个文件的文件控制块,不使用的话需要fclose函数来释放它。注意并不是删除该文件,这个文件依然没有改变,只是删除fopen函数创建的存储该文件信息的文件控制块。

函数原型

int fclose ( FILE* stream );

参数:文件控制块的地址。

功能:释放开辟的文件控制块的空间。形象说就是关闭一个文件。

返回值:关闭成功返回0,失败返回EOF。
在这里插入图片描述

1.3 文件操作的大致流程

#include <stdio.h>        
int main()      
{      
  // 1、打开文件      
  FILE* pf = fopen("log.txt", "w");      
  if(pf == NULL)      
  {      
    printf("open error\n");      
    return 1;      
  }      
      
  // 2、对文件进行一系列操作      
  // .......      
      
  // 3、关闭文件      
  fclose(pf);      
  pf = NULL;      
  return 0;
}  

2. 文件的顺序读写

在上面,我们介绍了最基本的打开、关闭文件,其本质是创建、释放文件控制块。当然这只是开始,最重要的是我们拿到文件控制块后,如何通过它来完成文件的读写操作。

对文件数据的读写可以分为顺序读写和随机读写。顺序读写,即挨着顺序逐个字符的对文件中的数据进行写入和读取。下面我们介绍C语言中对文件进行顺序读写的一系列函数。

2.1 字符的写入和读取函数 — fputc 和 fgetc

一、fputc

int fputc ( int character, FILE* stream );

参数

  • character:被写入的字符
  • stream:文件控制块的地址。

功能:字符被写入字节流的位置指示器所指示的位置,然后该指示器将自动前进1。
在这里插入图片描述

返回值:写入成功返回这个被写入的字符。如果写入错误,则返回EOF并设置错误指示符(ferror)。
在这里插入图片描述

函数使用举例

在当前目录不下创建一个新的文件log.txt,写入小写字母序列a~z。

void Testfputc()    
{    
  // 1、打开文件    
  FILE* pf = fopen("log.txt", "w");    
  
  // 2、使用fputc对文件进行字符的写入   
  int i = 0;    
  for(i = 'a'; i <= 'z'; ++i)    
  {    
    fputc(i, pf);    
  }    
  
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
}  

最终当前目录下生产一个文件log.txt。
在这里插入图片描述

二、fgetc

int fgetc ( FILE* stream );

参数:文件控制块地址。

功能:返回指定文件控制块的内部位置指示器当前指向的字符。然后将文件位置指示器+1。

返回值

  • 成功的话返回提取到的字符。
  • 如果调用时字节流位于文件结束位置,则该函数返回EOF并设置文件结束指示符(feof)。如果发生其他读取错误,该函数也返回EOF,但设置其错误指示符(ferror)。

函数使用举例

从上面刚刚写入完成的log,txt里读取数据。

void Testfgetc()    
{    
  // 1、用"r"方式打开已经存在的文件    
  // 刚打开时位置指示器指向字符的开始位置                                                                                   
  FILE* pf = fopen("log.txt", "r");    
    
  // 2、使用fgetc依次读取文件的每一个字符    
  int ch = 0;    
  while((ch = fgetc(pf)) != EOF)    
  {    
    printf("%c", ch);    
  }    
  printf("\n");    
    
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
}  

编译运行
在这里插入图片描述
注意:读取的过程只是在移动文件信息区里的_ptr指针,文件内容并没有任何更改。
在这里插入图片描述


2.2 字符串的写入和读取函数 — fputs 和 fgets

一、fputs

int fputs ( const char* str, FILE* stream );

参数

  • str:写入的字符串。
  • stream:被写入的文件控制块指针。

功能:写一个字符串到字节流中,写入的字符串遇到’\0’结束,并且最后的’\0’不会被写入字节流。

返回值:成功返回一个非负的数字。当写入错误时,函数返回EOF并设置错误指示符(ferror)。

函数使用举例

我们对log.txt写入一个字符串“hellow world”。

void Testfputs()    
{    
  // 1、以"w"方式打开文件
  FILE* pf = fopen("log.txt", "w");    
    
  // 2、使用fputs写入一行字符串    
  const char* s = "hello world";    
  fputs(s, pf);    
    
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;                                                                                                          
}  

查看当前目录下生成的log.txt
在这里插入图片描述
二、fgets

char * fgets ( char* str, int num, FILE* stream );

参数

  • str:把文件里读取到的字符串放到str里。
  • num:需要读取的字符的个数。
  • stream:文件信息区地址。

功能:从字节流中读取字符,并将它们作为C字符串存储到str中,直到(num-1)字符被读取,或者到达换行符或文件结束符,以最先发生的为准。
在这里插入图片描述

返回值

  • 调用成功返回str。
  • 如果读取到了字节流最后的文件结束符,则返回的指针为空指针,并设置文件结束符(feof)。
  • 如果发生读取错误,则设置错误指示符(ferror)并返回一个空指针。

关于fgets读取结束后的几种处理方式说明

  1. 如果确实可以读取到 num-1 个字符,在把num-1个字符复制到目的地str之后,最后会自动附加一个’\0\到str,这样合起来一共是n个字符。
  2. 如果中途读取到字节流中的文件结束符,那么读取结束,最后依然会附加一个’0’。
  3. 如果中途遇到’\n’,读取结束,'\n’也一起读取到str中。

函数使用举例

读取前面写入的字符串"hello world‘"到字符数组str中

void Testfgets()    
{    
  // 1、用"r"方式打开已经写入过的log.txt    
  FILE* pf = fopen("log.txt", "r");    
    
  // 2、使用fgets从文件里读取字符串放到str中    
  char str[12] = {0};    
  fgets(str, 20, pf);                                                                                                 
  printf("%s\n", str);    
    
  // 关闭文件    
  fclose(pf);    
  pf = NULL;    
} 

编译运行:
在这里插入图片描述

2.3 格式化写入和读取函数 — fprintf 和 fscanf

一、fprintf

int fprintf ( FILE* stream, const char* format, ... );

参数:第一个参数是被写入文件控制块地址,第二个参数格式控制序列,其用法就和printf一样。
在这里插入图片描述
功能:格式化写入数据。

返回值:写入成功返回写入的数据个数.如果发生写错误,则设置错误指示符(ferror)并返回一个负数。

函数使用举例

struct Person    
{    
  char name[20];                                                                                                      
  int age;    
};    
    
void Testfprintf()    
{    
  // 1、打开文件    
  FILE* pf = fopen("log.txt", "w");    
  // 2、对文件进行格式化写入    
  Person p = {"zhangsan", 18};    
  fprintf(pf, "%s %d", p.name, p.age);    
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
}  

查看当前目录下的log.txt文件
在这里插入图片描述
二、fscanf

int fscanf ( FILE* stream, const char* format, ... );

参数:第一个参数是被读取文件控制块地址,第二个参数格式控制序列,其用法就和scanf一样。
在这里插入图片描述
功能:从字节流里格式化读取数据到指定实参中。

返回值

  • 如果成功,函数将返回成功填充参数列表的项数。
  • 如果发生读取错误或在读取时到达文件结束,则设置对应的指示符(feof或ferror)。返回EOF。

函数使用举例

struct Person                                               
{                                         
  char name[20];                                     
  int age;                                                          
};                                                            
                      
void Testfscanf()    
{    
  // 1、打开前面格式化写入之后的文件    
  FILE* pf = fopen("log.txt", "r");    
  // 2、用fscanf格式化读取文件内容    
  Person p;    
  fscanf(pf, "%s %d", p.name, &p.age);    
  // 3、打印文件中读取出来的数据    
  printf("%s %d\n", p.name, p.age);    
  // 4、关闭文件    
  fclose(pf);    
  pf = NULL;                                                                                                          
}    

编译运行:
在这里插入图片描述


2.4 二进制写入和读取函数 — fwrite 和 fread

一、fwrite

size_t fwrite ( const void* ptr, size_t size, size_t count, FILE* stream );

参数

  • ptr:指向要写入的元素数组的指针。
  • size:每个元素所占字节大小。
  • count:元素个数。
  • stream:文件控制块地址。

功能
在这里插入图片描述
返回值

  • 写入成功返回写入的元素个数count。
  • 如果返回的数字与count不同则写入错误,设置错误指示符(ferror)。如果size或count为零,则函数返回零,错误指示符保持不变。

函数使用举例

void Testfwrite()    
{    
  // 1、以"wb"方式打开文件,写入内容将转化为二进制形式    
  FILE* pf = fopen("log.txt", "wb");    
  // 2、把数组arr的内容以二进制形式写入到文件中    
  int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};    
  fwrite(arr, sizeof(int), sizeof(arr)/sizeof(int), pf);    
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
} 

编译运行后,看到log.txt里都是一些看不懂的二进制乱码。
在这里插入图片描述

二、fread

size_t fread ( void* ptr, size_t size, size_t count, FILE* stream );

参数:和fwrite的一样,就不在重复介绍了,要说差别的话就是第一个参数作为输出型参数要被修改,所以是非const的。

功能:从字节流中读取count个元素,每个元素的大小为size字节,并将它们存储在ptr指定的内存块中。
在这里插入图片描述

返回值

  • 返回成功读取的元素总数count。
  • 如果这个数字与count参数不同,则可能是在读取时发生了读取错误或到达了文件结束。在这两种情况下,都设置了标识符,可以分别用ferror和feof来检查。

函数使用举例

读取刚刚写入到文件里的二进制存储的数据。

void Testfread()      
{                     
  // 1、以"rb"方式打开文件,进行二进制内容的读取    
  FILE* pf = fopen("log.txt", "rb");    
  // 2、读取文件内容到数组arr中    
  int arr[10] = {0};    
  fread(arr, sizeof(int), 10, pf);    
  int i = 0;    
  for(i = 0; i < 10; ++i)    
  {    
    printf("%d ", arr[i]);    
  }                   
  printf("\n");      
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
}

编译运行:
在这里插入图片描述


3. 文件的结束判定

我们在使用读取文件内容的函数时,读取结束有两种特殊的情况,即读取过程中出现错误或读取到文件结束标志,这时都返回相同的值,但它们设置了不同标识符,可以分别用ferror和feof来检查和加以区分读取文件结束的原因。

读取相关的函数返回值
fgetc出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回EOF。
fgets出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回NULL。
fscanf出错设置指示符(ferror),读取到EOF设置标识符(feof)。但都返回EOF。
fread返回的值小于要求读取的完整项的数目时,可能是读取数据时发生错误(设置标识符ferror),也可能是在达到读取的规定数目之前遇到文件结尾(设置标识符feof)。但都返回小于要求读取的完整项的数目。

3.1 ferror

int ferror ( FILE* stream );

只需传入文件控制块地址,函数内部会去检查里面的错误标识符是否为ferror,即检查是否发生错误,若使用时没有发生错误,则ferror函数返回0;否则,ferror函数将返回一个非零的值。
在这里插入图片描述

函数使用举例

if(ferror(pf))
{
	printf("文件读取发生错误而结束")}

3.2 feof

feof函数的功能也是判断使用某一文件指针的过程中,是否读取到文件末尾,若使用时没有读取到文件末尾,则feof函数返回0;否则,feof函数将返回一个非零的值。调用feof函数时,也只需将待检查的文件指针传入即可。

函数使用举例

if (feof(pf))
{
	printf("文件读取到文件末尾而结束");
}

3.3 实际中ferror和feof的配合使用

在实际使用中,读取文件结束后,要配合ferror和feof一起来使用,判断文件是正常结束、发生错误而结束还是读取到文件尾而结束。

void Testfgetc()    
{    
  // 1、用"r"方式打开已经存在的文件,内容为:abcdef                                                                                    
  FILE* pf = fopen("log.txt", "r");    
    
  // 2、使用fgetc依次读取文件的第一个字符
  int ch = 0;    
  ch = getc(pf);  
  // 判断读取情况
  if(ferror(pf))
  {
	 printf("文件读取发生错误而结束")}
  else if(feof(pf))
  {
	 printf("文件读取到文件末尾而结束");
  }
  else 
  {
  	printf("读取字符成功");
  }
  // 3、关闭文件    
  fclose(pf);    
  pf = NULL;    
}  

4. 文件的随机读写

前面我们介绍的顺序读写的函数,只能往后走不能往回走,这样使用起来不是很灵活,比如看下面这个例子:

文件log.txt已经存在,且内容为:abcdef

void Test()    
{    
  FILE* pf = fopen("log.txt", "r");    
  
  // 读取第一个字符,位置指示器向后移动一位    
  int ch = 0;    
  ch = fgetc(pf);    
  printf("%c\n", ch);// a    
  
  // 再次读取一个字符,位置指示器继续往后移动一位    
  ch = fgetc(pf);    
  printf("%c\n", ch);// b   
  
  fclose(pf);    
  pf = NULL;    
} 

读取了两个字符之后,位置指示器移到了字符c的位置,想要再次读取最开始的字符a,有什么办法能让位置指示器回到’a’位置呢?
在这里插入图片描述

4.1 fseek

int fseek ( FILE* stream, long int offset, int origin );

功能:将与字节流关联的位置指示器设置为新位置。

参数

  • stream:文件控制块地址。
  • offset:相较于origin位置,想要偏移的量。
  • origin:偏移前的初始位置(注意并非文件信息区的起始位置),C对origin定义了以下三个常量:
常量含义
SEEK_SET字节流第一个位置
SEEK_CUR字节流所在当前位置
SEEK_END字节流EOF的后一个位置(这个位置是无效的)

返回值:成功返回0,出错返回非0值,并设置错误标识符(ferror)。

函数使用举例

对于开头的例子我们使用fseek有三种方法可以让位置指示器回到最开始的’a’位置。

1.方法一:相对起始位置开始偏移

fseek(pf, 0, SEEK_SET);                                                                        
ch = fgetc(pf);                                                                                
printf("%c\n", ch); //a

2.方法二:相对当前位置开始偏移

fseek(pf, -2, SEEK_CUR);                                                                                            
ch = fgetc(pf);    
printf("%c\n", ch); //a

3.方法三:相对EOF后一个位置开始偏移

fseek(pf, -7, SEEK_END);                                                                                            
ch = fgetc(pf);    
printf("%c\n", ch); //a

4.2 ftell

long int ftell ( FILE* stream );

获得字节流当前的位置相到起始位置的偏移量。只需传入文件控制块地址即可,调用成功返回偏离量,失败返回-1。

函数使用举例

依然是最开始的那个例子,我们读取了两个字符后来带’c’位置,距离最开始的位置偏移了两个字符。

long distance = ftell(pf);    
printf("%ld\n", distance);//2  

4.3 rewind

void rewind ( FILE* stream );

让位置指示器回到最开始的位置,只需传入文件控制块地址即可,无返回值。

函数使用举例

使用rwind让该文件的我只指示器回到最开始’a’的位置。

// 让位置指示器回到最开始位置,即'a'位置
rewind(pf);    
// 直接读取就是'a'
ch = fgetc(pf);    
printf("%c\n", ch);//a                                                                                              
fclose(pf);  
Logo

更多推荐