在 ANSI C 中,offsetof 定义如下。
#define offsetof(st, m) \
((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))
既然我们正在取消引用 NULL 指针,为什么这不会引发分段错误?或者这是某种编译器破解,它看到只有偏移的地址被取出,所以它静态计算地址而不实际取消引用它?这段代码也是可移植的吗?
上述代码中的任何地方都没有取消引用。当*
或->
用于地址值以查找引用值时,会发生取消引用。上面的唯一用途*
是在类型声明中用于强制转换。
上面使用->
了运算符,但它不用于访问值。相反,它用于获取值的地址。这是一个非宏代码示例,应该使它更清晰一些
SomeType *pSomeType = GetTheValue();
int* pMember = &(pSomeType->SomeIntMember);
第二行实际上不会导致取消引用(依赖于实现)。它只是返回值SomeIntMember
内的地址pSomeType
。
您看到的是任意类型和 char 指针之间的大量转换。char 的原因是它是 C89 标准中唯一(可能是唯一)具有明确大小的类型之一。大小为 1。通过确保大小为 1,上面的代码可以执行计算值的真实偏移量的邪恶魔法。
虽然这是 的典型实现offsetof
,但标准并没有强制要求,它只是说:
标准头文件中定义了以下类型和宏
<stddef.h>
[...]
offsetof(
type
,
member-designator
)
它扩展为具有类型的整数常量表达式
size_t
,其值是以字节为单位的偏移量,member-designator
从其结构的开头(由 指定)到结构成员(由 指定type
)。类型和成员代号应使给定的
static
type
t;
然后表达式计算为地址常量。(如果指定的成员是位域,则行为未定义。)
&(t.
member-designator
)
阅读 PJ Plauger 的“标准 C 库”以讨论它以及其他项目,<stddef.h>
其中所有的边界线特性都可能(应该?)在适当的语言中,并且可能需要特殊的编译器支持。
它仅具有历史意义,但我在 386/IX 上使用了早期的 ANSI C 编译器(请参阅,我告诉过您具有历史意义,大约在 1990 年),该编译器在该版本上崩溃,offsetof
但在我将其修改为:
#define offsetof(st, m) ((size_t)((char *)&((st *)(1024))->m - (char *)1024))
那是某种编译器错误,尤其是因为标头与编译器一起分发并且不起作用。
在 ANSI C 中,offsetof
不是这样定义的。没有这样定义的原因之一是某些环境确实会抛出空指针异常,或者以其他方式崩溃。因此,ANSI C 将开放的实现留给offsetof( )
编译器构建器。
上面显示的代码对于不主动检查 NULL 指针但仅在从 NULL 指针读取字节时失败的编译器/环境来说是典型的。
要回答问题的最后一部分,代码不可移植。
仅当两个指针指向同一数组中的对象或指向数组最后一个对象之后的对象时,才定义和可移植两个指针相减的结果(7.6.2 加法运算符,H&S 第五版)
它没有段错误,因为您没有取消引用它。指针地址被用作从另一个数字中减去的数字,而不是用于寻址内存操作。
它计算成员m
相对于类型对象表示的起始地址的偏移量st
。
((st *)(0))
指的NULL
是类型的指针st *
。
&((st *)(0))->m
指的是这个对象中成员m的地址。由于这个对象的起始地址是0 (NULL)
,所以成员 m 的地址就是偏移量。
char *
转换和差异计算以字节为单位的偏移量。根据指针操作,当你对两个类型的指针进行区分时T *
,结果是T
操作数包含的两个地址之间表示的类型对象的数量。
清单 1:一组有代表性的offsetof()
宏定义
// Keil 8051 compiler
#define offsetof(s,m) (size_t)&(((s *)0)->m)
// Microsoft x86 compiler (version 7)
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)
// Diab Coldfire compiler
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
typedef struct
{
int i;
float f;
char c;
} SFOO;
int main(void)
{
printf("Offset of 'f' is %zu\n", offsetof(SFOO, f));
}
宏中的各种运算符按顺序进行评估,以便执行以下步骤:
((s *)0)
取整数零并将其转换为指向s
.((s *)0)->m
取消引用指向结构成员的指针m
。&(((s *)0)->m)
计算 的地址m
。(size_t)&(((s *)0)->m)
将结果转换为适当的数据类型。根据定义,结构本身位于地址 0。因此,指向的字段的地址(上面的步骤 3)必须是从结构开始的偏移量(以字节为单位)。
offsetof
引用宏的 C 标准:
C 标准,第 6.6 节,第 9 段
地址常量是空指针、指向指定静态存储持续时间对象的左值的指针或指向函数指示符的指针;它应使用一元运算
&
符或转换为指针类型的整数常量显式创建,或通过使用数组或函数类型的表达式隐式创建。数组下标[]
和成员访问.
和->
操作符、地址&
和间接*
一元操作符以及指针转换可用于创建地址常量,但不得使用这些操作符访问对象的值。
宏定义为
#define offsetof(type, member) ((size_t)&((type *)0)->member)
并且该表达式包括地址常数的创建。
虽然说实话,结果不是地址常量,因为它不指向静态存储持续时间的对象。但这仍然是一致的,即不应访问对象的值,因此不会取消引用转换为指针类型的整数常量。
另外,请考虑 C 标准中的这句话:
C 标准,第 7.19 节,第 3 段
类型和成员代号应使给定的
static type t;
然后表达式
&(t.member-designator)
计算为地址常量。(如果指定的成员是位域,则行为未定义。)
C 中的 struct 是一种复合数据类型(或记录)声明,它在内存块中以一个名称定义变量的物理分组列表,允许通过单个指针或通过返回的结构声明名称访问不同的变量同一个地址。
从编译器的角度来看,结构声明的名称是一个地址,而成员指示符是该地址的偏移量。