使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化
磁盘上的文件是文件
但在程序设计中,我们一般谈的文件有两种:程序文件和数据文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
我们平常写的代码就是程序文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件,这类文件被称为数据文件
我们在这里讨论的是数据文件
在此之前,我们所处理的数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到屏幕上
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件
也就是说,在这里,文件相当于就是终端即外部设备(键盘、屏幕等)
内存->文件,是输出操作(写),对应put/printf,将数据写入文件中(相当于将数据打印在屏幕上,这不过这里的屏幕变成了文件)
文件->内存,是输入操作(读),对应get/scanf将文件中的数据读取出来放到变量中(相当于对一个变量进行赋值)
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名、文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是有声明的,取名FILE
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件
FILE * fopen ( const char * filename, const char * mode )
第一个参数为文件名,第二个参数为文件的打开方式,返回值为一个指向该文件的文件信息区的文件指针
如果文件打开失败会返回NULL,所以在打开文件后,一定要判断是否打开成功
关于文件名的一些规则:
有两种写法一种是相对路径,另一种是绝对路径。
前者,是不写路径,只写文件名+后缀,这样打开的结果是在程序工程当前所在的路径文件下寻找是否有该文件名的文件,若没有这一名字的文件则创建一个这样的文件
后者,写路径,可以直接打开所指定的文件
int fclose ( FILE * stream )
参数为指向指定要关闭的流的FILE对象的指针
返回值:如果流成功关闭则返回零值,否则返回EOF
注意在关闭文件后,要记住将文件指针置空,以防其变成野指针
#include <stdio.h>
int main()
{
//打开文件
//相对路径
//FILE* pf = fopen("test.txt", "w");
//绝对路径
FILE* pf = fopen("c:\\code\\test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//具体操作由需求决定
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里介绍的八个函数,其头文件均stdio.h
以后看到
fputc/fputs/fprintf/fwrite
想到“屏幕”,即输出操作再由此联想,往文件里输出东西,即是“写”操作
文件在进行输入时,其相当于键盘。这正如scanf的功能,向内存中输入数据
以后看到
fgetc/fgets/fscanf/fread
想到“键盘”,即输入操作再由此联想,键盘向内存输入数据如同从文件中读取数据输入内存,即是“读”操作
对进行单个字符的读写操作,想对字符串操作需用到循环
int fputc ( int character, FILE * stream )
一次只将单个字符写入文件中
将字符写入流并推进位置指示器。
字符被写入流的内部位置指示器指示的位置,然后自动前进一
第一个参数:为所需要写入的字符
该函数会将要写入的字符升级为int类型,写入该值时,该值在内部转化为无符号字符
若写入成功则返回所写字符的ASCII值;若失败,则返回EOF并设置错误指示器
int fgetc ( FILE * stream )
一次只从文件中读取一个字符
返回指定流的内部文件位置指示器当前所指向的字符。然后,内部文件位置指示器将前进到下一个字符
这里的返回类型为int以容纳指示失败的特殊值EOF
如果读取成功则返回所读字符的ASCII值
如果位置指示器位于文件末尾,则该函数返回 EOF并设置流的文件尾 指示器
如果读取错误,返回 EOF,设置错误指示器
对字符串进行读写操作
int fputs ( const char * str, FILE * stream )
将 str 所指向的字符串写入流(文件)。
fputs不会自动换行,想换行需加上\n
若写入成功则返回非负值;若写入失败则返回EOF并设置错误指示器
char * fgets ( char * str, int num, FILE * stream )
从流中读取字符并将其作为 C字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件结尾,以先发生者为准
**注意:**换行符会使 fgets
停止读取,但它被函数视为有效字符,并被读取到 str 的字符串中
终止空字符会自动追加到复制到 str 的字符之后
注意
fgets
与gets
,前者会将换行符复制到字符串中,后者不会
第二个参数:要读取到 str 中的最大字符数(包括终止空字符)
将数据按照指定格式进行读写操作,这样就可以对结构体等数据类型进行操作
int fprintf ( FILE * stream, const char * format, ... )
将数据按指定格式写入文件中
若写入成功则返回写入的字符总数
这里的字符总数指的是写入文件中的字符的总个数
如:
#include<stdio.h> struct S { int a; double b; char c; }s; int main() { FILE* pf = fopen("test.txt", "w"); if (NULL == pf) { perror("fopen"); return 1; } s.a = 5; s.b = 2.0; s.c = 'a'; int ret=fprintf(pf, "%d%lf%c", s.a, s.b, s.c); printf("%d ", ret); fclose(pf); pf = NULL; return 0; }
打印结果为:10
这与文件中的字符数相同
倘若我将原fprintf语句改为
int ret=fprintf(pf, "%d %lf %c", s.a, s.b, s.c);
即在三个参数之间加入空格符那么这与原语句来说多了两个空格符,所以打印结果也应该多2,即为12
而实际上也是这样的
若写入失败则返回负数并设置错误指示器
struct S
{
char name[20];
int age;
float score;
};
fprintf(pf, "%s %d %.1f", s.name, s.age, s.score);
int fscanf ( FILE * stream, const char * format, ... )
从文件中将格式化的数据读取出来
此计数可以匹配预期的项目数,也可以由于匹配失败、读取错误或文件末尾的范围而减少(甚至为零)
若在读取时发生读取错误或到达文件末尾,则会设置相应的指示器(ferror或feof)
若在成功读取任何数据之前就读取失败或到达文件末尾,则返回EOF
struct S
{
char name[20];
int age;
float score;
}s;
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
屏幕是标准输出流,而文件也是一种输出流。以上六个函数我们都用于文件,但其实也可以用于屏幕
对任何一个C程序,只要运行起来,就默认打开三个流:
stdin-标准输入流-键盘
stdout-标准输出流-屏幕
stderr-标准错误流-屏幕
这3个流的类型都是FILE*
也就是说scanf(……)和fscanf(stdin,……)是一样的,printf(……)和fprintf(stdout,……)是一样的
例如:
int main()
{
int ch = fgetc(stdin);
fputc(ch, stdout);
return 0;
}
这种其实和用getc和putc的是一样的
数据以二进制的形式进行读写
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream )
写入一个计数元素数组,每个元素的大小为字节,从 ptr 指向流中的当前位置的内存块
第一个参数:指向要写入的元素数组的指针
第二个参数:要写入的每个元素的大小(以字节为单位)
第三个参数:元素数
若写入成功则返回成功写入的元素总数
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )
从流中读取计数元素数组(每个元素的大小为字节大小),并将它们存储在 ptr 指定的内存块中
第一个参数:指向大小至少为(大小*计数)字节的内存块的指针
第二个参数:要读取的每个元素的大小
第三个参数:元素数
若读取成功则返回成功读取的元素总数
scanf:按照一定的格式从键盘输入数据
printf:按照一定的格式把数据输出到屏幕上
适用于标准输入/输出流的格式化输入/输出语句
fscanf:按照一定的格式从所有输入流(文件/stdin)输入数据
fprintf:按照一定的格式向输出流(文件/stdout)输出数据
适用于所有的输入/输出流的格式化输入/输出语句
sscanf:从字符串中按照一定的格式读取出格式化的数据
sprintf:把格式化的数据按照一定的格式转换成字符串
int sprintf ( char * str, const char * format, ... )
把格式化的数据按照一定的格式转换成字符串
终止空字符会自动追加到内容之后
第一个参数:指向字符串的指针,注意该字符串要足够大以至于能包含生成的字符串
写入成功时,返回写入的字符总数(此计数不包括自动追加到字符串末尾的其他空字符)
失败时,返回负数
#include <stdio.h>
int main ()
{
char buffer [50];
int n, a=5, b=3;
n=sprintf (buffer, "%d plus %d is %d", a, b, a+b);
printf ("[%s] is a string %d chars long\n",buffer,n);
return 0;
}
输出:[5 plus 3 is 8] is a string 13 chars long
int sscanf ( const char * s, const char * format, ...)
从字符串中按照一定的格式读取出格式化的数据
读取成功,返回该函数返回参数列表中成功填充的项数
如果在成功解释任何数据之前输入失败,则返回 EOF
#include <stdio.h>
int main ()
{
char sentence []="Rudolph is 12 years old";
char str [20];
int i;
sscanf (sentence,"%s %*s %d",str,&i);
printf ("%s -> %d\n",str,i);
return 0;
}
输出:Rudolph -> 12
int fseek ( FILE * stream, long int offset, int origin )
根据文件指针的位置和偏移量来定位文件指针
第二个参数:相对于起点即将要偏移的字节数(向文件尾偏移为正,向文件头偏移为负)
第三个参数:偏移起点
SEEK_SET
:文件开头
SEEK_CUR
:文件指针当前位置
SEEK_END
:文件末尾
成功则返回0;失败返回非0
#include<stdio.h>
//fseek的测试
int main()
{
FILE* pf =fopen( "test.txt","r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//文件中的数据为abcdef,目标是读取d
//从文件头开始读
fseek(pf, 3, SEEK_SET);
int ch1 = fgetc(pf);
printf("%c\n", ch1);
//从文件尾向前读,所以这里的偏移量为负
fseek(pf, -3, SEEK_END);
int ch2 = fgetc(pf);
printf("%c\n", ch2);
//从文件指针当前位置开始读
fgetc(pf);//先让文件指针发生偏移
fseek(pf, 2, SEEK_CUR);
int ch3 = fgetc(pf);
printf("%c", ch3);
fclose(pf);
pf = NULL;
return 0;
}
long int ftell ( FILE * stream )
得到文件指针在当前位置处相对于起始位置的偏移量
void rewind ( FILE * stream )
让文件指针的位置回到文件的起始位置
数据在内存中的存储是以二进制的形式
**二进制文件:**数据不加转换就输出到外存
**文本文件:**数据在输出到外存之前转换成ASCII的形式,并以ASCII字符的形式进行存储
那么这两个文件有什么区别呢?
现有整数10000,其二进制形式为00000000 00000000 00100111 00010000
如果是以二进制的形式输出,则在磁盘上只占用4个字节
如果是以ASCII码的形式输出,那么1对应的ASCII为49,0对应的ASCII为48,10000共五个数字都转换成ASCII形式在输出,则在磁盘上会占用5个字节
首先来介绍一下两个函数feof和ferror
int feof ( FILE * stream )
检测文件中是否设置了文件尾指示器,若是则返回非零值,否则返回零
当文件正常读取至最后时,会读取到文件末尾的文件结束符EOF,此时会返回EOF并设置文件尾指示器,然后可以用feof来检测该文件是否有文件尾指示器,若有返回非0值,且说明是遇到文件尾而读取结束
int ferror ( FILE * stream )
检测文件中是否设置了错误指示器,若是则返回非零值,否则返回零
当文件读取或写入发生错误时,会在当前位置指示器处生成一个错误指示器,并生成错误的信息,然后可以用ferror来检测该文件中是否有错误指示器,若有则可以打印对应的错误信息
错误指示器,读或写操作中都有可能存在
文件尾指示器,仅会在读的时候存在
当文件因为读取失败而结束时,虽然它也返回了EOF,但用feof时,由于未设置文件尾指示器,feof不能检测到文件尾指示器,所以feof返回0,判断文件读取未结束,而实际上已经结束了,这就产生了问题。
所以不能用feof来判断文件读取是否结束
我们可以用feof和ferror这两个函数,来共同判断文件结束的原因。
若是读取失败而结束,则在文件中会有错误指示器可以通过ferror来检测
若是遇到文件尾而失败,则在文件中会有文件尾指示器可以通过feof来检测
这里只举了文本文件的例子
#include <stdio.h>
#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编译系统决定的
缓冲区存在的意义是提高操作系统的工作效率
对于输出操作:
从内存向磁盘输出数据时会先送到内存中的输出缓冲区,装满缓冲区后才一起送到磁盘上。
对于输入操作:
磁盘向计算机读入数据时会先从磁盘文件中读取数据输入到内存中的输入缓冲区,装满缓冲区后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
当然,并不是说一定要装满缓冲区才能刷新一次缓冲区,以下的情况也可以进行缓冲区的刷新
刷新缓冲区指的是将缓冲区的数据进行传输
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题
因篇幅问题不能全部显示,请点此查看更多更全内容