使用文件的原因
当在程序中输入一些内容并保存时,如果程序关闭,内容就会消失。为了杜绝此类事情发生,我们可以使用文件,将数据持久化,把数据放在磁盘文件或数据库中,只有我们选择自己删除数据的时候,数据才会消失,否则数据会一直保存,使用文件可以将数据直接存在电脑的硬盘上,使数据持久化。此篇博客主要讨论数据文件。
文件
磁盘上的文件是文件,但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
程序文件
程序文件包括源程序文件(后缀.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件或者输出文件的内容
![在这里插入图片描述](https://img-blog.csdnimg.cn/aff5fc184b9f4adfa71368c66c313d75.jpeg#pic_center)
文件名
一个文件有一个唯一的文件标识,文件名包含3个部分:文件路径+文件名主干+文件后缀,例如:c:\program\text.txt ,包含路径使相同的文件名更好区分
文件的打开和关闭
文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等),这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;//FILE重命名产生的结构体类型
![在这里插入图片描述](https://img-blog.csdnimg.cn/4b8242956b9c47939399a20910ec0590.jpeg#pic_center)
1.不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异
2.每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。一般都是通过一个FILE的指针 FILE* pf 来维护这个FILE结构的变量,这样使用起来更加方便
3.定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/f593336113674bcc930a9dbb68604371.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAVE5TRQ==,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)
文件的打开和关闭
FILE * fopen ( const char * filename, const char * mode )
参数:filename:C 字符串,要打开的文件的名称
mode:以什么方式打开
返回值:如果文件已成功打开,该函数将返回一个指向 FILE 对象的指针,该指针可用于在将来的操作中标识流。否则,将返回空指针
注意:1.文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
2.在打开文件同时,会返回一个FILE*的指针变量指向该文件
int fclose ( FILE * stream )
参数:stream 指向指定要关闭的流的 FILE 对象的指针
返回值:如果流成功关闭,则返回零值。失败时,返回 EOF。
注意:1.使用完fclose后,要将指向FILE的指针置为空指针NULL
打开方式:
文件使用方式 |
含义 |
如果指定文件不存在 |
"r"(只读) |
为了输入数据,以读模式打开一个已经存在的文本文件 |
出错 |
"w"(只写) |
为了输出数据,以写模式打开,清空现有的文件长度,如果文件不存在,则创建一个新的文件 |
建立一个新文件 |
"a"(追加) |
以写模式打开文件,会在现有文件末尾添加内容,如果文件不存在,则创建一个新的文件 |
建立一个新文件 |
"r+"(读写) |
以更新模式打开文件,可以读和写 |
|
"w+" |
以更新模式打开文件,可以读和写,如果文件存在,清空现有的文件长度,如果文件不存在,则创建一个新的文件 |
|
"a+" |
以更新的模式打开文件,可以读和写,在现有文件末尾添加内容,如果文件不存在,则创建一个新的文件,可以读取整个文件,但是只能从末尾添加内容 |
|
“rb”(只读)、“wb”(只写)、“ab”(追加)、“rb+”(读写)、“wb+”(读写)、“ab+”(读写) |
与以上相关模式类似,不过是以二进制模式,而不是文本模式打开文件 |
|
打开方式:
#include <stdio.h>
int main()
{
FILE* pf = fopen("project.txt", "w");//前提是有此文件,否则会报错
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("Hello World!", pf);
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/eae6597e88f04c8eac9716cbe3e3e203.jpeg#pic_center)
1.默认创建的project.txt在该程序所对应的文件夹下,是相对路径
2.如果没有这个project.txt,以r的方式打开,程序会报错No such file or directory
文件的顺序读写
函数 |
函数名 |
适用 |
补充 |
字符输入函数 |
fgetc |
所有输入流 |
字符输出函数 |
fputc |
所有输出流 |
文本行输入函数 |
fgets |
所有输入流 |
文本行输出函数 |
fputs |
所有输出流 |
格式化输入函数 |
fscanf |
所有输入流 |
格式化输出函数 |
fprintf |
所有输出流 |
二进制输入 |
fread |
文件 |
二进制输出 |
fwrite |
文件 |
fputc:
#include <stdio.h>
int main()
{
FILE* pf = fopen("project.txt", "w");//前提是有此文件,否则会报错
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/234c3332cb0b402786425d436e6f37ad.jpeg#pic_center)
1.1.fputc:向文件中输出字符,一次写一个
int fputs ( const char * str, FILE * stream ):将 str 所指向的 C 字符串写入流
成功后,将返回非负值。
出错时,该函数返回 EOF 并设置错误指示器 (ferror)
fgetc:
#include <stdio.h>
int main()
{
FILE* pf = fopen("project.txt", "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 \n", ret);
int ch = 0;
while ((ch = getc(pf)) != EOF)//一块读
{
printf("%c ", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/338c1f69eb464ff78e54a9f0df0b12bf.jpeg#pic_center)
1.向程序中输入字符,一次读一个
2.int fgetc ( FILE * stream ):从流中获取字符
成功后,将返回读取的字符(提升为 int 值)。
返回类型为 int 以适应特殊值 EOF,这表示失败
fputs:
#include <stdio.h>
int main()//写一行数据
{
FILE* pf = fopen("project.txt", "a");//w会清空之前的内容,用a会追加
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("Keep Running ",pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/a9f84342cd1d45f2baeffc25c3b5387b.jpeg#pic_center)
1.将 str 所指向的 C 字符串写入流
2.成功后,将返回非负值。出错时,该函数返回 EOF 并设置错误指示器 (ferror)
3.以w的模式写,会清空之前的内容,以a的模式写,会追加在之前的内容后
fgets:
#include <stdio.h>
int main()//读一行数据
{
FILE* pf = fopen("project.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
char arr[10];
fgets(arr, 4, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/bb0e27e344144c178ef6ee9796f681bc.jpeg#pic_center)
1.虽然读的是4个,实际是3个,还有一个’\0’
2.从流中读取字符并将其作为 C 字符串存储到 str 中,直到读取 (num-1) 字符或达到换行符或文件末尾(以先发生者为准)
fprintf:
#include <stdio.h>
typedef struct STU
{
char name[20];
int age;
double score;
}S;
int main()//以格式化形式输出数据
{
S stu = { "张三",18,90.0f };
FILE* pf = fopen("project.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写到文件中
fprintf(pf, "%s %d %f", stu.name, stu.age, stu.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/adaa5a4b51944d7a81213a6c364d8961.jpeg#pic_center)
1.以格式化的形式输出到文件中去
2.将格式所指向的 C 字符串写入流。如果 format 包含格式说明符(以 % 开头的子序列),则 format 后面的其他参数将被格式化并插入到生成的字符串中,以替换它们各自的说明符
fscanf:
#include <stdio.h>
typedef struct STU
{
char name[20];
int age;
double score;
}S;
int main()//以格式化形式输入数据
{
S stu = { 0 };
FILE* pf = fopen("project.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文中
fscanf(pf, "%s %d %lf", stu.name, &(stu.age), &(stu.score));
printf("%s %d %lf", stu.name, stu.age, stu.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/06322f540dc9458a8dc8ffb1354428a6.jpeg#pic_center)
1.从流中读取数据,并根据参数格式将其存储到附加参数所指向的位置
总结
![在这里插入图片描述](https://img-blog.csdnimg.cn/b65c6e238ac1426f9c34b2af17e272eb.jpeg#pic_center)
任何一个c程序,只要运行起来,就会默认打开三个流,都是FILE*类型:
stdin:标准输入流(键盘)
stdout:标准输出流(屏幕)
stderr:标准错误流(屏幕)
所以使用scanf和printf时,可以直接使用,不需要打开屏幕,而写文件则需要打开文件获取这个流,然后写或读数据
而fprintf适用于所有输出流,故可以在屏幕上打印,如下图所示
![在这里插入图片描述](https://img-blog.csdnimg.cn/fde0c8dd26d846a59e9ef0069a33130c.jpeg#pic_center)
fwrite:
#include <stdio.h>
typedef struct STU
{
char name[20];
int age;
double score;
}S;
int main()
{
S stu = { "张三",18,100.0f };
//以二进制形式写到文件中
FILE* pf = fopen("project.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制方式写
fwrite(&stu, sizeof(stu), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/3c8e750b5e374cdf84d92266d6b4f417.jpeg#pic_center)
因为是以二进制的形式写的,所以打开文件后是乱码
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream )
ptr:要写的元素来自哪里,指向要写入的元素数组的指针
size:要写入的每个元素的大小(以字节为单位)
count:元素数,每个元素的大小为字节大小
count=1 ,一次读1个结构体大小的数据,相当于读一次结构体中的所有内容
fread:
#include <stdio.h>
typedef struct STU
{
char name[20];
int age;
double score;
}S;
int main()
{
S stu = { 0 };
FILE* pf = fopen("project.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制方式读
fread(&stu, sizeof(stu), 1, pf);
printf("%s %d %lf", stu.name, stu.age, stu.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/547296794b0e43c8935f3a0322b79749.jpeg#pic_center)
sfrintf:
#include <stdio.h>
typedef struct STU
{
char name[20];
int age;
double score;
}S;
int main()
{
S stu = { "张三",18,90.0f };
S stu1 = { 0 };
char buf[100] = { 0 };
//把stu中的格式化数据转化成字符串放到buf中
sprintf(buf, "%s %d %lf", stu.name, stu.age, stu.score);
printf("%s",buf);
printf("\n");
//从字符串buf中获取一个格式化的数据到stu1中
sscanf(buf, "%s %d %lf", stu1.name, &(stu1.age), &(stu1.score));
printf("%s %d %lf", stu1.name, stu1.age, stu1.score);
return 0;
}
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/c3b7c6071ba642e5b20e6664a4bae438.jpeg#pic_center)
1.此时buf中的内容已经变成了字符串"张三 18 90.000000",所以用%s打印
scanf:是针对标准输入的格式化输入语句
printf:是针对标准输出的格式化输出语句
fscanf:是针对所有输入流的格式化输入语句
fprintf:是针对所有输出流的格式化输出语句
ssanf:从一个字符串中转化成一个格式化的数据
ssprintf:把一个格式化的数据转化成字符串
文件的随机读写
fseek
根据文件指针的位置和偏移量来定位文件指针
int fseek ( FILE * stream, long int offset, int origin )
offset:要从源偏移的字节数
origin的三个参数
SEEK_CUR :文件指针当前位置
SEEK_END :文件结尾
SEEK_SET :文件开头
ftell
返回文件指针相对于起始位置的偏移量
long ftell(FILE* stream );
rewind
让文件指针的位置回到起始位置
void rewind ( FILE * stream );
下面代码是关于以上三个函数的使用
#include <stdio.h>
int main()
{
char arr[10] = { 0 };
FILE* pf = fopen("project.txt", "r");//文件中已有ABCDEF
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);//A
//调整文件指针
fseek(pf, 2, SEEK_SET);//相对于起始位置偏移量
ch = fgetc(pf);
printf("%c\n", ch);//C
fseek(pf, 2, SEEK_CUR);//执行完上术代码,已经指向D,D的偏移量是0
ch = fgetc(pf);
printf("%c\n", ch);//F,然后指向F后
int ret = ftell(pf);
printf("%d\n", ret);//相对于起始位置的偏移量6
fseek(pf, -3, SEEK_END);//相对于最后位置偏移量
ch = fgetc(pf);
printf("%c\n", ch);//D
ret = ftell(pf);//然后指向E
printf("%d\n", ret);//相对于起始位置的偏移量4
//回到起始位置
rewind(pf);
fgets(arr, 7, pf);
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
输出结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/d33767a2dd9e480cb8dd37e69e2d0cf2.jpeg#pic_center)
文本文件和二进制文件
- 数据文件被称为文本文件或者二进制文件,数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件,如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件
- 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储
文件读取结束的判定
feof:判断结束的原因,已经结束了,读到文件尾结束
ferror:判断结束的原因,已经结束了,读的时候是否遇到错误而结束
- 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )例如:fgetc 判断是否为 EOF .fgets 判断返回值是否为 NULL
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。例如:fread判断返回值是否小于实际要读的个数
文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的,因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题
![在这里插入图片描述](https://img-blog.csdnimg.cn/cde568171d6b46279141756c365fbdac.jpeg#pic_center)
只有缓冲区放满才能读和写