0基础C语言积跬步之文件操作

目录

一、为什么使用文件?

二、什么是文件?

2.1程序文件

2.2数据文件

2.3文件名

三、文本文件和二进制文件

3.1文本文件

3.2二进制文件

3.3两种文件的比较

二进制文件形式:

文本文件形式:

四、文件的打开和关闭

4.1流和标准流

4.1.1什么是流?

4.2.2标准流

4.2文件指针

4.3文件的打开和关闭

五、文件的顺序读写

[5.1 顺序读写函数汇总表](#5.1 顺序读写函数汇总表)

[5.2 顺序读写函数详解](#5.2 顺序读写函数详解)

[5.2.1 fgetc 字符输入(读)](#5.2.1 fgetc 字符输入(读))

[5.2.2 fputc 字符输出(写)](#5.2.2 fputc 字符输出(写))

[5.2.3 fgets 按行读取字符串](#5.2.3 fgets 按行读取字符串)

[5.2.4 fputs 写入字符串](#5.2.4 fputs 写入字符串)

[5.2.5 fscanf 格式化读取](#5.2.5 fscanf 格式化读取)

示例用法:

1.从文件里读数据

2.从屏幕上拿数据

[5.2.6 fprintf 格式化写入](#5.2.6 fprintf 格式化写入)

[5.2.7 fread 二进制数据读取](#5.2.7 fread 二进制数据读取)

[5.2.8 fwrite 二进制数据写入](#5.2.8 fwrite 二进制数据写入)

六、一组函数的对比

[6.1 scanf/fscanf/sscanf](#6.1 scanf/fscanf/sscanf)

[6.1.1 sscanf 函数](#6.1.1 sscanf 函数)

6.2printf/fprintf/sprintf

[6.2.1 sprintf 函数](#6.2.1 sprintf 函数)

七、文件的随机读写

[7.1 fseek 函数](#7.1 fseek 函数)

[7.2 ftell 函数](#7.2 ftell 函数)

[7.3 rewind 函数](#7.3 rewind 函数)

八、文件读取结束的判定

[8.1 feof 函数](#8.1 feof 函数)

[8.2 ferror 函数](#8.2 ferror 函数)

8.3综合使用案例

使用示例1:(文本文件)

使用示例2:(二进制文件)

九、文件缓冲区


一、为什么使用文件?

程序运行时产生的数据默认存放在内存 中,内存属于临时存储空间:一旦程序结束运行、电脑断电,内存会被系统回收,所有数据会直接丢失,下次重新运行程序无法读取之前的数据。 而文件可以把数据存储在硬盘、U 盘等外部持久化存储设备里,即使程序关闭、设备断电,数据也不会消失,实现数据持久化保存,需要时可以再次读取使用,这就是使用文件的核心原因

二、什么是文件?

存放在硬盘等磁盘设备上的数据集合就是文件。从程序设计的功能角度划分,文件分为程序文件数据文件两类。

2.1程序文件

程序文件包括:源程序文件(.c为后缀);目标文件(Windows下.obj为后缀,Linux下.o为后缀);可执行程序文件(Windows下后缀为.exe,Linux 下没有强制后缀,一般默认生成a.out,依靠文件执行权限来判断是否为可执行文件)

2.2数据文件

文件内容不一定是程序代码,也可能是程序运行过程中读写处理的数据。 可以是程序读取的输入数据,也可以是程序运行后输出保存的结果数据。以往的数据交互是键盘输入、显示器输出,而数据文件是在磁盘中读写数据,实现数据持久化。

2.3文件名

文件标识就是文件名,方便起见,我们将文件标识简称为文件名,文件名一般指文件名主干+文件后缀

完整文件标识 = 文件路径 + 文件名主干(主文件名) + 文件后缀(扩展名)

例如:c:\code\test.txt

c:\code\是文件路径 test是文件名主干 .txt是文件后缀

文件路径c:\code\ 作用:指明文件在磁盘上的存放位置,用来定位文件所在文件夹。

文件名主干(主文件名)test 作用:用户自定义的文件名称,用来区分同文件夹下不同文件,是文件的核心标识。

文件后缀(扩展名).txt 作用:用来标识文件类型,操作系统根据后缀选择打开该文件的程序;.txt代表纯文本文件。

三、文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

3.1文本文件

内存中的数据先转换成 ASCII 字符,再存储到外存,文件里保存的是每个字符的 ASCII 编码

  • 存储规则:所有内容都当作字符处理,数字、字母、符号都对应一个 ASCII 码;
  • 打开方式:可以用记事本、VS 等文本编辑器直接看懂内容;
  • 特点:读写时需要做二进制 ↔ ASCII的编码转换,会消耗少量时间;占用空间通常更大。

3.2二进制文件

数据在内存中以二进制形式存储,如果不做任何编码转换,直接原样复制保存到外存的文件中,就是二进制文件

  • 存储规则:完全沿用数据在内存中的二进制存储格式;
  • 打开方式:用记事本打开会显示乱码,必须用对应专业软件解析;
  • 特点:无编码转换,读写速度更快;相同数值数据占用存储空间更小。

3.3两种文件的比较

例如我们要存储数据10000,如果用文本文件的形式存,那么我们将10000的每一位都当做一个字符,转化为对应的ASCII码值,一个字符占一个字节,就需要5个字节;

但如果我们用二进制文件的形式存10000这个int类型的数据,不需要进行编码转换,直接把内存中 int 的 4 字节二进制原样写入磁盘,我们就只需要4个字节就可以了,比文本文件占用的空间小一个字节

文本文件可用记事本直接打开看到10000;二进制文件用记事本打开会显示乱码,必须用程序按照 int 类型读取才能还原数据

下面我们来看看真实的在磁盘中是如何储存10000这个数据的:

二进制文件形式:

cpp 复制代码
#include <stdio.h>
int main()
{
    int a = 10000;
    // 以二进制只写模式打开文件 test.txt
    FILE* pf = fopen("test.txt", "wb");
    // 将变量a的4字节二进制数据写入文件
    fwrite(&a, 4, 1, pf);
    // 关闭文件
    fclose(pf);
    // 置空文件指针,防止野指针
    pf = NULL;
    return 0;
}

运行截图:

使用二进制编辑器以二进制文本文件方式打开test.txt

前面的00000000是地址偏移量,表示这个数据的写是从第一个字节开始写的

使用记事本打开:

10000转化为16进制是0x00002710,当我们以二进制形式将数据a写入文件时,可以看到,存储的确实是这四个字节的数据,当我们直接用记事本打开,出现的字符是我们看不懂的乱码

文本文件形式:

cpp 复制代码
#include <stdio.h>
int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "w");
    fprintf(pf, "%d", a);
    fclose(pf);
    pf = NULL;
    return 0;
}

运行截图:

使用文本编辑器以文本文件方式打开test.txt

使用记事本打开:

以文本文件方式读写整型数据时,程序会将数值的每一位数字单独当作字符处理,把每一个字符转换为对应的 ASCII 编码后再存入文件。使用记事本、VS 等文本编辑器打开文件时,编辑器会按照 ASCII 规则解析字节数据,直接还原出对应的字符,因此我们可以直观看懂文件中的内容

四、文件的打开和关闭

4.1流和标准流

4.1.1什么是流?

在 C 语言中,程序和外部设备(磁盘文件、键盘、显示器、打印机等)之间进行数据传输的通道就叫做。 可以把流理解成一条数据传输的管道:程序要读写外部数据时,数据会沿着这条管道流入 / 流出程序。 C 语言把所有输入、输出设备都统一抽象成「流」来操作,不管是文件、键盘还是屏幕,都可以用同一套文件操作函数处理,屏蔽了不同硬件的差异,不然程序员需要学习不同硬件专属的底层通信规则,开发过程会变得十分繁琐复杂

4.2.2标准流

C语言在程序运行时,会默认打开三个流

  • 标准输入流 stdin

    • 对应设备:键盘
    • 作用:用来读取键盘输入的数据,scanf默认从stdin读取数据。
  • 标准输出流 stdout

    • 对应设备:显示器(屏幕)
    • 作用:用来向屏幕打印输出数据,printf默认向stdout输出内容。
  • 标准错误流 stderr

    • 对应设备:显示器(屏幕)
    • 作用:专门用来输出程序错误、警告信息,保证错误信息可以及时在屏幕显示,不受缓冲区刷新规则影响,方便排查程序崩溃原因

stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为文件指针。
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。

4.2文件指针

1.程序每打开一个文件,操作系统会在内存中自动开辟一块文件信息区,用来存储文件名称、文件读写状态、当前读写位置、缓冲区地址、缓冲区大小等文件相关数据。

2.这些信息统一存放在系统预定义的结构体中,这个结构体类型名叫 FILE,定义在 <stdio.h> 头文件里。

3.我们定义一个 FILE* 类型的指针(文件指针 ),指向该FILE结构体变量,后续所有文件读写、关闭操作都通过这个指针完成。

比如VS2013 中 FILE 的底层定义:

cpp 复制代码
// 底层结构体
struct _iobuf {
	char* _ptr;     // 文件缓冲区当前位置指针
	int _cnt;       // 缓冲区剩余有效字节数
	char* _base;    // 文件缓冲区起始地址
	int _flag;      // 文件状态标志
	int _file;      // 文件描述符
	int _charbuf;   // 单字节缓冲标记
	int _bufsiz;    // 缓冲区大小
	char* _tmpfname;// 临时文件名
};
// 类型重命名:把struct _iobuf 别名成 FILE
typedef struct _iobuf FILE;

我们可以通过创建 FILE* 类型的指针,来找到我们相应文件对应的文件信息区,也就是这块结构体,进而用fopen、fclose等库函数进行打开、读写、关闭等操作

4.3文件的打开和关闭

文件在 读写 之前应该先 打开文件 ,在使用 结束 之后应该 关闭文件

调用fopen打开文件:操作系统会在内存自动创建 FILE 结构体、填充文件状态、缓冲区、读写位置等全部信息,然后返回该结构体地址赋值给pf;后续fgetcfputcfreadfwritefclose等文件操作函数,都依靠传入的pf找到这个 FILE 结构体,依靠结构体里的各项信息完成文件操作

打开文件成功

返回:动态创建的 FILE 结构体的首地址 ,可以用 FILE* 类型指针接收。

打开文件失败

返回:空指针 NULL

fclose(pf)关闭文件:释放 FILE 结构体占用的内存、刷新缓冲区、断开文件指针和磁盘文件的关联,操作后需要手动将pf=NULL防止野指针

关闭文件成功

返回:整数 0

关闭文件失败

返回:EOF(宏常量,值为 -1)

fopen 函数的原型:

cpp 复制代码
//打开文件
FILE* fopen(const char* filename, const char* mode);

参数说明:

  • const char * filename:文件名字符串,可以是相对路径"test.txt")或绝对路径 ("D:\\C_language\\6.18博客C语言文件操作\\test.txt");用const修饰代表文件名不可被函数修改。

上述test.txt在Windows 系统里真实的文件路径应该是(D:\C_language\6.18博客C语言文件操作\test.txt),但因为在C 语言字符串里,单个反斜杠 \ 是转义起始字符 ,不会被当成路径分隔符,编译器会把 \ 紧跟的字符组合解析成转义字符,导致路径错乱失效,所以我们使用 双斜杠 \\,才能表示一个普通的路径分隔符 \

当然我们也可以用正斜杆 /,不需要转义:("D:/C_language/6.18博客C语言文件操作/test.txt")

  • const char * mode mode 是指向字符串的常量字符指针,用来指定文件的打开方式 ,决定文件是读、写、追加,以及以文本模式 还是二进制模式打开。

文本文件打开模式(不带 b)

文件使用方式 含义 文件不存在 文件已存在
"r"(只读) 为读取数据,打开一个已存在的文本文件 打开失败,报错返回NULL 从文件起始位置读取原有内容,不可写入修改
"w"(只写) 为写入数据,创建 / 打开文本文件 自动新建空白文本文件 直接清空文件原有全部内容,从开头写入新数据
"a"(追加) 在文本文件末尾追加写入数据 自动新建空白文本文件 保留原有所有内容,仅能在文件末尾写入,不能修改前面内容
"r+"(读写) 可读可写,打开已存在的文本文件 打开失败,报错返回NULL 从文件开头位置读写,写入会覆盖原有字符,不会自动清空文件
"w+"(读写) 可读可写,新建文本文件 自动新建空白文本文件 先清空原有全部内容,既可以写入数据,也可以读取数据
"a+"(追加读写) 文件末尾追加写入,同时支持读取全部内容 自动新建空白文本文件 保留原文件内容;写入只能在文件末尾,读取可从头读到尾

二进制文件打开模式(带 b)

文件使用方式 含义 文件不存在 文件已存在
"rb"(只读) 读取二进制文件数据 打开失败,报错返回NULL 从文件起始位置读取二进制数据,不可写入
"wb"(只写) 写入二进制文件数据 自动新建空白二进制文件 清空原文件全部内容,从头写入二进制数据
"ab"(追加) 在二进制文件末尾追加数据 自动新建空白二进制文件 保留原内容,仅可在文件末尾追加写入
"rb+"(读写) 可读可写打开二进制文件 打开失败,报错返回NULL 从文件开头读写,写入会覆盖原有数据,不清空文件
"wb+"(读写) 可读可写新建二进制文件 自动新建空白二进制文件 清空原有内容,支持二进制数据的读写操作
"ab+"(读写) 二进制文件末尾追加写入 + 全局可读 自动新建空白二进制文件 保留原内容,写入只能在末尾,读取不受位置限制

实例代码:

cpp 复制代码
#include <stdio.h>
int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	//必须判断是否打开失败,失败则直接return 1结束程序
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//打开成功,操作文件
	//....
	//操作完成,关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

五、文件的顺序读写

5.1 顺序读写函数汇总表

函数名 功能 适用范围
fgetc 从流中读取单个字符 所有输入流(文件流、标准输入流stdin
fputc 向流中写入单个字符 所有输出流(文件流、标准输出流stdout
fgets 从流中读取一行字符串 所有输入流
fputs 向流中写入一行字符串 所有输出流
fscanf 从流中格式化读取数据 所有输入流
fprintf 向流中格式化写入数据 所有输出流
fread 二进制方式批量读取数据 仅文件输入流(二进制文件)
fwrite 二进制方式批量写入数据 仅文件输出流(二进制文件)

5.2 顺序读写函数详解

5.2.1 fgetc 字符输入(读)

fgetc 函数的原型:

cpp 复制代码
int fgetc(FILE* stream);

用法:

  1. 从指定输入流读取一个字符,返回该字符的 ASCII 值
  2. 读到文件末尾 / 读取失败,返回 EOF(值为 - 1)
  3. 适用:文件流、stdin(键盘)
cpp 复制代码
int ch = fgetc(pf);   // 从文件读取一个字符
int ch = fgetc(stdin);// 等价 getchar(),从键盘读取单个字符

示例用法:

首先我提前在test.txt中写入了abcdef六个字符,然后要将这六个字符读出来并打印在屏幕上

cpp 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int c = 0;
    //使用fgetc加循环拿出文件里存的所有字符
	while ((c=fgetc(pf) )!= EOF)
	{
		printf("%c ", c);

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

运行截图:

5.2.2 fputc 字符输出(写)

fputc 函数的原型:

cpp 复制代码
int fputc(int ch, FILE* stream);

用法:

  1. 从指定输入流读取一个字符,返回该字符的 ASCII 值
  2. 读到文件末尾、读取失败时返回 EOF(值为 - 1)
  3. 适用范围:文件流、标准输入流 stdin(键盘)
cpp 复制代码
fputc('a', pf);        // 将字符a写入文件
fputc('a', stdout);    // 等价 putchar('a'),在屏幕打印字符a

示例用法:

将字符'a'到'z'都写进文件里

cpp 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen:");
		return 1;
	}
	for (char ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

运行截图:成功写入

5.2.3 fgets 按行读取字符串

fgets 函数的原型:

cpp 复制代码
char* fgets(char* str, int num, FILE* stream);

用法:

  1. 从流中最多读取 num-1 个字符存入字符数组 str,数组末尾自动补充 \0
  2. 读取过程遇到换行符 \n 就停止读取,换行符 \n 会一并存入字符数组
  3. 读取成功返回传入的数组首地址 str;读到文件末尾或读取失败返回 NULL

示例用法:

首先我提前在test.txt中写入了abcdefghijklmn十四个字符,接着我要读取其中前九个字符放入字符串数组ch中并打印出这个数组

cpp 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen:");
		return 1;
	}
	char ch[10] = { 0 };
	fgets(ch, 10, pf);
	printf("%s\n", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

运行截图:

5.2.4 fputs 写入字符串

fputs 函数的原型:

cpp 复制代码
int fputs(const char* str, FILE* stream);

用法:

  1. 将字符串 str 整体写入指定输出流,不会自动在末尾追加换行符 \n
  2. 写入成功返回非负整数,写入失败返回 EOF
  3. fputs 只把字符串中可见的有效字符 写入文件,不会把字符串末尾用来标记字符串结束的隐藏 \0 写入磁盘文件
cpp 复制代码
fputs("hello world", pf);      // 将字符串写入文件
fputs("hello world", stdout); // 在屏幕输出字符串

示例用法:

cpp 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcde\n", pf);
	fputs("cdeee\n", pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

当我们连续写入两段字符串给文件时,文件中不会自动换行,需要我们主动写\n进行换行

运行截图:

5.2.5 fscanf 格式化读取

fscanf 函数的原型:

cpp 复制代码
int fscanf(FILE* stream, const char* format, 变量地址);
  • 第一个参数FILE* stream:指定从哪个输入流读取数据
  • 后面参数用法和 scanf 完全一样:格式字符串 + 变量地址 (比如fscanf (pf, "% d", &a);)
示例用法:
1.从文件里读数据

首先我在test.txt文件中写入了100,然后我使用fscanf函数给它读取到变量a中并打印

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    int a = 0;
    // 从文件里读取一个十进制整数存到a
    fscanf(pf, "%d", &a);
    printf("从文件读到的整数:%d\n", a); // 输出:100

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

运行截图:

2.从屏幕上拿数据

fscanf (stdin, "%d", &a) 等价于 scanf ("%d", &a)

cpp 复制代码
#include <stdio.h>
int main()
{
    int a = 0;
    printf("请输入一个整数:");
    // 等价 scanf("%d", &a);
    fscanf(stdin, "%d", &a);
    printf("你输入的数字:%d", a);
    return 0;
}

运行截图:

5.2.6 fprintf 格式化写入

fprintf 函数的原型:

cpp 复制代码
int fprintf(FILE* stream, const char* format, 写入的变量);

用法:

  1. 按照指定格式,将数据写入指定输出流;可以写入到文件pf,也可以写入到屏幕上,stdout
  2. 用法和 printf 完全一致,仅第一个参数多了文件流指针

示例用法:

cpp 复制代码
int a = 10;
fprintf(pf, "a=%d", a);       // 格式化内容写入文件
fprintf(stdout, "a=%d", a);   // 等价 printf("a=%d", a),格式化输出到屏幕上

5.2.7 fread 二进制数据读取

fread 函数的原型:

cpp 复制代码
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);

不会自动转换格式,直接按二进制字节原样读取

参数说明:

  • ptr:缓冲区首地址,用来存放从文件读到的数据,可以是数组、结构体地址;
  • size:单个数据占用的字节大小
  • count:需要读取的数据元素个数
  • stream:文件指针,文件必须以二进制读模式rb打开

返回值:

返回实际成功读取到的数据元素个数

调用一次fread函数,会尝试从文件中读取count个大小为size的数据块,函数返回本次实际成功读取到的数据块个数。若返回值等于count,代表本次成功读取到预期数量的数据;若返回值小于count,说明已读到文件末尾或读取发生错误,本次读取结束。想要持续读取文件全部内容,需要我们在外层手动写循环反复调用fread,判断返回值是否小于count来终止循环

比如:

cpp 复制代码
size_t ret;
char buf[100];
while((ret = fread(buf,1,100,fp)) == 100)
{
    // 处理读取到的ret个数据
}

示例用法:(常用于结构体,以结构体为例)

cpp 复制代码
#include <stdio.h>

struct Student
{
    char name[20];
    int age;
};

int main()
{
     //写入结构体
    struct Student s1 = { "张三", 18 };
    FILE* pf = fopen("stu.bin", "wb");
    fwrite(&s1, sizeof(struct Student), 1, pf);
    fclose(pf);

     //fread读取结构体
    struct Student s2;
    pf = fopen("stu.bin", "rb");
    size_t ret = fread(&s2, sizeof(struct Student), 1, pf);

    if (ret == 1)
    {
        printf("姓名:%s,年龄:%d\n", s2.name, s2.age);
    }

    fclose(pf);
    return 0;
}

运行截图:

5.2.8 fwrite 二进制数据写入

fwrite 函数的原型:

cpp 复制代码
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);

参数说明:

  • ptr:要写入文件的数据在内存中的起始地址;
  • size:单个数据的字节大小;
  • count:要写入多少个该类型数据;
  • 文件必须以二进制只写模式wb打开;
  • 直接把内存里的二进制字节原样写入文件,不转成文本;
  • 返回实际成功写入的数据元素个数。

使用示例:

cpp 复制代码
int arr[5] = {1,2,3,4,5};
fwrite(arr, sizeof(int), 5, pf);

六、一组函数的对比

6.1 scanf/fscanf/sscanf

  • scanf--从标准输入流中格式化读取数据放入变量中
  • fscanf--从指定的输入流中格式化读取数据放入变量中
  • sscanf--从字符串中格式化读取数据放入变量中

6.1.1 sscanf 函数

sscanf 函数原型:

cpp 复制代码
int sscanf(const char* buffer, const char* format, ...);

和scanf函数的用法基本一致,就是多了个字符串的地址,表示格式化读取字符串的内容并写进后面的变量中,下面我们来看一个使用案例:

cpp 复制代码
#include <stdio.h>
struct Stu
{
	char name[20];
	int age;
};
int main()
{
	char buf[20] = "kkeeper 21";
	struct Stu s1 = { 0 };
	sscanf(buf, "%s %d", s1.name,&(s1.age));
	printf("%s %d", s1.name, s1.age);
	return 0;
}

利用了 sscanf 函数将字符串数组buf中的数据读取到结构体 s1.name 和 &(s1.age) 中,因为s1.age是整型不是地址,所以要取地址,接着我们打印,看是否读取成功

运行截图:

6.2printf/fprintf/sprintf

  • printf--将数据格式化输出到标准输出流中
  • fprintf--将数据格式化输出到指定的输出流中
  • sprintf--将数据格式化输出到字符串中

6.2.1 sprintf 函数

sprintf 函数原型:

cpp 复制代码
int sprintf(char* buffer, const char* format, ...);

sprintf 函数的用法和 printf 函数类似,就是参数多一个字符串的地址,表示把后面的数据格式化输出到这个字符串当中,相当于将数据拷贝打印进了这个buffer字符串中,而不是打印到屏幕上,下面我们来看一个使用案例:

cpp 复制代码
#include <stdio.h>
struct Stu
{
	char name[20];
	int age;
};
int main()
{
	char buf[20] = { 0 };
	struct Stu s1 = { "kkeeper",21};
	sprintf(buf, "%s %d", s1.name,s1.age);
	printf("%s",buf);
	return 0;
}

将结构体struct Stu s1当中的数据通过sprintf 函数放入到字符串数组buf中,并打印出来

运行截图:

七、文件的随机读写

7.1 fseek 函数

fseek 函数的原型:

cpp 复制代码
int fseek(FILE* stream, long int offset, int origin);

作用:用来移动文件位置指针 ,实现文件的随机读写,不再只能从头到尾顺序读取 / 写入文件,可以直接跳到文件任意指定字节位置操作数据

参数说明:

  • FILE *stream:已经成功打开的文件指针;
  • long offset:偏移量,单位字节
    • 正数:向文件末尾方向偏移(向后)
    • 负数:向文件开头方向偏移(向前)
  • int origin:参考起始位置,只能选下面三个宏:
    • SEEK_SET:文件起始位置
    • SEEK_CUR:文件指针当前所在位置
    • SEEK_END:文件末尾位置

使用示例:

cpp 复制代码
​#include <stdio.h>
int main()
{
    FILE* pFile;
    // 以二进制只写模式打开文件
    pFile = fopen("example.txt", "wb");
    // 先写入字符串:This is an apple.
    fputs("This is an apple.", pFile);
    // 将文件指针从文件开头向后偏移9个字节
    fseek(pFile, 9, SEEK_SET);   
    // 从偏移后的位置写入字符串 " sam",覆盖原有内容
    fputs(" sam", pFile);
    // 关闭文件
    fclose(pFile);
    return 0;
}

我们以二进制只写模式打开文件,这样做是为了我们用fseek函数可以精确定位到我们所要的位置,原文本怎样,二进制模式打开就是怎样,如果用"w"文本模式打开,若文件内容中已有'\n',比如abc\n123,文本模式会自动将\n补全为\r\n,变成abc\r\n123多了一个字节的数据,当我们用fseek(4, SEEK_SET)想要定位到字符1,但因为换行被扩充多了一个字节,导致我们找到的不是1,而是\n,所以我们用二进制只写模式打开此文件

接着我们将字符串"This is an apple."写入到文件中,通过fseek(pFile, 9, SEEK_SET)将文件指针从文件起始位置向后偏移 9 个字节,定位到下标为 9 的字符n处,然后用fputs(" sam", pFile)覆盖此位置写入内容,得到的结果就是"This is a sample."

运行截图:

7.2 ftell 函数

ftell 函数的原型:

cpp 复制代码
long int ftell(FILE* stream);

作用:返回文件指针相对于起始位置的字节偏移量

使用示例:

cpp 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("myfile.txt", "wb");
	fputs("abcde",pf);
	fclose(pf);
	pf = NULL;
	FILE* pFile;
	long size;
	pFile = fopen("myfile.txt", "rb");
	if (pFile == NULL)
	{
		perror("Error opening file");
		return 1;
	}
	else
	{
		fseek(pFile, 0, SEEK_END); // non-portable
		size = ftell(pFile);
		printf("Size of myfile.txt: %ld bytes.\n", size);
	}
	fclose(pFile);
	pFile = NULL;
	return 0;
}

我们首先用二进制写入的模式打开文件,使用fputs将字符串"abcde"写入到文件中,紧接着关闭此文件再以二进制读的模式打开,然后再用fseek将偏移量置到文件末尾,紧接着用ftell返回文件末尾到文件起始之间的字节偏移量,因为此时文件里全是英文字符,一个英文字符占一个字节,所以其实得到的结果也就是此文件字符的个数

选用二进制模式打开文件,不会自动转换换行符,能够保证fseekftell统计的字节偏移量精准无误

运行截图:

7.3 rewind 函数

rewind 函数的原型:

cpp 复制代码
void rewind(FILE* stream);

作用:让文件指针的位置回到文件的起始位置

使用示例:

cpp 复制代码
#include <stdio.h>
int main()
{
	int n;
	FILE* pFile;
	char buffer[27];

	pFile = fopen("myfile.txt", "wb+");
	for (n = 'A'; n <= 'Z'; n++)
		fputc(n, pFile);
	rewind(pFile);

	fread(buffer, 1, 26, pFile);
	fclose(pFile);
    pFile = NULL;

	buffer[26] = '\0';
	printf("%s\n",buffer);
	return 0;
}

我们首先以二进制可读可写的模式打开文件myfile.txt,接着使用for循环搭配fputc函数将二十六个英文字母写进去,最关键的来了,写入完成后文件指针停留在文件末尾,所以接着我们使用了rewind函数将文件指针指向的位置重新置到文件起始位置,为我们后续使用fread顺序读取函数的正确使用做了铺垫,然后我们用fread将文件中这26个字节的数据放入字符数组buffer中,由于文件内不会自动存储字符串结束符\0,所以我们手动在buffer数组下标 26 的位置添加'\0'作为字符串终止标记,并打印,就能打印出我们文件当中的26个英文字母

运行截图:

八、文件读取结束的判定

8.1 feof 函数

feof 函数的原型:

cpp 复制代码
int feof(FILE* stream);

作用:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件末尾结束

  • 若因为读到文件末尾 EOF 导致读取结束:返回非 0(真),代表正常读取完成
  • 若因为文件读写出错导致读取结束:返回0(假)

8.2 ferror 函数

ferror 函数的原型:

cpp 复制代码
int ferror(FILE* stream);

作用:当文件读取结束的时候,判断是读取结束的原因是否是:发生读写错误

  • 如果之前的文件读写操作出现错误,返回非 0 值(真)
  • 如果没有发生读写错误,返回0(假)

8.3综合使用案例

总结:

  • feof:结束原因是读到文件末尾(正常结束)
  • ferror:结束原因是读写出错(异常结束)

使用示例1:(文本文件)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	int c; // 注意:int,非char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if (!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
	while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
	{
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))
		puts("\nI/O error when reading");
	else if (feof(fp))
		puts("\nEnd of file reached successfully");
	fclose(fp);
}

我们提前在test.txt文件中写入"abcde"五个字符

我们以读的模式打开文件test.txt,搭配fgetc函数读取文件里的内容,只要读出来的结果不等于EOF,就继续读,读取到的字符会通过putchar打印到屏幕上,直到等于EOF后不再读取,我们判断,如果ferror(fp)为真,就说明是文件读取错误,读取异常,导致错误,如果feof(fp)为真,就说明读取到文件末尾结束,正常读取结束

补充一点:fgetc 返回 EOF两种完全不同的原因,不只有 "读到文件末尾" 这一种:

  1. 正常读取到文件末尾,没有更多数据 → 返回 EOF
  2. 文件读写发生 I/O 异常(比如文件损坏、权限异常等),读取失败 → 也会返回 EOF

运行截图:

使用示例2:(二进制文件)

cpp 复制代码
#include <stdio.h>

enum { SIZE = 5 };

int main(void)
{
    // 定义double数组并初始化
    double a[SIZE] = { 1., 2., 3., 4., 5. };
    // 二进制只写模式打开文件
    FILE* fp = fopen("test.bin", "wb");

    // 将整个double数组写入二进制文件
    fwrite(a, sizeof * a, SIZE, fp);
    fclose(fp);

    // 定义接收数据的数组
    double b[SIZE];
    // 二进制只读模式打开文件
    fp = fopen("test.bin", "rb");

    // 读取SIZE个double类型数据
    size_t ret_code = fread(b, sizeof * b, SIZE, fp);

    if (ret_code == SIZE)
    {
        // 读取成功,遍历打印数组
        puts("Array read successfully, contents: ");
        for (int n = 0; n < SIZE; ++n)
            printf("%f ", b[n]);
        putchar('\n');
    }
    else
    {
        // 读取失败,判断是读到文件末尾还是读写错误
        if (feof(fp))
        {
            printf("Error reading test.bin: unexpected end of file\n");
        }
        else if (ferror(fp))
        {
            perror("Error reading test.bin");
        }
    }

    fclose(fp);
    return 0;
}

补充:sizeof 有两种用法:

  • 类型:必须加括号 sizeof(double)
  • 变量 / 表达式:可以不加括号 sizeof *a,写成 sizeof(*a) 也合法

首先我们定义了一个枚举常量SIZE赋值为5,代表大小为5,接着定义了一个double类型SIZE大小的数组,以"wb"二进制写入模式打开文件test.bin,并使用fwrite函数将这个数组的五个数据都写入到文件中,关闭文件,接着再创建了b数组来接收数据,用二进制只读模式"rb"打开文件,并使用fread函数读取文件中的数据复制到数组b中,如果返回值等于SIZE,我们就打印数组b的内容,返回值不等于SIZE,就看一下出错的原因,如果是遇到了文件末尾 if (feof(fp)),我们就打印 printf("Error reading test.bin: unexpected end of file\n"),如果是遇到了文件读错写误 else if (ferror(fp)),我们就使用perror("Error reading test.bin")打印一下错误的原因,最后关闭文件释放资源

运行截图:

九、文件缓冲区

我们在程序中使用文件时,都会为此文件单独创建一个文件缓冲区,当我们写入数据到文件中时,不是直接写的,而是先会把需要写入的数据先存入到缓冲区上,当缓冲区装满时就会自动写入磁盘 ,没装满时需要刷新 / 关文件才将缓冲区的内容写入磁盘中;当我们从文件中读取数据时,也不是直接读的,而是会将需读取的数据先存入到文件缓冲区中,再逐个读取数据,读取时除了fgetc逐个读取,也可以通过fgetsfread批量从缓冲区读取数据

我们用代码来实际感受文件缓冲区的存在:

cpp 复制代码
#include <stdio.h>
#include <windows.h>
//VS2022 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;
}

首先我们以只写的模式"w"打开文件,使用fputs函数将" abcdef "写入文件中,此时使用Sleep(10000)以毫秒为单位睡眠10秒,缓冲区不会被填满,数据依然存在缓冲区中,不会被写入到文件,此时我们打开文件test.txt会发现里面什么都没有,执行fflush(fp)手动刷新缓冲区后,我们再打开test.txt,就会发现文件里存入了" abcdef ",后面的Sleep是为了和fclose刷新缓冲区做个区别,表示这个缓冲区是通过fflush函数刷新,而不是fclose关闭文件时自动刷新

结论:

所以,由于文件缓冲区机制的存在,使用 C 语言进行文件操作时,必须手动刷新缓冲区,或是文件操作完毕后调用fclose关闭文件

若不执行刷新、不关闭文件,缓冲区中未写入磁盘的数据可能会仅存放在内存中,程序异常终止时数据会丢失,引发文件读写异常、数据保存失败等问题。

感谢大家的观看,新人求互三,关注我必回关!下章见!