文章目录
1. 为什么要使用文件
随着大家不断学习相信大家已经写了不少代码了,例如三子棋,通讯录等。但我们发现每次进入后无法与上次退出时保持一致,此时数据是存放在内存中,当我们程序退出的时,程序中的数据就不存在了,等下次运行程序的时,数据又需要重新录入,我们在想既然是通讯录或者相关记录的程序就应该把信息记录下来,我们自己选择删除数据的时 ,数据才 应该被删除。这里就涉及到了数据持久化的问题, 一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。而使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2. 文件的定义
计算机中文件是以硬盘为载体存储在计算机上的信息集合。
从文件功能的角度来分类,在程序设计中,我们一般指的文件有两种:程序文件、数据文件
2.1程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境
后缀为.exe)
2.2数据文件
程序运行时读写的数据,例如程序运行需要从中读取数据的文件,
或者输出内容的文件。本次我们以数据文件为主
2.3 文件名
文件有一个唯一的文件标识,以便用户识别和引用。
文件名由3部分组成:文件路径+文件名主干+文件后缀。
例如:
c:\code\test.txt
3. 打开和关闭文件
3.1文件指针
在我们对文件进行相关操作时,首先应该要打开文件才能进行操作。每个被使用的文件在内存中开辟了一个相应的文件信息区,用来记录文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型 的系统声明取名FILE.
这里我们要注意不同的编译器实现的方式不同。当打开一个文件的时候,系统会根据文件的情况自动并创建一个FILE结构的变量,我们作为使用者,直接使用即可。
FILE* pf;//文件指针变量
pf是 指向FILE类型数据的指针变量。使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。 通过文件指针变量能够找到与它关联的文件。
3.2 文件详细操作
FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );
那么更多的打开方式 ->fopen的详细打开方式
#include<stdio.h>
int main()
{
//打开文件
//相对路径
FILE* pd = fopen("test.txt", "w");//该文件创建在此工程文件下;
//绝对路径
FILE* PF=fopen("c:\\code\\test.txt","w");
//对pd进行判断防止为空
if (NULL == pd)
{
perror("fopen");//即时报错,防止出现更多错误
return;
}
//在文件内操作
//关闭文件
fclose(pd);
pd = NULL;//将pd置为空指针以防出现野指针
return 0;
}
上面代码我们可以看到创建了一个文件名为test.txt的文件,fopen返回地址用FILE*类型的文件指针pd 接收。
4.文件的顺序读写
首先我们要先知道对于键盘文件而言读写的不同
下面是对文件进行相关操作的函数:
这里我们一一进行介绍:
4.1 fputc fgetc函数
fputc fgetc两个函数的声明
更详细的介绍请查看
int fputc ( int character, FILE * stream );
// character写入的字符 stream文件指针名
int fgetc ( FILE * stream );
来看fputc代码演示
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "w");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
//在文件内操作
fputc('a', pd);
fputc('b', pd);
fputc('c', pd);
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
可以看到在相关打开的文件下 写入了三个字符 a b c
再来看一个
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "w");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
//在文件内操作
for (int i = 0; i < 26; i++)
{
fputc( 'a' + i, pd);
}
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
再来看fgtec的相关演示
test.txt文件内有已有字符内容:
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "r");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
//在文件内操作
char c = fgetc(pd);
printf("%c", c);
c = fgetc(pd);
printf("%c", c);
c = fgetc(pd);
printf("%c", c);
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
这里我们从test.txt文件里读去字符,fgetc每次读取一个字符并且读取后向后跳一个字符因此本次代码读取三个字符并打印。
那么我们在来吧文件内所有的字符打印出来
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "r");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
//在文件内操作
for (int i = 0; i < 26; i++)
{
char ch = fgetc(pd);
printf("%c ", ch);
}
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
输出结果:
当然我们还可以将这里的代码继续优化,由fgetc的返回值可知,读取正常为int型,如果读取失败会返回EOF。那么我们可以将代码优化。
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "r");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
int ch = 0;
while ((ch = fgetc(pd)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
4.2 fputs fgets函数
fputs fgets声明:
int fputs ( const char * str, FILE * stream );
// str为需要写入的字符串 stream文件指针名
char * fgets ( char * str, int num, FILE * stream );
// str将读取的内容放入 num最多读取的num-1个字符 stream为需要读取的文件指针名
fputs应用:
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "w");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
fputs("hello\n", pd);//注意这里需要我们主动添加\n才会进行换行
fputs("world\n", pd);
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
运行结果如下:
fgets的应用:由上图可知,test.txt文件内已有内容
#include<stdio.h>
int main()
{
//打开文件
FILE* pd = fopen("test.txt", "r");
if (NULL == pd)//判断空指针
{
perror("fopen");
return;
}
char arr[] = "***********";
fgets(arr, 6, pd);//将读取5个字符后放一个'\0'
//关闭文件
fclose(pd);
pd = NULL;
return 0;
}
进入arr内部后可以看到写入了5个字符,和一个’\0’
那么我们将读取的范围加大是否可以把剩下的字符一起读取呢?
答案是不会
我们可以看到,还是只将文件内一行内容读取出来,那么我们重复进行两次fgets读取,可以看到将文件内的两行字符串也读取出来了。
4.3 fscanf fprintf函数
fscanf fprintf函数声明
int fscanf ( FILE * stream, const char * format, ... );
int fprintf ( FILE * stream, const char * format, ... );
fprintf应用:
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s bba = { "bai",18,198.33 };
FILE* pp = fopen("name.txt", "w");
if (NULL == pp)
{
perror("fopen");
return;
}
fprintf(pp, "%s %d %5.2f", bba.name, bba.age, bba.high);
//这里我们注意到fprintf的用法与printf类似,第一个为文件指针名
fclose(pp);
pp = NULL;
return 0;
}
此时在路径下产生一个name.txt的文件,并写入相关内容。
fscanf应用:
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s bba = {0};
FILE* pp = fopen("name.txt", "r");
if (NULL == pp)
{
perror("fopen");
return;
}
fscanf(pp,"%s %d %5.2f", bba.name, &(bba.age),&(bba.high));
//这里也类似scanf的使用,从pp文件指针读取内容
printf("%s %d %5.2f", bba.name, bba.age, bba.high);
fclose(pp);
pp = NULL;
return 0;
}
可以看到从文件内读取到内容后并打印到屏幕上。
这里补充一个知识
对任何一个c程序,在运行时会默认打开三个流:
stdin - 标准输入流-键盘
stdout - 标准输出流-屏幕
stderr - 标准错误流-屏幕
那么我们在来看一下 这里printf为标准输出流 ,而 文件写入也为输出流,由下面这张图可知fgetc fputc fgets fputs fscanf fprintf适合所有的输入或输出流。那么我们就有疑问那么fgetc fputc fgets fputs fscanf fprintf这些能不能作用于标准输入流 标准输出流?
答案是可以的
我们在操作文件是需要打开关闭,但是在使用printf scanf时没有打开关闭,因为对任何一个c程序,在运行时会默认打开三个流:
stdin - 标准输入流-键盘
stdout - 标准输出流-屏幕
stderr - 标准错误流-屏幕
这三个标准流的类型都为FILE*的类型的,当我打开一个屏幕标准输出流时有一个stdout的输出流与之对应,当要从屏幕上输出数据时传stdout,从键盘上读取数据时传stdin
#include<stdio.h>
int main()
{
int ch = fgetc(stdin);//scanf相当于fsacnf(stdin, )
fputc(ch, stdout);//printf相当于fprintf(stdout, )
return 0;
}
因此我们在默认打开后就不需要打开等操作,直接可以使用
4.3 fread fwrite函数
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//从ptr地址处向后依次写入count个大小为size的数据到stream流文件里去
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
//从流文件内读取count个大小为size的数据到ptr所指向的空间
fwrite 应用:
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s bba = { "bai",18,198.33 };
FILE* pp = fopen("name.txt", "wb");//这里要注意是wb 输出数据,打开一个二进制文件而不是w输出数据,打开一个文本文件
if (NULL == pp)
{
perror("fopen");
return;
}
//二进制的写入数据
fwrite(&bba, sizeof(bba), 1,pp);
fclose(pp);
pp = NULL;
return 0;
}
我们看写入的二进制文件
这里可以看到二进制写入文件的内容我们并不认识,但电脑能正常识别。那么我们再将这个内容从中拿出来:
fread:
文件里的内容为:
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s bba = { 0 };
FILE* pp = fopen("name.txt", "rb");//二进制的方式读取
if (NULL == pp)
{
perror("fopen");
return;
}
//二进制的读取数据
fread(&bba, sizeof(bba), 1,pp);
printf("%s %d %5.2f", bba.name, bba.age, bba.high);
fclose(pp);
pp = NULL;
return 0;
}
可以看到成功将文件里的内容解读出来。
4.3 sscanf sprintf
scanf:按照一定的格式从键盘输入数据
printf:按照一定的格式把数据打印到屏幕上
fscanf:按照一定的格式从输入流(文件/stdin)输入数据
fprintf:按照一定的格式向输出流(文件/stdout)输出数据
那么我们再来看一个比较相似的函数
int sscanf ( const char * s, const char * format, ...);
//从字符串中按照一定格式读取处格式化数据
int sprintf ( char * str, const char * format, ... );
//将一个写一个format的数据到一个字符串str中
//将格式化的数据按照一定的格式转化成数据
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s bba = { "bai",18,156.2 };
char arr[500] = { 0 };
sprintf(arr, "%s %d %f", bba.name, bba.age, bba.high);
printf("%s", arr);//将bba里的内容以字符串的形式打印出来
return 0;
}
#include<stdio.h>
struct s
{
char name[20];
int age;
float high;
};
int main()
{
struct s switchd = { 0 };
char arr[500] = { 0 };
struct s bba = { "bai",18,156.2 };
sprintf(arr, "%s %d %f", bba.name, bba.age, bba.high);
sscanf(arr, "%s %d %f", switchd.name, &(switchd.age), &(switchd.high));
// arr中的字符串内容按照%s %d %f的格式取出放入switchd结构体中
printf("%s %d %5.2f", switchd.name, switchd.age, switchd.high);
//这里按照结构体的方式打印出来说明将字符串arr中的内容按照%s %d %f的形式转化并打音出来
return 0;
}
5.文件的随机读写
5.1 fseek
根据文件指针的位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
我们现在该程序内创建了name.txt的文件并写入内容:
SEEK_SET的效果:
#include<stdio.h>
int main()
{
FILE* pp = fopen("name.txt", "r");//二进制的方式读取
if (NULL == pp)
{
perror("fopen");
return;
}
fseek(pp, 3, SEEK_SET);//SEEK_SET为起始位置 表示从起始位置开始开始向后偏移3个位置
int ch = fgetc(pp);
printf("%c", ch);
fclose(pp);
pp = NULL;
return 0;
}
可以看到将name.txt内位置3的字符e打印出来
SEEK_END的效果:
#include<stdio.h>
int main()
{
FILE* pp = fopen("name.txt", "r");
if (NULL == pp)
{
perror("fopen");
return;
}
fseek(pp, -3, SEEK_END);//从末尾位置开始向前偏移3个位置
int ch = fgetc(pp);
printf("%c", ch);
fclose(pp);
pp = NULL;
return 0;
}
SEEK_CUR:
#include<stdio.h>
int main()
{
FILE* pp = fopen("name.txt", "r");
if (NULL == pp)
{
perror("fopen");
return;
}
int ch = fgetc(pp);
printf("%c\n", ch);
fseek(pp, 2, SEEK_CUR);//这时从当前位置开始向后移动2个位置
ch = fgetc(pp);
printf("%c", ch);
fclose(pp);
pp = NULL;
return 0;
}
5.2 ftell
long int ftell ( FILE * stream );
//返回文件指针相对于起始位置的偏移量
#include<stdio.h>
int main()
{
FILE* pp = fopen("name.txt", "r");
if (NULL == pp)
{
perror("fopen");
return;
}
int ch = fgetc(pp);//读取一个字符后向后偏移一个位置
printf("%c\n", ch);
fseek(pp, 2, SEEK_CUR);//这时从当前位置开始向后移动2个位置
ch = fgetc(pp);//读取一个字符后向后偏移一个位置
printf("%c\n", ch);
int x = ftell(pp);//1+2+1
printf("%d", x);
fclose(pp);
pp = NULL;
return 0;
}
5.3 rewind
void rewind ( FILE * stream );
//让文件指针的位置回到文件的起始位置
#include<stdio.h>
int main()
{
FILE* pp = fopen("name.txt", "r");
if (NULL == pp)
{
perror("fopen");
return;
}
int ch = fgetc(pp);
printf("%c\n", ch);
fseek(pp, 2, SEEK_CUR);
ch = fgetc(pp);
printf("%c\n", ch);
rewind(pp);//这里文件指针回到起始位置
ch = fgetc(pp);
printf("%c\n", ch);
fclose(pp);
pp = NULL;
return 0;
}
6.文本文件和二进制文件
在我们前面示范当中在文件内有些我们能够看懂为文本文件,但有些为乱码
例如:
这里的数据在内存中以二进制的形式存储,不加转换的输出到外存, 为二进制文件。
那么我们会想一个数据到底如何存入内存当中,***字符***一律以ASCII形式存储,***数值型数据***既可以用ASCII形式存储,也可以使用二进制形式直接存储。
这里我们用整数10000为例:
10000拆开为5个字符分别为1 0 0 0 0 ,因此在磁盘中占用5个字节,而如果以二进制的方式存入则为4个字节
7. 文件读取结束的判定
7.1 feof的错误使用和相关判定
int feof ( FILE * stream );
在我们日常判断文件结束时不能用feof函数的返回值来断定文件结束。
这个函数的正确用法是:当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
在读取时分为文本文件读取和二进制读取
在文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc读取正常返回读取的字符的ASCLL值
fgetc读取失败则返回 EOF .
fgets读取正常则返回读取到的地址
fgets读取失败返回 NULL .
在二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread 读取正常返回格式串中指定的数据的个数
fread读取失败返回小于格式串中指定的数据的个数
#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);//不等于EOF就打印一个字符
}
//判断是什么原因结束的
if (ferror(fp))//返回为真说明读取过程中发生i/o错误
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
#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 文件缓冲区
在ANSIC 标准采用“缓冲文件系统”处理的数据文件的,缓冲文件系统是指:系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装
满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓
冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根
据C编译系统决定的。
当然我们也可以强制放入硬盘当中。
所以有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文
件。