【C语言】文件操作

大家好,我是苏貝,本篇博客带大家了解文件操作,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️

目录

  • 一. 什么是文件
    • 1.1 程序文件
    • 1.2 数据文件
    • 1.3 文件名
  • 二. 文件的打开和关闭
    • 2.1 文件指针
    • 2.2 文件的打开和关闭
  • 三. 文件的顺序读写
    • 3.1 fputc
    • 3.2 fgetc
    • 3.3 fputs
    • 3.4 fgets
    • 3.5 fprintf
    • 3.6 fscanf
    • 如何理解输入输出(个人理解,可不看)
    • 3.7 fwrite 二进制输出
    • 3.8 fread
    • 3.9 对比scanf/fscanf/sscanf和printf/fprintf/sprintf
  • 四. 文件的随机读写
    • 4.1 fseek
    • 4.2 ftell
    • 4.3 rewind
  • 五. 文本文件和二进制文件
  • 六. 文件读取结束的判定
    • 6.1 被错误使用的feof
    • 6.2 文本文件读取结束判定
    • 6.3 二进制文件读取结束判定
  • 七. 文件缓冲区

一. 什么是文件

磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

1.1 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

1.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。本章讨论的是数据文件。在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

1.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径+文件名主干+文件后缀,例如: c:\code\test.txt。为了方便起见,文件标识常被称为文件名

二. 文件的打开和关闭

2.1 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名
字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统
声明的,取名FILE.
例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

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*的指针变量:

FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

2.2 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
若打开文件失败,则会返回NULL

//打开文件
FILE * fopen ( const char * filename, const char * mode );
//                           文件名               打开方式
//关闭文件
int fclose ( FILE * stream );

打开方式如下:

示例:
perror函数在上篇的最后有讲到,详情请点击

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

文件路径:绝对路径和相对路径。
绝对路径:比如说:该文件在C盘里的a文件夹里的b文件夹内,文件名为name,那么它的绝对路径是C:\a\b\name。
相对路径:.表示当前路径,可以省略;. .表示上一级路径,比如我要找到文件在上一级路径,那么我们写. .\data.txt,比如:

int main()
{
    //绝对路径
	FILE* pf = fopen("C:\\Users\\data.txt", "r");
	//两个\经过转义后变成一个\ 
	//相对路径
	//FILE* pf = fopen("..\\data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

三. 文件的顺序读写

我们先来讲fputc函数

3.1 fputc


我们看到,fputc的参数有stream,这是什么?中文翻译过来是流,流是一个高度抽象的概念。当我们想将数据传递给外部设备(屏幕、光盘、U盘等)时,由于不同的外部设备读取的方法肯定是有区别的,这就要求程序员知道每一个外部设备的读取方式,这对程序员的要求就比较高了,也很麻烦。所以就有人想出将数据传给流,流再管如何传递到外部设备,这样,程序员就只用知道如何将数据传到流中即可。流似水流,不过流里面流淌的数据

讲回fputc函数,函数功能是往流里写入一个字符

示例1: 往文件流里写入字符

int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = 0;
	fputc('a', pf);
	fputc('b', pf);
	fputc('c', pf);
	fputc('d', pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

注意:pf还是指向文件的起始位置没有变,移动的是光标。初始时,光标在首位,在写入a后,光标往后移一位,再写入b并放在光标指向的位置,光标再光标往后移一位,再写入c并放在光标指向的位置,光标再光标往后移一位,再写入d并放在光标指向的位置

编译运行后没有问题,让我们看看文件里面的内容:

示例2: 往标准输出流里写入字符

我们在写文件时,要先打开文件,最后关闭文件。scanf(标准输入流)和printf(标准输出流)分别是从键盘上读取数据和向屏幕上打印数据,那我们为什么不用实施打开键盘和打开屏幕操作呢?那是因为:C语言程序只要运行起来,就会默认打开3个流:(既然都叫流,那么它们3个的类型都为FILE*)

1.标准输入流 – stdin
2.标准输出流 – stdout
3.标准错误流 – stderr

下面代码因为是标准输出流,所以不会写在文件里,而是直接打印在屏幕中

int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = 0;
	fputc('a', stdout);
	fputc('b', stdout);
	fclose(pf);
	pf = NULL;
	return 0;
}

3.2 fgetc

功能:从流中读取字符,如果读取成功,返回字符的ASCII码值,读取失败,返回EOF,同时会设计一个错误标记,可用ferror检测。因为是读取字符,所以打开方式为“r”

示例:
由上面的fputc函数在文件里写入了abcd,所以文件现在的内容就是abcd

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(pf);
	printf("%c ", ch);//a

	ch = fgetc(pf);
	printf("%c ", ch);//b

	ch = fgetc(pf);
	printf("%c ", ch);//c

	ch = fgetc(pf);
	printf("%c ", ch);//d

	fclose(pf);
	pf = NULL;
	return 0;
}

3.3 fputs


功能:写一个字符串放入流中

示例:
先将文件的内容清空

int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("hello\n", pf);
	fputs("world", pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

编译运行后没有问题,让我们看看文件里面的内容:

3.4 fgets


功能:将流中的字符拷贝num-1个字符到起始位置是指针str指向的位置的数组中

示例1:
因为上面的fputs函数,所以文件现在的内容是:hello\n(换行)world
下面代码的目的是从流中拷贝3个字符到arr数组中

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char arr[4] = { 0 };
	fgets(arr, 4, pf);
	printf("%s", arr);
	fclose(pf);
	pf = NULL;
	return 0;
}

示例2:
上面示例中,num-1<文件第一行的字符个数>=5(hello),现在,我们让num-1更大

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char arr[100] = { 0 };
	fgets(arr, 10, pf);
	printf("%s", arr);
	fclose(pf);
	pf = NULL;
	return 0;
}

我们调试看看,发现虽然num=10,但是它并不会绝对地打印9个字符,当遇到换行符时会停止拷贝

如果我们想再读一行,

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char arr[100] = { 0 };
	fgets(arr, 10, pf);
	printf("%s", arr);
	fgets(arr, 10, pf);
	printf("%s", arr);

	fclose(pf);
	pf = NULL;
	return 0;
}

3.5 fprintf


功能:将格式化的数据写入流

示例:
先将文件的内容清空

struct S
{
	float f;
	char ch;
	int n;
};

int main()
{
	FILE* pf = fopen("data.txt", "w");
	struct S s = { 3.14,'w',11 };
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fprintf(pf, "%f-%c-%d", s.f, s.ch, s.n);
	fclose(pf);
	pf == NULL;
	return 0;
}

编译运行后没有问题,让我们看看文件里面的内容:

3.6 fscanf


功能:从流中读取格式化数据

struct S
{
	float f;
	char ch;
	int n;
};

int main()
{
	FILE* pf = fopen("data.txt", "r");
	struct S a = { 0 };
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fscanf(pf, "%f-%c-%d",&(a.f),&(a.ch),&(a.n) );
	printf("%f  %c  %d", a.f, a.ch, a.n);
	fclose(pf);
	pf == NULL;
	return 0;
}

如何理解输入输出(个人理解,可不看)


下面的只是博主个人理解

fputc函数是字符输入函数,与putchar有写相似,putchar是从内存输出到屏幕的,类似的,fputc是从内存输出到文件中的,所以是“w”
fgetc函数是字符输入函数,与getchar有写相似,getchar是从键盘输入到内存中,键盘是内存外的,类似的,fgetc是从内存外输入到内存中的,所以是“r”

fprintf函数是格式化输出函数,printf是从内存输出到屏幕的,类似的,fprintf是从内存输出到文件中的,所以是“w”

3.7 fwrite 二进制输出


功能:将一个有count个元素数组写入流中,每个元素的大小为size字节,指针ptr指向数组的起始地址

int main()
{
	FILE* pf = fopen("data.txt", "wb");
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fwrite(arr, sizeof(int), sizeof(arr) / sizeof(arr[0]), pf);
	return 0;
}


我们发现我们看不懂这里面的内容,其实这是正常的,因为这是以二进制的形式写入的,那我们来换一种我们能看得懂的方式:用fread函数

3.8 fread


我们发现,fread和fwrite函数的参数是一模一样的,功能:从流中读取数据块并拷贝到起始地址为指针ptr指向的位置,fread返回成功读取的元素个数。如果要读取count个大小为size字节的数据,如果真的读到了count个数据,返回count;如果没有读到count个数据,返回的是真实的读取到的完整的数据个数
如果fread函数只读取n个元素(n<count),且读取成功,那么返回n,此时fread函数的指示器会向后移n个元素,也就是说,再次调用fread函数时,是从第n+1个元素开始读取的

int main()
{
	FILE* pf = fopen("data.txt", "rb");
	int arr[10] = { 0 };
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fread(arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), pf);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

3.9 对比scanf/fscanf/sscanf和printf/fprintf/sprintf

scanf 是格式化的输入函数,针对是标准输入流 (键盘)
printf 是格式化的输出函数,针对的是标准输出流(屏幕)

fscanf 是针对所有输入流(文件流、标准输入流)的格式化输入函数
fprintf是针对所有输出流(文件流、标准输出流)的格式化输出函数

sscanf 将字符串转成格式化的数据
sprintf 将格式化的数据转换成字符串


功能:将格式化的数据写入字符串

struct S
{
	float f;
	char c;
	int n;
};

int main()
{
	struct S s = { 3.14,'w',11 };
	char arr[100] = { 0 };
	sprintf(arr, "%f-%c-%d",s.f,s.c,s.n);
	printf("%s", arr);
	return 0;
}


功能:将字符串转成格式化的数据

struct S
{
	float f;
	char c;
	int n;
};

int main()
{
	struct S s = { 3.14,'w',11 };
	char arr[100] = { 0 };
	sprintf(arr, "%f-%c-%d", s.f, s.c, s.n);
	//printf("%s", arr);
	struct S tmp = { 0 };
	sscanf(arr, "%f-%c-%d", &(tmp.f), &(tmp.c), &(tmp.n));
	printf("%f  %c  %d", tmp.f, tmp.c, tmp.n);
	return 0;
}

四. 文件的随机读写

4.1 fseek



使光标在任何想要的地方。offset是偏移量,origin是偏移量计算的起始位置,有3种取值,文件初始位置,当前光标所在的位置和文件的最后面位置

示例:
先将文件的内容修改为abcde

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = fgetc(pf);
	printf("%c\n", ch);//a

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	ch = fgetc(pf);
	printf("%c\n", ch);//d
	return 0;
}

那如果我不想让最后一个ch是d而是a该怎么办呢?即光标要从d处变为a处。有以下3种方法

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	//fseek(pf, 0, SEEK_SET);//1
	//fseek(pf, -3, SEEK_CUR);//2
	fseek(pf, -5, SEEK_END);//3
	ch = fgetc(pf);
	printf("%c\n", ch);
	return 0;
}

4.2 ftell

返回文件指针相对于起始位置的偏移量

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);
	int ret = ftell(pf);
	printf("ret=%d\n", ret);

	fseek(pf, -5, SEEK_END);
	ch = fgetc(pf);
	printf("%c\n", ch);
	ret = ftell(pf);
	printf("ret=%d\n", ret);
	return 0;
}

4.3 rewind


让文件指针的位置回到文件的起始位置,所以4.1也可以这样写

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	rewind(pf);
	ch = fgetc(pf);
	printf("%c\n", ch);
	return 0;
}

五. 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

六. 文件读取结束的判定

6.1 被错误使用的feof



如果在读取过程中因为遇到了文件结束标志而结束,返回一个非0值,否则返回0

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

6.2 文本文件读取结束判定

文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets ),例如:
fgetc 的返回值为 EOF 或者fgets 返回值为 NULL ,那么文本文件读取结束

将文件data1.txt的内容拷贝到文件data2.txt中

int main()
{
	FILE* pfread = fopen("data1.txt", "r");
	if (pfread == NULL)
	{
		perror("fopen");
		return 1;
	}

	FILE* pfwrite = fopen("data2.txt", "w");
	if (pfwrite == NULL)
	{
		perror("fopen");
		fclose(pfread);
		pfread = NULL;
		return 1;
	}
	int ch = 0;//EOF==-1,char类型的不能接收整型-1
	while ((ch = fgetc(pfread)) != EOF)
	{
		fputc(ch, pfwrite);
	}

	fclose(pfread);
	pfread = NULL;
	fclose(pfwrite);
	pfwrite = NULL;
	return 0;
}

6.3 二进制文件读取结束判定

二进制文件的读取结束判断,看fread判断返回值是否小于实际要读的个数。如果小于,代表读取结束;如果等于,那读取继续

下面代码中,如果fread函数的返回值==实际要读的个数SIZE,那么读取结束,读取成功。如果小于,那么读取也结束,不过读取并没有成功读取SIZE个元素。用feof(fp)判断,如果是读取到文件末尾了可还是读取的元素个数任然小于SIZE,那么feof函数返回一个非0的值,再打印。如果读取结束了但还没有到文件末尾,那么打印错误信息

enum 
{ 
	SIZE = 5 //枚举常量SIZE
};
int main(void)
{
	double a[SIZE] = { 1.0,2.0,3.0,4.0,5.0 };
	FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
	fwrite(a, sizeof(a[0]), SIZE, fp); // 写 double 的数组
	fclose(fp);

	double b[SIZE];
	fp = fopen("test.bin", "rb");
	size_t ret_code = fread(b, sizeof(a[0]), SIZE, fp); // 读 double 的数组
	if (ret_code == SIZE) {
		puts("Array read successfully, contents: ");
		for (int n = 0; n < SIZE; ++n) 
			printf("%f ", b[n]);
		putchar('\n');
	}
	else { // error handling
		if (feof(fp))
			printf("Error reading test.bin: unexpected end of file\n");
		else if (ferror(fp)) {
			perror("Error reading test.bin");
		}
	}
	fclose(fp);
	fp = NULL;
}

七. 文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

//VS2019 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。但我们一般不会去刷新缓冲区,所以一定要关闭文件。

好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月12日
下一篇 2023年12月12日

相关推荐