C语言结构体的字节对齐原则Word下载.docx
《C语言结构体的字节对齐原则Word下载.docx》由会员分享,可在线阅读,更多相关《C语言结构体的字节对齐原则Word下载.docx(17页珍藏版)》请在冰豆网上搜索。
上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认对齐设置呢,当然可以.例如:
#pragmapack
(2)/*指定按2字节对齐*/
structC
#pragmapack()/*取消指定对齐,恢复缺省对齐*/
sizeof(structC)值是8。
修改对齐值为1:
#pragmapack
(1)/*指定按1字节对齐*/
structD
sizeof(structD)值为7。
后面我们再讲解#pragmapack()的作用.
三.编译器是按照什么样的原则进行对齐的?
先让我们看四个重要的基本概念:
1.数据类型自身的对齐值:
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
2.结构体或者类的自身对齐值:
其成员中自身对齐值最大的那个值。
3.指定对齐值:
#pragmapack(value)时的指定对齐值value。
4.数据成员、结构体和类的有效对齐值:
自身对齐值和指定对齐值中小的那个值。
有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。
有效对齐值N是最终用来决定数据存放地址方式的值,最重要。
有效对齐N,就是表示“对齐在N上”,也就是说该数据的"
存放起始地址%N=0"
.而数据结构中的数据变量都是按定义的先后顺序来排放的。
第一个数据变量的起始地址就是数据结构的起始地址。
结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。
这样就不能理解上面的几个例子的值了。
例子分析:
分析例子B;
假设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的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。
根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。
所以0x0000A到0x000B也为结构体B所占用。
故B从0x0000到0x000B共有12个字节,sizeof(structB)=12;
其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?
按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍.其实诸如:
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了.
同理,分析上面例子C:
第一个变量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(structC)=8.
四.如何修改编译器的默认对齐值?
1.在VCIDE中,可以这样修改:
[Project]|[Settings],c/c++选项卡Category的CodeGeneration选项的StructMemberAlignment中修改,默认是8字节。
2.在编码时,可以这样动态修改:
#pragmapack.注意:
是pragma而不是progma.
五.针对字节对齐,我们在编程中如何考虑?
如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:
有一种使用空间换时间做法是显式的插入reserved成员:
structA{
chara;
charreserved[3];
//使用空间换时间
intb;
}
reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.
六.字节对齐可能带来的隐患:
代码中关于对齐的隐患,很多是隐式的。
比如在强制类型转换的时候。
例如:
unsignedinti=0x12345678;
unsignedchar*p=NULL;
unsignedshort*p1=NULL;
p=&
i;
*p=0x00;
p1=(unsignedshort*)(p+1);
*p1=0x0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.
七.如何查找与字节对齐方面的问题:
如果出现对齐或者赋值问题首先查看
1.编译器的biglittle端设置
2.看这种体系本身是否支持非对齐访问
3.如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。
ARM下的对齐处理
fromDUI0067D_ADS1_2_CompLib
3.13type
qulifiers
有部分摘自ARM编译器文档对齐部分
对齐的使用:
1.__align(num)
这个用于修改最高级别对象的字节边界。
在汇编中使用LDRD或者STRD时
就要用到此命令__align(8)进行修饰限制。
来保证数据对象是相应对齐。
这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节
对齐,但是不能让4字节的对象2字节对齐。
__align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。
2.__packed
__packed是进行一字节对齐
1.不能对packed的对象进行对齐
2.所有对象的读写访问都进行非对齐访问
3.float及包含float的结构联合及未用__packed的对象将不能字节对齐
4.__packed对局部整形变量无影响
5.强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定
义为packed。
__packedint*p;
//__packedint则没有意义
6.对齐或非对齐读写访问带来问题
__packedstructSTRUCT_TEST
charc;
}
;
//定义如下结构此时b的起始地址一定是不对齐的
//在栈中访问b可能有问题,因为栈上数据肯定是对齐访问[fromCL]
//将下面变量定义成全局静态不在栈上
staticchar*p;
staticstructSTRUCT_TESTa;
voidMain()
__packedint*q;
//此时定义成__packed来修饰当前q指向为非对齐的数据地址下面的访问则可以
p=(char*)&
a;
q=(int*)(p+1);
*q=0x87654321;
/*
得到赋值的汇编指令很清楚
ldr
r5,0x20001590;
=#0x12345678
[0xe1a00005]
mov
r0,r5
[0xeb0000b0]
bl
__rt_uwrite4
//在此处调用一个写4byte的操作函数
[0xe5c10000]
strb
r0,[r1,#0]
//函数进行4次strb操作然后返回保证了数据正确的访问
[0xe1a02420]
r2,r0,lsr#8
[0xe5c12001]
r2,[r1,#1]
[0xe1a02820]
r2,r0,lsr#16
[0xe5c12002]
r2,[r1,#2]
[0xe1a02c20]
r2,r0,lsr#24
[0xe5c12003]
r2,[r1,#3]
[0xe1a0f00e]
pc,r14
*/
/*
如果q没有加__packed修饰则汇编出来指令是这样直接会导致奇地址处访问失败
[0xe59f2018]
ldr
r2,0x20001594;
=#0x87654321
[0xe5812000]
str
r2,[r1,#0]
//这样可以很清楚的看到非对齐访问是如何产生错误的
//以及如何消除非对齐访问带来问题
//也可以看到非对齐访问和对齐访问的指令差异导致效率问题
sizeof进行结构体大小的判断
typedefstruct
inta;
charb;
}A_t;
charc;
}B_t;
chara;
intb;
}C_t;
voidmain()
char*a=0;
cout<
<
sizeof(a)<
endl;
//4
sizeof(*a)<
//1--这个能理解
cout<
sizeof(A_t)<
//8
sizeof(B_t)<
sizeof(C_t)<
//12
为什么是这样的结果啊?
2.语法:
sizeof有三种语法形式,如下:
1)sizeof(object);
//sizeof(对象);
2)sizeof(type_name);
//sizeof(类型);
3)sizeofobject;
//sizeof对象;
5.指针变量的sizeof
既然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。
所以在32位计算机中,一
个指针变量的返回值必定是4(以字节为单位),可以预计,在将来的64位系统
中指针变量的sizeof结果为8。
char*pc="
abc"
;
int*pi;
string*ps;
char**ppc=&
pc;
void(*pf)();
//函数指针
sizeof(pc);
//结果为4
sizeof(pi);
sizeof(ps);
sizeof(ppc);
sizeof(pf);
//结果为4
指针变量的sizeof值与指针所指的对象没有任何关系,正是由于所有的指针变量所占内存
大小相等,所以MFC消息处理函数使用两个参数WPARAM、LPARAM就能传递各种复杂的消息结
构(使用指向结构体的指针)。
6.数组的sizeof
数组的sizeof值等于数组所占用的内存字节数,如:
chara1[]="
inta2[3];
sizeof(a1);
//结果为4,字符串末尾还存在一个NULL终止符
sizeof(a2);
//结果为3*4=12(依赖于int)
一些朋友刚开始时把sizeof当作了求数组元素的个数,现在,你应该知道这是不对的,那
么应该怎么求数组元素的个数呢?
Easy,通常有下面两种写法:
intc1=sizeof(a1)/sizeof(char);
//总长度/单个元素的长度
intc2=sizeof(a1)/sizeof(a1[0]);
//总长度/第一个元素的长度
写到这里,提一问,下面的c3,c4值应该是多少呢?
voidfoo3(chara3[3])
intc3=sizeof(a3);
//c3==
voidfoo4(chara4[])
intc4=sizeof(a4);
//c4==
也许当你试图回答c4的值时已经意识到c3答错了,是的,c3!
=3。
这里函数参数a3已不再是
数组类型,而是蜕变成指针,相当于char*a3,为什么?
仔细想想就不难明白,我们调用
函数foo1时,程序会在栈上分配一个大小为3的数组吗?
不会!
数组是“传址”的,调用者
只需将实参的地址传递过去,所以a3自然为指针类型(char*),c3的值也就为4。
7.结构体的sizeof
这是初学者问得最多的一个问题,所以这里有必要多费点笔墨。
让我们先看一个结构体:
structS1
inti;
问sizeof(s1)等于多少?
聪明的你开始思考了,char占1个字节,int占4个字节,那么加起
来就应该是5。
是这样吗?
你在你机器上试过了吗?
也许你是对的,但很可能你是错的!
V
C6中按默认设置得到的结果为8。
Why?
为什么受伤的总是我?
请不要沮丧,我们来好好琢磨一下sizeof的定义——sizeof的结果等于对象或者类型所占
的内存字节数,好吧,那就让我们来看看S1的内存分配情况:
S1s1={
a,0xFFFFFFFF};
定义上面的变量后,加上断点,运行程序,观察s1所在的内存,你发现了什么?
以我的VC6.0为例,s1的地址为0x0012FF78,其数据内容如下:
0012FF78:
61CCCCCCFFFFFFFF
发现了什么?
怎么中间夹杂了3个字节的CC?
看看MSDN上的说明:
Whenappliedtoastructuretypeorvariable,sizeofreturnstheactualsize,
whichmayincludepaddingbytesinsertedforalignment.
原来如此,这就是传说中的字节对齐啊!
一个重要的话题出现了。
为什么需要字节对齐?
计算机组成原理教导我们这样有助于加快计算机的取数速度,否则
就得多花指令周期了。
为此,编译器默认会对结构体进行处理(实际上其它地方的数据变
量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度
为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。
这样,两个数中间就
可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
让我们交换一下S1中char与int的位置:
structS2
看看sizeof(S2)的结果为多少,怎么还是8?
再看看内存,原来成员c后面仍然有3个填充字
节,这又是为什么啊?
别着急,下面总结规律。
字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有
需要编译器会在成员之间加上填充字节(internaladding);
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一
个成员之后加上填充字节(trailingpadding)。
对于上面的准则,有几点需要说明:
1)前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?
因为有了第
1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。
想想为什么。
结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在
stddef.h中定义,如下:
#defineoffsetof(s,m)(size_t)&
(((s*)0)->
m)
例如,想要获得S2中c的偏移量,方法为
size_tpos=offsetof(S2,c);
//pos等于4
2)基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,
这里所说的“数据宽度”就是指其sizeof的大小。
由于结构体的成员可以是复合类型,比
如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,
而不是把复合成员看成是一个整体。
但在确定复合类型成员的偏移位置时则是将复合类型
作为整体看待。
这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以VC
6为例,以后不再说明):
structS3
charc1;
S1s;
charc2
S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以
S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整
个sizeof(S3)的值也应该被4整除。
c1的偏移量为0,s的偏移量呢?
这时s是一个整体,它作为结构体变量也满足前面三个准则
,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,
所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填
充字节。
最后得到sizeof(S3)的值为16。
通过上面的叙述,我们可以得到一个公式:
结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:
sizeof(struct)=offsetof(lastitem)+sizeof(lastitem)+sizeof(trail
ingpadding)
到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个
影响sizeof的重要参量还未被提及,那便是编译器的pack指令。
它是用来调整结构体对齐
方式的,不同编译器名称和用法略有不同,VC6中通过#pragmapack实现,也可以直接修改
/Zp编译开关。
#pragmapack的基本用法为:
#pragmapack(n),n为字节对齐数,其取值
为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移
量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof(item)=min(n,sizeof(item))
再看示例:
#pragmapack(push)//