当前位置: 首页 > news >正文

UNIX下C语言编程与实践24-UNIX 标准文件编程库:无格式读写函数族(字符、行、块)的使用

深入解析 UNIX 标准 IO 库中三类无格式读写函数,从语法到实战全覆盖

一、无格式读写函数族概述:何为“无格式”?

在 UNIX 标准文件编程库(stdio.h 提供)中,文件读写分为“有格式读写”和“无格式读写”两类。其中“无格式读写”指函数不解析数据的格式(如不将字符串转换为整数、不处理格式化符号),直接按原始数据形式(字符、字节流)与文件交互,核心优势是高效、通用,适用于文本文件和二进制文件。

无格式读写函数族按数据处理粒度,分为三类:

  • 按字符读写:以单个字符(1 字节)为单位读写数据,如 fgetcfputc
  • 按行读写:以“行”为单位读写文本数据(以换行符 \n 为行结束标志),如 fgetsfputs
  • 按块读写:以自定义大小的“数据块”(如 1024 字节)为单位读写数据,如 freadfwrite,是效率最高的无格式读写方式。

核心认知:所有无格式读写函数均基于“文件流(FILE*)”操作,需先通过 fopen 函数打开文件获取文件流指针,操作完成后通过 fclose 关闭流。文件流封装了文件描述符、缓冲区、读写位置等信息,是标准 IO 库的核心抽象。

二、按字符读写函数族:fgetc 与 fputc 实战

按字符读写函数是无格式读写中最基础的类别,以单个字符为单位与文件交互,适用于需要逐个处理字符的场景(如字符替换、字符统计)。核心函数包括 fgetc(读字符)和 fputc(写字符),此外还有宏定义 getc 和 putc,功能与前者一致但实现方式不同。

1. 核心函数解析

函数原型功能说明参数含义返回值注意事项
int fgetc(FILE *stream);从指定文件流 stream 中读取一个字符,同时将文件读写位置指针向后移动 1 字节stream:文件流指针(如 fopen 返回的指针)成功:返回读取的字符(unsigned char 类型转换为 int);
失败/EOF:返回 EOF(宏定义,值为 -1)
1. 需通过返回值是否等于 EOF 判断读取结束;
2. 读取文本文件和二进制文件均可使用
int fputc(int c, FILE *stream);将字符 c 写入指定文件流 stream,同时移动读写位置指针c:待写入的字符(int 类型,实际取低 8 位);
stream:文件流指针
成功:返回写入的字符(转换为 int);
失败:返回 EOF
1. 写入字符时需确保文件以“可写模式”打开(如 "w""a");
2. 写入换行符 '\n' 可实现换行
#define getc(stream) (...)与 fgetc 功能完全一致,读取一个字符同 fgetc同 fgetc1. 是宏定义,而非函数;
2. 执行速度略快(无函数调用开销),但不可作为函数指针传递
#define putc(c, stream) (...)与 fputc 功能完全一致,写入一个字符同 fputc同 fputc1. 是宏定义;
2. 执行速度略快,不可作为函数指针传递

2. 实战案例:用 fgetc 和 fputc 复制文件

需求:编写程序 copy_char.c,通过按字符读写实现文件复制功能(支持文本文件和二进制文件)。

#include <stdio.h>
#include <stdlib.h>// 函数:按字符复制文件(src_path 源文件,dest_path 目标文件)
int copy_file_by_char(const char *src_path, const char *dest_path)
{// 1. 打开源文件(只读模式)和目标文件(写入模式,不存在则创建,存在则覆盖)FILE *src_fp = fopen(src_path, "r");FILE *dest_fp = fopen(dest_path, "w");// 2. 检查文件是否成功打开if (src_fp == NULL) {perror("Failed to open source file");return -1;}if (dest_fp == NULL) {perror("Failed to open destination file");fclose(src_fp);  // 避免内存泄漏,先关闭已打开的源文件return -1;}// 3. 按字符读取源文件并写入目标文件int ch;  // 必须用 int 类型,因为 EOF 是 -1(char 无法存储)while ((ch = fgetc(src_fp)) != EOF) {if (fputc(ch, dest_fp) == EOF) {perror("Failed to write character");fclose(src_fp);fclose(dest_fp);return -1;}}// 4. 检查读取是否正常结束(避免因读取错误导致复制中断)if (ferror(src_fp)) {perror("Failed to read character");fclose(src_fp);fclose(dest_fp);return -1;}// 5. 关闭文件流fclose(src_fp);fclose(dest_fp);printf("File copied successfully (by character)\n");return 0;
}int main(int argc, char *argv[])
{// 检查命令行参数(需传入源文件和目标文件路径)if (argc != 3) {fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);exit(EXIT_FAILURE);}// 调用复制函数if (copy_file_by_char(argv[1], argv[2]) != 0) {exit(EXIT_FAILURE);}return EXIT_SUCCESS;
}

编译与测试命令

gcc copy_char.c -o copy_char
echo "Hello UNIX File IO" > test.txt
./copy_char test.txt test_copy.txt
cat test_copy.txt

File copied successfully (by character) Hello UNIX File IO

关键注意点

  • fgetc 返回值必须用 int 类型接收:因为 EOF 是 -1,若用 char 接收(默认 signed char),会与值为 0xFF 的字符(如扩展 ASCII 字符)混淆,导致读取提前结束;
  • 复制二进制文件需修改打开模式:将 "r" 改为 "rb"(二进制只读),"w" 改为 "wb"(二进制写入),避免换行符转换(如 Windows 下 \r\n 与 UNIX 下 \n 的差异);
  • 必须检查 ferrorfgetc 返回 EOF 可能是“正常结束”或“读取错误”,需通过 ferror(src_fp) 判断——返回非 0 表示读取错误。

三、按行读写函数族:fgets 与 fputs 实战

按行读写函数专门用于处理文本文件,以“行”为单位读写数据(行结束标志为 \n),适用于逐行解析文本的场景(如读取配置文件、日志文件)。核心函数是 fgets(读行)和 fputs(写行),需注意与不安全的 gets 函数区分。

1. 核心函数解析

函数原型功能说明参数含义返回值注意事项
char *fgets(char *str, int size, FILE *stream);从文件流 stream 中读取一行文本,存储到缓冲区 str,最多读取 size-1 个字符str:存储读取内容的缓冲区;
size:缓冲区大小(字节);
stream:文件流指针
成功:返回 str 指针;
失败/EOF:返回 NULL
1. 读取到 \n 或 size-1 个字符时停止,自动在缓冲区末尾添加 \0(确保字符串结束);
2. 若一行长度超过 size-1,会分多次读取该行剩余内容
int fputs(const char *str, FILE *stream);将字符串 str 写入文件流 stream,不自动添加换行符 \nstr:待写入的字符串(需以 \0 结束);
stream:文件流指针
成功:返回非负值;
失败:返回 EOF
1. 写入时不会自动添加 \n,需手动在字符串末尾添加(如 "line\n");
2. 仅写入 \0 前的内容,\0 不写入文件
char *gets(char *str);从标准输入(stdin)读取一行文本,存储到 strstr:缓冲区指针成功:返回 str
失败/EOF:返回 NULL
1. 严重不安全:无缓冲区大小限制,输入过长会导致缓冲区溢出,已从 C11 标准中移除;
2. 禁止使用,必须用 fgets(str, size, stdin) 替代
int puts(const char *str);将字符串 str 写入标准输出(stdout),自动在末尾添加 \nstr:待写入的字符串成功:返回非负值;
失败:返回 EOF
1. 与 fputs(str, stdout) 的区别:自动添加 \n
2. 若字符串中包含 \0,仅写入第一个 \0 前的内容

2. 实战案例:用 fgets 和 fputs 处理日志文件

需求:编写程序 filter_log.c,读取日志文件 app.log,筛选出包含“ERROR”关键字的行,写入 error.log

以下是调整缩进后的代码,符合C语言标准缩进规范(4空格缩进):

调整后的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define BUF_SIZE 1024int filter_error_log(const char *src_log, const char *dest_log) {FILE *src_fp = fopen(src_log, "r");FILE *dest_fp = fopen(dest_log, "a");if (src_fp == NULL || dest_fp == NULL) {perror("Failed to open log file");if (src_fp != NULL) fclose(src_fp);if (dest_fp != NULL) fclose(dest_fp);return -1;}char buf[BUF_SIZE];while (fgets(buf, BUF_SIZE, src_fp) != NULL) {if (strstr(buf, "ERROR") != NULL) {if (fputs(buf, dest_fp) == EOF) {perror("Failed to write error log");fclose(src_fp);fclose(dest_fp);return -1;}}}if (ferror(src_fp)) {perror("Failed to read log file");fclose(src_fp);fclose(dest_fp);return -1;}fclose(src_fp);fclose(dest_fp);printf("Error log filtered successfully\n");return 0;
}int main(int argc, char *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s\n", argv[0]);exit(EXIT_FAILURE);}if (filter_error_log(argv[1], argv[2]) != 0) {exit(EXIT_FAILURE);}return EXIT_SUCCESS;
}

测试脚本调整

# 创建测试日志文件 app.log
cat > app.log << EOF
2024-09-28 10:00:00 INFO: Application started
2024-09-28 10:05:00 ERROR: Database connection failed
2024-09-28 10:10:00 INFO: Retrying connection
2024-09-28 10:15:00 ERROR: Timeout waiting for DB
EOF# 编译并运行程序
gcc filter_log.c -o filter_log
./filter_log app.log error.log# 查看筛选结果
cat error.log

Error log filtered successfully 2024-09-28 10:05:00 ERROR: Database connection failed 2024-09-28 10:15:00 ERROR: Timeout waiting for DB

关键注意点

  • fgets 缓冲区大小需合理设置:若缓冲区过小(如小于一行长度),会分多次读取该行,需通过检查缓冲区末尾是否为 \n 判断是否读取完整行;
  • fputs 与 puts 的换行符差异:本例中 buf 已包含 fgets 读取的 \n,故用 fputs 直接写入;若用 puts,会导致一行末尾出现两个 \n
  • 追加模式的使用:目标日志用 "a" 模式打开,确保新筛选的错误行追加到文件末尾,而非覆盖原有内容。

四、按块读写函数族:fread 与 fwrite 实战

按块读写函数是无格式读写中效率最高的类别,以“数据块”为单位读写数据(如每次读写 1024 字节),减少 IO 操作次数,适用于读写二进制文件(如图片、视频、可执行文件)或大量文本数据。核心函数是 fread(读块)和 fwrite(写块),二者参数结构对称,使用逻辑一致。

1. 核心函数解析

函数原型功能说明参数含义返回值适用场景
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);从文件流 stream 中读取 nmemb 个“大小为 size 的数据块”,存储到 ptr 指向的缓冲区ptr:接收数据的缓冲区指针;
size:单个数据块的大小(字节);
nmemb:要读取的数据块数量;
stream:文件流指针
成功:返回实际读取的 数据块数量
失败/EOF:返回小于 nmemb 的值(可能为 0)
1. 读写二进制文件(如图片、结构体数据);
2. 批量读写文本文件(效率高于字符/行读写)
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);将 ptr 缓冲区中 nmemb 个“大小为 size 的数据块”写入文件流 streamptr:待写入数据的缓冲区指针;
其他参数同 fread
成功:返回实际写入的 数据块数量(通常等于 nmemb);
失败:返回小于 nmemb 的值
1. 写入二进制文件(如保存结构体实例);
2. 批量写入大量数据(如日志批量刷盘)

返回值关键认知fread 和 fwrite 的返回值是“实际处理的数据块数量”,而非字节数。计算实际读写的字节数需用“返回值 × size”。例如,fread(buf, 1024, 5, fp) 若返回 3,表示实际读取了 3×1024=3072 字节。

2. 实战案例:用 fread 和 fwrite 读写二进制文件(结构体存储)

需求:编写程序 student_bin.c,定义 Student 结构体,将 3 个学生信息以二进制形式写入 students.bin,再从该文件中读取并打印学生信息。

以下是整理后的C语言代码,已按照规范进行缩进和格式优化:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 定义学生结构体
typedef struct {char name[50];  // 姓名int age;        // 年龄float score;    // 成绩
} Student;// 函数:写入学生信息到二进制文件
int write_students(const char *file_path, const Student *students, int count) {FILE *fp = fopen(file_path, "wb");  // 二进制写入模式if (fp == NULL) {perror("Failed to open file for writing");return -1;}// 按块写入:每个块是1个Student结构体,共count个块size_t written = fwrite(students, sizeof(Student), count, fp);if (written != count) {perror("Failed to write students");fclose(fp);return -1;}fclose(fp);printf("Wrote %d students to binary file\n", count);return 0;
}// 函数:从二进制文件读取学生信息
int read_students(const char *file_path, Student *students, int max_count) {FILE *fp = fopen(file_path, "rb");  // 二进制读取模式if (fp == NULL) {perror("Failed to open file for reading");return -1;}// 按块读取:最多读取max_count个Student结构体size_t read = fread(students, sizeof(Student), max_count, fp);if (ferror(fp)) {perror("Failed to read students");fclose(fp);return -1;}fclose(fp);printf("Read %zu students from binary file\n", read);return (int)read;  // 返回实际读取的学生数量
}int main() {// 1. 准备学生数据Student students_write[] = {{"Alice", 20, 95.5},{"Bob", 21, 88.0},{"Charlie", 19, 92.3}};int student_count = sizeof(students_write) / sizeof(Student);// 2. 写入二进制文件if (write_students("students.bin", students_write, student_count) != 0) {exit(EXIT_FAILURE);}// 3. 读取二进制文件Student students_read[10];  // 缓冲区,最多存储10个学生int read_count = read_students("students.bin", students_read, 10);if (read_count == -1) {exit(EXIT_FAILURE);}// 4. 打印读取的学生信息printf("\nStudent Information:\n");printf("-------------------------\n");for (int i = 0; i < read_count; i++) {printf("Name: %s\n", students_read[i].name);printf("Age: %d\n", students_read[i].age);printf("Score: %.1f\n", students_read[i].score);printf("-------------------------\n");}return EXIT_SUCCESS;
}

编译运行说明

# 编译程序
gcc student_bin.c -o student_bin# 运行程序
./student_bin# 查看二进制文件大小(3个Student结构体)
ls -l students.bin

程序输出示例

Wrote 3 students to binary file
Read 3 students from binary fileStudent Information:
-------------------------
Name: Alice
Age: 20
Score: 95.5
-------------------------
Name: Bob
Age: 21
Score: 88.0
-------------------------
Name: Charlie
Age: 19
Score: 92.3
-------------------------
-rw-r--r-- 1 user user 174 Sep 28 16:30 students.bin

关键点说明

  • 结构体定义采用4空格缩进
  • 函数体和代码块使用4空格缩进
  • 二进制文件读写使用fwrite/fread的块操作模式
  • 错误处理包含文件打开失败和读写失败的检查
  • 主逻辑清晰分为写文件和读文件两个阶段

关键注意点

  • 必须用二进制模式打开文件:写入用 "wb",读取用 "rb",避免文本模式下的“换行符转换”和“EOF 处理”(如 0x1A 被视为 EOF)破坏二进制数据;
  • 结构体对齐问题:不同编译器的结构体对齐方式可能不同(如是否填充字节),若需跨平台读写,需统一结构体对齐方式(如用 #pragma pack(1) 取消对齐);
  • 字符串处理:结构体中的字符数组 name 需确保以 \0 结束,避免打印时出现乱码(本例中初始化时已隐含 \0)。

五、常见错误与解决方法

使用无格式读写函数时,易因缓冲区操作、返回值判断、文件模式设置等问题导致程序异常。以下是高频错误及对应的解决方法:

常见错误问题现象原因分析解决方法
缓冲区溢出(gets 函数)程序崩溃、数据错乱,甚至被注入恶意代码使用 gets 函数读取输入,该函数无缓冲区大小限制,输入长度超过缓冲区时会覆盖相邻内存1. 彻底禁止使用 gets,改用 fgets(str, size, stdin)
2. 若需读取标准输入,确保 fgetssize 等于缓冲区实际大小
fgetc 返回值用 char 接收读取文本文件时提前结束,或读取二进制文件时丢失数据fgetcEOF(-1),若用 char 接收(signed char),会与值为 0xFF 的字符混淆,导致误判为 EOF必须用 int 类型接收 fgetcchar 使用
fread/fwrite 返回值判断错误读写不完整数据,或忽略读写错误误将 fread/fwritenmemb1. 明确返回值是“数据块数量”,计算字节数需用“返回值 × size”;
2. 读写后必须检查返回值:若小于 nmemb,调用 ferror 判断是否为错误(非 EOF)
文本模式与二进制模式混淆二进制文件读写后损坏(如图片无法打开),或换行符显示异常1. 读写二进制文件时用文本模式("r"/"w"),导致换行符转换(如 \r\n 变为 \n)或 EOF 处理(0x1A 截断);
2. 读写文本文件时用二进制模式,导致换行符不转换(Windows 下 \r\n 显示为 ^M
1. 读写二进制文件:必须用 "rb"/"wb"/"ab" 模式;
2. 读写文本文件:用 "r"/"w"/"a" 模式(跨平台时需注意换行符差异)
文件流未关闭导致内存泄漏程序长期运行后内存占用增长,或数据未刷盘(缓冲区数据丢失)1. 打开文件后未调用 fclose 关闭流,导致文件描述符和缓冲区内存泄漏;
2. 程序异常退出前未关闭流,缓冲区中未刷盘的数据丢失
1. 遵循“打开即关闭”原则,用 fopen 打开后,确保所有分支(包括错误分支)都调用 fclose
2. 关键数据写入后,可调用 fflush 强制刷盘(避免依赖缓冲区自动刷盘)

六、拓展:文件流的缓冲机制

UNIX 标准 IO 库的文件流默认启用缓冲机制,即读写操作不直接与磁盘交互,而是先通过内存缓冲区暂存数据,当缓冲区满、遇到特定字符(如行缓冲的 \n)或调用 fflush 时,才将缓冲区数据刷写到磁盘(或从磁盘读取到缓冲区)。缓冲机制大幅减少了磁盘 IO 次数,是标准 IO 库高效的核心原因。

1. 三种缓冲类型

1. 全缓冲(Full Buffering)

适用场景:普通磁盘文件(如 test.txt)。

缓冲逻辑:缓冲区满(通常 4KB 或 8KB)或调用 fflushfclose 时,才刷写数据到磁盘;读取时缓冲区空则从磁盘读取一批数据填充缓冲区。

示例:用 fwrite 写入少量数据时,数据先存于缓冲区,需 fflush 才能立即在磁盘上看到。

2. 行缓冲(Line Buffering)

适用场景:标准输入(stdin)、标准输出(stdout,终端输出时)。

缓冲逻辑:遇到换行符 \n、缓冲区满或调用 fflush 时,刷写数据;读取时遇到 \n 或缓冲区空则填充。

示例printf("Hello\n") 会立即输出(因 \n 触发刷写),而 printf("Hello") 需 fflush(stdout) 才输出。

3. 无缓冲(Unbuffered)

适用场景:标准错误(stderr)、特殊设备文件(如 /dev/tty)。

缓冲逻辑:读写操作直接与设备交互,无缓冲区暂存,数据立即传递。

示例fprintf(stderr, "Error!") 会立即显示错误信息,无需等待缓冲,确保错误信息及时输出。

2. 缓冲机制的控制函数

可通过以下函数修改文件流的缓冲类型或手动控制缓冲,适应特殊需求:

函数原型功能说明参数含义示例
void setbuf(FILE *stream, char *buf);设置文件流的缓冲区:buf 非 NULL 则为全缓冲(缓冲区大小 BUFSIZ);buf 为 NULL 则无缓冲stream:文件流;
buf:自定义缓冲区(需至少 BUFSIZ 字节)
char buf[BUFSIZ]; setbuf(fp, buf);(全缓冲);
setbuf(fp, NULL);(无缓冲)
int setvbuf(FILE *stream, char *buf, int mode, size_t size);灵活设置缓冲类型和缓冲区大小,功能最全面mode_IOFBF(全缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲);
size:缓冲区大小
setvbuf(fp, NULL, _IOLBF, 0);(行缓冲,默认大小)
int fflush(FILE *stream);强制刷写文件流的输出缓冲区(将缓冲区数据写入磁盘);对输入流无效果stream:文件流;NULL 表示刷写所有输出流fflush(stdout);(刷写标准输出);
fflush(NULL);(刷写所有输出流)

缓冲机制的常见坑点

  • stdout 重定向后的缓冲类型变化:stdout 输出到终端时是行缓冲,重定向到文件时变为全缓冲,导致 printf("Hello") 无 \n 时,数据存于缓冲区,需 fflush 才写入文件;
  • 自定义缓冲区的生命周期:用 setbuf 设置自定义缓冲区时,缓冲区必须在文件流关闭前保持有效(不能是局部变量,否则函数返回后缓冲区被释放,导致数据错乱);
  • 无缓冲的性能影响:对频繁读写的文件使用无缓冲(如 setbuf(fp, NULL)),会导致大量磁盘 IO,显著降低性能,仅在实时性要求极高的场景(如日志实时输出)使用。

本文详细讲解了 UNIX 标准文件编程库中三类无格式读写函数族的使用,从函数语法、实战案例,到常见错误和缓冲机制,覆盖了无格式 IO 的核心知识点。按字符读写适用于精细字符处理,按行读写适用于文本文件逐行解析,按块读写适用于高效批量数据处理,需根据实际场景选择合适的函数。

掌握无格式读写函数是 UNIX 文件编程的基础,建议结合实际需求多做实践(如实现文件加密、数据备份工具),深入理解缓冲机制和返回值判断逻辑,避免常见错误,编写高效、健壮的文件操作代码。

http://www.dtcms.com/a/426721.html

相关文章:

  • mysql中的日志
  • Spring Cloud Nacos 配置中心详解:从基础使用到 MyBatis 整合(含多文档配置)
  • 去出海做产品吧,亚马逊爆款产品 属于电子类的消费产品。用全志A733完胜--
  • 设计配色网站租房合同范本下载word
  • 安卓生态进化史:从手机系统到全场景智能
  • 自适应网站开发工具网站优化排名提升
  • 中国建材网:重构建材行业生态的数字力量
  • 【有源码】基于Hadoop+Spark的豆瓣电影数据分析与可视化系统-基于大数据的电影评分趋势分析与可视化系统
  • 模板匹配算法原理
  • Matplotlib子图布局与响应式设计实战:GridSpec与CSS框架深度结合
  • 【图像处理进阶】边缘检测算法深度优化与复杂场景实战
  • yolov12 onnx导出tensorrt
  • 【Java学习】定时器Timer(源码详解)
  • 【数据结构】二叉树的数组表示推导
  • 前端版本更新,错误监控,解决方案 error / unhandledrejection,同步异步错误监控方案
  • 2023 美赛C Predicting Wordle Results(上)
  • 微退休(Micro-retirement)介绍
  • LeetCode热题100(1-7)
  • 想让图片可以在Word和WPS文档中自由移动?修改文字环绕
  • 连云港网站设计北京seo优化分析
  • PostgreSQL WAL 日志发展史 - pg9
  • 企业自有网站全国加盟网站大全
  • 做金融网站看那些素材怎样联系自己建设网站
  • Java的任务调度框架之 Quartz 以及 CronTrigger,CronScheduleBuilder 和 Cron表达式 笔记250930
  • 联想乐享重构智能搜索生态:ThinkPad T14p 2025升级信息首触“企业智能双胞胎”
  • 明远智睿 SSD2351 核心板:64 位四核含税不足 50 元,批量采购新选择
  • Flutter 自定义 View 权威指引
  • AWS | Linux 硬盘挂载综合教程
  • ntdll.pdb 包含查找模块 ntdll.dll 的源文件所需的调试信息
  • 精读C++20设计模式——行为型设计模式:策略模式