9

当我回答一个问题时只有 20 分钟,我想出了一个有趣的场景,但我不确定该行为:

让我有一个大小为 n 的整数数组,由 intPtr 指向;

int* intPtr;

让我也有一个这样的结构:

typedef struct {
int val1;
int val2;
//and less or more integer declarations goes on like this(not any other type)
}intStruct;

我的问题是如果我做演员intStruct* structPtr = (intStruct*) intPtr;

如果我遍历结构的元素,我确定能正确获取每个元素吗?在任何架构/编译器中是否存在未对齐的可能性(可能由于填充)?

4

6 回答 6

5

该标准相当具体,即使是 POD 结构(我相信最严格的结构类)也可以在成员之间进行填充。(“因此,在 POD 结构对象中可能存在未命名的填充,但不是在其开头,这是实现适当对齐所必需的。”——非规范性注释,但仍使意图非常清楚)。

例如,对比标准布局结构的要求(C++11,§1.8/4):

普通可复制或标准布局类型 (3.9) 的对象应占用连续的存储字节。”

...与数组的那些(§8.3.4/1):

数组类型的对象包含一个连续分配的非空集合,由 N 个类型为 T 的子对象组成。

在数组中,元素本身需要连续分配,而在结构中,只需要存储是连续的。

可能使“连续存储”要求更有意义的第三种可能性是考虑不可简单复制或标准布局的结构/类。在这种情况下,存储可能根本不连续。例如,一个实现可能会留出一个内存区域来保存所有私有变量,并留出一个完全独立的内存区域来保存所有公共变量。为了更具体一点,请考虑以下两个定义:

class A { 
    int a;
public:
    int b;
} a;

class B {
    int x;
public:
    int y;
} b;

有了这些定义,内存的布局可能类似于:

a.a;
b.x;

// ... somewhere else in memory entirely:

a.b;
b.y;

在这种情况下,元素存储都不需要是连续的,因此完全独立的结构/类的交错部分是允许的。

也就是说,第一个元素必须与整个结构位于同一地址 (9.2/17):“指向 POD 结构对象的指针,使用 reinterpret_cast 适当转换,指向其初始成员(或者如果该成员是一个位域,然后是它所在的单元),反之亦然。”

在您的情况下,您有一个 POD 结构,因此(第 9.2/17 节):“指向 POD 结构对象的指针,使用 reinterpret_cast 适当转换,指向其初始成员(或者如果该成员是位字段,然后到它所在的单元),反之亦然。” 由于第一个成员必须对齐,并且其余成员都是相同的类型,因此其他成员之间不可能真正需要任何填充(即,除了位字段,您可以放入结构中的任何类型也可以放入数组,其中需要连续分配元素)。如果您的元素小于一个单词,那么在面向单词的机器(例如早期的 DEC Alphas)上,填充可能会使访问变得更简单一些。例如,早期的 DEC Alpha(在硬件级别)一次只能读/写一个完整的(64 位)字。因此,让我们考虑类似四个char元素的结构:

struct foo { 
   char a, b, c, d;
};

如果需要将它们布置在内存中以使它们是连续的,则访问foo::b(例如)将需要 CPU 加载该字,然后将其向右移动 8 位,然后屏蔽以对该字节进行零扩展以填充整个登记。

存储会更糟——CPU 必须加载整个单词的当前值,屏蔽掉相应字符大小的部分的当前内容,将新值移动到正确的位置,或者将其放入单词中,最后存储结果。

相比之下,通过元素之间的填充,每个元素都成为一个简单的加载/存储,没有移位、屏蔽等。

至少如果内存服务,使用 DEC 的 Alpha 正常编译器,int是 32 位,并且long是 64 位(它早于long long)。因此,使用 4 个ints 的结构,您可能会期望在元素之间看到另外 32 位的填充(以及最后一个元素之后的另外 32 位)。

鉴于您确实有一个 POD 结构,但您仍然有一些可能性。我可能更喜欢使用offsetof获取结构成员的偏移量,创建它们的数组,并通过这些偏移量访问成员。我在之前的几个答案中展示了如何做到这一点。

于 2012-08-29T15:22:33.000 回答
3

严格来说,这种指针转换是不允许的,并且会导致未定义的行为。

然而,强制转换的主要问题是编译器可以自由地在结构内的任何位置添加任意数量的填充字节,除了第一个元素之前。所以它是否起作用取决于特定系统的对齐要求,以及是否启用了struct padding。

int不一定与可寻址数据块的最佳大小具有相同的大小,即使对于大多数 32 位系统来说都是如此。有些 32 位不关心错位,有些允许错位但产生效率较低的代码,有些必须对齐数据。理论上,64 位可能还希望在 int 之后添加填充(那里将是 32 位)以获得 64 位块,但实际上它们支持 32 位指令集。

如果您编写依赖此转换的代码,您应该添加如下内容:

static_assert (sizeof(intStruct) == 
               sizeof(int) + sizeof(int));
于 2012-08-27T08:22:36.627 回答
3

鉴于元素类型是标准布局,它保证是合法的。注意:以下所有引用均指向标准。

8.3.4 数组[dcl.array]

1 - [...] 数组类型的对象包含一组连续分配的非空子N对象类型T。[...]

关于struct带有Ntype 成员的a T

9.2 类成员 [class.mem]

14 - 分配具有相同访问控制的(非联合)类的非静态数据成员,以便后面的成员在类对象中具有更高的地址。[...] 实现对齐要求可能会导致两个相邻的成员不能立即分配 [...]
20 - 指向标准布局结构对象的指针,使用 a 进行适当转换reinterpret_cast,指向其初始成员 [. ..] 反之亦然。[注意:因此,标准布局结构对象中可能存在未命名的填充,但不是在其开头,这是实现适当对齐所必需的。——尾注]

所以问题是 a 中任何需要对齐的填充是否struct会导致其成员彼此之间不连续分配。答案是:

1.8 C++ 对象模型 [intro.object]

4 - [...] 普通可复制或标准布局类型的对象应占用连续的存储字节。

换句话说,struct a包含至少两个相同(标准布局)类型且不尊重身份的成员的标准布局x违反了 1.8:4。y&a.y == &a.x + 1

请注意,齐定义为 ( 3.11 对齐 [basic.align] )可以分配给定对象的连续地址之间的字节数;因此,一个类型的对齐T不能大于 的数组中相邻对象之间的距离T,并且(因为5.3.3 Sizeof [expr.sizeof]指定n 个元素的数组的大小是的大小的n倍一个元素)不能大于。因此,不需要在相同类型的结构的相邻元素之间进行任何额外的填充alignof(T)sizeof(T)通过对齐,因此不会被 9.2:14 支持。


关于 AProgrammer 的观点,我会将 26.4 复数 [complex.numbers] 中的语言解释要求std::complex<T>标准布局类型的要求。

于 2012-08-29T10:07:56.603 回答
2

那里的行为几乎可以肯定是编译器、体系结构和 ABI 相关的。但是,如果您使用 gcc,则可以使用__attribute__((packed))强制编译器一个接一个地打包结构成员,而无需任何填充。这样,内存布局应该与平面数组的布局相匹配。

于 2012-08-29T11:33:58.417 回答
1

当我前段时间搜索时,我没有发现任何东西可以保证它是有效的,并且我发现了 C++ 中 std::complex<> 情况的明确保证,如果它更普遍正确,可以更容易地制定,所以我怀疑我在搜索中遗漏了一些东西(但缺乏证据很难证明不存在,标准有时在其表述中是模糊的)。

于 2012-08-27T08:24:12.787 回答
1

C 结构的典型对齐方式保证结构中的数据结构成员将按顺序存储,这与 C 数组相同。所以订单不会是问题。

在对齐方面,由于您只有一种数据类型(int),尽管编译器有资格这样做,但没有必要添加填充来对齐您的数据成员。编译器可以在结构的开头添加填充,但不能在数据结构的开头添加填充。因此,如果编译器要在您的情况下添加填充,

而不是这个:[4Byte int][4Byte int][4Byte int]...[4Byte int]

您的数据结构必须像这样存储:
[4Byte Data] [4Byte Padding] [4Byte Data]... 这是不合理的。

总的来说,我认为这个演员应该在你的情况下没有问题,尽管我认为使用它是不好的做法。

于 2012-08-30T21:22:08.030 回答