UNIX下C语言编程与实践24-UNIX 标准文件编程库:无格式读写函数族(字符、行、块)的使用
深入解析 UNIX 标准 IO 库中三类无格式读写函数,从语法到实战全覆盖
一、无格式读写函数族概述:何为“无格式”?
在 UNIX 标准文件编程库(stdio.h
提供)中,文件读写分为“有格式读写”和“无格式读写”两类。其中“无格式读写”指函数不解析数据的格式(如不将字符串转换为整数、不处理格式化符号),直接按原始数据形式(字符、字节流)与文件交互,核心优势是高效、通用,适用于文本文件和二进制文件。
无格式读写函数族按数据处理粒度,分为三类:
- 按字符读写:以单个字符(1 字节)为单位读写数据,如
fgetc
、fputc
; - 按行读写:以“行”为单位读写文本数据(以换行符
\n
为行结束标志),如fgets
、fputs
; - 按块读写:以自定义大小的“数据块”(如 1024 字节)为单位读写数据,如
fread
、fwrite
,是效率最高的无格式读写方式。
核心认知:所有无格式读写函数均基于“文件流(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 | 同 fgetc | 1. 是宏定义,而非函数; 2. 执行速度略快(无函数调用开销),但不可作为函数指针传递 |
#define putc(c, stream) (...) | 与 fputc 功能完全一致,写入一个字符 | 同 fputc | 同 fputc | 1. 是宏定义; 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
的差异); - 必须检查
ferror
:fgetc
返回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 ,不自动添加换行符 \n | str :待写入的字符串(需以 \0 结束);stream :文件流指针 | 成功:返回非负值; 失败:返回 EOF | 1. 写入时不会自动添加 \n ,需手动在字符串末尾添加(如 "line\n" );2. 仅写入 \0 前的内容,\0 不写入文件 |
char *gets(char *str); | 从标准输入(stdin)读取一行文本,存储到 str | str :缓冲区指针 | 成功:返回 str ;失败/EOF:返回 NULL | 1. 严重不安全:无缓冲区大小限制,输入过长会导致缓冲区溢出,已从 C11 标准中移除; 2. 禁止使用,必须用 fgets(str, size, stdin) 替代 |
int puts(const char *str); | 将字符串 str 写入标准输出(stdout),自动在末尾添加 \n | str :待写入的字符串 | 成功:返回非负值; 失败:返回 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 的数据块”写入文件流 stream | ptr :待写入数据的缓冲区指针;其他参数同 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. 若需读取标准输入,确保 fgets size 等于缓冲区实际大小 |
fgetc 返回值用 char 接收 | 读取文本文件时提前结束,或读取二进制文件时丢失数据 | fgetc EOF(-1),若用 char 接收(signed char),会与值为 0xFF 的字符混淆,导致误判为 EOF | 必须用 int 类型接收 fgetc char 使用 |
fread/fwrite 返回值判断错误 | 读写不完整数据,或忽略读写错误 | 误将 fread/fwrite nmemb | 1. 明确返回值是“数据块数量”,计算字节数需用“返回值 × 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)或调用 fflush
、fclose
时,才刷写数据到磁盘;读取时缓冲区空则从磁盘读取一批数据填充缓冲区。
示例:用 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 文件编程的基础,建议结合实际需求多做实践(如实现文件加密、数据备份工具),深入理解缓冲机制和返回值判断逻辑,避免常见错误,编写高效、健壮的文件操作代码。