你很德布罗意的技术博客

C/C++内存对齐讲解

1. 为什么需要内存对齐

内存对齐是C语言编译器对数据在内存存放位置的一种特殊处理,需要内存对齐主要有两个原因:

  • 因为C语言可以应用在很多架构的CPU中,而不同平台的CPU对内存读取的要求都不同,有些平台只支持在特殊的位置读取特殊的变量,一旦内存没有对齐,可能会直接抛出硬件异常,于是编译器会对结构体数据在内存中存放的位置按一定的规则进行排布,以适应特殊平台上的要求。
  • 对于某些特殊的数据结构,比如栈结构,需要连续的存储单元进行存储,如果内存没有对齐,寄存器读取一个数据需要两次读取操作,而内存对齐后只需要读取一次即可。

2. 对齐规则

每一种编译器都会定义自己的“对齐系数”(或称“对齐模数”),程序可使用预编译命令#pragma pack(n)命令来指定编译器的对齐系数,n=1,2,4,8,16等等。

  • 对于结构体(struct)或联合(union)的成员,第1个数据成员的偏移量固定为0,其他数据成员根据每个数据成员自身的数据长度和#pragma pack指定的对齐系数中较小的一方进行内存对齐,比如int类型的数据成员占用4字节长度,而对齐系数为2,则按照对齐系数2进行内存对齐,int类型的地址偏移量需要是2的倍数。

  • 结构体(或联合)的数据成员进行对齐之后,结构体(或联合)本身也需要进行内存对齐,根据结构体(或联合)中最大长度的数据成员的长度和#pragma pack指定的对齐系数中较小的一方进行内存对齐。结构体的大小(可用sizeof()函数获取)须为最大长度数据成员的长度的整数倍,当不足这个长度时会自动在最后填充字节扩充长度。

  • 结构体(或联合)作数据成员时,其地址偏移量必须为其数据成员的最长长度的整数倍。即两个结构体嵌套时,编译器会找出里面的结构体的数据成员中的最大长度,以这个长度作为其偏移地址的对齐系数,在内存地址中寻找其倍数的位置存储里面的结构体。

  • 如果#pragma pack指定的对齐系数大于每一个数据成员的长度,那么指定的对齐系数将不起效

    3. 代码实验

    本次代码在macOS Catalina 10.15.6上使用g++命令编译,版本信息如下

  • Apple clang version 12.0.0 (clang-1200.0.32.2)

  • Target: x86_64-apple-darwin19.6.0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<iostream>
    using namespace std;

    #pragma pack(1)
    // #pragma pack(2)
    // #pragma pack(4)
    // #pragma pack(8)
    // #pragma pack(16)
    struct test{
    short a;
    char b;
    int c;
    double d;
    int e[7];
    };
    #pragma pack()

    int main()
    {
    cout << "size of short = " << sizeof(short) << endl;
    cout << "size of char = " << sizeof(char) << endl;
    cout << "size of int = " << sizeof(int) << endl;
    cout << "size of double = " << sizeof(double) << endl;
    cout << "size of test = " << sizeof(test) << endl;
    }
  • 注意#pragma pack()命令只对接下来定义的结构生效,如果将其置于代码首行则include进来的iostream等等模块都会受到影响,从而运行报错,所以要将其置于stuct的上一行。定义完后需要用#pragma pack()恢复默认对齐系数。

  • 输出各类型的长度为:

    size of short = 2
    size of char = 1
    size of int = 4
    size of double = 8

  • #pragma pack(1)对齐情况:

    size of test = 43

    分析:

    • 数据成员对齐:
      对齐系数n=1;
      short a: 长度2>1,按1对齐;0%1=0,起始偏移0; 存放区间[0, 1];
      char b: 长度1==1,按1对齐;2%1=0,起始偏移2;存放区间[2];
      int c: 长度4>1,按1对齐;3%1=0,起始偏移3;存放区间[3, 6];
      double d: 长度8>1,按1对齐;7%1=0,起始偏移7;存放区间[7, 14];
      int e[7]: 长度7*4=28>1,按1对齐;15%1=0,起始偏移15;存放区间[15, 42];
      数据长度43

    • 结构体对齐:
      结构体长度基数 = min(max(short, char, int, double, int e[7]), 1) = 1
      43 % 1 = 0,于是结构体长度为43.

  • #pragma pack(2)对齐情况:

    size of test = 44

    分析:

    • 数据成员对齐:
      对齐系数n=2;
      short a: 长度2==2,按2对齐;0%2=0,起始偏移0; 存放区间[0, 1];
      char b: 长度1<2,按1对齐;2%1=0,起始偏移2;存放区间[2];
      int c: 长度4>2,按2对齐;4%2=0,起始偏移4;存放区间[4, 7];
      double d: 长度8>2,按2对齐;8%2=0,起始偏移8;存放区间[8, 15];
      int e[7]: 长度7*4=28>2,按2对齐;16%2=0,起始偏移16;存放区间[16, 43];
      数据长度44

    • 结构体对齐:
      结构体长度基数 = min(max(short, char, int, double, int e[7]), 2) = 2
      44 % 2 = 0,于是结构体长度为44.

  • #pragma pack(4)对齐情况:

    size of test = 44

    分析:

    • 数据成员对齐:
      对齐系数n=4;
      short a: 长度2<4,按2对齐;0%2=0,起始偏移0; 存放区间[0, 1];
      char b: 长度1<4,按1对齐;2%1=0,起始偏移2;存放区间[2];
      int c: 长度4==4,按4对齐;4%4=0,起始偏移4;存放区间[4, 7];
      double d: 长度8>4,按4对齐;8%4=0,起始偏移8;存放区间[8, 15];
      int e[7]: 长度7*4=28>4,按4对齐;16%4=0,起始偏移16;存放区间[16, 43];
      数据长度44

    • 结构体对齐:
      结构体长度基数 = min(max(short, char, int, double, int e[7]), 4) = 4
      44 % 4 = 0,于是结构体长度为44.

  • #pragma pack(8)对齐情况:

    size of test = 48

    分析:

    • 数据成员对齐:
      对齐系数n=8;
      short a: 长度2<8,按2对齐;0%2=0,起始偏移0; 存放区间[0, 1];
      char b: 长度1<8,按1对齐;2%1=0,起始偏移2;存放区间[2];
      int c: 长度4<8,按4对齐;4%4=0,起始偏移4;存放区间[4, 7];
      double d: 长度8==8,按8对齐;8%8=0,起始偏移8;存放区间[8, 15];
      int e[7]: 长度7*4=28>8,按8对齐;16%8=0,起始偏移16;存放区间[16, 43];
      数据长度44

    • 结构体对齐:
      结构体长度基数 = min(max(short, char, int, double, int e[7]), 8) = 8
      48 % 8 = 0,于是结构体长度为48,此时自动在最后一个数据成员后面填充了48-44=4个字节的无用数据

  • #pragma pack(16)对齐情况:

    size of test = 48

    分析:

    • 数据成员对齐:
      对齐系数n=16;
      short a: 长度2<16,按2对齐;0%2=0,起始偏移0; 存放区间[0, 1];
      char b: 长度1<16,按1对齐;2%1=0,起始偏移2;存放区间[2];
      int c: 长度4<16,按4对齐;4%4=0,起始偏移4;存放区间[4, 7];
      double d: 长度8<16,按8对齐;8%8=0,起始偏移8;存放区间[8, 15];
      int e[7]: 长度7*4=28>16,按16对齐;16%16=0,起始偏移16;存放区间[16, 43];
      数据长度44

    • 结构体对齐:
      结构体长度基数 = min(max(short, char, int, double, int e[7]), 16) = 16
      48 % 16 = 0,于是结构体长度为48,此时自动在最后一个数据成员后面填充了48-44=4个字节的无用数据

可以看出,指定不同的对齐系数所得出的结构体的大小信息都不一样,笔试和面试当中都可能问到结构体的大小问题,大家要根据内存对齐的规则分析数据大小和结构体大小才能得出正确的结论哦。

再来看看嵌套结构体的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;

struct A{
short a;
char b;
int c;
double d;
int e[7];
};

struct B{
float f;
A a;
};

int main()
{
cout << "float大小:" << sizeof(float) << endl;
B b;
cout << "b的内存地址:" << &b << endl;
cout << "b.a的内存地址" << &(b.a) << endl;
cout << "b.a的偏移量:" << (uintptr_t)&(b.a) - (uintptr_t)&(b) << endl;
}

程序先定义了一个结构A,再定义了一个结构B,其中B有一个类型为结构A的数据成员,那么按照对齐规则,程序在存放成员B.a时会寻找结构A中最宽的成员的长度,以其倍数的偏移量存放B.a,我本以为会按int e[7]的长度=28的倍数来存放,但实际测试发现是按照double类型为最长来对齐的,可能编译器认为int数组也是按int类型处理吧~ 结果输出b.a的内存偏移是8

float大小:4
b的内存地址:0x7ffee92906b8
b.a的内存地址0x7ffee92906c0
b.a的偏移量:8

专题:

本文发表于 2020-10-22,最后修改于 2020-10-22。

本站永久域名www.namidame.tech,也可搜索「 你很德布罗意的技术博客 」找到我。

欢迎关注我的 微博 ,查看最近的文章和动态。


上一篇 « 求买卖股票的最大利润 下一篇 » 旋转数组

赞赏支持

客官,打赏一个呗~

i ali

支付宝

i wechat

微信

推荐阅读

Big Image