1.为什么使用文件?
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件。
int main()
{
int a = 0;
printf("a=%d\n", a);
scanf("%d", &a);
printf("a=%d\n", a);
return 0;
}
//a是内存上的一块区域
//我们只要退出了这个代码,之前对a的输入,之前的数据就都没了,退出代码就清除
2.什么是文件?
磁盘(硬盘)上的⽂件是⽂件。
但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件(从⽂件功能的⻆度来分类
的)。
程序文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows环境后缀为.exe)。
数据文件
⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件。
本章讨论的是数据⽂件。
在以前各章所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显⽰到显⽰器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上⽂件。
文件名
⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。
⽂件名包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀
例如: c:\code\test.txt
为了⽅便起⻅,⽂件标识常被称为⽂件名。
3.二进制文件和文本文件
根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件。
⼀个数据在⽂件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节。
下面的10000里面的每个数字就是以ASCII进行存储的
0的ASCII大小是48
1的ASCII大小是49
那么10000在内存中的存储形式就是下面的样子了
将10000转换为二进制写到文件里面去
#include <stdio.h>
int main()
{
int a = 10000;
FILE * pf = fopen("test.txt", "wb");//二进制写的方式打开文件
fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
//a的地址,4个字节,写1次,写到关联的问题里面去
fclose(pf);
pf = NULL;
return 0;
}
//打开文件,写文件,再关闭文件,最后再将pf置为空指针
//这个代码就是将10000转换为二进制写到文件里面去
4.文件的打开和关闭
铺垫
流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出
操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流
想象成流淌着字符的河。
C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
打开流,读\写,关闭流
标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
• stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
• stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出
流中。
• stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为⽂件指针。
C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。
文件指针
缓冲⽂件系统中,关键的概念是"⽂件类型指针",简称"⽂件指针"。
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名
字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系
统声明的,取名 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*的指针来找到文件信息区
FILE* pf;//⽂件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(是⼀个结构体变
量)。通过该⽂件信息区中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与
它关联的⽂件。
5.文件的顺序读写
重点:文件的打开和关闭
文件在使用之前应该打开文件,在使用结束后应该关闭文件
所以文件的操作就是:
1.打开文件----打开流
2.读写文件---读/写流
3.关闭文件---关闭流
在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。
ANSI C 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。
fopen是用来打开文件的
FILE*fopen(const char * filename ,const char *mode)
第一个参数是文件名 ,第二个参数是文件打开方式
|文件使用方式|含义|如果指定文件不存在| |-|-|-| |"r"(只读)|为了输⼊数据,打开⼀个已经存在的⽂本⽂件|出错| |"w"(只写)|为了输出数据,打开⼀个⽂本⽂件|建⽴⼀个新的⽂件| |"a"(追加)|向⽂本⽂件尾添加数据|建⽴⼀个新的⽂件| |"rb"(只读)|为了输⼊数据,打开⼀个⼆进制⽂件|出错| |"wb"(只写)|为了输出数据,打开⼀个⼆进制⽂件|建⽴⼀个新的⽂件| |"ab"(追加)|向⼀个⼆进制⽂件尾添加数据|建⽴⼀个新的⽂件| |"r+"(读写)|为了读和写,打开⼀个⽂本⽂件|出错| |"w+"(读写)|为了读和写,建议⼀个新的⽂件|建⽴⼀个新的⽂件| |"a+"(读写)|打开⼀个⽂件,在⽂件尾进⾏读写|建⽴⼀个新的⽂件| |"rb+"(读写)|为了读和写打开⼀个⼆进制⽂件|出错| |"wb+"(读 写)|为了读和写,新建⼀个新的⼆进制⽂件|建⽴⼀个新的⽂件| |"ab+"(读 写)|打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写|建⽴⼀个新的⽂件|
//打开文件
/*
fopen是用来打开文件的
FILE*fopen(const char * filename ,const char *mode)
文件名 文件打开方式
*/
//int main()
//{
// //1.打开文件
// FILE* pf=fopen("test.txt", "w");//打开文件是"w",这个函数的返回值是FILE*
// //那么我们就用一个FILE*的指针pf进行接收
// //打开文件成功的话,那么返回的就是有效的指针,文件的指针
// //如果打开失败的话,则返回一个空指针,所以我们要进行判断
// if (pf == NULL)
// {
// perror("fopen");
// return 1;//直接返回,没必要往下面走了
// }
// //...打开成功了
//
// return 0;
//}
/*
当我们打开文件储存的路径我们会发现
会多出一个test.txt文件,之前是没有的,我们现在是想打开它的,但是因为没有这个文件,
所以系统帮我们创建了一个文件
*/
/*
我们现在已经创建了这个文件,我们在里面编写数据:abcdef
保存后退出
我们再次运行这个代码打开文件,我们会发现之前的文件变成0kb了,里面的数据没有了
原因就是我们以w的形式打开的话,如果文件存在,他会将文件内容清空掉
如果文件不存在的话就会新建一个文件
*/
//int main()
//{
// //1.打开文件
// FILE* pf = fopen("test.txt", "r");
//
// if (pf == NULL)
// {
// perror("fopen");
// return 1;
// }
//
//
// return 0;
//}
/*
这里我们用r形式来打开文件
我们将之前的文件删除
那么我们再次运行代码
fopen: No such file or directory
就会报错,因为没有这个文件
*/
//关闭文件
//基本格式:int fclose(FILE*stream)
int main()
{
//1.打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
//关闭这个文件指针后,pf已经变成了野指针了
pf = NULL;
return 0;
}
切记:pf被fclose关闭之后我们要将pf赋值为NULL
因为关闭之后pf就变成野指针了
如果这个文件是桌面上的文件
我们需要知道这个文件的路径
FILE* pf=fopen("C:\\Users\\27890\\Desktop\\test.txt", "w");
我们要在文件名之前加上文件的路径就能打开非代码文件夹里面的文件了
但是我们会担心害怕转移字符的生成了,所以我们可以加上两个\
那么现在我们打开的就是这个文件
//打开非代码文件夹里面的文件
//int main()
//{
// //1.打开文件
// FILE* pf = fopen("C:\\Users\\27890\\Desktop\\test.txt", "r");
//
// if (pf == NULL)
// {
// perror("fopen");
// return 1;
// }
//
// //写文件
//
// //关闭文件
// fclose(pf);
// //关闭这个文件指针后,pf已经变成了野指针了
// pf = NULL;
// return 0;
//}
int main()
{
//1.打开文件
//假设文件在这个代码文件位置的上两级
//.表示当前路径
//..表示上一级路径
//下面的就是当前路径的上一级路径里面的文件
FILE* pf = fopen(".\\..\\test.txt", "r");
// .\\..\\..\\当前路径的上一级路径的上一级路径里面的文件
//那么这个就是相对路径,相对与当前位置的路径
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
//关闭这个文件指针后,pf已经变成了野指针了
pf = NULL;
return 0;
}
打开相对路径的文件的时候直接在文件名字之前加上
.\..\..\
在当前文件的上一级的文件的上一级文件里面,这样就能打开相对文件夹里面的文件了
如果是其他位置的文件,我们仅仅需要再文件名字前面加上地址就行了
另外:为了防止转义字符出现,我么就在每个右斜杠旁边再加一条右斜杠,这样防止转义字符的产生
我们以读的形式打开就只能读
以写的形式打开就只能写
不可做多余的事情
顺序读写介绍
除了最后两行的,其他的都是读和写文本信息
第一组:fputc 和fgetc
//fputc--写字符
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
/*fputc('a', pf);
fputc('b', pf);
fputc('c', pf);*/
//循环写入
for (int i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
/*
* fputc--写字符
int fputs(int *character,FILE*stream);
第一个参数是要写的字符
*/
/*
fgetc---读取一个字符
int fgets(FILE*stream)
只有一个参数就是那个文件的流
读取失败就会返回EOF,
读取正常的话会返回对应字符的ASCII码值
EOF是enf of file
文件的结束标志
*/
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//现在文件里面存放的是之前写入的26个字母
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
// //输出的就是a b c
// //我们在一开始读文件的时候,光标指向的是a,读完a之后,光标就指向了a的后一位的字母
// //文件的光标一直随着我们在读在变化
//
// //关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fputc函数返回的是对应字符的ASCII码值,两个参数,第一个参数是要写的字符
第二个参数是文件对应的流,文件指针
fgetc读取字符,参数是对应的文件的指针
读取失败就会返回EOF,
读取正常的话会返回对应字符的ASCII码值
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch=0;
while ((ch = fgetc(pf)) != EOF)//只要返回值不是EOF我们一直进行读取
{
printf("%c ", ch);
}
//那么这个输出的就是26个字母
return 0;
}
int main()
{
int ch=fgetc(stdin);//从键盘(标准输入流)上读取
fputc(ch, stdout);//将字符输出(写)到屏幕(标准输出流)
return 0;
}
从键盘上输入,在屏幕上输出
第二组:fputs和fgets
fputs:
int fputs(const charstr,FILEstream)
第一个参数是一个字符指针,指向了一个字符串,第二个是一个文件指针
返回类型是int
int main()
{
//1.打开文件
FILE* pf = fopen("hu.txt", "w");
if (pf == NULL)//判断打开是否成功
{
perror("fopen");
return 1;
}
//2.写文件
fputs("I am a student", pf);//传过去的是字符串首元素的地址
int ret=fputs("are you ok?", pf);
//这两个输入的字符串在一行上面
printf("%d", ret);//返回值是0,说明成功了,不是负数
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//如果fputs输入成功的话,返回的就是一个非负整数
//失败的话,函数会返回EOF
如果fputs输入成功的话,返回的就是一个非负整数
失败的话,函数会返回EOF
fgets:
charfgets(char str,int nmu,FILE*stream)
第一个参数就是一个指针,指向复制到读取字符串的字符数组的指针
第二个参数num 是这个字符串能拷贝多少个字符
//fgets--读文件
int main()
{
//1.打开文件
FILE* pf = fopen("hu.txt", "r");
if (pf == NULL)//判断打开是否成功
{
perror("fopen");
return 1;
}
//2.读文件
char arr[20] = { 0 };//将要读的文件放到这个数组内,相当于拷贝到这个数组内
fgets(arr,20,pf);
printf("%s", arr);
//将读到的数据放到arr里面
//但是这里的arr并不是合适的,I am a studentare y我们还有些数据并没有读到
//读取到的字符末尾还有一个'\0',总共下来就是20个字符了,真实读到的只有19个
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
//从流中读取字符串,将字符串赋值拷贝在字符串数组中,最多读num-1个字符
//如果遇到换行的话,我们会直接停下来的,那么我们会读取\n。\n后面还是要读取\0的
fgets在使用的时候,我们要先创创建一个字符串数组,这个数组会存储我们读到的数据的
num就是我们读取的数据个数,但是因为读取到的还有一个\0,所以我们实际读到的仅仅只有num-1个字符
//在键盘上读,输出在屏幕上
int main()
{
char arr[20] = { 0 };//存储我们读到的数据
fgets(arr, 20, stdin);//stdin是标准输入流
fputs(arr, stdout);//stdout是标准输出流
return 0;
}
fgets如果读取成功的话,那么会返回str--目标空间的起始地址,接不接收无所谓的
但是读取失败的话会返回一个空指针的
第三组:fscanf和fprintf
fprintf:
int fprintf(FILE*stream,const char *format,....)
第三个参数就是可变参数列表
而printf的参数没有第一个,因为printf默认操作的就是stdout
fprintf可以适用于所有的操作流,可以适用于文件流,也可以适用于标准输出流
fprintf将数据写到文件内
struct S
{
char name[20] ;
int age ;
float score ;
};
int main()
{
struct S s = { "lisi",18,88.5f };
//1.打开文件
FILE* pf = fopen("hu.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.写文件
/*fprintf()*/
fprintf(pf,"%s %d %f", s.name,s.age,s.score);//在前面加上文件指针,将后面所指的数据写到文件内
//printf("%s %d %f", name, age, score);
// 如果写入是结构体的数据也是可以的
//
// 将s里面的数据写到文件里面
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fscanf将文件读出来
fscanf:
int fscanf(FILE*stream,const char *format,....)
scanf和fscanf的差异也仅仅只是缺失文件指针
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
//1.打开文件
FILE* pf = fopen("hu.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//2.读文件
//scanf("%s %d %f",&name,&age,&score)
//从文件中读取信息,将读取的信息存放在s的各个成员中
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));//数组名就是地址,不用加取地址符号
//打印在屏幕上
//printf("%s %d %f", s.name, s.age, s.score);
fprintf(stdout,"%s %d %f", s.name, s.age, s.score);//默认输出流
//3.关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fscanf将文件内的数据拷贝存在结构体中
对比一组函数,介绍sscanf和sprintf的用法
scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf/printf 针对标准输入流(stdin)/标准输出流 (stdout) /格式化输入/输出函数
fscanf/fprintf 针对所有输入流(stdin)/输出流(stdout) /格式化输入/输出函数
第一种只能在键盘上输入和输出
第二种可以在文件和键盘上输入和输出
那么sprintf和sscanf有什么作用呢?
sprintf
int sprintf(char* str,const char* format,...)
sprintf作用就是将格式化的数据输入到指针str所指向的空间(字符串中)
可以理解为将格式化的数据转换为字符串
struct S
{
char name[20];
int age;
float score;
};
int main()
{
char arr[125] = { 0 };
struct S s = { "lisi",18,98.5f };
sprintf(arr, "%s %d %f", s.name, s.age, s.score);
//将后面的数据转换为字符串存在arr中
printf("%s", arr);//将数据以字符串形式打印出来
return 0;
}
/*
sprintf可以理解为将数据转换为字符串,然后存储在指定的字符数组中
*/
ascanf的作用就和sprintf作用相反
sprintf的作用是将数据以字符串的形式存储在数组内
那么sscanf就是将数组中的这些已经转化为字符串的格式化数据提取出来
sscanf
int sscanf(const char * s,const char* format , ....)
struct s
{
char name[20];
int age;
float score;
};
int main()
{
char arr[125] = { 0 };
struct s s = { "lisi",18,98.5f };
//临时变量
struct s tmp = { 0 };
//将s中的各个数据转换成字符串放在arr中
sprintf(arr, "%s %d %f", s.name, s.age, s.score);
//从字符串arr中提取格式化的数据,存放在tmp中
sscanf(arr,"%s %d %f", tmp.name, &(tmp.age), &(tmp.score));//name 是字符串首元素的地址
printf("%s %d %f", tmp.name, tmp.age, tmp.score);
return 0;
}
从字符串中提取格式化的数据,将字符串转化为格式化数据
总结:sprintf:将格式化的数据转换为字符串
sscanf:将字符串转化成格式化的数据
第五组:fread和fwrite
fwrite
size**t fwrite( const void *ptr ,size*t size,size_t count, FILE**stream)
有四个参数
将ptr指向的这块空间里面的count个大小为size的元素写到stream所指向的文件里
fread
size**t fread( const void *ptr ,size*t size,size_t count, FILE**stream)
两个函数的参数是一模一样的
那么fread的作用就是把stream里面的count个大小为size的元素写到ptr所指向的空间
从文件stream中读取count个大小为size个字节的数据,存放在ptr指向的空间中
//struct S
//{
// char name[20];
// int age;
// float score;
//};
//
//int main()
//{
// struct S s = { "lisi",18,98.6f };
// //将这些数据以二进制的形式写到文件中
//
// //打开文件
// FILE* pf = fopen("test.txt", "wb");//b就是二进制的形式,这里的wb就是二进制的写
//
// //判断文件打开成功没?
// if (pf == NULL)
// {
// perror("fopen");
// return 1;
// }
// //写文件
// fwrite(&s, sizeof(struct S), 1, pf);//写的数据来自s,我们要提供地址
// //这里只有1个结构体的数据,那么我们写1,写到pf所指向的文件里面去
//
// //关闭文件
// fclose(pf);
// pf = NULL;
// return 0;
//}
//这里我们已经将s中的数据以二进制的形式写到文件中去了
//那么下面的代码我们就进行解读
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
//读取二进制的数据到文件中
//打开文件
FILE* pf = fopen("test.txt", "rb");
//判断文件打开成功没?
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
fread(&s, sizeof(struct S), 1, pf);
//第一个参数是我们读完存放信息的地址
//第二个参数是一个元素的大小,
// 第三个元素是元素的个数
// pf就是我们要进行读的文件
printf("%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这两个函数的返回值都是size_t 类型的
成功读取几个就返回
假设我们在用fread的时候,我们从文件中读取5个大小为size的数据放到ptr里面去,但是这个文件只有三个,读不到5个
那么我们这个函数就会返回我们实际读到数据的真实个数
6.文件的随机读写
想在哪里读就在哪里读,想在哪里写就在哪里写
文件的随机读写要确定我们这个文件里面写进去了很多信息
我们要根据我们的需要,将文件指针的指针(文件内容的光标)位置进行调整
fseek的使用
fseek
int fseek(FILE* stream,long int offest,int origin)
第一个参数是文件指针,第二个参数是偏移量 ,第三个参数是起始位置
从第二个参数我们能回想起之前学的offsetof---计算结构体成员相较于起始位置的偏移量
我们一定要知道偏移量和起始位置,我们才能定位这个文件指针(光标)
对于第三个参数,我们有三种选项
1.SEEK_SET:文件的起始位置
2.SEEK_CUR:文件指针(光标)当前的位置
3.SEEK_END:文件的末尾
int main()
{
//读取二进制的数据到文件中
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件打开成功没?
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件,fgetc读取字符,返回被读字符的ASCII码值
int ch = 0;
ch=fgetc(pf);
printf("%c\n", ch);
//最开始,光标指向的是a,读完a之后,光标就会往后跳
ch = fgetc(pf);
printf("%c\n", ch);//这里打印出来的就是b,打印完b,光标就指向了c
//按照常规的话,下面的代码中的光标就指向了c,但是我们想直接读e,我们该做什么呢?
//所以我们现在要重新定位文件指针,将文件指针的(光标)指向e的位置
//fseek(pf, 4, SEEK_SET);//从文件起始位置偏移4下就能到e的位置了
//fseek(pf, 2, SEEK_CUR);//因为上面已经进行了b的打印了,那么光标就指向了c的位置,
//那么我们从当前位置进行光标的偏移,偏移两下光标就指向了e
fseek(pf, -2, SEEK_END);
//最后的位置就是f后面,光标指向了f的后面,那么这个光标前进两个指向的就是e了
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
向后偏移的话偏移量就是正数,向前的话偏移量就是负数
ftell的使用
ftell
long int ftell(FILE*stream)
返回文件指针相对于起始位置的偏移量
int main()
{
//读取二进制的数据到文件中
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件打开成功没?
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件,fgetc读取字符,返回被读字符的ASCII码值
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
//最开始,光标指向的是a,读完a之后,光标就会往后跳
ch = fgetc(pf);//b
printf("%c\n", ch);//这里打印出来的就是b,打印完b,光标就指向了c
fseek(pf, -2, SEEK_END);
//我们从文件的末尾进行访问,这是一个字符串,隐藏着一个'\0',所以我们要偏移两下才能到e
ch = fgetc(pf);
printf("%c\n", ch);//e
//读完e我们想知道当前位置相较于起始位置的偏移量
printf("%d", ftell(pf));//5
//为什么是5呢?
//因为上面我们刚读完e了,光标指向了f,那么这个位置距离起始位置的距离就是5了
//最开始的起始位置在a的前面
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
rewind的使用
让文件指针位置回到文件的起始位置
void rewind(FILE*stream)
int main()
{
//读取二进制的数据到文件中
//打开文件
FILE* pf = fopen("test.txt", "r");
//判断文件打开成功没?
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件,fgetc读取字符,返回被读字符的ASCII码值
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
//最开始,光标指向的是a,读完a之后,光标就会往后跳
ch = fgetc(pf);//b
printf("%c\n", ch);//这里打印出来的就是b,打印完b,光标就指向了c
fseek(pf, -2, SEEK_END);
//我们从文件的末尾进行访问,这是一个字符串,隐藏着一个'\0',所以我们要偏移两下才能到e
ch = fgetc(pf);
printf("%c\n", ch);//e
//让文件指针回归到最初的位置
//将文件指针重新定位到文件起始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
//那么这一次的打印就是a了
fclose(pf);
pf = NULL;
return 0;
}
7.文件的读取结束的判定
被错误使⽤的 feof
因为eof是文件结束的标志
以为feof函数是用来判断文件是否结束的,但其实不是的
牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。
feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。
在文件的读取过程中,有可能读取文件结束
结束的原因:1.遇到文件末尾
2.遇到错误了
,是判断结束的原因的
- ⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
• fgetc 判断是否为 EOF .
• fgets 判断返回值是否为 NULL .
- ⼆进制⽂件的读取结束判断,
判断返回值是否⼩于实际要读的个数
。
例如:
• fread判断返回值是否⼩于实际要读的个数
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp)//判断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);
fp = NULL;
return 0;
}
#include <stdio.h>
enum { SIZE = 5 };//枚举类型定义一个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 的数组
//a是数组名,首元素的地址,那么*a就是首元素,那么sizeof*a计算的就是每个元素的大小
//将a中的数据以二进制的形式写到fp所指向的文件里面去
fclose(fp);//关闭文件
double b[SIZE];
fp = fopen("test.bin", "rb");//打开读文件,二进制形式rb
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
//code是fread是实际读的个数,将fp中的数据以格式化数据的形式读到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))//是不是读取的时候遇到了文件末尾而结束的
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
int main()
{
//打开文件
FILE*pfread=fopen("test1.txt", "r");//这个文件我们是用来读的
if (pfread == NULL)//打开失败的情况
{
perror("fopen\n");
return 1;
}
FILE* pfwrite = fopen("test2.txt", "w");//写文件
if (pfwrite == NULL)//打开失败的情况
{
perror("fopen\n");
fclose(pfread);//第二个文件都打开失败了,我们就直接将第一个文件关掉
return 1;
}
//读写文件
int ch = 0;
//读pfread所指向的文件,将文件内的数据通过ch写到pfwrite里面
while ((ch = fgetc(pfread)) != EOF)//不等于EOF就说明读取正常
{
fputc(ch, pfwrite);//fputc的第一个参数是要写的字符,第二个是文件指针
}
//当返回值是EOF的时候就结束了
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
/*我们在while循环中,先用ch = fgetc(pfread) != EOF,
* fgetc的返回值就是对应字符的ASCII码值
* 那么我们先读pfread里面的每个字符,然后在每层循环为ch附上每个字符的ASCII码值
* 在循环内,fputc第一个参数就是要写的字符数据,第二个参数就是所指向的文件指针,
* ch已经被赋上值了,那么我们就直接利用fputc将字符数据写到prwrite所指向的文件中
*
*
*
*/
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");//这个文件我们是用来读的
if (pf == NULL)//打开失败的情况
{
perror("fopen\n");
return 1;
}
//读写文件
//int ch = 0;
//ch = fgetc(pf);
//int reet = feof(pf);//检测文件末尾
//printf("%d", reet);//打印出来的是0,如果不是结尾结束的话,那么就是不正常的
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);//将字符依次打印出来
}
int reet = feof(pf);
printf("%d", reet);//那么这里打印出来的就是1了
reet = ferror(pf);
printf("%d", reet);//输出的结果是0,因为此时已经是文件末尾了,并没有出现错误的情况
//关闭文件
return 0;
}
//如果这个是正常结束的话,到文件末尾的话,对于feof(pf)的话,返回值就是非0的数字
//如果不是结尾结束的话,那么返回的就是一个0
//w是写,r是读
对于feof来说的话,如果这个是正常结束的话,到文件末尾的话,对于feof(pf)的话,返回值就是非0的数字
如果不是结尾结束的话,那么返回的就是一个0
对于ferror来说的话,如果不是其他的错误信息导致停止话,就是返回的是0,
如果是什么错误信息导致的,那么这个返回的就是非0数字
8.文件缓冲区
ANSIC 标准采⽤"缓冲⽂件系统" 处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为
程序中每⼀个正在使⽤的⽂件开辟⼀块"⽂件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓
冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输
⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓
冲区的⼤⼩根据C编译系统决定的
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
//写文件,将这个一个字符串放到文件内
//先将abcdef放到缓冲区内
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);//10000毫秒就是10秒
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这⾥可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。
如果不做,可能导致读写⽂件的问题