结构体,面向对象的基础----小话c语言(11)

本文深入探讨了结构体对齐原理,包括不同成员间的对齐规则、位域的使用及其对内存布局的影响,并提供了多种获取成员偏移量的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[Mac-10.7.1 Lion Intel-based x64 gcc4.2.1]


Q:结构体的本质是什么?

A:结构体就像一种粘合剂,将事物之间的关系很好地组合在了一起。


Q:结构体对象中各个变量的内存存储位置和内置基本类型变量的存储有什么区别?

A:简单地说,它们没区别;复杂地说,它们有区别。简单在于它们终究会存储在内存中,复杂地说它们的位置可能有一些不同。如下:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));

typedef struct
{
    char    sex;
    int     age;
}student;

// print every byte of the obj
void    print_struct(void   *obj, int size)
{
    int i;
    unsigned char *temp = (unsigned char *)obj;
    for (i = 0; i < size; ++i)
    {
        printf("%#x ", temp[i]);
    }
    printf("\n");
}

int main()
{
    student s;
    s.sex = 'm';
    s.age = 25;
    print_struct(&s, sizeof(student));
    
    return 0;
}
运行结果:

可以看到s对象的sex成员值为'm',对应于0x6d,s的age成员值对应于0x19.因为笔者的机器为小端法,所以它们都在低地址显示,高地址补零。这里也可以看到student结构中的sex成员虽然是char类型,却依然占用了4字节,这是被补齐了。


Q:如何获取结构体中一个成员的偏移位置呢?

A:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    student s;
    PRINT_D((char *)&s.sex - (char *)&s)
    PRINT_D((char *)&s.age - (char *)&s)
    
    return 0;
}

如上代码,定义一个student类型的对象s,输出s的sex成员的地址和s地址的差即为偏移,同理age的偏移也可以获得。


Q:有更一般的方法么,不用创建对象,直接获得一个结构体某个成员的偏移位置?

A:有的。更一般就意味着更抽象。将上面的过程抽象,取一个特定结构体对象的某个成员的偏移。

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    PRINT_D(&((student *)0)->sex - (char *)0)
    PRINT_D((char *)&((student *)0)->age - (char *)0)
    return 0;
}
上面的代码,在地址0抽象出了一个student类型的指针,然后获得各个成员的偏移;

运行结果:


Q:上面的格式看起来有点复杂,有更简单的封装么?

A:我们可以采用宏定义:

#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)
如下代码:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    PRINT_D(OFFSET(student, sex))
    PRINT_D(OFFSET(student, age))
    return 0;
}
输出结果:

当然,也可以使用系统头文件中定义的宏offsetof,示例代码如下:

#include <stdio.h>
#include <string.h>
#include <stddef.h>

#define PRINT_LUD(intValue)     printf(#intValue" is %lu\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    PRINT_LUD(offsetof(student, sex))
    PRINT_LUD(offsetof(student, age))
    return 0;
}

Q:上面代码中的OFFSET宏,用地址0处的指针访问成员,难道不会出现访问违例?

A:先写如下代码:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    student *s = (student *)0;
    PRINT_D(s->age)
    return 0;
}
代码试图访问地址0处的数据,运行:

可以看到出现访问违例。那么OFFSET宏是如何正确执行的呢?我们查看下使用OFFSET的代码的汇编:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    int ret = OFFSET(student, age);
    return 0;
}
main函数的汇编代码:

0x00001f70 <main+0>:	push   %ebp
0x00001f71 <main+1>:	mov    %esp,%ebp
0x00001f73 <main+3>:	sub    $0x8,%esp
0x00001f76 <main+6>:	mov    $0x0,%eax
0x00001f7b <main+11>:	mov    %eax,%ecx
0x00001f7d <main+13>:	add    $0x4,%ecx
0x00001f80 <main+16>:	movl   $0x0,-0x4(%ebp)
0x00001f87 <main+23>:	mov    %ecx,-0x8(%ebp)
0x00001f8a <main+26>:	add    $0x8,%esp
0x00001f8d <main+29>:	pop    %ebp
0x00001f8e <main+30>:	ret    
可以看到,第五行%ecx为0, add $0x4,%ecx将%ecx加4,得到4,然后将这个数值放到ret变量的位置.可见,编译器已经进行了优化。换个更简单的代码:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    void *p = &((student *)0)->age;
    return 0;
}
上面的代码仅仅是获取地址为0的student指针的age成员的地址,汇编如下:

0x00001f70 <main+0>:	push   %ebp
0x00001f71 <main+1>:	mov    %esp,%ebp
0x00001f73 <main+3>:	sub    $0x8,%esp
0x00001f76 <main+6>:	mov    $0x0,%eax
0x00001f7b <main+11>:	mov    %eax,%ecx
0x00001f7d <main+13>:	add    $0x4,%ecx
0x00001f80 <main+16>:	movl   $0x0,-0x4(%ebp)
0x00001f87 <main+23>:	mov    %ecx,-0x8(%ebp)
0x00001f8a <main+26>:	add    $0x8,%esp
0x00001f8d <main+29>:	pop    %ebp
0x00001f8e <main+30>:	ret 
可以看到,编译器和上面一样,也进行了优化,直接地址4放入变量p中。当然,这两个示例告诉我们,使用地址的形式很容易让编译器进行对应的可能优化。这也为程序员使用欺骗编译器来得到需要得到的数据埋下了伏笔。我们继续看如下代码,不取地址0处的地址,直接取地址0处的成员数据:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    int     age;
}student;

int main()
{
    int age = ((student *)0)->age;
    return 0;
}

运行:

可以看出,出现了访问违例。因为如果代码非要访问地址0处的数据,编译器也不能再做什么优化,只能乖乖地去取数据,结果就违例了。


Q:关于结构体的对齐,到底遵循什么原则?

A:首先先不讨论结构体按多少字节对齐,先看看只以1字节对齐的情况:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

#pragma pack(1)
typedef struct
{
    char    sex;
    short   score;
    int     age;
}student;

int main()
{
    PRINT_D(sizeof(student))
    PRINT_D(OFFSET(student, sex))
    PRINT_D(OFFSET(student, score))
    PRINT_D(OFFSET(student, age))
    return 0;
}
输出:

可以看到,如果按1字节对齐,那么结构体内部的成员紧密排列,sizeof(char) == 1, sizeof(short) == 2, sizeof(int) == 4.

修改上面的代码, 去掉#pragma pack语句,代码如下:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct
{
    char    sex;
    short   score;
    int     age;
}student;

int main()
{
    PRINT_D(sizeof(student))
    PRINT_D(OFFSET(student, sex))
    PRINT_D(OFFSET(student, score))
    PRINT_D(OFFSET(student, age))
    return 0;
}
运行结果:

此时,各个成员之间就不像之前那样紧密排列了,而是有一些缝隙。这里需要介绍下对齐原则:

此原则是在没有#pragma pack语句作用时的原则(不同同台可能会有不同):

原则A:结构体或者union结构的成员,第一个成员在偏移0的位置,之后的每个成员的起始位置必须是当前成员大小的整数倍;

原则B:如果结构体A含有结构体成员B,那么B的起始位置必须是B中最大元素大小整数倍地址;

原则C:结构体的总大小,必须是内部最大成员的整数倍;

依据上面3个原则,我们来具体分析下:

sex在偏移0处,占1字节;score是short类型,占2字节,score必须以2的整数倍为起始位置,所以它的起始位置为2;age为int类型,大小为4字节,它必须以4的整数倍为起始位置,因为前面有sex占1字节,填充的1字节和score占2字节,地址4已经是4的整数倍,所以age的位置为4.最后,总大小为4的倍数,不用继续填充。

继续修改上面的代码,增加#pragma pack语句:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

#pragma pack(4)
typedef struct
{
    char    sex;
    short   score;
    int        age;
}student;

int main()
{
    PRINT_D(sizeof(student))
    PRINT_D(OFFSET(student, sex))
    PRINT_D(OFFSET(student, score))
    PRINT_D(OFFSET(student, age))
    return 0;
}
运行结果:

具体分析下:

有了#pragma pack(4)语句后,之前说的原则A和C就不适用了。实际对齐原则是自身对齐值(成员sizeof大小)和指定对齐值(#pragma pack指定的对齐大小)的较小者。依次原则,sex依然偏移为0,自身对齐值为1,指定对齐值为4,所以实际对齐为1; score成员自身对齐值为2,指定对齐值为4,实际对齐为2;所以前面的sex后面将填充一个1字节,然后是score的位置,它的偏移为2;age自身对齐值为4,指定对齐为4,所以实际对齐值为4;前面的sex和score正好占用4字节,所以age接着存放;它的偏移为4.

我们继续修改下代码:

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

#pragma pack(4)
typedef struct
{
    char    sex;
    int     age;
    short   score;
}student;

int main()
{
    PRINT_D(sizeof(student))
    PRINT_D(OFFSET(student, sex))
    PRINT_D(OFFSET(student, age))
    PRINT_D(OFFSET(student, score))
    return 0;
}
运行结果:

这个和上面的不同在于age成员被移到第二个位置;sex的偏移依然为0,age自身对齐为4,指定对齐为4,所以实际对齐为4,所以age将从偏移为4的位置存储。


Q:关于位域的问题,空域到底表示什么?

A:它表示之后的位域从新空间开始。

#include <stdio.h>
#include <string.h>

#define PRINT_D(intValue)     printf(#intValue" is %d\n", (intValue));
#define OFFSET(struct, member)  ((char *)&((struct *)0)->member - (char *)0)

typedef struct 
{
    int a : 1;
    int b : 3;
    int : 0;
    int d : 2;
}bit_info;

int main()
{
    PRINT_D(sizeof(bit_info))
    return 0;
}

运行结果:

bit_info中的a, b占用4个字节的前4位,到int : 0;时表示此时将填充余下所有没有填充的位,即刚刚的4个字节的余下28位;int d : 2;将从第四个字节开始填充,又会占用4个字节,所以总大小为8.


关于结构体如何和面向对象关联,c++很好地诠释了,这里不做介绍。


xichen

2012-5-18 11:06:47


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值