假设我有这样的结构:
struct MyStruct
{
uint8_t var0;
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
};
这可能会浪费很多(不是很多)空间。这是因为uint32_t
变量的必要对齐。
实际上(在对齐结构以便它实际上可以使用uint32_t
变量之后),它可能看起来像这样:
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;
};
更高效的结构将是:
struct MyStruct
{
uint8_t var0;
uint8_t var2;
uint8_t var3;
uint8_t var4;
uint32_t var1;
};
现在,问题是:
为什么编译器禁止(按标准)重新排序结构?
如果对结构进行重新排序,我不会发现你可以用脚射击自己。
答案 0 :(得分:34)
为什么编译器禁止(按标准)重新排序结构?
基本原因是:与C兼容。
请记住,C最初是一种高级汇编语言。在C中通过将字节重新解释为特定的struct
来查看内存(网络数据包......)是很常见的。
这导致依赖于此属性的多个功能:
C保证struct
的地址和第一个数据成员的地址是同一个,所以C ++也这样做(在没有virtual
继承/方法的情况下)
C保证,如果您有两个struct
A
和B
,并且两者都以数据成员char
开头,后跟数据成员{{1} (以及之后的任何内容),然后当您将它们放入int
时,您可以撰写union
成员并通过其B
阅读char
和int
成员,所以C ++也这样做:Standard Layout。
后者极其广泛,并完全阻止对大多数A
(或struct
)的数据成员进行重新排序。
请注意,标准允许一些重新排序:由于C没有访问控制的概念,C ++指定具有不同访问控制说明符的两个数据成员的相对顺序是未指定的。
据我所知,没有编译器试图利用它;但他们理论上可以。
在C ++之外,Rust之类的语言允许编译器重新排序字段,而主要的Rust编译器(rustc)默认情况下这样做。只有历史决策和强烈的向后兼容性才能阻止C ++这样做。
答案 1 :(得分:27)
如果对结构进行重新排序,我认为你没有任何方法可以用脚射击自己。
真的?如果这是允许的,默认情况下,即使在同一过程中,库/模块之间的通信也是非常危险的。
我们必须知道我们的结构是按照我们要求它们的方式定义的。填充是未指定的,这已经够糟糕了!幸运的是,您可以在需要时控制它。
好的,从理论上讲,可以制作一种新语言,类似地,成员可以重新订购,除非给出某些属性。毕竟,我们不应该对对象进行内存级别的魔术,所以如果只使用C ++习语,那么默认情况下你是安全的。
但这不是我们生活的现实。
如果用你的话说“每次都使用相同的重新排序”,你就可以把事情安全起来。该语言必须明确说明如何订购成员。标准写作复杂,理解复杂,实施起来很复杂。
只是保证订单与代码一样容易,并将这些决定留给程序员。请记住,这些规则源于旧的C,旧的C赋予程序员的权力。
您已经在问题中展示了通过简单的代码更改使结构填充有效是多么容易。在语言层面上不需要为你做任何增加的复杂性。
答案 2 :(得分:13)
该标准保证了分配顺序,因为结构可能代表某种内存布局,例如数据协议或硬件寄存器集合。例如,程序员和编译器都不能自由地重新排列TPC / IP协议中的字节顺序或微控制器的硬件寄存器。
如果订单无法保证,<div class="row">
<!-- Left half of the screen to hold list of words -->
<div class="col col-50" align="center">
<ion-list>
<ion-item ng-repeat="item in word_pair">
{{item.word}}
</ion-item>
</ion-list>
</div>
<!-- Right half of the screen to hold list of pairs -->
<div class="col col-50"">
<ion-list>
<ion-item ng-repeat="item in word_pair">
{{item.pair}}
<input type="text">
<span><i class="ion-checkmark"></i></span>
</ion-item>
</ion-list>
</div>
</div>
将仅仅是抽象数据容器(类似于C ++向量),我们不能假设它们,除了它们以某种方式包含我们放在其中的数据。在进行任何形式的低级编程时,它会使它们变得毫无用处。
答案 3 :(得分:6)
如果结构被其他编译器或其他语言生成的任何其他低级代码读取,编译器应保持其成员的顺序。假设您正在创建一个操作系统,并且您决定在C中编写部分操作系统,并在部署中编写部分操作系统。您可以定义以下结构:
struct keyboard_input
{
uint8_t modifiers;
uint32_t scancode;
}
将此传递给程序集例程,您需要手动指定结构的内存布局。您希望能够在具有4字节对齐的系统上编写以下代码。
; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]
现在说编译器会以实现定义的方式更改结构中成员的顺序,这意味着根据您使用的编译器和传递给它的标志,您可能最终得到第一个字节al中的scancode成员,或者使用修饰符成员。
当然问题不仅仅是简化为使用汇编程序的低级接口,而且如果使用不同编译器构建的库会相互调用(例如使用windows API构建带有mingw的程序),也会出现问题。
因此,语言只会迫使你考虑结构布局。
答案 4 :(得分:5)
请记住,不仅自动重新排序元素以改善打包可能会损害特定的内存布局或二进制序列化,但程序员可能已经仔细选择了属性的顺序以使常常的缓存局部性受益使用成员反对更少访问。
答案 5 :(得分:4)
你也引用了C ++,所以我会给你一个实际的理由,说明为什么会发生这种情况。
鉴于there's no difference between class
and struct
,请考虑:
class MyClass
{
string s;
anotherObject b;
MyClass() : s{"hello"}, b{s}
{}
};
现在C ++要求按照声明的顺序初始化非静态数据成员:
- 然后,按照它们的顺序初始化非静态数据成员 在类定义中声明
根据[base.class.init/13
]。因此编译器不能重新排序类定义中的字段,因为否则(作为示例)成员取决于其他人的初始化无法工作。
编译器并不是严格要求不在内存中重新排序(我可以说) - 但是,特别是考虑到上面的例子,跟踪它会非常痛苦。而且我怀疑任何性能改进,不像填充。
答案 6 :(得分:3)
Dennis Ritchie设计的语言定义了结构的语义,而不是行为,而是根据内存布局。如果结构S在偏移X处具有类型T的成员M,那么MS的行为被定义为取S的地址,向其添加X字节,将其解释为指向T的指针,并将由此识别的存储解释为一个左值。编写结构成员将更改其关联存储的内容,并且更改成员存储的内容将更改成员的值。代码可以自由地使用各种方法来操作与结构成员相关联的存储,并且语义将根据对该存储的操作进行定义。
代码可以操作与结构相关联的存储的有用方法之一是使用memcpy()将一个结构的任意部分复制到另一个结构的相应部分,或者使用memset()来清除任意部分的结构。结构体。由于结构成员按顺序排列,因此可以使用单个memcpy()或memset()调用复制或清除一系列成员。
标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储,或者存储的更改会影响成员值的要求,从而使结构布局的保证不如它们在里奇的语言。尽管如此,仍然保留了使用memcpy()和memset()的能力,并保留了保持结构元素顺序的能力。
答案 7 :(得分:-1)
想象一下,这个结构布局实际上是一个通过线路接收的内存序列,比如一个以太网数据包。如果编译器重新对齐事物以提高效率,那么你将不得不按所需顺序执行大量工作,而不是仅使用具有正确顺序和位置的所有正确字节的结构。