c struct offsetof

在头文件<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的参数是从右往左压栈的,这个话题会在另一篇文章中展开。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值