C语言进阶文件数据操作详解(万字教你真正理解文件使用)

Posted HyDraZya

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言进阶文件数据操作详解(万字教你真正理解文件使用)相关的知识,希望对你有一定的参考价值。

文件操作

目录

​​​​​​一、我们为什么需要文件?

二、究竟什么才是文件

程序文件

数据文件

三、文件名

四、文件类型

五、文件缓冲区

六、文件指针

七、打开文件和关闭文件

fopen

fclose

八、文件顺序读写表

流(stream)

fputc

fgetc

         fgets

fputs

fprintf

fscanf

对比一组函数

fwrite

fread

九、文件随机读写

fseek

ftell

rewind

十、文件读取结束的判定


一、我们为什么需要文件?

        由于我们之前所编写的程序在执行过程中所产生的数据及结果都只是临时存放在内存区域,一旦程序运行结束后,该程序所开辟的内存空间将全部返回给操作系统。那么此时我们再想要去查看之前的数据和执行结果,显示是不可能的。

那该如何解决这个问题呢? 

我们需要用到的就是——文件。 没错,文件 的作用就是可以将程序运行过程中所产生的数据结果都存储下来,即便程序结束运行,我们依然可以通过 文件 来找到这些数据,甚至再次运行程序时依然能使用这些数据。

二、究竟什么才是文件

准确的来说:计算机文件就是存储在计算机硬盘、磁盘上的信息集合。例如熟悉的图片文件、视频文件、程序等等。

文件有各种各样的类型,但是在程序设计中,我们一般谈的文件有两种:程序文件数据文件

程序文件

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


数据文件

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

这次我们便围绕数据文件进行讨论说明。

三、文件名

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

文件名包含3部分∶文件路径 + 文件名主干 + 文件后缀
例如︰ c : \\code\\text.txt
文件路径: c : \\code
文件名主干: test
文件后缀: .txt

当然为了方便起见,文件标识常被成为 文件名。文件名主干 的命名规则遵循标识符的命名规则。

但是我们要了解此时所称呼的文件名,实际上包括以上3部分内容,而不仅仅是文件名主干。

这边我来举例一些常见的文件后缀方便大家去了解文件的性质:

txt(文本文件)dat(数据文件)doc(Word生成的文件)xlsx(excel生成的文件)exe(可执行文件)jpg(图片文件)等……

四、文件类型

那么我们如何来区分文件类型呢?

首先根据数据的组织形式,数据文件被称为了 文本文件二进制文件

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

反之,如果在外存上以ASCII码的形式进行存储,如果在存储到外存前就转换,那么就是 文本文件


一个数据如何在内存中存储的?

字符在内存中一律是以ASCII码值形式进行存储的,而数值型数据既可以用ASCII码值形式存储,也可以使用二进制形式进行存储。

 例:

#include <stdio.h>
int main()
{
    int a = 10000;
    FILE *pf = fopen("eg.txt", "wb");
    //fopen 打开名为test.txt的文件 
    //wb 以二进制的形式写入文件
    fwrite(&a, 4, 1, pf);//写一个四个字节的数据放到以pf维护的文件中去
    fclose(pf);
    pf = NULL;
    return 0;
}

我们以二进制编辑器打开我们所创建的 eg.txt 文件中去:

我们可以看出这是以16进制形式显示的,并且由于编译器是按照小端进行存储的。

所以应该为 00 00 27 10

转换成二进制就是 00000000 00000000 00100111 00010000

图解:

五、文件缓冲区

我们来了解一下什么是文件缓冲区:

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

图解:

六、文件指针

缓冲文件系统中, 关键的概念是 “文件类型指针” , 简称 “文件指针” 。每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及文件当前位置等)。这些信息是保存在一个结构体变量中的。
该结构体类型是由系统声明的,取名为 FILE

例:

//vs2013编译环境提供的stdio.h头文件的文件类型申明
struct _iobuf
{
	char* _ptr;
	int  _cnt;
	char* _base;
	int  _f1ag;
	int  _file;
	int _charbuf;
	int _bufsiz;
	char* _tmpfname;
};
typedef struct _iobuf FILE;
  • 当然不同的C编辑器它所对应的FILE类型包含的内容也不完全相同,但从根本上大同小异。
  • 每当打开一个文件时,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用时我们不必在意这些细节。
  • 一般情况下通过FILE指针来维护这个FILE结构的变量,这样使用起来会更加便捷。


例:(创建FILE*的指针变量)

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

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

图解:

七、打开文件和关闭文件

我们都知道,文件在读写之前应该想先打开文件,在使用结束后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个 FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用 fopen 函数来打开文件,fclose 来关闭文件。


fopen

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

函数功能: Open a file.(打开文件)
返回类型: Each of these functions returns a pointer to the open file. A null pointer value indicates an error.(如果打开成功,返回指向文件信息区的指针,如果返回失败,返回空指针NULL)
函数参数1:filename(文件名,实际上包括3部分内容,而不仅仅是文件名主干。如果文件路径未写,则默认本路径)
函数参数2:Type of access permitted(文件打开方式)

文件打开方式表:

文件使用方式含义如果指定文件不存在
"r"(只读)为了输入数据,打开一个已经存在的文本文件出错
"w"(只写)为了输出数据,打开一个文本文件建立一个新文件
"a"(追加)向文本文件尾添加数据出错
"rb"(只读)为了输入数据,打开一个二进制文件出错
"wb"(只写)为了输出数据,打开一个文进制文件建立一个新文件
"ab"(追加)向一个二进制文件尾添加数据出错
"r+"(读写)为了读和写,打开一个文本文件出错
"w+"(读写)问了读和写,建立一个新的文件建立一个新文件

"a+"(读写)

打卡一个文件,在文件尾进行读写建立一个新文件
"rb+"(读写)为了读和写打开一个二进制文件出错
"wb+"(读写)为了读和写,新建一个新的二进制文件建立一个新文件
"ab+"(读写)打开一个二进制文件,在文件尾进行读和写建立一个新文件
注: ‘w’ 打开的时候如果有同名文件,该文件中的内容会被销毁

fclose

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

函数功能:Closes a stream(fclose) or closes all open streams(_fcloseall).(关闭文件)
返回类型:fclose returns 0 if the stream is successfully closed._fcloseall returns the total number of streams closed.Both functions return EOF to indicate an error(关闭文件成功返回0,关闭文件失败返回EOF(值为 - 1)来报错)
函数参数:Pointer to FILE structure(文件指针)


 例:

#include <stdio.h>
int main()
{
	//打开文件mysize.txt
	//相对路径
	//..表示上一级路径
	//fopen("../../mysize.txt", "r");
	//.表示当前路径
	FILE* pf = fopen("mysize.txt", "r");//相对路径
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//打开成功
	//读文件
	fclose(pf);//关闭文件
	return 0;
}

 我们首先新建一个文件:

执行结果: 

 

 我们此时再换成绝对路径:


 刚刚我们是以 “r”(只读)的方式来打开文件,现在我们来尝试用 “w”(只写)的方式来打开文件

#include <stdio.h>
int main()
{

	FILE* pf = fopen("eg.dat", "w");//相对路径
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//打开成功
	//读文件
	printf("file open success.\\n");
	fclose(pf);//关闭文件
	return 0;
}

观察文件夹,发现新建了 eg.dat 这么一个文件 :

八、文件顺序读写表

名称函数名功能(返回值)
字符输入函数fgetc(fp)

从文件指针 fp 所指向的文件的当前读指针位置读取一个字符,读取完成后指针自动后移指向下一个字符

成功时返回该字符,否则返回EOF

字符输出函数fputc(ch,fp)

将字符 ch 写入到文件指针 fp 所指向的文件的当前写指针位置

成功是返回字符本身,否则返回EOF

文本行输入函数fgets(str, n, fp)fp 所指向的文件的当前读指针位置读出 n 个字符放入字符串 str 中成功时返回该字符串地址,否则返回NULL
文本行输出函数fputs(str, fp)

将字符串 str 写入到文件指针 fp 所指向的文件的当前写指针位置

成功时返回非0值,否则返回EOF

格式化输入函数fscanf(fp, 格式控制字符串,地址列表)

格式控制串所描述的格式,从 fp 所指向的文件中读取数据,送到指定的变量中。

若输出操作成功,返回实际写入的字符

若输出操作失败,则返回EOF

格式化输出函数fprintf(fp, 格式控制字符串,输出列表)

输出项按照制定的格式写入 fp 所指向的文件中。

若输入操作成功,返回实际读出的数据

若没有读到数据项,则返回0

若文件结束或调用失败,则返回EOF

二进制输入size_t fread(buffer, size, count, stream)

读取 [count_num] 个对象(每个对象大小为 size(大小) 指定的字节数),并把它们替换到由 buffer (缓冲区) 指定的数组,数据来自给出的输入流

函数的返回值是读取的内容数量

二进制输出size_t fwrite(buffer, size, count, stream)

把 buffer 所指向的数组中的数据写入到给定流 stream 

若成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。

若该数字与 count 参数不同,则会显示一个错误。

单看这个表可能没有什么感觉,接下去就让我们来详细的进行每个函数的功能描述,在此之前需要先介绍一个函数。它是什么呢?


流(stream)

概念:输入输出是数据传送的过程, 数据如流水一样从一处流向另一处, 因此常将输入输出形象地称为流(stream), 即数据流。流表示了信息从源到目的端的流动。在输入操作时, 数据从文件流向计算机内存, 在输出操作时, 数据从计算机流向文件(如打印机、磁盘文件)。文件是由操作系统进行统一管理的, 无论是用Word打开或保存文件, 还是C程序中的输入输出都是通过操作系统进行的。“流”是一个传输通道, 数据可以从运行环境(有关设备)流入程序中, 或从程序流至运行环境。

图解: 

注:C语言程序,只要运行起来,就默认打开了三个流,类型均为 FILE*
stdin --- 标准输入流 --- 键盘
stdout --- 标准输出流 --- 屏幕
stderr --- 标准错误流 --- 屏幕


 fputc

例:

#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
	FILE* pf = fopen("mysize.txt", "w");
	if (pf == NULL)
	{
		printf("%s\\n", strerror(errno));
		return 0;
	}
	//写文件
	//关闭文件
	fputc('H', stdout);//stdout 标准输出流
	fputc('y', stdout);
	fputc('D', stdout);
	fputc('r', stdout);
	fputc('a', stdout);
	fclose(pf);

	return 0;
}

执行结果:


fgetc

例:

#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
	FILE *pf = fopen("mysize.txt","r");
	if (pf == NULL)
	{
		printf("%s\\n", strerror(errno));
		return 0;
	}
	//读文件
	int ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

执行结果: 


fgets

在举例前我们先在 mysize.txt 文本内部加上一行字符,这样能够更好的展现效果:

例:

#include <stdio.h>
int main()
{
	char buf[1024] = { 0 };
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	//读文件
	fgets(buf, 1024, pf);
	printf("%s", buf);//buffer自身拥有换行功能

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

执行结果:


 fputs

例1:

#include <stdio.h>
int main()
{
	char buf[1024] = { 0 };
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	//读文件
	fgets(buf, 1024, pf);
	puts(buf);
	fgets(buf, 1024, pf);
	puts(buf);

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

执行结果:

例2:

#include <stdio.h>
int main()
{	
	//从键盘读取一行文本信息
	char buf[1024] = { 0 };

	fgets(buf, 1024, stdin);//从标准输入流读取
	fputs(buf, stdout);
	//上面那种写法等价于下面这种写法
	gets(buf);
	puts(buf);

	return 0;
}

fprintf

例:

#include <stdio.h>
struct S
{
	int n;
	float f;
	char arr[10];
};
int main()
{
	struct S s = { 520, 13.14f, "HSS" };
	FILE *pf = fopen("mysize.txt", "w");
	if (pf == NULL)
	{
		return 0;
	}
	//格式化的形式写文件
	fprintf(pf, "%d %f %s", s.n, s.f, s.arr);
	fclose(pf);//关闭文件
	pf = NULL;

	return 0;
}

执行结果:

此时我们再去查看 mysize.txt 记事本就会发现里面的数据已经被格式化并且替换成了刚写的数据:


fscanf

例:

#include <stdio.h>
struct S
{
	int n;
	float f;
	char arr[10];
};
int main()
{
	struct S s = { 0 };
	FILE* pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	//格式化的输入数据
	fscanf(pf, "%d %f %s", &(s.n), &(s.f), s.arr);
	printf("%d %f %s\\n", s.n, s.f, s.arr);
	fclose(pf);//关闭文件
	pf = NULL;	
	return 0;
}

执行结果: 


对比一组函数

scanf / fscanf / sscanf
printf / fprintf / sprintf

此时我们还未了解过 sscanfsprintf,所以我们先进行举例说明:

例:

#include <stdio.h>
struct S
{
	int n;
	float f;
	char arr[10];
};
int main()
{
	struct S s = { 520, 13.14f, "HSS" };
	struct S tmp = { 0 };
	char buf[1024] = { 0 };
	//把格式化的数据转换成字符串存储到buffer
	sprintf(buf, "%d %f %s", s.n, s.f, s.arr);
	//从buffer中读取格式化的数据到tmp中
	sscanf(buf, "%d %f %s", &(tmp.n), &(tmp.f), tmp.arr);
	printf("%d %f %s\\n", tmp.n, tmp.f, tmp.arr);
	return 0;
}

执行结果:

 总结:

  • scanf/printf      ---    是针对标准输入流/标准输出流的格式化输入/输出语句
  • fscanf/fprintf    ---    是针对所有输入流/所有输出流的格式化输入/输出语句
  • sscanf/sprintf  ---    sscanf是从字符串中读取格式化的数据,sprintf是把格式化数据输出成(存储到)字符串


fwrite

例:

#include <stdio.h>
struct S
{
	char name[20];
	int age;
	int height;
};
int main()
{
	struct S s = {"HyDra", 18, 185};
	FILE *pf = fopen("mysize.txt", "wb");
	if (pf == NULL)
	{
		return 0;
	}
	//二进制的形式写文件
	fwrite(&s, sizeof(struct S), 1, pf);

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

执行结果:

为什么执行完里面是空白的呢?此时我们再看向 mysize.txt 文件:

 为什么后面的内容就变成乱码了呢?

原因很简单,由于 fwrite 的功能的就是进行二进制的输出来进行存储,所以当我们打开记事本时,由于HyDra是一个字符串,所以我们是可以看到的,但是记事本没有办法转换字符成为二进制,于是后面就成了乱码。


fread

例:

struct S
{
	char name[20];
	int age;
	int height;
};
int main()
{
	struct S tmp = { 0 };
	FILE *pf = fopen("mysize.txt", "rb");
	if (pf == NULL)
	{
		return 0;
	}
	//二进制的形式写文件
	fread(&tmp, sizeof(struct S), 1, pf);
	printf("%s %d %d\\n", tmp.name, tmp.age, tmp.height);
	fclose(pf);
	pf = NULL;
	return 0;
}

执行结果:

九、文件随机读写

一般情况下, 在对字符文件进行顺序读写时, 文件位置标记指向文件开头, 这时如果对文件进行读的操作, 就读第1个字符, 然后文件位置标记向后移一个位置, 在下一次执行读的操作时, 就将位置标记指向的第⒉个字符读入。依此类推, 遇到文件尾结束。

图解:

当然也可以根据读写的需要, 人为地移动文件位置标记的位置。
文件位置标记可以向前移、向后移, 移到文件头或文件尾, 然后对该位置进行读写,
显然这就不是顺序读写了, 而是随机读写。 


   fseek

  • 根据文件指针的位置和偏移量来定位文件指针

//函数原型
int fseek ( FILE * stream, long int offset, int origin );
//fseek(文件类型指针, 偏移量, 起始点)

 起始点的选项有三个:SEEK_CUR 、SEEK_END 、SEEK_SET

起始点名字用数字代表
文件开始位置SEEK_CUR0
文件当前位置SEEK_END1
文件末尾位置SEEK_SET2

例:

先打开mysize.txt编辑一下里面的的数据内容:

代码如下: 

#include <stdio.h>
int main()
{
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	//1.定位文件指针
	fseek(pf, 2, SEEK_CUR);
	//2,读取文件
	int tmp = fgetc(pf);
	printf("%c\\n", tmp);

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

执行结果:

偏移量2,从开头H向右偏移两个到D(原HyDra) 


   ftell

  • 返回文件指针相对于起始位置的偏移量(定位)
//函数原型
long int ftell ( FILE * stream );

例:

#include <stdio.h>
int main()
{
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	//1.定位文件指针
	fseek(pf, -2, SEEK_END);
	//2,读取文件
	int eg = ftell(pf);
	printf("%d\\n", eg);

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

执行结果:

由于是-2,从文件结尾向左偏移2个,指向 r 前面的位置,所以结果是3(原HyDra)


   rewind

  • 让文件指针的位置回到文件的起始位置
//函数原型
void rewind ( FILE * stream );

 例:

#include <stdio.h>
int main()
{
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}

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

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

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

执行结果

由于恢复到了起始位置,所以打印出来仍为 H (原HyDra)

十、文件读取结束的判定

有一个函数特别容易被错误使用:feof

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

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

  • fgetc判断是否为EOF
  • fgets判断返回值是否为NULL

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

  • fread判断返回值是否小于实际要读的个数

例1(文本文件):

这里我们引用的是之前创建的 eg.txt 文件,我们将里面的内容全部清空。

 代码如下:

#include <stdio.h>
int main()
{
	//feof(); EOF - end of file - 文件结束标志
	FILE *pf = fopen("eg.txt", "r");
	if (pf == NULL)
	{
		return 0;
	}
	int size = fgetc(pf);
	printf("%d\\n", size);//-1
	fclose(pf);
	pf = NULL;
	return 0;
}

执行结果:

 由此可见,文件结束的位置确实存在一个 EOF


例2(二进制文件):

#include <stdio.h>
int main()
{
	FILE *pf = fopen("mysize.txt", "r");
	if (pf == NULL)
	{
		perror("open file eg.txt");
		return 0;
	}
	//读文件
	int ag = 0;
	while ((ag = fgetc(pf)) != EOF)
	{
		putchar(ag);
	}
	if (ferror(pf))
	{
		printf("error\\n");
	}
	else if (feof(pf))
	{
		printf(" end of file\\n");
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

执行结果:

以上是关于C语言进阶文件数据操作详解(万字教你真正理解文件使用)的主要内容,如果未能解决你的问题,请参考以下文章

appium详细总结,书写万字教你避开环境配置坑,看看你是否有未踩过的坑

万字教你如何用 Python 实现线性规划

C语言进阶—— 文件操作(详解)

万字教你如何用 Python 实现线性规划

万字教你如何用 Python 实现线性规划

C语言进阶学习笔记六详解文件操作(看完这篇,你的文件操作就入门了!)