C语言结构体的大小——内存对齐和位域的使用

C语言结构体对齐

C语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。内容虽然很基础,但一不小心就会弄错。写出一个struct,然后sizeof,你会不会经常对结果感到奇怪?sizeof的结果往往都比你声明的变量总长度要大,这是怎么回事呢?结合网上的资料和自己的编程实践,总结如下。

首先考虑一个问题,为什么要设计内存对齐的处理方式呢?如果体系结构是不对齐的,成员将会一个挨一个存储,显然对齐更浪费了空间。那么为什么要使用对齐呢?体系结构的对齐和不对齐,是在时间和空间上的一个权衡对齐节省了时间。假设一个体系结构的字长w,那么它同时就假设了在这种体系结构上对宽度为w的数据的处理最频繁也是最重要的。它的设计也是从优先提高对w位数据操作的效率来考虑的。

既然内存对齐是必要的,那么结构体到底是怎样对齐的呢?

       (在没有定义#pragma pack宏的情况下):

       原则1、数据成员对齐规则:结构struct或联合union)的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员按其类型大小和默认对齐参数(32位系统通常默认按4字节对齐)中较小的一个对齐每个成员的起始地址%每个成员的自身对齐值=0,否则补空直至满足条件比如对于char型数据,其自身对齐值为1,对于short型为2对于int型为4double32位机大小8字节、系统默认对齐方式是4字节,取其较小者,所以其对齐值是4

       原则2结构体作为成员:如果一个结构里有结构体成员,则按照其成员中自身对齐值最大的那个值对齐。struct a里存有struct bb里有charintdouble等元素,那b应该从4的整数倍开始存储。)

       原则3、收尾工作:结构体的总大小,也就是sizeof的结果,必须为所用过的最大对齐参数的整数倍,不够就补空字节。比如在32位系统中最大对齐参数4,如果一个结构中有类型为intlongfloatdouble等长度等于或大于4的成员,则其总大小必须是4的整数倍。


       这三个原则具体怎样理解呢?我们看下面几个例子,通过实例来加深理解。

       例1

typedef struct _A {
                     short a1;
                     short a2;
                     short a3;
                    }A;

typedef struct _B{
                   long a1;
                   short a2;
                  }B;

       sizeof(A) = 6; 这个很好理解,三个short对齐方式都是2

       sizeof(B) = 8; 这个是不是比预想的大2个字节?long4short2,整个为8,因为原则3

       例2

typedef struct _A{
                    int a;
                    char b;
                    short c;
                    }A;

typedef struct _B{
                   char b;
                   int a;
                   short c;
                    }B;

       sizeof(A) = 8; int4char1short2,这里用到了原则1和原则3

       sizeof(B) = 12; 是否超出预想范围?char1int4short2,怎么会是12?还是原则1和原则3

       深究一下,为什么是这样,我们可以看看内存里的布局情况。

                         a           b         c
       A的内存布局:1111,    1*,      11

                         b           a           c
       B的内存布局:1***,     1111,   11**

       其中星号*表示填充的字节。A中,b后面为何要补充一个字节?因为cshort类型,其起始位置要为2的倍数,就是原则1c的后面没有补充,因为bc正好占用4个字节,整个A占用空间为4的倍数,也就是最大成员int类型的倍数,所以不用补充。

       B中,bchar1b后面补充了3个字节,因为aint4,根据原则1,起始位置要为4的倍数,所以b后面要补充3个字节。c后面补充两个字节,根据原则3,整个B占用空间要为4的倍数。如果c后面不补充,整个B的空间为10,不符,所以要补充2个字节。

       再看一个结构中含有结构成员的例子:

       例3typedef struct _A{
                     int a;
                     double b;
                     float c;
                    }A;

                typedef struct _B{
                     char e[2];
                     int f;
                     double g; 
                     short h;
                     A i;
                    }B;

       sizeof(A) = 16这个比较好理解,int4double8float4double对齐方式取了较小的默认方式,按4字节对齐。所以整个A的大小为16

       sizeof(B) = 36看看B的内存布局。

                        e       f           g          h            i
       B的内存布局:11* *,   1111,   11111111, 11 * * ,  1111, 11111111, 1111


       i其实就是A的内存布局。i的起始位置要为4的倍数,所以h后面要补齐。把B的内存布局弄清楚,有关结构体的对齐方式基本就算掌握了。

       以上讲的都是没有#pragma pack宏的情况,如果有#pragma pack宏,那么对齐方式就要按照宏的定义来。比如上面的结构体前加#pragma pack(1),内存的布局就会完全改变。sizeof(A) = 16; sizeof(B) = 32;

       有了#pragma pack(1),内存不会再遵循原则1和原则3了,按1字节对齐。没错,这不是理想中的没有内存对齐的世界吗?!

                        a          b            c
       A的内存布局:1111,     11111111,   1111

                       e    f         g              h      i
       B的内存布局:11, 1111, 1111111, 11 , 1111, 11111111, 1111

       那#pragma pack(2)的结果又是多少呢?#pragma pack(8)呢?留给大家自己思考吧,相信没有问题。

       还有一种常见的情况,结构体中含位域字段。位域成员不能单独被取sizeof值。C99规定intunsigned intbool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。

       使用位域的主要目的是压缩存储,其大致规则为:
       1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
       2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
       3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,gcc通常采用压缩的方式
       4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
       5) 整个结构体的总大小为对齐方式的整数倍。对齐方式去最宽基本类型成员大小和默认对齐方式的较小者。

        还是让我们来看看例子。

       例4typedef struct _A{
                     char f1 : 3;
                     char f2 : 4;
                     char f3 : 5;
                     }A;

                     a       b       c
       A的内存布局:111,    1111 *,   11111 * * *

       位域类型为char,第1个字节仅能容纳下f1f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(A)的结果为2

       例5typedef struct _B{
                    char f1 : 3;
                    short f2 : 4;
                    char f3 : 5;
                    }B;

       由于相邻位域类型不同,在VC6中其sizeof6,在gcc中为2

如果是

typedef struct _B{
                    char f1 : 3;
                    int f2 : 4;
                    char f3 : 5;
                    }B;
gcc中大小则是4,它们仍然被压缩在了一起,但是长度必须是基本类型int型大小的整数倍;

如果是

typedef struct _B{
                    char f1 : 3;
                    long long  f2 : 4;
                    char f3 : 5;
                    }B;

32位系统,gcc编译后大小仍然是4它们仍然被压缩在了一起。虽然long long大小是8,但默认对齐参数是4


       例6struct C{
                     char f1 : 3;
                     char f2;
                     char f3 : 5;
                    };

       非位域字段穿插在其中,不会产生压缩,在VC6gcc中得到的大小均为3
       
       最后顺便提一点,在设计结构体的时候,一般会尊照一个习惯,就是把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间。

你可能感兴趣的:(编程基础)