C语言-第九章-加餐:文件位置指示器与二进制读写

传送门:C语言-第九章:文件读写

第一节:文件位置指示器

文件位置指示器是下一次操作要对文件进行操作的位置(读或写),可以理解为光标的位置。

进行读操作和写操作时,指示器的初始位置都在文件开头;

进行追加操作时,指示器的位置在文件结尾。

进行读写操作时,指示器的位置也会随之后移,我们也可以用用一些函数改变文件指示器的位置。

1-1.fseek 函数

fseek 函数根据一个起始位置,将指示器偏移到对应位置,函数原型如下:

stream:要改变指示器位置的文件

offset:相对于 origin 位置的偏移量,设置0就表示指示器指向 origin

origin:起始位置,有三种选项:

①SEEK_CUR :指示器当前的位置

②SEEK_END:文件结尾

③SEEK_SET :文件开头

返回值:如果设置成功,它将返回0;否则返回非0值

1-2.ftell 函数

ftell 函数将返回指示器相对于文件开头位置的偏移量,函数原型如下:

stream:返回此文件的指示器偏移量

返回值:成功后。将返回偏移量;否则返回-1

1-3.rewind 函数

让指示器返回到文件开头,函数原型如下:

stream:让stream的指示器指向文件开头

1-4.案例

下面是一个案例:得到一个文件的大小并读取文件中的所有内容。

我们先自定义好一份文件中的内容,随便在网上复制一些内容:

完整代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf = fopen(".\\text.txt", "r"); // 以读方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	// 让指示器指向文件结尾
	fseek(pf,0,SEEK_END);
	// 得到当前位置(文件结尾)相对于文件开头的偏移量
	int size = ftell(pf); // 这个偏移量就是文件内容的总字节大小
	// 将指示器复位(重新指向文件开头),以便之后的读取
	rewind(pf);
	// 开辟一块空间,用于存放读取的文件内容,注意'\0'也要计算大小
	char* content = (char*)malloc(size+1);
	// 读取文件内容
	fgets(content,size+1,pf);

	// 打印content
	printf("%s\n",content);

	return 0;
}

第二节:文件的二进制读写

为什么要有二进制读写?

文件之所以要有二进制读写方式,主要是基于以下几个原因:

  1. 精确控制数据

    二进制读写允许你以字节为单位精确地控制数据的读写。在处理非文本文件(如图片、视频、音频文件等)时,这些文件中的数据是以二进制形式(即0和1的组合)存储的。使用二进制读写可以直接处理这些字节数据,而无需进行任何形式的转换或解释。

  2. 性能优化

    对于大文件或需要高速读写的应用场景,二进制读写通常比文本读写更高效。文本读写可能需要将二进制数据转换为特定的字符编码(如UTF-8),并在读写时进行转换,这会增加额外的处理时间。而二进制读写则直接操作字节,避免了这些额外的开销。

  3. 兼容性和移植性

    在处理跨平台或跨语言的文件时,二进制读写可以保证数据的精确性和一致性。由于文本文件可能受到不同平台字符编码和换行符差异的影响,使用二进制读写可以避免这些问题,确保数据在不同平台和语言之间的一致性和可移植性。

  4. 处理复杂数据结构

    在序列化复杂数据结构(如对象、数组等)到文件时,二进制格式提供了一种更加紧凑和高效的方式来存储这些结构。通过将数据结构转换为二进制表示,并在需要时重新恢复为原始结构,可以实现快速的数据交换和存储。

  5. 直接操作底层数据

    在需要直接操作硬件或系统底层数据的场景中(如驱动程序开发、嵌入式系统开发等),二进制读写是必不可少的。这些场景要求直接读写设备的内存地址或寄存器,而这些数据通常是以二进制形式存在的。

以上内容来自文心一言

二进制操作如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf = fopen(".\\text.txt", "w+b"); // 以二进制读写方式打开文件
	if (pf == NULL)
	{
		perror("文件打开失败");
		return 0;
	}
	// 以二进制形式存储数据
	char put[] = "你好,世界,hello,world";
	fwrite(put, sizeof(put), 1, pf);
	// 指示器复位
	rewind(pf);
	// 读取二进制内容
	char get[sizeof(put)];
	fread(get, sizeof(put), 1, pf);
	// 打印数据
	printf("%s\n", get);
	return 0;
}

文件中的中文变成了一些看不懂的符号,但是读取回来的内容与之前相同。说明vs编译器和txt文件对英文字符的编码格式相同,对中文的编码格式不同。

接下来我们简单介绍一下二进制的读写操作:

2-1.文件的二进制打开

①rb:二进制读

②wb:二进制写

③ab:二进制追加

④r+b:二进制读写,不清空文件内容

⑤w+b:二进制读写,要清空文件内容

⑥a+b:二进制追加+读

2-2.fread 二进制读函数

函数原型如下:

ptr:存放读取到的内容的空间

size:一次读取的字节数(一个元素的大小)

count: 读取次数(元素个数)

stream:被读取的文件

返回值:返回成功读取的元素的个数;读取失败返回值小于count,如果因为文件内容少,读到文件结尾返回值也会小于count,需要用 feof 函数判断文件是不是正常结束的。

feof 函数的用法如下:

cpp 复制代码
int feof(FILE* stream); // 正常结束返回真,否则返回假

2-3.fwrite 二进制写函数

函数原型如下:

ptr:存放将写入的内容的空间

size:一次写入的字节数(一个元素的大小)

count: 写入次数(元素个数)

stream:被写入的文件

返回值:返回成功写入的元素的个数;写入时如果发生错误则返回值小于count。

2-4.图片读写

图片在文件中是以二进制的形式存储的,我们可以用二进制读写来操作图片文件,比如将一份图片进行复制,具体做法如下:

首先在网上随便找一张图片放在代码文件夹下,然后自己创建一个副本,后缀要一致:

此时这个副本里是没有内容的,完整代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	FILE* pf1 = fopen(".\\山水风景.png", "r+b"); // 以二进制读方式打开原图片
	FILE* pf2 = fopen(".\\山水风景副本.png", "w+b"); // 以二进制写方式打开副本图片
	if (pf1 == NULL || pf2 == NULL)
	{
		perror("文件打开失败");
		return 0;
	}

	// 让指示器指向文件结尾
	fseek(pf1, 0, SEEK_END);
	// 得到当前位置(文件结尾)相对于文件开头的偏移量(文件大小)
	int size = ftell(pf1);
	// 开辟一块空间,用于存放读取的文件内容
	char* content = (char*)malloc(size);
    // 返回文件开头
	rewind(pf1);

	// 读取内容
	fread(content, size, 1, pf1);
	// 写入内容
	fwrite(content, size, 1, pf2);
	return 0;
}

执行上述代码,就得到一个副本图片了:

第三节:printf 与 fprintf、scanf 与 fscanf的联系

3-1.printf 与 fprintf

printf 函数实际上是 fprintf 函数的一种特殊情况:printf 函数的本质是向屏幕文件 写入数据,而 fprintf 函数是向**任意文件(包括屏幕文件)**写入数据。

我们知道,一个文件在被操作时先要要用 fopen 打开这个文件,但是 printf 函数似乎不需要打开屏幕文件,就可以向屏幕输出数据。真实情况是屏幕文件在程序执行时是被默认打开的,不需要我们打开。

既然屏幕文件默认是打开的,那么它有没有文件指针指向它呢?答案是肯定的,这个文件指针是一个来自C语言标准库的外部变量 stdout,它包含在<stdio.h>文件中。

我们可以使用 stdout + fprintf 向屏幕打印数据:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	char arr[] = "Hello,world";
	fprintf(stdout,"%s\n",arr); // 与下面条语句等价
	printf("%s\n", arr);
	return 0;
}

屏幕文件的本质:

屏幕文件本质是一块缓冲区,当缓冲区的内容满足一定条件时,缓冲区的内容才会输出到屏幕上,有以下3种条件:

(1)屏幕文件关闭:当程序结束和调用 fclose(stdout) 时,屏幕文件都会关闭,缓冲区的内容都会输出到屏幕上;

(2)缓冲区满:当缓冲区被占满时,里面的内容会被输出到屏幕上,以腾出空间;

(3)出现'\n':当缓冲区的内容中有换行符时,会把换行符之前(包括换行符)的所有数据输出到屏幕。

3-2.scanf 与 fscanf

与上面类似,scanf 是 fscanf 的一种特殊情况:scanf 是从键盘文件 获取数据写到任意变量中;fscanf 是从任意文件(包括键盘文件) 获取数据写到任意变量中。而指向键盘文件的文件指针是 stdin

我们也就可以使用 stdin + fscanf 从键盘获取数据写到变量中:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int a = 0;
	fscanf(stdin,"%d",&a); // 等价于 scanf("%d", &a);
	printf("a=%d\n", a);
	return 0;
}

下期预告:

下一次的内容是综合案例,我们要使用前面所学的知识(特别是文件操作)写一个通讯录,它具有以下功能:

(1)添加联系人

(2)删除联系人

(3)根据名字查询联系人的其他信息

(5)修改已有联系人的信息

(6)程序每次运行都保存上次的所有联系人

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程5 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust