C语言文件和文件操作

在了解C语言文件操作时,首先我们应该知道什么是文件?需要一点文件的基础知识。 

目录

文件知识储备

流的概念

文件指针

⽂件的打开和关闭

⽂件的顺序读写

⽂件的随机读写

⽂件读取结束的判定

⽂件缓冲区

文件知识储备

文件是可以持久化保存数据,一般磁盘上的文件就是文件。在程序设计中,以功能分类文件一般分为两种:程序文件,数据文件。

程序⽂件

程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows 环境后缀为.exe)。

数据⽂件
⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件。
本文主要讨论数据文件
一个文件是需要唯一标志(文件名),一般由三个部分组成: 件路径+文件名主干+文件后缀 。数据文件从数据组成形式(储存方式)又可以分成二进制文件文本文件
二进制文件:就是将内存中以二进制储存的数据,不加转换直接输出到外存(如:文件)的文件。
文本文件: 如果要求在外存上以ASCII码(中文是其他编码规则)的形式存储,则需要在存储前转换。以ASCII字符的形式存储(外存)的⽂件就是⽂本⽂件(将内存中二进制数据按照ASCLL编码规则写入到外存文件)。
所以 字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⼆进制形式输出,则在磁盘上只占4个字节(VS2022测试)。
代码实现
int main()
{
	FILE* pf = fopen("test.txt", "wb");//wr以二进制文本写入
	if (pf == NULL)
	{
		perror("fopen");//报错,也可以用assert
		return 1;//提前结束程序
	}
	int a = 10000;
	fwrite(&a, 4, 1, pf);
	fclose(pf);//关闭文件,节省资源
	pf = NULL;//赋空指针无意解引用
	return 0;
}

以二进制编辑器打开test.txt,可以看到它的十六进制,查看是不要关闭文件(屏蔽fclose(pf))

流的概念

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,从外部输入输出数据是需要一些操作的,不同的外部设备的输⼊输出操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流 想象成流淌着字符的河。C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语⾔程序在启动的时候,默认打开了3个流:
stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出
流中。
stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为⽂件指针。
C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。

文件指针

缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”。
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名 字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系 统声明的,取名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*的指针变量指向该⽂件,也相当于建⽴了 指针和⽂件的关系。
ANSIC 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。

⽂件使⽤⽅式

含义

如果指定件不存在

“r”(只读)

为了输⼊数据,打开⼀个已经存在的⽂本⽂件

出错

“w”(只写)

为了输出数据打开一个文本文件

⽴⼀个新的

a”(追加)

件尾添加数据

⽴⼀个新的

“rb”(只读)

为了输数据,打开进制文件

出错

“wb”(只读)

为了输数据,打开进制文件

⽴⼀个新的

“ab”(追加)

向一个二进制件尾添加数据

⽴⼀个新的

“r+”(读写)

为了读和写,打开本文件

出错

“w+”(读写)

为了读和写,建立本文件

⽴⼀个新的

“a+”(读写)

打开件,在件尾进读写 建⽴⼀个新的

⽴⼀个新的

“wb+”(读写)

为了读和写,新建个新的进制

⽴⼀个新的

“rb+”(读写)

为了读和写打开进制

出错

“ab+”(读写)

打开进制件,在件尾进读和写

建立个新的

例子可看上面的代码

⽂件的顺序读写

文件读写应该是要有一定的规则的

顺序读写函数介绍

函数名

功能

fgetc

字符输函数

所有输

fputc

字符输出函数

所有输出流

fgets

函数

所有输

fputs

输出函数

所有输出流

fscanf

格式化输函数

所有输

fprintf

格式化输出函数

所有输出流

fread

进制输

fwrite

进制输出

函数具体定义(如果有的我写的不是很清楚可以看我的另外一篇博客,哪里有说去哪里可以看c语言这些语法知识)

int fgetc( FILE *stream );//steam是流的意思,该函数可以从流读取数据输入到要赋值变量

fgetc返回成功读取的字符的ASCLL码值,或返回 EOF 以表示错误或文件结束。 使用 feof 或 ferror 来区分错误和文件结束情况。对于 fgetc,如果发生读取错误,则设置流的错误指示器。

int fputc( int c, FILE *stream );//该函数是把c的内容输出到流,返回值为所写字符的ASCLL码值,EOF表示错误

char *fgets( char *string, int n, FILE *stream );//返回的是读取到的字符串的地址,返回 NULL 表示错误或文件结束条件。使用 feof 或 ferror 确定是否发生错误。

int fputs( const char *string, FILE *stream );//如果成功,这些函数中的每一个都返回一个非负值。出现错误时,fputs返回EOF

int fscanf( FILE *stream, const char *format [, argument ]... );//stream后面就和scanf写法一样,下面也有代码示例

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

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

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );//前面有

上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流)
每个用法大同小常,我举几个例子(如要查看编辑的文件:源文件->添加->现有项(vs2022为例))
//对fputc的示例
int main()
{
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL)
    {
	    perror("fopen");
	    return 1;
    }
    for (int ch = 'a'; ch <= 'z'; ch++)
	    fputc(ch, pf);
    fclose(pf);
     pf = NULL;
}
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//对fgets和fprintf的示例
	char ch[100];
	fgets(ch, 27, pf);
	fprintf(stdout, "%s", ch);//读取pf指向的文件(test.txt)里面的的内容并且输出到屏幕
	//stdout标准输出

	//后面的概念例子
	printf("\n");
	printf("%d\n", ftell(pf));//ftell返回现在光标和首位置的偏移量
	rewind(pf);//指针(光标)回到首位

	int c = 0;
	//fgetc的例子
	while ((c = fgetc(pf)) != EOF)//fgetc结束返回EOF
	{
		printf("%c", (char)c);//用fgetc读取,其实也可可以不用强行转换为char
		//因为字符本来就是以ASCLL储存
	}
	fclose(pf);
	pf = NULL;
	return 0;
}
//fscanf
int main()
{
    int a = 0;
    printf("输入一个数:");
    if (fscanf(stdin, "%d", &a) != 1)
    {
	    printf("读取失败");
	    return 1;
    }
    printf("\n(printf输出)输入值为%d:", a);
    fprintf(stdout, "\n(fprintf输出)输入值为%d:", a);
}
对⽐⼀组函数:
scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf——从标准输入流读取格式化数据
fscanf——从指定输入流读取格式化数据
sscanf——在字符串中格式化读取数据
sprintf——把格式化的数据写入字符串
struct stu
{
	char name[20];
	int age;
	float score;
}s;
//fscanf演示
int main()
{
	s.age = 20;
	s.score = 80.5f;
	*s.name = "zhangsan";
	sprintf(s.name,"%d %f",s.age,s.score);
	printf("%s", s.name);
	int a = 0;
	char ch[20] = "123";
	if ((sscanf(ch, "%d", &a) == 1))
		printf("\nch = %d", a);

	return 0;
}

以上了解完后可以就可以尝试一下文件的拷贝,我作一个简单的示例
int main()
{
	FILE* pf = fopen("data.txt", "r");//data.txt事先我已经放了数据,这边就不写了
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	FILE* cpf = fopen("data_copy.txt", "w");
	if (cpf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch;
	while ((ch = fgetc(pf)) != EOF)
	{
		fputc(ch, cpf);
	}
	fclose(pf);
	fclose(cpf);
	cpf = NULL;
	pf = NULL;
	return 0;
}

⽂件的随机读写

很显然有顺序读写那么应该也有随机读写

fseek:int fseek( FILE *stream, long offset, int origin );//steam是文件流,offset是要偏移的量,origin必须是三个常量之一。返回值:如果成功,fseek 返回 0。否则,它返回一个非零值。在无法进行查找的设备上,返回值未定义。

fseek 函数将与流关联的文件指针(如果有)移动到与原点偏移字节的新位置。流上的下一个操作发生在新位置。在打开用于更新的流上,下一个操作可以是读取或写入。参数 origin 必须是以下常量之一,在 STDIO 中定义.

origin的三个常量 (SEEK_CUR   文件当前位置,SEEK_END   文件末尾,SEEK_SET    文件起始位置

rewind:void rewind( FILE *stream );//让光标回到起始位置

ftell:long ftell( FILE *stream );//返回值是光标现在位置相对于起始位置的偏移量

例子:

#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char ch[20] = "hello world";//现在文件内是hello
	fputs(ch,pf);//数组名就是数组首元素地址
	fclose(pf);
	pf = NULL;

	//
	FILE* ptf = fopen("test.txt", "r");
	if (ptf == NULL)
	{
		perror("fopen");
		return 1;
	}
	char c = fgetc(ptf);
	printf("打印h:%c\n", c);//打印一个字符,应该是h

	//我想打印w,那么光标应该指向w
	//hello world,起始位置相对于w的偏移量应该是6(空格也是字符),但是已经读取了h,现在指针应该偏移了一位
	//所以相对于现在位置只要控制指针偏移5为即可
	fseek(ptf, 6,SEEK_SET);//相对于起始位置
	printf("相对于起始位置打印w:%c\n", fgetc(ptf));

	//现在光标(指针)应该来到了o
	printf("%d\n",ftell(ptf));//可以用ftell确定一下,应该是7

	//我想打印d
	fseek(ptf, -1, SEEK_END);//相对于末尾
	printf("相对于末尾位置打印d:%c\n", fgetc(ptf));

	//指针又来到了末尾(读取一次指针自动下移一位)
	fseek(ptf, -1, SEEK_CUR);//再次打印d
	printf("相对于当前位置打印d:%c", fgetc(ptf));
    fclose(ptf);
    ptf = NULL;
	return 0;
}

⽂件读取结束的判定

我在文件的顺序读写中有提到设置流的错误指示器,这里讲一下它们是什么

feof 和 ferror

牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。

feof 的作⽤是:当⽂件读取结束后,判断是读取结束的原因是否是遇到⽂件尾结束,对于是遇到文件末尾结束返回非零,否则返回零。

ferror的作用是:用于检测流上的错误标签是否被设置(在文件的读取或写入时如果发生错误会设置一个错误标签),如果错误标签被设置返回非零,否则返回零。所以是文件结束后,判断是否是发生错误导致文件结束,

既然我们知道了判断文件结束后结束的原因,那么我们也应该知道如何知道文件结束了
⽂本⽂件读取是否结束,判断返回值是否为 EOF fgetc ),或者 NULL fgets
⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
fread判断返回值是否⼩于实际要读的个数feof返回值:如果当前位置不是文件末尾,则返回0。是文件末尾则返回非零。
下面我举一个例子
int main()
{
	FILE* pf = fopen("test.txt", "r");//fputc写入应该是'w',故意写错测试
	if (pf == 0)
	{
		perror("fopen");
		return 1;
	}
	for(char c = 'a'; c <= 'z'; c++)
		fputc(c, pf);
	if (feof(pf))//是末尾返回非零,反之零
	{
		printf("\n遇到文件末尾,读取正常结束");
	}
	else if (ferror(pf))//有错误标记返回零,反之非零
	{
		perror("fputc");
	}
	return 0;
}

可以看到错误提示

⽂件缓冲区

一点注意事项

ANSIC 标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为

程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓
冲区,装满缓冲区后才⼀起送到磁盘上(提高效率)。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输 入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

//这是我从别处搞来的
#include <stdio.h>
#include <windows.h>
//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语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭文件。 如果不做,可能导致读写⽂件的问题
作者水平不高,如有错误感谢指正,最后谢谢你的耐心阅读,再见!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值