核心概念
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;
}
如何编译和运行
- 将以上代码保存为一个文件,
struct_example.c。 - 使用 gcc 编译:
gcc struct_example.c -o struct_example - 运行程序:
./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)、甚至不同的编译选项(
-m32vs.-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); // 再写入字符串内容
}
| 优点 | 缺点 |
|---|---|
| 简单快速:只需一个函数调用即可完成整个结构体的读写。 | 可移植性差:受平台字节对齐影响,跨平台数据交换容易出错。 |
| 高效:直接内存拷贝,速度快。 | 不处理指针:不能直接序列化包含指针的结构体,需要额外处理。 |
| 文件紧凑:二进制格式通常比文本格式占用更少空间。 | 可读性差:二进制文件无法用文本编辑器直接查看。 |
- 对于个人项目、快速原型或在同一平台/环境下运行的应用,使用
fwrite和fread来处理结构体是完全可行的,非常方便。 - 对于需要跨平台、跨语言、长期维护或数据可读性要求高的项目,强烈建议避免直接
fwrite结构体,应采用文本格式(如 CSV, JSON)或专业的二进制序列化库。
