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

Posted 大家好我叫张同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言进阶学习笔记六详解文件操作(看完这篇,你的文件操作就入门了!)相关的知识,希望对你有一定的参考价值。

前言

在之前的学习过程中,我们所编写的程序在执行过程中所产生的数据及结果都只是临时存放在内存区域,一旦程序运行结束,该程序所涉及的内存空间全部返回给操作系统。这时候如果我们想要去查看这些数据和结果,显然是做不到的!那有没有什么方法能够解决这个问题呢?也就是说可以将程序运行过程所产生的过程数据和结果数据都保存起来,即便程序结束,我们也可以找到这些内容,甚至是在之后所写的其他程序中也能继续使用这些内容。
答案当然是-- - 有,这也就是我们这篇文章所要讨论的东西–文件。



为什么需要文件?

这个问题其实刚刚已经说到了,简而言之,我们需要文件是
(1)防止程序结束后,数据就消失了
(2)为了持久化的保存数据


什么是文件?

计算机中所说的文件是以计算机硬盘为载体,存储在计算机上的信息集合。例如我们熟悉的文本文件,图片文件,视频文件,程序等等。
文件有各种各样的类型,但是在程序设计中,我们一般谈的文件有两种∶程序文件、数据文件
(1)程序文件
包括源程序文件(后缀为.c ), 目标文件(windows环境后缀为.obj), 可执行程序(windows环境后缀为.exe)。
(2)数据文件
文件的内容不是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。


文件名

如果我们要区分不同的人,很多时候需要通过他 / 她的名字来进行区分;如果要区分不同的物品,也要通过不同的物品名称来区分一样。所以为了区分不同的文件,也需要给不同的文件不同的名称一样。
一个文件要有一个唯一的文件标识,以便用户识别和引用。

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

为了方便起见,文件标识常被称为文件名。但是我们要了解此时所称呼的文件名,实际上包括以上3部分内容,而不仅仅是文件名主干。

1) 文件名主干的命名规则遵循标识符的命名规则。
2)后缀用来表示文件的性质, 如:doc(Word生成的文件), txt(文本文件),dat(数据文件), c(C语言源程序文件)'cpp(C++源程序文件), for(FORTRAN语言源程序文件),pas(Pascal语言源程序文件),obj(目标文件), exe(可执行文件).ppt(电子幻灯文件), bmp(图形文件)等。
3)文件路径:分为相对路径和绝对路径
绝对路径:c : \\code\\test.txt
相对路径:test.txt


文件缓冲区

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(文件打开方式)

文件打开方式如下表:

注意:‘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(文件指针)

举例:
1)在项目工程所在文件路径下新建一个测试文件

2)测试代码

#include<stdio.h>
int main()
{
	//打开文件
	FILE* pf1 = fopen("test1.txt", "r");
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	//......
	//关闭文件
	fclose(pf1);
	return 0;
}

3)文件打开结果

调整一下(为了进一步理解相对路径和绝对路径,这里的filename使用绝对路径的方式)

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

#include<stdio.h>
int main()
{
	FILE* pf = fopen("file1.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	//... 
	printf("open file success!\\n");
	//关闭文件
	fclose(pf);
}

在我们刚刚的路径下是没有file1.dat这个文件的,因为我们用“w”的方式打开文件,所以会创建一个新的文件


现在我们来尝试将一些内容写入到打开的文件中:



流(stream)的概念

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

C语言把文件看作一个字符(或字节)的序列, 即由一个一个字符(或字节)的数据顺序组成。一个输入输出流就是一个字符流或字节(内容为二进制数据)流。
C的数据文件由一连串的字符(或字节)组成, 而不考虑行的界限, 两行数据间不会自动加分隔符,对文件的存取是以字符(字节)为单位的。输入输出数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制, 这就增加了处理的灵活性。
这种文件称为流式文件。

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

例如:

接下来我们进行读文件,将前面生成的文件file1.dat读出来

刚刚这种是从文件里面读,我们也可以从标准输入流stdin–键盘读

fgetc如果读取文件失败返回EOF(值为 - 1)

上面说的fputc\\fgetc都是一个个字符方式读取的,如果要用这两个函数来处理字符串的话,效率就会非常低,那么用没用类似于这两个函数,但是可以一行一行读取的函数呢?


这两个函数就是用来实现刚刚说的功能的。
举例

#include<stdio.h>
int main()
{
	FILE* pf = fopen("file2.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 一行一行写
	fputs("Hello world", pf);
	//关闭文件
	fclose(pf);
	return 0;
}


如果想要写入的字符串换行,则需要在字符串内容中添加换行‘\\n’

读文件,读的时候会将‘\\’0放进去


前面进行的是字符的输入输出, 而实际上数据的类型是丰富的。大家已很熟悉用printf 函数和scanf 函数向终端进行格式化的输入输出, 即用各种不同的格式以终端为对象输入输出数据。其实也可以对文件进行格式化输入输出, 这时就要用fprintf函数和 fscanf函数, 从函数名可以看到, 它们只是在printf和 scanf的前面加了一个字母f。它们的作用与printf 函数和scanf函数相仿, 都是格式化读写函数。只有一点不同: fprintf 和 fscanf 函数的读写对象不是终端而是文件。
它们的一般调用方式为

fprintf(文件指针, 格式字符串, 输出表列);
fscanf(文件指针, 格式字符串, 输入表列);

例如:

fprintf(fp, "%d,%6.2f", i, f);

举例:假如我们要将一个结构体类型的数据写入到我们的文件中(一般输入数据也叫做读取数据,输出数据也叫做写入数据)

#include<stdio.h>
struct S
{
	char ch[10];
	int a;
	float f;
};
int main()
{
	struct S s = { "student",10,3.333333 };
	//打开文件
	FILE* pf = fopen("file3.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写数据
	fprintf(pf, "%s %d %f", s.ch, s.a, s.f);
	//关闭文件
	fclose(pf);
	return 0;
}


现在我们将输出的结构体文件重新读入到结构体变量中:

#include<stdio.h>
struct S
{
	char ch[10];
	int a;
	float f;
};
int main()
{
	struct S s = { 0 };
	//打开文件
	FILE* pf = fopen("file3.dat", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读数据
	fscanf(pf, "%s %d %f", s.ch, &(s.a), &(s.f));

	//打印数据
	printf("%s %d %f\\n", s.ch, s.a, s.f);
	//关闭文件
	fclose(pf);
	return 0;
}

当然我们也可以用fprintf,stdout是标准输出流,也就是屏幕

上面我们说谈到的输入和输出实际上都是以文本数据的形式,也就是ASCII码形式。

那么如何以二进制的形式读写呢?

在程序中不仅需要一次输入输出一个数据, 而且常常需要一次输入输出一组数据(如数组或结构体变量的值),C语言允许用fread函数从文件中读一个数据块, 用fwrite函数向文件写一个数据块。在读写时是以二进制形式进行的。在向磁盘写数据时, 直接将内存中一组数据原封不动、不加转换地复制到磁盘文件上, 在读入时也是将磁盘文件中若干字节的内容一批读入内存。

函数原型
fread(buffer,size, count, fp);
fwrite(buffer, size, count, fp);

1)buffer:是一个地址。 对fread来说, 它是用来存放从文件读入的数据的存储区的地址。 对fwrite来说,是要把此地址开始的存储区中的数据向文件输出(以上指的是起始地址)。
2)size : 要读写的字节数。
3)count : 要读写多少个数据项(每个数据项长度为size)。
4) fp : FILE类型指针。 在打开文件时指定用二进制文件, 这样就可以用fread和 fwrite函数读写任何类型的信息。

例如 :

fread(f, 4, 10, fp);

其中, f是一个float型数组名(代表数组首元素地址)。这个函数从fp所指向的文件读入10个4个字节的数据, 存储到数组f中。
举例:

#include<stdio.h>
struct S
{
	char ch[10];
	int a;
	float f;
};
int main()
{
	struct S s = { "student",10,3.333333 };
	//打开文件
	FILE* pf = fopen("file3.dat", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写数据
	fwrite(&s, sizeof(struct S), 1, pf);
	//关闭文件
	fclose(pf);
	return 0;
}

因为是以二进制形式写的,所以我们用文本编辑器去打开,实际上得到的是一个乱码,是无法直接看懂的。
我们可以将其放到vs中,用二进制编辑器去看,就可以得到二进制相关信息,

具体步骤如下:
(1)鼠标右键源文件,添加现有项,将file3.dat添加进来

(2)鼠标右键点击file3.dat文件,选择“打开方式”

(3)下拉到最底部,选择“二进制编辑器”

(4)打开后的效果如下:

除了这种方法外,我们还可以通过fread函数去看fwrite函数写的内容,因为两个都是以二进制数据为操作对象,所以用fwrite写的内容,fread肯定是看得懂的,比方说刚刚的例子,我们还可以采用这种方式:

#include<stdio.h>
struct S
{
	char ch[10];
	int a;
	float f;
};
int main()
{
	struct S s = { 0 };
	//打开文件
	FILE* pf = fopen("file3.dat", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读数据
	fread(&s, sizeof(struct S), 1, pf);
	//打印数据
	printf("%s %d %f\\n", s.ch, s.a, s.f);
	//关闭文件
	fclose(pf);
	return 0;
}


对比一组函数:

scanf / fscanf / sscanf
printf / fprintf / sprintf

scanf与printf是一组-- - 从标准输入流(stdin) / 输出流(stdout)中输入 / 输出格式化的数据
fscanf与fprintf一组-- - 从所有流类型(包括标准输入流\\输出流,文件流等各种流)输入 / 输出格式化的数据
sscanf与sprintf一组-- - 从一个字符串中输入 / 输出格式化数据

函数原型
int sscanf(const char*
buffer, const char* format[, argument] …); int sprintf(char* buffer,
const char* format[, argument] …);

使用举例:

#include<stdio.h>
struct S
{
	char ch[15];
	int age;
	float f;
};
int main()
{
	//将一个结构体的数据转换成一个字符串
	struct S s = { "Hello_world",20,5.5 };
	struct S temp = { 0 };
	char buffer[100] = { 0 };
	sprintf(buffer, "%s %d %f", s.ch, s.age, s.f);
	printf("%s\\n", buffer);
	//将buffer的字符串内容还原成一个结构体
	sscanf(buffer, "%s %d %f", temp.ch, &(temp.age), &(temp.f));
	printf("%s %d %f\\n", temp.ch, temp.age, temp.f);

	return 0;
}


文件随机读取

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

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

fseek函数

根据文件指针的位置和偏移量来定位文件指针。
函数原型:int fseek(FILE * stream,long int offset,int origin);
fseek(文件类型指针, 偏移量, 起始点)

起始点的选项有三个

举例:
(1)在源文件所在路径下创建test.txt,输入一串字符

(2)编写相应的打开文件及读文件的程序

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

(3)运行结果

现在我们运用fseek函数对当前文件指针位置进行偏移,比如:

本来pf读了a之后,应该移动到b的位置,但是用fseek让其偏移 - 1,也就是向左偏移一个位置(一个字节),又回到了a。

当起始点为SEEK_END 的时候,只能向左(向前)偏移,也就是偏移量只能为负值
当起始点为SEEK_SET 的时候,只能向右(向后)偏移,也就是偏移量只能为正值


ftell函数

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


rewind

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

汇总相关代码:

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

	int ch = fgetc(pf);
	printf("%c\\n", ch);
	//在当前位置进行偏移
	fseek(pf, -1, SEEK_CUR);
	ch = fgetc(pf);
	printf("%c\\n", ch);
	ch = fgetc(pf);
	printf("%c\\n", ch);

	//使用ftell来确定当前位置与起始点之间的偏移量
	int ret = ftell(pf);
	printf("%d\\n", ret);

	//让文件指针回到起始位置
	rewind(pf);
	ch = fgetc(pf);
	printf("%c\\n", ch);

	//关闭文件
	fclose(pf);
	return 0;
}

文件类型

根据数据的组织形式,数据文件可分为文本文件和二进制文件。
数据在内存中是以二进制形式存储的, 如果不加转换地输出到外存,就是二进制文件,
可以认为它就是存储在内存的数据的映像,所以也称之为映像文件(image file)。
如果要求在外存上以ASCII代码形式存储,
则需要在存储

以上是关于C语言进阶学习笔记六详解文件操作(看完这篇,你的文件操作就入门了!)的主要内容,如果未能解决你的问题,请参考以下文章

看完这篇你还能不懂C语言/C++内存管理?

C语言进阶学习笔记三字符串函数详解(爆肝吐血整理,建议收藏!!!)

关于 Docker 镜像的操作,看完这篇就够啦 !(下)

C语言进阶学习笔记三字符串函数+内存函数详解

Java进阶之路:看完这篇Kubernetes的深入分析后,我完全掌握了这门技术!

别再说自己不会了!知乎超赞回答:Java如何进阶?看完这篇彻底明白了