万字详解C语言关于文件操作的所有内容,简单易懂,欢迎观看

目录

 

1. 使用文件的意义

2. 文件分类

2.1 程序文件

2.2 数据文件

2.3 文件名

3.文件的打开关闭

3.1 文件指针

3.2 文件的打开和关闭

 4. 文件的顺序读写

流 是什么?

4.1 fgetc 和 fputc

4.2 fgets 和 fputs

4.3 fscanf 和 fprintf

 4.4 fwrite 和 fread 

改造文件版本通讯录

5.文件的随机读取

5.1 fseek

5.2 ftell

5.3 rewind

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

 7.文件读取结束的判断

7.1被错误使用的 feof 

8. 文件缓存区


 

1. 使用文件的意义

在我前面的博客中有介绍通讯录的实现,运行时,我们可以增加联系人的信息,此时的数据存放在内存中,当我们关闭程序时,数据就被自动销毁了,等我们再打开程序时,又需要重新输入,真正的通讯录显然是不会这样运行的。我们需要保证数据的持久化问题,把信息记录下来,在我们主动删除时才会删除。

我们一般实现数据持久化的方法有,把数据放在磁盘文件,存放到数据库等方式,使用文件,我们可以将数据直接放在硬盘上,做到了数据的持久化。

2. 文件分类

在程序设计时,我们讨论的文件一般由两种:程序文件,数据文件(从文件功能来分类)

2.1 程序文件

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

2.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要读取的数据,或者输出内容的文件。

本篇博客讨论的是数据文件

2.3 文件名

一个文件要有一个唯一的文件标识,用于用户识别和引用。

文件名包含三个部分:文件路径+文件名主干+文件后缀

例如:D:\C.CODE\Contact1.0

为了方便,文件标识通常被称为文件名。

3.文件的打开关闭

3.1 文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(例如文件名称,文字状态,文件当前位置等)。这些信息保存在一个结构体变量中。该结构体类型是由系统声明的,取名FILE

每当我们打开一个文件时,系统会根据文件的情况自动创建一个FILE结构体变量,并填充其中的信息,具体如何实现我们不必了解。

我们一般都是通过一个FILE指针来维护这个结构体变量,这样用起来更加方便。

例如: FILE *pf  ;

文件指针变量

定义pf 是一个指向结构体类型数据的指针变量。可以使pf 指向某个文件的文件信息区,通过该文件信息区的信息就可以访问文件。

通过文件指针找到与它关联的文件。

3.2 文件的打开和关闭

文件在读写应该先打开文件,使用结束时应该关闭文件。(博客末尾有解释。)

在编写程序的时候,在打开文件的同时,会返回一个FILE* 的指针变量指向该文件,也相当于建立了指针和文件的联系。

ANSIC 规定使用 fopen函数来打开文件,fclose函数来关闭文件。

打开文件: FILE* fopen ( const char* filename ,const char* mode) ; 

打开文件失败时会返回空指针。

关闭文件 : int  fclose ( FILE * stream) ;

参数为空指针时,程序报错

具体打开方式如下:

文件使用方式

含义              

如果指定文件不存在

“r” (只读)

为了输出数据,打开一个已经存在的文本文件

出错

“w”  (只写)

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

建立一个新文件

“a” (追加)

向文本数据的末尾添加数据

建立一个新文件

“rb” (只读)

为了输入数据,打开一个二进制文件

出错

“wb”  (只写)

为了输出数据,打开一个二进制文件

建立一个新文件

“ab” (追加

向二进制文件添加数据

出错

“r+” (读写)

为了读和写打开一个文本文件

出错

“W+” (读写)

为了读和写建立一个文本文件

建立一个新的文件

“a+” (读写)

打开一个文件,在末尾进行读写

建立一个新的文件

“rb+” (读写)

为了读和写打开一个二进制文件

出错

“wb+” (读写)

为了读和写建立一个新的二进制文件

建立一个新的文件

“ab+” (读写)

打开一个二进制文件,在末尾进行读写

建立一个新的文件

表中应该为英文的双引号。

代码实例:


#include<stdio.h>

int main()
{
	FILE* pfile;

	// 打开文件

	pfile = fopen("myfile.text", "w");

	//文件操作

	if (pfile != NULL)
	{
		fputs("test", pfile);
	}
	fclose(pfile);  // 关闭文件
	return 0;

}

我们打开文档所在的位置,可以发现已经新建了一个myfile.text 文件。我们点开后会发现已经存入了我们输入的信息(test)。

 

 4. 文件的顺序读写

功能

函数名

适用于

字符输入函数

fgetc

所有输入流

字符输出函数

fputc

所有输出流

文本行输入函数

fgets

所有输入流

文本行输出函数

fputs

所有输出流

格式化输入函数

fscanf

所有输入流

格式化输出函数

fprintf

所有输出流

二进制输入

fread

文件

二进制输出

fwrite

文件

流 是什么?

可能有人会疑惑,流是什么?,简单的说,正常我们写一个C程序会用到很多外部设备,比如键盘,屏幕,硬盘,网卡等等,如果对众多的外部设备都分别列出具体的操作方法,无疑是特别繁琐的,加重了程序员的学习成本,而且也没有必要,所以我们可以简单的理解为,封装了一个巨厉害的函数,我们只需要操作它,就可以实现对所有外部设备的控制,这种就可以称为 流。

 

在C程序运行时,会默认运行,标准输入流 stdin (键盘)  ,标准输出流  stdout (屏幕) ,标准错误流 stderr 。它们的类型都是 FILE*  这也是很多时候我们可以直接使用他们的理由。

4.1 fgetc 和 fputc

fgetc  

int fgetc ( FILE * stream)

如果读取错误,返回 EOF

fputc

int fputc (int c ,  FILE * stream) 

代码实例:

向文件写入26个字母。

#include<stdio.h>

int main()
{
	FILE* pfile;

	// 打开文件

	pfile = fopen("myfile.text", "r");

	//文件操作

	
	if (pfile != NULL)
	{
		int ch = 0;
		while ((ch = fgetc(pfile)) != EOF)
		{
			printf("%c", ch);
		}
		fclose(pfile);
        pfile = NULL;
   	}
	
	
	return 0;

}

从文件中读取字母

#include<stdio.h>

int main()
{
	FILE* pfile;

	// 打开文件

	pfile = fopen("myfile.text", "r");

	//文件操作

	
	if (pfile != NULL)
	{
		int ch = 0;
		while ((ch = fgetc(pfile)) != EOF)
		{
			printf("%c", ch);
		}
		fclose(pfile);
        pfile = NULL;
   	}
	
	
	return 0;

}

4.2 fgets 和 fputs

fputs

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

正常输出时,返回非负值 ,输出错误时,返回EOF 。

代码实例:

#include<stdio.h>

int main()
{
	FILE* pf = fopen("text.tet", "w");

	if (pf == NULL)
	{
		perror("fopen:");
	}
	else
	{
		fputs("abcdef", pf);

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

 

fgets

char *fgets( char *string, int n, FILE *stream );

读取错误,返回空指针

fgets函数从输入流参数中读取字符串,并将其存储在字符串中。fgets从当前流位置读取字符,包括第一个换行符,直到流结束,或直到读取的字符数等于n–1,以先到者为准。存储在字符串中的结果将附加一个空字符。如果已读取,则新行字符将包含在字符串中。

代码实例:


#include<stdio.h>

int main()
{
	FILE* pf = fopen("text.tet", "r");
	char arr[100] = { 0 };
	if (pf == NULL)
	{
		perror("fopen:");
	}
	else
	{
		fgets(arr, 7,pf);
		printf("%s", arr);
		
		fclose(pf);
		pf = NULL;
	}
	return 0;
}

4.3 fscanf 和 fprintf

scanf   int scanf( const char *format [,argument]... );

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

printf  int printf( const char *format [, argument]... );

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

通过和printf函数和scanf函数的对比我们可以发现,他们的参数除了多了文件指针,其他的完全一致。我们在使用的时候,直接加上文件指针,其他的细节与普通的函数没有区别。

fprintf 函数使用 

代码实例:

#include<stdio.h>
struct S
{
	char name[10];
	int age;
	double score;
};

int main()
{
	struct S s = { { "张三" },{20}, {55.5} };

	FILE* pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		perror("fopen:");
	}
	else
	{
		fprintf(pf, "%s %d %lf", s.name, s.age, s.score);

	}
}

 fscanf函数使用

代码实例:

#include<stdio.h>
struct S
{
	char name[10];
	int age;
	double score;
};

int main()
{
	struct S s = { 0 };

	FILE* pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen:");
	}
	else
	{
		fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.score));    // 从文件中输入信息到程序 结构体s 中
		fprintf(stdout, "%s %d %lf", s.name, s.age, s.score);    // 将结构体s 的信息 直接输出到 标准输出流stdout (屏幕)

	}
}

 4.4 fwrite 和 fread 

fwrite函数,将数据写入流中

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

buffer 数据的地址 

size 一个数据的大小

count  数据的数量

stream  要写入流的位置

#include<stdio.h>

struct S
{
	char name[10];
	int age;
	double score;
};

int main()
{
	struct S s = { "张三",10,8.3};
	FILE* pf = fopen("text.tet", "wb");

	if (pf == NULL)
	{
		perror("fopen");
	}
	else
	{
		fwrite(&s, sizeof(struct S), 1, pf);
		fclose(pf);
		pf = NULL;
	}
}

 因为是二进制写入,用记事本打开显示的是乱码。

fread 函数,从文件中读取二进制信息

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

从流(stream)中读取 count 个 大小为 size 的数据,存入 buffer 这个地址指向的空间中。

#include<stdio.h>

struct S
{
	char name[10];
	int age;
	double score;
};

int main()
{
	struct S s = {0};
	FILE* pf = fopen("text.tet", "rb");  // 打开文件

	if (pf == NULL)
	{
		perror("fopen");
	}
	else
	{
		fread(&s, sizeof(struct S), 1, pf);   //从文件中读取信息
		fprintf(stdout, "%s,%d,%lf", s.name, s.age, s.score);   //直接将信息输出到屏幕上
		fclose(pf);
		pf = NULL;
	}
}

 文件中存储的数据类型为结构体S,那么我们同样也需要用结构体S的类型来接收数据,否则就出错

学到这里,我们就可以将我们的通讯录再改造一下,将其升级为文件版本。

改造文件版本通讯录

首先我们需要知道应该做出哪些优化。

1. 文件结束时,我们应该将输入的数据,存入文件中

//将输入的数据存入文件中

void save_contact(contact* pc)
{
	FILE* pf = fopen("contact.tt","wb");

	int i = 0;

	if (pf == NULL)
	{
		perror("save_fopen");
	}
	else
	{
		for (i = 0; i < pc->sz; i++)
		{
			fwrite(pc->data + i, sizeof(peoinfo), 1, pf);
		}
		fclose(pf);
		pf = NULL;
		printf("保存文件成功");
	}

}

2. 当我们再次运行程序时,应该在程序初始化阶段,读取文件中的数据存入程序中,我们选择封装一个读取函数,并且改造初始化函数。

//从文件读取数据

void load_contact(contact* pc)
{
	FILE* pf = fopen("contact.tt", "rb");
	peoinfo tmp = { 0 };

	if (pf == NULL)
	{
		perror("save_fopen");
	}
	else
	{
		while (fread(&tmp, sizeof(peoinfo), 1, pf))   // fread 函数的返回值为它读取的数据个数,读完数据后,返回值为0,循环结束
		{
			cheak_capacity(pc);
			pc->data[pc->sz] = tmp;
			pc->sz++;
		}
		
		fclose(pf);
		pf = NULL;
		printf("保存文件成功");
	}
}
//初始化   文件版本
void Init_Contact(contact* pc)
{
	pc->sz = 0;
	pc->capacity = capMAX;
	pc->data = (peoinfo*)malloc(pc->capacity * sizeof(peoinfo));
	if (pc->data == NULL)
	{
		perror("Init_Contact:malloc:");
	}
	memset(pc->data, 0, pc->capacity * sizeof(peoinfo));
	load_contact(pc);

}

细节问题比如新增的函数声明之类就不多说了。

此时,程序算的上一个真正的通讯录。

文件版本通讯录源码分享:Contact 3.0 · 斯文/mytest - 码云 - 开源中国 (gitee.com)

之前静态版本的博客分享:http://t.csdn.cn/i2oNW

5.文件的随机读取

5.1 fseek

int fseek( FILE *stream, long offset, int origin );

根据文件指针的位置和偏移量来定位文件指针。stream 为流,offest 为偏移量 origin复杂一点。

origin:

SEEK_CUR

Current position of file pointer     文件当前位置

SEEK_END

End of file          文件末尾位置

SEEK_SET

Beginning of file   文件开始位置

代码实例:


#include<stdio.h>

int main()
{
	FILE* pf = fopen("test1.txt", "w");
	fputc('a', pf);
	fputc('b', pf);
	fputc('c', pf);
	fputc('d', pf);

	fseek(pf, 1, SEEK_SET);   // 将文件指针定位到和文件初始位置偏移量为1 的位置
	fputc('j', pf);
	fclose(pf);
	pf = NULL;

	return 0;

}

 

5.2 ftell

long ftell( FILE *stream );

返回文件指针相对于起始位置的偏移量。(返回类型为long)

5.3 rewind

void rewind( FILE *stream );

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

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

数据在内存中以二进制形式存储,如果不加以转换输出到外存,就是二进制文件

以ASCII字符的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式储存,数值型数据也可以用ASCII形式储存,也可以使用二进制形式储存。

举个例子:

将10000输入到磁盘中。

1 ASCII形式

#include<stdio.h>

int main()
{
	FILE* pf = fopen("test2.txt", "w");
	fputs("10000", pf);
	fclose(pf);
	pf = NULL;
}

 2 二进制形式

#include<stdio.h>

int main()
{
	int a = 10000;
	FILE* pf = fopen("test2.txt", "wb");
	fwrite(&a, 4, 1, pf);
	fclose(pf);
	pf = NULL;
}

 当我们使用二进制打开,

 

 7.文件读取结束的判断

7.1被错误使用的 feof 

不可以用feof的返回值来判断文件是否结束。

而是应用于当文件读取结束时,判断是读取失败还是遇到文件末尾

文本文件判断读取是否结束,判断返回值是否为EOF 或者 NULL。

例如:fgetc 判断是否为 EOF

二进制的文件读取判断返回值是否小于实际要读的个数

例如:fread 判断返回值是否小于实际要读的个数。

ferror

int ferror( FILE *stream );

Tests for an error on a stream.

feof

int feof( FILE *stream );

Tests for an error on a stream.

8. 文件缓存区

ANSIC 标准采用 “标准缓冲文件”处理数据文件,系统自动的在内存中为程序中每一个正在使用的文件开辟一块“文件缓存区”。从内存向磁盘输出数据会先送到内存的缓存区,装满缓冲区后才一起送到磁盘上。如果从磁盘向程序读入文件,同样要经过缓冲区(充满),再从缓冲区将数据送到程序。缓冲区的大小由C编译决定。

 我们可以用一个代码检验


#include<stdio.h>

int main()
{
	FILE* pf = fopen("test3.txt", "w");
	fputs("abcdef", pf); //  先将数据放在缓冲区。
	Sleep(20000);  // 睡眠20秒,我们可以打开文件,发现没有内容
	printf("刷新缓冲区\n");
	fflush(pf);  // 刷新缓冲区,将数据写入磁盘。
	Sleep(20000);
	fclose(pf); //文件关闭时,会自动刷新缓冲区,上面的睡眠20秒,就是为了区别fflush 和 flose
	pf = NULL;
}

大家可以根据这段代码自行验证。

由此我们可以得出,由于文件缓存区的存在,C语言在操作文件时,需做刷新缓冲区或者在文件操作结束时关闭文件。如果不做,可能导致文件读写出现问题。

本篇博客到此结束,评论区欢迎讨论,有问必答

谢谢观看!

 

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值