
在 C 语言文件操作中,二进制文件读写是处理非文本数据(如图片、音频、视频、结构体)的核心技术。相比文本文件的格式化读写(fprintf/fscanf),二进制读写以 “原始字节流” 为操作单位,跳过数据转换环节,具备更高的效率和更强的通用性。
二进制文件是数据以内存原始存储格式直接存储的文件,不进行任何字符编码或格式转换。例如:
int类型的数值1024,在内存中以 4 字节(32 位系统)存储为0x00000400,写入二进制文件后仍保持该字节序列;
1024转换为字符'1'、'0'、'2'、'4'(对应 ASCII 码0x31、0x30、0x32、0x34)存储,读取时需反向转换。
对比维度 | 二进制读写(fread/fwrite) | 文本读写(fprintf/fscanf) |
|---|---|---|
数据存储形式 | 原始字节流,与内存布局完全一致 | 字符编码形式,可能转换换行符(\n↔\r\n) |
读写效率 | 高:无数据转换,直接 IO 操作 | 低:需格式化转换,额外 CPU 开销 |
处理数据类型 | 通用:二进制数据、结构体、文本均可 | 局限:仅适用于文本数据,需格式化声明 |
跨平台兼容性 | 需处理字节序、结构体对齐问题 | 换行符转换可能导致兼容性问题 |
适用场景 | 多媒体文件、大文件、结构体序列化 | 配置文件、日志文件、人类可读文本 |
可视化流程图:二进制读写的极简流程

文本读写的复杂流程

fread()是 C 语言标准库(<stdio.h>)提供的二进制读取函数,核心优势在于精确控制字节读取:
int、float等基本类型,还是自定义结构体、大型字节数组,均可直接读取;
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);参数拆解(逐字段说明):
参数名 | 类型 | 作用与注意事项 |
|---|---|---|
ptr | void * | 指向存储读取数据的缓冲区指针,需预先分配足够空间(如数组、结构体变量、动态内存); void*类型支持任意数据类型,使用时需强制转换(如(int*)ptr)。 |
size | size_t | 每个数据项的字节大小(必须用sizeof()计算,避免硬编码),如sizeof(int)、sizeof(User)。 |
nmemb | size_t | 计划读取的数据项个数,总读取字节数 = size * nmemb。 |
stream | FILE * | 已打开的文件指针,必须以二进制读取模式打开(如"rb"),否则可能出现数据错乱。 |
返回值核心说明:
nmemb:
nmemb,表示所有数据项均读取成功;
nmemb的非零值(如计划读 10 个 int,实际读 3 个);
ferror(stream)判断错误类型)。
fread()的底层本质是 “从文件流中复制字节到缓冲区”,简化伪代码如下,帮助理解核心流程:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
// 1. 参数合法性校验:空指针、无效大小直接返回0
if (ptr == NULL || stream == NULL || size == 0 || nmemb == 0) {
return 0;
}
// 2. 计算总需读取的字节数
size_t total_bytes = size * nmemb;
// 3. 转换为unsigned char*,确保逐字节操作(避免类型对齐问题)
unsigned char *buf = (unsigned char *)ptr;
size_t bytes_read = 0; // 已读取的字节数
// 4. 循环读取字节:处理部分读取场景(如IO中断、文件末尾)
while (bytes_read < total_bytes) {
// 底层系统调用:从文件流读取部分字节(模拟OS IO接口)
size_t read_now = sys_read(stream->fd, buf + bytes_read, total_bytes - bytes_read);
if (read_now == 0) { // 读取到0字节:文件末尾或错误
break;
}
bytes_read += read_now;
}
// 5. 处理读取结果:设置文件流状态(错误/EOF)
if (bytes_read % size != 0) { // 读取字节数不是size的整数倍:数据不完整
stream->error = 1;
} else if (bytes_read < total_bytes && !stream->eof) { // 未到末尾但读取中断:错误
stream->error = 1;
}
// 6. 返回实际读取的数据项个数(总字节数 / 每个数据项大小)
return bytes_read / size;
}场景 1:读取二进制多媒体文件(如图片、音频)
多媒体文件(.png、.mp3)本身就是二进制格式,需逐字节读取后处理(如传输、解码):
// 读取PNG图片文件到内存
unsigned char *read_png(const char *path, long *file_size) {
FILE *fp = fopen(path, "rb");
if (fp == NULL) {
perror("fopen failed");
return NULL;
}
// 1. 获取文件大小(移动到文件末尾)
fseek(fp, 0, SEEK_END);
*file_size = ftell(fp);
fseek(fp, 0, SEEK_SET); // 恢复文件指针到开头
// 2. 动态分配缓冲区(存储整个图片)
unsigned char *png_data = (unsigned char *)malloc(*file_size);
if (png_data == NULL) {
fclose(fp);
return NULL;
}
// 3. 读取全部字节:size=1(每个数据项1字节),nmemb=file_size
size_t read = fread(png_data, 1, *file_size, fp);
if (read != *file_size) {
perror("fread failed");
free(png_data);
png_data = NULL;
}
fclose(fp);
return png_data;
}场景 2:结构体序列化与反序列化(数据持久化)
在嵌入式系统、网络编程中,常将结构体数据写入文件保存(序列化),后续读取恢复(反序列化):
// 定义传感器数据结构体
typedef struct {
int sensor_id; // 传感器ID
float temperature; // 温度值
long timestamp; // 采集时间戳
} SensorData;
// 从文件读取多个传感器数据
SensorData *read_sensor_data(const char *path, int *count) {
FILE *fp = fopen(path, "rb");
if (fp == NULL) return NULL;
// 1. 计算文件中结构体个数
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
*count = file_size / sizeof(SensorData);
fseek(fp, 0, SEEK_SET);
// 2. 分配内存
SensorData *data = (SensorData *)malloc(*count * sizeof(SensorData));
if (data == NULL) {
fclose(fp);
return NULL;
}
// 3. 批量读取结构体
size_t read = fread(data, sizeof(SensorData), *count, fp);
if (read != *count) {
perror("read sensor data failed");
free(data);
data = NULL;
}
fclose(fp);
return data;
}场景 3:大文件分块读取(避免内存溢出)
处理 GB 级大文件时,无法一次性加载到内存,需用固定大小缓冲区分块读取:
#define BUF_SIZE 1024*1024 // 1MB缓冲区
void process_large_file(const char *path) {
FILE *fp = fopen(path, "rb");
if (fp == NULL) return;
unsigned char buf[BUF_SIZE];
size_t read_bytes;
// 循环分块读取,直到文件末尾
while ((read_bytes = fread(buf, 1, BUF_SIZE, fp)) > 0) {
// 处理当前块数据(如解析、过滤、写入其他文件)
process_block(buf, read_bytes);
// 检查读取错误
if (ferror(fp)) {
perror("fread error");
break;
}
}
fclose(fp);
}"rb"(Windows 系统中"r"会转换\r\n为\n,导致二进制数据错乱;Linux/Unix 系统中"r"与"rb"差异较小,但建议统一用"rb"保证兼容性)。
size * nmemb,否则会导致内存溢出(如计划读取 10 个int(40 字节),缓冲区仅分配 30 字节,会覆盖后续内存数据)。
size_t read = fread(data, sizeof(int), 10, fp);
if (read < 10) {
if (feof(fp)) {
printf("警告:未读取到10个数据,已到达文件末尾\n");
} else if (ferror(fp)) {
perror("错误:读取文件失败");
}
} 4. 结构体对齐问题:直接读写结构体时,编译器可能会为结构体成员添加填充字节(如struct {char a; int b;}在 32 位系统中大小 为 8 字节,而非 5 字节),导致跨编译器 / 平台读写错乱。解决方案:
#pragma pack(1) // 强制1字节对齐(取消填充)
typedef struct {
char a;
int b;
} AlignTest;
#pragma pack() // 恢复默认对齐 5. 字节序兼容:不同 CPU 架构(x86 为小端,ARM 可配置为大端)存储多字节数据(如int)的字节顺序不同,跨平台读取需统一字节序:
// 读取大端字节序的int值(如网络传输的二进制数据)
int read_big_endian_int(FILE *fp) {
unsigned char buf[4];
fread(buf, 1, 4, fp);
// 大端转小端:buf[0]是高位字节,buf[3]是低位字节
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}6. 文件指针位置:fread()会移动文件指针(指向下次读取的位置),若需重复读取,需用fseek()或rewind()重置指针。
7. 动态内存需释放:若缓冲区是malloc分配的,读取完成后必须free,避免内存泄漏。
8. 权限检查:读取前需确保文件存在且有读权限(Linux 下用access(path, R_OK)检查)。
9. 避免混合读写:同一文件流中,若同时使用fread()和fwrite(),需用fflush()或fseek()刷新 / 定位指针,否则可能出现数据错乱。
10. 32 位系统大文件支持:32 位系统中size_t最大为 4GB,读取超过 4GB 的文件需用fseeko()和ftello()(支持 64 位偏移量)。
以下示例实现 “读取二进制文件中的整数数组,计算平均值”,涵盖参数校验、错误处理、资源释放:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DATA_COUNT 100 // 计划读取的整数个数
int main() {
const char *filename = "numbers.bin";
FILE *fp = NULL;
int *data = NULL;
size_t read_count = 0;
int sum = 0;
float avg = 0.0f;
// 1. 分配缓冲区:存储100个int
data = (int *)malloc(DATA_COUNT * sizeof(int));
if (data == NULL) {
fprintf(stderr, "malloc failed: 内存分配失败\n");
return EXIT_FAILURE;
}
memset(data, 0, DATA_COUNT * sizeof(int)); // 初始化缓冲区
// 2. 以二进制读取模式打开文件
fp = fopen(filename, "rb");
if (fp == NULL) {
perror("fopen failed");
free(data);
return EXIT_FAILURE;
}
// 3. 读取数据:每个数据项4字节(sizeof(int)),共100个
read_count = fread(data, sizeof(int), DATA_COUNT, fp);
if (read_count == 0) {
perror("fread failed");
fclose(fp);
free(data);
return EXIT_FAILURE;
}
// 4. 处理数据:计算平均值
for (size_t i = 0; i < read_count; i++) {
sum += data[i];
}
avg = (float)sum / read_count;
printf("成功读取%d个整数,平均值:%.2f\n", (int)read_count, avg);
// 5. 检查是否读取到文件末尾
if (feof(fp)) {
printf("提示:已到达文件末尾,实际读取个数:%zu(计划:%d)\n", read_count, DATA_COUNT);
}
// 6. 释放资源(顺序:先关文件,再释放内存)
fclose(fp);
free(data);
data = NULL; // 避免野指针
return EXIT_SUCCESS;
}
fwrite()与fread()是配对函数,用于将内存中的原始数据以字节流形式写入文件,核心特点:
fprintf那样指定格式符(如%d、%f),直接写入内存数据;
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);参数拆解(与 fread 对比):
参数名 | 类型 | 作用与注意事项 |
|---|---|---|
ptr | const void * | 指向待写入数据的缓冲区指针,const表示缓冲区数据只读(避免写入时修改源数据)。 |
size | size_t | 每个数据项的字节大小(同 fread,需用sizeof()计算)。 |
nmemb | size_t | 计划写入的数据项个数,总写入字节数 = size * nmemb。 |
stream | FILE * | 已打开的文件指针,必须以二进制写入模式打开(如"wb":覆盖写入;"ab":追加写入)。 |
返回值核心说明:
nmemb:
nmemb,表示所有数据均写入成功;
nmemb的值(如磁盘空间不足、权限不足),需通过ferror(stream)判断错误类型。
fwrite()的底层是 “从缓冲区复制字节到文件流”,简化伪代码如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
// 1. 参数合法性校验
if (ptr == NULL || stream == NULL || size == 0 || nmemb == 0) {
return 0;
}
// 2. 计算总需写入的字节数
size_t total_bytes = size * nmemb;
const unsigned char *buf = (const unsigned char *)ptr; // 只读缓冲区
size_t bytes_written = 0; // 已写入的字节数
// 3. 循环写入:处理部分写入场景(如磁盘满、IO中断)
while (bytes_written < total_bytes) {
size_t write_now = sys_write(stream->fd, buf + bytes_written, total_bytes - bytes_written);
if (write_now == 0) { // 写入失败
stream->error = 1;
break;
}
bytes_written += write_now;
}
// 4. 返回实际写入的数据项个数
return bytes_written / size;
}场景 1:写入结构体数据到文件(数据持久化)
将程序运行时的结构体数据写入文件,下次启动时读取恢复:
// 保存用户配置到二进制文件
typedef struct {
int screen_width; // 屏幕宽度
int screen_height; // 屏幕高度
int volume; // 音量
int fullscreen; // 是否全屏(0/1)
} Config;
int save_config(const Config *config, const char *path) {
if (config == NULL || path == NULL) return -1;
FILE *fp = fopen(path, "wb");
if (fp == NULL) {
perror("fopen failed");
return -1;
}
// 写入整个结构体
size_t written = fwrite(config, sizeof(Config), 1, fp);
if (written != 1) {
perror("fwrite failed");
fclose(fp);
return -1;
}
fclose(fp);
return 0;
}
// 使用示例
Config user_config = {1920, 1080, 80, 1};
save_config(&user_config, "config.bin");场景 2:网络数据保存到本地(二进制协议)
网络编程中,接收的二进制协议数据(如 TCP/UDP 数据包)需保存到文件:
// 保存网络接收的二进制数据到文件
void save_network_data(const unsigned char *data, size_t len, const char *path) {
FILE *fp = fopen(path, "ab"); // 追加模式:避免覆盖已有数据
if (fp == NULL) {
perror("fopen failed");
return;
}
size_t written = fwrite(data, 1, len, fp);
if (written != len) {
perror("fwrite failed");
fclose(fp);
return;
}
fflush(fp); // 强制刷新缓冲区:确保数据立即写入磁盘
fclose(fp);
}场景 3:生成二进制格式的日志文件(高效存储)
相比文本日志,二进制日志体积更小、写入更快,适合高并发场景:
// 二进制日志结构体
typedef struct {
long timestamp; // 时间戳(毫秒)
int log_level; // 日志级别(0=DEBUG,1=INFO,2=ERROR)
char message[256]; // 日志内容
} BinaryLog;
// 写入二进制日志
void write_binary_log(const BinaryLog *log, const char *log_path) {
FILE *fp = fopen(log_path, "ab");
if (fp == NULL) return;
// 批量写入(支持一次写入多个日志条目)
fwrite(log, sizeof(BinaryLog), 1, fp);
fflush(fp); // 高并发场景需强制刷新,避免日志丢失
fclose(fp);
}1. 文件打开模式选择:
"wb"(若文件已存在,清空原有内容);
"ab"(在文件末尾添加数据,保留原有内容);
"rb+"(读取并写入,不清空原有内容),需注意文件指针位置。
2. 数据类型一致性:写入的数据类型需与读取时一致(如写入int,读取时也需用int),否则会出现数据解析错误(如用long读取int数据,可能读取多余字节)。
3. 缓冲区刷新:fwrite()会使用系统缓冲区,数据可能未立即写入磁盘(缓存于内存),若程序异常退出会导致数据丢失。解决方案:
fflush(fp)强制刷新;
setvbuf(fp, NULL, _IONBF, 0)设置无缓冲模式(效率较低,慎用)。
4. 磁盘空间检查:写入大文件前,可通过系统调用检查磁盘剩余空间(如 Linux 的statvfs),避免因磁盘满导致写入失败。
5. 字符串写入二进制文件:字符串在 C 语言中以'\0'结尾,若直接用fwrite写入,会包含'\0'字符(占 1 字节)。若需存储纯字符串内容,应使用strlen(data)计算有效长度:
char *str = "Hello, Binary!";
// 正确:写入字符串有效长度(不含'\0')
fwrite(str, 1, strlen(str), fp);6. 跨平台换行符:二进制模式下写入'\n'不会自动转换为"\r\n"(Windows 文本模式会转换),若需兼容 Windows 文本读取,需手动处理。
以下示例实现 “写入结构体数组到文件,再读取并验证数据一致性”,涵盖完整的读写闭环:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
// 定义学生信息结构体(设置1字节对齐,确保跨平台兼容)
#pragma pack(1)
typedef struct {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
} Student;
#pragma pack()
#define STUDENT_COUNT 3 // 学生人数
int main() {
const char *filename = "students.bin";
FILE *fp = NULL;
Student students[STUDENT_COUNT] = {
{101, "Zhang San", 95.5f},
{102, "Li Si", 88.0f},
{103, "Wang Wu", 92.3f}
};
Student read_students[STUDENT_COUNT] = {0};
size_t written, read;
// -------------------------- 第一步:写入数据 --------------------------
fp = fopen(filename, "wb");
if (fp == NULL) {
perror("fopen for write failed");
return EXIT_FAILURE;
}
// 写入3个学生结构体
written = fwrite(students, sizeof(Student), STUDENT_COUNT, fp);
if (written != STUDENT_COUNT) {
perror("fwrite failed");
fclose(fp);
return EXIT_FAILURE;
}
printf("成功写入%d个学生数据\n", (int)written);
fflush(fp); // 强制刷新缓冲区
fclose(fp);
fp = NULL;
// -------------------------- 第二步:读取数据 --------------------------
fp = fopen(filename, "rb");
if (fp == NULL) {
perror("fopen for read failed");
return EXIT_FAILURE;
}
// 读取3个学生结构体
read = fread(read_students, sizeof(Student), STUDENT_COUNT, fp);
if (read != STUDENT_COUNT) {
perror("fread failed");
fclose(fp);
return EXIT_FAILURE;
}
printf("成功读取%d个学生数据\n", (int)read);
// -------------------------- 第三步:验证数据一致性 --------------------------
for (int i = 0; i < STUDENT_COUNT; i++) {
assert(read_students[i].id == students[i].id);
assert(strcmp(read_students[i].name, students[i].name) == 0);
assert(read_students[i].score - students[i].score < 1e-6); // float精度允许误差
printf("学生%d:ID=%d, 姓名=%s, 成绩=%.1f\n",
i+1, read_students[i].id, read_students[i].name, read_students[i].score);
}
fclose(fp);
printf("数据写入与读取一致,验证通过!\n");
return EXIT_SUCCESS;
}
对比维度 | fread () 函数 | fwrite () 函数 |
|---|---|---|
核心功能 | 从文件流→缓冲区(读取) | 从缓冲区→文件流(写入) |
指针参数属性 | void *(可写:接收数据) | const void *(只读:提供数据) |
触发的 IO 方向 | 输入 IO(从外部文件到程序内存) | 输出 IO(从程序内存到外部文件) |
典型错误原因 | 文件不存在、权限不足、文件末尾 | 磁盘空间不足、权限不足、文件只读 |
缓冲区要求 | 需预先分配足够空间,避免溢出 | 缓冲区需包含有效数据,避免访问越界 |
数据依赖 | 依赖文件中已存在的数据 | 依赖内存中已准备好的数据 |
常见搭配函数 | fopen("rb")、feof()、ferror() | fopen("wb"/"ab")、fflush()、fclose() |
原因:
"r"代替"rb");
int,读取时用float);
解决方案:
"rb"/"wb"模式;
#pragma pack(1)设置结构体对齐;
原因:
解决方案:
chmod +w);
fflush();
原因:
long在 32 位系统是 4 字节,64 位是 8 字节)。
解决方案:
int32_t、uint64_t代替int、long);
fread()和fwrite()是 C 语言二进制文件操作的基石,其核心价值在于高效、精确地处理原始字节数据,适用于多媒体文件、结构体序列化、大文件处理等文本读写无法覆盖的场景。掌握这两个函数的关键在于:
在实际开发中,二进制读写常与内存管理、IO 优化、跨平台适配结合,是嵌入式开发、后台开发、游戏开发等领域的高频考点。熟练运用这两个函数,能显著提升数据存储与传输的效率,避免常见的 IO 错误。
面试题 1:请简述 C 语言中 fread 函数的参数含义、返回值及实际使用场景。(字节跳动 2023 年嵌入式工程师面试题)
答案:
1. 函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
2. 参数含义:
ptr:指向存储读取数据的缓冲区指针,需预先分配足够空间;
size:每个数据项的字节大小(如sizeof(int)),需用sizeof()计算;
nmemb:计划读取的数据项个数,总读取字节数 = size * nmemb;
stream:已以二进制模式(如"rb")打开的文件指针。
3. 返回值:实际读取的数据项个数,可能小于nmemb(到达文件末尾或错误),返回 0 表示读取失败。
4. 实际场景:读取二进制多媒体文件(图片 / 音频)、结构体反序列化、大文件分块读取。
面试题 2:C 语言中二进制读写(fread/fwrite)与文本读写(fprintf/fscanf)的核心区别是什么?何时选择二进制读写?(腾讯 2024 年后台开发面试题)
答案:
1. 核心区别:
2. 适用场景:二进制读写适用于非文本数据 / 结构体 / 大文件,文本读写适用于人类可读的文本数据。
3. 选择二进制读写的场景:
float、double类型)。
面试题 3:使用 fwrite 写入结构体数据时,如何解决跨平台兼容性问题?(牛客网高频题,字节跳动、阿里均考过)
答案:跨平台兼容性问题的核心是 “结构体对齐差异” 和 “CPU 字节序差异”,解决方案如下:
1. 统一结构体对齐方式:使用#pragma pack(1)强制 1 字节对齐(取消编译器自动填充),示例:
#pragma pack(1)
typedef struct { int id; char name[20]; } User;
#pragma pack()2. 采用固定大小数据类型:用stdint.h中的int32_t、uint64_t等代替int、long,避免不同平台数据类型大小差异。
3. 统一字节序:对多字节数据(如int32_t、float)进行字节序转换,使用网络字节序(大端)存储,示例:
// 写入时:主机序→网络序
int32_t net_id = htonl(user.id);
fwrite(&net_id, sizeof(int32_t), 1, fp);
// 读取时:网络序→主机序
int32_t net_id;
fread(&net_id, sizeof(int32_t), 1, fp);
user.id = ntohl(net_id);. 逐个字段读写(兼容优先级最高):避免直接读写结构体,改为逐个字段写入 / 读取,不依赖结构体内存布局。
博主简介 byte轻骑兵,现就职于国内知名科技企业,专注于嵌入式系统研发,深耕 Android、Linux、RTOS、通信协议、AIoT、物联网及 C/C++ 等领域。乐于技术分享与交流,欢迎关注互动! 📌 主页与联系方式
⚠️ 版权声明 本文为原创内容,未经授权禁止转载。商业合作或内容授权请联系邮箱并备注来意。