假设我有一个结构并将偏移量提取到一个成员:
struct A {
int x;
};
size_t xoff = offsetof(A, x);
我如何才能在给定一个指针struct A
以符合标准的方式提取成员?当然,假设我们有一个正确的struct A*
和一个正确的偏移量。一种尝试是执行以下操作:
int getint(struct A* base, size_t off) {
return *(int*)((char*)base + off);
}
这可能会起作用,但请注意,例如,如果指针是同一数组的指针(或超过末尾的指针),则指针算术似乎仅在标准中定义,情况不一定如此。因此,从技术上讲,这种结构似乎依赖于未定义的行为。
另一种方法是
int getint(struct A* base, size_t off) {
return *(int*)((uintptr_t)base + off);
}
这也可能会起作用,但请注意,intptr_t
不需要存在,据我所知,intptr_t
的算术不需要产生正确的结果(例如,我记得一些 CPU 有能力处理非字节对齐的地址,这表明数组中每个char
intptr_t
以 8 步长增加)。
看起来标准中忘记了一些东西(或者我错过了什么)。
根据C标准,7.19 常见定义<stddef.h>
第3段offsetof()
定义为:
宏是
NULL
它扩展到实现定义的空指针常量;和
offsetof(*type*, *member-designator*)
它扩展到具有类型的整数常量表达式
size_t
,其值是以字节为单位的偏移量,到 结构成员(由成员指定符指定),来自 其结构的开头(按类型指定)。
因此,offsetoff()
返回以字节为单位的偏移量。
6.2.6.1 总则第4段规定:
存储在任何其他对象类型的非位字段对象中的值 包括 n ×CHAR_BIT位,其中n是该类型对象的大小,以字节为单位。
因为CHAR_BIT被定义为char
中的位数,char
是一个字节。
因此,根据标准,这是正确的:
int getint(struct A* base, size_t off) {
return *(int*)((char*)base + off);
}
这会将base
转换为char *
,并向地址添加off
字节。 如果off
是offsetof(A, x);
的结果,则生成的地址是base
指向的structure A
内x
的地址。
你的第二个例子:
int getint(struct A* base, size_t off) {
return *(int*)((intptr_t)base + off);
}
取决于将有符号intptr_t
值与无符号size_t
值相加的结果。
标准 (6.5.6) 只允许数组的指针算术的原因是结构可能具有填充字节以满足对齐要求。因此,在结构内进行指针算术确实是形式上未定义的行为。
在实践中,只要你知道自己在做什么,它就会起作用。base + off
不会失败,因为我们知道那里有有效的数据,并且只要正确访问它,它就不会错位。
因此,(intptr_t)base + off
确实是更好的代码,因为不再有任何指针算法,而只是普通的整数算法。因为intptr_t
是一个整数,所以它不是一个指针。
正如评论中指出的,此类型不保证存在,根据 7.20.1.4/1,它是可选的。我想为了获得最大的可移植性,您可以切换到保证存在的其他类型,例如intmax_t
或ptrdiff_t
.然而,一个不支持intptr_t
的 C99/C11 编译器是否有用是有争议的。
(这里有一个小的类型问题,即intptr_t
是有符号类型,不一定与size_t
兼容。您可能会遇到隐式类型升级问题。如果可能的话,使用uintptr_t
更安全。
接下来的问题是*(int*)((intptr_t)base + off)
是否是明确定义的行为。标准中关于指针转换的部分(6.3.2.3)说:
任何指针类型都可以转换为整数类型。除了 前面指定的结果是实现定义的。如果 结果不能用整数类型表示,行为是 定义。结果不必在任何值的范围内 整数类型。
对于这种特定情况,我们知道我们在那里有一个正确对齐的int
,所以很好。
(我认为任何指针别名问题也不适用。至少使用gcc -O3 -fstrict-aliasing -Wstrict-aliasing=2
编译不会破坏代码。