目录
我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复在。
这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
——————————————————————————————————————————————————
1 . 文件的打开和关闭
,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;
每个被使用的文件都在内存中开辟了一个相应的 ==文件信息区 == ,用来存放文件的相关信息(如文件的名
字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
fopen & fclose
fopen的第一个参数是文件夹的位置,第二个参数是打开的方式,返回的值的类型是指向FILE类型的指针。
fclose的第一个参数FILE类型的指针。
每次打开文件的时候都会有一个文件信息区的创建,同时会返回这个文件信息区的起始地址。
这些是文件的打开方式,前三个是常用的:
#include<stdio.h>
#include<errno.h>
#include<errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
——————————————————————————————————————————————————
2.文件的顺序读写
文件的读操作
在工程的文件夹下创建一个名为test.txt的文本文件,文件中写入abcdefg。运行下面的程序,第一次读到a,第二次读到b。这个时候文件指针指向了c的位置。
#include<errno.h>
#include<stdio.h>
#include<string.h>
int main()
{
FILE * pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
fclose(pf);
pf = NULL;
return 0;
}
读一行:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//
char buf[1000] = { 0 };
fgets(buf,1000,pf);//1000为读取的个数
printf("%s", buf);
fclose(pf);
pf = NULL;
return 0;
}
文件的写操作
c语言程序,只要运行起来,就默认打开三个流:
1 stdin 标准输入流
2 stdout 标准输出流
3 stderr 标准错误流
它们的类型都是FILE※类型,可以看到:在写文件时,如果我的参数是pf,就写到额文件中,如果我的参数是stdout表示标准输出流,那么我的结果就打印到了屏幕上。不论是哪一种方式,都是输出。
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);//输入到文件
fputc(ch, stdout);//输入到显示屏
}
fclose(pf);
pf = NULL;
return 0;
}
那如果我要写一行呢?
以只读的方式打开这个文件,会将原文件的内容全部覆盖掉。
注意写一行的时候,使用的是双引号
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("hello world!\n", pf);
fputs("hello\n ", pf);
fclose(pf);
pf = NULL;
return 0;
}
——————————————————————————————————————————————————
文件的拷贝
int main()
{
FILE* pr = fopen("data.txt", "r");
if (pr == NULL)
{
printf("open for reading:%s\n", strerror(errno));
return 0;
}
FILE* pw = fopen("data2.txt", "w");
if (pw == NULL)
{
printf("open for writing:%s\n", strerror(errno));
fclose(pr);
pr = NULL;
return 0;
}
//拷贝文件
int ch = 0;
while ((ch = fgetc(pr)) != EOF)
{
fputc(ch, pw);
}
//关闭文件
fclose(pr);
pr = NULL;
fclose(pw);
pw = NULL;
return 0;
}
格式化输出函数 fscanf、fprintf
scanf 从 标准输入流 (stdin)上进行格式化输入的函数
printf 向 标准输出流(stdout)上进行格式化的输出函数
fscanf 可以从标准输入流、指定的文件流上读取格式化的数据
fprintf 把数据按照格式化的方式输出到标准输出流或指定的文件
如果需要将结构体中的数据写入到文件pf中,那么以写的方式打开文件,使用函数fprintf写入文件。
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三",20,95.3 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//xie
fprintf(pf, "%s %d %lf", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
如果需要将文件中的数据写入到结构体,那么以读的方式打开文件,使用函数fscanf写入文件。
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//du
fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.d));
printf("%s %d %lf\n", s.name, s.age, s.d);
fclose(pf);
pf = NULL;
return 0;
}
二进制的读写
二进制写
因为在写数据的时候,没有人知道你是要写入整型还是结构体还是其他的,所以统一使用void※接收,第二个参数是元素的大小,第三个参数是写的次数,第四个参数是文件的位置。
文件打开的方式是以二进制的形式打开,所以应该是“wb”,这里要注意。
使用fwrite 函数,第一个参数来自结构体变量s;第二个参数为元素的大小;第三个参数是写的次数,结构体数组中有两个元素,所以可以写两次;第四个参数是文件pf的位置。
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s[2] = { {"张三",20,95.3},{"李四",22,99.8} };
FILE* pf = fopen("data.txt", "wb");//打开一个二进制文件
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//按照二进制的方式写文件
fwrite(&s, sizeof(struct Stu), 2, pf);
//文本中,以二进制的方式写入,而记事本是以文本的方式解析,所以是乱码
fclose(pf);
pf = NULL;
return 0;
}
我们是以二进制的方式写入的,而记事本是以文本的方式来解析其中的内容,那么解析出来的就当然是乱码。
既然二进制文件我们看不懂,但我们有办法啊。
我们可以使用二进制读的方式读取文件。
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s[2] = { 0 };
FILE* pf = fopen("data.txt", "rb");//打开一个二进制文件
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//按照二进制的方式读文件
fread(s, sizeof(struct Stu), 2, pf);
printf("%s %d %lf\n", s[0].name, s[0].age, s[0].d);
printf("%s %d %lf\n", s[1].name, s[1].age, s[1].d);
fclose(pf);
pf = NULL;
return 0;
}
sscanf 和 sprinft
scanf 从 标准输入流 (stdin)上进行格式化输入的函数
printf 向 标准输出流(stdout)上进行格式化的输出函数
sscanf 可以把一个字符串中提取(转化)出格式化数据
sprintf 把一个格式化的数据转化成字符串
也就是说fscanf和fprintf的功能要更加强大!
下面我们来试一试:
使用sprintf()把内容写入buf中,然后使用printf打印出来。
struct Stu
{
char name[20];
int age;
double d;
};
int main()
{
struct Stu s = { "张三",20,95.3};
char buf[100] = { 0 };
sprintf(buf,"%s %d %lf",s.name,s.age,s.d);
printf("%s\n", buf);
//printf("%s %d %lf",s.name,s.age,s.d);
return 0;
}
sscanf和sprintf是一对,接下来从buf字符串中按照格式要求读取内容(格式:tmp.name, &(tmp.age), &(tmp.d)),其中对于字符串类型,本身传入的就是第一个值的地址,所以不需要取地址操作符,而其他类型的需要使用取地址操作符。
读取的内容放到一个tmp的结构体变量中,通过printf()函数可以观察是否拷贝成功!
int main()
{
struct Stu s = { "张三",20,95.3};
struct Stu tmp = { 0 };
char buf[100] = { 0 };
sprintf(buf,"%s %d %lf",s.name,s.age,s.d);
printf("%s\n", buf);
sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.d));
printf("%s %d %lf", tmp.name, tmp.age, tmp.d);
return 0;
}
——————————————————————————————————————————————————
3.文件的随机读写
文件的随机读写也就是说我可以在文件的任意位置进行读写。
3.1 fseek
在工程的文件夹下创建一个名为test.txt的文本文件,文件中写入abcdefg。运行下面的程序,第一次读到a,然后指针向后偏移一位;第二次读到b,再次偏移一位;这个时候文件指针指向了c的位置,然后通过fseek函数,向后从当前指针位置(SEEK_CUR)向后偏移三个位置,到了f,然后再次读取的时候,就读取到了f。
int main()
{
FILE * pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
//定位文件指针
fseek(pf, 3, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//f
fclose(pf);
pf = NULL;
return 0;
}
当然,使用fseek(pf, -2, SEEK_END);也可以从后往前数,实现任意位置的偏移。
如这一个程序所示:首先在文件中写入从a到z的字母,然后定位到倒数第二个元素y,把y改写成#
这个程序要求使用者对里面的内容非常熟悉。
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//定位文件指针
fseek(pf, -2, SEEK_END);
fputc('#', pf);
fclose(pf);
pf = NULL;
return 0;
}
3.2 ftell
返回文件指针相对于起始位置的偏移量
3.2.1 rewind
让文件指针的位置回到文件的起始位置
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
int ret = ftell(pf);
printf("%d\n", ret);//2
rewind(pf);
//fseek(pf, 0, SEEK_SET);
ret = ftell(pf);
printf("%d\n", ret);//0
fclose(pf);
pf = NULL;
return 0;
}
——————————————————————————————————————————————————
4.文本文件和二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
例如:10000这个数字,以二进制的形式和ASCII码的形式存储是不一样的。
将10000以二进制的形式写入text.txt中:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("text.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中,4个字节,写1次
fclose(pf);
pf = NULL;
return 0;
}
以文本的形式打开这个文件可以看到是乱码:
那么该如何查看呢??
在源文件中添加现有项,然后选择打开方式->二进制编码器:
最后的结果如图所示:
解释一下:
——————————————————————————————————————————————————
5.文件读取结束的判定
判断文件是否结束
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL . - 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
feof 函数
当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
——————————————————————————————————————————————————
6.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据,会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
其实,我们使用 printf ()函数在屏幕上打印信息的时候,期间还有许许多多的过程,printf()函数调用了系统的API,(在缓存区存放着,)然后让操作系统在屏幕上打印信息。
下面的程序可以演示一下缓存区的效果:
#include <stdio.h>
#include <windows.h>
//VS2019 WIN10环境测试
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;
}