结构体内存对齐及大小的判断

1、结构体

    C语言中,结构体是一块连续的内存,各个成员按照定义的顺序,一次尽心存放,编译器会按照语法进行分析之后,计算需要的大小空间进行分配,为了每个成员都可以被快读访问到,所以需要进行地址对齐,

struct MyStruct
{
    int a;
    char b;
    int c;
};

    一个结构体的大小,绝对不是按照变量的字节数进行相加得到的,必须考虑到地址的对齐。

    偏移量:偏移量指的是结构体变量中成员的地址和结构体变量地址的差,而结构体的大小是等于:最后一个成员的偏移量 + 最后一个成员的大小。

当然这个最后一个成员的偏移量域结构体的大小受制于地址对齐的要求,所以,它们必须满足一下的要求:

    (1)结构体变量中成员的偏移量必须是自身成员(char、int、float、double)的整数倍;

    (2)结构体的大小必须是所有成员变量类型大小的整数倍。记住是成员。

    分析:

成员    第一个成员的偏移量    类型/大小
a            0               4/4
b          0 + 4             1/1 
c          4 + 1             4/4

 

第一个成员的偏移量是零,零是自身成员的整数倍。

第二个成员的偏移量是第一个成员的偏移量+第一个成员的大小,所以是四个字节,4字节是自身成员大小(1)的整数倍

因为最后一个成员的地址偏移量是 5,又因为这个地址的偏移量必须是自身成员c的的整数倍,那么地址的偏移量就只能是8;

所以结构体的大小 = 8 + 4,而结构体的大小又必须是所有成员的整数倍,所以满足要求,确定结构体的大小是: 12个字节

例子2:

struct MyStruct1
{
    int b;
    short c;
};

分析:有了上面的例子,我们可以知道:

偏移量      大小
int b             0        4个字节
short c          0 + 4       2

 

所以,总的地址的偏移量: 4,满足条件1的要求:地址的偏移量必须是自身成员大小的整数倍。

所以,结构体的大小: 成员的偏移量 + 最后成员的大小 = 4 + 2 = 6,显然这个不满足第二个条件: 结构体的大小必须是所有成员大小的整数倍,那么编译器会在 short 后面 ,进行地址的填充2个字节,以满足地址对齐的要求,最终结构体的大小 = 8;这个就满足了条件2

例子2:

    结构体成员完全一样,但是顺序不一样,影响到结构体大小的例子

struct MyStruct2
{
    char a;
    char b;
    int c;
};   // 结构体的大小是 8 个字节
struct MyStruct1
{
    char a;
    int c;
    char b;
}; // 结构体的大小是12个字节

    分析:

对于MyStruct1而言,

偏移量 大小

偏移量       大小
char a;          0          1/1
int c;          0 + 1       4/4
char b;         4 + 4       1/1

第二个成员的偏移量是1,不满足条件1:地址的偏移量是自身成员的整数倍,那么地址的偏移量只能是4,地址的偏移量只能是8,

结构体的大小: 地址的偏移量 + 最后一个成员的大小 = 8 + 1 =9,显然又是不满足条件2,结构体的大小只能是所所有成员大小的整数倍,那么编译器会自动进行地址对齐,在最后的成员的位置,补充三个字节,所以,结构体的大小就是12个字节

    对于MyStruct2而言

地址的偏移量      类型/大小
char a;         0            1/1
char b;       0 + 1          1/1
int c;         1 + 1         4/4

 

第二个成员的偏移量是1,显然是满足自动hi偏移量是自身成员大小的整数倍

那么地址的偏移量为2,显然是不满足条件1的要求,所以地址的偏移量必须为4,

结构体的大小: 地址的偏移量+ 最后成员的大小 = 4 + 4 =8,8个字节满足条件2,所以结构体的大小就是8

2、结构体的嵌套

struct MyStruct2
{
    char c;
    int j;
};
struct MyStruct1
{    
   short i
   struct MyStruct2 k;
};

    对于结构体嵌套的问题的话,就可以将结构体进行展开,那么就是

struct MyStruct1
{
    
    short i;
    struct MyStruct2
    {
       char c
       int j;
     };
};

    结构体展开了,但是有一个是必须注意: 展开后第一个成员的偏移量是被展开的结构体中最大的成员的整数倍,

    分析:

偏移量        类型/大小
short i;           0           2/2
char c;           421/1  // 被展开的结构体的第一个成员的偏移量是被展开结构体中最大的昌源的整数倍     
int j            4 + 1        4/4
所以结构体的的偏移量5,显然不能满足条件,所以偏移量只能是8,
结构体的大小:偏移量 + 最后一个成员的大小 = 8 + 4 = 12字节,满足条件2的要求,所以结构体的大小就是12个字节

    这里必须记住的是,被展开的结构体的第一个成员的偏移量,是被展开结构体中最大的成员的整数倍,所以,c 就只能是四个字节,而不是两个字节。

3、结构体包含指针

    当结构体中包含指针的时候,这个我们知道不论指针指向多大的内存块,指针的大小都是四个字节的,所以,就按四个字节进行处理,

struct MyStruct2
{
    char a;
    int *p;
    char c;
};
struct MyStruct1
{
    char a;
    char c;
    int *p;
};

    对于 MyStruct1而言,

偏移量       大小
char a;          0          1
char c;        0 + 1        1
int *p;        1 + 1        4

 

地址的偏移量为2,显然不满足条件1,所以地址的偏移量就为4,

结构体的大小 = 地址的偏移量 + 最后成员的大小 = 4 + 4 =8, 显然是满足条件2的要求,所以最终结构体的大小就是8个字节,

    对于 MyStruct2而言

地址偏移量       类型/大小
char a;           0            1/1
int *p;          0 + 1         4/4
char c;          4 + 4         1/1

 

第二个成员的地址偏移量是1,显然不满足地址偏移量是成员大小(4)的整数倍,所以只能是4,

所以第三个成员,地址的偏移量只能是8,满足条件1的要求

结构体的大小 = 地址的偏移量 + 最后一个成员的大小 = 8 + 1 =9,而9字节显然是不满足条件2:结构体的大小必须是所有成员的整数倍,编译器会自动进行地址的填充三个字节,所以最终结构体的大小是12个字节。

例子:

struct MyStruct1
{
        int a;
        double b;
        char c[9];

};

 

偏移量     类型/大小
    int i;          0        4/4
    double b;       4        8/8
    char c[9]      4 + 8     1/9

 

   第二个成员的偏移量是4,4和自身成员大小(8)是整数倍,那么地址的偏移量就是8,

   第三个成员的偏移量是 8 +8 =16,16 是char类型的整数倍,那么偏移量就确定是16,

    结构体的大小 = 偏移量 + 成员的大小 16 + 9 =25,很显然,25不是所有成员(4,8,1)的整数倍,所以结构体的大小就是32.

4、gcc 对齐指定

    对于现阶段我们使用的 32 位的系统已经编译器,默认的都是按照四个字节进行对齐的。我们是可以手动进行指定对齐的方式的。


gcc 支持但是不推荐的对齐指令:

如果没有程序猿进行指定的话,结构体默认都是按照四个字节进行对齐的。这里提供对齐指定:

#pragma pack(n) (n=1/2/4/8)

    当 n 为 1 的时候,或者当 n 没写(默认为1)时候,这个时候就是按照一个字节对齐,也就是设置编译器不对齐(取消对齐)。

    实际应用中:

以 :

#pragma pack(n)  进行开始指定

以:

#pragma pack()  进行结束指定,

gcc 推荐的对齐指令:

__attribute__((packed))  
__attribute__((aligned(n))

__attribute__((packed))  :

    放在在结构体定义的后面,所起到作用是取消内存对齐。

__attribute__((aligned(n))):

    放在结构体定义定义的后面,所起到的作用是指定结构体的整体以 n 个字节进行对齐。

5、offsetof  宏与 container_of 宏

    上面对于结构体大小的计算的时候,一直使用到偏移量(变量离结构体开始地址的距离),这里介绍使用宏的方式计算偏移量。

offsetof  :

    计算结构体中某个元素和结构体首地址的偏移量(地址的距离)。

引子:

struct student
{
    char a;
    int b;
    short c;
};
char        0    1/1
int         1+3    4/4
short       8    2/2

    int 这里的偏移量当为 1 的时候,很显然是不满组条件: 1 是 4 的整数倍,所以偏移量只能是 4; short 这里的偏移量等于 4 + 4 = 8,8 是 2 的整数倍,所以是满足条件的;而结构体的大小等于: 8 + 2 = 10,而10 不是所有类型大小的整数倍,所以就必须不足,所以结构体的大小为 12。所以就可以通过指针的偏移量访问结构体的成员变量:

int main(int argc, char *argv[])
{
    student s1;
    s1.a = 11;
    s1.b = 22;
    s1.c = 33;
    char *p = (char *)((char *)&s1 + 0);
    printf("s1.a = %d\n", s1.a);
    printf("*p = %d\n", *p);
    int *pp = (int *)((char *)&s1 + 4);
    printf("s1.b = %d\n", s1.b);
    printf("*pp = %d\n", *pp);
    short *ppp = (short *)((char *)&s1 + 8);
    printf("s1.c = %d\n", s1.c);
    printf("*ppp = %d\n", *ppp);
    while (1);
}

 

 

打印的结果:

s1.a = 11

*p = 11

s1.b = 22

*pp = 22

s1.c = 33

*ppp = 33

    s1 是结构体变量,通过结构体指针的方式是指向的结构体的首地址,所以从上面计算的地址的偏移量,int b 的地址的偏移量是 4,所以通过结构体地址加 4 就可以访问到。可见是可以通过结构体偏移量的方式进行访问的。但是这种每次都要自己手动计算的方式去计算地址的偏移量过于麻烦,所以就引入了 offsetof 宏定义去计算地址的偏移量:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
struct student
{
    char a;
    int b;
    short c;
};
int main(int argc, char *argv[])
{
    student s1;
    printf("%d\n", offsetof(struct student, a));
    printf("%d\n", offsetof(struct student, b));
    printf("%d\n", offsetof(struct student, c));
    while (1);
}

 

 

 

打印输出:

0

4

8

分析:

    输入的第一个参数是结构体的名字:struct student;输入的第二个参数是结构体成员的变量。

    (TYPE *) : 就是将这个传输的结构体使用上,强制转换成为一个结构体的指针

    (TYPE *)0 :将零地址强制转换为 TYPE * ,那么零就指向了了一个结构体,

&((TYPE *)0)->MEMBER : 因为零地址指向了结构体,所以通过箭头访问结构体的成员MEMBER,接着求出成员变量的地址。因为结构体的地址是零地址开始的,所以 MEMBER 的地址就是偏移量。

    size_t  : 最后将整个地址转为 size_t 类型的,也就是 unsigned int 类型。

可见是使用 offsetof 的方式是完全可以计算得出偏移量的。

container_of 宏:

#define container_of(ptr, type, member) ({            \
    const typeof(((type *)0)->member) * __mptr = (ptr);    \
    (type *)((char *)__mptr - offsetof(type, member)); })

 

type : 结构体类型

member : 结构体的成员变量

ptr : 是指向结构体成员变量的指针,


作用是:如果知道结构体某个成员变量的指针(地址),来获取这个整个结构体对象的指针(地址)。

#define container_of(ptr, type, member) ({            \
    const typeof(((type *)0)->member) * __mptr = (ptr);    \
    (type *)((char *)__mptr - offsetof(type, member)); })
struct student
{
    char a;
    int b;
    short c;
};
int main(int argc, char *argv[])
{
    struct student s1;
    struct student *ps;
    
    short *p = &(s1.c);
    printf(" &(s1.c) = %p\n", &(s1.c));

    // 通过对象的地址,计算得到 s1 的地址
    ps = container_of(p, struct student, c);
    printf(" ps = %p\n", ps);
    while (1);
}

 

 

 

container_of 的返回值就是结构体对象的地址,所以就可以通过结构体对象的地址去访问整个对象的所有的元素。

分析:

    分为两条语句去实现:

const typeof(((type *)0)->member) * __mptr = (ptr);

    typeof 关键字是 GNUC 对 C新增的一个扩展的关键字,用于获得一个对象的类型:typeof(((type *)0)->member),通过成员对象获取到 MEMBER 的类型;然后将 ptr 地址强制类型转换 MEMBER 类型的地址。之所以还要进行强制类型转换,是因为传进来的 ptr 的值,只是传了值而已并没有传递指针类型嘛,所以经过强制类型转换使得这个地址是有指针类型的。

(type *)((char *)__mptr - offsetof(type, member));

    使用传进去的 ptr 经过强制类型转换的 mptr 地址减去偏移量的地址就是结构体对象的地址了。

    其实本质上,就是根据结构体的成员变量的地址,然后根据这个成员变量的地址偏移,最后用结构体成员变量的地址减去成员变量的地址便宜,就可以得到了结构体的首地址。思路还是比较简单的,

总结:结构体大小的计算

    (1)偏移量必须是当前成员变量类型大小的整数倍,也就是说偏移量必须是大于等于当前成员变量类型大小的,而且必须是整数倍

    (3)结构体的大小等于,偏移量加上最后成员的大小,而且必须满足,结构体的大小是所有的成员变量类型大小的整数倍

posted @ 2015-11-04 16:49  qxj511  阅读(760)  评论(0编辑  收藏  举报