本文包含原理图、PCB、源代码、封装库、中英文PDF等资源
您需要 登录 才可以下载或查看,没有账号?注册会员
×
全文请见附件
C语言字节对齐.pdf
(262.59 KB, 下载次数: 13)
在word里写好了,在论坛里不好排版,请下载附件观看全文
全文章节:
- 字节对齐的由来
- 字节对齐规则
- 非字节对齐访问
- 非字节对齐的方法
- 非字节对齐类型的字节对齐规则
- 非字节对齐的影响
字节对齐的由来
程序在运行时会将数据临时存放在内存中,芯片内核需要对这些数据进行计算,不断的
读取内存以获得数据,并将计算结果写入内存。计算机体系经过若干年的发展,最终确定了
以8bits 作为其基本的存储单元——byte(字节),这是每个地址所对应的最小访问单元,
在C 语言中对应一个char 型的变量。
下图为芯片内核访问内存的示意图。芯片内核通过控制总线控制内存的动作,通过地址
总线告知内存地址,数据总线上出现交互的数据。
图1 访问内存示意图
假设上图是8 位机的示意图,那么数据总线的宽度是8bits,由8 根数据线组成,这样
芯片内核与内存之间一次就可以同时交换8 个bits 的数据,正好是一个字节。图中右侧的
每个小格子代表一个存储地址,对应一个字节。
下面通过一段C 语言代码来具体看看芯片内核与内存之间的数据交互过程。
char data[2];
data[0] = 2;
data[1] = data[0] + 1;
第一行代码定义了2 个字节的数组data。假设data 数组被编译到地址0x100,那么
data[0]这个字节就被存储在地址为0x100 的内存空间,data[1]这个字节就被存储在地址为
0x101 的内存空间。
第二行对应的硬件动作是将数据2 存入到data[0]中,也就是将数据2 存入到内存中的
0x100 地址,执行这条语句时,芯片内核对控制总线、地址总线和数据总线进行操作,控制
总线上出现写信号,地址总线上出现数据0x100,数据总线上出现数据0x02。此时内存就
知道需要将数据2 写入到地址0x100 中,完成一次写操作。
第三行先读出data[0]中的数据,芯片内核将控制总线置为读信号,将地址总线置为
0x100,此时,内存就会从其内部取出0x100 地址中的数据,也就是数据2,2 将出现在数
据总线上,此时芯片内核就会通过数据总线读取到data[0]中的数据了。接下来芯片内核计
算2+1=3,需要将数字3 写入到data[1]中,芯片内核将控制总线置为写信号,将地址总线
置为0x101,将数据总线置为3,内存接收到这些信号后,就会将数据3 存入到其内部0x101
地址中,完成本次操作。
内存内部细节
数据总线
地址总线
芯片内核 内存 0x100 2 3
控制总线
从上述介绍的过程可以看出,芯片内核与存储芯片之间每次操作可以传递1 个字节的
数据,如果要传递多个字节的数据就需要重复这个过程,这受限于数据总线的宽度。
计算机技术在不断的发展,在8bits 数据总线之后又相继出现了16bits、32bits 乃至
64bits 数据总线,它们分别对应于我们所谓的8 位机、16 位机、32 位机以及64 位机。对
于16 位机一次可以交互2 个字节的数据,32 位机一次可以交互4 个字节的数据,64 位机
一次可以交互8 个字节的数据,可以看出总线的带宽增加了,速度成倍提高。
以32 位机为例,我们在访问0 地址时,可以一次访问4 个字节的数据,这4 个字节的
数据占用了4 个内存地址,也就是说访问0 地址时同时可以访问0、1、2、3 这4 个地址,
访问4 地址时可以同时访问4、5、6、7 这4 个地址。我们不难得出这样的结论:在地址总
线上只要出一个地址,就可以连同访问这个地址及其后面的3 个地址中的数据,这4 个地
址正好可以组成一个32bits 的数据,通过访问数据总线一次即可获得,而对这个地址的要
求就是:需要4 字节对齐(对于64 位机则需要8 字节对齐)。在芯片设计时遵循了这个要
求,地址总线上只需要出现0、4、8……这样4 的整数倍的地址就可以同时访问连续4 个字
节的内存空间,这就是字节对齐的根源——是由硬件决定的!为了配合硬件的4 字节对齐
访问,软件的编译器链接器也对软件做了限制,需要4 字节对齐访问。
有关计算机的设计五花八门,上述有关控制总线、地址总线、数据总线的介绍只是原理性的介绍,不同芯片在具体实现时
会有所不同。
字节对齐规则
我们在写代码时一般并不会指定变量存放在内存中的地址,这是由编译器链接器决定
的,而编译器链接器则遵循了4 字节对齐的原则,以32 位机为例,其规则是1 字节长度的
变量可以被编译链接到任何地址,2 字节长度类型的变量被编译链接到2 的整数倍的地址,
4 字节长度类型的变量被编译链接到4 的整数倍的地址。因此,取signed/unsigned char
类型变量的地址,它可以是任意地址。取signed/unsigned short int 类型变量的地址,它一
定是2 的整数倍。取signed/unsigned int,signed/unsigned long 类型变量的地址,它一定
是4 的整数倍。
C 语言的结构体类型由多种基本类型组成,比较利于讨论字节对齐的问题,下面我们将
以结构体为例讲解字节对齐规则。以下例子除特殊说明外,均是在X86 32 位CPU,VC2010
环境下测试。
例1:
typedef struct example1
{
char a;
}EXAMPLE1;
结构体EXAMPLE1 比较简单,它其实就是一个char 型,它的长度sizeof(EXAMPLE1)
为1。
例2:
typedef struct example2
{
char a;
short b;
}EXAMPLE2;
结构体EXAMPLE2 中包含了2 个变量,其中char 型a 的长度为1,short 型b 的长度
为2,但结构体EXAMPLE2 的整体长度sizeof(EXAMPLE2)却为4,而不是1+2=3,这种
现象就是字节对齐造成的。
为了方便观察结构体中变量相对结构体头的偏移地址,我们定义如下的宏:
#define OFFSET(s, e) ((unsigned int)(&((s*)0)->e))
其中s 为结构体类型,e 为结构体中的变量,OFFSET 返回的就是结构体中的变量e
相对于结构体s 的偏移地址。通过该结构就可以看出结构体在内存中的分布。
求得结构体EXAMPLE2 的数据如下:
sizeof(EXAMPLE2) 4
OFFSET(EXAMPLE2, a) 0
OFFSET(EXAMPLE2, b) 2
画出结构体EXAMPLE2 在内存中分布如下:
ab
b
其中每个格子代表一个字节,a 和b 之间灰色背景的格子是编译器为了字节对齐而保留
的一个字节空间。为什么会保留一个字节的空间呢,这是因为结构体的对齐长度必须是其内
部变量类型中最长的对齐长度,也就是说存放结构体的起始地址必须是其内部变量类型中最
长的对齐长度的整数倍。结构体EXAMPLE2 中变量a 的对齐长度是1,变量b 的对齐长度
是2,因此EXAMPLE2 存放的地址必须是2 的整数倍。变量a 可以存放在任何地址,因此
存放在EXAMPLE2 开始的第一个字节,这个字节所在的地址是2 的整数倍,接下来的字节
(灰色)所在的地址不是2 的整数倍,而变量b 又只能存放在2 的整数倍地址,因此a 和b
之间只好空出1 个字节,这就使结构体EXAMPLE2 的长度变为4 了。
例3:
typedef struct example3
{
char a;
short b;
int c;
}EXAMPLE3;
在结构体EXAMPLE2 的基础上再增加一个int 变量c 构造成结构体EXAMPLE3,按照
例2 中介绍的方法分析一下结构体EXAMPLE3 的长度。
EXAMPLE3 中最长对齐长度的变量是c,4 个字节,因此EXAMPLE3 开始的地址必须
是4 的整数倍。变量a 是1 个字节,存放在EXAMPLE3 开始的第一个字节。变量b 是2
个字节,需要在a 之后空出1 个字节,才能存放在2 字节对齐的地址。变量c 是4 个字节,
需要存放在4 字节对齐的地址,前面的变量a、保留字节和变量b 之后已经是4 字节对齐的
地址了,因此变量c 可以直接存放在变量b 之后。
按照上面的分析,我们可以画出EXAMPLE3 在内存中的分布示意图:
a b b
c c c c
可以看到EXAMPLE3 占有8 个字节。我们再使用sizeof 和OFFSET 计算EXAMPLE3
的数据进行验证,如下:
sizeof(EXAMPLE3) 8
OFFSET(EXAMPLE3, a) 0
OFFSET(EXAMPLE3, b) 2
OFFSET(EXAMPLE3, c) 4
例4:
typedef struct example4
{
char a;
char b;
short c;
int d;
}EXAMPLE4;
在结构体EXAMPLE3 的基础上再增加一个char 的变量构造成结构体EXAMPLE4,
EXAMPLE4 比EXAMPLE3 多了一个char 型变量,那么EXAMPLE4 是否会比EXAMPLE3
长1 个字节?
EXAMPLE4 中最长的对齐长度的变量是d,4 个字节,因此EXAMPLE4 开始的地址必
须是4 的整数倍。变量a 是1 个字节,存放在EXAMPLE4 开始的第一个字节。变量b 是1
个字节,对字节对齐没有要求,直接存放在a 后面。变量c 是2 个字节,在a、b 之后已经
是2 字节对齐的地址了,因此c 可以直接存放在b 之后,对齐到2 个字节。变量d 是4 个
字节,在a、b、c 之后已经是4 字节对齐的地址了,因此d 可以直接存放在c 之后,对齐
到4 个字节。
按照上面的分析,我们可以画出EXAMPLE4 在内存中的分布示意图:
a b c c
d d d d
可以看到EXAMPLE4 虽然比EXAMPLE3 多了一个变量,但与EXAMPLE3 一样同样
占有8 个字节。我们再使用sizeof 和OFFSET 计算EXAMPLE3 的数据进行验证,如下:
sizeof(EXAMPLE4) 8
OFFSET(EXAMPLE4, a) 0
OFFSET(EXAMPLE4, b) 1
OFFSET(EXAMPLE4, c) 2
OFFSET(EXAMPLE4, d) 4
例5:
typedef struct example5
{
short a;
char b;
}EXAMPLE5;
再来看EXAMPLE5,按照上面介绍的规则你是否会认为它的长度是3?
EXAMPLE5 在内存中分布示意图如下:
a a
b
结构体不但要保证其存放的地址需要对齐到其内部变量类型中最长对齐长度的长度的
整数倍,其长度也要保证是其内部变量类型中最长的对齐长度的整数倍。EXAMPLE5 中最
长的对齐长度变量是a,2 个字节,因此它也必须是2 字节的整数倍,所以在b 之后需要填
充1 个字节。因此sizeof(EXAMPLE5)为4。
例6:
typedef struct example6
{
char a;
int b;
short c;
}EXAMPLE6;
按照前面介绍的方法可以得知EXAMPLE6 的长度是12,在内存中分布示意图如下:
ab
b b b
c c
EXAMPLE6 的数据如下:
sizeof(EXAMPLE6) 12
OFFSET(EXAMPLE6, a) 0
OFFSET(EXAMPLE6, b) 4
OFFSET(EXAMPLE6, c) 8
例7:
typedef struct example7_1
{
char a;
int b;
char c;
}EXAMPLE7_1;
typedef struct example7_2
{
short a;
EXAMPLE7_1 b;
char c;
}EXAMPLE7_2;
当一个结构体被包含在另外一个结构体中时,我们仍可以使用上面的方法进行分析。
先来看被包含的结构体EXAMPLE7_1,它按照4 字节对齐,长度是12,它的内存分
布示意图如下:
ab
b b b
c
对于结构体EXAMPLE7_2,short 型为2 字节对齐,EXAMPLE7_1 型被看做一个整体,
为4 字节对齐,char 型为1 字节对齐,因此结构体EXAMPLE7_2 也需要4 字节对齐,可
以得出EXAMPLE7_2 的内存分布示意图如下:
a a
b.a
b.b b.b b.b b.b
b.c
c
由于EXAMPLE7_1 作为一个整体存在,其内部的char 型变量b.a 并不会直接接在变
量a 后面,char 型变量c 也不会直接接在EXAMPLE7_2 内部的b.c 之后。由于EXAMPLE7_2
是4 字节对齐的,因此变量c 之后需要保留3 个字节对齐到4 字节。
例8:
typedef struct example8_1
{
char a;
short b;
}EXAMPLE8_1;
typedef struct example8_2
{
char a;
EXAMPLE8_1 b;
char c;
}EXAMPLE8_2;
再来看一下例8 这个例子,EXAMPLE8_1 按照2 字节对齐,长度是4,它的内存分布
示意图如下:
ab
b
对于结构体EXAMPLE8_2,char 型为1 字节对齐,EXAMPLE8_1 型为2 字节对齐,
因此结构体EXAMPLE8_2 也需要2 字节对齐。在EXAMPLE8_2 中将EXAMPLE8_1 看做
一个整体,可以得出EXAMPLE8_2 的内存分布示意图如下:
a
b.a
b.b b.b
c
由于EXAMPLE8_1 作为一个整体存在,其内部的char 型变量b.a 并不会直接接在变
量a 后面。由于EXAMPLE8_2 是2 字节对齐的,因此变量c 之后需要保留1 个字节对齐
到2 字节。
上面我们了解了字节对齐的规则,是以32 位机为例的。8 位机中硬件一次所能操作的
最大长度是1 个字节,多个字节的操作也是由单个字节组成的,因此8 位机没有字节对齐
的概念。例如过去所广泛使用的8 位单片机,它的int 型是2 个字节,long 型是4 个字节,
但受硬件限制在硬件操作时都是按字节操作的。
理解了这一点,下面的结构体在8 位机上的结果也就不意外了:
|