C语言结构体中的内存对齐以及位域和位域的应用

来源:互联网 发布:与人工智能相关的股票 编辑:程序博客网 时间:2024/04/26 02:53

                   结构体中的内存对齐





我们常常会发现,当我定义了一个结构体但是siziof之后却发现结果常常大于我原来定义的在结构体里面所有

类型的和,然后我查询了许多的资料,终于发现了内存对齐问题。



许多计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通

为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当

一种型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),

而称T比S(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度

。比如这么一处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如

软件能保证double型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,

我们就可能需要两次存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。

些处理器在数据不满足齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正

工作。但是果想提升性能,那么所有的程序数据都应该尽可能地对齐。


1.数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个

数据成员存储的起始位置要从该成员自身大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开

始存储)。


编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类

所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐数



2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开

始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)


为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本

成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的

要求,也就是将预开辟空间的首地址后移几个字节。


3.结构体成员相对首地址偏移量必须是成员大小的整数倍,也就是内存对齐。

  通俗易懂的说法就是

   strcut A {

    char a;

    int b;

    };

 这个时候char a由于是第一个直接存储,然后呢int b根据我上面的话,为了让它对齐,然后给a前面

加上3个字节,然后相对首地址偏移量就是它的整数倍了(这里一般就把前面那个成员加上补充的字

节的和自己一样大就好了),当然前面字节数比你大就是已经OK了,直接存储进去就可以了。


4.还有就是结构体最后阶段,比如你算出来的sizeof是70而结构体内最大对齐数为8,那么你就需要在把70

变成8的整数倍(不能减只能加。。。),这个是因为结构体的总大小,也就是sizeof的结果,必须是其内

部最大成员的整数倍,不足的要补齐。



这个表就是常见的系统中的对齐数。



接下来我们就要看看怎样使用这些概念计算最大对齐数,


1)将结构体内所有数据成员的长度值相加,记为sum_a; 

2)将各数据成员为了内存对齐,按各自对齐模数而填充的字节数累加到和sum_a上,记为sum_b。对齐模数

是#pragma pack指定的数值以及该数据成员自身长度中数值较小者。该数据相对起始位置应该是对齐模式

的整数倍; 

3)将和sum_b向结构体模数对齐,该模数是【#pragma pack指定的数值】、【未指定#pragma pack时,系统默

认的对齐模数(32位系统为4字节,64位为8字节)】和【结构体内部最大的基本数据类型成员】长度中数值

较小者。结构体的长度应该是该模数的整数倍。


如果觉得没大看懂。。  那就看看下面的例题~~   例题我也会写的很细。

而在GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样,根据最宽的基本数据类

型来定。在GCC中,对齐模数的准则是:对齐模数最大只能是4,也就是说,即使结构体中有double类型,

对齐模数还是4,所以对齐模数只能是 1,2,4。而且在上述结构体成员相对首地址偏移量必须是成员大

小的整数倍,如果这个成员大小小于等于4则按照上述准则进行,但是如果大于4了,则结构体每个成员相

对于结构体首地址的偏移量只能按照是4的整数倍来进行判断是否添加填充。


看如下例子:

struct T
   {
    char ch;
    double d ;
   };

那么在GCC下,sizeof(T)应该等于12个字节。

如果结构体中含有位域(bit-field),那么VC中准则又要有所更改:

1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段

存储,直到不能容纳为止;

2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元

开始,其偏移量为其类型大小的整数倍;

3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段

存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;


然后说了这么多,来两个例子练习一下。   我的都是在windows - 32下面进行的哈。


=》》》》》例题


  struct A{

   double d;

   int a;

   int b;

   int c;

  };


这个答案应该是24,首先double在最前面他就直接对齐了,最大对齐数为8,然后int a对齐数为4直接存储进来

,剩下的由于对齐数都为4,所以不需要在前面添加字节直接存储进来。现在把他们相加,8B+4B+4B+4B=20B,

停!!  还没完,结构体的总大小需要是它最大对齐数的整数倍,所以呢 总大小为20B+4B=24B。


我们接下来看第二个

struct b{

char a;

int b;

char d;

struct A e;

};

第二个例子最大的不同就是结构体的嵌套,其实用我们的定理解决它也是简单易懂的,首先char a是第一个然后

直接对齐直接存储对齐数为1,接下来int b对齐数为4,由于第一个才有1个字节所以需要先给他补充3个字节然后

让b存储进来(结构体成员相对首地址偏移量必须是成员大小的整数倍),接下来char d已经对齐直接存储,再

下来就很关键了,下面是一个结构体,我们已知结构体的大小了和他的对齐数(结构体对齐数就是它里面成员的

最大对齐数),所以他的大小为24B,对齐数为8.然后我们发现前一个为char型,所以我们需要在struct A e前添

加7个字节让他对齐,然后再存储e,整个过程就这么走完了,他的历程:

                1B+3B+4B+1B+7B+24B=40B 

 刚刚好40B就是最大对齐数8的倍数,所以不需要添加字节就是40.





=》接下来看看涉及位域的例子


struct N
   {
   char c:2;
   int i:4;
   };

i成员相对于结构体首地址的偏移应该是4的整数倍,所以c成员后要填充3个字节,然后再开辟4个字节 的空间
作为int型,其中4位用来存放i,所以上面结构体在VC中所占空间为8个字节;而对于采用压缩方式的编译器来
说,如果填充的3个字节能容纳后面成员的位,则压缩到填充字节中,不能容纳,则要单独开辟空间,所以上面
结构体N在GCC或者Dev- C++中所占空间应该是4个字节。

如果位域字段之间穿插着非位域字段,则不进行压缩;

typedef struct
   {
      char c:2;
      double i;
      int c2:4;`

在GCC下占据的空间为16字节,在VC下占据的空间应该是24个字节。(如果不知道位域的话我接下来就讲到了)

总结:

1.内存对齐与编译器设置有关,首先要搞清编译器这个默认值是多少

2.如果不想编译器默认的话,可以通过#pragma pack(n)来指定按照n对齐

3.每个结构体变量对齐,如果对齐参数n(编译器默认或者通过pragma指定)大于该变量所占字节数(m),那么就按

照m对齐,内存偏移后的地址是m的倍数,否则是按照n对齐,内存偏移后的地址是n的倍数。也就是最小化长度规则

4.结构体总大小: 对齐后的长度必须是成员中最大的对齐参数的整数倍。最大对齐参数是从第三步得 到的。

5.补充:如果结构体A中还要结构体B,那么B的对齐方式是选它里面最长的成员的对齐方式


我自己又想了一下,如果你想节约空间,不妨把那些占用空间大的类型写在前面,占用空间小的写在后面,那么就不会

有因为需要内存对齐而开辟的空间了。(个人想法。。。  有啥缺点指出来)


2.位域


有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,

只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,

称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的

位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位

域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:struct 位域结构名 


{ 位域列表 };
其中位域列表的形式为: 类型说明符 位域名:位域长度

例如:

 
   struct bs
   {
   int a:8;
   int b:2;
   int c:6;
   };


位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式例如: 

struct bs

{
   int a:8;
   int b:2;
   int c:6;
   }data;


说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:

1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

struct bs

{
   unsigned a:4:
   unsigned :0 :/*空域*/
   unsigned b:4 :/*从下一单元开始存放*/
   unsigned c:4:

}

在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如: 

struct k
   {
   int a:1:
   int :2 :/*该2位不能使用*/
   int b:3:
   int c:2:
   };
   从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。


 3.位域的使用

位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名 位域允许用各种格式输出。
 main(){
   struct bs
{
   unsigned a:1;
   unsigned b:3;
   unsigned c:4;
   } bit,*pbit;
   bit.a=1;
   bit.b=7;
   bit.c=15;
   printf("%d,%d,%d/n",bit.a,bit.b,bit.c);
   pbit=&bit;
   pbit->a=0;
   pbit->b&=3;
   pbit->c|=1;
   printf("%d,%d,%d/n",pbit->a,pbit->b,pbit->c);
}


上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示

位域也是可以使用指针的。程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)

程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针 方式

给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=", 该行相当于: pbit->b=pbit->b&3位域b中原有值

为7,与3作按位与运算的结果为3(111&011=011,十进制值为 3)。同样,程序第16行中使用了复合位运算"|=", 相

当于: pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。 位域可以有效地节省空间~~  





0 0
原创粉丝点击