目录
- 一、为什么使用文件?
- 二、什么是文件?
-
- [2.1 程序文件](#2.1 程序文件)
- [2.2 数据文件](#2.2 数据文件)
- [2.3 文件名](#2.3 文件名)
- 三、二进制文件和文本文件
- 四、文件的打开和关闭
-
- [4.1 流和标准流](#4.1 流和标准流)
-
- [4.1.1 流](#4.1.1 流)
- [4.1.2 标准流](#4.1.2 标准流)
- [4.2 文件指针](#4.2 文件指针)
- [4.3 文件的打开和关闭](#4.3 文件的打开和关闭)
- 五、文件的顺序读写
-
- [5.1 顺序读写函数介绍](#5.1 顺序读写函数介绍)
-
- [a. fgetc](#a. fgetc)
- [b. fputc](#b. fputc)
- [c. fgets](#c. fgets)
- [d. fputs](#d. fputs)
- [e. fread与fwrite](#e. fread与fwrite)
- [f. scanf、fscanf与sscanf](#f. scanf、fscanf与sscanf)
- [g. printf、fprintf与sprintf](#g. printf、fprintf与sprintf)
- 六、文件的随机读写
-
- [6.1 fseek](#6.1 fseek)
- [6.2 ftell](#6.2 ftell)
- [6.3 rewind](#6.3 rewind)
- 七、文件读取结束的判定
-
- [7.1 被错误使用的feof与ferror](#7.1 被错误使用的feof与ferror)
- 八、文件缓冲区
- 九、拷贝文件
一、为什么使用文件?
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件。
二、什么是文件?
磁盘(硬盘)上的⽂件是⽂件。
但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件(从⽂件功能的⻆度来分类
的)。
2.1 程序文件
程序⽂件包括源程序⽂件(后缀为.c
),⽬标⽂件(windows环境后缀为.obj
),可执⾏程序(windows环境后缀为.exe
)。
2.2 数据文件
⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件。
本文讨论的是数据⽂件。
在以前各章所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显⽰到显⽰器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上的⽂件。
2.3 文件名
⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤。
⽂件标识包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀
例如: C:\Windows\Test\test.txt
。
其中C:\Windows\Test\
就是文件路径 ,test
就是文件名主干 ,.txt
就是文件名后缀 。
为了⽅便起⻅,⽂件标识常被称为⽂件名。
需要注意:
- 文件名中有一些不能被包含的字符,例如这些字符:
\
,/
,:
,*
,?
,"
,<
,>
,|
。 - 文件路径指的是从盘符到该文件所经历的路径中各符号名的集合。
- 后缀名决定了文件的默认打开方式(例如以
.txt
打开的文件是文本文档,以.bin
打开的文件是二进制文件)。 - 文件名中不一定包含文件后缀(例如有些文件可能被隐藏了后缀,像这样
C:\Windows\Test\test
)。
三、二进制文件和文本文件
根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件。
⼀个数据在⽂件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节('1','0','0','0','0'
每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节(一个 int 的大小)。
测试代码:
c
//代码1
#include <stdio.h>
int main()
{
int a = 10000;
FILE * pf = fopen("test.txt", "w");
fprintf(pf, "%d", a);//ASCII码的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
结果1:
c
//代码2
#include <stdio.h>
int main()
{
int a = 10000;
FILE * pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
在VS上打开二进制文件:
运行程序后在解决资源管理项添加现有项,选择对应文件的打开方式。
结果2:
可以看到在结果1中,test.txt
文件中存储的是5个字符,所以需要占用5个字节的空间。而整型10000
的二进制形式是00000000 00000000 00100111 00010000
,化作十六进制是0x00002710
,存储在test.txt
中只需要4个字节(上图是小端字节序)。
四、文件的打开和关闭
铺垫了这么多,最关键的还是需要学会如何从文件读取数据与如何输出数据到文件。
4.1 流和标准流
4.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出所需要的操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河,通过流将数据运送到需要的地方。
C程序针对⽂件、键盘与屏幕画面等的数据输⼊输出操作都是通过流操作的。⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
stdin
------标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf
函数就是从标准输⼊流中读取数据。stdout
------标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf
函数就是将信息输出到标准输出流中。stderr
------标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf
、printf
等函数就可以直接进⾏输⼊输出操作的。
stdin
、stdout
、stderr
三个流的类型是:FILE*
,通常称为⽂件指针。
C语⾔中,就是通过FILE*
的⽂件指针来维护流的各种操作的。
4.2 文件指针
缓冲⽂件系统中,关键的概念是"⽂件类型指针",简称"⽂件指针"。
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE
。
例如,VS2013编译环境提供的stdio.h
头⽂件中有以下的⽂件类型申明:
c
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*
的指针变量:
c
FILE* pf;//⽂件指针变量
定义pf
是⼀个指向FILE
类型数据的指针变量。可以使pf
指向某个⽂件的⽂件信息区(⼀个结构体变量)。通过该⽂件信息区(结构体变量)中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件。
⽐如:
4.3 文件的打开和关闭
⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件。
在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*
的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。
ANSI C规定使⽤fopen
函数来打开⽂件,fclose
来关闭⽂件。
c
//fopen原型
FILE* fopen(const char* filename, const char* mode);
//fclose原型
int fclose(FILE* stream);
mode
表⽰⽂件的打开模式,下⾯都是⽂件的打开模式:
运用实例:
c
/* fopen fclose example */
#include <stdio.h>
int main()
{
FILE* pFile;//创建FILE*类型指针pFile
pFile = fopen("test.txt", "w");//以只写的模式打开文件"test.txt",pFile指向开辟后的文件信息区
//文件操作
if (pFile != NULL)//判断pFile是否为空指针,确保接受到了文件信息区的地址再去使用
{
fputs("fopen example", pFile);//输出字符串到文件流中
fclose(pFile);//关闭文件
}
return 0;
}
运行结果:
五、文件的顺序读写
5.1 顺序读写函数介绍
上⾯说的适⽤于
所有输⼊流 ⼀般指适⽤于标准输⼊流 和其他输⼊流 (如⽂件输⼊流);
所有输出流 ⼀般指适⽤于标准输出流 和其他输出流(如⽂件输出流)。
a. fgetc
fgetc
是一个从输入流中读取数据的输入函数,返回值是指定流中位置指示器当前指向的字符,返回后位置指示器再次后移一位。
fgetc
只有一个参数FILE* stream
,即指定一个输入流,从该输入流中读取字符。
对于返回值,为什么fgetc
要选择int
而不是char
呢?
其实是因为读取文件遇到文件尾或者读取失败时,fgetc
函数会返回EOF
(值为-1),所以为了兼顾fgetc
选择int
作为返回类型,既可以返回字符又可以返回EOF
。
使用示例:
项目文件中已经有一个test.txt
文件,文件情况如上。
我们现在需要将test.txt
文件内的字符数据通过输入流输入到test.c
文件里的字符数组ch[20]
中。
这里需要先提醒一个小细节
当fgetc
在文件中一个一个读取字符时,会有一条竖线,叫作光标。如图,这就是我们在fgetc
介绍时提及的位置指示器的直观显现。当我们第一次调用fgetc
读取文件时,光标是在文件第一个字符f
前,当函数fgetc
读取完第一个字符就会返回,同时光标会后移一位,到了f
与o
之间,下一次调用fgetc
时光标就从这一位置开始读取字符。
c
#include <stdio.h>
int main()
{
FILE* pfin = fopen("test.txt", "r");
if (!pfin)//pfin==NULL则进入if语句
{
//fopen打开文件错误,报错并return退出程序
perror("fopen");
return 1;
}
char ch[20] = { 0 };
int i = 0;
while ((ch[i] = fgetc(pfin)) != EOF)//当fgetc的返回值为EOF,即遇到文件末尾时,退出循环
{
printf("%c", ch[i]);//打印字符
i++;
}
fclose(pfin);//文件打开使用完必须关闭
pfin = NULL;//pfin指针如果不用应该设置为空指针
return 0;
}
b. fputc
fputc
是向输出流中写入字符的输出函数,每写入一个字符,位置指示器后移一位。
fputc
有两个参数,分别是int character
与FILE* stream
。character
是我们需要输出的字符,stream
是指定的输出流。
使用示例:
现在程序中有一字符串"Hello David!\n"
需要输出到test.txt
文件中,我们用fputc
来完成。
c
#include <stdio.h>
#include<string.h>
int main()
{
FILE* pfout = fopen("test.txt", "w");
if (!pfout)//pfout为空则报错
{
perror("fopen");
return 1;
}
char str[20] = "Hello David!\n";
for (int i = 0; i < strlen(str); i++)
{
fputc(str[i], pfout);//将字符数组str中的元素依次输出到文件中
}
fclose(pfout);//文件打开使用完后必须关闭
pfout = NULL;//pfout指针不使用需要及时置为NULL
return 0;
}
可以看到,我们利用字符输出函数fputc
成功的把str
数组中的所有字符都输出到了test.txt
文件中,就连\n
都输出到了文件中(上图中的第二行)。
c. fgets
fgets
是一个从输入流中读取字符串的文本行输入函数。
需要注意fgets
有3个参数,char* str
与FILE* stream
没什么好讲,即接收输入流数据的字符数组的地址与指定的输入流。关键是num
,num
是我们指定输入字符个数的参数,但这里有一个细节就是真正从文件输入到数组的字符个数实际上是num-1
,第num
个字符永远是\0
。
同时,如果出现num
的大小大于第一行的字符数且存在第二行的情况下,fgets
会把第一行都读取完,并把\n
也读取了,最后一位仍是设置为\0
,然后光标到了第二行,函数fgets
返回。
总结:
fgets
读取结束的条件是读取到了num-1
个字符或遇到文件尾,同时,如果读取时光标到了下一行也会停止读取。
使用示例:
以fputc
中写好的test.txt
文件为例。
现在需要用fgets
读取test.txt
文件,将其中的文本信息放入另一个数组ch[20]
中。
c
#include <stdio.h>
#include<string.h>
int main()
{
FILE* pfin = fopen("test.txt", "r");
if (!pfin)//pfin为空则报错
{
perror("fopen");
return 1;
}
char ch[20] = { 0 };
fgets(ch, 20, pfin);//读取到文件末尾或者读取了19个字符或者到了下一行停止
printf("%s", ch);
fclose(pfin);
pfin = NULL;//pfout指针不使用需要及时置为NULL
return 0;
}
Hello David!\n
存入ch[20]
数组中,且fgets
读取结束自动在末尾添加\0
。
d. fputs
fputs
是向输出流中写入字符串的文本行输出函数。
fputs
有2个参数,分别是const char* str
与FILE* stream
。stream
是指定的输出流。str
是需要写入输出流的字符串的地址。对于字符串的输出有一个细节需要特别关注,那就是fputs
的输出遇到\0
停止且字符串中的\0
并不会输出。
使用示例:
我们用fputs
重写一下fputc
中的代码,如下:
c
#include <stdio.h>
#include<string.h>
int main()
{
FILE* pfout = fopen("test.txt", "w");
if (!pfout)//pfout为空则报错
{
perror("fopen");
return 1;
}
char str[20] = "Hello David!\n";
for (int i = 0; i < strlen(str); i++)
{
fputc(str[i], pfout);//将字符数组str中的元素依次输出到文件中
}
fclose(pfout);//文件打开使用完后必须关闭
pfout = NULL;//pfout指针不使用需要及时置为NULL
return 0;
}
修改:
c
#include <stdio.h>
#include<string.h>
int main()
{
FILE* pfout = fopen("test.txt", "w");
if (!pfout)//pfout为空则报错
{
perror("fopen");
return 1;
}
char str[20] = "Hello David!\n";
fputs(str, pfout);
fclose(pfout);//文件打开使用完后必须关闭
pfout = NULL;//pfout指针不使用需要及时置为NULL
return 0;
}
e. fread与fwrite
fread
是从输入流中读取二进制数据的输入函数,且只适用于文件输入流。
fread
有四个参数。ptr
是指向一段内存块或数组的指针。size
为字节数,代表了需要读取的元素的类型。count
为需要读取的元素个数。stream
为文件输入流。
fread
返回成功读取的元素个数,读取失败返回值小于count
,读取成功返回值等于count
,如果size
或count
等于0则返回值为0。
fread
有一个细节需要注意,size
决定了一次读取的字节数,count
决定了读取次数/元素个数,如果count
大于文件能够读取的个数,那么fread
自动结束。且fread
不同于fputs
,读取后不会自动加\0
。
fwrite
是向输出流中写入二进制数据的输出函数,且只适用于文件输出流。
fwrite
有4个参数。prt
是指向需要写入的数组的指针。size
为要写的数组的元素的大小。count
为需要写入的元素个数。stream
为文件输出流。
fwrite
返回成功写入的元素个数。如果写入失败则返回值小于count
,如果写入成功则返回值等于count
,如果size
或count
等于0则返回值为0。
fwrite
有个细节需要注意,不同的size
决定了一次写入的字节数,count
决定了写入的次数/元素个数,如果count
设置过大,会将我们指定内存块后面未知内存地址中的数据也写入文件中。
使用示例:
现在我们需要用fwrite
输出字符串"Hello David!\n"
的二进制数据到二进制文件test.bin
中,再用fread
读取test.bin
文件中的数据存入字符数组ch[20]
中。
c
#include <stdio.h>
#include<string.h>
int main()
{
//用fwrite写入数据
FILE* pfout = fopen("test.bin", "wb");
if (!pfout)
{
perror("fopen");
return 1;
}
char str[20] = "Hello David!\n";
fwrite(str, sizeof(char), strlen(str), pfout);
fclose(pfout);//文件打开使用完后必须关闭
pfout = NULL;//pfout指针不使用需要及时置为NULL
//用fread读取数据
FILE* pfin = fopen("test.bin", "rb");
if (!pfin)
{
perror("fopen");
return 1;
}
char ch[20] = "XXXXXXXXXXXXXXXXXXXX";//测试count大于文件能读取的数据范围时fread是否自动结束
fread(ch, sizeof(char), strlen(str) + 10, pfin);
for (int i = 0; i < strlen(str); i++)
{
printf("%c", ch[i]);
}
fclose(pfin);
pfin = NULL;
return 0;
}
fread
确实自动结束:
可以发现,文件中的数据被全部读取完后,即使读取的个数小于count
,fread
也自动结束。
f. scanf、fscanf与sscanf
我们发现,fscanf
与sscanf
相比起我们熟知的scanf
多了一个参数,这就是不同所在。
首先我们来重新认识一下scanf
,scanf
通过我们今天的知识,其实我们可以知道,scanf
是一个从标准输入流stdin
中读取指定格式的数据format
的标准输入流函数。
而fscanf
与sscanf
就是在流上与scanf
有所不同。fscanf
是更广泛的适用于所有输入流的格式化输入函数,而sscanf
不是从流而是从字符串读取格式化数据的输入函数。
使用示例:
我们现在有上述test.txt
文件和字符串char sentence []="Rudolph is 12 years old"
。现在我们需要使用fscanf
读取文件中的信息,用sscanf
读取字符串中的Rudolph
与12
。前者我们需要把文件信息存入字符数组ch[20]
中,后者我们需要分别存入字符数组str[20]
与整型变量i
中。
c
#include <stdio.h>
#include<string.h>
int main()
{
//先读取文件信息打印
FILE* pfin = fopen("test.txt", "r");
if (!pfin)
{
perror("fopen");
return 1;
}
char ch[20] = { 0 };
fscanf(pfin, "%[^\n]", ch);
for (int i = 0; i < strlen(ch); i++)
{
printf("%c", ch[i]);
}
printf("\n");
fclose(pfin);
pfin = NULL;
//再读取字符串信息打印
char sentence[] = "Rudolph is 12 years old";
char str[20] = { 0 };
int i = 0;
sscanf(sentence, "%s %*s %d", str, &i);
printf("%s->%d\n", str, i);
return 0;
}
需要注意的细节就是和scanf
类似,fscanf
与sscanf
都会遇到空格停止读取,所以我们需要仔细考虑format
的写法。
在fscanf
中,我们的目的是全部读取,不跳过空格,所以这里使用了%[^\n]
这个格式符,这个格式符中^
是省略的意思,即省略\n
其他字符全部读取。
在sscanf
中,我们的目的是有选择性的读取字符,所以我们使用了%*s
这个格式符,其中*
可以使得%s
读取到的字符被直接跳过省略,不存储到任何变量中。可以看到sscanf(sentence, "%s %*s %d", str, &i)
中第一个%s
读取到了Rudolph
后,%*s
应该读取到is
,但我们并没有放字符变量去接收is
,是因为读取到is
后is
直接被省略了。
g. printf、fprintf与sprintf
和上面三个输入函数scanf,fscanf,sscanf
对应,printf,fprintf,sprintf
也在写入数据的地方有所不同。
printf
已经知道是输出到标准输出流stdout
中,而fprintf
适用于所有输出流可以输出到指定输出流中,而sprintf
可以输出格式化的数据到字符串中。
使用示例:
现在我们需要用fprintf
输出结构体变量s
的信息到test.txt
文件中:
c
#include <stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "小明", 22, 60.0f };
FILE* pfout = fopen("test.txt", "w");
if (!pfout)
{
perror("fopen");
return 1;
}
fprintf(pfout, "%s同学是%d岁,在这次考试得了%.1f分。\n", s.name, s.age, s.score);
fclose(pfout);
pfout = NULL;
return 0;
}
结果:
现在我们不需要输出"%s同学是%d岁,在这次考试得了%.1f分。\n"
到文件中,而是输出到字符数组中储存,用sprintf
应该怎么写呢?
c
#include <stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "小明", 22, 60.0f };
char str[50] = { 0 };
sprintf(str, "%s同学是%d岁,在这次考试得了%.1f分。\n", s.name, s.age, s.score);
printf("%s", str);
return 0;
}
结果:
可以发现一个小细节,sprintf
输出数据到字符数组中,和fputs
类似都会自动在字符串尾添加\0
。
六、文件的随机读写
6.1 fseek
根据⽂件内部位置指示器(或称文件指针)的位置和偏移量来移动⽂件指针(⽂件内容的光标)。
fseek
有3个参数,steam
是文件输入/输出流,offset
是移动的距离,origin
是开始移动的起始位置。
origin
有3个值,分别是SEEK_SET
代表起始位置,SEEK_CUR
代表当前位置,SEEK_END
代表末尾位置。
如果fseek
成功移动光饼/位置指示器则返回0值,如果失败则返回非0值。
使用示例:
c
/* fseek example */
#include <stdio.h>
int main()
{
FILE* pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile);//写入字符串
fseek(pFile, 9, SEEK_SET);//从SEEK_SET这个位置向后移动9
fputs(" sam", pFile);//移动后再次写入字符串
fclose(pFile);
return 0;
}
结果:
这里来讲一下这段代码的细节:
首先我们向代码中写入了一段初始信息This is an apple.
,然后我们通过fseek
移动光标。
fseek(pFile, 9, SEEK_SET)
的意思是,从SEEK_SET
,即初始位置开始移动9个字节的距离。所以光标起始位置在T
之前,从此处开始移动9个字节,到了a
与n
之间。
接着fputs(" sam", pFile);
写入字符串,因为我们example.txt
文件打开模式为"wb"
,而不是"ab"
,所以再次写入时不是追加而是覆盖,即n ap
被 sam
替换。
最终This is an apple.
变成This is a sample.
,程序结束。
6.2 ftell
返回⽂件指针相对于起始位置的偏移量。
ftell
只有1个参数,即文件输入/输出流stream
。
使用示例:
我们对上述文件进行文件长度计算。
c
/* ftell example : getting size of a file */
#include <stdio.h>
int main()
{
long size;
FILE* pFile = fopen("example.txt", "rb");
if (pFile == NULL)
perror("Error opening file");
else
{
fseek(pFile, 0, SEEK_END); // non-portable光标在文件尾不移动
size = ftell(pFile);
fclose(pFile);
printf("Size of myfile.txt: %ld bytes.\n", size);
}
return 0;
}
结果:
上述代码结合了fseek
与ftell
,先用fseek
找到文件尾,再用ftell
计算了此时光标距离文件头的偏移量,这样就得出了文件长度。
6.3 rewind
让⽂件指针的位置回到⽂件的起始位置。
使用示例:
c
/* rewind example */
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
//将A~Z输出到文件中
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
//输出后文件光标的最终位置在'Z'后
rewind(pFile);//使用rewind调整光标回到初始位置------'A'之前
//读取文件内容存储到字符数组buffer中
fread(buffer, 1, 26, pFile);//从rewind调整的位置开始读取数据
fclose(pFile);
buffer[26] = '\0';
puts(buffer);
return 0;
}
七、文件读取结束的判定
7.1 被错误使用的feof与ferror
牢记:
在⽂件读取过程中,不能⽤feof
函数的返回值直接来判断⽂件的是否结束。
feof
的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。
当我们打开一个流时,这个流上有两个标记值,一个是EOF
,一个是error
,我们需要使用对应的feof
与ferror
去检测标记值,我们才能确定文件结束的原因究竟是读写失败还是遇到文件末尾。
-
⽂本⽂件 读取是否结束,判断返回值是否为
EOF
(值为-1),或者NULL
。 -
⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
- 例如:
fread
判断返回值是否⼩于实际要读的个数
- 例如:
现在我们回过头来看看前面介绍过的顺序读写函数:
c
//如果读取错误或读到文件末尾,返回EOF;如果读取正常,返回读取到的字符的ASCII码值
int fgetc ( FILE * stream );
//如果读取错误或遇到文件末尾,返回NULL;如果读取正常,返回的是存储读取到的字符串的字符数组的地址
char * fgets ( char * str, int num, FILE * stream );
八、文件缓冲区
ANSIC 标准采⽤"缓冲⽂件系统"处理的数据⽂件,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个正在使⽤的⽂件开辟⼀块"⽂件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。
用代码展示直观过程:
c
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
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;
}
第一阶段,已经开始写数据但数据还放在输出缓冲区,没有到文件中:
第二阶段,刷新缓冲区,数据写入文件:
第三阶段,文件关闭,刷新缓冲区,缓冲区无数据,程序结束。
这⾥可以得出⼀个结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件的问题。
九、拷贝文件
结合本篇的知识,我们可以实现文件拷贝的操作,下面是代码样例:
c
#include<stdio.h>
int main()
{
//创建data.txt并输出字符串到该文件
FILE* pfout = fopen("data.txt", "w");
if (!pfout)
{
perror("fopen");
return 1;
}
fputs("I am iron man.", pfout);
fclose(pfout);
pfout = NULL;
//打开data.txt文件读取数据存入字符数组
FILE* pfin = fopen("data.txt", "r");
if (!pfin)
{
perror("fopen");
return 1;
}
fseek(pfin, 0, SEEK_END);
int offset = ftell(pfin);
rewind(pfin);
char s[50] = { 0 };
fgets(s, offset + 1, pfin);
fclose(pfin);
pfin = NULL;
//创建data_copy.txt文件并拷贝字符数组内容到该文件中
pfout = fopen("data_copy", "w");
if (!pfout)
{
perror("fopen");
return 1;
}
fputs(s, pfout);
fclose(pfout);
pfout = NULL;
return 0;
}
效果:
完。