结构体对齐理论
发布日期:2021-06-29 05:35:36 浏览次数:2 分类:技术文章

本文共 5188 字,大约阅读时间需要 17 分钟。

1:平台在STM32

1、自身对齐值算法

在相同的对齐方式下,结构体内部数据定义的顺序不同,结构体整体占据内存空间也不同。

如下结构体定义:

struct A
{
int a;
char b;
short c;
};

说明

// a 的自身对齐值为 4,偏移地址为 0x00~0x03,a 的起始地址 0x00 满足 0x00%4=0
// b 的自身对齐值为 1,由于紧跟 a 之后的地址,即 0x04 满足 0x04%1=0,所以 b 存放在 0x04 地址空 //间
// c的自身对齐值为2,由于紧跟b之后的地址0x05%2不是0,而0x06%2=0
// 因此c的存放起始地址为0x06,存放在0x06~0x07空间。
// 在b和c之间的0x05地址则补空字节

结构体A中包含:

1)4 字节长度的 int;
2)1 字节长度的 char;
3)2 字节长度的 short 型数据一个。

A 用到的空间应该是 7 字节。

但是因为编译器要对数据成员在空间上进行对齐,由于结构体自身对齐值取数据成员中自身对齐值的最大值,即 4,并且 0x00~0x07 的 8 字节空间满足 8%4=0,所以 sizeof(strcut A) 值为 8。

现在把该结构体调整成员变量的顺序:

1.1 B结构体说明

struct B

{
char b;
int a;
short c;
};

说明:

// b的自身对齐值为1,其起始地址为0x00,由于满足0x00%1=0,所以b存放在0x00地址空间
// a的自身对齐值为4,由于紧跟b之后的地址0x01%4不是0,而0x04%4=0
// 因此c的存放起始地址为0x04,存放在0x04~0x07空间。
// 在b和a之间的0x01~0x03地址则补3个空字节。
// c的自身对齐值为2,由于紧跟a之后的地址0x08%2=0,因此c的存放起始地址为0x08,存放在0x08~0x09空间

这时候同样是总共7个字节的变量,但是由于结构体自身对齐值取数据成员中自身对齐值的最大值,即4;

并且0x00~0x09的10字节空间不满足10%4=0,而12%4=0,所以sizeof(struct B)的值却是12,即在紧跟c之后的0x0A~0x0B地址还需补两个空字节,使得整个结构体占用的字节空间为12个字节。

2 指定对齐

下面我们使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。

#pragma pack (2) /指定按2字节对齐,等价于#pragma pack(push,2)/

struct C

{
char b;
int a;
short c;
};

#pragma pack () /取消指定对齐,恢复缺省对齐,等价于#pragma pack(pop)/

sizeof(struct C) 值是8。

修改对齐值为1:

#pragma pack (1) /指定按1字节对齐/
struct D
{
char b;
int a;
short c;
};

#pragma pack () /取消指定对齐,恢复缺省对齐/

sizeof(struct D) 值为7。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,long类型,其自身对齐值为4,double,long long类型,其自身对齐值为8单位字节。

3 概念

2、四个概念值

1)数据类型自身的对齐值,就是上面交代的基本数据类型的自身对齐值;
2)指定对齐值:#pragma pack (value)时的指定对齐值value;
3)结构体或者类的自身对齐值:其数据成员中自身对齐值最大的那个值;
4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值;

有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。

有效对齐值N是最终用来决定数据存放地址方式的值,最重要。

有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0"。

而数据结构中的数据变量都是按定义的先后顺序来排放的。
第一个数据变量的起始地址就是数据结构的起始地址。
结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整,就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍。

这样就不难理解上面的几个例子的值了。

4 例子分析:

struct B

{
char b;
int a;
short c;
};

假设B从地址空间0x0000开始排放。

该例子中没有定义指定对齐值,该值默认为4。

第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.

第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,符合0x0004%4=0, 且紧靠第一个变量。

第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。

再看数据结构B的自身对齐值为其变量中最大对齐值(这里是a)和指定对齐值(这里是4)中较小的那个,所以就是4,所以结构体的有效对齐值也是4。

根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。
所以0x0000A到0x000B也为结构体B所占用,故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12;、

4.1 /指定按2字节对齐/

#pragma pack (2) /指定按2字节对齐/

struct C {
char b;
int a;
short c;
};

#pragma pack () /取消指定对齐,恢复缺省对齐/

第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;
第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。
第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合0x0006%2=0。

所以从0x0000到0x00007共八字节存放的是C的变量,又C的自身对齐值为4;

所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.
更改c编译器的缺省字节对齐方式:

在缺省情况下,c编译器为每一个变量或数据单元按其自然对界条件分配空间;

一般地可以通过下面的两种方法来改变缺省的对界条件:

方法一:

使用#pragma pack(n),指定c编译器按照n个字节对齐;
使用#pragma pack(),取消自定义字节对齐方式。

方法二:

__attribute(aligned(n)),让所作用的数据成员对齐在n字节的自然边界上;
如果结构中有成员的长度大于n,则按照最大成员的长度来对齐;
__attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
比如:
typedef struct
{
}attribute((aligned(4))) param_t;

4.2、示例

例子一:

#pragma pack(4)
class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。由于下一成员short b是按2字节对齐,因此char a后面只需补一个空字节
short b; //第三个成员,自身长2,#pragma pack(4),取小值为2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};

说明:

可见,此类实际占用的内存空间是9个字节。

根据规则5,结构整体的对齐是min( sizeof( int ), pack_value ) = 4,所以sizeof( TestB ) = 12;
char a 占用一个字节,后面补3个空字节;
short b和char c可以放在同一4字节空间中,后面只需补一个空字节。

例子二:

#pragma pack(2)

class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。由于下一成员short b按2字节对齐,因此char a后面只需补一个空字节
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};

可见结果与例子一相同,各个成员的位置没有改变,此时结构整体的对齐是min( sizeof( int ), pack_value ) = 2,所以sizeof( TestB ) = 10;char a后面补一个空字节;char c后面补一个空字节。

例子三:

#pragma pack(4)
class TestC
{
public:
char a; //第一个成员,放在[0]偏移的位置,由于下一成员short b按2字节对齐,因此char a后面只弱补一个空字节
short b; //第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
char c; //第三个,自身长为1,放在[4]的位置。
};

整个类的实际内存消耗是5个字节,整体按照min( sizeof( short ), 4 ) = 2对齐,所以结果是sizeof( TestC ) = 6。

例子四:

struct Test
{
char x1; //第一个成员,放在[0]位置。由于下一成员short x2按2字节对齐,因此char x1后面只需补一个空字节,
short x2; //第二个成员,自身长度为2,按2字节对齐,所以放在偏移[2,3]的位置,
float x3; //第三个成员,自身长度为4,按4字节对齐,所以放在偏移[4,7]的位置,
char x4; //第四个成员,自身长度为1,按1字节对齐,所以放在偏移[8]的位置,
};

所以整个结构体的实际内存消耗是9个字节,考虑到结构整体的对齐是4个字节,所以整个结构占用的空间是12个字节。char x1和short x2共用一个4字节空间,后面补一个空字节;char x4后面需要补3个空字节。

例子五:

#pragma pack(8)

struct s1
{
short a; //第一个,放在[0,1]位置,
long b; //第二个,自身长度为4,按min(4, 8) = 4对齐,所以放在[4,7]位置
};

所以结构体的实际内存消耗是8个字节;

结构体的对齐是min( sizeof( long ), pack_value ) = 4字节,所以整个结构占用的空间是8个字节。
struct s2
{
char c; //第一个,放在[0]位置,
s1 d; //第二个,根据规则四,对齐是min( 4, pack_value ) = 4字节(由于s1占用了8个字节,这里为什么不是8字节??),所以放在[4,11]位置,
long long e; //第三个,自身长度为8字节,所以按8字节对齐,所以放在[16,23]位置,
};

所以实际内存消耗是24字节。整体对齐方式是8字节,所以整个结构占用的空间是24字节。

#pragma pack()
所以:sizeof(s2) = 24, char c后面是空了3个字节接着是s1 d, s1 d后面又补了4个空字节,char c占一个字节,后面补7个字节;s1 d占8个字节;long long e占8个字节。

可以加入QQ群:687360507

与大伙沟通交流,技术在于分享而进步

转载地址:https://blog.csdn.net/zhi_Alanwu/article/details/104895780 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:嵌入式软件的框架
下一篇:链表---理论

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月17日 12时50分40秒