在C语言中将原始结构体内容(字节)写入文件,对实际写入的大小感到困惑。

7

这是一个基本问题,但我预期这个结构体应该占用13个字节的空间(1个char和3个unsigned int各占4个字节)。然而,sizeof(ESPR_REL_HEADER)返回的值是16个字节。

typedef struct {
  unsigned char version;
  unsigned int  root_node_num;
  unsigned int  node_size;
  unsigned int  node_count;
} ESPR_REL_HEADER;

我想要做的是使用一些值初始化这个结构体,并将它所包含的数据(原始字节)写入文件的开头,这样当我以后打开这个文件时,我可以重建这个结构体并获取有关文件其余部分内容的一些元数据。
我像这样初始化结构体并将其写入文件:
int esprime_write_btree_header(FILE * fp, unsigned int node_size) {
  ESPR_REL_HEADER header = {
    .version       = 1,
    .root_node_num = 0,
    .node_size     = node_size,
    .node_count    = 1
  };

  return fwrite(&header, sizeof(ESPR_REL_HEADER), 1, fp);
}

当我在进行实验时,node_size目前为4。

在我将结构体写入文件后,该文件包含以下数据:

-bash$  hexdump test.dat
0000000 01 bf f9 8b 00 00 00 00 04 00 00 00 01 00 00 00
0000010

我希望你能真正包含以下内容:
-bash$  hexdump test.dat
0000000 01 00 00 00 00 04 00 00 00 01 00 00 00
0000010

请原谅我的新手问题。我正在尝试学习 :) 如何高效地将结构体的数据组件写入文件?

8个回答

6
微处理器并非设计用于从任意地址获取数据。像 4 字节的 int 这样的对象应仅存储在可被四整除的地址上。这个要求称为 对齐
C 语言允许编译器在结构体成员之间插入 填充字节 以对齐它们。填充量是不同平台之间的一个变量,另一个主要变量是 字节序。这就是为什么如果您想要程序在多台机器上运行,就不应该简单地“倾倒”结构体到磁盘上。
最佳实践是显式地编写每个成员,并在二进制输出之前使用 htonl 将字节序转换为大端序。当读回时,使用 memcpy 移动原始字节,不要使用。
char *buffer_ptr;
...
++ buffer_ptr;
struct.member = * (int *) buffer_ptr; /* potential alignment error */

但是相反要做
memcpy( buffer_ptr, (char *) & struct.member, sizeof struct.member );
struct.member = ntohl( struct.member ); /* if member is 4 bytes */

谢谢。所以基本上是手动构建一个字节数组并将其写入磁盘,然后当我从磁盘读取它时,将字节从该数组复制回新分配的结构体的成员中?我只是在学习,但我希望以一种方式完成这个任务,使文件在各种机器上始终保证具有相同的格式,是吗? - d11wtq
1
@d11wtq жҳҜзҡ„пјҢдёәдәҶжңҖдҪіеҸҜ移жӨҚжҖ§пјҢжӮЁеә”иҜҘдҪҝз”Ёmemcpyе°Ҷеӯ—иҠӮж•°з»„дёӯзҡ„еӯ—иҠӮеӨҚеҲ¶еҲ°жҲҗе‘ҳдёӯпјҢ然еҗҺи°ғз”ЁntohlпјҲжҲ–йҖӮеҪ“зҡ„еҮҪж•°пјүжқҘдҝ®еӨҚеӯ—иҠӮйЎәеәҸгҖӮ - Potatoswatter
太好了,谢谢。我有一些阅读要做。成为新手真的很难 :) - d11wtq

3

1

当您使用fwrite将结构体原样写入时,它们会按照内存中的格式进行写入,包括由于填充而插入到结构体中的“死字节”。此外,多字节数据会以您系统的字节序进行写入。

如果您不希望发生这种情况,请编写一个函数,从您的结构体中序列化数据。您可以仅写入非填充区域,并以可预测的顺序编写多字节数据(例如在网络字节序中)。


1
结构体遵循对齐规则,这意味着其中的一些项会被填充。看起来第一个 unsigned char 字段已经填充为 4 个字节。
这里的一个陷阱是,规则因系统而异,所以如果您在一个编译器上编译的程序中使用 fwrite 写入整个结构体,然后尝试在另一个系统上使用 fread 读取它,则可能会获得垃圾数据,因为第二个程序将假定数据已对齐以适应其对结构布局的概念。
通常,您必须执行以下操作之一:
  1. 决定保存的数据文件仅对具有特定特征的程序版本有效(取决于您使用的编译器的文档行为),或
  2. 不将整个结构写为一个,而是实现一个更正式的数据格式,其中每个元素都单独写入,并且其大小明确受控。
(相关问题是字节顺序可能会不同;通常也适用于相同的选择,除了在选项2中,您希望明确指定数据格式的字节顺序。)

在第二点方面,是否有一个好的模式可以遵循? 我在尽我所能地尽量减少磁盘I/O(这不是过早优化,而实际上是此练习的重点......我正在探索树算法,以低I/O开销将数据集存储在磁盘上,只是为了好玩。写四次将是低效的,所以我假设我应该在将其写入之前将数据复制到C中的另一个数据中? 就像无符号字符类型的数组一样? - d11wtq
写入操作通常会被缓冲(从而减少实际调用操作系统来写入数据的次数),因此它可能并不像你想象的那么昂贵。如果您的数据是固定大小的,您可以将其写入对应于您的数据格式的较大缓冲区中,然后一次性使用 fwrite 写入。这样做可能更容易些。 - Edmund
是的,最终我做的就是将字节在内存中复制到缓冲区,然后一次性写入。谢谢。 - d11wtq

1
这是由于内存对齐(memory alignment)的原因。第一个字符被扩展为占用4个字节的内存。事实上,像int这样的更大类型只能在4字节块的开头“启动”,因此编译器会填充字节以达到这一点。
我曾经遇到过位图头部相同的问题,它以2个字符开头。我在结构体中使用了char bm[2],并想了两天#$%^头部的第3和第4个字节去哪里了...
如果要防止这种情况,可以使用__attribute__((packed)),但必须注意内存对齐对您的程序的方便运行很重要

1

尽量不要这样做!大小差异是由编译器/链接器使用的填充和对齐方式引起的,以优化通过速度访问变量。填充和对齐规则有语言和操作系统之分。此外,在不同硬件上写入整数并读取它们可能会出现问题,因为字节顺序不同。

按字节编写您的元数据,并使用不能被误解的结构来表示。空字符终止的ASCII字符串可以使用。


1

这看起来很有趣,但我认为对于我的特定需求来说有点过头了。它还通过向序列化数据添加自己的信息来增加数据的大小。我的文件将具有严格的格式(在初始标头之后为B树),因此理论上我应该能够仅通过将数据从文件复制回内存,就能准确地知道数据类型。 - d11wtq
+1пјҢеҫҲжңүи¶ЈпјҢдҪҶеҢ…жӢ¬.cж–Ү件жҳҜеӨ–йғЁдҫқиө–зҡ„жҳҺзЎ®е®ҡд№үгҖӮ - Potatoswatter
@Potatoswatter,许可证允许您重新分发程序,因此您不必担心 tpl.c 和 tpl.h 的内部依赖关系,可以将其捆绑到您的程序中。确实,由于元数据和字符串数据表示,它会增加大小,但可移植性和快速部署肯定是问题。 - dAm2K

0
如果您想以特定格式编写数据,请使用unsigned char数组...
unsigned char outputdata[13];
outputdata[0] = 1;
outputdata[1] = 0;
/* ... of course, use data from struct ... */
outputdata[12] = 0;
fwrite(outputdata, sizeof outputdata, 1, fp);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接