目录
1.为什么使用文件?
2.什么是文件?
3.文件的打开和关闭
3.1文件指针
3.2文件的打开和关闭
4.文件的顺序读写
4.1"流"的概念
4.2 字符输入输出函数:fgetc/fputc
4.3 文本行输入输出函数:fgets/fputs
4.4 格式化输入输出函数:fscanf/fprintf
4.5对比scanf,fscanf,printf,fprintf,sscanf,sprintf函数
4.6二进制输入输出函数:fread/fwrite
5.文件的随机读写
5.1fseek函数
5.2ftell函数
5.3rewind函数
6.文本文件和二进制文件
7.文件读取结束的判定
8.文件缓冲区
使用文件可以将数据直接存放在电脑的硬盘上,实现数据持久化
磁盘上的文件是文件,在程序设计中,从文件功能的角度分类,文件可以分为两类
- 程序文件:包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(Windows环境下后缀为.exe)
- 数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或是输出内容的文件
以前所处理数据的输入输出都是以终端为对象的,即从终端的键盘输出数据,运行结果显示到显示器上,其实有时候我们会把信息输出到磁盘上,在需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用
文件名包含了3部分:文件路径+文件名主干+文件后缀
例:C:\mycode\test.txt 红色部分为文件路径,蓝色部分为文件主干名,黑色部分为文件后缀
文件指针:缓冲文件系统中,关键的概念是"文件类型指针",简称文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,如文件的名字,文件状态及文件当前的位置等,这些信息是保存在一个结构体变量中的,该结构体类型是有系统声明的,取名为FILE
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE类型的结构变量,并填充其中的信息,一般都是通过一个FILE指针来维护这个FILE结构的变量,这样使用起来更方便。
FILE* pf;//定义一个文件指针变量
pf是指向一个FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区,通过该文件信息区中的信息就能够访问该文件,即通过文件指针变量能够找到与它关联的文件
注:打开文件就会产生文件信息区
打开文件的同时,返回一个FILE*的指针变量指向该文件,相当于建立了指针和文件的关系
ANSIC规定使用fopen函数打开文件,fclose函数关闭文件
fopen函数
FILE * fopen ( const char * filename, const char * mode );第一个参数是要打开的文件名,是一个字符串
注:打开桌面文件应当使用绝对路径
第二个参数是打开文件的模式,包含以下几种
"r":以读模式打开文件,此模式下要打开的文件必须存在
"w":以写模式打开文件,此模式下会创建一个空的文件进行写入;如果该文件已经存在,则写入的内容会覆盖该文件的原始内容
"a":在文件末尾打开文件进行输出。输出操作始终将数据写入文件末尾,并对其进行扩展。重新定位操作(以下将进行介绍)将被忽略。如果文件不存在,则创建该文件。
fclose函数
int fclose ( FILE * stream );参数:一个指向需要被关闭文件的指针
返回值:如果文件成功关闭,返回0;如果识别,返回EOF(-1)
fopen和fclose函数使用举例
#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
当我们以"w"模式打开文件且该文件不存在时,会在该项目工程下创建一个空文件
介绍文件的顺序读写函数之前,我们先了解一个概念:流
"流"实际是在内存和外部设备之间抽象出来的一个概念
我们所熟知的外部设备,例如:屏幕,键盘,U盘,光盘,网络等,它们的读写方式都是存在差异的,为了方便对外部设备的读写操作,我们就引入流的概念
在文件的相关操作中,也有流的概念:当我们要对一个文件进行操作时,必须先使用fopen函数打开文件,获得指向该文件信息区的指针,该指针就可以称为“文件流”;
更详细的对"流"的讲解可以参考【流】的基本概念_c语言流的概念_圣喵的博客-CSDN博客
注:本文以下顺序读写与随机读写的函数中流均指的是文件流
字符输入输出函数:fgetc/fputc
int fgetc( FILE * stream ); //fgetc函数可以从流中读取一个字符, int fputc ( int character, FILE * stream ); //fputs函数可以写入一个字符到流注:fgetc函数和fputc函数的返回值都是整型,写入/读取成功返回0,写入/读取失败返回EOF
fputc函数使用举例:我们向工程路径下的text.txt文件中写入26个小写字母
注:
- 向文件中写入数据,应该以"w"模式打开文件
- 检查文件指针pf的有效性
- 写入结束,需要使用fclose函数关闭文件
#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//写文件
char i = 0;
for (i = 'a'; i < 'z'; i++)
{
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgetc函数使用举例:我们读取工程路径下的text.txt文件中的内容
注:
- 从文件中读取数据,应该以"r"模式打开文件
- 检查文件指针pf的有效性
- 注意fgetc函数的返回值为整型,当返回0时读取成功,返回EOF(-1)读取失败
- 写入结束,需要使用fclose函数关闭文件
#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
文本行输入输出函数:fgets/fputs
char * fgets ( char * str, int num, FILE * stream ); //fgets函数可以从流中读取一行数据 int fputs ( const char * str, FILE * stream ); //fputs函数可以向流中写入一行数据注:
- fputs函数的返回值为整型,如果写入成功返回一个非负值,写入失败返回EOF
- fputs函数写入时,如果原来文件中存在内容,则新写入的内容会覆盖原有内容
- fgets函数的返回值为字符指针,如果读取成功返回str,读取失败返回NULL
- fgets函数的参数:str为读取的内容要存入的地址,num为读取字符的个数
- gets函数实际读取字符的个数为num-1,因为字符串的末尾存在'\0'
fputs函数使用举例:我们向工程路径下的text.txt文件中写入字符串"hello world"
#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//写文件
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgets函数使用举例:我们从工程路径下的text.txt文件中读取5个字符
注:
- fgets函数实际读取的元素个数为num-1,因为字符串末尾存在'\0'
#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//读文件
char arr[5] = { 0 };
fgets(arr, 5, pf);
printf(arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
格式化输入输出函数:fscanf/fprintf
int fscanf ( FILE * stream, const char * format, ... ); //fsanf函数可以从流中读取格式化数据 int fprintf ( FILE * stream, const char * format, ... ); //fprintf函数可以向流中写入格式化数据格式化数据:即C 字符串,包含一系列字符,这些字符控制如何处理从流中提取的字符:
空格字符:该函数将读取并忽略在下一个非空格字符之前遇到的任何空格字符(空格字符包括空格、换行符和制表符)。格式字符串中的单个空格验证从流中提取的任意数量的空格字符(包括无)。
非空格字符,格式说明符 (%) 除外:任何不是空格字符(空白、换行符或制表符)或格式说明符的一部分(以 % 字符开头)的字符都会导致函数从流中读取下一个字符,将其与此非空格字符进行比较,如果匹配,则将其丢弃,函数继续使用格式的下一个字符。如果字符不匹配,函数将失败,返回流的后续字符并使其未读状态。
格式说明符:由初始百分号 (%) 形成的序列表示格式说明符,用于指定要从流中检索并存储到附加参数所指向的位置的数据的类型和格式。
区分fscanf函数和fprintf函数:
例:结构数据的写入
#include
#include
#include
struct S
{
char arr[10];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 25, 50.5f };
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
//结构体数据写入
fprintf(pf, "%s %d %f", s.arr, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
结构体数据读取:
#include
#include
#include
struct S
{
char arr[10];
int age;
float score;
}s;
int main()
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//文件打开成功
// 结构体数据读取
fscanf(pf, "%s %d %f", s.arr, &s.age, &s.score);
printf("%s %d %f", s.arr, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
scanf函数:针对标准输入的格式化输入语句
printf函数:针对标准输入的格式化输初语句
fscanf函数:针对所有输入的格式化输入语句
fprintf函数:针对所有输入的格式化输入语句
sscanf函数:把一个字符串转化成格式化数据
sprintf函数:把一个格式化的数据转换成字符串
sscanf函数和sprintf函数
int sscanf ( const char * s, const char * format, ...); //sscanf函数可以从s(字符串)读取数据并根据参数格式将它们存储到附加参数给出的位置 //把字符串转换成格式化数据 //就像使用scanf函数一样,但是scanf函数是从标准输入(stdin)读取 //附加参数:指向可以存储相应格式数据的空间 int sprintf ( char * str, const char * format, ... ); //sprintf函数可以从附加参数给出的位置读取数据并以字符串格式存放在str指向的位置 //格式化数据转换成字符串 //得到的字符串末尾会自动追加终止空字符 //附加参数:至少与格式参数的个数一样多,即与格式相对应返回值:
sscanf函数
成功后,该函数返回参数列表中成功填充的项数。此值可以与预期的项数匹配,在匹配失败的情况下可能更少(甚至为零)。
输入失败,则返回 EOF。sprintf函数
成功后,将返回写入的字符总数。此数不包括自动追加在字符串末尾的其他 null 字符。
失败时,返回负数。
二进制输入输出函数:fread/fwrite
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); //fread函数函数可以从流中把count个元素读取到ptr指向的内存块中,每个元素的大小为size字节 size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); //fwrite函数函数可以从ptr指向的内存块中把count个元素写入流中,每个元素的大小为size字节,其中ptr是由const修饰(不能被修改)的泛型指针,可以是任何数据类型返回值:
返回成功写入/读取的元素总数。
如果此数字与 count 参数不同,则写入/读取错误阻止函数完成。
如果zize或count为零,则该函数返回零
fwrite函数使用举例
#include
struct S
{
char arr[10];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan",25,50.5f };
FILE* pf = fopen("text.txt", "wb");
if (pf == NULL)
{
perror("fopen error!");
return;
}
//文件打开成功
// 二进制方式写入
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
可以发现以二进制方式写入文件中的数据和实际数据并不一样
但是当我们以二进制方式从文件中读取这些数据时,我们又可以得到原数据
#include
struct S
{
char arr[10];
int age;
float score;
}s;
int main()
{
FILE* pf = fopen("text.txt", "rb");
if (pf == NULL)
{
perror("fopen error!");
return;
}
//文件打开成功
//二进制方式读取
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f", s.arr, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们已经介绍过个文件顺序读取函数,但其存在一定的局限性,例如fgetc函数每次只能读写一个字符,fputs函数只能从前往后写入数据,因此我们还有一些函数,可以进行文件的随机读写,例如:fseek函数,ftell函数,rewind函数等
int fseek ( FILE * stream, long int offset, int origin ); //fseek函数可以根据文件指针的偏移量来定位文件指针返回值:
如果成功,该函数返回零。
否则,返回非零值。参数说明:
offset为偏移量
origin为起始位置,相对于偏移量,有以下三种
SEEK_SET 文件开头 SEEK_CUR 文件指针的当前位置 SEEK_END 文件结尾
举例:对于一个文件中的字符串"abcdef"
#include
int main()
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen error!");
return;
}
//文件打开成功
int ret = fseek(pf, 2, SEEK_SET);
printf("%d\n", ret);
int ch = fgetc(pf);
printf("%c\n", ch);
ret = fseek(pf, 2, SEEK_CUR);
printf("%d\n", ret);
int ch = fgetc(pf);
printf("%c\n", ch);
ret = fseek(pf, -1, SEEK_END);
printf("%d\n", ret);
int ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
运行结果
结果分析:
字符串:"abcdef"
pf默认指向的是文件的开头,SEEK_SET和SEEK_END的位置如下
int ret = fseek(pf, 2, SEEK_SET);
这条语句的效果是pf相对于SEEK_SET的位置正向偏移两个位置
此时由pf指向的位置读取一个字符,为字符'c',
ret = fseek(pf, 2, SEEK_CUR);
这条语句的效果是pf相对于SEEK_CUR的位置正向偏移两个位置,SEEK_CUR为当前文件指针的位置,上次fseek函数调用完之后SEEK_CUR指向的位置如下 :
pf相对于SEEK_CUR的位置正向偏移两个位置,由pf指向的位置读取一个字符,为字符'f'
ret = fseek(pf, -1, SEEK_END);
这条语句的效果是pf相对于SEEK_END的位置反向偏移一个位置
此时由pf指向的位置读取一个字符,为字符'f',
long int ftell ( FILE * stream ); //ftell函数可以计算文件指针的偏移量 //一般和fseek函数一起使用
使用举例:
int main()
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen error!");
return;
}
//文件打开成功
int ret = fseek(pf, 2, SEEK_SET);
printf("%d\n", ftell(pf));
ret = fseek(pf, 2, SEEK_CUR);
printf("%d\n", ftell(pf));
ret = fseek(pf, -1, SEEK_END);
printf("%d\n", ftell(pf));
return 0;
}
运行结果
void rewind ( FILE * stream ); //rewind函数可以使文件指针回到起始位置
根据数据的组织形式,数据文件分为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件
一个数据在内存中的存储方式:
字符一律以ASCII码形式存储,数值型数据既可以用ASCII码存储,也可以以二进制形式存储
例如:一个整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用五个字节(每个字符一个字节),如果以二进制形式输出,则在磁盘上只占用4个字节(整形数据)
整数10000
二进制形式存储:00000000 00000000 00100111 00010000
ASCII码值形式存储:00110001 00110000 00110000 00110000 00110000
注:字符1的ASCII码值是49,字符0的ASCII码值是48
#include
int main()
{
int a = 10000;
FILE* pf = fopen("text.txt", "wb");
if (pf == NULL)
{
perror("fopen error!");
return;
}
fwrite(&a, 4, 1, pf);
fclose(pf);
return 0;
}
可以发现,当我们以二进制的形式把整数写入文件时,产生了一些我们看不懂的符号
但是当我们把text.txt文件以二进制的形式打开时,我们又可以看到10000的十六进制形式(以小端模式存储)
int feof ( FILE * stream ); //文件指针到达文件尾则返回一个非零值,否则返回0 int ferror ( FILE * stream ); //文件读取时遇到输入/输出错误导致读取失败返回一个非零值,否则返回0在文件读取过程中,不能用feof函数的返回值直接判断文件是否结束
feof函数应用于文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
- 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
- fgetc函数判断是否为EOF
- fgets函数判断是否为NULL
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
- fread函数判断返回值是否小于实际要读的个数
feof函数和ferror函数使用举例:
#include
int main()
{
int c;
FILE* pf = fopen("text.txt", "r");
if (!pf)
{
perror("file opening failed");
return;
}
//fgetc函数当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(pf)) != EOF)
{
putchar(c);
}
printf("\n");
//判断什么原因导致文件读取结束
if (ferror(pf))
{
puts("I/O error when reading");
}
else if (feof(pf))
{
puts("End of file reached successfulliy");
}
fclose(pf);
return 0;
}
一个例子:
#include
enum
{
SIZE = 5
};
int main()
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* pf = fopen("text.bin", "wb");//以二进制模式写入文件
if (!pf)
{
perror("file opening failed");
return;
}
fwrite(a, sizeof * a, SIZE, pf);//写入数组
fclose(pf);
double b[SIZE];
pf = fopen("text.bin", "rb");
size_t ret_code = fread(a, sizeof * b, SIZE, pf);//读取数组
//读取成功
if (ret_code == SIZE)
{
puts("Array read successfully,contents:");
for (int n = 0; n < SIZE; ++n)
{
printf("%f ", b[n]);
}
putchar('\n');
}
//读取失败
else
{
//判断什么原因导致文件读取结束
if (ferror(pf))
{
//遇到错误结束
puts("Error reading text,bin\n");
}
else if (feof(pf))
{
//遇到文件末尾结束
puts("Error reading text,bin:unexpected end of file\n");
}
}
return 0;
}
ANSIC标准采用"缓冲文件系统"处理文件数据,所谓文件缓冲系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,充满缓冲区后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定。
缓冲区存在的意义:
缓冲区的存在提高了操作系统的效率,例如fwrite函数调用,操作系统需要提供接口,频繁的写入会打断了操作系统执行其他事务,将数据放满缓冲区后再进行输送可以避免频繁的占用操作系统。