内存寻址与内存对齐

948 阅读5分钟

以64位的CPU为例进行简单说明。
char       1字节
int          4字节
long       8字节

内存寻址

疑问:
在学习C语言结构体时提到了内存对齐,有如下说法:

对这段话的疑问是,为什么寻址的时候一定要是4的倍数呢,我从地址编号1的内存位置往后读取4个字节不是也行嘛?

现在假设内存寻址读取时可以从任意地址开始,结果将会如下所示:

就如深颜色框中所示,如果从“地址0”开始读取4位,然后再从“地址1”读取四位.......可以看出知道“地址4”,之前的读取都会出现重复读取。

所以,以4的倍数进行内存寻址,可以以最快的速度寻址:不遗漏一个字节,也不会重复对一个字节寻址

内存对齐

比方说如下代码:

#include <stdio.h> 
int main() {
    char str;
    int num;
    printf("%o\n", &str);
    printf("%o\n", &num);
    return 0;
}

假设内存段长这样:

预计输出结果我想的应该是如下图所示,一个类型紧挨着一个类型,也就是说应该相差一个地址:
但实际却不是,运行结果为:

str 的内存地址:30377437
num 的内存地址:30377430

可见,明显相差了 7 个地址

而关于为什么str的内存地址比num的要大,后面再说了。
从上图可以看出,已经发生了所谓的“内存对齐”的现象。

首先来了解一下数据总线和地址总线的概念。

CPU、地址总线和数据总线的关系图
上图是CPU、地址总线和数据总线的关系图。
数据总线通俗来讲就是传输数据的线路,而CPU到内存之间如果是64位操作系统的话就会有64根数据总线相连,32位的话则是32根;而每一根数据总线所能传输的就是0和1两种,64根数据总线代表着64个bit位,64bit = 8byte;所以,每次CPU能读取的数据就是8个字节大小,这点很重要。
地址总线同样的,64根地址总线连接CPU和内存,也就可以代表有2的64次方个地址。

先假设每一个类型的变量都一个紧挨着一个的在内存中存储,那么,如果按照上述代码的话,存储的时候,char会在“编号0”地址上,int会在“编号1”地址上,当读取的时候,一次性读取8字节大小数据,只需要读取一遍就可以把两个变量都能内存中读取出来,这样看来不是也挺好的吗?

现在再来看另外一段代码:

#include <stdio.h> 
int main() {
    char str;
    long num;
    printf("%o\n", &str);
    printf("%o\n", &num);
    return 0;
}

这一段代码中,我们把原先的int类型的num改成了long类型的,long类型在64位操作系统中占据8字节大小。

想象一下,按照一个紧挨着一个的话,会是怎样?

如上图所示,那么CPU按照一次性8字节读取的时候,如果我们想要读取long类型的变量,那么根据内存寻址规则,需要对8的倍数的内存寻址(64位操作系统的寻址步长是8,32位则是4),也就是说我们要去“地址0”上读取8个字节数据,获取long类型对应的字节内容A,然后再去“地址8”上读取8个字节数据,获取long类型对应的字节内容B,最后组合A和B得到最终的long数据。图示如下:

也就是说需要两次读取,并且还要重组才能获取到long这个数据。

那如果采用“内存对齐”填充的方式呢?再来看一张图:

采用“内存对齐”后,从“地址8”开始读取8个字节大小数据,由于long在64位系统中正好占据8个字节内存大小,所以只要读取这一次就可以获取需要的数据了。而灰色部分,从地址1到地址7就是内存填充后浪费的部分。换句话就是“以空间换时间”。

这时候还不算完,下面再来看一段代码吧:

#include <stdio.h> 
int main() {
    long num;
    int num1;
    char str;
    printf("%o\n", &num);
    printf("%o\n", &num1);
    printf("%o\n", &str);
    return 0;
}

上述代码中增加了一个int类型,并且声明顺序是按照内存大小从大到小的,与之前的代码生命顺序相反,我们来看看结果:

num 的内存地址:30377434
num1 的内存地址:30377430
str 的内存地址:30377427

可以看到,long和int相差7, int和char相差4,存储结构类似下图:

红色是long 8字节,蓝色是int 4字节,黄色是char 1字节。内存非常充分的利用起来了,完全没有填充浪费。

所以,在定义数据时,最好是按照内存大小从大到小来进行声明

以上理解并不全面,日后补充,都不是最终定论。