结构体字节对齐

来源:互联网 发布:linux查看ip地址命令 编辑:程序博客网 时间:2024/06/05 18:35

转自http://blog.sina.com.cn/s/blog_9b0604b40101phnb.html


字节对齐的准则:其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。例如上面第一个结构体变量。

实际使用过程中具体细划为2中模式:模式1:不指定对齐准则;概括为2点:(1)结构体中的成员各自以自己的数据类型为对齐重则存储。
(2)结构体本身的对齐按照成员中最宽基本类型大小作为整个结构的对齐标准。
例子:
struct test{
char a;   //占1个字节
short b;  //占2个字节
double c; //已64位系统为例中占8个字节
int d;    //占4个字节
};
结果是24不是15:讲解如下
存储过程是按照0x0的位置开始的,变量a占1个字节放在开始位置,变量b按照常理是要在0x1的位置上存储,但是0x1不能被short类型字节
数2整除,所以要补齐变量b要从0x2的位置开始存储2个字节,变量c按照常理是要从0x4开始存储,但是0x4不能被8整除所以要补齐,
变量c要从0x8的位置开始存储8个字节,变量d按照常理要从0x16的位置存储,我们看到0x16可以被int类型4个字节整除,所以变量d从0x16
的位置开始存储4个字节,到此本来应该是结束了存储,可以确定struct test 大小为20了,依据前面的2条理论得知,struct test中的
最宽类型是double型占8个字节作为struct test对齐准则,20个长度不能被8整除,所以要在补齐4个字节,最后struct test大小为24。
可以再使用上面的总结验证一下,下面的结构体字节是不是16而不是14
struct test{
short b;  //占2个字节
int d;    //占4个字节
double c; //已64位系统为例中占8个字节
};

模式2指定对齐准则:一般使用#pragma pack(n)作为对齐的方式标识。(1)当指定对齐值n小于结构体中最宽基本数据类型值的时候,每个成员都按照
指定对齐准则为标准,同时结构体本身的大小的判断标准也是依据#pragma pack(n)对齐标准做判断。
例子:
#pragma pack(4)
struct test
{
double x;  //已64位系统为例中占8个字节
char a;   //占4个字节
int b;    //占1个字节
char m[4]; //占9个字节
double d;  //已64位系统为例中占8个字节
int c;
};
结果:大小是32
结果分析:struct test最宽字节类型是double占8个字节,n为4小于8,所以所有的对齐方式都是按照n为4的方式对齐。0x0位置存储变量x,
0x8的位置可以被4整除所以存储变量a,0x9不能被4整除,所以要补齐3个字节。0x12能被4整除所以存储变量b,0x16能被4整除存储数组m[4],
补齐之后,0x20能被4整除存储变量d,0x28能被4整除,所以存储变量c。此时我们计算的struct test大小是32,32能被4整除所以最后确定大小为32。
struct std
{
float a;  
char b;
struct test c;
int d;
};
大小是44

(2),当#pragma pack(n)中的n大于结构体中最宽基本数据类型值的时候,每个成员对齐方式是按照结构体中最宽基本数据类型的大小来对齐存储的,
但是结构体本身的大小是依据#pragma pack(n)中的n值来判断对齐的。
例子:
#pragma pack(8)
struct test
{
double x;  //已64位系统为例中占8个字节
char a;   //占4个字节
int b;    //占1个字节
char m[4]; //占9个字节
double d;  //已64位系统为例中占8个字节
int c;
};
结果是40
struct std
{
float a;  
char b;
struct test c;
int d;
};
结果是56
结果讲解:pragma pack(8)中8大于结构体struct std中最宽的基本数据类型int,因此起始位置0x0存放4字节的a,
依据上面原则我们对齐标准时4所以0x4可以被4整除,0x4存储变量b,补齐3个字节,0x8可以被4整除,所以存储大小为40的
结构体struct test 变量c,0x48可以被4整除,所以存储占4个字节的变量d,此时得到struct std的大小是52,但是依据规则
判断52不能被8整除所以要补齐,最后struct std的大小是56。

 


位域使用
1,使用位域的主要目的是压缩存储,其大致规则为:
1) 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域
2) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
3) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
4) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
5) 如果位域字段之间穿插着非位域字段,则不进行压缩;
6) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

struct k
  {         
  int a:1         
  int :2         
  int b:3        
  int c:2
  unsigned :0
  };

位操作符总结:
首先要明确一个字节的位是从左往右算的,其次是位域的概念,位域在构造类型中定义的时候用:来表示位操作,当进行位操作的时候
我们不考虑字节对齐的方式,位操作不跨越字节,当位不够定义的位时,从下一个字节开始取位。当处理位的构造类型的时候我们主要是看
结构中最大的那个结构类型来定位构造类型的空间大小。
例子1,
typedef struct AA
{
   int a:5;//表示5个操作位
   int b:2;
}AA;
int main()
{
AA aa={a:10,b:8}; //这里表示给变量赋值为10和8,变量中用冒号表示赋值的意思。
char buf[100]={0};
strcpy(buf,"0123456789sdafsdfsdfsdafsdfsadfsdf");
memcpy(&aa,buf,sizeof(aa));
cout<<sizeof(aa)<<endl;
cout<<aa.a<<endl;
cout<<aa.b<<endl;
return 0;
}
例子2:
#include
#include
#include
#include
using namespace std;
typedef struct test
{
  int a:2;
  int b:2;
  int c:1;
}test;

int main()
{
test aa;
aa.a=1;
aa.b=3;
aa.c=1;
printf("%d\n",aa.a);
printf("%d\n",aa.b);
printf("%d\n",aa.c);
return 0;
}
结果是:
1
-1
-1
分析结果:位操作的时候有几个位我们就把这几个位按照高低位处理,高位是1表示复数,是0表示正数,
接下来就是按照表示规则和有符号和无符号转化方式来表示结果,内存上是不变的但是展示结果是不一样。
例子:aa.b = 3,内存中表示11,现在的大小是2个位,高位是1打印的时候表示这是个复数,所以我们转换为
反码+1的结果表示,反码是00 补码是01,结果值是1,所以打印结果是-1。

2,位域的对齐方式:
#pragma pack(1)  

struct s4{  

char a:4; 

 short b:4;  

short c:4;  

long d; };  

输出S4 sizeof:7  


#pragma pack(1)  

struct s4{  

char a:4;  

short b:4;  

char c:4;  

long d; };  

输出S4 sizeof:8


#pragma pack(2)  

struct s4{  

char a:4;  

short b:4;  

char c:4;  

long d; };  

输出S4 sizeof:10


#pragma pack(2)  

struct s4{  

char a:4;  

short b:4;  

short c:4;  

long d; };  

输出S4 sizeof:8  

#pragma pack(2)  

struct s4{  

char a:4;  

short b:4;  

short c:7;  

long d; };  

输出S4 sizeof:8
使用 #pragma pack(或其他开关)需注意的问题
1. 为了保证执行速度,尽量不使用#pragma pack; 
2. 不同的编译器生成的代码极有可能不同,一定要查看相应手册,并做实验。 
3. 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如 果很多人使用该头文件,
并不是每个人都知道应该pack。特别是为别人开发库文件时,如果 一个库函 数使用了struct作为其参数,
当调用者与库文件开发者使用不同的pack时,就会造 成错误, 而且该类错误很不好查。在VC及BC提供的头文件中,
除了能正好对齐在四字节上的结构外,都加了pack,否则我们编的Windows程序哪一个也不会正常运行。 
4. 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值,将产生非预期结果。 
VC中提供了一种安全使用pack的方法:#pragma pack( [ push | pop ], n ),将当前的align值压入编译器的一个内部堆栈,
并使用n作为当前的align值。  #pragma pack(pop)则将内部堆栈中的栈顶值作为当前的align值,这样就保证了嵌套pack时的正确。 
5. 不要多人同时定义一个数据结构。在多人合作开发一个软件模块时,为了保持自己的编程风格,
每个人都要对同一结构定义一份符合自己风格的数据类型,当两个人之间需 要传递该数据结构时,
如果两个人的 pack 值不一样,就会产生错误,该类错误也很难查。   

原创粉丝点击