目录
1) fputc - 向文件输出一个字符(写一个字符到文件里)
1)fputs - 向文件输出一个字符串 (写入一行字符串到该文件中)
1)fseek - 可以以(文件起始位置、当前位置、文件末尾)为起点,通过偏移量让指针向前或向后移动访问数据。
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
一、 为什么使用文件
我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数 据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯 录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。 这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据 库等方式。使用文件我们可以将数据 直接存放在电脑的硬盘 上,做到了数据的持久化。
二. 什么是文件
磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
2.2 数据文件
而我们这里的文件管理,指的是 数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显 示器上。 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理 的就是磁盘上文件。
例如: c:\code\test.txt为了方便起见,文件标识常被称为 文件名 。c:\code\ test .txt文件路径 文件名主干 文件后缀
文件路径:相对路径 和 绝对路径
1)相对路径:直接就是文件名主干+文件后缀(没有写具体的哪个盘,下面的哪个路径)该文件的位置放置在, 本源文件(.c)的同路径下(相对路劲默认放在:工程文件下)例如:我的源文件在该路径下(..\test_3_28\test_3_28)
用相对路径创建的文件就在该路径下![]()
FILE* pf = fopen("test .txt", "w");
2)绝对路径:开头是以 磁盘名开头\..\ 文件名主干 文件后缀例如:FILE* pf = fopen("c:\code\test .txt", "w");
三. 文件的打开和关闭
3.1 文件指针
缓冲文件系统中,关键的概念是 “ 文件类型指针 ” ,简称 “ 文件指针 ” 。每个被使用的文件都在内存中开辟了一个相应的 文件信息区 ,用来存放文件的相关信息(如文件的名 字,文件状态及文件当前的位置等)。这些信息是保存在一个 结构体变量 中的。 该结构体类型是由系统 声明的 ,取名 FILE .vs2013系统中是这样声明该结构体类型的: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 * pf ; // 文件指针变量定义pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变 量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联 的文件 。
3.2 文件的打开和关闭
文件在读写之前应该先 打开文件 ,在使用结束之后应该 关闭文件 。在编写程序的时候,在打开文件的同时,都会返回一个FILE* 的指针变量指向该文件,也相当于建立了指 针和文件的关系。ANSIC 规定使用 fopen函数 来打开文件 , fclose 来关闭文件
// 打开文件FILE * fopen ( const char * filename , const char * mode );// 关闭文件int fclose ( FILE * stream )
文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件“a”(追加) 向文本文件尾添加数据 建立一个新的文件“rb”(只读) 为了输入数据,打开一个二进制文件 出错“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件“ab”(追加) 向一个二进制文件尾添加数据 出错“r+”(读写) 为了读和写,打开一个文本文件 出错“w+”(读写) 为了读和写,建立一个新的文件 建立一个新的文件“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件“rb+”(读写) 为了读和写打开一个二进制文件 出错“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件
1、相对路径打开
#include <stdio.h>
#include<string.h>
#include<errno.h>
//********* 文件的打开与关闭
/*
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
*/
int main()
{
//打开文件(此时该相对路径下没有该文件,则创建一个新文件)
FILE* pf = fopen("test.txt", "w");
//FILE* pf = fopen("123.txt", "r");//如果该文件夹不存在,采用r只读,打开会报错
if (pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
//关闭文件
fclose(pf);
pf == NULL;
}
最开始:没有test.txt文件
执行程序后:生成了test.txt
同样的:如果尝试读取一个不存在的文件,返回null空指针,报错
2、绝对路径打开
例如:在桌面创建打开一个文件(C:\Users\27224\Desktop)
在使用绝对路径时,因为\本身是转义字符的标志,所以有时候会将该路径理解成别的意思。
所以要用\\ 来表示 \ 。(C:\\Users\\27224\\Desktop)
否则编译器会提示语法错误
3.以w写文件的方式打开(注意)
如果,该文件不存在,则建立一个新文件。
如果存在,则直接创建新的同名文件,覆盖掉。(所以有时候,一个文件里面放着内容,你打开同名的文件,此时,原文件就新的同名文件被覆盖了,内容也没了,是个新的空文件)
例子:
假设这里有个文件test.txt,在里面写上一些内容
然后,再以 w 的方式打开同名的文件
鼠标点击该文件,发现:空了
四. 文件的顺序读写
功能 函数名 适用于字符输入函数 fgetc 所有输入流字符输出函数 fputc 所有输出流文本行输入函数 fgets 所有输入流文本行输出函数 fputs 所有输出流格式化输入函数 fscanf 所有输入流格式化输出函数 fprintf 所有输出流二进制输入 fread 文件二进制输出 fwrite 文件
1、fgetc (读一个字符)与fputc(写一个字符)
1) fputc - 向文件输出一个字符(写一个字符到文件里)
int main() { //以 w 打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //fputc 向文件输出一个字符(写一个字符到文件里) fputc('a', pf);//写一个字符a到pf指向的文件里 //关闭文件 fclose(pf); pf = NULL; return 0; }
鼠标点进文件,的确把字符a写入到test.txt文件里了
通过fputc输出26个小写字母,就只需要运用一个循环就好了
int main() { //以 w 打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //fputc 向文件输出一个字符(写一个字符到文件里) //fputc('a', pf);//写一个字符a到pf指向的文件里 //fputc 向文件输出26个小写字母(循环) int i = 0; for (i = 0; i < 26; i++) { fputc('a' + i, pf); } //关闭文件 fclose(pf); pf = NULL; return 0; }
![]()
2)fgetc - 从文件中读取1个字符
//fgetc - 读文件操作 int main() { //test.txt在上一步已经创建,且写入了内容,这一次以只读的方式打开,不改变里面的内容 //以 r 的方式打开文件,只读 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //fgetc 从文件中读取一个字符 int ch = fgetc(pf); printf("%c\n", ch); //关闭文件 fclose(pf); pf = NULL; return 0; }
同样的,从文件中读取26个字符(通过循环)
int main() { //test.txt在上一步已经创建,且写入了内容,这一次以只读的方式打开,不改变里面的内容 //以 r 的方式打开文件,只读 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //fgetc 从文件中读取一个字符 //int ch = fgetc(pf); //printf("%c\n", ch); //fgetc 读取26个字符 (循环) int i = 0; int ch = 0; for (i = 0; i < 26; i++) { ch = fgetc(pf); printf("%c\n", ch); } //关闭文件 fclose(pf); pf = NULL; return 0; }
注意:
fgetc本身自带的功能,就是有一个指针指向第一个字符,读完一个字符,指针向后移动,就自动就读下一个字符了。
如果写pf++;这样是错的,打错特错!!
首先pf指针指向的是,该文件对应的文件信息区,一旦移动,
就找不到该区域了,无法进行打开关闭以及读写的操作了。
其次,fgetc自带这个功能,不用搞这种事情。
2.fgets和fputs
1)fputs - 向文件输出一个字符串 (写入一行字符串到该文件中)
int main() { //以 w 打开文件(由于之前已经创建过test.txt,这一次直接覆盖为空文件) FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //fputs - 向文件输出一个字符串 (写入一行字符串到该文件中) fputs("hello bit\n", pf); //fputs 连续输入多行时,不会自动换行,所以要自己加上\n fputs("nihao\n", pf); fputs("lalalala\n", pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
写一行是这样的
写两行(多行)是紧接着上一句后面写(不会自动换行)
2)fgets - 读一行字符串
如果想打印n个字符,这里的数字就要写上n+1,因为这个数字包含了\0
例如:
这些函数使用与所有流(标准化输入流(键盘)、标准化输出流(屏幕)、文件流)
刚才我们时针对文件流进行的操作,现在,试着对标准化输入输出流操作一下
struct S { char name[20]; int age; float f; }; int main() { //fputc('a', pf);//对文件流操作 //fputc('a', stdout);//对标准化输出流操作 - 将字符a输出到屏幕上 //fputc('b', stdout); //fputc('c', stdout); //fputc('d', stdout); // fputs("abcdefghi",pf);//对文件流操作 //fputs("abcdefghi",stdout);//对标准化输出流操作 // struct S s = { 0 }; fscanf(stdin, "%s %d %f", s.name, &s.age, &s.f);//标准化输入lisi,22,5.5f fprintf(stdout, "%s %d %f\n", s.name, s.age, s.f);//标准化输出 return 0; }
4.1 对比一组函数:
scanf/fscanf/sscanfprintf/fprintf/sprintf这里演示讲解这句函数的使用和对比
1.scanf 与 printf
![](https://img-blog.csdnimg.cn/6f793a3863564193a11365d0b0cecde0.png)
2.fscanf 与 fprintf
![](https://img-blog.csdnimg.cn/6ccd4835114e4794a1409830cd232c4f.png)
struct S
{
char name[20];
int age;
float f;
};
int main()
{
//创建一个数据类型为struct S的变量
struct S s = { "zhangsan",34,3.14f };
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fprintf写入文件
fprintf(pf, "%s %d %f\n", s.name, s.age, s.f);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2)fscanf - 读文件
struct S
{
char name[20];
int age;
float f;
};
int main()
{
struct S s = { "zhangsan",34,3.14f };
FILE* pf = fopen("test.txt", "r");//以读的形式打开
if (pf == NULL)
{
perror("fopen");
return 0;
}
//fscanf读文件
fscanf(pf, "%s %d %f\n", s.name, &s.age, &s.f);
printf("%s %d %f\n", s.name, s.age, s.f);
fclose(pf);
pf == NULL;
return 0;
}
3.sscanf与 sprintf
![](https://img-blog.csdnimg.cn/ee610e6da20141398fcb138a9e113ae8.png)
![](https://img-blog.csdnimg.cn/442cea08c1f24400ad42f49cc3803de5.png)
//sscanf和sprintf函数
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
//序列化和反序列化的时候
struct S s = { 12,3.4f,"liuhai" };
//sprintf - 将格式化字符转化为字符串
char ch[50] = { 0 };
sprintf(ch, "%d %f %s\n", s.n, s.f, s.arr);
printf("%d %f %s\n", s.n, s.f, s.arr);
//printf(ch);
//sscanf - 将字符串转换为相应的格式
struct S tmp = { 0 };
sscanf(ch,"%d %f %s", &tmp.n, &tmp.f, tmp.arr);
printf("%d %f %s\n", tmp.n, tmp.f, tmp.arr);
return 0;
}
4.2二进制读写文件
1)fwrite - 以二进制形式写入数据
#include <stdio.h>
#include<errno.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
//以二进制形式打开文件 wb
FILE* pf = fopen("test.dat", "wb");
if (pf == NULL)
{
perror(fopen);
return 1;
}
//fwrite - 以二进制方式写入文件
struct S s = { "zhangsan",23,99.f };
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
结果打开文件发现是这样的东西,好像并不是我们写入的东西啊。为什么?
因为,之前我们写入的方式,是以文本的形式,而这一次我们用二进制的形式打印
这两种形式并不相同,所以我们会读不懂,好像乱码一样。
那为什么zhangsan就是我们能看懂的数据呢?
因为,刚好zhangsan是字符串,无论文本形式还是二进制形式都是相同的。
2)fread - 以二进制形式读取
上面用fwrite写入了数据,但很不巧二进制我们读不懂
因此为了进一步验证,这是不是二进制写入的,所以我们要用二进制形式来读取。
其中fread 的返回值是:有效读取数据的个数。
int main()
{
//以二进制式形式读文件, rb
FILE* pf = fopen("test.dat", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//fread - 读文件
struct S s = { 0 };
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
通过二进制方式读取文件,发现读取的内容与最开始写入文件里的内容一致
五、 文件的随机读写
1)fseek - 可以以(文件起始位置、当前位置、文件末尾)为起点,通过偏移量让指针向前或向后移动访问数据。
![](https://img-blog.csdnimg.cn/74f9ff0ebeab4f0ba5e3fb74c09368e7.png)
int main()
{
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//在文件里写一些数据 abcdefghi
//读一些数据
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
ch = fgetc(pf);
printf("%c\n", ch);//d
//读完d后,指针指向了e
//如果现在想要读到b ,使用fseek随机读 :a b c d e f g h i
//应该在当前e的位置,往前面(左边)移动3位
//int fseek ( FILE * stream, long int offset, int origin );
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);//读到了b
printf("%c\n", ch);
//从起始位置,读到后面第5个字符f
fseek(pf, 5, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);//f
//从文件末尾位置(i的后面),往前2个字符
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//h
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
2)ftell - 返回当前位置的偏移量
当我们不知道文件的数据读取到哪了,不知道指针的位置的时候,使用ftell可以获取到当前的位置(偏移量)
int main()
{
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//在文件里写一些数据 abcdefghi
//读一些数据
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
ch = fgetc(pf);
printf("%c\n", ch);//d
//读完d后,指针指向了e
//如果现在想要读到b ,使用fseek随机读 :a b c d e f g h i
//应该在当前e的位置,往前面(左边)移动3位
//int fseek ( FILE * stream, long int offset, int origin );
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);//读到了b
printf("%c\n", ch);
printf("%d\n", ftell(pf));//返回当前位置,偏移量4,证明此时指针指向偏移量为4的位置,字符为e
//从起始位置,读到后面第5个字符f
fseek(pf, 5, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);//f
printf("%d\n", ftell(pf));//返回当前位置,偏移量6,证明此时指针指向偏移量为4的位置,字符为g
//从文件末尾位置(i的后面),往前2个字符
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);//h
printf("%d\n", ftell(pf));//返回当前位置,偏移量8,证明此时指针指向偏移量为4的位置,字符为i
//关闭文件
fclose(pf);
pf == NULL;
return 0;
}
3)rewind - 让文件指针回到起始位置
int main()
{
FILE* pf = fopen("test.dat", "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
ch = fgetc(pf);
printf("%c\n", ch);//d
//当前指针指向e的位置
//使用rewind让指针回到初始位置
rewind(pf);
printf("%d\n", ftell(pf));//偏移量0
ch = fgetc(pf);
printf("%c\n", ch);//a
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
六、 文本文件和二进制文件
根据数据的组织形式,数据文件被称为 文本文件 或者 二进制文件 。数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件 。如果要求在外存上以 ASCII 码的形式存储,则需要在存储前转换。以 ASCII 字符的形式存储的文件就是 文 本文件 。一个数据在内存中是怎么存储的呢?字符一律以ASCII 形式存储,数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储。例如:整数10000,如果以 ASCII 码的形式输出到磁盘,则磁盘中占用 5 个字节(每个字符一个字节),而 二进制形式输出,则在磁盘上只占4 个字节( VS2013 测试)。整数1,以 ASCII码的形式输出到磁盘,则占1个字节,而二进制形式输出则占4个字节。说明,不同的形式存储不同的数据类型的数据,其结果也是不一样的。
#include <stdio.h>
int main()
{
int a = 10000;
//以二进制形式打开文件,写wb
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式把10000写到文件中
//打开文件是二进制内容看不懂
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
虽然二进制文件打开我们看不懂,无法判断存的二进制内容,我们可以在编译器右键 - 属性 - 以二进制打开文件,可以看到
分析:
存储的10000,内存中整型二进制为:0000 0000 0000 0000 0010 0111 0001 0000
以十六进制读取:00 00 27 10
以小端形式存储:10 27 00 00
发现:这的确就是我们写入二进制文件的内容10000
七 、文件读取结束的判定
7.1 被错误使用的feof
牢记 : 在 文件读取过程 中, 不能用feof函数 的返回值直接用来判断文件的是否结束。而是 应用于 当文件读取结束 的时候,判断是读取失败结束,还是遇到文件尾结束 。1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:fgetc 判断是否为 EOF . (因为fgetc返回的是一个整型 ,EOF表示-1,为整型)
fgets 判断返回值是否为 NULL . (fgets返回的是一个字符型指针,NULL表示空指针)
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:fread判断返回值是否小于实际要读的个数 。
//判断文件读取结束的原因
#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);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
八、 文件缓冲区
ANSIC 标准采用 “ 缓冲文件系统 ” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序 中每一个正在使用的文件开辟一块“ 文件缓冲区 ” 。从内存向磁盘输出数据会先送到内存中的缓冲区,装 满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根 据C编译系统决定的。
//VS2013 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;
}
这个可以测试玩一玩,蛮有趣的,刚开始写入内容时,先放到了输出缓冲区,所以在睡眠的10s内我们看不到内容。
然后10s之后活塞新了缓冲区,再打开文件就看到里面的东西了。
因为本身fclose关闭文件也会刷新缓冲区,为了区分输出的内容是写入时刷新的,而不是关闭时刷新的,所以再关闭之前也睡眠了10s。