结构体对齐访问

14.结构体的对齐访问:
用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列,而不是简单地顺序排列,这就是内存对齐。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是某个数K的倍数,这就是所谓的内存对齐,而这个K则被称为该数据类型的对齐模数(alignment modulus)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。

内存对齐原因:
结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。内存本身是一个物理硬件(DDR内存芯片,SoC的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率最高。
当然,还有其他因素:比如Cache的一些缓存特性,还有其他硬件MMU、LCD...的一些内存依赖特性,所以会要求内存访问对齐。
某些平台只能在特定的地址处访问特定类型的数据; 为了提高存取数据。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元存放,则需要2个读取周期读取改变量。
内存对齐的特点:
1.开头:结构体(struct或者union)的数据成员,第一个数据放在OFFSET为0的位置,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始。(如int在32位机为4字节,则要从4的整数倍开始存储。)
2.结构体内个数据程员的内存对齐,即该数据程员相对结构体的起始位置:结构体每个成员相对结构体首地址的偏移量(OFFSET)是对其参数的整数倍,如有需要会在成员之间填充字节。编译器在为结构体程员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该程员,若不是,则填充若干字节,已达到整数倍的要求。
3.结构体作为成员:如果一个结构体中有某些结构体成员,则结构体成员要从其内部的整数倍地址开始存储。(如struct a 里有struct b,b 里有char ,int ,double等元素,那么b应该从8的整数倍开始存储。)
3.结构体的总长度:结构体变量所占空间的大小是对齐参数大小的整数倍。如有需要会在最后一个程员末尾填充若干字节使得所占空间大小是对其参数大小的整数倍。

每中类型的对其参数在不同的编译环境中的对其参数是不一样的。
char short int float double 指针
Linux(32): 1 2 4 4 8 4
GCC: 1 2 4 4 4 4
不同平台的编译器默认的对齐参数是不一样的,但是我们可以通过代码#pragma pack(n)去设置它,n的取值取1、2、4、8。   
  
结构体大小的计算方法和步骤:
首先是整个结构体,整个结构体变量4字节对齐是由编译器保证的,我们不用操心,不过不同平台下的编译器的对齐参数是不一样的,要看具体情况。32位linux是4位,64位是8位(如果设置了对齐参数,则要根据模数是#pragma pack(n)指定的n,如果没有指定的话就使用系统默认的,32位系统4字节,64位8字节和结构体内部的基本数据类型成员长度中数值较小的一方,结构体的长度应该是该模数的整数倍)。
然后一个元素一个元素的分析,首先分析第一个元素,第一个元素肯定会放在整个数据结构类型OFFSET=0的位置,但是它的终止位置是由第二个元素决定的;也是分析第二个元素所占的内存长度,假设第二个元素是int型的,那么第二个元素的起始地址必须是4的整数倍,以此类推,到最后一个元素的时候,就考虑整个数据结构的类型对齐(32位系统下是4的整数倍对齐)。
例子1:
struct mystruct1
{
int a; //4 //放在OFFSET=0的位置,结束地址未定;
char b; //0 +1 //a的结束地址为3,从地址4开始放b,结束地址未定
short c; //1 + 2 //b的结束地址5,从地6个位置开始放c,结束地址未定
}; //8是4的整数倍,整个结构体的结束地址是7。
  
例子2:
struct mystruct2
{
char a; //1
int b; //3+4
short c; //0+2
}Mys2; //10 + 2 = 12;
  
struct stu
{
char sex; //1
int length; //3+4
char name[10]; //10
}; 18+2 = 20;
    
gcc支持但不推荐的对齐指令:
#pragma pack() #pragma pack(n) (n = 1/2/4/8)
1.#pragma是用来指挥编译器的,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我们不希望对齐方式是4,而是希望是别的(比如1字节对齐,8字节对齐或128字节对齐)。
2.常用的设置编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐;第二种是#pragma pack(4),这个括号的数字就是我们希望多少字节对齐。
3.我们需要#pragma pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对其参数就是n.

#pragma pack(16)
struct stu1
{
char sex;
int length;
char name[10];
};
#pragma pack()

GCC推荐的对齐指令:
#pragma pack的方式在很多C环境下都是支持的,但是GCC虽然可以用但是不建议使用。GCC推荐的对齐指令__attribute__((packed)) __attribute__(aligned(n))。
1.__attrubute__ ((packed))使用时直接放在进行内存对齐的类型定义后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
2.__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐。(注意是结构体变量整体n字节对齐,而不是结构体内个元素也要n字节对齐。)

/*  当用到typedef时,要特别注意__attribute__ ((packed))放置的位置,相当于:
  *  typedef struct str_stuct str;
  *  而struct str_struct 就是上面的那个结构。
  */
typedef struct {
        char sex;
        int     length;
        char    name[10];
} __attribute__ ((packed)) str;


/* 在下面这个typedef结构中,__attribute__ ((packed))放在结构名str_temp之后,其作用是被忽略的,注意与结构str的区别。*/
typedef struct {
        char sex;
        int     length;
        char    name[10];
}str_temp __attribute__ ((packed));
  
/*结构体内嵌结构体时:如果一个结构体中有某些结构体成员,则结构体成员要从其内部的整数倍地址开始存储。(如struct a 里有struct b,b 里有char ,int ,double等元素,那么b应该从8的整数倍开始存储。)
*/   
struct mystruct1
{
int a;
char b;
short c;
};


typedef struct myStruct5
{ // 1字节对齐 4字节对齐
    int a; // 4 4
    struct mystruct1 s1; // 7 8
    double b; // 8 8
    int c; // 4 4
}MyS5;   

猜你喜欢

转载自blog.csdn.net/qq_20725221/article/details/51476460
今日推荐