美文网首页C
C 迷你系列(三)内存对齐

C 迷你系列(三)内存对齐

作者: Tubetrue01 | 来源:发表于2021-02-09 13:25 被阅读0次

引言

现代计算机一般是 32 比特或 64 比特地址对齐,如果要访问的变量没有对齐,可能会触发总线错误。当数据小于计算机的字(word)尺寸,可能把几个数据元素放在一个字中,称为包入(packing)。许多编程语言自动处理数据结构对齐。C 语言或者汇编等语言允许特别控制对齐的方式。【维基百科】

示例

我们以这个例子为说明:

struct p
{
    char a;
    int b;
    short c;
    double d;
};
int main()
{
    printf("char : %lu\n", sizeof (char));
    printf("int : %lu\n", sizeof (int));
    printf("short : %lu\n", sizeof (short));
    printf("double : %lu\n", sizeof (double));
    printf("size of p is %lu\n", sizeof (struct p));
    return 0;
}

--- output
char : 1
int : 4
short : 2
double : 8
size of p is 24

问题

从以上例子中我们知道每个类型占用的字节大小,最后得出总的内存大小是 24 字节。如果说总的类型大小是 15 个字节的话,那么多出来的 9 个字节就是填充后的内存大小了,但是这个 24 字节是怎么算出来的呢?

分析

  • 首先 a 是结构体的第一个元素,所以我们假定它的地址为 0x0;a 是 char 类型,也就是占用一个字节。得出 b 的首地址是 0x0 + 1 = 0x1;
  • b 是 int 类型,占用 4 个字节,0x1 不能被 4 整除,所以需要补充额外的 3 个字节,让 b 的地址从 0x4 开始,同时得出 c 的地址 0x8;
  • c 是 short 类型,占用 2 个字节,0x8 的地址可以被 2 整除,所以不需要填充,从而得出 d 的地址为 0x10;
  • d 是 double 类型,占用 8 个字节,但是 0x10 不能被 8 整除,所以需要填充 6 个字节,即:0x16(只是为了说明方便,采用十进制说明);
  • 此时得出总的内存大小为 1 + 4 + 2 + 8 + 3(填充) + 6(填充) = 24 字节,该结构体中最大的内存类型为 double 8 字节,24 字节可以被 8 整除,所以不再需要填充。

内存图

image.png

验证

说了这么多,对不对呢?我们来验证一下:

int main()
{
    struct p val;
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    return 0;
}
--- output
address of a is 0x7ffeeca67740
address of b is 0x7ffeeca67744
address of c is 0x7ffeeca67748
address of d is 0x7ffeeca67750

比较幸运的是,地址跟我们例子里说明的一样都是 0 为起始地址,从地址上我们验证我们之前的分析是正确的。当然有一点需要注意的是,为了说明方便,这里地址用的是十进制,即 0x16,其实这是错的,因为地址是 16 进制。

补充

我们在分析的最后有提到过总的内存大小与结构体最大成员内存的整除关系,这是什么意思呢?其实就是说整个结构体是否需要内存填充进行对齐。我们看下边一个例子:

struct p
{
    char a;
    int b;
    short c;
    double d;
    short e;
};
int main()
{
    struct p val;
    printf("size of p is %lu\n", sizeof val);
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    printf("address of e is %p\n", &val.e);

    return 0;
}
--- output
size of p is 32
address of a is 0x7ffee624b738
address of b is 0x7ffee624b73c
address of c is 0x7ffee624b740
address of d is 0x7ffee624b748
address of d is 0x7ffee624b750

我们在结构体最后添加一个 short 类型的成员,结果整个内存变成了 32 字节,也就是说增加了 6 字节填充,这是为什么呢?因为增加了 short 之后,整个内存变成了 26 字节,但是 26 字节不能被结构体中最大的 double 8 字节整除,所以还需要在结构体最后补充 6 字节才行。

顺序很重要

从上述例子中,我们发现增加了 short 之后,整个内存变成了 32 字节。那么我们思考一下,结构体中字段不变,我们把它们的顺序变更一下,你认为它们的空间会发生变化吗?

struct p
{
    char a;
    int b;
    short c;
    short e;
    double d;
};

如上图,我们把最后的 short 往上移动了一下,让 e 在 d 的上边。猜猜内存如何变化?

size of p is 24
address of a is 0x7ffeebe98740
address of b is 0x7ffeebe98744
address of c is 0x7ffeebe9874a
address of d is 0x7ffeebe98750
address of e is 0x7ffeebe98748

它的内存就又变成了 24 字节,为什么,就是因为 e 提升之后,e 与 d 之间的内存填充发生了变化,如图:

image.png

所以,我们发现顺序也会影响内存的大小,这个需要特殊注意。

改变内存填充

那么有没有办法破坏这种填充,让程序员自己决定该如何填充呢?答案是肯定的,C 灵活的地方就在于你可以“任意”改变游戏规则。

  • 法一
struct __attribute__((packed)) p
{
    char a;
    int b;
    short c;
    short e;
    double d;
};
int main()
{
    struct p val;
    printf("size of p is %lu\n", sizeof val);
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    printf("address of e is %p\n", &val.e);

    return 0;
}
--- output
size of p is 17
address of a is 0x7ffeedbae748
address of b is 0x7ffeedbae749
address of c is 0x7ffeedbae74d
address of d is 0x7ffeedbae751
address of e is 0x7ffeedbae74f

总的内存大小就是实际的字段大小了,但是这里取字段地址的时候会提示警告信息:Taking address of packed member 'xx' of class or structure 'p' may result in an unaligned pointer value。

  • 法二
#pragma pack(1)
struct  p
{
    char a;
    int b;
    short c;
    short e;
    double d;
};
#pragma pack()

int main()
{
    struct p val;
    printf("size of p is %lu\n", sizeof val);
    printf("address of a is %p\n", &val.a);
    printf("address of b is %p\n", &val.b);
    printf("address of c is %p\n", &val.c);
    printf("address of d is %p\n", &val.d);
    printf("address of e is %p\n", &val.e);

    return 0;
}
--- output
size of p is 17
address of a is 0x7ffee3a3c748
address of b is 0x7ffee3a3c749
address of c is 0x7ffee3a3c74d
address of d is 0x7ffee3a3c751
address of e is 0x7ffee3a3c74f

#pragma pack(n) 其中 n 代表按照 n 字节进行填充,你可以随意指定,怎么样?是不是很灵活?当然,你要为你自己的程序负责哈!最后,#pragma pack() 用来取消当前设置的对齐方式,也就是说作用域限制在当前结构体。
最终内存图:


image.png

相关文章

  • C 迷你系列(三)内存对齐

    引言 现代计算机一般是 32 比特或 64 比特地址对齐,如果要访问的变量没有对齐,可能会触发总线错误[https...

  • sizeof与字节对齐

    参考 【面试题】sizeof引发的血案编译器与字节对齐c 语言字节对齐问题详解C/C++内存对齐内存存取粒度C和C...

  • 内存对齐

    在C语言柔性数组一文中,提到了内存对齐,于是想写篇文章总结总结内存对齐。 内存对齐 为什么需要内存对齐 计算机系统...

  • 内存对齐

    本次主要讨论三个问题: 什么是内存对齐 内存对齐的好处 如何对齐 内存对齐 内存对齐是一种提高内存访问速度的策略。...

  • golang 和 C++ 的内存对齐

    golang 和 C++的内存对齐,基本一致,记住规则和对应类型的 size 即可 内存对齐规则 有效对齐值是固定...

  • 内存对齐

    知识点概要 OC对象内存对齐结构体内存对齐 OC对象内存对齐 计算内存大小的三种方式 1.sizeof:系统提供的...

  • C/C++内存对齐

    在面试或工作中,经常会遇到内存对齐的问题。这里结合我的理解谈一谈对内存对齐的理解。 1. 为什么要内存对齐,不对齐...

  • C面试-内存对齐

    参考: 【嵌入式时代】C语言面试题详解(第7节),不知道“内存对齐”的程序员是不合格的 转载自:C语言的内存对齐 ...

  • c++内存对齐

    1、为什么要进行内存对齐呢? 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台...

  • 【C进阶】内存对齐

    码字不易,对你有帮助 点赞/转发/关注 支持一下作者微信搜公众号:不会编程的程序圆看更多干货,获取第一时间更新 想...

网友评论

    本文标题:C 迷你系列(三)内存对齐

    本文链接:https://www.haomeiwen.com/subject/xivqxltx.html