1、#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER) (include/linux/stddef.h)
1.1 功能:
返回结构体TYPE中MEMBER成员相对于结构体首地址的偏移量,以字节为单位。
1.2 解析:
此类复杂表达式的解析应该采用从内向外、逐层理解的方式。
首先,(TYPE *)0表示将数字0强制类型转换为TYPE类型(TYPE为结构体类型)的指针。因此这里的0代表内存地址0,即我们认为内存地址0开始的sizeof(TYPE)个字节内存储的是一个TYPE类型的变量。
然后,((TYPE *)0)->MEMBER 得到该结构体变量中的MEMBER成员变量,
而 &(((TYPE*)0)->MEMBER) 使用取地址符&取得了MEMBER成员变量的地址,(size_t)加在前面表示将MEMBER成员变量的地址强制类型转换为size_t(即unsigned int),并将结果作为宏的返回值。
可见,offsetof宏返回的是MEMBER成员在内存中的实际地址。又因为整个结构体的起始地址是0,因此MEMBER成员的实际地址在数值上就等于MEMBER成员相对于结构体首地址的偏移量。
1.3 扩展思考:
1.3.1 使用offsetof宏会影响内存0地址处的值吗?
答案是不会,从1.3.2可知offsetof宏的运算是在C编译器编译时完成的,因此内存的0地址在机器指令中根本未被操作,当然不会影响其值了。
1.3.2offsetof宏返回的MEMBER相对于结构体首地址的偏移量是如何得到的?->符号如何能正确寻址到结构体中某个成员变量?
想探究struct如何通过->精确寻址每一个成员,最好的办法就是将C代码汇编为.S的汇编语言代码,通过观察汇编代码可以看到C编译器对代码处理的具体细节。我们的示例代码如下:
#include"stdio.h"
#definemyoffsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
typedefstruct st
{
int a;
int c; //将该行加上或去掉,对比得到的汇编代码的差别
short d; //将该行加上或去掉,对比得到的汇编代码的差别
char b;
}st;
intgetoffsetof(void)
{
return myoffsetof(struct st, b);
}
将以上代码保存为offsetof.c,并且使用arm-linux-gcc offsetof.c –S执行汇编,则会得到offsetof.s文件,内容如下:
.file "offsetof.c"
.text
.align 2
.global getoffsetof
.type getoffsetof, %function
getoffsetof:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args= 0
mov ip, sp // 这三行
stmfd sp!, {fp, ip, lr, pc} // 是函数
sub fp, ip, #4 // 栈帧保存
mov r3, #10 // #10即是offsetof宏计算得到的值
mov r0, r3 // 将返回值置于R0中
sub sp, fp, #12 // 函数栈帧
ldmfd sp, {fp, sp, lr} // 恢复
bx lr // 函数返回
.size getoffsetof, .-getoffsetof
.ident "GCC: (GNU) 4.1.2"
以上汇编代码中mov r3, #10一句可以看出,offsetof宏计算member的偏移量是C编译器在编译阶段完成的,而并不需要CPU在运行时去计算得出。
可以尝试着更改struct st中成员b之前的成员,然后再次汇编,对比汇编后代码的不同,以此来验证我们的推论。