在头文件<stddef.h>中定义了宏 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
它的作用是获取结构体中成员的偏移量。
初看觉得这个宏设计得很巧妙,但细想又担心它会不会出现访问违例(access violation)而core dump啊。
让我们通过下面的程序来分析一下吧:
#include <stdio.h>
//#include <stddef.h>
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
int main(void)
{
typedef struct {
int i;
double d;
char name[10];
float f;
} TS;
TS *pp = (TS*)1;
int offset1 = (int)&(pp->d) - (int)pp;
int offset2 = (int)&(pp->d);// - (int) pp;
printf("%d, %d, %d\n", offset1, offset2, offsetof(TS, d));
return 0;
}
$ g++ offset.cpp -m32 ; ./a.out
4, 5, 4
如果我们把offset2那一行的注释去掉:int offset2 = (int)&(pp->d) - (int)pp; 则结果为4, 4, 4.
通过他们的汇编代码我们能清楚地看出编译器到底干嘛了:
int offset2 = (int)&(pp->d); // - (int)pp; | int offset2 = (int)&(pp->d) - (int)pp; |
.file "offset.cpp" .section .rodata .LC0: .string "%d, %d, %d\n" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp movl $1, 20(%esp) => pp赋值 movl $4, 24(%esp) => offset1 赋值 movl 20(%esp), %eax => offset2 赋值 addl $4, %eax movl %eax, 28(%esp) movl $4, 12(%esp) => offsetof(TS, d) 参数压栈 movl 28(%esp), %eax => offset2 参数压栈 movl %eax, 8(%esp) movl 24(%esp), %eax => offset1参数 压栈 movl %eax, 4(%esp) movl $.LC0, (%esp) => 字符串参数压栈 call printf movl $0, %eax leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2" .section .note.GNU-stack,"",@progbits | .file "offset.cpp" .section .rodata .LC0: .string "%d, %d, %d\n" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp movl $1, 20(%esp) => pp赋值 movl $4, 24(%esp) => offset1 赋值 movl $4, 28(%esp) => offset2 赋值 movl $4, 12(%esp) => offsetof(TS, d) 参数压栈 movl 28(%esp), %eax => offset2 参数压栈 movl %eax, 8(%esp) movl 24(%esp), %eax => offset1参数 压栈 movl %eax, 4(%esp) movl $.LC0, (%esp) => 字符串参数压栈 call printf movl $0, %eax leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2" .section .note.GNU-stack,"",@progbits |
$ diff offset1.s offset2.s -U2
--- offset1.s 2014-10-15 09:54:50.066410438 +0800
+++ offset2.s 2014-10-15 09:55:15.314411246 +0800
@@ -18,7 +18,5 @@
movl $1, 20(%esp)
movl $4, 24(%esp)
- movl 20(%esp), %eax
- addl $4, %eax
- movl %eax, 28(%esp)
+ movl $4, 28(%esp)
movl $4, 12(%esp)
movl 28(%esp), %eax
从上面的汇编代码可以看出,(int)&(pp->d) - (int)pp被编译器直接计算出结果为4, 这个是d的偏移量,因为它减去了起始地址; 而(int)&(pp->d),则是先得到pp的值,再加上d的偏移量,从而得到d的地址,这是成员相对于起始地址的地址。
这里我们故意将pp的值设为1,所以(int)&(pp->d)的值为5。即使将pp的值设为0,编译器还是会分几步算出(int)&(pp->d)的值, 只不过这时候的值刚好等于d的偏移量。
所以我们可以看出,在计算(int)&(pp->d)的值时,编译器并不关心起始地址是多少,它直接将pp所指向的地址值,再加上d的偏移量,就得到了d的地址。结构体成员的偏移量是固定的,编译时就确定的。 即使我们不给pp赋值,(int)&(pp->d) - (int)pp还是能计算出正确的偏移量,而(int)&(pp->d)则会是一个随机值了。这个语义告诉我们,编译器在看到(int)&(pp->d)时,并不会通过pp去访问d,然后得到d的地址,而是直接将pp的值作为起始地址,在加上d的偏移量,从而得到d的地址。所以也就不会出现访问违例的问题了。
从上面的汇编代码还可以看出,offsetof能直接算出偏移量,因为它没有经过一个中间指针,而是直接将0强制转换为结构体的起始地址。
另外,从汇编代码可以看出,printf的参数是从右往左压栈的,这个话题会在另一篇文章中展开。