【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用

文件操作

    • Ⅰ 文件和文件控制块
      • A.文件的定义
      • B.文件控制块(FCB)
    • Ⅱ 文件操作
    • Ⅲ 文件操作函数
      • A. fopen() & fclose()
      • B. fprintf() & fscanf()
        • a. fprintf()
        • b. fscanf()
      • C. fputs() & fgets()
        • a. fputs()
        • b. fgets()
      • D. fputc() & fgetc()
        • a. fputc()
        • b. fgetc()
      • E. fwrite() & fread()
        • a. fwrite()
        • b. fread()
      • F. feof()
      • G. ftell() & fseek()
        • a. ftell()
        • b. fseek()

Ⅰ 文件和文件控制块

A.文件的定义

文件是计算机表达信息的最小逻辑单位,是信息二进制化在外存中的集合。

B.文件控制块(FCB)

为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称之为“文件控制块(FCB)。

操作系统的FCB,在不同系统的程序设计语言中,对应的名字不同,但是本质的数据类型是一样的,在C语言中,对应的便是FILE类型。

关于FCB有以下几点需要注意:

  • FCB是操作系统珍贵的有限资源。
  • 操作系统对文件的操作必须通过FCB进行。

因此,FILE是操作系统有限的资源,在对文件进行操作前,必须先申请这个资源。

Ⅱ 文件操作

C语言对文件的操作要通过FILE类型,通常的操作顺序:

  1. 打开文件 (其本质为申请FCB)
  2. 读、写文件
  3. 关闭文件 (其本质为归还FCB)
	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方式打开某文件,若该文件不存在,则创立该文件,且为空文件。若该文件存在,则会清空原来文件的所有内容。

Ⅲ 文件操作函数

A. fopen() & fclose()

fopen()原型为 FILE * fopen(const char * path,const char * mode);其本质是申请FCB资源;
fclose()原型为 int fclose(FILE *stream);其本质是归还FCB资源。

B. fprintf() & fscanf()

关于fprintf() 和 fscanf()两个函数,有两个需要注意的地方:这两个函数,无论文件是以 t方式 打开,还是以 b方式 打开,其内容都是文本,即ASCII码。,所以写进去的数据,本质都是字符串,读数据会将字符串转为需要的数据类型,然后读出。

进行一次读写操作后,位置指针向后移,且fscanf()遇到空格和换行时结束,注意空格时也结束。

a. fprintf()

原型: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;
}

程序运行结束,文件夹里便多了一个文件。
在这里插入图片描述
内容为
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第1张图片
可以看到,a和b的值通过fprintf()写入了文件中。

b. fscanf()

原型: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, 无数据这三组,返回值结果如下
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第2张图片
可以看到,只有当所有数据都读取失败,才会返回-1,否则只返回成功读取的数据个数。

C. fputs() & fgets()

fputs() & fgets()和fprintf() & fscanf()规律是一样的,fputs()为写入函数,fgets()为读函数,不再过多的讲解。

a. fputs()

原型: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”的文件,内容如下
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第3张图片
成功写入。

b. fgets()

原型: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;
}

结果如下
在这里插入图片描述

D. fputc() & fgetc()

a. fputc()

原型: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码,就是输入的这个值。

b. fgetc()

原型: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。

E. fwrite() & fread()

fwrite() & fread() 是读写最专业的函数,对于C语言的初学者,更喜欢也更愿意用对文本文件的操作,即通常使用fprintf() & fscanf()或fputs() & fgets()等函数进行读写操作,这也可以实现将内存数据存储到外存中,但是有如下弊病:

以存储整型量为例,若一个文件中,以ASCII码形式存储大量int型数据。由于将数据按ASCII码存储,那么不同长度的数值将在文件中占用不同的字节数,如果需要在文件中对数据进行定位(按序号或下标),则因为每一个数据占用的字节数不同,需要一个数一个数遍历,效率极低。

而如果用二进制储存,即以fwrite() & fread()函数为主要的读写函数,则对于整型量而言,无论其长度为多少,每一个数都将占用相同的空间,若文件中存在大量int型数据,则每个数都固定的占4B,这就相当于一个数组。可以通过fseek()函数直接定位,大大提高了效率。对于像结构体这样可能每个实例大小都不一样的数据类型,更应该用二进制进行读写。

a. fwrite()

原型: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。

b. fread()

原型: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()函数区分开。

F. feof()

原型: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的内容为
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第4张图片
该程序运行结果为:
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第5张图片
可以看到,没有输出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的时候就应该跳出循环了,但是结果是怎么样呢?
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第6张图片
不仅没有在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;
}

测试结果如下
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第7张图片
完美。

G. ftell() & fseek()

a. ftell()

原型: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 中的内容为
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第8张图片
可以看到,通过fgets()函数我从文件中读取五个字符,该程序结果如下
在这里插入图片描述
即偏移量为5,也就是位置指针最后一个指向的位置。

b. fseek()

原型: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” 是文本文件存储的,内容如下
【C语言基础】->文件操作详解->一篇文章读懂关于文件的庞杂函数使用_第9张图片
代码运行结果如下
在这里插入图片描述
其字节数正是这几个数的总长度以及三个空格,相当于这一个字符串的长度,但我们存进去的明明希望是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)的大小。

/*
所以这篇文章终于结束了,这是我写的最痛苦的一篇博客,因为我真的写了太多验证代码了,反反复复测试了很多遍,确保我写的内容是无误的。希望能帮助到看这篇文章的人。
*/

你可能感兴趣的:(C语言基础,c语言,指针)