C语言文件操作

📚 个人主页: ByteWizard

※ 专栏目录: 《C语言》

春风得意马蹄疾,一日看尽长安花


📚 ByteWizard 的简介:


目录

  • 1.为什么使用文件
  • 2.什么是文件
    • [2.1 程序文件](#2.1 程序文件)
    • [2.2 数据文件](#2.2 数据文件)
    • [2.3 文件名](#2.3 文件名)
  • 3.二进制文件和文本文件
  • [4. 文件的打开与关闭](#4. 文件的打开与关闭)
    • [4.1 流和标准流](#4.1 流和标准流)
      • [4.1.1 流](#4.1.1 流)
      • [4.1.2 标准流](#4.1.2 标准流)
    • [4.2 文件指针](#4.2 文件指针)
    • [4.3 文件的打开与关闭](#4.3 文件的打开与关闭)
      • [4.3.1 fopen](#4.3.1 fopen)
      • [4.3.2 fclose](#4.3.2 fclose)
  • [5. 文件的顺序读写](#5. 文件的顺序读写)
    • [5.1 fputc](#5.1 fputc)
    • [5.2 fgetc](#5.2 fgetc)
    • [5.3 feof 和 ferror](#5.3 feof 和 ferror)
    • [5.4 fputs](#5.4 fputs)
    • [5.5 fgets](#5.5 fgets)
    • [5.6 fprinf](#5.6 fprinf)
    • [5.7 fscanf](#5.7 fscanf)
    • [5.8 fwrite](#5.8 fwrite)
    • [5.9 fread](#5.9 fread)
    • [5.10 对比一组函数:](#5.10 对比一组函数:)
      • [5.10.1 sprintf](#5.10.1 sprintf)
      • [5.10.2 sscanf](#5.10.2 sscanf)
  • [6. 文件的随机读写](#6. 文件的随机读写)
    • [6.1 fseek](#6.1 fseek)
    • [6.2 ftell](#6.2 ftell)
    • [6.3 rewind](#6.3 rewind)
  • [7. 文件缓冲区](#7. 文件缓冲区)
    • [7.1 fflush](#7.1 fflush)
  • [8. 更新文件](#8. 更新文件)

1.为什么使用文件

文件的作用是持久化保存数据 。程序运行时数据通常存放在内存中,程序退出后内存会被回收,数据也会丢失。使用文件可以把数据保存下来 ,方便下次运行程序时继续读取和使用


2.什么是文件

文件是磁盘(硬盘)上的文件,可以用来长期保存数据。

在程序设计中,文件按功能可分为两类:

2.1 程序文件

程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境 后缀为.obj),可执⾏程序(windows环境 后缀为.exe)。


2.2 数据文件

保存程序运行时需要读取或者写入的数据,不一定是程序本身。

我们接下来主要讨论数据文件

以前的数据输入输出多以终端为对象 ,比如从键盘输入、在显示器输出,数据文件 则是将信息保存到磁盘上 ,需要时再从磁盘读取到内存中使用

2.3 文件名

文件名是文件的唯一标识,方便用户识别和引用文件。

文件名通常由三部分组成:

文件路径 + 文件名 + 文件后缀

例如:c:\code\test.txt

其中 c:\code是文件路径,test是文件名主干,.txt是文件名后缀。通常将整个文件标识简称为文件名

我们了解完上述之后,再来了解一下文件的相对路径 以及绝对路径

1. 绝对路径:

绝对路径是从磁盘根目录开始写的完整路径 ,不管当前程序在哪运行,都能明确找到文件。

例如:C:\Users\Byte\Desktop\data.txt

含义是:C盘 → Users → Byte → Desktop → data.txt

2.相对路径:

在了解相对路径的时候,我们要了解一下两个小点:

  1. . -- 当前目录

  2. .. -- 上一级路径

相对路径是相对于当前工作目录来查找文件的。

下图中是项目目录 ,里面包含 .vcxproj.c 源文件等项目文件 。VS 调试运行程序时,默认情况下常把该目录作为当前工作目录

如果当前工作目录是 C:\Users\Byte\Desktop,要访问同一目录下的 data.txt,可以写:data.txt.\data.txt

如果当前工作目录是 C:\Users\Byte,要访问桌面上的 data.txt,可以写:Desktop\data.txt.\Desktop\data.txt

如果文件在上一级目录,可以使用 ..\文件名


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

数据文件按组织形式分为文本文件二进制文件

二进制文件: 内存中的数据不经过转换,直接以二进制的形式存入外存文件。

文本文件: 数据在存储前转换为 ASCII 字符形式 , 在保存到外存文件中。

文件中数据的存储方式:

字符数据 通常以 ASCII 形式 存储。

数值型数据 既可以用 ASCII形式 存储,也可以用二进制形式来存储。

例如整数10000 :

  1. 用ASCII 形式存储:占 5个字节 (每一个字符占一个字节)

  2. 用二进制形式存储:占4个字节

因此,二进制存储通常比 ASCII 存储更节省空间

演示代码:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
	fclose(pf);
	pf = NULL;
	return 0;
}

补充一点,在VS上打开⼆进制⽂件:

找到对应的文件,点击添加

鼠标右键test.txt,点击打开方式

正常情况下可以看到:

为什么在二进制存储中是这个结果,由于这里涉及到大小端存储 的问题,相关知识点:C语言数据在内存中的存储, 我们通过画图来演示一下:


4. 文件的打开与关闭

文件操作通常分为三步:

  1. 打开文件

  2. 读取 / 写入文件

  3. 关闭文件

可以类比给瓶子装水和取水:

  1. 打开瓶子

  2. 装水 / 取水

  3. 关上瓶子

4.1 流和标准流

4.1.1 流

程序中的数据可能需要输出到文件、屏幕等外部设备,也可能需要从键盘、文件等外部设备读取数据。由于不同设备的输入输出方式不同,为了方便程序员操作,提出了 的概念。

流可以理解为一条"传递字符数据的通道" 。在 C 语言中,对文件、屏幕、键盘等进行输入输出操作,本质上都是通过流来完成的。

一般情况下:先打开流,再读写数据,最后关闭流。


4.1.2 标准流

C 程序启动时,会默认打开 3 个标准流,所以我们平时用 scanfprintf 时,不需要手动打开流。

标准流 含义 常见作用
stdin 标准输入流 通常从键盘读取数据
stdout 标准输出流 通常向屏幕输出普通信息
stderr 标准错误流 通常向屏幕输出错误信息

例如:scanf("%d",&a);本质上是从stdin中读取数据,printf("hello world\n")本质上是把数据输出到stdout中。

在 C 语言中,stdinstdoutstderr 的类型都是:FILE*,也就是文件指针

因此,C 语言通过 FILE * 来维护和操作各种流,包括键盘、屏幕、文件等输入输出对象。


4.2 文件指针

文件指针 本质上是指向 FILE 类型 结构体的指针,通常写作:FILE* fp

在 C 语言的缓冲文件系统中,每个被使用的文件都会在内存中对应一个文件信息区 ,用来保存文件的相关信息(文件名、文件状态、文件当前位置、读写方式、缓冲区信息),这些信息被保存在一个结构体变量中 ,该结构体类型由系统声明,名字叫:FILE

VS2013编译环境提供的stdio.h头⽂件中有以下的⽂件类型申明:

c 复制代码
struct _iobuf 
{
	char* _ptr;
	int   _cnt;
	char* _base;
	int   _flag;
	int   _file;
	int   _charbuf;
	int   _bufsiz;
	char* _tmpfname;
};

typedef struct _iobuf FILE;

不同编译器中的 FILE 结构体内容可能不完全相同,例如 MSVCGCC 中的实现细节会有差异,但作用基本一致,都是用来维护文件相关信息

程序一般不会直接操作 FILE 结构体本身,而是通过文件指针来操作文件:FILE* pf,FILE* pf表示定义了一个文件指针变量 pf,它可以指向某个文件对应的 FILE 结构体。

也就是说,文件指针并不是直接指向 磁盘上的文件内容,而是指向内存中与该文件相关的文件信息区 。程序通过这个文件指针 ,就能间接找到并操作与它关联的文件


4.3 文件的打开与关闭

文件读写前要先打开文件 ,使用结束后要关闭文件

在 C 语言中:使用 fopen 打开文件,使用 fclose 关闭文件。打开文件后会返回一个 FILE* 类型的指针变量,这个文件指针用于建立程序与文件之间的联系,后续读写操作都通过它完成。

4.3.1 fopen

项目 说明
函数名 fopen
所属头文件 stdio.h
函数原型 FILE *fopen(const char *filename, const char *mode);
功能 打开 filename 指定的文件,并将该文件与一个流关联起来
参数 filename 要打开的文件名,可以是相对路径 ,也可以是绝对路径
参数 mode 文件打开方式,用来指定对文件进行读、写、追加等操作
返回值 打开成功:返回指向 FILE 对象的文件指针
返回值 打开失败:返回 NULL
注意点 使用 fopen 后,应判断返回值是否为 NULL,防止文件打开失败

mode表⽰对打开的⽂件的操作⽅式:

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

代码演示:

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

int main()
{
	FILE* pf1 = fopen(".\\test.txt", "r"); //写法1:相对路径 - 文件在同一目录下
	FILE* pf2 = fopen("test.txt", "r"); //写法2:相对路径 - 文件在同一目录下
	FILE* pf3 = fopen("..\\test2.txt", "r"); //写法2:相对路径 - 文件不在同一目录下(假设在上级目录下)
	FILE* pf4 = fopen("C:\\2026C就业课语言代码\\2026-c-code\\2026_6_3\\2026_6_3\\test.txt", "r"); //写法3:绝对路径
	if (pf1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("打开文件成功\n");
	}
	if (pf2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("打开文件成功\n");
	}if (pf3 == NULL)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("打开文件成功\n");
	}if (pf4 == NULL)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("打开文件成功\n");
	}
	//读文件


	//关闭文件
	fclose(pf1);
	fclose(pf2);
	fclose(pf3);
	fclose(pf4);
	pf1 = NULL;
	pf2 = NULL;
	pf3 = NULL;
	pf4 = NULL;
	return 0;
}

在 C 语言字符串中,\ 是转义字符的开始。如果写 ".\test.txt",其中的 \t 会被解析为制表符。因此要表示路径中的反斜杠,应写成 ".\\test.txt";也可以使用 ./test.txt。转义字符相关知识点链接:初识C语言


4.3.2 fclose

项目 内容
函数名 fclose
所属头文件 stdio.h
函数原型 int fclose(FILE *stream);
功能 关闭 stream 指向的文件,并取消文件与流之间的关联
参数 stream:指向要关闭的流的 FILE * 文件指针
返回值 关闭成功返回 0,关闭失败返回 EOF

fclose函数的主要作用:

作用 说明
关闭文件 结束文件的使用
解除关联 取消文件和流之间的联系
刷新缓冲区 将输出缓冲区中还没写入文件的数据写入文件
释放资源 释放与该流相关的内存缓冲区等资源
丢弃输入缓冲区 未读取的输入缓冲区内容会被丢弃

代码演示:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>


int main()
{

    FILE* fp = fopen("test.txt", "r"); // 以 "r" 的形式打开文件,如果文件不存在,则打开失败

    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    printf("打开文件成功,可以对文件进行操作\n");

    fclose(fp);   // 不再使用文件时,需要关闭文件
    fp = NULL;    // 将指针置为 NULL,避免成为野指针

    return 0;
}

5. 文件的顺序读写

在进行文件读写时,会涉及下面的函数:

函数名 功能 适用于
fgetc 从输入流中读取一个字符 所有输入流
fputc 向输出流中写入一个字符 所有输出流
fgets 从输入流中读取一个字符串 所有输入流
fputs 向输出流中写入一个字符串 所有输出流
fscanf 从输入流中读取带格式的数据 所有输入流
fprintf 向输出流中写入带格式的数据 所有输出流
fread 从输入流中读取一块数据 文件输入流
fwrite 向输出流中写入一块数据 文件输出流

其中,所有输入流 一般包括:标准输入流 stdin文件输入流其他输入流。像 fgetcfgetsfscanf 这类读取函数,既可以从键盘读取,也可以从文件中读取;

所有输出流 一般包括:标准输出流 stdout文件输出流其他输出流。像 fputcfputsfprintf 这类写入函数,既可以向屏幕输出,也可以向文件中写入。


5.1 fputc

项目 内容
函数名 fputc
所属头文件 stdio.h
函数原型 int fputc(int character, FILE *stream);
功能 stream 指向的输出流中写入一个字符
参数 character 要写入的字符,虽然类型是 int,但实际写入的是一个字符
参数 stream 指向输出流的 FILE * 文件指针,可以是文件流 ,也可以是 stdout
写入位置 字符会写入当前文件位置指示器所指向的位置
写入后变化 写入成功后,文件位置指示器 会自动向后移动一个字符位置
返回值:成功 返回写入的字符 ,返回类型为 int
返回值:失败 返回 EOF(end of file),通常是 -1
错误检查 如果写入失败,会设置错误标志,可用 ferror(stream) 检查

这里要弄清光标文件位置指示器的概念:

概念 说明
光标 通常是屏幕或编辑器中可见的位置
文件位置指示器 文件流内部维护的当前读写位置,看不见
作用 决定下一次从哪里读、往哪里写
自动移动 读写操作后会根据实际读写的字符数自动移动
手动控制 可用 fseekrewind 改变位置,用 ftell 获取当前位置

代码演示:

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

int main()
{
	//以w形式打开文件,才能正确的写文件
	FILE* pf = fopen(".\\test.txt", "w");

	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//代码1:一次性写入几个字符
	//写入文件
	//fputc('x', pf);
	//fputc('y', pf);
	//fputc('z', pf);
	//在test.txt中写入xyz
	
	//代码2:循环写入字符
	for (char x = 'a'; x <= 'z'; x++)
	{
		fputc(x, stdout); //向屏幕中输出a ~ z
	}


	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

5.2 fgetc

项目 内容
函数名 fgetc
所属头文件 stdio.h
函数原型 int fgetc(FILE *stream);
功能 stream 指向的输入流中读取一个字符
参数 stream FILE * 类型的文件指针,可以是 stdin,也可以是文件输入流
适用于 所有输入流,例如标准输入流 stdin、文件输入流
读取位置 文件位置指示器当前指向的位置读取字符
读取后变化 读取成功后,文件位置指示器自动向后移动一个字符位置
返回值:成功 返回读取到的字符 ,返回类型为 int
返回值:文件结束 如果读到文件末尾,返回 EOF,并设置文件结束标志,可用 feof(stream) 检查
返回值:读取错误 如果读取出错,也返回 EOF,并设置错误标志,可用 ferror(stream) 检查
注意点 因为失败和文件结束都会返回 EOF,所以需要用 feof()ferror() 区分原因
代码演示:
c 复制代码
#include <stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");

	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	////读取文件-方式1
	//int ch = fgetc(pf);
	//printf("%c\n", ch);
	//ch = fgetc(pf);
	//printf("%c\n", ch);
	//ch = fgetc(pf);
	//printf("%c\n", ch);
	//ch = fgetc(pf);
	//printf("%c\n", ch);

	//读取文件-方式2
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
		printf("%c", ch);
	printf("\n");


	if (feof(pf))
		printf("文件正常读取结束\n");
	if (ferror(pf))
		printf("文件读写错误\n");

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

5.3 feof 和 ferror

函数 函数原型 作用 检测对象 返回值含义
feof int feof(FILE *stream); 检测文件是否到达文件末尾 文件结束指示符 若文件结束指示符已被设置,返回非 0;否则返回 0
ferror int ferror(FILE *stream); 检测文件读/写过程中是否发生错误 错误指示符 若错误指示符已被设置,返回非 0;否则返回 0

代码演示:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

//假设test.txt文件中存放abcdef
int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen\n");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        int c = fgetc(fp);
        if (c == EOF)
        {
            if (feof(fp))
                printf("遇到文件末尾了\n");
            else if (ferror(fp))
                printf("读取发生了错误\n");
        }
        else
        {
            fputc(c, stdout); //使用fputc在标准输出流上打印字符
        }
    }

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

注意:文件打开模式与实际操作不匹配,会导致 I/O 错误。用写模式打开却读取,会发生读取错误;用读模式打开却写入,会发生写入错误,看如下代码:

c 复制代码
#include <stdio.h>
//以写的形式打开文件后,再去读文件,就会发生错误
int main()
{
    FILE* fp = fopen("test.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    int c = fgetc(fp);//读文件
    if (c == EOF)
    {
        if (feof(fp))
            printf("遇到文件末尾了\n");
        else if (ferror(fp))
        {
            printf("读写文件发生了错误\n"); //执行语句
        }
    }
    else
    {
        fputc(c, stdout); //使用fputc在标准输出流上打印字符
    }

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.4 fputs

项目 说明
函数名 fputs
函数原型 int fputs(const char *str, FILE *stream);
所属头文件 #include <stdio.h>
功能 将字符串 str 写入到 stream 指定的流中
参数 str 指向要写入的字符串,字符串必须以 \0 结尾
参数 stream FILE * 类型的文件流指针,表示要写入的目标流
写入内容 写入字符串中的有效字符 ,但不写入字符串末尾的 \0
适用对象 可用于文件流,也可用于标准输出流 stdout
成功返回值 返回一个非负整数
失败返回值 返回 EOF,通常为 -1
错误检测 写入失败时会设置流的错误指示器,可使用 ferror(stream) 检查错误原因

代码演示:

c 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");

	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputs("hello ", stdout); //使用 fputs 向标准输出流 stdout 写入字符串
	fputs("world", pf); //使用 fputs 向 pf 指向的文件流中写入字符串

	fclose(pf);
	pf = NULL;

	return 0;
}

5.5 fgets

项目 说明
函数名 fgets
函数原型 char *fgets(char *str, int num, FILE *stream);
所属头文件 #include <stdio.h>
功能 stream 指定的输入流中读取字符串 ,并存放到 str 指向的字符数组中
参数 str 字符数组指针,用来保存读取到的字符串
参数 num 最多读取的字符数量,包含字符串结尾的 \0
实际最多读取字符数 最多读取 num - 1 个有效字符,因为最后要留一个位置存放 \0
参数 stream 输入流指针,可以是文件流 ,也可以是标准输入流 stdin
读取停止条件 读到换行符 \n、读到文件末尾 EOF、达到 num - 1 个字符、发生读取错误
是否保留换行符 如果在读取范围内读到了 \n,会把 \n 一起存入 str
是否自动添加 \0 会自动在读取到的字符串末尾添加 \0
成功返回值 返回 str 指针
失败返回值 返回 NULL
文件末尾情况 如果尝试读取时已经遇到文件末尾 ,返回 NULL,可用 feof(stream) 判断
读取错误情况 如果发生读取错误 ,返回 NULL,可用 ferror(stream) 判断

代码演示:

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

int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读文件
    char arr[20] = "----------";
    
    while (fgets(arr, 20, fp) != NULL)
    {
        printf("%s", arr);
    }


    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.6 fprinf

项目 说明
函数名 fprintf
函数原型 int fprintf(FILE *stream, const char *format, ...);
所属头文件 #include <stdio.h>
功能 格式化数据写入到指定的文件流中
printf 的关系 fprintfprintf 类似,但 printf 默认输出到屏幕,而 fprintf 可以输出到指定流
参数 stream 指向 FILE 对象的指针,表示要写入的目标流
参数 format 格式化字符串,包含普通文本和格式说明符,如 %d%s%f
参数 ... 可变参数列表,用来提供与格式说明符对应的数据
输出目标 可以是文件流,也可以是标准输出流 stdout、标准错误流 stderr
成功返回值 返回成功写入的字符总数 ,返回值为非负数
失败返回值 返回负值
错误检测 写入失败时,会设置对应流的错误指示器,可以使用 ferror(stream) 检测

代码演示:

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

struct Stu
{
    char name[20];
    int age;
    float score;
};

int main()
{
    struct Stu s = { "zhangsan", 23, 100.0f };

    FILE* fp = fopen("test.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    //写文件
    fprintf(stdout, "名字:%s 年龄:%d 成绩:%.2f\n", s.name, s.age, s.score); // 向标准输出流 stdout 输出格式化数据
    fprintf(fp, "名字:%s 年龄:%d 成绩:%.2f\n", s.name, s.age, s.score); // 向 fp 指向的文件流中写入格式化数据


    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.7 fscanf

项目 说明
函数名 fscanf
函数原型 int fscanf(FILE *stream, const char *format, ...);
所属头文件 #include <stdio.h>
功能 从指定文件流中读取格式化数据
scanf 的关系 scanf 默认从键盘 stdin 读取;fscanf 可以从指定流读取,如文件流、stdin
参数 stream 指向 FILE 对象的指针,表示要读取数据的输入流
参数 format 格式化字符串,用来规定读取数据的格式 ,如 %d%f%s
参数 ... 可变参数列表,通常是变量地址 ,用来保存读取到的数据
成功返回值 返回成功读取并赋值的数据个数
读取失败返回值 如果在读取任何数据前就遇到文件末尾读取错误 ,返回 EOF
格式不匹配 如果格式与数据不匹配,读取会停止,返回已成功读取的数据个数 ,可能为 0
文件末尾检测 可使用 feof(stream) 判断是否到达文件末尾
读取错误检测 可使用 ferror(stream) 判断是否发生读取错误

代码演示:

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

struct Stu
{
    char name[20];
    int age;
    float score;
};

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

    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    //读文件
    //scanf("%s %d %f", s.name, &(s.age), &(s.score));
    // 从 fp 指向的文件流中按指定格式读取数据,保存到结构体变量 s 中
    // 注意:%s 对应字符数组名 s.name,数组名本身就是地址;%d 和 %f 对应变量地址 &s.age、&s.score
    fscanf(fp, "名字:%s 年龄:%d 成绩:%f", s.name, &(s.age), &(s.score)); 
    fprintf(stdout, "名字:%s 年龄:%d 成绩:%.2f", s.name, s.age, s.score);

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.8 fwrite

项目 说明
函数名 fwrite
函数原型 size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
所属头文件 #include <stdio.h>
功能 将数据块写入到 stream 指定的文件流中
写入方式 二进制形式写入
参数 ptr 指向要写入数据的内存地址
参数 size 每个数据项的大小,单位是字节
参数 count 要写入的数据项个数
参数 stream 指向 FILE 类型结构体的文件流指针
返回值类型 size_t
成功返回值 返回实际成功写入的数据项个数
失败返回值 如果写入失败,返回值可能小于 count
错误检测 可使用 ferror(stream) 判断是否发生写入错误

很多人可能不知道数据项是什么 ,在fwrite中,数据项 可以理解为:数据项就是你规定的"一份数据" ,在 fwrite 里,不是它自己决定什么叫"一份数据",而是你通过 sizecount 告诉它,每份数据有多大,要写几份数据

代码演示:

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

struct Stu
{
    char name[20];
    int age;
    float score;
};

int main()
{
    struct Stu s = {.name = "zhangsan", .score = 150.0f , .age = 20}; //不按顺序初始化

    FILE* fp = fopen("test.txt", "wb"); //应该用二进制写入的方式打开文件
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fwrite(&s, sizeof(struct Stu), 1, fp);

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.9 fread

项目 说明
函数名 fread
函数原型 size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
所属头文件 #include <stdio.h>
功能 stream 指定的文件流中读取数据块,并保存到 ptr 指向的内存空间中
读取方式 通常用于二进制方式读取
参数 ptr 指向内存空间的指针,用来存放从文件中读取到的数据
参数 size 每个数据项的大小,单位是字节
参数 count 要读取的数据项数量
参数 stream 指向 FILE 类型对象的文件流指针,表示从哪个文件流中读取
返回值类型 size_t
成功返回值 返回实际成功读取的数据项个数
读取不足 如果返回值小于 count,可能是读到文件末尾或发生读取错误
文件末尾检测 可使用 feof(stream) 判断是否到达文件末尾
读取错误检测 可使用 ferror(stream) 判断是否发生读取错误

代码演示:

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

struct Stu
{
    char name[20];
    int age;
    float score;
};

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

    FILE* fp = fopen("test.txt", "rb"); //以二进制方式读取文件
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    fread(&s, sizeof(struct Stu), 1, fp);
    printf("%s %d %.2f\n", s.name, s.age, s.score);

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

5.10 对比一组函数:

scanf / fsanf / sscanf

printf / fprintf / sprintf

5.10.1 sprintf

项目 说明
函数名 sprintf
函数原型 int sprintf(char *str, const char *format, ...);
所属头文件 #include <stdio.h>
功能 将格式化的数据写入字符数组,也就是生成一个字符串
printf 的关系 printf 是把格式化数据输出到屏幕;sprintf 是把格式化数据写入字符串
参数 str 指向字符数组的指针,用来保存生成后的字符串
参数 format 格式化字符串,用来指定数据的输出格式,如 %d%f%s
参数 ... 可变参数列表,提供与格式说明符对应的数据
写入位置 写入到用户指定的字符数组中
成功返回值 返回写入字符数组中的字符个数,不包括结尾的 \0
失败返回值 返回负值
是否自动添加 \0 会在生成的字符串末尾自动添加 \0
使用风险 如果 str 指向的字符数组空间不够,可能造成缓冲区溢出,最终导致程序崩溃

代码演示:

c 复制代码
//代码1
#include <stdio.h>

struct Stu
{
    char name[20];
    int age;
    float score;
};

int main()
{
    struct Stu s = { .score = 149.0f,.name = "zhangsan",.age = 21 };

    char str[30] = { 0 };

    sprintf(str, "%s %d %.2f", s.name, s.age, s.score);
    printf("%s\n", str);

	return 0;
}
//代码2
#include <stdio.h>

int main()
{
    int num = 12345;
    char str_num[10] = { 0 };
    sprintf(str_num, "%d", num);
    printf("%s\n", str_num);

	return 0;
}

5.10.2 sscanf

项目 说明
函数名 sscanf
函数原型 int sscanf(const char *str, const char *format, ...);
所属头文件 #include <stdio.h>
功能 从字符串 str 中按照指定格式读取数据
scanf 的关系 scanf 从键盘输入读取;sscanf 从字符串中读取
参数 str 要解析的源字符串,也就是数据来源
参数 format 格式化字符串,用来规定如何解析数据 ,如 %d%f%s
参数 ... 可变参数列表,通常传入变量地址 ,用来保存解析出来的数据
成功返回值 返回成功解析并赋值的数据个数
失败返回值 如果解析失败或没有成功匹配任何数据,返回 EOF,通常是 -1
常见用途 从已有字符串中提取整数、浮点数、字符串等结构化数据

代码演示:

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

struct Stu
{
    char name[20];
    int age;
    float score;
};

int main()
{
    struct Stu s = { .score = 149.0f,.name = "zhangsan",.age = 21 };

    char str[30] = { 0 };

    sprintf(str, "%s %d %.2f", s.name, s.age, s.score);
    printf("%s\n", str);

    //从str中解析出一个结构体数据
    struct Stu t = { 0 };
    sscanf(str, "%s %d %f\n", t.name, &(t.age), &(t.score));
    fprintf(stdout, "%s %d %.2f\n", t.name, t.age, t.score);

	return 0;
}

6. 文件的随机读写

6.1 fseek

项目 内容
函数名 fseek
所属头文件 stdio.h
函数原型 int fseek(FILE *stream, long int offset, int origin);
主要功能 根据文件指针的位置和偏移量,移动文件内部的位置指针,也就是改变文件内容的光标
作用 用于文件的随机读写,可以跳到文件中的指定位置进行读取或写入
返回值 成功返回 0,失败返回非 0,通常是 -1
参数 含义 说明
stream 文件指针 指向已经打开的文件,例如 fp
offset 偏移量 表示从指定起点移动多少个字节,可以是正数、负数或 0
origin 起始位置 表示从哪里开始移动文件指针

orignal有三个取值:

取值 含义 大白话理解
SEEK_SET 文件开头位置 从文件最开始的位置算起
SEEK_CUR 文件当前位置 从当前文件指针所在位置算起
SEEK_END 文件末尾位置 从文件最后的位置算起

使用注意事项:

类型 说明
二进制文件 fseek 行为比较稳定,可以精确移动到某个字节位置
文本文件 某些环境下不一定完全稳定,因为换行符可能会发生转换
文本文件推荐写法 offset 设置为 0origin 设置为 SEEK_SETSEEK_CURSEEK_END
ftell 配合 可以先用 ftell() 获取当前位置,再用 fseek() 回到该位置
使用前提 文件必须已经成功打开,否则不能使用 fseek

代码演示:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
    //假设test.txt文件中存储了hello world
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    int c = fgetc(fp);
    fputc(c, stdout); //h
    // fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置

    // 定位文件位置指示器
    //fseek(fp, 2, SEEK_CUR);  // 以当前位置为起点,向后移动 2 个字符
    //fseek(fp, 4, SEEK_SET);  // 以文件开头为起点,向后移动 4 个字符
    fseek(fp, -4, SEEK_END);   // 以文件末尾为起点,向前移动 4 个字符

    c = fgetc(fp);
    fputc(c, stdout);// 情况1读到 l,情况2读到 o,情况3读到 o

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

6.2 ftell

项目 内容
函数名 ftell
所属头文件 stdio.h
函数原型 long int ftell(FILE *stream);
主要功能 返回当前文件位置指示器相对于文件起始位置的偏移量
作用对象 已经打开的文件
常见用途 获取当前读写位置、记录文件位置、配合 fseek 恢复到指定位置

参数说明:

参数 含义 说明
stream 文件指针 指向一个已经打开的文件,例如 fp

返回值:

情况 返回值 含义
成功 当前偏移量 返回当前位置相对于文件开头的偏移量,单位通常是字节
失败 -1L 说明发生错误,例如文件流无效或文件不支持定位

使用注意事项:

文件类型 说明
二进制文件 ftell 返回值通常表示从文件开头算起的精确字节数,比较可靠
文本文件 由于不同系统对换行符处理不同,例如 Windows 中 \r\n 和 Linux 中 \n 不一样,ftell 返回值不一定能简单理解成字符个数
推荐用法 在文本文件中,ftell 通常和 fseek 配合,用来记录和恢复位置

代码演示:

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

int main()
{
    //假设test.txt文件中存储了hello world
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    int c = fgetc(fp);
    fputc(c, stdout); //h
    int pos = ftell(fp);//1
    // fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置

    // 定位文件位置指示器
    fseek(fp, 2, SEEK_CUR);  // 以当前位置为起点,向后移动 2 个字符
    c = fgetc(fp);
    fputc(c, stdout); //l

    fseek(fp, pos, SEEK_SET);  // 以文件开头为起点,向后移动 4 个字符
    c = fgetc(fp);
    fputc(c, stdout); //e

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

6.3 rewind

项目 内容
函数名 rewind
所属头文件 #include <stdio.h>
函数原型 void rewind(FILE *stream);
函数功能 将文件位置指示器重新定位到文件开头
参数说明 stream:指向已打开文件的文件指针
返回值 无返回值,即 void
主要作用 让文件从头开始重新读写
额外作用 清除文件的错误标志和文件结束标志
fseek 的区别 fseek(fp, 0, SEEK_SET) 只移动文件位置指示器;rewind(fp) 不仅移动到文件开头,还会清除错误标志和文件结束标志
注意事项 rewind 没有返回值,不能直接判断是否执行成功;如果需要判断是否定位成功,建议使用 fseek

代码演示:

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

int main()
{
    //假设test.txt文件中存储了hello world
    FILE* fp = fopen("test.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    int c = fgetc(fp);
    fputc(c, stdout); //h
    // fgetc 读取一个字符后,文件位置指示器(光标)自动向后移动 1 个字符位置

    // 定位文件位置指示器
    fseek(fp, 2, SEEK_CUR);  // 以当前位置为起点,向后移动 2 个字符
    c = fgetc(fp);
    fputc(c, stdout); //l

    rewind(fp); //让文件指示器回到起始位置

    c = fgetc(fp);
    fputc(c, stdout); //h

    //不再使用文件时,需要关闭文件
    fclose(fp);
    fp = NULL; //将指针置为NULL,避免成为野指针。
    return 0;
}

7. 文件缓冲区

文件缓冲区是程序和磁盘文件之间的一块内存临时区域。程序读写文件时,数据通常不会马上直接和磁盘交互,而是先经过缓冲区。

写文件时:

程序数据先进入缓冲区,等缓冲区满了、调用 fflush、调用 fclose程序正常结束时,再真正写入磁盘。

读文件时:

系统通常先从磁盘一次性读入一批数据到缓冲区,程序再从缓冲区中逐个读取。

这样做的好处是:减少磁盘访问次数,提高文件读写效率。


7.1 fflush

项目 内容
函数名 fflush
所属头文件 #include <stdio.h>
函数原型 int fflush(FILE *stream);
函数功能 强制刷新指定流的缓冲区,确保缓冲区中的数据被写入目标设备或文件
参数说明 stream:指向文件流的指针,如 stdout、文件指针等
用于输出流 将缓冲区中还没有写入的数据立即写入文件或屏幕
用于输入流 行为不标准,不同编译器结果可能不同,不建议使用
参数为 NULL 刷新所有已经打开的输出流
返回值 成功返回 0,失败返回 EOF
常见用法 fflush(stdout); 用来立即刷新屏幕输出
注意事项 1 fflush 主要用于输出流或更新流
注意事项 2 不建议使用 fflush(stdin); 清空输入缓冲区,因为它不是标准 C 语言用法
注意事项 3 程序正常结束时会自动刷新缓冲区,但程序异常退出时缓冲区数据可能丢失

代码演示:

c 复制代码
#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); // 刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)

    printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
    Sleep(10000);

    fclose(pf);
    // 注:fclose在关闭文件的时候,也会刷新缓冲区
    pf = NULL;

    return 0;
}

我们从中得出结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。


8. 更新文件

行为 "r+" "w+" "a+"
含义 读写模式 读写模式 追加读写模式
文件不存在时 打开失败 自动创建新文件 自动创建新文件
文件存在时 保留原有内容 清空原有内容 保留原有内容
初始文件指针位置 文件开头 文件开头 文件末尾
写入是否覆盖原数据 是,可在指定位置覆盖 是,原内容先被清空,再从头写入 否,默认在文件末尾追加数据
典型用途 修改文件的部分内容 创建新文件或完全重写文件 在文件末尾追加内容,比如写日志

在文件以 r+w+a+ 这类可读可写 模式打开时,读写操作不能随便连续切换,中间通常要做一次刷新或重新定位

写完后如果要继续读 ,需要先调用 fflush() 刷新缓冲区,或者使用 fseek()rewind() 重新定位文件位置指示器。这样可以保证刚刚写入的数据已经同步到文件中,并且读取位置是确定的。

读完后如果要继续写 ,在写入之前需要先使用 fseek()rewind() 重新定位文件位置指示器 。这样可以明确接下来要写入的位置,避免写入位置不确定。

代码演示:

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

int main()
{
    FILE* fp = fopen("test.txt", "w+");
    if (fp == NULL)
    {
        perror("fopen for w+");
        return 1;
    }

    //写abcdefghi到文件中
    fputs("abcdefghi", fp);
    //刷新缓冲区,保证数据写入文件
    fflush(fp);
    //要读取数据b字符,先定位文件指针
    fseek(fp, 1, SEEK_SET);

    int ch = fgetc(fp);//读取字符
    printf("%c\n", ch); //b
    //abcdefghi
    
    //在b的位置开始写入hello
    fseek(fp, -1, SEEK_CUR);
    //解释:因为前面读取一个字符后,文件指示器现在指向了c,需要从当前位置退回一个字符

    fputs("hello", fp);
    //ahelloghi
    // 
    //关闭文件
    fclose(fp);
    fp = NULL;

    return 0;
}
相关推荐
scan7242 小时前
SystemMessage,HumanMessage,AIMessage,ToolMessage
开发语言·前端·javascript
小科先生2 小时前
配置java环境变量
java·开发语言
meilindehuzi_a2 小时前
撕开 JS 的 Class 面具:从构造函数的 new 降生到顶层原型链的终极通关
开发语言·javascript·ecmascript
Zephyrus_20232 小时前
LSM6DSV16X驱动移植+调试
c语言·arm开发
天天进步20152 小时前
Python全栈项目--智能远程医疗系统
开发语言·python
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法
Vertira3 小时前
VS2022 配置Qt5/6 [已解决]
开发语言·qt
RSTJ_16253 小时前
PYTHON+AI LLM DAY SIXTY-SEVEN
开发语言·python
FuckPatience3 小时前
C# 继承中的使用new的陷阱,和abstract /virtual 的不同
开发语言·c#