内存对齐和 char * 指针的妙用

什么是内存对齐

内存空间是按照字节进行编码地址的, 从编号0, 1, 2 ,..., 等等一直按照顺序对地址进行标号,这里的标号就是所谓的内存地址。
我们定义一个变量 int x = 1;,那么这个 &x 就是它的内存地址。理论上, &x 可能是 0x00c7f500, 可能是 0x00c7f501,也可能是 0x00c7f504,也可能是其他的任何值。只要从改地址开始往后的4个字节保留给这个 int x 就可以(假设 sizeof(int) 为 4字节,以下讨论也以4字节数据总线为例)。
但是,实际情况是内存地址的分配并不是随意的。内存中的数据需要经过数据总线传输到CPU中继续处理,而数据总线访问内存数据,很多处理器都是从偶数地址开始,一次访问4字节。
回到我们的例子,如果 &x 为 0x00c7f504,那么数据总线一次访问4个字节就可以把变量 x 读入CPU中。
而假如 &x 为 0x00c7f501,那么变量 x 的内存空间为: 0x00c7f501, 0x00c7f502, 0x00c7f503, 0x00c7f504 这4个字节。那么数据总线要把 0x00c7f500、0x00c7f501、0x00c7f502、0x00c7f503 4个字节读入CPU,这时候变量 x 还有一个字节0x00c7f504没有被读入。需要在下一个总线周期把 0x00c7f504 及以后的3个字节都读入后,才能完全把变量 x 的内存空间读入。这样,便需要两个总线周期才能访问变量 x 的所有内存空间。
综上,为了数据总线传输效率的考虑,我们把 x 的起始地址放在能被4整除的位置更好。
类似的道理,short 需要放在能被2整除的地址上,char 放在能被1整除的地址上(特殊的是,任何地址都可以被1整除)。对于 struct,其对齐要求为其所有成员变量的对齐值的最大值。

内存 Padding

由于变量的内存地址不是顺序安排的,而是要保证能被 sizeof(x) 整除,所以自然产生了内存 Padding的问题,如下:

struct A
{
    char ch;
    int x;
};
A a;

该 struct A 的对齐要求为其成员对齐值的最大值,也就是4字节对齐。
加入 &a 为地址 4,那么 &a.ch 为地址 4, 由于 int x 需要为4的倍数,所以,&a.x 为 8。这样,地址 5、6、7就是为了内存对齐而空闲的Padding。sizeof(A) 的值为8字节。

有趣的 char 变量的地址

不知道你发现了吗,short 变量要求地址能被2整除,也就是 0、2、4、6等等这样的偶数;int 变量要求地址能被4整除,也就是0、4、8、12等等这样的数;char 变量要求地址能被1整除,关键是所有的数都可以被1整除,所以 char 变量的地址可以在任何地方,0、1、2、3等等。
由于 char 变量地址可以取任何值,而且一个char就是一个字节。所以常常可以把其作为一个起始指针使用,如下的 ptr 指针:

struct A
{
    union {
        char ptr[1];
        int values[5];
    };
};
A a;

ptr 指针地址就是 &a,而且可以使用 ptr[1] 这样的方式访问具体哪个字节的数据。这在某些场景下,很有用,比如在实现 Swizzle 时。