在学习c语言的过程中,我们被明确告知空指针是不能解引用的,会触发段错误,未定义行为,balabala

事实上这也没错,但是,有一个特殊的例子,请看下面这段程序

#include <stdio.h>

typedef struct mystruct {
    char a; // 0
    int b; // 4
    char c; // 8
} mystruct_t; // 12

int main(void)
{
    mystruct_t *p = NULL;
    size_t addr = (size_t) &p->a;
    printf("a: %ld\n", addr);

    return 0;
}

敏锐的你,很快就发现了,我们好像对空指针p解引用了

但实际上这段代码是可以运行的(起码在我的机器上是如此)

1

可以看到,打印出来的值为0,在多添加一点代码

#include <stdio.h>

typedef struct mystruct {
    char a; // 0
    int b; // 4
    char c; // 8
} mystruct_t; // 12

int main(void)
{
    mystruct_t *p = NULL;
    size_t addr = (size_t) &p->a;
    printf("a: %ld\n", addr);
    addr = (size_t) &p->b;
    printf("b: %ld\n", addr);
    addr = (size_t) &p->c;
    printf("c: %ld\n", addr);

    return 0;
}

1

你是否发现我要干什么了?

没错,打印出来的值,正好是这些 成员在各自结构体中的偏移量

所以,让我们绕回来,为什么空指针在这里能够解引用呢?

事实上,并没有发生解引用这个行为,你的 信仰---空指针不能解引用 没有崩塌,我就是个标题党

通过观察生成的汇编代码,我发现,(size_t) &p->a 这个的值在编译期就确定为0了,所以自然不会发生 解引用 (一个运行时发生的动态行为) 了,故而没有报错

那为什么能够在编译期确定呢?

是因为 p = NULL,就相当于 p = 0,换句话说,就是 p的值在编译期就能够确定为0

然后我们的->运算符,正常来说,是 先进行指针运算,然后以某种方式保存下来,等到 程序运行时 在去指定的地址处获取具体的值,这就是 我理解的解引用

还是举 &p->a 这个例子,那么他在编译后得到的地址应该是,p的绝对地址加上a成员的偏移量,正常来说,p的绝对地址是要在运行时才能知道a成员的偏移量能够在编译期确定(编译器要收集类型信息嘛)

当我们设置 p = NULL 时,意味着 p的绝对地址为0,由上面的公式,我们就能够在编译期得到具体的值,这就是他为什么没有报错了,因为 我们根本就没在运行时访问对应的内存,我们只是获取了这个非法地址罢了,又没干什么坏事

你可以试试加上 p->a = 0未定义行为 段错误 警告

有了以上的解析

我们可以得到一个宏,用于计算结构体成员的偏移量

#define offsetof(type, member) ((size_t) &(((type *) 0)->member))

(type *) 0 就相当于 NULL 了,不过显示指定为 0 更保险,

所以最后我们的程序变成了

#include <stdio.h>

#define offsetof(type, member) ((size_t) &(((type *) 0)->member))

typedef struct mystruct {
    char a; // 0
    int b; // 4
    char c; // 8
} mystruct_t; // 12

int main(void)
{
    printf("%ld\n", sizeof(mystruct_t));
    printf("a: %ld\n", offsetof(mystruct_t, a));
    printf("b: %ld\n", offsetof(mystruct_t, b));
    printf("c: %ld\n", offsetof(mystruct_t, c));

    return 0;
}
 posted on 2025-06-11 17:00  Dylaris  阅读(106)  评论(0)    收藏  举报