地址/数据总线

地址总线
cpu 需要从内存读取数据,需要通过地址总线把地址传到内存,内存准备好数据,输出到地址总线,交给 cpu,如果地址总线只有8根, 那么地址只有8位,寻址空间为256个地址,想使用更大的内存,就需要有更宽的地址总线,32位地址总线可以寻址4G内存

数据总线
若数据总线为 8 位,则每次寻址后可操作 1 个字节。若想操作更多数据,就加宽数据总线,一次想操作4字节,就需要用32根数据总线,每次操作字节数就是机器字长(32 位/64 位计算机一般描述的是计算机数据总线宽度,即机器字长)
image.png
在这种数据操作模式下,存在如下问题:

  1. 若 CPU 数据总线宽度为 64 位,即 8 字节的机器字长,每次可操作 8 字节数据
  2. 需要操作的数据地址为 1-8,共 8 字节数据
  3. CPU 需要进行两次操作才可以完整读取 8 个字节,第一次读取 0-7 字节,第二次读取 8-15 字节,最后拼接需要的 8 字节
  4. 这种操作方式浪费了 CPU 资源,所以需要进行内存对齐
    image.png

内存对齐

内存对齐的概念

计算机的内存空间是按照字节进行划分的,但是实际情况,在访问特定的变量需要在特定的地址进行访问,这就需要类型数据按照特定规则排序,而不是按照顺序一个一个存放,这种叫做内存对齐
粒度:cpu 一次访问一块内存的大小,我们称之为粒度(即机器字长)
32位 CPU 粒度:4字节
64位CPU粒度:8字节
内存对齐作用:减少 CPU 访问内存次数,提高 CPU 读取内存效率(若设计不当,会存在空间浪费,内存对齐相当于用空间换时间)

结构体对齐规则

  1. 第一个成员在结构体变量偏移量为 0 的地址处,也就是第一个成员必须从头开始。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值
  3. 结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
  4. 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

实例 1

1
2
3
4
5
6
struct ss
{
int x;
char i;
short q;
}

char 1 int 4 short 2,实际占用 8 字节,实际内存情况如下:

int int int int char X short short
1 2 3 4 5 6 7 8

int 结束占用4,满足char 对齐条件( 4%1/ = 0)
char 结束占用5,不满足short 对齐条件(5%2!=0)
补齐 1 字节到 6,满足 short 条件( 6%2=0 )
存入short ,满足结构体条件( 8%4=0 )
整体占用 8位

实例 2

1
2
3
4
5
struct ss
{
int x;
char i;
}

char 1 int 4,实际占用 8 字节,实际内存情况如下:

int int int int char X X X
1 2 3 4 5 6 7 8

int 结束占用 4,满足 char 对齐条件( 4%1 != 0)
char 结束占用 5,不满足结构体对齐条件(5%4 !=0,结构体对齐长度为结构体最大成员长度)
补齐 6,7,8 字节,满足结构体对齐条件
整体占用 8 位

pragma pack

由于不是所有的硬件平台都能访问任意地址上的任意数据,为了提高读取效率需要做内存对齐

具体的内存对齐规则由编译器决定,一般编译器会指定一个默认的对齐数,可以后期修改

#pragma pack (n) 是一个编译器指令,它用来指定结构体,联合体,和类的成员的内存对齐方式

#pragma pack(1)的意思是,从这个指令开始,所有的结构体,联合体,和类的成员都按照1字节的边界对齐,也就是说,它们在内存中紧密地排列,没有任何的空隙或者填充。这样可以节省内存空间,但是也可能降低内存访问的效率,甚至导致一些平台上的错误。 #pragma pack(1)的使用方法是,在需要指定内存对齐方式的地方,写上 #pragma pack(1)这一行,它会影响它后面的所有的结构体,联合体,和类的定义,直到遇到下一个 #pragma pack 指令或者文件结束为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Test1 {
char a;
int b;
};

// 使用#pragma pack(1)指令,指定后面的结构体,联合体,和类的成员按照1字节的边界对齐
#pragma pack(1)
struct Test2 {
char a;
int b;
};
// 使用#pragma pack()指令,恢复默认的内存对齐方式
#pragma pack()

sizeof(Test1) = 8
sizeof(Test2) = 5

相关指令

#pragma pack(1)可以配合#pragma pack()#pragma pack(push)#pragma pack(pop)来使用,以恢复或保存内存对齐方式。#pragma pack()的含义和作用是,恢复默认的内存对齐方式,也就是按照编译器的设置来对齐。#pragma pack(push)#pragma pack(pop)的含义和作用是,保存和恢复内存对齐方式,也就是把当前的内存对齐方式压入一个栈中,然后在需要的时候弹出来。#pragma pack(push)#pragma pack(pop)的使用方法和注意事项是,#pragma pack(push)后面可以跟一个可选的参数,表示要压入栈的内存对齐方式,如果没有参数,就表示压入当前的内存对齐方式。#pragma pack(pop)后面可以跟一个可选的参数,表示要弹出栈的内存对齐方式,如果没有参数,就表示弹出最近压入的内存对齐方式。#pragma pack(push)#pragma pack(pop)必须成对出现,否则会造成栈的不平衡,导致一些错误。

#C语言