fwrite如何高效写入结构体数据?

99ANYc3cd6
预计阅读时长 20 分钟
位置: 首页 C语言 正文

核心概念

fwrite 函数是 C 标准库 <stdio.h> 中的一个函数,用于将一块内存中的数据(例如一个数组、一个结构体)写入到指定的文件流中。

其函数原型为:

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr: 指向要写入数据的内存块的指针,对于结构体,这个指针就是结构体变量的地址。
  • size: 每个数据项的大小(以字节为单位),对于结构体,sizeof(你的结构体名)
  • nmemb: 要写入的数据项的数量,如果要写入一个结构体,这个值就是 1
  • stream: 指向 FILE 对象的指针,该 FILE 对象指定了要写入的文件。
  • 返回值: 成功写入的数据项的数量,如果出错,则返回一个小于 nmemb 的值。

完整示例:写入和读取单个结构体

这个例子将定义一个 Student 结构体,创建一个结构体变量,并将其写入文件,然后再从文件中读出。

定义结构体

我们定义一个包含不同数据类型的结构体,这能更好地展示 fwrite 的能力。

#include <stdio.h>
#include <string.h>
// 定义一个学生结构体
struct Student {
    int id;         // 整数
    char name[50];  // 字符数组
    float score;    // 浮点数
};

写入结构体到文件

void write_student_to_file(const char *filename, struct Student student) {
    // 1. 以二进制写入模式 ("wb") 打开文件
    // "w" 是写入,"b" 是二进制模式,对于结构体,二进制模式至关重要!
    FILE *file = fopen(filename, "wb");
    if (file == NULL) {
        perror("Error opening file for writing");
        return;
    }
    // 2. 使用 fwrite 写入结构体
    // &student: 结构体变量的地址
    // sizeof(struct Student): 每个结构体的大小
    // 1: 写入1个这样的结构体
    // file: 文件指针
    size_t written = fwrite(&student, sizeof(struct Student), 1, file);
    // 3. 检查写入是否成功
    if (written == 1) {
        printf("Successfully wrote student data to %s\n", filename);
    } else {
        perror("Error writing to file");
    }
    // 4. 关闭文件
    fclose(file);
}

从文件中读取结构体

void read_student_from_file(const char *filename) {
    // 1. 以二进制读取模式 ("rb") 打开文件
    // "r" 是读取,"b" 是二进制模式
    FILE *file = fopen(filename, "rb");
    if (file == NULL) {
        perror("Error opening file for reading");
        return;
    }
    // 2. 创建一个结构体变量来存储从文件读取的数据
    struct Student read_student;
    // 3. 使用 fread 读取结构体
    // &read_student: 存储读取数据的结构体变量的地址
    // sizeof(struct Student): 每个结构体的大小
    // 1: 读取1个这样的结构体
    // file: 文件指针
    size_t read_items = fread(&read_student, sizeof(struct Student), 1, file);
    // 4. 检查读取是否成功
    if (read_items == 1) {
        printf("\nSuccessfully read student data from %s:\n", filename);
        printf("ID: %d\n", read_student.id);
        printf("Name: %s\n", read_student.name);
        printf("Score: %.2f\n", read_student.score);
    } else {
        // 如果文件为空或读取失败
        printf("Could not read student data from file. It might be empty or corrupted.\n");
    }
    // 5. 关闭文件
    fclose(file);
}

main 函数:整合所有代码

int main() {
    const char *filename = "student.dat";
    // 创建一个要写入的结构体变量
    struct Student student_to_write = {101, "Zhang San", 95.5f};
    // 写入文件
    write_student_to_file(filename, student_to_write);
    // 从文件中读取
    read_student_from_file(filename);
    return 0;
}

如何编译和运行

  1. 将以上代码保存为一个文件,struct_example.c
  2. 使用 gcc 编译:gcc struct_example.c -o struct_example
  3. 运行程序:./struct_example

预期输出:

Successfully wrote student data to student.dat
Successfully read student data from student.dat:
ID: 101
Name: Zhang San
Score: 95.50

运行后,你会在当前目录下看到一个名为 student.dat 的文件,如果你用文本编辑器打开它,会看到一堆乱码,这正是二进制文件的特征。


关键注意事项和最佳实践

必须使用二进制模式 ("wb", "rb")

这是最重要的一点!如果你使用文本模式("w", "r"),操作系统会进行一些转换,比如在 Windows 系统下,换行符 \n 会被转换成 \r\n,这会破坏你写入的二进制数据结构,导致读取时数据错位或失败。对于所有非纯文本的数据(如结构体、数组),始终使用二进制模式。

字节对齐和可移植性问题

这是 fwrite 结构体最大的“坑”。

  • 字节对齐:为了提高内存访问效率,编译器会在结构体成员之间插入“空洞”(padding bytes)。struct { char c; int i; } 的大小可能是 8 字节而不是 5 字节,因为 int 通常需要 4 字节对齐。
  • 问题:不同的编译器、不同的操作系统(如 Windows vs. Linux)、甚至不同的编译选项(-m32 vs. -m64)可能导致同一个结构体的内存布局(即 sizeof 的结果和成员的偏移量)不同。
  • 后果:如果你在 Windows 上用 64 位编译器编译并写入一个结构体,然后把这个 student.dat 文件拿到一个 32 位的 Linux 系统上读取,数据很可能是错误的。

如何解决?

  • 方案一(推荐):使用文本格式,将结构体的每个成员单独写入,用分隔符(如逗号、空格)隔开,虽然文件会变大,但具有极高的可移植性和可读性。
  • 手动处理字节对齐,使用 #pragma pack(1) 指令告诉编译器不要进行字节填充,但这仍然不能保证所有平台上的 int, float 等基本类型的大小都相同(某些嵌入式系统上 int 可能是 2 字节)。
  • 使用序列化库,对于复杂的项目,使用专门的序列化库(如 Protocol Buffers, FlatBuffers, Cap'n Proto)是更健壮的解决方案。

处理指针成员

如果你的结构体中包含指针成员(char *name),fwrite 不会 你期望的那样工作。

struct BadExample {
    int id;
    char *name; // 这是一个指针!
};
struct BadExample s;
s.id = 1;
s.name = "Hello"; // name 存储的是字符串 "Hello" 的内存地址
// 错误的写入方式
fwrite(&s, sizeof(struct BadExample), 1, file);

这样写入文件后,文件里存储的是指针 s.name 的值(一个内存地址),而不是字符串 "Hello" 本身,当你读取这个文件时,程序会尝试将这个无效的内存地址赋值给另一个程序中的指针,这几乎必然会导致段错误。

正确做法:必须将指针指向的数据(即字符串内容)也写入文件。

struct GoodExample {
    int id;
    char name[50]; // 使用固定大小的字符数组,而不是指针
};

或者,如果必须用指针,你需要手动写入指针指向的数据:

struct Person {
    int age;
    char *city;
};
// 写入
void write_person(FILE *file, const struct Person *p) {
    fwrite(&p->age, sizeof(int), 1, file);
    int len = strlen(p->city) + 1; // +1 for null terminator
    fwrite(&len, sizeof(int), 1, file); // 先写入字符串长度
    fwrite(p->city, sizeof(char), len, file); // 再写入字符串内容
}
优点 缺点
简单快速:只需一个函数调用即可完成整个结构体的读写。 可移植性差:受平台字节对齐影响,跨平台数据交换容易出错。
高效:直接内存拷贝,速度快。 不处理指针:不能直接序列化包含指针的结构体,需要额外处理。
文件紧凑:二进制格式通常比文本格式占用更少空间。 可读性差:二进制文件无法用文本编辑器直接查看。
  • 对于个人项目、快速原型或在同一平台/环境下运行的应用,使用 fwritefread 来处理结构体是完全可行的,非常方便。
  • 对于需要跨平台、跨语言、长期维护或数据可读性要求高的项目,强烈建议避免直接 fwrite 结构体,应采用文本格式(如 CSV, JSON)专业的二进制序列化库
-- 展开阅读全文 --
头像
dede图片集栏目编辑器怎么用?
« 上一篇 今天
织梦CMS集成环境怎么搭建?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]