深入理解C语言(4):文件操作详解


  • 文章主题:文件操作详解🌏
  • 所属专栏:深入理解C语言📔
  • 作者简介:更新有关深入理解C语言知识的博主一枚,记录分享自己对C语言的深入解读。😆
  • 个人主页:[₽]的个人主页🏄🌊

文件操作详解

为什么使用文件

在一般程序运行的情况中,程序结束时(在C语言中就是main函数的结束)所有的数据也就从内存中清空了。无法做到持久性地储存数据。那么有什么方法能够使我们的数据持久化呢?😋但答案是有的。

数据持久化的方法有把数据存放到磁盘文件中、把数据存放到数据库中等。

使用文件我们可以直接使数据存放到电脑的硬盘上,做到了数据的持久化(电脑上的所有文件都是为了数据持久化储存到磁盘上的东西,C文件本身也是文件只不过可以通过编程决定它是在内存中引用非持久化的数据还是在文件等地方引用持久化的数据而已)。


什么是文件

磁盘上的储存的一份数据------文件。

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。------即与该语言设计的程序实现编译、运行时直接相关的文件。

数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件,即使用时需要将其中储存的数据拿出来用但是又不直接参与该程序的编译、运行的只提供数据的文件。

现在我们要讨论的就是数据文件,在以前我们所处理的输入输出的数据都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上的数据。其余的数据则是在程序运行的过程中间歇性申请的,都不属于储存在磁盘中的永久性数据。

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件名包含3部分:文件路径+文件名主干+文件后缀

例如: c:\code\test.txt

为了方便起见,文件标识(即文件名主干+文件后缀)也常被称为文件名。

文件申请的路径:

与从盘符(即某磁盘的符号如:C盘的C)到该文件中经历的路径中各个符号名的集合的文件路径(各符号名的集合用盘符:\经历的各文件夹的符号名\···\)不同

  1. 绝对路径(从盘符出发直到文件标识的集合表示的符号名):盘符:\经历的各文件夹的符号名\···\文件标识(狭义文件名),因为是需要通过符号名确定探查的位置的所以这种情况下符号名是不能够被省略的。
c 复制代码
//C中的表示
" c:\\code\\test.txt"
  1. 相对路径(从当前文件夹到那个文件所要经过的文件夹的集合(这种情况时规定只需用省略号(个数可随意,但至少得大于1,不然在fopen函数内部会报错(函数内部设计了一个断言的形式来规避这种可能会引起函数调用出错或内部逻辑可能会出错的情况))表示其集合,因为无需具体定位直接从当前文件夹往上探即可,但具体探的次数肯定还是得用多少个省略号所表示清楚的),具体的文件标识却写在该集合后):盘符:\经历的各文件夹的符号名\···\文件标识(狭义文件名),'.'可以随意书写个数,但因C语言中为防止和其他的转义字符混淆,\在C语言中被规定只能用'\\'来表示,所有的防止与转义字符之类的混淆的表示方法都是规定只能用这种方法来表示的。
c 复制代码
//C中的表示
" ...\\...\\test.txt"

文件的打开和关闭

文件指针

缓冲文件系统(C语言所设计的一种将外存中的信息转换到内存中进行储存(专门设计用来储存这一系列随文件打开而创造的文件信息的内存(即一系列随文件打开而创建的文件信息区)的区域被叫作文件缓冲区(即文件信息区是文件缓冲区的一部分,文件缓冲区会储存一系列的随文件打开而创造的文件信息区))使程序可以方便直接从内存中的缓冲区读取到文件信息的一种C语言处理文件信息的文件系统,在下文会详细的介绍C语言所设计的文件系统中的缓冲文件系统中用于储存文件信息的文件缓冲区这种内存块)中,关键的概念是"文件类型指针",简称"文件指针"。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。C语言中这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的(通过在<stdio.h>中进行声明的方式来实现的系统声明,因为文件的信息的输入输出也是属于标准信息输入输出的一种,且相对于size_t这种也是系统声明的类型范围较窄(因为只是专门用于输入输出的),所以是采用的在范围偏窄一些的在标准输入输出库进行的声明的这种系统声明形式借助库为媒介来声明的系统声明形式),取名FILE

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型声明:

c 复制代码
struct _iobuf {
       char *_ptr;
       int   _cnt;
       char *_base;
       int   _flag;
       int   _file;
       int   _charbuf;
       int   _bufsiz;
       char *_tmpfname;
       };
typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息, 使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。(即一般都会是通过结构体指针访问成员的形式来实现的维护FILE结构变量的作用)。

下面我们可以创造一个FILE*类型的指针变量:

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

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件(即访问该文件中储存的具体数据)。也就是说,通过文件指针变量能够找到与它关联的文件(即与它关联的文件中的具体数据)。

如:

文件的打开和关闭

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

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向储存该文件相关的各种信息的文件信息区,也相当于建立了指针和文件的关系。

ANSI C^1^规定使用fopen函数来打开文件,fclose来关闭文件。

c 复制代码
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );//所谓流就是指向在内存中储存相关转自外存中的信息的内存块的方便程序读取和写入外存信息的通过指向方便传输储存外存信息的中间内存的形式沟通了外存与内存的指针,

打开方式如下:

文件使用方式 含义 如果指定文件不存在
"r"(只读) 为了输入数据(即从外存向内存(文件信息区)中输入数据),打开一个已经存在的文本文件 出错,即打开失败,打开失败就会返回NULL,下文亦如此
"w"(只写) 为了输出数据(即从内存(文件信息区)向外存中输出数据),打开一个文本文件 建立一个新的文件(即通过创造了一个新文件的形式打开成功,打开成功就会返回指向对应文件信息区的指针/流(FILE*类型变量))
"a"(追加) 向文本文件尾添加数据 建立一个新的文件
"rb"(只读) 为了输入数据,打开一个二进制文件 出错
"wb"(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
"ab"(追加) 向一个二进制文件尾添加数据 出错
"r+"(读写) 为了读和写,打开一个文本文件 出错
"w+"(读写) 为了读和写,建议一个新的文件 建立一个新的文件
"a+"(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
"rb+"(读写) 为了读和写打开一个二进制文件 出错
"wb+"(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
"ab+"(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件
例:
c 复制代码
/* fopen fclose example */
#include <stdio.h>
int main ()
{
 FILE * pFile;
 //打开文件
 pFile = fopen ("myfile.txt","w");
 //文件操作
 if (pFile!=NULL)
{
   fputs ("fopen example",pFile);
   //关闭文件
   fclose (pFile);
}
 return 0;
}

文件的顺序读写

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输入流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输入流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输入流
二进制输入 fread 文件
二进制输出 fwrite 文件

对比一组函数

scanf/fscanf/sscanf:

scanf
c 复制代码
int scanf ( const char * format, ... );

stdin 读取格式化数据

  • stdin 读取数据,并根据参数格式将其存储到附加参数所指的位置。
  • 附加参数应指向已分配的对象,该对象由格式字符串中的相应格式说明符指定。
fscanf
c 复制代码
int fscanf ( FILE * stream, const char * format, ... );

从流中读取格式化的数据(不像 scanf 只能用对应键盘输入的标准输入流 stdin , 所有的流都由你所输入的流参数决定)

  • 从流中读取数据,并根据参数格式将数据存储到其他参数所指向的位置(即根据其格式一模一样的数据所对应的格式说明符所对应的部分获得其参数值)。
  • 附加参数应指向已分配的对象,该对象由格式字符串中的相应格式说明符指定。
sscanf
c 复制代码
int sscanf ( const char * s, const char * format, ...);

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

  • 从 s 读取数据,并根据参数格式将它们存储到附加参数给出的位置,就像使用 scanf 一样,但从 s 读取数据而不是标准输入 (stdin)。
  • 附加参数应指向已分配的对象,该对象由格式字符串中的相应格式说明符指定。

printf/fprintf/sprintf:

printf
c 复制代码
int printf ( const char * format, ... );

将格式化数据打印到 stdout

  • format 指向的 C 字符串写入标准输出 (stdout)。如果 format 包含格式说明符(以 % 开头的子序列(该处即指%开头的字符串中的一个属于该字符串的较独立的可单独理解的有对应字符串之后别的参数功能的格式控制符)),则格式后面的附加参数将格式化并插入到生成的字符串中,以替换它们各自的说明符。
fprintf
c 复制代码
int fprintf ( FILE * stream, const char * format, ... );

将格式化的数据写入流

  • 将按格式指向的 C 字符串写入流。如果 format 包含格式说明符(以 % 开头的子序列),则格式后面的附加参数将格式化并插入到生成的字符串中,以替换它们各自的说明符。
  • 在 format 参数之后,该函数至少需要与 format 指定的其他参数一样多的附加参数。
sprintf
c 复制代码
int sprintf ( char * str, const char * format, ... );

将格式化的数据写入字符串

  • 使用在 printf 上使用 format 时将打印的相同文本组成一个字符串,但内容不是打印,而是作为 C 字符串存储在 str 指向的缓冲区中。
  • 缓冲区的大小应足够大,以包含整个生成的字符串(有关更安全的版本,请参阅snprintf)。
  • '\0会自动追加在内容之后。
  • 在 format 参数之后,该函数至少需要格式所需的其他参数。

文件的随机读写

fseek

根据文件指针的位置和偏移量来定位文件指针。

c 复制代码
int fseek ( FILE * stream, long int offset, int origin );

例:

c 复制代码
/* fseek example */
#include <stdio.h>
int main ()
{
 FILE * pFile;
 pFile = fopen ( "example.txt" , "wb" );
 fputs ( "This is an apple." , pFile );
 fseek ( pFile , 9 , SEEK_SET );
 fputs ( " sam" , pFile );
 fclose ( pFile );
 return 0;
}

ftell

返回文件指针相对于起始位置的偏移量

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

例:

c 复制代码
/* ftell example : getting size of a file */
#include <stdio.h>
int main ()
{
 FILE * pFile;
 long size;
 pFile = fopen ("myfile.txt","rb");
 if (pFile==NULL) perror ("Error opening file");
 else
{
   fseek (pFile, 0, SEEK_END);   // non-portable
   size=ftell (pFile);
   fclose (pFile);
   printf ("Size of myfile.txt: %ld bytes.\n",size);
}
 return 0;
}

rewind

让文件指针的位置回到文件的起始位置

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

例:

c 复制代码
/* rewind example */
#include <stdio.h>
int main ()
{
 int n;
 FILE * pFile;
 char buffer [27];
 pFile = fopen ("myfile.txt","w+");
 for ( n='A' ; n<='Z' ; n++)
   fputc ( n, pFile);
 rewind (pFile);
 fread (buffer,1,26,pFile);
 fclose (pFile);
 buffer[26]='\0';
 puts (buffer);
  return 0;
}

文本文件和二进制文件

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

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

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

一个数据在内存中是怎么存储的呢? 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而

二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

测试结果:

原理图:

例:

c 复制代码
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}

VS中的二进制文件打开方式


文件读取结束的判定

被错误使用的 feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

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

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
    例如:
  • fgetc 判断是否为 EOF .
  • fgets 判断返回值是否为 NULL .
  1. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
  • fread判断返回值是否小于实际要读的个数。

正确的使用:
文本文件的例子:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
   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);
}

二进制文件的例子:

c 复制代码
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
   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");
   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);
}

文件缓冲区

ANSIC 标准采用"缓冲文件系统"处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根

据C编译系统决定的。
文件缓冲区图解

c 复制代码
#include <stdio.h>
#include <windows.h>
//VS2013 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;
}

这里可以得出一个结论 : 因为有缓冲区的存在,C语言在操作文件的时候,需要刷新缓冲区或者在文件操作结束的时候关闭文件。

如果不做,可能导致读写文件的问题。


总结

以上就是对自定义类型的深度解析,😄希望对你的C语言学习有所帮助!作为刚学编程的小白,可能在一些设计逻辑方面有些不足,欢迎评论区进行指正!看都看到这了,点个小小的赞或者关注一下吧(当然三连也可以~),你的支持就是博主更新最大的动力!让我们一起成长,共同进步!


  1. ANSI C是由美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准。ANSI C 主要标准化了现存的实现, 同时增加了一些来自 C++ 的内容 (主要是函数原型) 并支持多国字符集 (包括备受争议的三字符序列)。 ANSI C 标准同时规定了 C 运行期库例程的标准。 ↩︎
相关推荐
数据小爬虫@6 分钟前
如何利用PHP爬虫获取速卖通(AliExpress)商品评论
开发语言·爬虫·php
青い月の魔女24 分钟前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
java1234_小锋1 小时前
MyBatis如何处理延迟加载?
java·开发语言
最后一个bug1 小时前
STM32MP1linux根文件系统目录作用
linux·c语言·arm开发·单片机·嵌入式硬件
FeboReigns1 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns1 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
学前端的小朱1 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
摇光932 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
沐泽Mu2 小时前
嵌入式学习-QT-Day09
开发语言·qt·学习
小猿_002 小时前
C语言实现顺序表详解
c语言·开发语言