空指针(NULL)的用法问题

问题的背景

这两天在研究字节对齐的问题,偶然看到了博客园某位答主的代码:

/* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
#define OFFSET(st, field)     (size_t)&(((st*)0)->field)
typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;

int main(void){
    printf("a-%d, b-%d, c-%d, d-%d\n", 
            OFFSET(T_Test, a), 
            OFFSET(T_Test, b), 
            OFFSET(T_Test, c), 
            OFFSET(T_Test, d));
    printf("a-%d\n", (size_t)&(((T_Test*)0)->a));
    return 0;
}

这段代码的用途是查看结构中每个字段的偏移地址,程序的输出如下:

a-0, b-2, c-4, d-8
a-0

问题

我的问题是

#define OFFSET(st, field)     (size_t)&(((st*)0)->field)

这个宏定义中的 (size_t)&(((st*)0)->field) 是如何起作用的,((st*)0) 不是一个 st* 类型的空指针吗,为什么能够通过它访问 T_Test 中的字段。

tyoedef struct{ int a; char b; }Msg; 结构体Msg,如果将一个整数0强转成Msg可以理解为,Msg所在的首地址为0,那么((Msg *)0)->b,就可以理解为取结构体成员b的内容,但是我们自己知道其实0这个首地址并没有存储结构体Msg的信息,取内容也就毫无意义,所以我们不要取内容。我们看看对这个成员取地址(引用)会有什么事情发生,&(((Msg *)0)->b)就相当于结构体Msg的成员b的首地址。我们也可以换一种思维理解,其实&(((Msg *)0)->b)就是b相对于结构体首地址的偏移量,因为结构体首地址为0,偏移后的地址减去首地址就是偏移量,偏移后的地址-0=偏移量,那么偏移量就等于偏移后的地址,那么如果在对结构体成员做偏移处理的时候看着就非常清晰,而且后期维护添加成员使用这个方法源码不用做任何改动,因为无论增加多少成员偏移量都是动态的并不是用常量写死的. 希望对你有用

((st*)0) 是一个 st* 类型的空指针.
但是这里没有去访问里面的元素,只是%d去打印了地址的值,所以合法,他这样做的目的是为了将a的地址设为0,从而更好的看地址的偏移
T_Test* p = (T_Test*)0;
printf("%d\n",&(p->a));
这样是合法的

但是此时你使用以下就不行了
T_Test* p = (T_Test*)0;
printf("%d\n",p->a);
或者你可以使用%p去打印地址,也是null
T_Test* p = (T_Test*)0;
printf("%p\n",p);