文件操作
文件的相关概念
先看下面的代码:
#include<stdio.h>
int main()
{
int a = 0;
printf("%d\n", a);
scanf("%d", &a);
//这里有一个bug,我在%d 后面加了个' '
//"%d " 读取数字后跳过所有后续空白字符,直到遇到非空白字符才结束 几乎永远不要用
printf("%d\n", a);
return 0;
}
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
注意数据不是永久化的保存:有时会硬盘/磁盘损坏,导致数据的丢失
什么是文件?
磁盘(硬盘)上的文件是文件。
程序文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows
环境后缀为.exe)。
数据文件
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
我们在.c文件中想要打印数据到屏幕上,这个过程叫做写;
我们想要通过终端的键盘向.c 文件中输入一个数的过程叫做输入/读
上面的程序文件和数据文件的相互转换过程也是一样的
我们这里讨论的文件都是关于数据文件的
处理数据的输⼊输出都是以终端为对象的,这里的文件就是终端的一种
文件名
文件名的作用:⼀个文件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。
以 C:\code\test.txt 为例
⽂件名包含3部分:⽂件路径 + ⽂件名主⼲+ ⽂件后缀
c:\code\ test .txt
人们常将 "名称主干 + 扩展名" 合称为 "文件名"
(如 test.txt),忽略路径部分
文件标识常被称为文件名。
二进制文件和文本文件
根据数据的组织(也叫数据的内容)形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制⽂件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
数据在文件中的存储:
字符一律以ASCII形式存储,
数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
以整数10000为例:
使用该代码在VS上进行测试:
#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默认打开的是文本文件,我们在VS上打开二进制文件的方法,如图:
这是10000的二进制文件
文件的打开和关闭
流和标准流
程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都需要进行以下三个步骤:
1.打开流 2. 读/写 3. 关闭流
标准流
为什么我们从键盘输入数据,向屏幕上输出数据,没有打开流呢?
因为C语⾔程序在启动的时候,默认打开了3个流:
• stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
• stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出
流中。
• stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE *** ,通常称为文件指针**。
在C语言中,我们是通过文件指针对流进行各种操作的
文件指针
“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,⽤来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名 FILE,这样我们就可找到文件并且访问它
不同的C编译器的FILE类型包含的内容大同小异
我们平时打开文件时系统的操作:
每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息
我们创建⼀个FILE*的指针变量 pf,它是⼀个指向FILE类型数据的指针变量,让pf指向某个文件的文件信息区(是⼀个结构体变量)。通过该文件信息区中的信息就能够访问该文件。
通过文件指针变量能够间接找到与它关联的文件。
我们可以通过流程图更好的了解这个过程:
文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C 规定使⽤ fopen 函数来打开文件, fclose 来关闭文件。
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );
mode表示文件的打开模式,文件的打开模式如下图:
下面是具体的实操环节:
#include<stdio.h>
int main()
{
//打开文件
// 打开文件成功的话,返回的是有效的指针
//// //如果打开失败,则返回NULL
FILE* pc = fopen("test.txt", "w");
//FILE* pc = fopen("test.txt", "r");
//后面其他的模式是类似的
//如果将该文件放在桌面上的话,打开它时需要打开文件所在的路径
//FILE* pc = fopen("C:\\Users\\PC\\Desktop\\应用中心\\test.txt", "w");
//FILE* pc = fopen("test.txt", "w");
//上面的被称为绝对路径
// . 表示当前路径 ..表示上一个路径
//如果该文件在该路径的上一个路径中,则使用..
//FILE* pc = fopen(".\\.\\..\\test.txt", "w");
//这里为了方便操作,就将文件放在绝对路径下
//需要判断,万一这里是NULL呢
if (pc == NULL)
{
perror("fopen");
return 1;
}
//使用
//....
//关闭文件
fclose(pc);
pc = NULL;//防止成为野指针
return 0;
}
文件的顺序读写
顺序读写函数
fputc
该函数的功能:将单个字符写入到文件或标准输出中
用这个函数能够将单个字符输出到屏幕(等价于putchar)
int fputc ( int character, FILE * stream );
character:这里放的是你要写入的字符,它是以ASCII码的形式传递的
stream:FILE*是文件指针指向的文件,在这里是将文件的地址传给它
该函数的返回值:
如果该函数返回成功,返回的是写入的字符(转换为unsigned char后作为 int 返回)
返回失败:返回EOF (通常是-1)
实例:在文件中以只写的形式打印(使用fput)26个字母
#include<stdio.h>
int main()
{
//打开文件
FILE* pa = fopen("test.txt", "w");
if (pa == NULL)
{
perror("fopen");
return 1;
}
//使用
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fput(i, pa);
}
//关闭文件
fclose(pa);
pa = NULL;
return 0;
}
fgetc
该函数的功能是:从文件中读取单个字符到程序中。
int fgetc(FILE *stream);
参数stream:内容是一个指向文件的指针或者使用stdin在屏幕上进行读取,与stdout配套使用
返回值:如果函数成功返回,返回的是它读取到的字符;如果失败,第一种情况:该函数读取到末尾了,遇到EOF,可以使用feof函数进行判断是否是这个;第二种原因:在读的过程中遇到错误了,可以使用ferror进行判断错误原因是否为这个
实例:读取test.txt文件中的单个字符
前提条件:需要创建一个文件在里面输入 abcdef
#include<stdio.h>
int main()
{
FILE* pv = fopen("test.txt", "r");
if (pv == NULL)
{
perror("fopen");
return 1;
}
char ch = 'm';
ch = fgetc(pv);
printf("%c ", ch);
//a
ch = fgetc(pv);
printf("%c ", ch);
//b
ch = fgetc(pv);
printf("%c ", ch);
//c
ch = fgetc(pv);
printf("%c ", ch);
//d
ch = fgetc(pv);
printf("%c ", ch);
//e
ch = fgetc(pv);
printf("%c ", ch);
//f
fclose(pv);
pv = NULL;
return 0;
}
这里可以用循环的方式达到多次读取
//这里可以用循环的方式达到多次读取
#include<stdio.h>
int main()
{
FILE* pv = fopen("test.txt", "r");
if (pv == NULL)
{
perror("fopen");
return 1;
}
int ch = 0;
while ((ch = fgetc(pv)) != EOF)
{
//ch = fgetc(pv);这里应该没有这行代码,循环的判断条件已经读取过一遍了,
// 再加上这行代码就形成了二次读取打印时就打印第二次读取的数据了
printf("%c ", ch);
}
fclose(pv);
pv = NULL;
return 0;
}
fgetc必须配 “r”(只读) fputc必须配 “w” (只写)
fputs
该函数的作用:将字符串写入到流(文件) 中去
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
终端就是一个文本输入输出设备,在计算机中就是键盘,屏幕等等
函数表达式:
int fputs ( const char * str, FILE * stream );
str:要写入流/文件的字符串
stream:是一个指向流/文件的指针,这个参数使得fputs函数与文件建立联系,将字符串写入到文件中去
返回值 – int
如果代码运行成功,返回一个非负的值;
运行失败,代码返回EOF并且通过ferror设置错误标志(打印出错误所在)
fputs练习:
//fputs
#include<stdio.h>
int main()
{
//使用文件相关函数时,需要先到开一个文件然后才能进行文件函数的相关操作
//先写大概的逻辑步骤:1,2,3
//1.打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.使用文件
fputs("one piece is true\n", pf);
fputs("I am a handsome", pf);
//这里不会自动换行,在文件中我们会看到他们是在一行上,如果想要换行,那就得在字符串后面加上"\n"
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgets
该函数的功能:读取文件/流中的一行字符串到程序/内存中
函数表达式:
char * fgets ( char * str, int num, FILE * stream );
str :该参数是一个字符数组,用来存放文件中的字符串的
num:能够存放文件中最大字符的个数,包含’\0’
stream:指向一个文件的指针,这个参数使得fgets函数与文件建立联系,通过该函数读取文件中的一行数据
值得注意的点:
注意点1:如果在文件中将数据放在当前行中,函数实际上读取字符的个数为 num-1(因为程序会在最后自动加一个 '\0', 占num的最后一个字符位置)
注意点2:如果将一行数据在中间进行换行操作,那函数识别到 '\n'后 ,在后面加一个 '\0'后停止读取,实际上读取到的字符个数为:num-2个,num 包含:实际字符 + '\n' + '\0'
注意点3:如果函数遇到EOF,停止读取后面的字符,读取到实际字符的个数 = EOF前面的字符 + '\0'
返回值:
如果成功返回,返回str的起始地址;
返回失败分成两种情况:
情况1:如果是发生在没有读取任何数据之前返回失败,返回NULL(str中的内容不变)
情况2:如果是发生在读取数据之后返回失败,返回NULL(str中会发生变化)
小练习:
//fgets
////测试该函数时需要先在文件中输入一行数据 -- 这里以26个英文字母为例
#include<stdio.h>
int main()
{
//使用文件相关函数时,需要先到开一个文件然后才能进行文件函数的相关操作
//1.打开文件
FILE* pf = fopen("test.txt", "r");
//因为是从文件中读取数据,所以我们要改为 "r"
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.使用文件
char arr[20] = "xxxxxxxxx";
////验证注意点1 --- 使用F10观察在监视窗口输入arr,20观察的内容
fgets(arr, 20, pf);
printf("%s\n", arr);
////abcdefghijklmnopquv
//为了能够让我们在屏幕前看到,这里使用printf函数进行打印
////验证注意点2
//fgets(arr, 10, pf);
////abcdefgh
//注意点3待验证-- 在文件中输入abcdef,通过F10验证,在监视窗口输入 arr,10 可得 a b \0
//fgets(arr, 10, pf);
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//fputs适用于所有输出流,fgets适用于所有输入流,他们不仅仅可以在文件和程序中进行读取,也可以通过键盘输入打印在屏幕上上
//以下就是为了验证上述结论
#include<stdio.h>
int main()
{
char arr[20] = { 0 };
fgets(arr, 20, stdin);
fputs(arr, stdout);
return 0;
}
总结:fgetc、fputc是针对单个字符的;而fgets、fputs是针对字符串的
fgets必须配 “r”(只读) fputs必须配 “w” (只写)
我们知道在程序中不可能只有字符和字符串,还有其他类型的,比如浮点型、整型等等,这时就需要以下几个函数:
fprintf
该函数是一个格式化的输出函数,格式化:限定格式
函数的功能:将数据写入/打印到文件中去
函数的表达式:
int fprintf ( FILE * stream, const char * format, ... );
stream :文件指针变量,将文件与内存建立联系
format :限定格式,我认为就是占位符,它后面的值是与它的占位符是相对应的
… :可变参数列表,简单来说就是占位符和对应的值的个数是可变的,需要几个就写几个
返回值:
如果成功返回,返回的是写入到文件中字符的个数
如果返回失败,返回ferror报错的同时返回一个负数
该函数可以与printf函数进行比较学习,前者是将数据打印到文件中去,后者打印到屏幕上
小案例:
//fprintf
//打印员工的基本信息到test.txt文件中去,用结构体
struct S
{
char name[20];
int age;
float score;//要与下面的%f配套使用
};
#include<stdio.h>
int main()
{
struct S s = { "zhansan", 18, 88.88f };//输入数据时这里的float的数据后面需要加上f
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)//; //if后面别加;
{
perror("fopen");
return 1;
}
fprintf(pf, "%s %d %.3f", s.name, s.age, s.score);
//以前打印用:printf,类别即可
//printf("%s %d %.3lf", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
fscanf
有了格式化的输出,配套的还有格式化的输入,与前面函数一样,我们可以将scanf与之类别
小练习:
//fscanf
//读取文件中员工的基本信息到程序中去
//使用下面函数的前提条件:在文件中输入员工对应结构体中的基本信息
struct S
{
char name[20];
int age;
float score;
};
//使用scanf 输入数据时,只需要%f即可,不需要%.f中的 '.',只需要% + 对应的类型,例如:% + d
//使用printf可以限定输出格式,加 '.',例如:%.3f,读取到小数点后三位
#include<stdio.h>
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//scanf("%s %d %.3lf", s.name, &(s.age), &(s.score));
//数组名是地址,所以不需要取地址
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
//和scanf相比它就多了个文件指针变量
//将数据打印在屏幕上,验证打印是否正确
printf("%s %d %.1f\n", s.name, s.age, s.score);
//这里也可用fprintf打印,将参数换成stdout时,等同于printf
fprintf(stdout, "%s %d %.1f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
fscanf和fprintf不仅仅可以用在文件流中,还可以用在标准输入输出流中,
fprintf和fscanf包含scanf和printf的功能以下是在屏幕上输入数据,将所输入的数据打印到屏幕上
struct S
{
char name[20];
int age;
float score;
};
#include<stdio.h>
int main()
{
struct S s = { 0 };
//scanf("%s %d %.f", s.name, &(s.age), &(s.score));
//数组名是地址,所以不需要取地址
fscanf(stdin, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %.f\n", s.name, s.age, s.score);
fprintf(stdout, "%s %d %.f\n", s.name, s.age, s.score);
return 0;;
}
下面再给大家介绍一组函数:
sprintf
该函数的功能:将格式化的数据写入到字符串中,将格式化的数据转换为字符
串
函数表达式:
int sprintf ( char * str, const char * format, ... );
可将他与fprintf进行类别得到,str:指向存放字符串的指针
sscanf
该函数的功能:从字符串中提取格式化的数据,将字符串转换为格式化的数据
函数表达式:
int sscanf ( const char * s, const char * format, ...);
以下是关于它俩的练习:
//sprintf和sscanf
struct S
{
char name[20];
int age;
float score;
};
#include<stdio.h>
int main()
{
char arr[100] = { 0 };
struct S s = { "xuihua", 18, 88.88};
////将变量s中格式化的数据写入到字符串中,并且打印出来
//sprintf(arr, "%s %d %.f\n", s.name, s.age, s.score);
////printf("%s %d %.f\n", s.name, s.age, s.score);
//printf("%s\n", arr);
//注意:这组函数在相互转换时对应的格式化占位符应当相同,简单来讲:出来什么样,进来就还是什么样
struct S tmp = { 0 };
//scanf("%s %d %f", s.name, &(s.age), &(s.score));
//数组名是地址,所以不需要取地址
//将字符串arr提取到格式化的数据存放到tmp里
sscanf(arr, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %.f\n", s.name, s.age, s.score);
return 0;;
}
比较下面一组函数,并说出各自的功能(复习时自己说一遍)
printf / fprintf / sprintf
scanf / fscanf / sscanf
scanf/printf :针对 标准输入/标准输出流的 格式化 输入/输出函数
标准的输入/输出流 ---- 操作系统为每个程序自动打开的预定义数据通道,用于处理基本的输入和输出操作。例如用户通过键盘输入数据和将数据打印到屏幕上
scanf/printf :针对 所有输入/所有输出流的 格式化 输入/输出函数
比如:将程序中的数据写入到文件中和将文件中的数据读取到程序中
sprintf/sscanf
将格式化的数据写入到字符串中,将格式化的数据转换为字符串
从字符串中读取提取格式化的数据到字符串中,将字符串转换为格式化的数据
fwrite
该函数的功能:将数据以二进制的形式写入到文件中
函数的表达式:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
以上参数总结成一句话:将ptr指向的count个大小为size个字节的数据写入到文件/流中去
函数的返回值:
返回的是成功读取元素的总数
如果该数与size的个数不一致,则会通过ferror进行报错检测;
如果size或count为0,则返回0,ferror无操作
对应的练习:
//fwrite
struct S
{
char name[20];
int age;
float score;
};
#include<stdio.h>
int main()
{
//创建函数的相关变量
struct S ptr = { "cuihua", 56, 88.88f };//记得加上f
//打开文件
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");//这里使用perror,而不是ferror,在文件中用的都是perror
return 1;
}
//写文件 --- "wb"
fwrite(&ptr, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
对应的还有
fread函数
该函数的功能:在文件中将二进制形式的数据读取到内存中
该函数的表达式:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从文件中读取count个大小为size个字节的数据,指向ptr所在的空间中
该函数的返回值:
返回成功读取到的元素总数;
如果返回的元素总数与count不一致,要么是读取错误过程中出现了错误,要么是读取到文件末尾,这两种情况都可通过perror进行错误检测;
如果size或count为0,则返回0,perror无操作
对应的练习:
//fread
//前提是使用了fwrite以二进制的数据写入到文件中,在程序中写好数据,fwrite会帮你进行转换成二进制的数据写入到文件中
struct S
{
char name[20];
int age;
float score;
};
#include<stdio.h>
int main()
{
//创建函数的相关变量
struct S ptr = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件 --- --- "rb"
fread(&ptr, sizeof(struct S), 1, pf);
//这里的意思是将pf指向的文件中个数为1个大小为sizeof(struct S)的二进制数据读取到程序中
//这里为了能够验证数据真的在程序中,现进行打印
printf("%s %d %float\n", ptr.name, ptr.age, ptr.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
前面都是按照顺序读和写的函数,接下来介绍的是文件的随机读写函数
文件的随机读写函数
顾名思义,就是从文件的任意位置读和写,以下是相关的函数:
fseek函数
该函数的功能:根据文件指针的位置和偏移量来定位文件指针(这里定位的是文件内容的光标)
函数的表达式:
int fseek ( FILE * stream, long int offset, int origin );
stream:指向流/文件的指针,在这里用来与文件建立联系
offset:偏移量,需要根据origin的位置进行设置,达到想要的去读和写的那个位置,这个参数是可正可负的整数;
offsetof — 用来计算结构体成员相较于其实位置的偏移量(额外知识)
origin:起始位置,这个参数有三种选择
函数的返回值:
如果函数操作成功,该函数返回0;其他情况,返回非0的值;
如果返回错误,通过ferror检测出错误所在
//fseek
//在文件中输入abcdef,通过fseek函数,在不同位置找到并且打印字符 'e'
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
//这是按照正常顺序读
//ch = fgetc(pf);
//printf("%c\n", ch);//a
//ch = fgetc(pf);
//printf("%c\n", ch);//b
//ch = fgetc(pf);
//printf("%c\n", ch);//c
//fetc的返回值是int 当它返回失败时,返回的是EOF(-1),需要返回一个整数
ch = fgetc(pf);
printf("%c\n", ch);//a
//情况1:在起始位置
fseek(pf, 4, SEEK_SET);
//情况2:在当前位置
fseek(pf, 3, SEEK_CUR);
//情况3:在末尾位置
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//e
//左边为负数,右边调整为正数
//上述代码测试时,要一个一个测试,
//当然不是说该函数只能使用一次,可以使用多次,但是在多次使用时知道代码调整的过程,做到人码合一
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ftell
该函数的功能:返回文件指针相较于起始位置的偏移量
long int ftell ( FILE * stream );
让我们通过实例看看:
//ftell
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//e
printf("%d\n", ftell(pf));
//这里通过ftell函数返回pf所指向的文件中的光标的位置相较于起始地址的偏移量
//该偏移量是 long int类型的打印时用%d
//abcde | f --- 找到e之后的光标所在地,数出它距离 | abcdef,这个光标有多少个间隔
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
rewind
让文件指针回到文件的其实位置(让光标回到起始位置)
void rewind ( FILE * stream );
该函数无返回值
通过小案例加以说明:
//rewind
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
fseek(pf, -1, SEEK_END);
printf("%c\n", fgetc(pf));
printf("%d\n", ftell(pf));
rewind(pf);
printf("%d\n", ftell(pf));//0
ch = fgetc(pf);
printf("%c\n", ch);//a
//rewind无返回值,这里通过ftell和fgetc进行检验光标是否回到了起始位置
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//使用这些函数要牢记打开文件的方式,以读的形式打开时,就要调用关于读的函数;以写的形式,要调用写的函数;
//不要以读的形式打开关于写的函数,将他们搞混了
我们学习了上面的读取文件中的函数,那么时候能够判断它是否读取结束呢,以下就是关于文件读取结束的判定:
文件读取结束的判定
feof的正确使用
int feof ( FILE * stream );
feof的作用:当文件读取结束的时候,判断文件读取结束的原因是否是:遇到文件末尾
该函数的返回值:如果没有遇到EOF,它的返回值为0,如果遇到返回非0
测试feof没有遇到EOF时返回的值为0
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件+判断
//先读取文件1中的数据到内存中,再将内存中的数据写入到文件2中
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)//等价于if(!pf)
{
perror("fopen");
return 1;
}
//读文件
ch = fgetc(pf);
printf("%c ", ch);
//这里是为了测试feof没有遇到EOF时返回的值为0
int ret = feof(pf);
printf("%d\n", ret);
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
这里是为了测试feof函数遇到EOF后它的返回值为非0,以及ferror
#include<stdio.h>
int main()
{
int ch = 0;//为了防止fgetc函数结束后返回未知的数据,设立ch是为了先检查再使用fgetc
// //--- 实际上起的作用见下面的while循环
//打开文件+判断
FILE* pf = fopen("test.txt", "r");//只要将这里的r改为w,因为这里是读取文件,
//如果改成w,就可以验证下面的ferror的返回值为非0
if (pf == NULL)//等价于if(!pf)
{
perror("fopen");
return 1;
}
//读文件
//ch = fgetc(pf);
//printf("%c ", ch);
////这里是为了测试feof没有遇到EOF时返回的值为0,如果在读取过程中出现错误,ferror返回的值为非0
//int ret = feof(pf);
//printf("%d\n", ret);
//这里是为了测试feof函数遇到EOF后它的返回值为非0,
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
int n = ferror(pf);
printf("%d ", n);
int ret = feof(pf);
printf("%d", ret);
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
在文件读取的过程中,feof函数的返回值不是直接判断文件是否结束的标志
文本文件读取是否结束
文本文件读取是否结束,需判断他是否为EOF(fgetc), 或者NULL(fgets)
比如:
fgetc 判断是否为 EOF .(fgetc 函数读取数据结束后,返回的是EOF)
fgets 判断返回值是否为 NULL .(fgets 函数读取数据结束后,返回的是NULL)
二进制文件读取是否结束
二进制文件读取是否结束,需判断它的返回值是否小于实际要读的个数,如果小于说明读取到文件的末尾了
例如:
fread判断返回值是否小于实际要读的个数。
ferror(读取过程中出现错误)
int ferror ( FILE * stream );
返回值:
读取过程中出现错误,程序会设置一个以那种错误的状态值(也叫标记),ferror可以检测到那个标记,如果确实是因为错误而读取结束,则该函数返回一个非0的值,如果没有错误,则返回0
关于ferror返回值的练习在feof中
ferror读取过程中出现错误,文本文件和二进制文件都可以用
以下是针对两种文件的实例:
文本文件
//文本文件的:
//需要在文件中输入数据,这里以abcdef为例
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件+判断
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)//等价于if(!pf)
{
perror("fopen");
return 1;
}
//读取文件
//int ch = fgetc(pf);这里只运行一次,会导致后面死循环
while ((ch = fgetc(pf)) != EOF)//这是用来判断它是否读取结束
//在这里不能够将创建变量放进去:int ch = fgetc(pf)) != EOF,会报错
{
//printf("%c ", fgetc(pf));
// fgetc 在文件末尾返回 EOF(-1)时,若直接将其传给 printf 的 %c 进行输出,会导致未知的字符显示
printf("%c ", ch);//等价于 putchar(ch);
}
printf("\n");
//判断文件读取结束的原因是啥
if (feof(pf))//利用feof的返回值
{
printf("文件读取到末尾后结束\n");
}
else if (ferror(pf))
{
printf("文件读取失败结束\n");
}
//关闭文件
fclose(pf);
pf == NULL;
return 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 的数组
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);
}
练习:将test1.txt 中的数据拷贝到test2.txt的文件中去
#include<stdio.h>
int main()
{
int ch = 0;//为了防止fgetc函数结束后返回未知的数据,设立ch是为了先检查再使用fgetc
// //--- 实际上起的作用见下面的while循环
//打开文件+判断
FILE* pf = fopen("test.txt", "w");//只要将这里的r改为w,因为这里是读取文件,
//如果改成w,就可以验证下面的ferror的返回值为非0
if (pf == NULL)//等价于if(!pf)
{
perror("fopen");
return 1;
}
//读文件
//ch = fgetc(pf);
//printf("%c ", ch);
////这里是为了测试feof没有遇到EOF时返回的值为0,如果在读取过程中出现错误,ferror返回的值为非0
//int ret = feof(pf);
//printf("%d\n", ret);
//这里是为了测试feof函数遇到EOF后它的返回值为非0,
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
int ret = feof(pf);
printf("%d", ret);
if (ret = ferror(pf))
{
perror("test");//如果在读取过程中出现错误,ferror返回的值为非0,但是他设置了标记,但是这里检测不出来
return 1;
}
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
文件缓冲区
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
我们在写从内存向磁盘输出数据会先送到内存中的缓冲区***,装满缓冲区后才一起送到磁盘上***;如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区***(充满缓冲区) ,然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)***
缓冲区的大小根据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");
//这里睡眠10秒,是为区分文件关闭时的缓冲区刷新
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件时数据的丢失。