C语言(超详细讲解,帮你零基础玩转C语言) | 文件操作——数据文件的操作

该博客聚焦C语言文件操作,先介绍文件概念,包括作用、分类、存储形式等,接着阐述文件使用,如打开关闭、顺序与随机读写函数,还提及文件读取结束判定方法,强调操作文件时刷新缓冲区或关闭文件的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文件操作

引言

当涉及数据的存储和管理时,文件操作是C语言中至关重要的一部分。通过文件操作,程序可以读取、写入和修改文件中的数据,这使得C语言成为处理各种数据类型的理想选择。本博客将介绍C语言中文件操作的基础知识,包括如何打开、读取和写入文件,以及如何处理文件指针和错误处理。我们还将探讨如何使用不同的文件模式(例如读取、写入、追加等)来实现各种操作,并提供一些实用的技巧和最佳实践。通过这篇博客,读者将能够了解如何在C语言中有效地处理数据文件,为他们的项目和应用程序提供可靠的文件管理功能。

1. 文件的概念

1.1 文件的作用及概念

文件的作用
大家有没有想过,为什么我们会使用文件?如果没有文件,那我们写的程序的数据只能存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的。如果要将数据进行持久化的保存,我们可以使用文件。
文件的概念及分类
文件是储存在我们电脑的磁盘上的。在程序设计中,从文件功能的角度来分类,我们一般会涉及到两种文件,一种是程序文件,一种是数据文件。
程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章所介绍的就是数据文件。


1.2 数据文件的概念及存储形式

我们处理数据的输入输出的时候,不只是以终端为对象,即从终端的键盘输入数据,运行的结果显示到显示器上。还可以以文件为对象,把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘的文件了。

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

数据文件的存储形式
根据数据的组织形式,数据文件被称为文本文件或者二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

数据是如何存储在文件中的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
示例
整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。请添加图片描述


2.文件的使用

2.1 文件的打开和关闭

2.1.1 流


C语言中的流(stream)是一个抽象的概念,用于处理输入和输出数据的序列。流可以看作是数据从源头(输入流)流向目的地(输出流)的通道。在C语言中,流可以是与文件、设备或内存缓冲区相关联的抽象数据流。

流的主要作用是提供了一种统一的接口来处理不同来源和去向的数据,例如从键盘读取数据、向屏幕输出数据、从文件读取数据等。通过流,C程序可以以一种统一的方式处理这些数据,而不需要关心数据的具体来源和去向。

标准流
为什么我们在键盘上输入数据的时候,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:

stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。

stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。

stderr - 标准错误流,大多数环境中输出到显示器界面。

这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。


2.1.2 文件缓冲区

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

请添加图片描述


2.1.3 文件指针

缓冲文件系统中,关键的概念是 “文件类型指针”,简称 “文件指针”
每个被使用的文件都在内存中开辟了一个相应的 文件信息区,用来存放文件的相关信息
(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中
的。该结构体类型是由系统声明的,取名 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.1.4 文件的打开模式

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

//打开⽂件
FILE * fopen ( const char * filename, const char * mode );

//关闭⽂件
int fclose ( FILE * stream );

mode表示文件的打开模式,下面都是文件的打开模式:

请添加图片描述

示例“w”

#include <stdio.h>
int main ()
{
	 FILE * pFile;
 //打开⽂件
 	pFile = fopen ("myfile.txt","w");
 //⽂件操作
 	if (pFile!=NULL)    //检查文件是否打开成功
 	{
 		fputs ("fopen example",pFile);   //向文件中写入fopen example
 	//关闭⽂件
 	fclose (pFile);
	}
 return 0;
}

2.2 文件函数的介绍与使用

2.2.1 文件的顺序读写

请添加图片描述

非必须,需用自取
fgetc

从流中获取字符。

int fgetc ( FILE * stream ); //(文件指针)

返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器会前进到下一个字符。
如果在调用时流已到达文件结束,则该函数返回 EOF 并设置流的文件结束指示器(feof)。
如果发生读取错误,则该函数返回 EOF 并设置流的错误指示器(ferror)。
fgetc 和 getc 是等效的,除了在某些库中 getc 可能被实现为宏之外。

返回值:
成功时,返回读取的字符(提升为 int 值)。
返回类型为 int,以适应特殊值 EOF,该值表示失败:
如果位置指示器位于文件结尾,则函数返回 EOF,并设置流的文件结束指示器(feof)。
如果发生其他读取错误,函数也返回 EOF,但会设置其错误指示器(ferror)。


fputc

向流中写入字符

int fputc ( int character, FILE * stream );  // (字符,文件指针)

将字符写入流并推进位置指示器。
该字符被写入到流的内部位置指示器指示的位置,然后该位置指示器自动前进一个位置。

int character :要写入的字符的 int 提升值。 在写入时,该值会被内部转换为 unsigned char。

返回值:
成功时,返回已写入的字符。
如果发生写入错误,返回 EOF 并设置错误指示器(ferror)。


fgets

从流中获取字符串

char * fgets ( char * str, int num, FILE * stream );

从流中读取字符,并将它们存储为 C 字符串到 str 中,直到读取了 (num-1) 个字符,或者首先达到换行符或文件结束符,以先发生者为准。
换行符会使 fgets 停止读取,但它被该函数视为有效字符,并包含在复制到 str 中的字符串中。
在复制到 str 中的字符之后自动添加终止空字符。
请注意,fgets 与 gets 相比有很大的不同:fgets 不仅接受流参数,还允许指定 str 的最大大小,并将任何结束的换行符包含在字符串中。

char* str:指向一个字符数组的指针,用于存储读取的字符串。
int num:要复制到 str 中的最大字符数(包括终止空字符)。

返回值:
成功时,函数返回 str。
如果在尝试读取字符时遇到文件结束符,将设置 eof 指示器(feof)。如果这在任何字符被读取之前发生,则返回的指针是空指针(并且 str 的内容保持不变)。
如果发生读取错误,则设置错误指示器(ferror),并且还返回空指针(但 str 指向的内容可能已更改)。


fputs

向流中写入字符串

int fputs ( const char * str, FILE * stream );

将由 str 指向的 C 字符串写入流。
该函数从指定地址(str)开始复制,直到达到终止空字符(‘\0’)。此终止空字符不会被复制到流中。
请注意,fputs 与 puts 不同之处不仅在于可以指定目标流,而且 fputs 不会写入额外的字符,而 puts 则会自动在末尾附加换行符。

const char* str:要写入到流中的内容的 C 字符串。

返回值:
成功时,返回一个非负值。
发生错误时,函数返回 EOF 并设置错误指示器(ferror)。


fscanf

从流中读取格式化的数据

int fscanf ( FILE * stream, const char * format, ... );

从流中读取数据,并根据参数 format 将它们存储到由额外参数指向的位置。
额外参数应该指向已分配的对象,类型与格式字符串中相应的格式说明符指定的类型相匹配。

const cahr* format

C 字符串包含一系列控制从流中提取的字符如何处理的字符序列:
空白字符:函数将读取并忽略在下一个非空白字符之前遇到的任何空白字符(空白字符包括空格、换行和制表符 – 参见 isspace)。格式字符串中的单个空白字符验证从流中提取的任何数量的空白字符(包括没有)。
非空白字符,除了格式说明符(%):任何不是空白字符(空格、换行或制表符)或格式说明符的字符(以 % 字符开头)将导致函数从流中读取下一个字符,将其与此非空白字符进行比较,并且如果匹配,则将其丢弃并继续使用 format 的下一个字符。如果字符不匹配,则函数失败,返回并将流的后续字符保持未读。
格式说明符:由初始百分号(%)组成的序列表示格式说明符,用于指定从流中检索的数据类型和格式,并存储到额外参数指向的位置。

返回值:
成功时,函数返回成功填充的参数列表项数。此计数可以与预期的项目数匹配,也可以因匹配失败、读取错误或到达文件结束而少于预期数(甚至为零)。
如果在读取过程中发生读取错误或到达文件结束,将设置相应的指示器(feof 或 ferror)。如果在成功读取任何数据之前发生任何情况,则返回 EOF。
如果在解释宽字符时发生编码错误,则函数将 errno 设置为 EILSEQ。


fprintf

向流中写入格式化的数据

int fprintf ( FILE * stream, const char * format, ... );

将由 format 指向的 C 字符串写入流。如果 format 包含格式说明符(以 % 开头的子序列),则跟在 format 后面的额外参数将被格式化并插入到结果字符串中,替换相应的格式说明符。
在 format 参数之后,函数期望至少有与 format 指定的一样多的额外参数。

format:包含要写入流中的文本的 C 字符串。
它可以选择包含嵌入的格式说明符,这些格式说明符将被后续额外参数中指定的值替换,并按要求格式化。

返回值:
成功时,返回写入的字符总数。
如果发生写入错误,则设置错误指示器(ferror)并返回一个负数。
如果在写入宽字符时发生多字节字符编码错误,则将 errno 设置为 EILSEQ 并返回一个负数。


fraed

从流中读取数据块

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

从流中读取数据块,并将其存储到由 ptr 指定的内存块中。
流的位置指示器将被读取的字节数总和推进。
如果成功,则读取的总字节数为(size*count)。

void* ptr:指向至少具有(sizecount)字节大小的内存块的指针,转换为 void 类型。
size_t size:每个要读取的元素的大小,以字节为单位。 size_t 是一个无符号整数类型。
size_t count:每个元素的数量,每个元素的大小为 size 字节。 size_t 是一个无符号整数类型。

返回值:
成功读取的元素总数将被返回。
如果此数字与 count 参数不同,则可能发生了读取错误或在读取时已达到文件结束。在这两种情况下,将设置适当的指示器,分别可以使用 ferror 和 feof 进行检查。
如果 size 或 count 中有一个为零,则函数返回零,并且流状态和 ptr 指向的内容均保持不变。
size_t 是一个无符号整数类型。


fwrite

向流中写入数据块

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

将由 ptr 指向的内存块中的数组写入流中的当前位置。
流的位置指示器将被写入的字节数总和推进。
在内部,函数将 ptr 指向的块解释为一个类型为 unsigned char 的(size*count)元素的数组,并按顺序将它们写入流中,就好像对每个字节调用了 fputc 一样。

const void * ptr :指向要写入的元素数组的指针,转换为 const void* 类型。
size_t size:每个要写入的元素的大小,以字节为单位。 size_t 是一个无符号整数类型。
size_t count:每个元素的数量,每个元素的大小为 size 字节。 size_t 是一个无符号整数类型。

返回值:
成功写入的元素总数将被返回。
如果此数字与 count 参数不同,则表示写入错误阻止了函数的完成。在这种情况下,流的错误指示器(ferror)将被设置。
如果 size 或 count 中有一个为零,则函数返回零,并且错误指示器保持不变。
size_t 是一个无符号整数类型。


对比一组函数

scanf / fscanf / sscanf
printf / fprintf / sprintf

scanf —— 从标准输入流上读取格式化的数据
fscanf —— 从指定的输入流上读取格式化的数据
sscanf —— 在字符串中读取格式化的数据
printf —— 把数据以格式化的形式打印在标准输出流上
fprintf —— 把数据以格式化的形式打印在指定的输出流上
sprintf —— 把格式化的数据转化成字符串


2.2.2 文件的随机读写
fseek

根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。

int fseek ( FILE * stream, long int offset, int origin );  //(文件指针,偏移量,起始位置)

FILE * stream:指向识别流的 FILE 对象的指针。

long int offset :二进制文件:从原点偏移的字节数。 文本文件:要么是零,要么是 ftell 返回的值。

int origin :用作偏移参考的位置。由 中定义的以下常量之一指定,专门用作此函数的参数:
常量 参考位置
SEEK_SET 文件开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件结尾

示例

#include <stdio.h>
int main ()
{
 	FILE * pFile;
 	pFile = fopen ( "example.txt" , "wb" );
 	fputs ( "This is an apple." , pFile );
 	fseek ( pFile , 9 , SEEK_SET );
 	fputs ( " sam" , pFile );
 	fclose ( pFile );
 	return 0;
}

ftell

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

long int ftell ( FILE * stream ); //(文件指针)

成功时,返回位置指示器的当前值。
失败时,返回 -1L,并将 errno 设置为系统特定的正值。

示例

#include <stdio.h>
int main ()
{
 	FILE * pFile;
 	long size;
 	pFile = fopen ("myfile.txt","rb");
 	if (pFile==NULL) 
 	perror ("Error opening file");
 	else
 	{
 		fseek (pFile, 0, SEEK_END); // non-portable
 		size=ftell (pFile);
 		fclose (pFile);
 		printf ("Size of myfile.txt: %ld bytes.\n",size);
 	}
 	return 0;
}

rewind

让文件指针的位置回到文件的起始位置

void rewind ( FILE * stream ); //(文件指针)

示例

#include <stdio.h>
int main ()
{
 	int n;
 	FILE * pFile;
 	char buffer [27];
 
 	pFile = fopen ("myfile.txt","w+");
 	for ( n='A' ; n<='Z' ; n++)
 	fputc ( n, pFile);
 	rewind (pFile);
 
 	fread (buffer,1,26,pFile);
 	fclose (pFile);
 
 	buffer[26]='\0';
 	printf(buffer);
 	return 0;
}

2.3 文件读取结束的判定

被错误使用的feof
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。

请添加图片描述

  1. 文本文件读取是否结束,判断返回值是否为 EOFfgetc ),或者 NULLfgets
    例如:
    fgetc 判断是否为 EOF .
    fgets 判断返回值是否为 NULL .

请添加图片描述

  1. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    fread判断返回值是否小于实际要读的个数。

文本文件的示例

#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);
}

二进制文件的示例

include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
	double a[SIZE] = {1.,2.,3.,4.,5.};
	FILE *fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式	
	fwrite(a, sizeof *a, SIZE, fp); 
// 写 double 的数组
 	fclose(fp);
 	double b[SIZE];
 	fp = fopen("test.bin","rb");
 	size_t ret_code = fread(b, sizeof *b, 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);
}

在文章的最后在给大家补充一个知识点

#include <stdio.h>
#include <windows.h>
//VS2022 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能会导致读写文件的问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值