C语言结构体中的sizeof()和对齐方式:

7

前言: 我对结构体对齐做了研究。看过this这个问题,this还有this这个问题——但是仍然没有找到我的答案。

我的实际问题:

这里是我创建的代码片段,以澄清我的问题:

#include "stdafx.h"
#include <stdio.h>

struct IntAndCharStruct
{
    int a;
    char b;
};

struct IntAndDoubleStruct
{
    int a;
    double d;
};

struct IntFloatAndDoubleStruct
{
    int a;
    float c;
    double d;
};

int main()
{
    printf("Int: %d\n", sizeof(int));
    printf("Float: %d\n", sizeof(float));
    printf("Char: %d\n", sizeof(char));
    printf("Double: %d\n", sizeof(double));
    printf("IntAndCharStruct: %d\n", sizeof(IntAndCharStruct));
    printf("IntAndDoubleStruct: %d\n", sizeof(IntAndDoubleStruct));
    printf("IntFloatAndDoubleStruct: %d\n", sizeof(IntFloatAndDoubleStruct));
    getchar();
}

它的输出是:

Int: 4
Float: 4
Char: 1
Double: 8
IntAndCharStruct: 8
IntAndDoubleStruct: 16
IntFloatAndDoubleStruct: 16

我理解了IntAndCharStructIntAndDoubleStruct的对齐方式,但是我不理解IntFloatAndDoubleStruct的对齐方式。
简单来说:为什么sizeof(IntFloatAndDoubleStruct) != 24
谢谢! P.S:我在使用标准控制台应用程序,Visual-Studio 2017。 编辑: 根据评论,测试了IntDoubleAndFloatStruct(元素排序不同),并在sizeof()中得到24,如果答案也能注意并解释这种情况,那将是很好的。

2
交换float和double的顺序,就应该得到24。 - Gerhardh
1
你为什么期望得到24?如果没有使用double,你对int和float的期望是什么? - Gerhardh
1
@Gerhardh - 调换了顺序,确实得到了 24!至于我的期望,我希望对齐到最大的元素-所以 8 字节 * 3 元素= 24。int 和 float 大小相同(4),所以 8 在我看来没问题。 - MordechayS
1
我喜欢你研究的链接列表,而不是像其他问题那样只写“我读了所有东西”。干得好。 - Yunnosch
1
sizeof()的正确格式说明符是 "%zu" - Andrew Henle
显示剩余7条评论
3个回答

6
在您的平台上,以下内容适用: intfloat 的大小均为4。 double 的大小和对齐要求为8。
我们从您展示的sizeof输出中得知这一点。sizeof(T)给出了类型为T的两个连续元素地址之间的字节数。因此,我们知道对齐要求如上所述。(注)
现在,编译器报告IntFloatAndDoubleStruct的大小为16。它是否可行?
假设我们有一个地址对齐到16的对象。
- 因此,int a位于地址X,对齐到16,因此对于int来说很好。它将占用字节[X,X+4) - 这意味着float c可以从X + 4开始,对于float来说,这是对的。它将占用字节[X + 4,X + 8) - 最后,double d可以从X + 8开始,对于double来说,这是对的。它将占用字节[X + 8,X + 16) - 这样,剩下的X + 16就可以免费提供给下一个结构体对象,再次对齐到16。
因此,没有理由将成员中的任何一个后移,因此整个结构体可以很好地适配到16字节。
(注)这不是严格正确的:对于每个类型,我们都知道大小和对齐方式<= N,其中N是对齐要求的倍数,并且不存在N1 < N,也满足这一点。但是,这是非常微小的细节,为了清晰起见,答案仅假定原始类型的实际大小和对齐要求是相同的,在OP的平台上这是最可能的情况。

但是编译器不是将所有元素都对齐到相同的大小(8)吗?我以为'int'也应该对齐到8! - MordechayS
@MordechayS 为什么会这样呢?sizeof 实际上告诉你对齐要求。我已经详细解释了答案。 - Angew is no longer proud of SO
@Angew sizeof 只告诉原始类型的对齐要求。 - Tyker
@Angew: sizeof 不能告诉你对齐要求。一个处理器可能有8字节的 double 对象,但只需要它们是4字节对齐的。这可能发生是因为处理器只能访问4字节的内存,所以它需要使用两个访问来加载/存储双精度浮点数,无论其地址是0模8还是4模8,因此它不关心哪个是真实的。对象的对齐要求由 _Alignof 运算符报告。 - Eric Postpischil
@EricPostpischil 好的,这就是为什么我在评论中加上了“有效地”这个限定词。问题的表面水平是否值得如此精细的解答还有待商榷,但我已经在答案中添加了一个脚注。 - Angew is no longer proud of SO

5
您的结构体必须有 8*N 个字节,因为它包含一个 8 字节(double)的成员。这意味着结构体在内存中的地址(A)是 8 的倍数 (A%8 == 0),它的结束地址将是 (A+8N),也是 8 的倍数。
接下来,您存储了两个 4 字节变量(int + float),这意味着您现在占用了内存区域 [A,A+8)。现在,您存储了一个 8 字节变量(double)。由于 (A+8) % 8 == 0(因为 A%8 == 0),所以不需要填充。因此,没有填充,总共使用了 4+4+8 == 16 字节。
如果您更改顺序为 int -> double -> float,则将使用 24 个字节,因为 double 变量原始地址不是 8 的倍数,因此它必须填充 4 个字节才能获得有效地址(并且结构体末尾还会有填充)。
|--------||--------||--------||--------||--------||--------||--------||--------|
|   each ||   cell ||  here  ||represen||-ts  4  || bytes  ||        ||        |
|--------||--------||--------||--------||--------||--------||--------||--------|

A        A+4       A+8      A+12      A+16      A+20      A+24                      [addresses]
|--------||--------||--------||--------||--------||--------||--------||--------|    
|   int  ||  float || double || double ||        ||        ||        ||        |    [content - basic case]
|--------||--------||--------||--------||--------||--------||--------||--------|

first padding to ensure the double sits on address that is divisble by 8
last  padding to ensure the struct size is divisble by the largest member's size (8)
|--------||--------||--------||--------||--------||--------||--------||--------|    
|   int  || padding|| double || double || float  || padding||        ||        |    [content - change order case]
|--------||--------||--------||--------||--------||--------||--------||--------|

首先,我刚看完您的回答并学到了很多,非常感谢!但是,我仍然有一个问题:在“基本情况”中,为什么我们不需要浮点元素从可除以结构体最大元素(8)的地址开始? - MordechayS
你能更详细地解释一下结构体与元素对齐的区别吗? - MordechayS
让我澄清一下:如果对齐用于性能/简化-为什么我们允许将元素放在非8因子位置?(例如基本情况中的浮点元素) - MordechayS
  1. 性能/简单性和内存使用之间存在权衡。为了简单起见,我们可以仅允许8字节对齐浮点数,但这将增加成本。
  2. 至于为什么-不确定。可以推测这被认为是在内存中打包结构的正确方法-您将其切成块以适应最大类型(8字节),每个块您尽可能填充(2个4字节/ 1个4字节+ 4填充等)。
- CIsForCookies
你可能想要编辑你的问题评论,希望其他人会有更好的答案。 - CIsForCookies
显示剩余2条评论

0
编译器将插入填充以保证每个元素的偏移量是其大小的某个倍数。
在这种情况下,int 将位于偏移量=0(相对于结构实例的地址),float 位于偏移量=4,double 位于偏移量=8,因为 intfloat 的大小加起来等于 8。
末尾没有填充 - 结构的大小已经是 16,这是 double 大小的倍数。

处理器并不总是要求对象位于其大小的倍数地址上。在 OP 所问的特定情况下似乎成立,但处理器可能具有只需要四字节对齐的八字节“double”对象,或者可能具有需要 16 字节对齐的十字节对象。 - Eric Postpischil
@EricPostpischil:当然。还有另一种可能性:不需要对齐,但是推荐使用对齐访问。例如,假设OP的目标是x86/64,当地址未对齐时,用于加载double的MOVSD指令会更加昂贵-但仍然可行。 - joe_chip

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