深入内存对齐的详解

所属分类: 软件编程 / C 语言 阅读数: 124
收藏 0 赞 0 分享

1.引子

    在结构中,编译器为结构的每个成员按其自身的自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

    例如,下面的结构各成员空间分配情况(假设对齐方式大于2字节,即#pragma pack(n), n = 2,4,8...下文将讨论#pragmapack()):

复制代码 代码如下:

struct test
{
     char x1;
     short x2;
     float x3;
     char x4;
};

    结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,即偏移地址是2的倍数。因此,编译器在x2和x1之间填充了一个空字节,将x2放在了偏移地址为2的位置。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,整个结构体的大小是最大对界单元大小的整数倍(结构体内部有结构体时也遵循这个规则,下文将提到),编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。
    关于为什么要内存对齐,参考<解析内存对齐 Data alignment: Straighten up and fly right的详解>。看了这篇文章便可以更轻松的理解下面的内容。

    好了,下面说说#pragma pack:

2.#pragma pack()

    该预处理指令用来改变对齐参数。在缺省情况下,C编译器为每一个变量或数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对齐参数:

     · 使用伪指令#pragma pack (n),C编译器将按照n字节对齐。

     · 使用伪指令#pragma pack (),取消自定义字节对齐方式。

也可以写成:

#pragma pack(push,n)

#pragma pack(pop)

#pragma pack (n)表示每个成员的对齐单元不大于n(n为2的整数次幂)。这里规定的是上界,只影响对齐单元大于n的成员,对于对齐字节不大于n的成员没有影响。其实从字面意思,pack是“包裹,打包”的意思,#pragma pack(n)规定n个字节是一个“包裹”,个人认为实在不理解的话可以认为处理器一次性可以从内存中读/写n个字节,这样好理解。对于大小小于n的成员,当然是按照自己的对齐条件对齐,因为不论怎么放都可以一次性取出。对于对齐条件大于n个字节的成员,成员按照自身的对齐条件对齐和按照n字节对齐需要相同的读取次数,但按照n字节对齐节省空间,何乐而不为呢。可以参考我上面提到的<解析内存对齐 Data alignment: Straighten up and fly right的详解>。下面是一位大牛的观点,和我说的是一个意思:

   All it means is that each member of it will require alignment no greater than n.It doesn't mean that each member will have alignment requirement n.Notice, after all, it's called pack and not align for a reason-- precisely because it controls packing, not alignment.

另外,GNU C还有如下的一种方式:

     · __attribute__((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

     · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

3.结构体内成员如何找出自己的位置

首先遵循以下规则:

1.  每个成员分别取自己的对齐方式和#pragma pack指定的对齐参数二者的较小值作为自己的对齐方式。

2.  复杂类型(如结构)的对齐方式是该类型声明时所使用的对齐方式,或者说是声明时它的所有成员使用的对齐参数的最大值,最后和此时的#pragma pack指定的对齐参数二者取极小值。大牛是这么说的:

The documentation for #pragma pack(n) says that "The alignment of a member will be on a boundary that is either a multiple of n or a multiple of the size of the member,whichever is smaller". However I think this is incorrect; the docs should say that the alignment of a member will be on a boundary that is either a multiple of n or the alignment requirement of the member, whichever is smaller.

3.  对齐后的长度必须是成员中最大的对齐参数(不是成员的大小)的整数倍,这样在处理数组时可以保证每一项都边界对齐。

4.  对于数组,比如:char a[3];这种,它的对齐方式和分别写3个char是一样的。也就是说它还是按1个字节对齐.

        如果写: typedef char Array3[3];

        Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度。

5.  不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。

看一个简单的例子:

复制代码 代码如下:

#pragma pack(8)
struct s1
{
    short a;
    long b;
};
struct s2
{
    char c;
    s1 d;
    long long e;
};
#pragma pack()

    成员对齐有一个重要的条件:每个成员分别对齐。即每个成员按自己的方式对齐.

    也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐。并且结构的长度必须为所用过的所有对齐参数的整数倍(只要是最大的对齐参数的整数倍即可),不够就补空字节(视编译器而定)。

    S1中,成员a是2字节默认按2字节对齐,指定对齐参数为8,这两个值中取2,a按2字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,a后补2个字节后存放b,所以sizeof(S1)应该为8。8是4的倍数,满足上述的第3条规则。

    S2中,c和S1中的a一样,按2字节对齐,而d是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是该结构定义(声明)时它的所有成员使用的对齐参数中最大的一个,S1的是4,小于指定的8。所以成员d就是按4字节对齐,c后补2个字节,后面是8个字节的结构体d。成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以d后又补上4个字节,从第16个字节开始放置成员e。这时,长度为24,已经可以被最大对齐参数8(成员e按8字节对齐)整除。这样,一共使用了24个字节。

   上面的不够复杂?再来一个:

复制代码 代码如下:

#pragma pack(4)
struct s1
{
    char a;
    double b;
};
#pragma pack()

#pragma pack(2)
struct s2
{
    char c;
    struct s1 st1;
};
#pragma pack()

 
#pragma pack(2)
struct s3
{
    char a;
    long b;
};
#pragma pack()

#pragma pack(4)
struct s4
{
    char c;
    struct s3 st3;
};
#pragma pack()

    先看s1,a放在偏移地址为0的位置(第一个字节)。b默认8字节对齐,但指定对齐参数是4字节,所以b按4字节对齐,放在偏移地址为4的位置,a后补3个字节。所以sizeof(s1)是12。结构体s1的对齐参数是4,下面会用到。

    再看s2,c放在第一个字节。st1自己的对齐参数是4,但此时指定的对齐参数是2,所以st1按照2字节对齐,c后补一个字节后存放st1。注意,st1内部是不会变的,声明s1时是什么样就是什么样,因为我们要保证sizeof(s2.st1) == sizeof(s1),如果不这样就乱套了。这样sizeof(s2)是14。结构体s2的对齐参数是2,14是2的整数倍。

    再看s3,a放在第一个字节。b默认4字节对齐,但指定的对齐参数是2,所以b按2字节对齐,放在偏移地址为2的位置,a后补一个字节。sizeof(s3)是6。结构体s3的对齐参数是2(后面会用到),6是2的整数倍。

    最后看s4,c放在第一个字节。st3自己的对齐参数是2,指定的对齐参数是4,所以st3取极小值,按2字节对齐,放在偏移地址为2的位置,c后补一个字节。sizeof(s4)是8,结构体的对齐参数是2,8是2的整数倍。

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多