目录
本篇文章将记录文件的相关操作知识,正文开始:
1.什么是文件
磁盘(硬盘)里文件是文件,文件按功能分类分为数据文件,程序文件。
1.1程序文件:包括源程序文件(后缀.c),目标文件(windows环境下后缀.obj),可执行程序(windows环境下后缀.exe)。
1.2数据文件:数据文件里不一定是程序,而是程序运行时读写的数据,比如程序运行时需要从文件里读数据,或者向文件里写数据。
本篇文章讨论的是数据文件,之前我们处理数据的输入输出都是以终端为对象的,即从终端的键盘输入,结果直接显示在显示器上。其实我们有时会把数据输入到硬盘里,当需要的时候再从硬盘里读取数据到内存里,这里处理的就是硬盘上文件
1.3文件名:文件要有唯一的一个文件标识,文件标识由三部分组成:文件路径+文件名主干+文件后缀,比如c:\code\test.txt ,为了方便起见,文件标识常被称为文件名。
2.二进制文件和文本文件
根据数据的组成形式,数据文件被分为文本文件和二进制文件。 数据在内存中以二进制的形式存储,如果不加改变的输入到外存的文件中,这个文件就是二进制文件。 如果要求转换成ASCII形式存储在外存,则需要在存储前转换。以ASCII形式存储的文件就是文本文件。 那么数据在文件中是怎样存储的呢? 字符一律使用ASCII形式存储,数值型可以用ASCII形式,也可以用二进制形式存储。这里并没有说明哪种形式会更节省空间就比如存储10000,用ASLII形式需要5个字节空间,每个字符占一个字节空间,用二进制形式只用四个字节空间(整形)。
#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上打开二进制文件的方法:
10000在二进制文件里(10 27 00 00)(16进制小端)
3.为什么要使用文件
如果不使用文件,我们的数据就只能存储在内存里,但是程序运行结束内存就要被回收,数据就会丢失,下次再启动程序,啥都没有了,为了使数据可以持久化的保存,我们要使用文件。
4.文件的打开和关闭
4.1流和标准流
4.1.1流
我们程序的数据会向各种外部设备输出,同时也会从各种外部设备获取数据,但不同的设备输入输出操作不同会很麻烦,这时又有聪明的大佬开始操作了,为了方便程序员对各种设备的操作,他们抽象出了流的概念,我们可以把流想象成流淌着字符的河。C程序针对画面,文件,键盘等的数据的输入输出都是通过流操作的。一般来说我们想向流里写数据,或者读数据,都要先打开流,再操作。
4.1.2标准流
那为什么我们之前运行程序都没感觉到流的存在,而是直接显示结果或直接键盘输入数据了呢? 那是因为C语言程序在启动的时候默认打开了3个流: - stdin -标准输入流,在大多数的环境里从键盘输入,scanf函数就是从标准输入流里读取数据。 - stdout -标准输出流,大多数环境里输出到显示器界面,printf函数就是将数据输出到标准输出流里。 - stderr -标准错误流,大多数环境里输出到显示器界面。 这就是被默认打开的3个流,我们可以使用scanf,printf等函数直接进行输入输出操作,stdin,stdout,stderr3个流的类型是FILE *,通常称为文件指针。在C语言里,就是通过文件指针来维护流的各种操作的。
4.2文件指针
在缓冲文件系统中,重要的概念是文件类型指针,简称文件指针。 在一个文件被使用时,内存会为文件开辟一块文件信息区,这里面会存放文件的名称,文件的状态等信息。这些信息被保存在一个结构体变量里,该结构体类型是由系统声明的,取名为FILE。 比如在VS2013里,stdio.h头文件里有文件类型的申明。
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* pf;//⽂件指针变量
定义pf是一个指向FILE类型的指针变量,可以让它指向一个文件的文件信息区(是一个结构体变量)。通过该文件信息区里的信息就能访问该文件,也就是说可以通过文件指针变量间接访问与之相关联的文件。
4.3文件的打开与关闭
文件在使用之前应该打开文件,在使用完后应该关闭文件。在编写文件时和打开文件时,都会返回一个指向该文件的FILE*的指针变量,相当于建立了文件与指针的关系。 ANSI C规定了fopen函数用来打开文件,fclose函数用来关闭函数。
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );
mode表示文件的打开方式,以下都是文件的打开方式(这里就只说五个常用的):
文件使用方式 含义 出错
r(只读) 为了输⼊数据,打开⼀个已经存在的⽂本⽂件 出错
“w”(只写) 为了输出数据,打开⼀个⽂本⽂件 建⽴⼀个新的⽂件
“a”(追加) 向⽂本⽂件尾添加数据 建⽴⼀个新的⽂件
“rb”(只读) 为了输⼊数据,打开⼀个⼆进制⽂件 出错
“wb”(只写) 为了输出数据,打开⼀个⼆进制⽂件 建⽴⼀个新的⽂件
相关代码:
/* 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;
}
5.文件的顺序读取
5.1顺序读取相关函数介绍
函数名 功能 适⽤于
fgetc 字符输⼊函数 所有输⼊流
fputc 字符输出函数 所有输出流
fgets ⽂本⾏输⼊函数 所有输⼊流
fputs ⽂本⾏输出函数 所有输出流
fscanf 格式化输⼊函数 所有输⼊流
fprintf 格式化输出函数 所有输出流
fread ⼆进制输⼊ ⽂件输⼊流
fwrite ⼆进制输出 ⽂件输出流
上面说的适用于所有输入/输出流包括标准输入/输出流和其他输入/输出流(文件输出/输入流)。
5.2对比一组函数
scanf/fscanf/sscanf printf/fprintf/sprintf
6.文件的随机读取
6.1 fseek
根据文件指针的位置以及偏移量来定位文件指针(就是文件内容里光标的位置)。
int fseek ( FILE * stream, long int offset, int origin );
例子:
/* 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;
}
6.2 ftell
返回文件指针相较于起始位置的偏移量。
long int ftell ( FILE * stream );
例子:
/* 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;
}
6.3 rewind
让文件指针回到起始位置。
void rewind ( FILE * stream );
例子:
/* 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';
printf(buffer);
return 0;
}
7.文件读取结束的判定
7.1被错误使用的feof
需要牢记:在文件读取的过程里,不可以用feof的返回值来直接判断文件是否结束。 feof的作用是在文件读取结束后,判断文件读取结束的原因是否是:遇到文件尾结束。
1.文本文件读取是否结束,用fgetc函数读取的话就判断返回值是不是EOF,用fgets函数读取的话就判断返回值是不是NULL。 2.二进制文件读取是否结束,就判断返回值是否小于实际要读的个数 。
文本文件的例子:
#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);
}
ferror函数是会检测错误标记,我们前面读取过程中如果因为读取的方式不对,或者出现了一些I/O相关的错误,那么它就会涉及到一个错误的标记,ferror就会检测到,返回非零值。
二进制文件的例子:
#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);
}
8.文件缓冲区
ANSI C标准是使用“缓冲文件系统”处理数据文件的,缓冲文件系统意思是系统自动地在内存里为程序里所有正在使用的文件开辟一个文件缓冲区。从内存向磁盘输出数据时,数据会先送到缓冲区,缓冲区存满了后会一起送到磁盘上。从磁盘向内存输入数据时,会先送到缓冲区,缓冲区存满后再逐个送到程序数据区。缓冲区的大小是根据C编译系统决定的。
#include <stdio.h>
#include <windows.h>
//VS2022 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;
}
由上面的程序我们可以得到一个结论:因为有缓冲区的原因,我们需要刷新缓冲区或者关闭文件(以此来刷新缓冲区,因为前面说了缓冲区满了才会一起送走,那么最后总会还剩一点,刷新缓冲区时才能输出缓冲区的数据到文件(磁盘))。如果不做,可能会出现读写文件的问题。
这篇文件操作的笔记到这里就告一段落啦,希望大佬们能三连(含蓄的笑容)。