[C语言]文件操作详解

目录

一. 什么是文件

二. 文件的打开和关闭

2.1 文件指针

2.2 文件的打开和关闭

2.2.1 文件打开函数fopen

2.2.2 文件的重新打开函数freopen

2.2.3 文件的关闭函数fclose

2.2.4 文件的打开方式

三. 流的概念

四. 文件的读写

4.1 文件的顺序读写

4.1.1 fgetc函数

4.1.2 fputc函数

4.1.3 fgets函数

4.1.4 fputs函数

4.1.5 fscanf函数

4.1.6 fprintf函数

4.1.7 fwrite函数

4.1.8 fread函数

4.2 文件的随机读取

4.2.1 fseek函数

4.2.2 ftell函数

4.2.3 rewind函数

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

六. 文件读取结束的判断 

6.1 feof函数(经常被误用于判断文件指针是否到达文件末尾)

6.2 判断文件内容是否全部读取结束的正确方法

6.3 ferror函数

6.4 判断文件是否读取结束的代码演示

七. 文件缓冲区


一. 什么是文件

在程序设计中,文件分为两种

1. 程序文件

 包括源程序文件(.c)、目标文件(windows环境下为.obj、Linux环境下为.o),可执行程序(windows环境下为.exe)

2. 数据文件

 程序运行时要获取数据的文件或输出数据的文件

文件名:文件路径+文件主干名+文件后缀名

二. 文件的打开和关闭

2.1 文件指针

每个被使用的文件都在内存中开辟了一个相应的信息区,用于存放文件的相关信息(如文件的名称、状态、位置等),这些信息保存在结构体变量中,该结构体由系统进行声明,并取名为FILE。

图2.1  文件信息区图解

文件指针的创建:FILE* pf;  //pf为文件指针变量

通过文件指针,就能找到与它相关联的文件 

2.2 文件的打开和关闭

2.2.1 文件打开函数fopen

函数原型:FILE* fopen(const char* filename, const char* mode)

函数参数:filename -- 被打开文件的文件名、mode -- 文件打开的方式

函数返回值:若文件打开成功,则返回指向被开打文件的文件指针,若打开失败,返回NULL

警告:由于文件打开可能会失败,因此,在使用文件之前,一定要检查fopen的返回值是否为空指针(文件是否打开成功),确认文件打开成功之后再使用文件。

2.2.2 文件的重新打开函数freopen

函数原型:FILE *freopen( const char *filename, const char *mode, FILE *stream );

函数功能:关闭文件(流)stream,以新的方式(mode)打开名为filename的文件

函数返回值:若文件打开成功,返回指向新打开文件filename的文件指针,若打开失败,返回空指针NULL

2.2.3 文件的关闭函数fclose

函数原型:int fclose(FILE* stream);

注意:程序中打开的文件在使用完毕后一定要调用fclose函数关闭,并且在fclose关闭文件之后,文件指针应被置为空指针,关闭文件应执行两条语句:fclose(pf);pf = NULL;

2.2.4 文件的打开方式

打开方式(mode)解读若指定打开的文件不存在
"r"(只读)为了读取数据,打开一个存在的文本文件出错
"w"(只写)为了输出数据,打开一个文本文件新建一个文件
"a"(追加)为了在文件尾追加数据,打开一个文本文件新建一个文件
"rb"(只读)为了读取数据,打开一个存在的二进制文件出错
"wb"(只写)为了输出数据,打开一个二进制文件新建一个文件
"ab"(追加)为了在文件尾追加数据,打开一个二进制文件新建一个文件
"r+"、"rb+"(读写)为了读写数据,打开一个文本文件(二进制文件)出错
"w+"、"wb+"(读写)为了读写数据,新建一个文本文件(二进制文件)新建一个文件
"a+"、"ab+"(读写)为了在文件尾部读写数据,新建一个文本文件(二进制文件)新建一个文件

演示代码2.1:(文件的打开和关闭)

//先使用只读的方式打开文件text1.txt,再使用freopen重新以只写的方式打开,最后关闭文件。

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test1.txt", "r"); //以只读的方式打开文件
	if (NULL == pf) //检验文件是否打开成功
	{
		perror("fopen");
		return 1;
	}

	pf = freopen("test1.txt", "w", pf);  //以只写的方式重新打开pf指向的文件
	if (NULL == pf)
	{
		perror("freopen");
		return 1;
	}

	fclose(pf);  //关闭文件
	pf = NULL;

	return 0;
}

三. 流的概念

在程序设计中,流是一个高度抽象的概念。通俗的理解就是,程序输出数据都是向流中输出,程序也都是从流中读取数据。如屏幕、硬盘、U盘、键盘、光盘等,都可以被通俗地理解为流。流的图解见图3.1。

图3.1 流的抽象图解

 一个C语言程序运行起来,就默认打开三个流

  1. stdin —— 标准输入流 —— 一般为键盘
  2. stdout —— 标准输出流 —— 一般为屏幕
  3. stderr —— 标准错误流 —— 一般为屏幕

四. 文件的读写

4.1 文件的顺序读写

4.1.1 fgetc函数

函数原型:int fgetc(FILE* stream);

函数功能:从输入流中读取一个字符(适用于所有输入流)

函数返回值:若读取成功,返回读取到字符对应的ASCII码值,若读取失败或读到文件末尾,返回EOF

演示代码4.1两次调用fgetc函数从文件test1.txt中读取字符并打印出来(文件的内容为:abcdef),程序的运行结果为:ab

演示代码4.1:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test1.txt", "r"); //以只读的方式打开文件
	if (NULL == pf) //检验文件是否打开成功
	{
		perror("fopen");
		return 1;
	}

	printf("%c", fgetc(pf));
	printf("%c", fgetc(pf)); //两次调用fgetc函数打印字符,程序运行结果为ab

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.2 fputc函数

函数原型:int fputc(int c, FILE* stream);

函数功能:向输出流中写入字符

函数参数:stream为要写入数据的输出流,c为要写入的字符对应的ASCII码值

函数返回值:若输出成功,返回对应字符的ASCII码值,若输出失败,返回EOF

演示代码4.2使用fputc函数向test2.txt文件中写入两个字符ab,文件的内容变为ab

演示代码4.2:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test2.txt", "w"); 
	if (NULL == pf) //检验文件是否打开成功
	{
		perror("fopen");
		return 1;
	}

	fputc('a', pf);
	fputc('b', pf); //两次调用fputc函数将ab写入文件

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.3 fgets函数

函数原型:char* fgets(char* buff, int n, FILE* stream);

函数功能:从文件(流)中读取字符串(文本行)

函数参数:buff -- 从文件(流)中读取到的字符串存放的位置、n -- 一次最多读取到的字符的个数、stream -- 读取字符串的流

函数返回值:读取成功返回指向buff首元素的指针,读取失败返回NULL

关于fgets函数的三点注意事项:

  1. 函数实际上最多从文件中读取n-1个字符,因为第n个字符的位置要存放字符串结束标志'\0'。
  2. fgets函数在文件中读取到换行符'\n'时,无论是否已经读取了n-1个字符,都停止读取。
  3. 如果缓冲区buff开辟的空间只有一个字节,那么就无法将stream中的内容读入到其中,因为buff中至少留出1字节的位置来存储'\0'。

演示代码4.3两次使用fgets函数在文件test3.txt文件中读取文本行,将两次读取的内容分别存入到缓冲区buf1和buf2中,两个fgets函数的返回值分别赋给ret1和ret2,打印读到的字符串的内容。程序运行结果为:aaaa (换行)  bbbb。

文件test3.txt的内容为两行字符,分别为aaaa和bbbb。

演示代码4.3:

#include<stdio.h>

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

	char buf1[10] = { 0 };
	char buf2[10] = { 0 };

	char* ret1 = fgets(buf1, 10, pf);  //读取文件中第一行数据(最多度9个字符)
	char* ret2 =  fgets(buf2, 10, pf);  //读取文件中第二行数据(最多度9个字符)

	printf("%s", ret1); //aaaa
	printf("%s", ret2); //bbbb

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.4 fputs函数

函数原型:int fputs(const char* string, FILE* stream);

函数功能:文本行输出函数,将字符串string的内容传入到文件(流)stream中

函数返回值:若输出成功,返回一个非负数,若输出失败,返回EOF

注意:两次使用fputs函数向文件中传输字符串并不会自动引入换行符,若希望两个字符串间换行,则应在两次使用fputs之间人工向文件中传入换行符。

演示代码4.4两次调用fputs,向文件test4.txt中依次传入字符串aaaa和bbbb,运行程序后test4.txt文件的内容为aaaabbbb。

演示代码4.4:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test4.txt", "w"); 
	if (NULL == pf) 
	{
		perror("fopen");
		return 1;
	}

	char ch1[] = "aaaa";
	char ch2[] = "bbbb";

	fputs(ch1, pf);
	fputs(ch2, pf); //向文件中依次传入字符串aaaa和bbbb

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.5 fscanf函数

函数原型:int fscanf( FILE *stream, const char *format [, argument ]... );

函数功能:从文件中读取格式化的数据 

函数返回值:若成功读取到数据,返回读取到字符的个数,若读取失败,返回EOF。

通常在format里采用%d、%s、%lf等表示读取数据的类型。%s表示字符串、%f和%lf分别表示单精度浮点型和双精度浮点型数据,%d表示整形数据。

如果在%后面加星号,就表示转换后的值被丢弃而不进行存储。%后面还可以添加一个整数来表示宽度,如%2s表示一次最多读取两个字符。

演示代码4.5使用fscanf函数,从文件中依次读取整形数据、浮点型数据和字符串,并打印。文件中的内容为10 3.1415 abcd。

演示代码4.5:

#include<stdio.h>

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

	int i = 0;
	float f = 0.0;
	char ch[10] = { 0 };

	fscanf(pf, "%d %f %s", &i, &f, ch); //从文件pf中格式化读取数据

	printf("i = %d\n", i);  //i = 10
	printf("d = %f\n", f); //d = 3.141500
	printf("ch = %s\n", ch); //ch = abcd

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.6 fprintf函数

函数原型:int fprintf( FILE *stream, const char *format [, argument ]...);

函数功能:向文件(流)stream中输入格式化的数据

函数返回值:若成功向文件中输入数据,则返回输入到文件中数据的字节数,若输入失败,则返回一个负数值。

演示代码4.6中,定义了一个学生信息结构体,将这个结构体进行初始化,并将初始化后的信息传入到文件test6.txt中,程序执行完成后文件的内容为:zhangsan  20  98.500000。

演示代码4.6:

#include<stdio.h>

struct stu
{
	char name[20];
	int age;
	float score;
};

int main()
{
	FILE* pf = fopen("test6.txt", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	struct stu s = { "zhangsan", 20, 89.5f };

	fprintf(pf, "%s  %d  %f\n", s.name, s.age, s.score);

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.7 fwrite函数

函数原型:size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

函数功能:以二进制的方式向文件中写入数据

函数参数:

  1. buffer:缓冲区,其中的数据要被写入到流中
  2. size:被写入的单个数据所占的字节数
  3. count:写入到文件(流)中的元素的个数

函数返回值:返回写入到文件中完整元素的个数,若返回值小于count,则极有可能发生了错误

演示代码4.7以二进制的方式向文件test7.txt中写入两个结构体数据。

演示代码4.7:

struct stu
{
	char name[20];
	int age;
	float score;
};

int main()
{
	FILE* pf = fopen("test7.txt", "wb");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	struct stu s[2] = { {"zhangsan", 20, 89.5f}, {"lisi", 30, 90.5f} };

	fwrite(s, sizeof(struct stu), 2, pf);  //将数组s的两个数据输入到pf

	fclose(pf);
	pf = NULL;

	return 0;
}

4.1.8 fread函数

函数原型:size_t fread( void *buffer, size_t size, size_t count, FILE *stream );

函数功能:将文件(流)中的数据以二进制的方式读取到缓冲区buffer

函数参数:

  1. buffer:接收流stream中数据的缓冲区
  2. size:单个读取到的完整元素的大小(字节为单位)
  3. count:读取到的数据的个数

函数返回值:返回实际读取的完整元素的数量,如果发生错误或者在读取完整元素个数达到count之前就读到了文件末尾,则函数返回值小于count。

演示代码4.8从test7.txt文件(演示代码4.7代码执行后的文件)中以二进制的方式读取数据,并将其存放在结构体数组s中。打印结构体数组的内容,程序运行结果为:zhangsan  20  89.500000 (换行) lisi  30  90.500000。

演示代码4.8:

#include<stdio.h>

struct stu
{
	char name[20];
	int age;
	float score;
};

int main()
{
	FILE* pf = fopen("test7.txt", "rb");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	struct stu s[2] = { 0 };

	fread(s, sizeof(struct stu), 2, pf);

	printf("%s  %d  %f\n", s[0].name, s[0].age, s[0].score); //zhangsan  20  89.500000
	printf("%s  %d  %f\n", s[1].name, s[1].age, s[1].score); //lisi  30  90.500000

	fclose(pf);
	pf = NULL;

	return 0;
}

4.2 文件的随机读取

4.2.1 fseek函数

函数功能:使文件指针移动到某个特定的位置

函数原型:int fseek( FILE *stream, long int offset, int origin );

函数参数:

  1. stream:希望发生偏移的文件指针
  2. offset:发生偏移的量
  3. origin:偏移量的相对参考位置

函数返回值:函数成功执行(文件指针偏移成功),返回0,否则返回一个非0值。

origin表示发生偏移的相对位置,是以下三个常量之一:

  • SEEK_SET:表示文件的开头
  • SEEK_CUR:表示文件指针当前的位置
  • SEEK_END:表示文件的末尾

演示代码4.9以读写的方式打开文件test9.txt,使用fputs函数将字符串abcdef传入到文件中。此时,文件指针应该指向文件的末尾,但我希望从文件中读取到第二个字符b,那么,就可以使用fseek函数使文件指针相对于文件起始位置偏移1个字节单位,此时再使用fgetc函数从文件中读取一个字符并打印,程序运行结果为b。

演示代码4.9:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test9.txt", "w+");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	char ch[10] = "abcdef";
	fputs(ch, pf);  //将abcdef传入到文件中

	fseek(pf, 1, SEEK_SET); //使文件指针pf相对于起始位置偏移1
 
	printf("%c\n", getc(pf)); //打印b, 从文件获取一个字符并打印

	fclose(pf);
	pf = NULL;

	return 0;
}

4.2.2 ftell函数

函数功能:返回文件指针相对于文件起始位置的偏移量

函数原型:long int ftell( FILE *stream );

演示代码4.10以只写的方式打开文件test10.txt,向文件中写入两个字符(ab),此时文件指针相对于文件起始位置的偏移量为2,调用ftell函数计算这个偏移量并打印,程序运行结果为2。

演示代码4.10:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test10.txt", "w+");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputs("ab", pf);

	int long off = ftell(pf); //计算此时文件指针相对于文件起始位置的偏移量
	printf("offset = %ld\n", off); //offset = 2

	fclose(pf);
	pf = NULL;

	return 0;
}

4.2.3 rewind函数

函数功能:使函数指针返回到文件的起始位置

函数原型:void rewind( FILE *stream );

演示代码4.11中以读写的方式打开文件test11.txt,向文件中写入字符串"abcdefgh",此时文件指针相对于文件起始位置的偏移量为8。代码希望从文件中读取字符a,a的位置相对于文件起始位置的偏移量为0,程序使用rewind(pf)指令使pf返回到文件起始位置,从文件中读取一个字符并打印,程序的运行结果为a。

演示代码4.11:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test11.txt", "w+"); //以读写的方式打开文件
	if (NULL == pf) //检验文件是否打开成功
	{
		perror("fopen");
		return 1;
	}

	fputs("abcdefgh", pf);  //将字符串"abcdef"写入到文件
	printf("%ld\n", ftell(pf));  //计算文件指针相对于文件起始位置的偏移量并打印,结果为8

	rewind(pf); //调用rewind函数使文件指针回到文件的起始位置

	printf("%c\n", fgetc(pf)); //从文件中获取字符并打印,结果为a

	fclose(pf);
	pf = NULL;

	return 0;
}

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

根据数据的组织形式,数据文件分为二进制文件和文本文件。

  • 二进制文件:数据在内存中以二进制的形式进行存储,如果不加以转换就输出到外存,就是二进制文件。
  • 文本文件:如果要求数据在外存上以ASCII码值的形式进行存储,那么就需要在存储之前进行转。以ASCII码值的形式存储的文件就是文本文件。

二进制文件可能会比文本文件节省空间。如图5.1所示,存储十进制数10000,使用ASCII码的形式存储(文本文件)需要占用5个字节的内存空间,而使用二进制的形式存储需要4字节的内存空间。但是,二进制文件的可读性要远低于文本文件。

图5.1  十进制数10000以ASCII码和二进制方式存储的图解

六. 文件读取结束的判断 

6.1 feof函数(经常被误用于判断文件指针是否到达文件末尾)

函数原型:int feof( FILE *stream );

函数返回值:当文件指针指向文件末尾时,返回非零值,当文件指针不指向文件末尾时,返回零。

警告:feof函数不应用于判断文件读取是否结束,而是用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

6.2 判断文件内容是否全部读取结束的正确方法

应当根据相关函数的返回值来判断读取是否全部结束

(1)读取文本文档时

  • 若使用fgtec函数逐字符读取文件内容,fgetc返回EOF时,文件读取可能全部结束(文件指针指向文件末尾)
  • 若使用fgets函数按行(字符串)读取文件内容,fgets返回NULL时,文件读取可能全部结束

(2)读取二进制文件时

  • 若fread函数返回值小于希望读取到的完整元素个数(函数第三个参数),则可能是文件读取全部结束前的最后一次读取

注意,上文在讲述根据函数返回值判断文件是否全部读取结束时,我都是写的可能全部读取结束,而不是一定,这是因为如果文件没有全部读取结束就发生读取失败,fgetc、fgets函数的返回值也会是EOF、NULL。fread即使不是预期的最后一次读取也会返回小于希望读取到的完整元素个数的数。

那么,如何判读文件读取结束是因为文件内容全部读取结束(文件指针指针指向文件末尾)还是读取过程发生了错误(读取失败)?这里就需要在调用fclose函数关闭文件前使用feof函数判断文件指针是否到达文件末尾。如果feof返回非零值,那么文件全部内容确实都被读取了,如果feof函数返回零,那么则表明读取过程发生了错误,文件内容实际上并没有被全部读取。

fgetc、fgets以及fread函数的返回值对比辨析

(1)fgetc函数

  • 若读取成功,则返回读取到的字符数据的ASCII码值
  • 若读取失败,则返回EOF

(2)fgets函数

  • 若读取成功,则返回指向存放读取到的字符串的内存空间首元素的指针
  • 若读取失败,则返回NULL

(3)fread函数

  • 返回实际读取到的完整元素的个数
  • 如果发现读取到的完整元素个数小于指定读取元素的个数,就是最后一次读取

6.3 ferror函数

函数功能:判读在使用文件(流)的过程中是否发生了错误。

函数原型:int ferror(FILE* stream)

返回值:若在流中没有发生任何错误,函数返回0,若发生错误,函数返回非零值。

6.4 判断文件是否读取结束的代码演示

演示代码6.1从文件test12.txt中使用fgets函数逐字符串读取文件内容,并将每次读取到的内容存储到字符数组ch中,如果正常读取,fgets函数将返回指向ch首元素的指针,如果读取结束或出现错误,则应返回NULL。

通过ferror函数和feof函数联合来判断文件指针是否到达文件末尾,如果读取过程出错,ferror的返回值为真,程序提示文件读取出错。如果成功读取文件全部内容,文件指针会指向文件末尾,feof的返回值为真,程序提示文件读取成功。

演示代码6.1:

#include<stdio.h>

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

	//逐字符读取文件内容
	char c = 0;
	while ((c = fgetc(pf)) != EOF) //从文件中读取单个字符
	{
		putchar(c); //将读取到的字符输出
	}

	//逐字符串读取
	char ch[10] = { 0 };
	rewind(pf); //将文件指针置回到文件起始位置
	while (fgets(ch, 10, pf) != NULL)
	{
		printf("%s", ch);
	}

	putchar('\n');

	if (ferror(pf)) //ferror函数用于判断文件是否出粗,出错返回非零值,否则返回0
	{
		printf("文件读取过程发生错误!\n");
	}
	else if(feof(pf)) //feof用于判断文件指针是否指向文件末尾
	{
		printf("文件读取结束,读取成功!\n");
	}

	fclose(pf);
	pf = NULL;

	return 0;
}

七. 文件缓冲区

ANSIC 标准采用 缓冲文件系统 处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区” 。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
相比于将程序中的数据实时传输到磁盘,文件缓冲区的存在会提高数据传输的效率。
如果希望在缓冲区没有装满时,就立即将缓冲区的数据传入到磁盘上,可以通过调用fflush函数来实现。
fflush的函数原型:int fflush( FILE * stream );
函数功能:将缓冲区中的数据立即传到文件(流)stream中
同时,如果程序运行结束,无论文件缓冲区是否装满,其中的数据都将被传送到磁盘。
全文结束,感谢大家的阅读,敬请批评指正。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值