C++中的结构体填充

69

如果我在C++中有一个struct,是否没有办法安全地将其读写到跨平台/编译器兼容的文件中?

因为如果我理解正确,每个编译器根据目标平台以不同的方式'填充'。


4
使用二进制输入输出所带来的效率往往不能补偿在研究、设计、开发,尤其是调试和维护方面花费的资金。源代码应该易于理解,但不应过于简单。 - Thomas Matthews
4个回答

59

不可能。这是因为C++在二进制级别上缺乏标准化导致的。

唐·瑟曼在他的书 Essential COM 中引用了一段话,其中写到:

C++和可移植性


一旦决定将一个C++类作为DLL分发, 就会面临C++的一个基本弱点, 即缺乏二进制级别上的标准化。 虽然ISO/ANSI C++草案试图规范哪些程序将编译以及运行它们的语义影响是什么, 但它并未尝试标准化C++的二进制运行时模型。 当客户端尝试从与构建FastString DLL所使用的开发环境不同的C++开发环境链接到FastString DLL的导入库时, 这个问题将首次显现出来。

不同的编译器会对结构体进行不同的填充。即使您使用相同的编译器,结构体的打包对齐方式也可以基于使用的 pragma pack 不同而不同。

不仅如此,如果您编写了两个成员完全相同的结构体,唯一的区别就是它们声明的顺序不同,那么每个结构体的大小可能(并且经常)不同。

例如,请参见以下内容:

struct A
{
   char c;
   char d;
   int i;
};

struct B
{
   char c;
   int i;
   char d;
};

int main() {
        cout << sizeof(A) << endl;
        cout << sizeof(B) << endl;
}

使用gcc-4.3.4进行编译,您将获得以下输出:

8
12

也就是说,尽管两个结构体具有相同的成员,但它们的大小是不同的!

归根结底,标准没有规定如何进行填充,因此编译器可以自由决定,你不能假设所有编译器都会做出相同的决定。


5
__attribute__((packed))这个东西,我用来处理共享内存结构以及映射网络数据的结构。它会影响性能(参见http://digitalvampire.org/blog/index.php/2006/07/31/why-you-shouldnt-use-__attribute__packed/),但对于与网络相关的结构体来说是一个有用的特性。(据我所知,这不是标准,因此答案仍然正确)。 - Pijusn
我不明白为什么结构体A的大小是8而不是更大。 { char c; // 那这个呢? char d; // 大小为1 + 填充3 int i; // 大小为4 }; - Dchris
6
编译器可能会小心翼翼地确保每个字段都根据自己的自然对齐方式进行对齐。C和D只有一个字节,因此无论您将它们放在哪里,单字节CPU指令都可以对齐。但是int需要对齐到4字节边界,为了达到这个目标,需要在D之后填充两个字节。这样就可以得到8。 - hoodaticus
大多数编译器都会以相同的方式对齐成员,似乎是这样。是否真的有编译器会在 A::cA::d 之间放置填充?如果没有,那么我说问题只是标准没有做出任何保证,尽管每个编译器似乎都在做着相同的事情(就像 reinterpret_cast 一样)。 - Indiana Kernick

30
如果你有机会自己设计结构体,那么它应该是可以实现的。基本想法是你应该设计它,以便不需要插入填充字节。第二个技巧是你必须处理大小端差异。
我将描述如何使用标量构造结构体,但只要对每个包含的结构体应用相同的设计,你应该能够使用嵌套结构体。
首先,在C和C++中的一个基本事实是,类型的对齐方式不能超过类型的大小。如果超过了,那么就无法使用malloc(N*sizeof(the_type))来分配内存。
从最大的类型开始对结构体进行布局。
 struct
 {
   uint64_t alpha;
   uint32_t beta;
   uint32_t gamma;
   uint8_t  delta;

接下来,手动填充结构体,以便最终匹配最大的类型:

   uint8_t  pad8[3];    // Match uint32_t
   uint32_t pad32;      // Even number of uint32_t
 }

下一步是决定结构体应该以小端或大端格式存储。最好的方法是在写入结构体之前或读取结构体之后,如果存储格式与主机系统的字节序不匹配,则在原地“交换”所有元素。


2
@Phil,像uint32_t这样的基本类型可能具有与其大小相匹配的对齐要求,在这种情况下为四个字节。编译器可能会插入填充以实现此目的。通过手动执行此操作,编译器将不需要执行此操作,因为对齐始终正确。缺点是在具有较少严格对齐要求的系统上,手动填充的结构将比由编译器填充的结构更大。您可以按升序或降序进行此操作,但如果按升序进行,则需要在结构体中间插入更多的填充。 - Lindydancer
1
只有在计划将结构体用于数组时,才需要在结构体末尾进行填充。 - Lindydancer
2
在一般情况下(例如,当您使用其他人设计的结构体时),可以插入填充以确保没有字段位于硬件无法读取的位置(如其他答案中所解释的)。但是,当您自己设计结构体时,只要小心一些,就可以确保不需要填充。这两个事实并不相互矛盾!我相信这个启发式规则适用于所有可能的架构(假设类型没有比其大小更大的对齐要求,在C中也不合法)。 - Lindydancer
2
@Lindydancer - 如果您打算将它们合成为随机内容的连续内存块,而不仅仅是同类数组,则需要填充。 填充可以使您在任意边界上进行自我对齐,例如sizeof(void*)或SIMD寄存器的大小。 - hoodaticus
1
@TimSeguine,“类型的对齐方式不能超过类型的大小”这个说法是正确的。否则,malloc(2*sizeof(a_type))(或new[])将不会返回一个可以访问两个元素的数组。在给定的系统上,std::max_align_t是最高对齐标量(如long double)的typedef。如果它是一个typedef到具有较低对齐的标量,则C++实现将被破坏。但是,如果您能提供一个sizeof(type) < alignof(type)成立的单个示例,那么请证明我是错误的。 - Lindydancer
显示剩余5条评论

10

不,没有安全的方法。除了填充之外,您还需要处理不同的字节顺序和不同内置类型的大小。

您需要定义文件格式,并将结构体转换为该格式。序列化库(例如boost::serialization或Google的protocolbuffers)可以帮助完成此任务。


1
一个结构体(或类)的大小可能不等于其成员大小之和。 - Thomas Matthews
2
@Thomas:没错。而且这只是开始。 - Erik

3
长话短说,没有平台无关的标准符合方式来处理填充(padding)。标准中称填充为“对齐”,并在3.9/5开始讨论:
“对象类型有对齐要求(3.9.1, 3.9.2)。完整对象类型的对齐是一个表示字节数的实现定义整数值;对象被分配到满足其对象类型对齐要求的地址上。”
但它继续讨论并深入到标准的许多黑暗角落。对齐是“实现定义”的,这意味着在不同编译器之间或者甚至在相同编译器下的地址模型(即32位/64位)之间可能会有所不同。
除非您真的有严格的性能要求,否则可以考虑以不同的格式将数据存储到磁盘中,例如char字符串。许多高性能协议在自然格式可能是其他格式时使用字符串发送所有内容。例如,我最近参与的低延迟交易数据源以字符串格式发送日期,格式如下:“20110321”,时间也类似地发送:“141055.200”。尽管该交易数据源每秒发送500万条消息,但他们仍然使用字符串发送所有内容,因为这样可以避免大小端和其他问题。

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