本文介绍了C语言中关于文件操作的内容知识,内容较为生涩,没有理解可以多次观看
我们在写代码的过程中,有的时候某一些数据我们是想把它保存下来的,而不是说只有在程序运行的时候,这些数据才能被我们录入或显示出来,我们总会遇到数据持久化的场景,所以这时我们需要将数据保存下来,一般数据持久化的方式有,把数据存到磁盘文件里,或存放到数据库里面
以后我们如果想导出这部分的数据时,只要打开磁盘的文件就行了,就可将数据导入到我们所期望的程序当中
数据是事实或观察的结果,是对客观事物的逻辑归纳,是用于表示客观事物的未经加工的原始素材。数据可以是连续的值,比如声音、图像、温度、压力等都称为模拟数据,也称为模拟量,相对于数字量而言,指的是取值范围是连续的变量或者数据。模拟数据是指在某个区间产生的连续值。当然数据也可以是离散的,如符号、文字称为数字数据
在计算机系统中,数据以二进制信息单元0、1 的形式表示
在计算机科学中,数据是指所有能输入计算机并被计算机程序处理的符号的介质的总称,是用于输入电子计算机进行处理,具有一定意义的数字、字母、符号和模拟量等的通称。计算机存储和处理的对象十分广泛,表示 这些对象的数据也随之变得越来越复杂。
对于逻辑归纳,比如电路,开和关分别对应着二进制数字1和0,对于一个电路,这是一个事实,而对于电路中开路断路的描述,则是通过数据来实现的。这就是逻辑归纳的数据
计算机程序是一组计算机能识别和执行的指令(说的简单点就是代码呗,我们平常敲的计算机能读懂的东西),这种指令运行于电子计算机上,也是满足人们需求的一种信息化工具。
他以某些程序设计语言编写,运行于某种目标结构体系上。
举个栗子,程序就如同以英语(程序设计语言)写作的文章,要让一个懂得英语的人(编译器)同时也会阅读这篇文章的人(结构体系)来阅读、理解、标记这篇文章。一般的,以英语文本为基础的计算机程序要经过编译、链接而成为人难以解读,但可轻易被计算机所解读的数字格式,然后放入计算机内部运行。
数据得到程序指令的处理,才能得到大多数普通人能看懂的数据(网页,app,游戏),数据只有得到程序的处理才有价值,也只有处理之后才能被称为是信息,单纯的数据其实是没有任何的意义的
计算机文件(或称文件、电脑档案、档案),是存储在某种长期储存设备上的一段数据流。所谓“长期储存设备”一般指磁盘、光盘、磁带等。其特点是所存信息可以长期、多次使用,不会因为断电而消失。计算机文件分为文本文件和二进制文件,文本文件仅由字符的串行构成,除此之外的文件都是二进制文件。
在程序设计中,我们所谈的文件,一般有两种,程序文件和数据文件(根据文件功能所划分)
包括源程序文件(后缀为.c),目标文件(windows环境下后缀为.obj),可执行程序(windows环境下是.exe)
文件的内容不是程序指令,而是程序运行时读入和写入的数据,可能包括程序运行需要从中读取数据的文件或输出内容的文件
我们着重讨论数据文件 我们最常见到的就是将数据从标准输入流(键盘)输入,显示到标准输出流(屏幕、也就是显示器)中 其实有时候我们会将数据输出到磁盘文件上,当需要的时候,再将数据从磁盘文件中拿出来,这时我们就必须学会在程序中如何操作文件
讲解文件指针之前,我们先来给大家介绍一下,缓冲文件系统:
ANSI C标准采用“缓冲文件系统”处理数据文件。 所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。 如果从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘中的文件里面去。 如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区 (充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区 (给程序变量) 。
举个栗子: 我们在程序中使用到的文件都会开辟一个文件信息区(存放文件信息),这些存在于系统声明的信息都是由编译器的开发人员(微软员工)定义好类型的,并且该结构体的类型声明为FILE(源码中利用FILE创建了一个_public_file变量)。
每当我们打开一个文件时,系统会根据文件的情况自动创建一个FILE类型的结构体变量,用于填充我们文件情况的信息,我们在使用时,直接使用就好了,不必关心操作系统是如何操作的
所以我们就可以定义一个文件指针变量
FILE*pf; 这个变量可以存放我们打开的磁盘中文件的地址
然后我们就可以通过这个指针,去访问,它所维护的那个文件中所存储的信息了
文件在读写之前应该先打开文件,在我们使用完毕之后要关闭文件
ANSI C规定使用fopen和fclose函数来打开和关闭文件,下面是fopen和函数fclose的介绍
我们打开文件的方式,C语言规定有以下几种:
从介绍中可以读出,fopen是需要两个参数的第一个是我们的文件名,第二个参数是我们打开文件的方式,我们如果只读或只写或追加的话,打开文件的方式由表格可知,有三种,分别是(“r”,“w”,“a”)。而且他的返回值也是一个指向打开这个文件的指针,如果打开失败,他会返回一个空指针,所以在接收fopen函数返回值时,我们一般还要判断返回值是否有效,也就是确定他是否为空指针
举个栗子: 我们先来介绍几个有关写入数据到文件里面的函数
fgetc,fputc分别是字符输入函数和字符输出函数,他们都适用于所有输入流和所有输出流,如果你记不住这几个函数的功能分别是什么,其实将他翻译一下就好了,get character of file和put character of file其实就是从流中读取出一个字符和向流中写入一个字符的功能
int fgetc( FILE *stream );参数是文件指针,也就是你要操作的文件地址 int fputc( int c, FILE *stream );第一个参数是你要写进去的字符(它以ascll码值的形式存储),第二个参数是文件指针,也就是你要操作的文件地址
1.fputc代码展示:
int main()
{
FILE* pfwrite = fopen("test.txt", "w");//以只写的方式打开文件名为test.txt的文件,路径在我们这个工程的目录底下
if (pfwrite == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//写文件,用fputc将字符写到文件流中
else
{
fputc('l', pfwrite);
fputc('o', pfwrite);
fputc('v', pfwrite);
fputc('e', pfwrite);
}
//关闭文件,将指针置为空指针
fclose(pfwrite);
pfwrite == NULL;
return 0;
}
2.fgetc代码展示
int main()
{
FILE* pfread = fopen("test.txt", "r");
if (pfread == NULL)
{
printf("%s", strerror(errno));
}
else
{
printf("%c", fgetc(pfread));
printf("%c", fgetc(pfread));
printf("%c", fgetc(pfread));
printf("%c", fgetc(pfread));
}
//关闭文件,将文件指针置为空指针
fclose(pfread);
pfread == NULL;
return 0;
}
fgets,fputs分别是文本行输入函数和文本行输出函数,他们都适用于所有输入流和所有输出流,get string of file和put string of file,其实就是从一个流中读取字符串和向一个流中写入字符串
int fputs( const char *string, FILE *stream );第一个参数是你要放到文件里面的字符串,第二个参数是你所操作的文件指针 char *fgets( char *string, int n, FILE *stream );第一个参数是你从文件读取出来字符串后,字符串所存储的地方(可以是一个字符数组),第二个参数是你所读取的字符串的最大字符个数,第三个参数是你所操作的文件地址,也就是文件指针
1.fputs代码展示:
int main()
{
FILE* pf = fopen("test.txt", "w");
char buf[1024] = { 0 };
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
else
{
fputs("wyn", pf);
//fgets(buf, 1024, pf);
//printf("%s", buf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.fgets代码展示
int main()
{
FILE* pf = fopen("test.txt", "r");
char buf[1024] = { 0 };
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
else
{
//fputs("wyn", pf);
fgets(buf, 1024, pf);
printf("%s", buf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fscanf,fprintf分别是格式化输入函数和格式化输出函数,他们也适用于所有输入流和所有输出流,我们对scanf和printf是比较熟悉的,我们知道他是一种格式化的输入和输出函数,但我们以前都知道,使用scanf或printf时我们只需要向其中输入数据让printf在显示器里显示出来就可以了,但其实这两个函数只是使用了默认输入流(键盘)和输出流设备(屏幕显示器),scanf: Read formatted data from the standard input stream. printf: Print formatted output to the standard output stream.
而我们现在介绍的这两个函数其实多了一个参数,这个参数就是指向文件的文件指针,我们可以更改这个文件流。
int fprintf( FILE *stream, const char *format [, argument ]…); int printf( const char *format [, argument]… );
通过函数互相的对比,我们可以轻松看出fprintf和fscanf的第二个参数其实就是格式化的形式,我们可以自己去定义这个格式
int fscanf( FILE *stream, const char *format [, argument ]… ); int scanf( const char *format [,argument]… );
1.fprintf代码展示
typedef struct stu
{
int n;
float score;
char arr[10];
}stu;
int main()
{
stu s1 = { 100,3.14f,"iloveyou" };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fprintf(pf, "%d %f %s", s1.n, s1.score, s1.arr);
//我们将信息内容打印到文件流pf里面去
fclose(pf);
pf = NULL;
return 0;
}
原来的printf是把信息打印到标准输出流里面,但现在我们可以通过fprintf将信息打印到文件输出流里面,其实说白了就是,改变了信息的目的地,原来的目的地是显示器,现在的目的地可以是文件
2.fscanf代码展示
typedef struct st
{
int n;
float score;
char arr[10];
}stu;
int main()
{
stu s1 = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fscanf(pf, "%d %f %s", &(s1.n), &(s1.score), s1.arr);
//从文件中将信息输入到我们想要存放的变量里面去,以前是从键盘中将信息输入到变量里面去
fprintf(stdout, "%d %f %s", s1.n, s1.score, s1.arr);
//从标准输出流(屏幕)中将我们存放到变量中的信息打印出来
fclose(pf);
pf = NULL;
return 0;
}
原来的scanf是,把我们从标准输入流设备(键盘),输入的信息存放到我们创建的变量里面去,但现在的fscanf是从文件流里面将我们的信息存放到我们所创建的变量,其实说白了就是改变了信息的来源方式,原先来自于键盘,现在可以来自于文件
fread,fwrite分别是二进制输入函数和二进制输出函数,他们的适用范围只有文件流这一种流,是不包括标准输入和输出流的(这个需要特殊记忆一下)
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );第一个参数是我们从文件中读取数据后,数据要被暂时存放的位置,第二个参数是我们读取的信息的字节大小,第三个参数是要读取的项目整体大小,第四个参数是从哪个文件里读取,我们需要传一个文件的地址 size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );fwrite函数和fread函数只有第一个参数是不一样的,我们这里只给介绍第一个参数,第一个参数是我们要将变量中存放的信息写到文件里面去,我们传过去的是变量的地址
1.fwrite代码展示
typedef struct stu
{
char name[20];
int age;
double score;
}stu;
int main()
{
stu s = { "张三",20,55.9 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fwrite(&s, sizeof(stu), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
注意这里的文件内容是二进制形式的,并不是文本内容,所以我们是看不懂的,如果想看懂特德内容,可以用fread函数将文件中的信息以二进制的形式读取出来方便我们查看
2.fread代码展示
typedef struct stu
{
char name[20];
int age;
double score;
}stu;//注意stu现在是一种类型
int main()
{
stu s = { "张三",20,55.9 };
stu tmp = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fread(&tmp, sizeof(stu), 1, pf);
printf("%s %d %lf", tmp.name, tmp.age, tmp.score);
fclose(pf);
pf = NULL;
return 0;
}
这里通过运行结果可以看出,我们上面的fwrite函数成功将信息以二进制的形式写到文件test.txt里面去了,正因为如此,我们通过二进制读取的方式才能将文件的信息以我们能看懂的方式输出到屏幕上
1.scanf/printf 是针对标准输入流/标准输出流的 一种格式化输入语句和输出语句 2.fscanf/fpirntf 是针对所有输入流/所有输出流的 一种格式化输入语句和输出语句(包含文件输入流和文件输出流) 3.sscanf/sprintf 是从字符串中读取格式化的数据 是把格式化的数据存储到(输出成)字符串
这里我们看一下,sscanf和sprintf这两个函数的声明介绍
int sscanf(const char* buffer, const char* format[, argument] …); int sprintf(char* buffer, const char* format[, argument] …);
通过观察我们可以看到,其实这两个函数与最初的scanf和printf相比,只是多了一个要传缓冲区地址的这么一个参数
typedef struct stu
{
int n;
float score;
char arr[10];
}stu;
int main()
{
stu s = { 1024,3.14,"love" };
stu tmp = { 0 };
char buf[1024] = { 0 };
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
//原来是输出到屏幕上,现在我们将一组格式化的数据输出到(存储到)字符串数组buf里面
printf("%s\n", buf);
//将格式化的数据转换成字符串存储到buf里面,我们在打印到stdout中
sscanf(buf, "%d %f %s", &(tmp.n), &(tmp.score), tmp.arr);
//原来是从键盘这种标准输入流中将信息输入到我们想要放到的变量 里面
//现在我们从buf这种输入流,将信息输入到我们想要的变量(结构体tmp)里面
printf("%d %f %s\n", tmp.n, tmp.score, tmp.arr);
return 0;
}
通过代码运行结果可以看出,我们成功将一组格式化的数据写到字符数组里面,也成功的将字符串数组里面的内容拿出来,存放到我们想让他存在的变量里面,其实就是将原来的scanf和printf的功能扩大了一个使用的范围
根据数据的组织形式,我们将数据文件分为文本文件和二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,那就是二进制文件,如果我们将数据的形式转换为ascll码的话,那就是文本文件
值得注意的是,字符在内存中均以ascll码的形式存储,数值型数据既可以用ascll形式存储,也可以用二进制形式存储
当用ascll形式存储时,我们会将10000看成5个字符,我们将这5个字符所对应的ascll码值,存储到内存里面 当用二进制形式存储时,10000其实就是个整型,我们将它按照4字节32比特位存储即可
前面给大家介绍的fgetc,fputc,fgets,fputs,fscanf,fprintf,fread,fwrite等操作文件的函数,其返回的指针是有统一顺序的,他的文件指针都是指向文件内容的第一个信息数据的地址的。
这里我们给大家介绍一下,能够改变文件指针指向位置的三个函数
函数具体功能实现:
int main()
{
FILE* pf = fopen("test.txt", "r");//以只读的方式打开文件test.txt
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
//1.定位文件指针
fseek(pf, -2, SEEK_END);
//2.读取文件
int ch = fgetc(pf);//从pf指向的文件中读取内容
printf("%c", ch);
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里的fseek可以调整指针位置,以当前位置为起点,移动特定的偏移量到我们想要的位置
函数具体功能实现:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
int ch = fgetc(pf);//从pf指向的文件中读取第一个字符,相应的指针位置相对于起始量偏移了1
int h = fgetc(pf);//偏移了2
int pos = ftell(pf);//所以这里的ftell返回值应该是2
printf("%d\n", pos);//结果应为2
return 0;
}
我们这里的ftell函数作用起始就是返回当前文件指针相对于起始位置的偏移量
函数功能实现:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
}
int ch = fgetc(pf);
int h = fgetc(pf);
int pos = ftell(pf);
printf("%d\n", pos);//结果为2
rewind(pf);//我们这里让文件位置重新回到起始位置
pos = ftell(pf);//然后再用ftell返回当前文件指针的偏移量大小
printf("%d\n", pos);//返回到起始位置后,结果为0
return 0;
}
所以这个函数功能也是比较简单的,就是重置我们的文件指针指向的位置,让他回到起始位置
Tests for end-of-file on a stream.这个是feof的函数介绍,我们可以知道,这个函数是用来测试文件结束形式的。所以这个函数不是用来判断文件是否结束的,而是用来判定文件是如何结束的,到底是因为读取到\0结束的?还是因为其他原因导致文件读取错误,而导致文件结束的?
所以很多人看到feof(end of file)时会把他认为成一个判断文件是否结束的函数,但其实不是这样的The feof function returns a nonzero value after the first read operation that attempts to read past the end of the file. It returns 0 if the current position is not end of file. There is no error return.通过这里的feof函数的返回值介绍(如果当前位置不是文件末尾则返回一个0(有可能发生了读取文件错误),如果成功读取结束的话,将返回一个非0值),我们就可以明白了,这个函数的确是用来测试文件读取结束的方式的,既有可能是读取错误导致的结束,也有可能是读到文件末尾,从而导致的读取结束
perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到 标 准 错误 (stderr) 。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno 的值来决定要输出的字符串。 在库函数中有个error变量,每个error值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了error的值。perror函数只是将你输入的一些信息和现在的error所对应的错误一起输出。
1.ferror(fault error)的功能: 测试流上的错误。如果流上没有发生错误,ferror返回0。否则,它返回一个非零值。 2.perror的功能: 直接打印错误信息(里面包含我们所输入的信息和错误码所对应的信息一并打印出来) 3.strerror的功能: 把错误码对应的错误信息的字符串地址返回(配合errno使用,errno是一个全局变量,当出现错误时,errno会对应一个库中错误信息对应的错误码,然后我们再用strerror打印这个错误码对应的错误信息)
文本文件的例子:
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) //fp是空指针的时候,进入到if语句,输出错误信息
{
perror("File opening failed");
return ;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
//这里循环的条件就是,读取的字符不是EOF,一直将字符输出
putchar(c);
}
//文件读取之后结束了,然后判断是什么原因结束的
if (ferror(fp))//发生错误,返回一个非0值,进入if语句,打印读取失败的信息
puts("I/O error when reading");
//在读取文件时,input或output发生错误
else if (feof(fp))
//如果函数feof返回非0值,说明是遇到了EOF结束的,如果当前的位置不是文件末尾,则返回0,也就不是遇到EOF
puts("End of file reached successfully");
fclose(fp);
}
自己实现相应的代码,加深理解
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
putchar(ch);
}
printf("\n");
//跳出循环,判断是怎么结束的???
if (ferror(pf))//是读取失败结束???
{
printf("error\n");
}
else if (feof(pf))//还是遇到文件尾结束???
{
printf("end of file\n");
}
fclose(pf);
pf = NULL;
return 0;
}
总结:所以ferror和feof要配合使用,以此来判断是读取失败结束还是遇到文件尾结束
ferror如果检测没有错误返回0 有错误返回非0值
feof如果没到文件尾的话他会返回一个0,到达文件尾返回一个非0值
//二进制文件例子:
#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 的数组到pf流里面去
fclose(fp);//然后关闭文件
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 将从fp流中读取的数据放到double数组b里面(以二进制的形式读出来)
if (ret_code == SIZE)
//如果返回值等于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))
//(到达文件尾返回一个非0值进入if语句,但它并非我是所预取的文件结束位置,虽然我的预期有可能是错误的,但你返回值<SIZE,我认为这就是unexpected的)
{
printf("Error reading test.bin: unexpected end of file\n");
//提前阅读文件结束,导致返回值小于SIZE,这时unexpected end of file非预期文件结束
}
else if (ferror(fp))
{
perror("Error reading test.bin");//读取错误导致文件读取结束
}
}
fclose(fp);
}
(1)文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets) 例如: fgetc判断是否为EOF(fgetc返回读取为int的字符,或返回EOF以指示错误或文件结束)
fgets判断是否为NULL(返回NULL表示错误或文件结束条件,使用feof或ferror来确定是否发生了错误)
(2)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数 例如:fread判断返回值是否小于实际要读的个数 (Fread返回实际读取的完整项数,如果发生错误或在达到count之前遇到文件结束,则该数可能小于count)