深入C语言:文件操作实现局外影响程序

简介: 深入C语言:文件操作实现局外影响程序

一、什么是文件

文件其实是指一组相关数据的有序集合。这个数据集有一个名称,叫做文件名。文件通常是驻留在外部介质(如磁盘等)上的,在使用时才调入内存中来。


文件一般讲两种:程序文件和数据文件:


  • 程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
  • 数据文件:包括程序运行时所读写的数据。本篇所涉及的就是数据文件。


二、文件的使用

文件的操作一般分三步:1.打开文件;2.读/写;3.关闭文件;


三、文件的打开与关闭

3.1 流与标准流

在C语言中,“流”(stream)是一种用于输入和输出数据的抽象概念,我们可以把流想象成流淌着字符的河。它是一种数据的传输方式,可以将数据从一个地方传送到另一个地方。在C语言中,输入流和输出流是通过一组标准库函数来实现的,这些函数允许程序从键盘或文件中读取数据,或者将数据写入到屏幕或文件中。


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


C语言中的流可以分为标准流(standard streams)和文件流(file streams)


注:C语言中操作流的主要函数是标准I/O库中的stdio.h头文件中定义的函数。


3.1.1 标准流

我们需要清楚,C语言程序,只要运行起来,就会默认打开3个流(标准流)

标准输入流(stdin):用于读取输入数据,默认情况下是键盘输入。

标准输出流(stdout):用于向终端或命令行窗口输出数据。

标准错误流(stderr):用于输出错误信息。


3.1.2 文件流

C语言中的文件流是一种用于在程序中读取和写入文件的流。通过文件流,可以在C程序中打开文件,从文件中读取数据或将数据写入文件中。这样可以有效地处理大量数据、持久性存储以及与文件系统的交互。


本次,我们重点讨论文件流

3.2 文件指针

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


文件指针的使用方式:

FILE* pf;


定义一个文件指针变量pf,它可以指向某个文件的文件信息区,通过其即可访问到该文件。


3.3 文件的打开与关闭

⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件,在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。


ANSIC规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。

FILE * fopen ( const char * filename, const char * mode );  //打开文件
int fclose ( FILE * stream ); //关闭文件


3.3.1 文件的访问方式

文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(追加) 为了输出数据,打开一个文本文件(清空原有数据) 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件(清空原有数据) 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 建立一个新的文件
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,创建一个新的文件(清空原有数据) 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写新建一个二进制文件(清空原有数据) 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件


注1:当文件打开失败出错时,会返回一个空指针,因此我们一定要在打开文件之后,对文件指针进行有效性检查

注2:对于打开进行更新的文件(包含“+”号的文件),允许输入和输出操作,在写入操作之后的读取操作之前,应刷新(fflush)或重新定位流(fseek,fsetpos,rewind)。流应在读取操作之后的写入操作之前重新定位(fseek、fsetpos、rewind)(只要该操作未到达文件末尾)


3.4 文件的使用方式

代码示例如下:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
 
int main()
{
    //此时该路径下没有名为data.txt的文件,因此会打开失败
  FILE* fp = fopen("data.txt", "r");
  if (NULL == fp)
  {
    perror("fopen");
    return 1;
  }
 
  fclose(fp);
  fp = NULL;
 
  return;
}


#include<stdio.h>
 
int main()
{
    //用写的方式打开文件,如果文件不存在,会在该路径底下创建一个新的名为data.txt的文件
  FILE* fp = fopen("data.txt", "w");
  if (NULL == fp)
  {
    perror("fopen");
    return 1;
  }
 
  fclose(fp);
  fp = NULL;
 
  return;
}


四、文件的顺序读写

顺序读写的函数:

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


4.1 单字符输入输出

(一)fputc函数

#include<stdio.h>
int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "w");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
    //将abc放进文件
  fputc('a', pf);
  fputc('b', pf);
  fputc('c', pf);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}

(二)fgetc函数

#include<stdio.h>
int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "r");//只读
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fputc('a', pf);
  fputc('b', pf);
  fputc('c', pf);
  int ch = fgetc(pf);
  printf("读出来的字符为:%c\n", ch);
  ch = fgetc(pf);
  printf("读出来的字符为:%c\n", ch);
  ch = fgetc(pf);
  printf("读出来的字符为:%c\n", ch);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}

4.2 文本行输入输出

(一)fputs函数

#include<stdio.h>
int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "w");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fputs("hello betty", pf);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


(二)fgets函数

1.声明:char *fgets(char *str, int n, FILE *stream)

  1. str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
  2. n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  3. stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

2.作用:从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

3.返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。


#include<stdio.h>
int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fputs("hello betty", pf);
  char arr[] = "##########";
  fgets(arr, 5, pf);
  puts(arr);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}

为什么只有四个字符呢 ,我们打开调试,会发现/0也要算一个字符

4.3 格式化输入输出

(一)fscanf函数

typedef struct student {
  char name[20];
  int height;
  float score;
}stu;
int main()
{
  stu s = { "beidi", 170, 95.0 };
  //写文件
  FILE* pf = fopen("test.txt", "w");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fprintf(pf, "%s %d %f", s.name, s.height, s.score);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


(二)fprintf函数

typedef struct student {
  char name[20];
  int height;
  float score;
}stu;
int main()
{
  stu s = { "beidi", 170, 95.0 };
  //写文件
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fscanf(pf, "%s %d %f", s.name, &(s.height), &(s.score));
  printf("%s %d %f", s.name, s.height, s.score);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


4.4 二进制输入输出

(一)fread函数

typedef struct student {
  char name[20];
  int height;
  float score;
}stu;
int main()
{
  stu s = { "beidi", 170, 95.0 };
  //写文件
  FILE* pf = fopen("test.txt", "wb");//二进制写入
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fwrite(&s, sizeof(s), 1, pf);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


二进制语言人类无法识别,但电脑能准确读取

(二)fwrite函数

typedef struct student {
  char name[20];
  int height;
  float score;
}stu;
int main()
{
  stu s = {0};
  //写文件
  FILE* pf = fopen("test.txt", "rb");//二进制写出
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fread(&s, sizeof(s), 1, pf);
  printf("%s %d %f", s.name, s. height, s.score);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


4.5 补充

sscanf和sprintf函数

typedef struct student {
  char name[20];
  int height;
  float score;
}stu;
 
int main()
{
  char buf[100] = { 0 };
  stu s = { "betty", 170, 95.0f };
  stu tmp = { 0 };
  //将这个结构体的成员转化为字符串
  sprintf(buf, "%s %d %f", s.name, s.height, s.score);
  printf("%s\n", buf);
  //将这个字符串中内容还原为一个结构体数据呢
  sscanf(buf, "%s %d %f", tmp.name, &(tmp.height), &(tmp.score));
  printf("%s %d %f", tmp.name, tmp.height, tmp.score);
  return 0;
}


五、文件的随机读写

所谓的随机读写,其实就是指定我们想要读写的位置


5.1 fseer()函数

  • 该函数可以从定位位置的偏移量处开始读写;
  • int fseek( FILE *stream, long offset, int origin );

                    文件流          偏移量    起始位置

  • 返回值:
  1. 如果成功,fseek返回0;
  2. 否则,它返回一个非零值;
  3. 在无法查找的设备上,返回值未定义;      


使用实例:

int main()
{
  //打开文件
  FILE* pf = fopen("test.txt", "r");
  if (pf == NULL)
  {
    perror(" fopen fail");
    return 1;
  }
  fseek(pf, 4, SEEK_SET);
  //从起始位置偏移四个字节
  int ch1 = fgetc(pf);
  printf("%c ", ch1);
  fseek(pf, -3, SEEK_END);
  //从结束位置偏移七个个字节
         int ch2 = fgetc(pf);
  printf("%c ", ch2);
  fseek(pf, 1, SEEK_CUR);
  //从当前位置偏移一个字节
  int ch3 = fgetc(pf);
  printf("%c ", ch3);
  //关闭文件
  fclose(pf);
  pf = NULL;//防止野指针
  return 0;
}


5.2 ftell()函数


  1. 头文件:#include<stdio.h>
  2. 声明:long int ftell(FILE *stream)
  1. stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  1. 作用:返回⽂件指针相对于起始位置的偏移量
  2. 返回值:该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。


我们可以利用fseek和ftell来计算文件的长度(不包含’\0’),下列是代码示例

int main()
{
  FILE* pFile;
  long size;
  pFile = fopen("test.txt", "rb");
  if (pFile == NULL) 
    perror("Error opening file");
  else
  {
    fseek(pFile, 0, SEEK_END); //non-portable
    size = ftell(pFile);
    fclose(pFile);
    printf("文件长度为: %ld bytes.\n", size);
  }
  return 0;
}


5.3 rewind()函数

  1. 头文件:#include<stdio.h>
  2. 声明:void rewind(FILE *stream)
  1. stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流
  1. 作用:让⽂件指针的位置回到⽂件的起始位置
  2. 返回值:该函数不返回任何值。


rewind常常在文件读与写同时使用时,以方便文件读取。下面是rewind的具体使用实例:

#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);//放入26个字母
  rewind(pFile);//回到起始位置,方便读取
  fread(buffer, 1, 26, pFile);//读取·
  fclose(pFile);
  buffer[26] = '\0';//字符串的结束标识
  printf(buffer);
  return 0;
}


六、二进制文件与文本文件

我们知道数据在内存中是以二进制形式存储的,对于文件而言:如果不加转换直接输出到外存就是二进制文件;如果要在外存上以ASCII码形式存储,就需要提前转换最后以ASCII码值形式存储的文件就是文本文件。


对于字符,一律使用ASCII码形式存储,但对于数值型数据,即可以使用ASCII码存储也可以使用二进制形式存储。

举例:

数字10000的两种存储形式:

二进制文件:

文本文件:

首先将10000分成'1','0','0','0','0', 这五个字符,用每个字符对应的ASCII码值进行转换:


显而易见,二进制文件存储和文本文件存储对不同范围的数字可以做到节省空间。


七、文件读取结束判定

feof()函数:该函数被许多人错误用来判断文件是否读取结束,其实它的作用是判断文件读取结束的原因;


文件读取结束有两种情况:1.读取过程中出现异常; 2.读取到文件末尾;

要找出文件读取是哪个原因,就分为以下情况:

文本文件:

  • 如果用 fgetc() 读取,要判断 feof() 的返回值是否为EOF;
  • 如果用 fgets() 读取,要判断 feof() 的返回值是否为NULL(0);


二进制文件:

       都是使用 fread() 读取,要判断其返回值与指定读取个数的大小,如果小于实际要读的个数,就说明发生读取异常,如果等于实际要读的个数,就说明是因读取成功而结束;

对于读取异常的判断,我们考虑判断 ferror() 函数的返回值:

  1. 若ferrror()为真——异常读取而结束;
  2. 若feof()为真——正常读取到尾而结束;


对文本文件的判断:

#include<stdio.h>
#include<string.h>
#include<errno.h>
 
int main()
{
  FILE* pf = fopen("test.txt", "r");
 
  if (pf == NULL)
  {
    perror("fopen is failed !");
    return;
  }
  int c = 0;
  //由于要检查EOF——EOF本质是0——所以是int
  while (c = fgetc(pf) != EOF)
  {
    putchar(c);
  }
  //直到while不执行了—读取结束了—判断是什么原因结束的
  if (ferror(pf))
  {
    printf("读取中出现错误\n");
  }
  else if (feof(pf))
  {
    printf("读取到文件尾\n");
  }
 
  fclose(pf);
  pf = NULL;
 
  return 0;
}

对二进制文件的判断:

#include<stdio.h>
#include<string.h>
#include<errno.h>
 
int main()
{
  FILE* pf = fopen("test.txt", "rb");
  int arr[5] = { 0 };
 
  if (pf == NULL)
  {
    return;
  }
 
  size_t num = fread(arr, sizeof(int), 5, pf);
 
  if (num == 5)
  {
    //说明全部读取成功
    printf("Array read successfully\n");
  }
  else
  {
    //说明读取不够指定长度—判断是什么原因
    if (ferror(pf))
    {
      printf("读取中出现错误\n");
    }
    else if (feof(pf))
    {
      printf("读取到文件尾\n");
    }
  }
 
  fclose(pf);
  pf = NULL;
 
  return 0;
}

八、文件缓冲区

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

  • 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。
  • 如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
  • 缓冲区的⼤⼩根据C编译系统决定的。
include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
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语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件的问题。

相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析