MSVC 下 C/C++ 结构体内存对齐与填充规则

版权声明:原创文章,未经授权,请勿转载

MSVC 系列编译器下 C/C++ 结构体成员在内存中的布局,主要受编译器的打包对齐规则影响。

而所谓的打包就是指将结构体成员按照特定的规则映射到内存中,这样可以使某些硬件架构能够更快地访问数据,并且结构体在存储时更紧凑,从而节省空间。

MSVC 系列的打包对齐主要由下面两个方面决定:

  1. /Zp 编译器选项
  2. #pragma pack([n]) 预处理器指令

两者的作用是一样的,都是控制结构体成员对齐的规则,而主要区别在于前者作用于在整个文件的编译期间,而后者书写在源代码中,影响后面的所有代码,并且后者可以覆盖前者。

/Zp 编译器选项

/Zp 是一个编译器选项,用于控制如何将结构成员打包到内存中,并在模块中为所有结构指定同一包装 [1]

  • 1 将结构打包在 1 字节边界上。与 /Zp 一样。
  • 2 在 2 字节边界上打包结构。
  • 4 在 4 字节边界上打包结构。
  • 8 在 8 字节边界上打包结构(x86、ARM 和 ARM64 的默认值)。
  • 16 在 16 字节边界上打包结构(x64 和 ARM64EC 的默认值)。

MSVC x86 体系下 默认的 /Zp8,所以结构体成员对齐到 8 字节边界上。

MSVC x64 体系下 默认的 /Zp16

#pragma pack([n]) 预编译指令

#pragma pack([n]) 是一个预编译指令,用于设置结构体成员对齐的规则 [2]

其中n为 1、2、4、8 或 16,作用同/Zp。如果使用不带参数的#pragma pack,结构成员将打包为 /Zp 指定的值。

对齐规则

  • 结构成员按其声明顺序进行存储 [3]
    • 第一个成员的内存地址最低,最后一个成员的内存地址最高。
    • 每个成员在内存中的位置偏移量记作 offset, 第一个成员的偏移量总是 offset 0
  • 结构成员的 offset 分配依赖于 对齐需求 alignment-requirement,而这个 对齐需求 需要满足如下公式:
    • alignment-requirement = min(n, sizeof(item))
    • offset % alignment-requirement == 0
    • 其中 n 是使用 /Zp[n] 选项 或者 #pragma pack(n) 杂注 表示的包装大小,而 item 是结构成员。 默认包装大小为 /Zp8
    • 这种 对齐需求 也叫做 字节边界
  • 经过 对齐需求 分配 offset 后, 结构成员之间会出现 间隙,这个间隙也可以称为 填充

参考链接

测试代码

// test.cpp
#include <stddef.h>
#include <stdio.h>

// MSVC x86 default alignment is 8 because of /Zp8
#pragma pack(show)
struct S {
    int    a;    // offset 00, size 04, alignment 4
    short  b;    // offset 04, size 02, alignment 2
    double c;    // offset 08, size 08, alignment 8
};

struct C {
    char   a;    // offset 00, size 01, alignment 1
    short  b;    // offset 02, size 02, alignment 2
    double c;    // offset 08, size 08, alignment 8
    int    d;    // offset 16, size 04, alignment 4
    char   e;    // offset 20, size 01, alignment 1
    double f;    // offset 24, size 08, alignment 8
};

#pragma pack(2)  // set alignment to 2 overwrite /Zp8
#pragma pack(show)
struct T {
    char   a;    // offset 00, size 01, alignment 1
    int    b;    // offset 02, size 04, alignment 2
    double c;    // offset 06, size 08, alignment 2
    short  d;    // offset 14, size 02, alignment 2
    char   e;    // offset 16, size 01, alignment 1
    int    f;    // offset 18, size 04, alignment 2
};
#pragma pack()   // restore default alignment to 8
#pragma pack(show)

#ifndef max
#   define max(a, b) (((a) > (b)) ? (a) : (b))
#endif

#ifndef min
#   define min(a, b) (((a) < (b)) ? (a) : (b))
#endif

#ifndef offsetof
#   define offsetof(s,m) ((size_t)&(((s*)0)->m))
#endif

#define memsize(s,m) (sizeof(((s*)0)->m))
#define memptrint(s,m,a) \
    printf("%s::%s offset %02d, size %02d, alignment %d \n", \
        #s, #m, offsetof(s, m), memsize(s,m), min(a, memsize(s,m))); \

#ifndef PACKALIGN
#   define PACKALIGN 8
#endif

int main() 
{
    memptrint(S, a, PACKALIGN);
    memptrint(S, b, PACKALIGN);
    memptrint(S, c, PACKALIGN);

    printf("\n");
    memptrint(C, a, PACKALIGN);
    memptrint(C, b, PACKALIGN);
    memptrint(C, c, PACKALIGN);
    memptrint(C, d, PACKALIGN);
    memptrint(C, e, PACKALIGN);
    memptrint(C, f, PACKALIGN);

    printf("\n");
    memptrint(T, a, 2);
    memptrint(T, b, 2);
    memptrint(T, c, 2);
    memptrint(T, d, 2);
    memptrint(T, e, 2);
    memptrint(T, f, 2);
}

下面是MSVC x86平台下的编译结果及输出:

$ ..\x86\cl.exe /Zi /DDEBUG=1 test.cpp && test.exe

test.cpp
test.cpp(6): warning C4810: pragma pack(show) 的值 == 8
test.cpp(23): warning C4810: pragma pack(show) 的值 == 2
test.cpp(33): warning C4810: pragma pack(show) 的值 == 8
Microsoft (R) Incremental Linker Version 14.29.30153.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:test.exe
/debug
test.obj
S::a offset 00, size 04, alignment 4
S::b offset 04, size 02, alignment 2
S::c offset 08, size 08, alignment 8

C::a offset 00, size 01, alignment 1
C::b offset 02, size 02, alignment 2
C::c offset 08, size 08, alignment 8
C::d offset 16, size 04, alignment 4
C::e offset 20, size 01, alignment 1
C::f offset 24, size 08, alignment 8

T::a offset 00, size 01, alignment 1
T::b offset 02, size 04, alignment 2
T::c offset 06, size 08, alignment 2
T::d offset 14, size 02, alignment 2
T::e offset 16, size 01, alignment 1
T::f offset 18, size 04, alignment 2

指定 /Zp1 后,结构体成员对齐到1字节边界上:

$ ..\x86\cl.exe /Zi /Zp1 /DPACKALIGN=1 /DDEBUG=1 test.cpp && test.exe

test.cpp
test.cpp(6): warning C4810: pragma pack(show) 的值 == 1
test.cpp(23): warning C4810: pragma pack(show) 的值 == 2
test.cpp(33): warning C4810: pragma pack(show) 的值 == 1
Microsoft (R) Incremental Linker Version 14.29.30153.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:test.exe
/debug
test.obj
S::a offset 00, size 04, alignment 1
S::b offset 04, size 02, alignment 1
S::c offset 06, size 08, alignment 1

C::a offset 00, size 01, alignment 1
C::b offset 01, size 02, alignment 1
C::c offset 03, size 08, alignment 1
C::d offset 11, size 04, alignment 1
C::e offset 15, size 01, alignment 1
C::f offset 16, size 08, alignment 1

T::a offset 00, size 01, alignment 1
T::b offset 02, size 04, alignment 2
T::c offset 06, size 08, alignment 2
T::d offset 14, size 02, alignment 2
T::e offset 16, size 01, alignment 1
T::f offset 18, size 04, alignment 2