字节对齐

用C语言写程序时需要知道是大端模式还是小端模式。

所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中;

所谓的端模,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

 

   为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

   例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x2211中。小端模式,刚好相反,还是0x1122。

   我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

 

   Java、.NET大行其道的今天,C语言作为一门经典的高级语言,存在的唯一理由就是其高效、精练。随着PC硬件升级和降价,C语言由于其自身的复杂度,在开发PC应用软件时,已经很少使用。但是在开发嵌入式系统软件和操作系统时,由于普遍强调微内核,少占用空间和高效,因此,在系统开发舞台上,C语言依旧是主角

 
   在实际的程序开发中,为了提高数据的读取效率,在内存资源足够的情况下,一般在定义数据结构时,应该考虑四字节对齐,其原因很简单,现在的计算机多数是32位,也就是四字节。在每次读取数据时,一般都是直接读取32位的数据。有些情况下,字节对齐的数据结构,要比非对齐的数据结构占用空间少。以下分别就这两方面举例阐述。
      1、充分考虑四字节对齐,可以节省存储空间
typedef struct tagAAA
{
    char name[10];
    long sno;
    char sex;
    float score[4]; 
}AAA;
         
typedef struct tagBBB
{
    char name[10];
    char sex;
    long sno;
    float score[4];
}BBB;

在VC下,调试,可以很容易看出来,AAA占的存储空间为36,BBB占的存储空间为32。原因很简单,在四字节对齐的情况下,按四个字节为单位分配存储空间,如果不足,会自动补充,本次分配不足以存放下面的变量时,会重新分配空间。
        AAA:
              |   name[0]   |   name[1]   |   name[2]   |   name[3]   |
              -----------------------------------------------------------------
              |   name[4]   |   name[5]   |   name[6]   |   name[7]   |             
              -----------------------------------------------------------------
              |   name[8]   |   name[9]   |                  |                 |   1
               -----------------------------------------------------------------
              |                                   sno                                  |   2
              -----------------------------------------------------------------
              |     sex        |                 自动填充                           |   3                      
              -----------------------------------------------------------------
              |                          score[0]                                     |
              -----------------------------------------------------------------
              |                          score[1]                                     |
              -----------------------------------------------------------------
              |                          score[2]                                     |
              -----------------------------------------------------------------
              |                          score[3]                                     |
              -----------------------------------------------------------------
1. 由于剩下的两个字节不足以存放sno(long占四个字节),所以重新分配
              2.long变量占四个字节,32bits
              3.剩余三个字节的空间,不足以重放一个float变量,因此重新分配
               由此可以轻易的计算出,AAA占36个字节,同理,很容易计算出BBB占32个字节空间。
           
2、字节对其的情况下,可以更高效的访问
     假设一个结构体的数据如下存储:
              ----------------------------------------------------------------------
              |        12        |       34       |        56        |        78         |   -----------(A)
              ---------------------------------------------------------------------- 
 
              ----------------------------------------------------------------------
              |        XX        |       YY       |        12       |         34        |  
              ----------------------------------------------------------------------   -----------(B)
              |        56        |        78       |       XX       |         YY        |
              ----------------------------------------------------------------------
在A情况下,一次性读取数据成功,但是,在B情况下,需要读取数据两次,由此,可看出效率的差异。
     一般情况下,字节对齐遵从系统字节数与要求的对齐字节数相比,最小原则,即:假设要求按八字节对齐,但是系统为32位系统,则按照4字节对齐。在四字节对齐时,局部会按照2字节对齐,如:
     struct tagAAA
     {
           char a;
           short b;
            char c;
     }AAA;
结构体占据的空间为8字节而不是4字节,原因就是:
                    ----------------------------------------------
                    |    a     |          |             b              |
                    ----------------------------------------------
                    |   c      |                                        |
                    ----------------------------------------------
而不是:
                    ----------------------------------------------
                    |    a     |          b          |        c        |
                    ----------------------------------------------
其原因就是局部会以2字节对齐
 
----------------------------------------------------------------------------------------------------------------------------------------------------------
c++ 中字节对齐主要存在符合类型中: union struct class

先介绍四个概念:

1)数据类型自身的对齐值:基本数据类型的自身对齐值,等于sizeof(基本数据类型)

2)指定对齐值#pragma pack (value)时的指定对齐值value

3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。

  有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是 数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整 数倍)

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

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

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

 

1 union

Union就是取整个成员中大的内存块作为整个共用体的内存大小

对齐方式为成员中对齐方式最大的成员的对齐方式, 对界取编译器对界方式与自身大小中较小的一个

测试程序如下:

#include <iostream>

#include <stdio.h>

using std::cout;

using std::endl;

union u1

{

double a;

int b;

};

union u2

{

char a[13];

int b;

}; 

union u3

{

char a[13];

char b;

}; 

#pragma pack(2)//对界方式2字节¨²

union u4

{

char a[13];

int b;

}; 

union u5

{

char a[13];

char b;

};

 

#pragma pack()  //恢复默认对界方式

void main()

{

cout<<sizeof(u1)<<endl;

cout<<sizeof(u2)<<endl;

cout<<sizeof(u3)<<endl;

cout<<sizeof(u4)<<endl;

cout<<sizeof(u5)<<endl;

system("pause");

}

测试结果如下:

8

16

13

14

13

结论:由于默认是8字节对齐, 在u1 a8字节,很明显u1的大小就为8

u2中由于int4字节,char个字节,所以min(8,max(4,1))=4,以4字节对齐,u2理论为13个字节,要以4字节对齐的话所以为16字节。同理 u3 minmax(1,1),8=1sizeofu3=13, u4:  minmax(1,4),2=2sizeofu4=14u5: minmax(1,1),2=1sizeofu5=13.

2   struct/class:都是对整个成员所占内存大小的求和。大小仅和成员变量的类型有关还和定义的先后顺序有关

举个例子,一个结构体如下:

typedef struct T

    char c; //本身长度1字节 

    __int64 d;  //本身长度8字节

    int e;  //本身长度4字节

    short f;  //本身长度2字节

    char g;  //本身长度1字节

    short h;  //本身长度2字节

}; 

    假设定义了一个结构体变量C,在内存中分配到了0x00的位置,显然:

    对于成员C.c  无论如何,也是一次寄存器读入,所以先占一个字节。

    对于成员C.d  是个64位的变量,如果紧跟着C.c存储,则读入寄存器至少需要3次,为了实现最少的2次读入,至少需要以4字节对齐;同时对于8字节的原始变量,为了在寻址单位上统一,则需要按8字节对齐,所以,应该分配到0x08-0xF的位置。

    对于成员C.e  是个32位的变量,自然只需满足分配起始为整数个32位即可,所以分配至0x10-0x13

    对于成员C.f  是个16位的变量,直接分配在0x14-0x16上,这样,反正只需一次读入寄存器后加工,边界也与16位对齐。

    对于成员C.g  是个8位的变量,本身也得一次读入寄存器后加工,同时对于1个字节的变量,存储在任何字节开始都是对齐,所以,分配到0x17的位置。

    对于成员C.h  是个16位的变量,为了保证与16位边界对齐,所以,分配到0x18-0x1A的位置。

    分配图如下(还不正确,耐心读下去)

    结构体C的占用空间到h结束就可以了吗?我们找个示例:如果定义一个结构体数组 CA[2],按变量分配的原则,这2个结构体应该是在内存中连续存储的,分配应该如下图:
 

    分析一下上图,明显可知,CA[1]的很多成员都不再对齐了,究其原因,是结构体的开始边界不对齐。

    那结构体的开始偏移满足什么条件才可以使其成员全部对齐呢。想一想就明白了:很简单,保证结构体长度是原始成员最长分配的整数倍即可。
    上述结构体应该按最长的.d成员对齐,即与8字节对齐,这样正确的分配图如下:

    当然结构体T的长度:sizeof(T)==0x20;

     再举个例子,看看在默认对齐规则下,各结构体成员的对齐规则:

typedef struct A 

    char c;  //1个字节

    int d;  //4个字节,要与4字节对齐,所以分配至第4个字节处

    short e;  //2个字节, 上述两个成员过后,本身就是与2对齐的,所以之前无填充

 }; //整个结构体,最长的成员为4个字节,需要总长度与4字节对齐,所以, sizeof(A)==12 

typedef struct B 

    char c;  //1个字节

    __int64 d;  //8个字节,位置要与8字节对齐,所以分配到第8个字节处

    int e;  //4个字节,成员d结束于15字节,紧跟的16字节对齐于4字节,所以分配到16-19

    short f;  //2个字节,成员e结束于19字节,紧跟的20字节对齐于2字节,所以分配到20-21

    A g;  //结构体长为12字节,最长成员为4字节,需按4字节对齐,所以前面跳过2个字节

//24-35字节处

    char h;   //1个字节,分配到36字节处

    int i;   //4个字节,要对齐4字节,跳过3字节,分配到40-43 字节

}; //整个结构体的最大分配成员为8字节,所以结构体后面加5字节填充,被到48字节。故:

//sizeof(B)==48;

    具体的分配图如下:

 上述全部测试代码如下:

#include "stdio.h" 

typedef struct A 

    char c; 

    int d; 

    short e; 

 

}; 

typedef struct B 

    char c; 

    __int64 d; 

    int e; 

    short f; 

    A g; 

    char h; 

    int i; 

}; 

typedef struct C 

    char c; 

    __int64 d; 

    int e; 

    short f; 

    char g; 

    short h; 

}; 

typedef struct D 

    char a; 

    short b; 

    char c; 

}; 

int main() 

 

    B *b=new B; 

    void *s[32]; 

    s[0]=b; 

    s[1]=&b->c; 

    s[2]=&b->d; 

    s[3]=&b->e; 

    s[4]=&b->f; 

    s[5]=&b->g; 

    s[6]=&b->h; 

    s[7]=&b->g.c; 

    s[8]=&b->g.d; 

    s[9]=&b->g.e; 

    s[10]=&b->i; 

    b->c= 0x11; 

    b->d= 0x2222222222222222; 

    b->e= 0x33333333; 

    b->f=0x4444; 

    b->g.c=0x50; 

    b->g.d=0x51515151; 

    b->g.e=0x5252; 

    b->h=0x66; 

    int i1=sizeof(A); 

    int i2=sizeof(B); 

    int i3=sizeof(C); 

    int i4=sizeof(D); 

    printf("i1:%d\ni2:%d\ni3:%d\ni4:%d\n",i1,i2,i3,i4);//12 48 32 6 

运行时的内存情况如下图:

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

写出一个struct,然后sizeof,你会不会经常对结果感到奇怪?sizeof的结果往往都比你声明的变量总长度要大,这是怎么回事呢?讲讲字节对齐吧.

 

/******************************分割线

如果体系结构是不对齐的,A中的成员将会一个挨一个存储,从而sizeof(a)为11。显然对齐更浪费了空间。那么为什么要使用对齐呢?
体系结构的对齐和不对齐,是在时间和空间上的一个权衡。对齐节省了时间。假设一个体系结构的字长为w,那么它同时就假设了在这种体系结构上对宽度为w的数据的处理最频繁也是最重要的。它的设计也是从优先提高对w位数据操作的效率来考虑的。比如说读写时.............此处省略50万字

 

***********************************************************/

 

上面是你随便 google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则:(在没有#pragma pack宏的情况下,务必看完最后一行)

 

1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。

 

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

 

3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.

 

 

等你看完此3条原则,2分钟已经过去,抓紧时间,实战3分钟:

 

 

typedef struct bb
{
 int id;             //[0]....[3]
 double weight;      //[8].....[15]      原则1
 float height;      //[16]..[19],总长要为8的整数倍,补齐[20]...[23]     原则3
}BB;

typedef struct aa
{
 char name[2];     //[0],[1]
 int  id;         //[4]...[7]          原则1

 double score;     //[8]....[15]    
 short grade;    //[16],[17]        
 BB b;             //[24]......[47]          原则2
}AA;

int main()
{
  AA a;
  cout<<sizeof(a)<<" "<<sizeof(BB)<<endl;
  return 0;
}

 

结果是

48 24
ok,上面的全看明白了,内存对齐基本过关.

 

再讲讲#pragma pack().

在代码前加一句#pragma pack(1),你会很高兴的发现,上面的代码输出为

32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;

这不是理想中的没有内存对齐的世界吗.没错,#pragma pack(1),告诉编译器,所有的对齐都按照1的整数倍对齐,换句话说就是没有对齐规则.

 

明白了不?

 

那#pragma pack(2)的结果又是多少呢?对不起,5分钟到了,自己去测试吧.

 

ps:Vc,Vs等编译器默认是#pragma pack(8),所以测试我们的规则会正常;注意gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值