关于c ++:Struct由编译器重新排序

Struct Reordering by compiler

本问题已经有最佳答案,请猛点这里访问。

假设我有这样的结构:

1
2
3
4
5
6
7
8
struct MyStruct
{
  uint8_t var0;
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

这可能会浪费一堆(而不是一吨)的空间。这是因为uint32_t变量必须对齐。

实际上(在对结构进行了调整以便它可以实际使用uint32_t变量之后),它可能看起来像这样:

1
2
3
4
5
6
7
8
9
struct MyStruct
{
  uint8_t var0;
  uint8_t unused[3];  //3 bytes of wasted space
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

更有效的结构是:

1
2
3
4
5
6
7
8
struct MyStruct
{
  uint8_t var0;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
  uint32_t var1;
};

现在,问题是:

为什么编译器(按标准)禁止重新排序结构?

如果结构被重新排序,我看不出有什么方法可以让你的脚自己被射中。


Why is the compiler forbidden (by the standard) from reordering the struct?

基本原因是:为了与C兼容。

记住,C最初是一种高级汇编语言。在C中,通过将字节重新解释为特定的struct来查看内存(网络包,…)。

这导致多个功能依赖于此属性:

  • C保证EDCOX1(0)的地址和它的第一个数据成员的地址是相同的,所以C++也一样(在EDCOX1×2继承/方法不存在的情况下)。

  • C保证,如果您有两个structAB并且都是从一个数据成员char开始,然后是一个数据成员int(以及之后的任何内容),那么当您将它们放入union时,您可以编写B成员并通过其A成员读取charint,所以C++也一样:标准布局。

后者非常广泛,并且完全阻止对大多数structclass的数据成员重新排序。

注意,标准确实允许一些重新排序:由于C没有访问控制的概念,C++指定了两个具有不同访问控制说明符的数据成员的相对顺序。

据我所知,没有编译器试图利用它,但理论上他们可以。

在C++之外,诸如Ru锈这样的语言允许编译器重新排序字段,而主RISC编译器(RUSTC)默认是这样的。只有历史决定和强烈的向后兼容性要求才能阻止C++这样做。


I don't see any way you could shoot your self in the foot, if the struct was reordered.

真的?如果允许这样做,库/模块之间的通信,即使是在同一个过程中,默认情况下也是非常危险的。

"宇宙"论据

我们必须知道我们的结构是按照我们要求的方式定义的。填充未指定已经够糟糕了!幸运的是,您可以在需要时控制它。

好吧,理论上,一种新的语言可以被做成这样,类似地,成员可以重新排序,除非给出了一些属性。毕竟,我们不应该在对象上做内存级别魔术,所以如果只使用C++习语,默认情况下是安全的。

但这不是我们生活的现实。

"宇宙外"论点

用你的话说,如果"每次都使用相同的重新排序",你就可以确保事情的安全。该语言必须明确说明如何对成员进行排序。标准编写起来很复杂,理解起来很复杂,实现起来也很复杂。

只需保证顺序和代码中的顺序一样,并将这些决定留给程序员就容易多了。记住,这些规则源于旧的C,而旧的C为程序员提供了动力。

您已经在您的问题中展示了如何通过简单的代码更改使结构填充有效。不需要在语言级别增加任何复杂性来为您完成这项工作。


标准之所以保证分配顺序,仅仅是因为结构可以表示特定的内存布局,例如数据协议或硬件寄存器集合。例如,程序员和编译器都不能自由地重新安排TPC/IP协议中字节的顺序,也不能自由地重新安排微控制器的硬件寄存器。

如果订单没有得到保证,EDCOX1(0)将仅仅是抽象的数据容器(类似于C++向量),我们不能承担太多,除非它们以某种方式包含了我们放入的数据。在进行任何形式的低级编程时,这将使它们实际上更无用。


如果结构由其他编译器或其他语言生成的任何其他低级代码读取,则编译器应保持其成员的顺序。假设您正在创建一个操作系统,并且您决定用C编写它的一部分,用汇编编写它的一部分。您可以定义以下结构:

1
2
3
4
5
struct keyboard_input
{
    uint8_t modifiers;
    uint32_t scancode;
}

您将它传递给一个程序集例程,在那里您需要手动指定结构的内存布局。您希望能够在4字节对齐的系统上编写以下代码。

1
2
3
; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]

现在假设编译器将以实现定义的方式更改结构中成员的顺序,这意味着根据您使用的编译器和传递给它的标志,您可以以al中scancode成员的第一个字节结束,也可以以modifiers成员结束。

当然,问题不只是减少到与汇编例程的低级接口,而且如果使用不同编译器构建的库相互调用(例如,使用Windows API构建mingw程序),问题也会出现。

因此,语言只是强迫您考虑结构布局。


记住,不仅自动重新排序元素以改进打包可以损害特定的内存布局或二进制序列化,而且程序员可能已经仔细选择了属性的顺序,以使经常使用的成员的缓存位置对很少访问的成员有利。


你也引用C++,所以我会给你一个实际的原因,这是不可能发生的。

鉴于classstruct之间没有区别,考虑:

[cc lang="cpp"]class myclass{字符串S;另一对象B;myclass():s"你好


丹尼斯·里奇设计的语言定义了结构的语义,而不是行为,而是内存布局。如果结构s在偏移量x处具有类型t的成员m,则m.s的行为定义为获取s的地址,向其添加x字节,将其解释为指向t的指针,并将由此标识的存储解释为左值。写入结构成员将更改其关联存储的内容,更改成员存储的内容将更改成员的值。代码可以自由地使用各种各样的方法来操作与结构成员相关联的存储,并且可以根据对该存储的操作来定义语义。

代码操作与结构关联的存储的有效方法之一是使用memcpy()将一个结构的任意部分复制到另一个结构的相应部分,或者使用memset()清除结构的任意部分。由于结构成员是按顺序排列的,因此可以使用单个memcpy()或memset()调用复制或清除一系列成员。

标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储的要求,或对存储的更改影响成员值的要求,从而使结构布局的保证不如里奇的语言有用。尽管如此,仍然保留了使用memcpy()和memset()的能力,并且保留这种能力需要保持结构元素的顺序性。


假设这个结构布局实际上是一个"通过线"接收到的内存序列,比如一个以太网包。如果编译器重新对齐以提高效率,那么您将不得不以所需的顺序执行大量的工作,而不是仅仅使用一个结构,它以正确的顺序和位置拥有所有正确的字节。