首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C语言标准库函数】标准输入输出函数详解[4]:二进制文件读写函数

【C语言标准库函数】标准输入输出函数详解[4]:二进制文件读写函数

作者头像
byte轻骑兵
发布2026-01-21 15:14:52
发布2026-01-21 15:14:52
1170
举报

在 C 语言文件操作中,二进制文件读写是处理非文本数据(如图片、音频、视频、结构体)的核心技术。相比文本文件的格式化读写(fprintf/fscanf),二进制读写以 “原始字节流” 为操作单位,跳过数据转换环节,具备更高的效率和更强的通用性。

一、二进制文件读写基础认知

1.1 什么是二进制文件?

二进制文件是数据以内存原始存储格式直接存储的文件,不进行任何字符编码或格式转换。例如:

  • 一个int类型的数值1024,在内存中以 4 字节(32 位系统)存储为0x00000400,写入二进制文件后仍保持该字节序列;
  • 文本文件会将1024转换为字符'1'、'0'、'2'、'4'(对应 ASCII 码0x31、0x30、0x32、0x34)存储,读取时需反向转换。

1.2 二进制读写 vs 文本读写

对比维度

二进制读写(fread/fwrite)

文本读写(fprintf/fscanf)

数据存储形式

原始字节流,与内存布局完全一致

字符编码形式,可能转换换行符(\n↔\r\n)

读写效率

高:无数据转换,直接 IO 操作

低:需格式化转换,额外 CPU 开销

处理数据类型

通用:二进制数据、结构体、文本均可

局限:仅适用于文本数据,需格式化声明

跨平台兼容性

需处理字节序、结构体对齐问题

换行符转换可能导致兼容性问题

适用场景

多媒体文件、大文件、结构体序列化

配置文件、日志文件、人类可读文本

可视化流程图:二进制读写的极简流程

文本读写的复杂流程

二、fread () 函数深度解析

2.1 函数简介:为什么选择 fread?

fread()是 C 语言标准库(<stdio.h>)提供的二进制读取函数,核心优势在于精确控制字节读取

  • 支持任意数据类型:无论是intfloat等基本类型,还是自定义结构体、大型字节数组,均可直接读取;
  • 高效处理大文件:通过设置 “数据项大小” 和 “数量”,可分块读取超大文件,避免内存溢出;
  • 底层兼容性强:与操作系统 IO 接口深度适配,在嵌入式、桌面、服务器环境中均稳定工作。

2.2 函数原型与参数详解

代码语言:javascript
复制
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
    1. 正常情况:返回nmemb,表示所有数据项均读取成功;
    2. 到达文件末尾:返回小于nmemb的非零值(如计划读 10 个 int,实际读 3 个);
    3. 读取错误:返回 0(需通过ferror(stream)判断错误类型)。

2.3 函数实现(伪代码)

fread()的底层本质是 “从文件流中复制字节到缓冲区”,简化伪代码如下,帮助理解核心流程:

代码语言:javascript
复制
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;
}

2.4 核心使用场景

场景 1:读取二进制多媒体文件(如图片、音频)

多媒体文件(.png、.mp3)本身就是二进制格式,需逐字节读取后处理(如传输、解码):

代码语言:javascript
复制
// 读取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:结构体序列化与反序列化(数据持久化)

在嵌入式系统、网络编程中,常将结构体数据写入文件保存(序列化),后续读取恢复(反序列化):

代码语言:javascript
复制
// 定义传感器数据结构体
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 级大文件时,无法一次性加载到内存,需用固定大小缓冲区分块读取:

代码语言:javascript
复制
#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);
}

2.5 关键注意事项

  1. 文件打开模式必须为二进制:读取二进制文件时,必须用"rb"(Windows 系统中"r"会转换\r\n\n,导致二进制数据错乱;Linux/Unix 系统中"r""rb"差异较小,但建议统一用"rb"保证兼容性)。
  2. 缓冲区必须足够大:缓冲区大小需≥size * nmemb,否则会导致内存溢出(如计划读取 10 个int(40 字节),缓冲区仅分配 30 字节,会覆盖后续内存数据)。
  3. 必须检查返回值:忽略返回值会导致数据读取不完整却不知情(如文件损坏、磁盘错误),正确做法是:
代码语言:javascript
复制
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 字节),导致跨编译器 / 平台读写错乱。解决方案:

代码语言:javascript
复制
#pragma pack(1) // 强制1字节对齐(取消填充)
typedef struct {
    char a;
    int b;
} AlignTest;
#pragma pack() // 恢复默认对齐

5. 字节序兼容:不同 CPU 架构(x86 为小端,ARM 可配置为大端)存储多字节数据(如int)的字节顺序不同,跨平台读取需统一字节序:

代码语言:javascript
复制
// 读取大端字节序的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 位偏移量)。

2.6 实战示例:完整的二进制文件读取流程

以下示例实现 “读取二进制文件中的整数数组,计算平均值”,涵盖参数校验、错误处理、资源释放:

代码语言:javascript
复制
#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 () 函数深度解析

3.1 函数简介

fwrite()fread()是配对函数,用于将内存中的原始数据以字节流形式写入文件,核心特点:

  • 无格式限制:无需像fprintf那样指定格式符(如%d、%f),直接写入内存数据;
  • 支持批量写入:可一次性写入多个数据项(如数组、结构体数组),效率高于逐个字节写入;
  • 数据完整性:写入的字节与内存中完全一致,适合需要精确存储的场景(如加密密钥、二进制协议数据)。

3.2 函数原型与参数详解

代码语言:javascript
复制
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
    1. 正常情况:返回nmemb,表示所有数据均写入成功;
    2. 写入错误:返回小于nmemb的值(如磁盘空间不足、权限不足),需通过ferror(stream)判断错误类型。

3.3 函数实现(伪代码)

fwrite()的底层是 “从缓冲区复制字节到文件流”,简化伪代码如下:

代码语言:javascript
复制
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;
}

3.4 核心使用场景(结合实际项目)

场景 1:写入结构体数据到文件(数据持久化)

将程序运行时的结构体数据写入文件,下次启动时读取恢复:

代码语言:javascript
复制
// 保存用户配置到二进制文件
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 数据包)需保存到文件:

代码语言:javascript
复制
// 保存网络接收的二进制数据到文件
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:生成二进制格式的日志文件(高效存储)

相比文本日志,二进制日志体积更小、写入更快,适合高并发场景:

代码语言:javascript
复制
// 二进制日志结构体
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);
}

3.5 与 fread 互补的注意事项

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)计算有效长度:

代码语言:javascript
复制
char *str = "Hello, Binary!";
// 正确:写入字符串有效长度(不含'\0')
fwrite(str, 1, strlen(str), fp);

6. 跨平台换行符:二进制模式下写入'\n'不会自动转换为"\r\n"(Windows 文本模式会转换),若需兼容 Windows 文本读取,需手动处理。

3.6 实战示例:完整的二进制文件写入 + 读取流程

以下示例实现 “写入结构体数组到文件,再读取并验证数据一致性”,涵盖完整的读写闭环:

代码语言:javascript
复制
#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 核心差异对比

对比维度

fread () 函数

fwrite () 函数

核心功能

从文件流→缓冲区(读取)

从缓冲区→文件流(写入)

指针参数属性

void *(可写:接收数据)

const void *(只读:提供数据)

触发的 IO 方向

输入 IO(从外部文件到程序内存)

输出 IO(从程序内存到外部文件)

典型错误原因

文件不存在、权限不足、文件末尾

磁盘空间不足、权限不足、文件只读

缓冲区要求

需预先分配足够空间,避免溢出

缓冲区需包含有效数据,避免访问越界

数据依赖

依赖文件中已存在的数据

依赖内存中已准备好的数据

常见搭配函数

fopen("rb")、feof()、ferror()

fopen("wb"/"ab")、fflush()、fclose()

五、常见问题与解决方案

5.1 fread 读取到的数据是乱码 / 错误值?

原因

  1. 文件打开模式错误(用"r"代替"rb");
  2. 结构体未设置对齐,跨编译器读写;
  3. 数据类型不匹配(如写入int,读取时用float);
  4. 字节序差异(跨 CPU 架构读写)。

解决方案

  • 统一使用"rb"/"wb"模式;
  • #pragma pack(1)设置结构体对齐;
  • 确保读写数据类型一致;
  • 对多字节数据进行字节序转换。

5.2 fwrite 返回值小于请求个数,数据写入不完整?

原因

  1. 磁盘空间不足;
  2. 文件权限不足(如只读文件);
  3. 缓冲区未刷新,程序异常退出;
  4. 大文件写入时超过文件系统限制。

解决方案

  • 写入前检查磁盘空间;
  • 确保文件有写权限(Linux 用chmod +w);
  • 关键数据写入后调用fflush()
  • 分块写入大文件,循环重试未写入部分。

5.3 跨平台读写二进制文件失败?

原因

  1. 结构体对齐方式不同;
  2. CPU 字节序不同(大端 / 小端);
  3. 数据类型大小不同(如long在 32 位系统是 4 字节,64 位是 8 字节)。

解决方案

  1. 结构体对齐统一为 1 字节;
  2. 用固定大小的数据类型(如int32_tuint64_t代替intlong);
  3. 对所有多字节数据进行字节序转换(统一使用网络字节序);
  4. 避免直接读写结构体,改为逐个字段读写(最兼容但效率较低)。

fread()fwrite()是 C 语言二进制文件操作的基石,其核心价值在于高效、精确地处理原始字节数据,适用于多媒体文件、结构体序列化、大文件处理等文本读写无法覆盖的场景。掌握这两个函数的关键在于:

  1. 理解参数含义与返回值判断;
  2. 重视文件打开模式、缓冲区管理、错误处理;
  3. 解决跨平台兼容性问题(对齐、字节序)。

在实际开发中,二进制读写常与内存管理、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. 选择二进制读写的场景:

  • 处理图片、音频、视频等非文本文件;
  • 需高效存储 / 传输大量数据(如 GB 级大文件);
  • 结构体数据的序列化与反序列化(如配置文件、传感器数据);
  • 避免格式化转换导致的精度损失(如floatdouble类型)。

面试题 3:使用 fwrite 写入结构体数据时,如何解决跨平台兼容性问题?(牛客网高频题,字节跳动、阿里均考过)

答案:跨平台兼容性问题的核心是 “结构体对齐差异” 和 “CPU 字节序差异”,解决方案如下:

1. 统一结构体对齐方式:使用#pragma pack(1)强制 1 字节对齐(取消编译器自动填充),示例:

代码语言:javascript
复制
#pragma pack(1)
typedef struct { int id; char name[20]; } User;
#pragma pack()

2. 采用固定大小数据类型:用stdint.h中的int32_tuint64_t等代替intlong,避免不同平台数据类型大小差异。

3. 统一字节序:对多字节数据(如int32_tfloat)进行字节序转换,使用网络字节序(大端)存储,示例:

代码语言:javascript
复制
// 写入时:主机序→网络序
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++ 等领域。乐于技术分享与交流,欢迎关注互动! 📌 主页与联系方式

  • CSDN:https://blog.csdn.net/weixin_37800531
  • 知乎:https://www.zhihu.com/people/38-72-36-20-51
  • 微信公众号:嵌入式硬核研究所
  • 邮箱:byteqqb@163.com(技术咨询或合作请备注需求)

⚠️ 版权声明 本文为原创内容,未经授权禁止转载。商业合作或内容授权请联系邮箱并备注来意。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、二进制文件读写基础认知
    • 1.1 什么是二进制文件?
    • 1.2 二进制读写 vs 文本读写
  • 二、fread () 函数深度解析
    • 2.1 函数简介:为什么选择 fread?
    • 2.2 函数原型与参数详解
    • 2.3 函数实现(伪代码)
  • 2.4 核心使用场景
    • 2.5 关键注意事项
    • 2.6 实战示例:完整的二进制文件读取流程
  • 三、fwrite () 函数深度解析
    • 3.1 函数简介
    • 3.2 函数原型与参数详解
    • 3.3 函数实现(伪代码)
    • 3.4 核心使用场景(结合实际项目)
    • 3.5 与 fread 互补的注意事项
    • 3.6 实战示例:完整的二进制文件写入 + 读取流程
  • 四、fread 与 fwrite 核心差异对比
  • 五、常见问题与解决方案
    • 5.1 fread 读取到的数据是乱码 / 错误值?
    • 5.2 fwrite 返回值小于请求个数,数据写入不完整?
    • 5.3 跨平台读写二进制文件失败?
  • 附:经典面试真题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档