在学习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解引用了
但实际上这段代码是可以运行的(起码在我的机器上是如此)

可以看到,打印出来的值为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;
}

你是否发现我要干什么了?
没错,打印出来的值,正好是这些 成员在各自结构体中的偏移量
所以,让我们绕回来,为什么空指针在这里能够解引用呢?
事实上,并没有发生解引用这个行为,你的 信仰---空指针不能解引用 没有崩塌,我就是个标题党
通过观察生成的汇编代码,我发现,(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
浙公网安备 33010602011771号