语言的结构体可以将不同类型的对象聚合到一个对象中,在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,但由于 C 的内存对齐机制以及不同机器间的差异,各个成员之间可能会有间隙,所以不能简单的通过成员类型所占的字长来推断其它成员或结构体对象的地址。
如果要计算结构体中某成员相对于该结构体首地址的偏移量,一般第一个反应就是该成员的地址与结构体对象的首地址之间的字节数,就比如我定义了这样一个结构体类型:
typedef struct list_node { int ivar; char cvar; double dvar; struct list_node *next; } list_node;
就用这个类型来定义一个变量:list_node ln; 假设现在求 ln.dvar 的地址与 ln 的地址之间相差多少个字节,用这个表达式:(char *)&ln.dvar - (char *)&ln 即可求出,这个式子的原理是,用 & 符取到 ln.dvar 和 ln 的地址,强制将其转换为 char * 型并将二者相减,之所以要转换为 char * 型,是因为 char 型变量占一个字节,根据指针减法的特性,我们要求二者之间的字节数,当然要以单个的字节为单位。好了,求偏移量的基本原理就是这个,那么问题来了,假如你在调用函数的时候,只通过参数传入了某个结构体成员,函数中并没有该结构体对象本身,这样子上述方法就不管用了,那该怎么办?我们从另一个角度想问题,如果说 B - A 的差就是偏移量,那如果令 A 等于 0,那么 B 它本身的值是不是就是偏移量了?基于这个思路,我们来看看伟大的 Linux 内核是怎样实现求偏移量的操作的:
/* * 选自 linux-2.6.7 内核源码 * filename: linux-2.6.7/include/linux/stddef.h */ #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
linux 中是定义了一个宏 offsetof,这个宏接受两个参数,一个 TYPE 代表结构体的类型,另一个 MEMBER 代表结构体中的成员,我们看看后面的宏替换部分发生了什么,先看 ((TYPE *)0) 这个部分,它把 0 这个数字强制转换成 TYPE * 型的指针类型,这样 ((TYPE *)0) 这个整体就相当于一个指针指向了 0 这个地址,不管 0 这个地址是否合法,是否真的有这么一个结构体对象,它都会把以 0 地址为首的一片连续内存当成一个结构体对象操作,那么再看 ((TYPE *)0)->MEMBER 这个部分,((TYPE *)0) 这个指针要取结构体对象中的 MEMBER 成员,因为这只是读内存的操作,并没有写入数据,所以虽说地址不合法,但并不会发生段错误,这样取到 MEMBER 成员后,前面的 & 符就可以对 MEMBER 成员取地址了,刚才我也说了,B - A 的差是偏移量的话,如果 A 等于 0,那么 B 本身就是偏移量,那正好对应现在的情况,((TYPE *)0) 本身就是以 0 地址为首进行操作,那么它取到的 MEMBER 成员所在的地址就是相对于结构体首地址的偏移量,然后再把这个地址强制转换成 size_t 类型,于是该成员的偏移量就得到了。有没有感觉很精妙。
知道成员偏移量了,那么求结构体对象本身的地址不就易如反掌了吗,成员的地址减去偏移量不就是结构体对象的首地址了吗,基于这个思路,我们再看看伟大的 Linux 是怎样实现的:
/* * 选自 linux-2.6.7 内核源码 * filename: linux-2.6.7/include/linux/stddef.h */ #define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );})
它同样是定义了一个宏,这个宏接受 3 个参数,第一个 ptr 代表已知成员的地址,第二个 type 代表结构体的类型,第三个 member 代表已知的成员,在宏替换的部分,它用到了一个 typeof 关键字,((type *)0)->member 这部分和上面讲的一样,用于获取假设于 0 地址的结构体对象的 member 成员,然后用 typeof 获取了 member 成员的类型,用该类型定义了一个相应的指针变量 __mptr 并将其赋值为 ptr,然后 ( (char *)__mptr - offsetof(type,member) ) 这部分就是将 __mptr 强制转换为 char * 型,再减去 member 成员的偏移量就得到了该结构体对象的首地址,再把这个首地址强制转换为 type * 型也就是结构体本身的类型对应的指针类型,最终该结构体对象的地址就求出来了。
原文博客:http://blog.csdn.net/zhanshen2015/article/details/51500757