详解C语⾔的结构体中成员变量偏移问题
c语⾔中关于结构体的位置偏移原则简单,但经常忘记,做点笔记以是个记忆的好办法
原则有三个:
a.结构体中的所有成员其⾸地址偏移量必须为器数据类型长度的整数被,其中第⼀个成员的⾸地址偏移量为0,
例如,若第⼆个成员类型为int,则其⾸地址偏移量必须为4的倍数,否则就要“⾸部填充”;以此类推
b.结构体所占的总字节数即sizeof()函数返回的值必须是最⼤成员的长度的整数倍,否则要进⾏“末尾填充”;
c.若结构体A将结构体B作为其成员,则结构体B存储的⾸地址的偏移量必须为B中所含成员数据长度最⼤值的整数倍,
如若B中成员为int,double,char,则B的偏移量要为8的整数倍;否则进⾏“中间填充”。
相信⼤家在c语⾔程序开发的过程⼀定都使⽤过结构体,那么不知你对结构体中成员变量偏移这块是如何理解的?本⽂将和⼤家⼀起分享下,本⼈最近关于c语⾔中结构体偏移的⼀些思考和总结。
⽰例1
我们先来定义⼀下需求:
已知结构体类型定义如下:
struct node_t{
char a;
int b;
int c;
};
且结构体1Byte对齐
#pragma pack(1)
求:
结构体struct node_t中成员变量c的偏移。
注:这⾥的偏移量指的是相对于结构体起始位置的偏移量。
看到这个问题的时候,我相信不同的⼈脑中浮现的解决⽅法可能会有所差异,下⾯我们分析以下⼏种可能的解法:
⽅法1
如果你对c语⾔的库函数⽐较熟悉的话,那么你第⼀个想到的肯定是offsetof函数(其实只是个宏⽽已,先姑且这样叫着吧),我们man 3 offsetof查看函数原型如下:
#include <stddef.h>
size_t offsetof(type, member);
有了上述的库函数,我们⽤⼀⾏代码就可以搞定:
offsetof(struct node_t, c);
当然这并⾮本⽂探讨的重点,请继续阅读。
⽅法2
当我们对c语⾔的库函数不熟悉的时候,此时也不要着急,我们依然可以使⽤我们⾃⼰的⽅法来解决问题。
最直接的思路是:【结构体成员变量c的地址】减去【结构体起始地址】
我们先来定义⼀个结构体变量node:
struct node_t node;
接着来计算成员变量c的偏移量:
(unsigned long)(&(node.c)) - (unsigned long)(&node)
&(node.c)为结构体成员变量c的地址,并强制转化为unsigned long;
&node为结构体的起始地址,也强制转化为unsigned long;
最后我们将上述两值相减,得到成员变量c的偏移量;
⽅法3
按照⽅法2的思路我们在不借助库函数的情况下,依然可以得到成员变量c的偏移量。但作为程序员,我们应该善于思考,是不是可以针对上⾯的代码做⼀些改进,使我们的代码变得更简洁⼀些?在做具体的改进之前,我们应该分析⽅法2存在哪些⽅⾯的问题。
相信不⽤我多说,细⼼的你⼀定已经察觉到,⽅法2中最主要的⼀个问题是我们⾃定义了⼀个结构体变量node,虽然题⽬中并未限制我们可以⾃定义变量,但当我们遇到⽐较严且题⽬中不允许⾃定义变量的时候,此时我们就要思考新的解决⽅法。
在探讨新的解决⽅法之前,我们先来探讨⼀个有关偏移的⼩问题:
⼩问题
这是⼀道简单的⼏何问题,假设在座标轴上由A点移动到B点,如何计算B相对于A的偏移?这个问题对于我们来说是⾮常的简单,可能⼤部分⼈都会脱⼝⽽出并得到答案为B-A。
那么这个答案是否完全准确呢?⽐较严谨的你觉得显然不是,原因在于,当A为坐标原点即A=0的时候,上述答案B-A就直接简化为B了。
这个⼩⼩的简单的问题,对于我们来说有什么启⽰呢?
我们结合⽅法2的思路和上述的⼩问题,是不是很快就得到了下⾯的关联:
(unsigned long)(&(node.c)) - (unsigned long)(&node)
和
B - A
我们⼩问题的思路是当A为坐标原点的时候,B-A就简化为B了,那么对应到我们的⽅法2,当node的内存地址为0即
(&node==0)的时候,上⾯的代码可简化为:
(unsigned long)(&(node.c))
由于node内存地址==0了,所以
node.c //结构体node中成员变量c
我们就可以使⽤另外⼀种⽅式来表达了,如下:
((struct node_t *)0)->c
sizeof结构体大小上述代码应该⽐较好理解,由于我们知道结构体的内存地址编号为0,所以我们就可以直接通过内存地址的⽅式来访问该结构体的成员变量,相应的代码的含义就是获取内存地址编号为0的结构体struct node_t的成员变量c。
注:此处只是利⽤了编译器的特性来计算结构体偏移,并未对内存地址0有任何操作,有些同学对此可能还有些疑问,详细的了解该问题可参考关于c语⾔结构体成员变量访问⽅式的⼀点思考。
此时,我们的偏移求法就消除了struct node_t node这个⾃定义变量,直接⼀⾏代码解决,:
(unsigned long)(&(((struct node_t *)0)->c))
上述的代码相对于⽅法2是不是更简洁了⼀些。
这⾥我们将上⾯的代码功能定义为⼀个宏,该宏的作⽤是⽤来计算某结构体内成员变量的偏移(后⾯的⽰例会使⽤该宏):
#define OFFSET_OF(type, member) (unsigned long)(&(((type *)0)->member))
使⽤上⾯的宏,就可以直接得到成员变量c在结构体struct node_t中的偏移为:
OFFSET_OF(struct node_t, c)
⽰例2
和⽰例1⼀样,我们先定义需求如下:
已知结构体类型定义如下:
struct node_t{
char a;
int b;
int c;
};
int *p_c,该指针指向struct node_t x的成员变量c
结构体1Byte对齐
#pragma pack(1)
求:
结构体x的成员变量b的值?
拿到这个问题的时候,我们先做⼀下简单的分析,题⽬的意思是根据⼀个指向某结构体成员变量的指针,如何求该结构体的另外⼀个成员变量的值。
那么可能的⼏种解法有:
⽅法1
由于我们知道结构体是1Byte对齐的,所以这道题最简单的解法是:
*(int *)((unsigned long)p_c - sizeof(int))
上述代码很简单,成员变量c的地址减去sizeof(int)从⽽得到成员变量b的地址,然后再强制转换为int *,最后再取值最终得到成员变量b的值;
⽅法2
⽅法1的代码虽然简单,但扩展性不够好。我们希望通过p_c直接得到指向该结构体的指针p_node,然
后通过p_node访问该结构体的任意成员变量了。
由此我们得到计算结构体起始地址p_node的思路为:
【成员变量c的地址p_c】减去【c在结构体中的偏移】
由⽰例1,我们得到结构体struct node_t中成员变量c的偏移为:
(unsigned long)&(((struct node_t *)0)->c)
所以我们得到结构体的起始地址指针p_node为:
(struct node_t *)((unsigned long)p_c - (unsigned long)(&((struct node_t *)0)->c))
我们也可以直接使⽤⽰例1中定义的OFFSET_OF宏,则上⾯的代码变为:
(struct node_t *)((unsigned long)p_c - OFFSET_OF(struct node_t, c))
最后我们就可以使⽤下⾯的代码来获取成员变量a,b的值:
p_node->a
p_node->b
我们同样将上述代码的功能定义为如下宏:
#define STRUCT_ENTRY(ptr, type, member) (type *)((unsigned long)(ptr)-OFFSET_OF(type, member))
该宏的功能是通过结构体任意成员变量的指针来获得指向该结构体的指针。
我们使⽤上⾯的宏来修改之前的代码如下:
STRUCT_ENTRY(p_c, struct node_t, c)
p_c为指向结构体struct node_t成员变量c的指针;
struct node_t结构体类型;
c为p_c指向的成员变量;
注:
上述⽰例中关于地址运算的⼀些说明:
int a = 10;
int * p_a = &a;
设
p_a == 0x95734104;
以下为编译器计算的相关结果:
p_a + 10 == p_a + sizeof(int)*10 =0x95734104 + 4*10 = 0x95734144
(unsigned long)p_a + 10 == 0x95734104+10 = 0x95734114
(char *)p_a + 10 == 0x95734104 + sizeof(char)*10 = 0x95734114
从上述三种情况,相信你应该能体会到我所要表达的意思了。(注:后续某博⽂将从编译器的⾓度对该问题进⾏详细的阐述)
结论
本⽂通过⼏个⽰例描述了c语⾔结构体有关偏移的⼀些有意思的事情,希望能够对你有所帮助。为什么会有上述思考,相信有些同学已经看出⼀些端倪,这也正是后续博⽂将要描述的主题。
如⽂中有错误之处,欢迎指出。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论