文件是计算机表达信息的最小逻辑单位,是信息二进制化在外存中的集合。
为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称之为“文件控制块(FCB)。
操作系统的FCB,在不同系统的程序设计语言中,对应的名字不同,但是本质的数据类型是一样的,在C语言中,对应的便是FILE类型。
关于FCB有以下几点需要注意:
因此,FILE是操作系统有限的资源,在对文件进行操作前,必须先申请这个资源。
C语言对文件的操作要通过FILE类型,通常的操作顺序:
FILE *fp; //fp为指针类型,不是FILE实例
fp = fopen(文件名, 打开方式);
FILE *fopen(const char *fileName, const char *mode);
fclose(FILE *)
fileName为文件名。
mode包含两部分内容:
1.操作方式
r | read only(只读,默认方式) |
---|---|
w | write(创建/改写) |
a | append(追加写) |
r+ | 既读又写 |
2.识别方式
t | text / ASCII(文本文件,字节流文件,非格式化文件) |
---|---|
b | binary(二进制文件,格式化文件) |
两个注意事项:
1.以r方式打开某文件,若该文件不存在,则fopen()返回NULL,表示申请 FCB失败;
2.以w方式打开某文件,若该文件不存在,则创立该文件,且为空文件。若该文件存在,则会清空原来文件的所有内容。
fopen()原型为 FILE * fopen(const char * path,const char * mode);其本质是申请FCB资源;
fclose()原型为 int fclose(FILE *stream);其本质是归还FCB资源。
关于fprintf() 和 fscanf()两个函数,有两个需要注意的地方:这两个函数,无论文件是以 t方式 打开,还是以 b方式 打开,其内容都是文本,即ASCII码。,所以写进去的数据,本质都是字符串,读数据会将字符串转为需要的数据类型,然后读出。
进行一次读写操作后,位置指针向后移,且fscanf()遇到空格和换行时结束,注意空格时也结束。
原型:int fprintf (FILEstream,const charformat, [argument])
功能:向文件指针所指向的文件中写入相应类型的数据。
返回值:若成功,返回输出字符数;若失败,则返回负值。
printf("%d %d", 5, 684) == fprintf(文件指针, “%d %d”, 5, 684);
测试代码如下
#include
int main() {
int a = 3;
int b = 5;
FILE *fp;
fp = fopen("test1.txt", "w");
fprintf(fp, "%d %d\n", a, b);
fclose(fp);
return 0;
}
程序运行结束,文件夹里便多了一个文件。
内容为
可以看到,a和b的值通过fprintf()写入了文件中。
原型:int fscanf(FILEstream,charformat,[argument…]);
功能:从文件指针所指的文件中读取数据并赋值给变量
返回值:返回成功读取的数据个数,若文件中没有数据可以读,则返回-1。
测试代码如下
#include
int main() {
int a;
int b;
FILE *fp;
fp = fopen("test1.txt", "r");
fscanf(fp, "%d %d", &a, &b);
printf("%d %d\n", a, b);
fclose(fp);
return 0;
}
结果为
可以看到成功的从我用fprintf()写入的文件中读取了3 和 5并赋值给 a 和 b。
关于fscanf()的返回值我也做了测试,代码如下
#include
int main() {
int a;
int b;
int c;
FILE *fp;
fp = fopen("test1.txt", "r");
c = fscanf(fp, "%d %d", &a, &b);
printf("%d %d\n", a, b);
printf("%d\n", c);
fclose(fp);
return 0;
}
我以此将文件中的数据改为 3 5, 3, 无数据这三组,返回值结果如下
可以看到,只有当所有数据都读取失败,才会返回-1,否则只返回成功读取的数据个数。
fputs() & fgets()和fprintf() & fscanf()规律是一样的,fputs()为写入函数,fgets()为读函数,不再过多的讲解。
原型:int fputs(const char *str, FILE *fp);
功能:将一个字符串str写入文件指针fp所指向的文件中
返回值:若成功则返回一个非负值,(我的测试结果返回的一直是0),若失败返回EOF,通常为-1.
测试代码
#include
int main() {
char strOne[30] = "allin or nothing";
FILE *fp;
fp = fopen("test2.txt", "w");
fputs(strOne, fp);
fclose(fp);
return 0;
}
程序运行结束,文件中多了一个“test2.txt”的文件,内容如下
成功写入。
原型:char *fgets(char *buf, int count, FILE *stream);
功能:从文件中读取一行字符串,遇到换行,文件末尾或者读取到count - 1时,读取停止
返回值:若读取成功则返回一个相同的目标字符串;如果读取错误或未读取到任何字符,则返回NULL。(我的测试结果,若失败则不返回任何值或者说无法读取)
测试代码如下
#include
int main() {
char strOne[30];
char strTwo[30];
FILE *fp;
fp = fopen("test2.txt", "r");
fgets(strOne, 6, fp);
fgets(strTwo, 20, fp);
puts(strOne);
puts(strTwo);
fclose(fp);
return 0;
}
原型:int fputc(int ch, FILE *stream);
功能:从文件指针所指向的文件中写入一字节的信息。
返回值:如果成功,则返回写入的字符,如果是字符型,则返回其ASCII码值;
如果失败,则返回EOF。
以下为测试代码
#include
int main() {
char a = 'A';
int b = 3;
int c;
int d;
FILE *fp;
fp = fopen("test3.txt", "w");
c = fputc(a, fp);
d = fputc(b, fp);
printf("%d %d\n", c, d);
fclose(fp);
return 0;
}
因为fputc()只能录入一个字节的信息,所以我录入一个四字节的变量b作为尝试,并分别输出两次写入的返回值。以下为写入文件的内容
可以看到b的变量值3是无法被正常写入的,但是返回值呢?
A正常被写入文件,返回了其ASCII码值65,3虽然没有被正常写入,但是仍被返回其值,且不是其ASCII码,就是输入的这个值。
原型:int fgetc(FILE *stream);
功能:从文件中读取一个字节的信息,读取后,光标位置向后移动一字节。
返回值:若成功,返回从文件中读取的一个字节的信息;若失败,则返回EOF,一般为-1。
测试代码
#include
int main() {
char a;
int b;
int c;
FILE *fp;
fp = fopen("test3.txt", "r");
a = fgetc(fp);
b = fgetc(fp);
c = fgetc(fp);
printf("%c %d\n", a, b);
printf("%d\n", c);
fclose(fp);
return 0;
}
运行结果为:
可以看到很神奇的是,在fputc()中,我们写入了一个不成功的3,但是读取的时候仍然成功读出了3。再继续读的时候,文件已经没有数据了,所以读取失败,返回-1。
fwrite() & fread() 是读写最专业的函数,对于C语言的初学者,更喜欢也更愿意用对文本文件的操作,即通常使用fprintf() & fscanf()或fputs() & fgets()等函数进行读写操作,这也可以实现将内存数据存储到外存中,但是有如下弊病:
以存储整型量为例,若一个文件中,以ASCII码形式存储大量int型数据。由于将数据按ASCII码存储,那么不同长度的数值将在文件中占用不同的字节数,如果需要在文件中对数据进行定位(按序号或下标),则因为每一个数据占用的字节数不同,需要一个数一个数遍历,效率极低。
而如果用二进制储存,即以fwrite() & fread()函数为主要的读写函数,则对于整型量而言,无论其长度为多少,每一个数都将占用相同的空间,若文件中存在大量int型数据,则每个数都固定的占4B,这就相当于一个数组。可以通过fseek()函数直接定位,大大提高了效率。对于像结构体这样可能每个实例大小都不一样的数据类型,更应该用二进制进行读写。
原型:size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)
功能:向指定的文件写入若干数据块,即ptr只能是数组或者是结构体等存储类
返回值:返回count,无论count数与真实写入的相不相符。
测试代码如下:
#include
int main() {
int arr[10] = {
1, 2, 3, 4};
FILE *fp;
fp = fopen("test4.txt", "wb");
fwrite(arr, sizeof(int), 4, fp);
fclose(fp);
return 0;
}
程序运行后生成一个文件夹"test4.txt",但是有一点需要注意,直接用文本模式打开这个文件是无法看到任何值的,只有乱码,因为fwrite()是将数据以二进制形式存储进去的,所以需要用能看二进制的编辑器打开文件,比如UltraEdit,结果如下
可以看到,整整齐齐地排列着1, 2, 3, 4。
原型:size_t fread( void * buffer , size_t size , size_t count , FILE * stream );
功能:从数组、结构体中读取指定数量的字节信息。
返回值:返回成功读取的数据数量。
测试代码如下
#include
int main() {
int a;
int arr[10];
FILE *fp;
fp = fopen("test4.txt", "rb");
a = fread(arr, sizeof(int), 6, fp);
printf("%d %d %d %d\n", arr[0], arr[1], arr[2], arr[3]);
printf("%d\n", a);
fclose(fp);
return 0;
}
文件里只有四个数据,所以我将count设为6,测试返回值。结果如下:
可以看到数据正常读入了变量中,返回值为4,是成功读取的数量,而不是6,这一点需要和fwrite()函数区分开。
原型:int feof(FILE *stream);
功能:判断最近一次文件读函数是否成功读取数据。
返回值:若读函数成功,则feof()返回0;若读函数失败,则feof()返回非0。
关于这个函数,有些教材表述都是错误的,正确的理解是: feof()不是一个状态标识函数,而是一个动作标识函数。
以下为测试代码
#include
int main() {
int num;
int status;
FILE *fp;
fp = fopen("test5.txt", "r");
status = fscanf(fp, "%d", &num);
printf("status = %d\nfeof() = %d\n", status, feof(fp));
while (status > 0 && !feof(fp)) {
printf("num = %d\n", num);
status = fscanf(fp, "%d", &num);
printf("status = %d\nfeof() = %d\n", status, feof(fp));
}
printf("<%d>\n", num);
fclose(fp);
return 0;
}
其中 test5.txt的内容为
该程序运行结果为:
可以看到,没有输出num = 7的机会,循环就跳出了。这个程序可能符合了某些广为流传的错误观念,即feof()是判断下一个数据是否能成功读取,这是错误的!!
由于fscanf()在遇到空格都会结束,所以不容易实验,我们还是用二进制来读写。通过fwrite()函数将以下数据写入文件中
int arr[4] = {
1024, 2048, 3, 4};
测试代码如下
#include
int main() {
int point;
int num;
FILE *fp;
fp = fopen("test4.txt", "r");
while (!feof(fp)) {
fread(&num, sizeof(int), 1, fp);
point = ftell(fp);
printf("num = %d\nfeof() = %d\n", num, feof(fp));
printf("point = %d\n", point);
}
fclose(fp);
return 0;
}
如果按照广为流传的说法,在输出num = 3的时候就应该跳出循环了,但是结果是怎么样呢?
不仅没有在num = 3的时候跳出去,反而多输出了一次num = 4,所以feof()并不是一个所谓判断下一个数据是否存在的状态辨识函数,而是判断最近一次读函数是否成功的动作标识函数。
因此,我们需要最后做判断,即把读函数放在循环后面,就可以保证数据的正确读入了,改善后的代码如下:
#include
int main() {
int point;
int num;
FILE *fp;
fp = fopen("test4.txt", "r");
fread(&num, sizeof(int), 1, fp);
while (!feof(fp)) {
point = ftell(fp);
printf("num = %d\nfeof() = %d\n", num, feof(fp));
printf("point = %d\n", point);
fread(&num, sizeof(int), 1, fp);
}
fclose(fp);
return 0;
}
原型:long ftell(FILE *stream);
功能:得到文件位置指针相对于文件首的偏移量。
返回值:返回文件位置指针的偏移量。
测试代码如下
#include
int main() {
char strOne[30];
long point;
FILE *fp;
fp = fopen("test2.txt", "r");
fgets(strOne, 6, fp);
point = ftell(fp);
puts(strOne);
printf("%ld\n", point);
fclose(fp);
return 0;
}
test2.txt 中的内容为
可以看到,通过fgets()函数我从文件中读取五个字符,该程序结果如下
即偏移量为5,也就是位置指针最后一个指向的位置。
原型:int fseek(FILE *stream, long offset, int fromwhere);
功能:移动文件内部位置指针。
返回值:成功则返回0,失败则返回非零。
// fseek()函数的定位方式有三种:
// 1、SEEK_SET,从最开始定位;
// 2、SEEK_CUR,从文件读写指针当前所在位置定位;
// 3、SEEK_END,从文件末尾定位。
// 其中,第二个参数类型是long,且,可正可负;
// 若为正数,则,向后定位;若为负数,则,向前定位。
fromwhere有三个值,即以上三种;
offset为偏移量,即偏移定位位置(第三个参数位置)的偏移量。
根据ftell() & fseek()函数我们可以知道文件的字节数,通过这个例子,也可以说明二进制存储和文本文件存储的差别。
以下为测试代码
#include
int main() {
int num;
FILE *fp;
fp = fopen("test6.txt", "r");
fseek(fp, 0, SEEK_END);
num = ftell(fp);
printf("文件字节数:%d\n", num);
fclose(fp);
return 0;
}
“test6.txt” 是文本文件存储的,内容如下
代码运行结果如下
其字节数正是这几个数的总长度以及三个空格,相当于这一个字符串的长度,但我们存进去的明明希望是int型的数据,所以这就是用文本存储的弊端。我们用fwrite()函数,把相同的数据写入。
#include
int main() {
int arr[10] = {
1024, 2048, 5121221, 54813};
FILE *fp;
fp = fopen("test4.txt", "wb");
fwrite(arr, sizeof(int), 4, fp);
fclose(fp);
return 0;
}
那么我们来看"test4.txt"的字节数。
是16字节,也就是4 * sizeof(int)的大小。
/*
所以这篇文章终于结束了,这是我写的最痛苦的一篇博客,因为我真的写了太多验证代码了,反反复复测试了很多遍,确保我写的内容是无误的。希望能帮助到看这篇文章的人。
*/