相信你们在c语言程序开发的过程必定都使用过结构体,那么不知你对结构体中成员变量偏移这块是如何理解的?本文将和你们一块儿分享下,本人最近关于c语言中结构体偏移的一些思考和总结。html
另外这篇博文还能够帮你更好的理解这个问题c语言中两种宏定义的区别,关于这个思考有哪些方面的意义,细心的你可能发现本文所属的类别为linux内核设计与实现,而并不是 GNU C语言编程,可能有些同窗会有些许好奇。不过不用着急,若是对本篇博文意义感兴趣的同窗,可继续关注后续的博文,会有进一步的阐述。node
咱们先来定义一下需求:linux
已知结构体类型定义以下:程序员
struct node_t{ char a; int b; int c; };
且结构体1Byte对齐算法
#pragma pack(1)
求:编程
结构体struct node_t中成员变量c的偏移。函数
注:这里的偏移量指的是相对于结构体起始位置的偏移量。spa
看到这个问题的时候,我相信不一样的人脑中浮现的解决方法可能会有所差别,下面咱们分析如下几种可能的解法:.net
若是你对c语言的库函数比较熟悉的话,那么你第一个想到的确定是offsetof函数(其实只是个宏而已,先姑且这样叫着吧),咱们man 3 offsetof查看函数原型以下:设计
#include <stddef.h> size_t offsetof(type, member);
有了上述的库函数,咱们用一行代码就能够搞定:
offsetof(struct node_t, c);
固然这并不是本文探讨的重点,请继续阅读。
当咱们对c语言的库函数不熟悉的时候,此时也不要着急,咱们依然可使用咱们本身的方法来解决问题。
最直接的思路是:【结构体成员变量c的地址】 减去 【结构体起始地址】
咱们先来定义一个结构体变量node:
struct node_t node;
接着来计算成员变量c的偏移量:
(unsigned long)(&(node.c)) - (unsigned long)(&node)
&(node.c)为结构体成员变量c的地址,并强制转化为unsigned long;
&node为结构体的起始地址,也强制转化为unsigned long;
最后咱们将上述两值相减,获得成员变量c的偏移量;
按照方法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
上述代码应该比较好理解,因为咱们知道结构体的内存地址编号为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)
和示例1同样,咱们先定义需求以下:
已知结构体类型定义以下:
struct node_t{ char a; int b; int c; };
int *p_c,该指针指向struct node_t x的成员变量c
结构体1Byte对齐
#pragma pack(1)
求:
结构体x的成员变量b的值?
拿到这个问题的时候,咱们先作一下简单的分析,题目的意思是根据一个指向某结构体成员变量的指针,如何求该结构体的另一个成员变量的值。
那么可能的几种解法有:
因为咱们知道结构体是1Byte对齐的,因此这道题最简单的解法是:
*(int *)((unsigned long)p_c - sizeof(int))
上述代码很简单,成员变量c的地址减去sizeof(int)从而获得成员变量b的地址,而后再强制转换为int *,最后再取值最终获得成员变量b的值;
方法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语言结构体有关偏移的一些有意思的事情,但愿可以对你有所帮助。为何会有上述思考,相信有些同窗已经看出一些端倪,这也正是后续博文将要描述的主题。
如文中有错误之处,欢迎指出。
[1] http://isis.poly.edu/kulesh/stuff/src/klist/
[2] https://www.kernel.org/doc/Documentation/CodingStyle
[3] http://www.kroah.com/log/linux/container_of.html
若是您对算法或编程感兴趣,欢迎扫描下方二维码并关注公众号“算法与编程之美”,和您一块儿探索算法和编程的神秘之处,给您不同的解题分析思路。