写通讯录程序时,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录,那么每次运行通讯录程序时都需要重新录入,会很麻烦。因此通讯录就应该把信息记录下来,只有主动删除数据时,数据才会不复存在,这就涉及数据持久化的问题。
一般数据持久化的方式有:把数据存放在磁盘文件、存放到数据库等方式。
磁盘上的文件是文件 :当把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中,这时候处理的就是磁盘上文件。
不过程序设计中,从功能角度来看,一般文件分为两种:程序文件、数据文件
(1)程序文件:windows环境中的源程序文件(.c)、目标文件(.obj)、可执行程序(.exe)。
(2)数据文件:文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本博客主要讲述数据文件。
文件名作为文件的唯一标识,包含3部分:文件路径+文件名主干+文件后缀
如:C:\code\test.txt
文件的名称是以显示文件扩展名为准的,如不勾选文件扩展名时,文件名称为file.txt
如果勾选文件扩展名,那么文件名称为file.txt.txt,这时候如果想打开file.txt文件,那么无论如何都会失败。
(1)文件类型指针
简称“文件指针”,每个被使用的文件,都在内存中开辟了相应的文件信息区,用来存放文件的相关信息(包括文件名、文件状态、文件位置等)。这些信息保存在FILE结构体变量中,FILE结构体类型是系统声明的。
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* pf;//文件指针变量
(2)文件的打开和关闭
//打开文件
FILE* open(const char* filename, const char* mode);
//关闭文件
int close(FILE * stream);
程序和文件读取、写入示意图:
打开方式有多种:
文件使用方式 |
含义 |
如果指定文件不存在 |
“r”(只读) |
为了使用数据,打开一个已经存在的文本文件 |
出错 |
“w”(只写) |
为了输出数据,打开一个文本文件 |
建立一个新的文件 |
“a”(追加) |
向文本文件末尾添加数据 |
出错 |
“rb”(只读) |
为了输入数据,打开一个二进制文件 |
出错 |
“wb”(只写) |
为了输出数据,打开一个二进制文件 |
建立一个新的文件 |
“ab”(追加) |
像一个二进制文件末尾添加数据 |
出错 |
“r+”(读写) |
为了读和写,打开一个文本文件 |
出错 |
“w+”(读写) |
为了读和写,建立一个新的文件 |
建立一个新的文件 |
“a+”(读写) |
打开一个文件,在文件末尾进行读写 |
建立一个新的文件 |
“rb+”(读写) |
为了读和写打开一个二进制文件 |
出错 |
“wb+”(读写) |
为了读和写,新建一个新的二进制文件 |
建立一个新的文件 |
“ab+”(读写) |
打开一个二进制文件,在文件末尾进行读和写 |
建立一个新的文件 |
在VS2019 解决方案下手动创建文件test.data
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("test.data", "r");
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("fopen test", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
如果使用绝对路径打开文件,向test.data文件里面写东西
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("C:\Users\Delia\source\repos\bit4\test.data", "w");//使用绝对路径打开文件
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("fopen test", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
路径会报错:
这是因为路径没有使用转义字符,会把路径中的\r 、\b、\t当做转义字符来处理,路径就找不到了,路径需要加上转义字符
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");//路径加上转义字符
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("fopen test", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行之后, 打开test.data文件,写入成功:
功能 |
函数名 |
适用于 |
字符输入函数(读取) |
fgetc |
所有输入流 |
字符输出函数(写入) |
fputc |
所有输出流 |
文本行输入函数(读取) |
fgets |
所有输入流 |
文本行输出函数(写入) |
fputs |
所有输出流 |
格式化输入函数(读取) |
fscanf |
所有输入流 |
格式化输出函数(写入) |
fprintf |
所有输出流 |
二进制输入(读取) |
fread |
文件 |
二进制输出(写入) |
fwrite |
文件 |
流的含义:
清除test.data的内容:使用w方式打开文件,并且不写入,会将文件中的内容清空
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,打开test.data文件,内容被清空:
int fputc ( int character, FILE * stream );//写入成功,则返回写入的字符
fputc支持所有输出流,所以fputc向文件和屏幕上都可以写入内容
(1)向文件写入内容:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputc('h', pf);
fputc('e', pf);
fputc('l', pf);
fputc('l', pf);
fputc('o', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,打开test.data文件,结果为:
(2)向屏幕输出信息:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
fputc('h', stdout);
fputc('e', stdout);
fputc('l', stdout);
fputc('l', stdout);
fputc('o', stdout);
}
ctrl+F5运行,将hello打印在屏幕上:
int fgetc ( FILE * stream );//读取成功则返回读取到的字符
fgetc支持所有输入流,fgetc从文件和屏幕上都可以读取内容
(1)fgetc从文件里面读取字符,先手动将test.data里面的内容改为world
读取文件内容前3个字符:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取
//文件操作
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
return 0;
}
ctrl+F5运行,读取到前3个字符
(2)fgetc也可以从标准输入读取字符
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int ret = fgetc(stdin);
printf("%c", ret);
ret = fgetc(stdin);
printf("%c", ret);
ret = fgetc(stdin);
printf("%c", ret);
return 0;
}
ctrl+F5运行,输入字符串,读取到前3个字符
fputc和fgetc每读一次,相当于指针+1, 读取结束会返回EOF(-1)。
int fputs ( const char * str, FILE * stream );//以文本(ASCII码)方式写入成功则返回一个非负值
fputs向所有输出流写入文本行
(1)向文件test.data文件中写入字符串:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//写文件-按照行来写
fputs("abcd", pf);
fputs("efg", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,两行字符串写入到test.data文件中。
如果需要换行,写入的字符串末尾加上换行符即可:
//写文件-按照行来写
fputs("abcd\n", pf);
fputs("efg", pf);
char * fgets ( char * str, int num, FILE * stream );//以文本方式读取字符,num是最大读取字符数,不定义时为99,最后一个为\0。
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
char arr[10] = { 0 };
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fgets(arr,4, pf);//实际上最多读取3个字符,最后一个字符是\0
printf("%s\n", arr);
fgets(arr, 4, pf);//实际上最多读取3个字符,最后一个字符是\0
printf("%s\n", arr);
fgets(arr, 4, pf);//实际上最多读取3个字符,最后一个字符是\0
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,结果如下:
把格式化数据写入到流中
int fprintf ( FILE * stream, const char * format, ... )//读取成功就返回读取到的字符个数,否则返回负数
返回值为int的原因有两个:字符是用ASCII码表示的;函数在读取失败会返回EOF(-1),char(1-255)没办法存-1,int可以存-1。
fprintf和printf对比,fprintf比printf多了一个参数:FILE流参数。
使用fprintf把格式化数据写入到流中
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char c[20];
int i;
float f;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
//对格式化的数据进行写文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f", "hello", 5, 2.6);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5,查看运行结果:
从流中读取格式化数据
int fscanf ( FILE * stream, const char * format, ... );//根据格式读取并存储流中的数据
fscanf和scanf对比,fscanf比scanf多了一个参数:FILE流参数。
使用fscanf把格式化数据读取到流中
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char c[20];
int i;
float f;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
//对格式化的数据进行写文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fscanf(pf, "%s %d %f", s.c, &(s.i), &(s.f));
//打印数据
//printf("%s %d %f", s.c, s.i, s.f);
fprintf(stdout,"%s %d %f", s.c, s.i, s.f);//使用fprintf函数把数据输出到标准屏幕,作用同上一句printf
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5,查看运行结果:
以二进制的形式把count个大小为size的数据写入到流中
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );//返回成功写入的元素个数
使用fwrite把数据块写入到流中:
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char c[20];
int i;
float f;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
//数据块进行写文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");//写入
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5,打开test.data文件,发现字符串以二进制的形式和以文本的形式写进去的内容是一样的,都是abcdef,整数和浮点数以二进制的形式和以文本的形式写进去的内容是不一样的:
从流中读取count个大小为size的数据到buffer里面去
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );//返回成功读取的元素个数
使用fread从流中读取数据块
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char c[20];
int i;
float f;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
//数据块进行写文件
FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f", s.c, s.i, s.f);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,结果如下:
把格式化数据写入到字符串
int sprintf ( char * str, const char * format, ... );//执行成功则返回写入的字符个数
sprintf和printf对比,sprintf比printf多了一个参数:字符串
从字符串读取格式化数据
int sscanf ( const char * s, const char * format, ...);//返回成功读取的字符的个数
sscanf和scanf对比,sscanf比scanf多了一个参数:字符串
使用sprintf 先把一个格式化的数据,转换成字符串,再使用sscanf从字符串中还原出一个结构体数据
#define _CRT_SECURE_NO_WARNINGS 1
#include
struct S
{
char c[20];
int i;
float f;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
struct S tmp = { 0 };
char buf[100] = { 0 };
//sprintf 把一个格式化的数据,转换成字符串
sprintf(buf, "%s %d %f", s.c, s.i, s.f);
printf("%s\n", buf);//以字符串形式打印
//sscanf从字符串中还原出一个结构体数据
sscanf(buf, "%s %d %f", tmp.c, &(tmp.i), &(tmp.f));
printf("%s %d %f\n", tmp.c,tmp.i,tmp.f);
return 0;
}
ctrl+F5运行,结果如下:
对比scanf/fscanf/sscanf 和 printf/fprintf/sprintf
文件指针在读文件时,定位文件内容不断发生变化,如果不想顺序读取呢?可以进行文件随机读写
根据文件指针的位置和偏移量来定位文件指针
int fseek ( FILE * stream, long int offset, int origin );//成功则返回0,否则返回非0
test.data中存放的是abcdef
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
FILE* pf = fopen("test.data", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
//调整文件指针
fseek(pf, 1, SEEK_CUR);//当前指针指向b,向后偏移一位,指向c
ch = fgetc(pf);
printf("%c\n", ch);//c
fclose(pf);
pf = NULL;
return 0;
}
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );//若成功则返回指针位置的值
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
FILE* pf = fopen("test.data", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
//调整文件指针
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//b
//计算指针相对于起始位置的偏移
int ret = ftell(pf);
printf("%d\n", ret);
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,结果如下所示:
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
FILE* pf = fopen("test.data", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
ch = fgetc(pf);
printf("%c\n", ch);//c
//调整文件指针
fseek(pf, -2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);//b
//计算指针相对于起始位置的偏移
int ret = ftell(pf);
printf("%d\n", ret);
//让文件指针回到起始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);//
fclose(pf);
pf = NULL;
return 0;
}
ctrl+F5运行,结果如下所示:
根据数据的组织形式,数据文件被称为文本文件或者二进制文件:数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节,31 30 30 30 30),而二进制形式输出,则在磁盘上只占4个字节(10 27 00 00)
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int a = 10000;
FILE* pf = fopen("test.data", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&a, sizeof(int), 1, pf);
return 0;
}
VS2019解决方案-源文件-添加-现有项,打开test.data文件
右击文件名-打开方式-二进制编辑器-确定
二进制打开文本文件:
feof用于判断当文件读取结束的时候,是读取失败结束,还是遇到文件尾结束。
int feof ( FILE * stream );//返回非0则遇到文件尾结束,返回0则读取失败结束
所以使用feof时,文件一定读取结束了,因此不能用feof的返回值判断文件是否结束。
新建test1.data文本,向test.data里面存放的内容:
将test.data中的内容写入到test1.data中:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
FILE* pfread = fopen("test.data", "r");
if (pfread == NULL)
{
return 1;
}
FILE* pfwrite = fopen("test1.data", "w");
if (pfread == NULL)
{
fclose(pfread);
pfread = NULL;
return 1;
}
//文件打开成功
//读写文件
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
//写文件
fputc(ch, pfwrite);
}
if (feof(pfread))
{
printf("遇到文件结束标志,文件正常结束\n");
}
else if(ferror(pfread))
{
printf("文件读取失败结束\n");
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
ctrl+F5运行,结果如下:
且test.data的内容已经全部拷贝到test1.data中:
由此可以看出 ,feof返回非0,且遇到文件尾结束的。
文本文件读取结束和二进制文件读取结束的判断不同:
(1)文本文件读取是否结束,判断返回值是否为EOF(fgetc),或NULL(fgets)。上面代码就是判断文本文件读取是否结束。
fgetc函数在读取结束的时候,会返回EOF;正常读取的时候,返回的是读取到的字符的ASCII码值。
fgets函数在读取结束的时候,会返回NULL,正常读取的时候,返回存放字符串的空间起始地址。
(2)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
fread函数在读取的时候,返回的是实际读取到的完整元素的个数。如果发现读取到的完整元素的个数小于指定的元素个数,这就是最后一次读取了。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int main()
{
FILE* pf = fopen("test.data", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.data文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.data文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
ctrl+F5运行,打开test.data,文件为空
过10秒后,打开test.data,发现内容已经写入进去了,是从缓冲区写到文件的。