我应该如何将结构写入 C 中的文件?
How should I write a struct to a file in C?
我正在尝试将结构写入二进制文件。我希望我的代码是跨平台的,所以我不确定是否只用 fwrite 编写整个结构。如果我这样做,那么结构的大小将根据每个平台的原始类型的大小而变化(在平台 A 中,int 的大小将与平台 B 中的大小不同。因此结构不会大小相同,文件最终会不同)。
但是我对此一无所知,所以我是否应该单独编写结构的每个成员,序列化结构 (我该怎么做?) ,或者只写一个 fwrite 的结构?
记住写的文件要跨平台兼容
提前致谢
编辑:我的结构类似于
typedef struct {
int health;
float x, y;
char ID[];
} Player;
你可以选择简单的方法或困难的方法。
困难的方法:尝试以二进制格式编写结构;并与字节顺序和整数大小问题作斗争。这是一个难题,已经设计了完整的库来尝试回答它(protobuf-c, tpl 和 Apache Avro——抱歉,仅限于 2 个链接——是我遇到的前 3 个例子)。
现在是简单的方法。
写入文本文件。
(ASCII) 文本格式几乎是您唯一可以考虑的完全可移植性:它是编写文本的标准方式;而我们人类或多或少对如何写数字有妥协。
由于您的结构很简单,您可以只写字段,每个字段占一行,加载时只需一个接一个地读取每个字段。
可能出现的唯一问题可能是 ID;你没有描述的格式,所以我不能更准确。
希望对您有所帮助,
Ekleog
实际上,你必须做一个"interface specification"作为文件中的结构,供其他人阅读,是你的程序与其他人的接口。
所以你定义文件的格式(文件中的数据)。例如"every line holds one field. The first line holds the health as an ASCII number, the second line..."等等。
然后您"publish"为所有需要阅读您的文件的人制定规范。
当然,您现在必须调整您的程序以发出您指定的文件。
既然你说你正在构建一个游戏原型并且你主要想使用 x86 和 x86_64 CPU,那么二进制序列化会更容易一些。
但首先要记住一些事情:
long
值在 x86 和 x86_64 中具有不同的大小。
- 因此,
long
值也有不同的对齐要求。
double
值可能需要 8 或 4 字节对齐,具体取决于 OS 和体系结构。
- 字节顺序通常是个问题,但较新的 Apple 产品不使用 PowerPC,因此您几乎可以保证您正在为小端机器工作。
开始之前的最后一个信息:您可以使用编译器宏在编译时收集有关系统架构的信息。在 gcc 中,__LP64__
表示您正在编译 64 位可执行文件。我确定 msvc 有类似的宏。
解决不同大小的变量问题:
stdint.h
文件包含 int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t
的类型定义。所有这些都保证是该位数。您可以在平台之间安全地使用 int64_t
而不是 long
。如果某个平台由于某种原因没有这些 typedef,您可以自己使用 typedef 定义它们,因为无论如何您都可以使用编译器预处理器宏获取有关目标系统的信息。
解决不同的对齐问题:
这个毛茸茸的。最简单的解决方案是让 serializable_player
结构包含 player
结构包含的所有字段,但告诉编译器打包它(gcc 的 packed
属性),这样编译器就不会'放任何填充物。然后在写文件的时候,你从player
创建一个serializable_player
,直接写
转换 from/to 可序列化类型可能是一种开销。如果你能承受浪费一点内存,你也可以强制对齐结构的每个成员。将 double
值与 8 个字节对齐可能是一个好主意。在 gcc 中,您可以使用 aligned
属性。
注意:打包结构通常会降低性能,因为您要在整个游戏中使用 player
,不要 将其打包。
处理不同的浮点表示:
如果您有可能需要支持使用不同浮点表示的体系结构,您将无法将它们存储为二进制。
我曾经在一家游戏开发公司工作,当通过网络发送浮点数时,我们将它们表示为整数。我们所做的是:计算我们想要的最小分辨率 r
。然后计算它可能的最小值和最大值(取决于玩家所在的多人游戏地图),min
和 max
。在这种表示中,我们可以表示 (max - min) / r
个不同的数字,我们需要 log2((max - min) / r)
位来存储它。由于接收方也知道 min、max、r,我们不需要在网络数据包中包含该信息。我们说过min
表示全0,max
表示全1,其他值介于两者之间。这是一个实时动作游戏,地图不是太大(多人游戏支持 64 位玩家),我们没有遇到甚至低于 32 位的问题,玩家不是 shaking/flickering,性能很好。
如果您使用类似的方法,您将不会遇到 serializing/deserializing 二进制浮点数问题。
最后,如果您将来以大端架构为目标,则只需执行一些小步骤(如果您不使用联合)。每个尺寸只需要 convert_to_le_*
个函数。这些函数对于小端机器应该是空的,对大端机器做按位运算。在每次序列化之前和每次反序列化之后,您应该为每个在大端机器中以不同方式表示的成员调用这些函数。支持小端机器在这里可能是一个更好的主意,因为您的主要受众可能有 x86 和 x86_64 机器。
如果您使用联合,您的结构表示也应该在不同的体系结构中有所不同。
我知道将值存储在 JSON 或明文中更容易,并且 space 优化硬盘几乎总是不必要的。但是,如果您打算制作一款多人游戏,事先准备好二进制 serialization/deserialization 可能会有所收获,因为您无法实时发送 JSON 数据。
编辑:如评论中所建议,如果您想更改文件结构、添加更多字段等,使用 JSON 似乎更好。如果有机会这样做并且您仍然决定使用二进制文件文件结构,您的保存文件应在前几个字节包含一个幻数,代表版本。当您决定 add/remove 一个字段时,您应该更新版本。当您读取存档文件时,您应该首先检查版本,并相应地处理每个版本。
编辑 2:此答案的某些部分特定于 x86 和 x86_64 CPU。例如,使用 long
而不是 int64_t
是有意义的,因为 long 在两种体系结构中都是 8 个字节。如果需要支持更广泛的区域,我只会推荐 int*_t typedefs。这里的一个例子是 linux 内核,其中 s32
用于 带符号的 32 位整数 如果需要强制执行大小。
我正在尝试将结构写入二进制文件。我希望我的代码是跨平台的,所以我不确定是否只用 fwrite 编写整个结构。如果我这样做,那么结构的大小将根据每个平台的原始类型的大小而变化(在平台 A 中,int 的大小将与平台 B 中的大小不同。因此结构不会大小相同,文件最终会不同)。
但是我对此一无所知,所以我是否应该单独编写结构的每个成员,序列化结构 (我该怎么做?) ,或者只写一个 fwrite 的结构? 记住写的文件要跨平台兼容
提前致谢
编辑:我的结构类似于
typedef struct {
int health;
float x, y;
char ID[];
} Player;
你可以选择简单的方法或困难的方法。
困难的方法:尝试以二进制格式编写结构;并与字节顺序和整数大小问题作斗争。这是一个难题,已经设计了完整的库来尝试回答它(protobuf-c, tpl 和 Apache Avro——抱歉,仅限于 2 个链接——是我遇到的前 3 个例子)。
现在是简单的方法。
写入文本文件。
(ASCII) 文本格式几乎是您唯一可以考虑的完全可移植性:它是编写文本的标准方式;而我们人类或多或少对如何写数字有妥协。
由于您的结构很简单,您可以只写字段,每个字段占一行,加载时只需一个接一个地读取每个字段。
可能出现的唯一问题可能是 ID;你没有描述的格式,所以我不能更准确。
希望对您有所帮助,
Ekleog
实际上,你必须做一个"interface specification"作为文件中的结构,供其他人阅读,是你的程序与其他人的接口。
所以你定义文件的格式(文件中的数据)。例如"every line holds one field. The first line holds the health as an ASCII number, the second line..."等等。
然后您"publish"为所有需要阅读您的文件的人制定规范。
当然,您现在必须调整您的程序以发出您指定的文件。
既然你说你正在构建一个游戏原型并且你主要想使用 x86 和 x86_64 CPU,那么二进制序列化会更容易一些。
但首先要记住一些事情:
long
值在 x86 和 x86_64 中具有不同的大小。- 因此,
long
值也有不同的对齐要求。 double
值可能需要 8 或 4 字节对齐,具体取决于 OS 和体系结构。- 字节顺序通常是个问题,但较新的 Apple 产品不使用 PowerPC,因此您几乎可以保证您正在为小端机器工作。
开始之前的最后一个信息:您可以使用编译器宏在编译时收集有关系统架构的信息。在 gcc 中,__LP64__
表示您正在编译 64 位可执行文件。我确定 msvc 有类似的宏。
解决不同大小的变量问题:
stdint.h
文件包含 int8_t int16_t int32_t int64_t uint8_t uint16_t uint32_t uint64_t
的类型定义。所有这些都保证是该位数。您可以在平台之间安全地使用 int64_t
而不是 long
。如果某个平台由于某种原因没有这些 typedef,您可以自己使用 typedef 定义它们,因为无论如何您都可以使用编译器预处理器宏获取有关目标系统的信息。
解决不同的对齐问题:
这个毛茸茸的。最简单的解决方案是让 serializable_player
结构包含 player
结构包含的所有字段,但告诉编译器打包它(gcc 的 packed
属性),这样编译器就不会'放任何填充物。然后在写文件的时候,你从player
创建一个serializable_player
,直接写
转换 from/to 可序列化类型可能是一种开销。如果你能承受浪费一点内存,你也可以强制对齐结构的每个成员。将 double
值与 8 个字节对齐可能是一个好主意。在 gcc 中,您可以使用 aligned
属性。
注意:打包结构通常会降低性能,因为您要在整个游戏中使用 player
,不要 将其打包。
处理不同的浮点表示:
如果您有可能需要支持使用不同浮点表示的体系结构,您将无法将它们存储为二进制。
我曾经在一家游戏开发公司工作,当通过网络发送浮点数时,我们将它们表示为整数。我们所做的是:计算我们想要的最小分辨率 r
。然后计算它可能的最小值和最大值(取决于玩家所在的多人游戏地图),min
和 max
。在这种表示中,我们可以表示 (max - min) / r
个不同的数字,我们需要 log2((max - min) / r)
位来存储它。由于接收方也知道 min、max、r,我们不需要在网络数据包中包含该信息。我们说过min
表示全0,max
表示全1,其他值介于两者之间。这是一个实时动作游戏,地图不是太大(多人游戏支持 64 位玩家),我们没有遇到甚至低于 32 位的问题,玩家不是 shaking/flickering,性能很好。
如果您使用类似的方法,您将不会遇到 serializing/deserializing 二进制浮点数问题。
最后,如果您将来以大端架构为目标,则只需执行一些小步骤(如果您不使用联合)。每个尺寸只需要 convert_to_le_*
个函数。这些函数对于小端机器应该是空的,对大端机器做按位运算。在每次序列化之前和每次反序列化之后,您应该为每个在大端机器中以不同方式表示的成员调用这些函数。支持小端机器在这里可能是一个更好的主意,因为您的主要受众可能有 x86 和 x86_64 机器。
如果您使用联合,您的结构表示也应该在不同的体系结构中有所不同。
我知道将值存储在 JSON 或明文中更容易,并且 space 优化硬盘几乎总是不必要的。但是,如果您打算制作一款多人游戏,事先准备好二进制 serialization/deserialization 可能会有所收获,因为您无法实时发送 JSON 数据。
编辑:如评论中所建议,如果您想更改文件结构、添加更多字段等,使用 JSON 似乎更好。如果有机会这样做并且您仍然决定使用二进制文件文件结构,您的保存文件应在前几个字节包含一个幻数,代表版本。当您决定 add/remove 一个字段时,您应该更新版本。当您读取存档文件时,您应该首先检查版本,并相应地处理每个版本。
编辑 2:此答案的某些部分特定于 x86 和 x86_64 CPU。例如,使用 long
而不是 int64_t
是有意义的,因为 long 在两种体系结构中都是 8 个字节。如果需要支持更广泛的区域,我只会推荐 int*_t typedefs。这里的一个例子是 linux 内核,其中 s32
用于 带符号的 32 位整数 如果需要强制执行大小。