通过void指针进行指针转换是否定义良好?

17

假设我有一些如下定义的结构:

struct foo { int a; };
struct bar { struct foo r; int b; };
struct baz { struct bar z; int c; };

C标准是否保证以下代码是严格符合规范的?

struct baz x;
struct foo *p = (void *)&x;
assert(p == &x.z.r);

这种构造的动机是提供一致的编程习语,用于将已知兼容的指针类型转换为指针类型。


下面是C语言关于结构体及其初始成员可转换性的说明:

在一个结构体对象中,非位字段成员和位字段所在的单元的地址按照它们声明的顺序递增。适当转换后,指向结构体对象的指针指向其初始成员(或如果该成员是位字段,则指向其中所在的单元),反之亦然。结构体对象中可能有未命名的填充,但不会位于其开始处。
C.11 §6.7.2.1¶15

这是关于 void 指针转换的说明:

指向 void 的指针可以转换为任何对象类型的指针,也可以从任何对象类型的指针转换为指向 void 的指针;结果应与原始指针相等。
C.11 §6.3.2.3¶1

这是关于对象指针类型之间转换的说明:

对象类型的指针可以转换为不同对象类型的指针。如果结果指针未针对引用的类型正确对齐68),则行为未定义。否则,在再次转换时,结果应与原始指针相等。

68) 通常情况下,“正确对齐”概念是可传递的:如果对类型A的指针对于对类型B的指针正确对齐,并且对类型B的指针对于对类型C的指针正确对齐,则对类型A的指针对于对类型C的指针正确对齐。
C.11 §6.3.2.3¶7

从上面的内容中我理解,通过 void * 转换将对象指针转换为不同类型的对象指针是完全可以的。但是,我得到了一个意见表明这种转换是不合适的。


2
这里与您的特定示例相关的还有C11 6.2.5.28:“所有指向结构类型的指针应具有相同的表示和对齐要求”,因此在这个示例中,您至少可以确保不会陷入“如果结果指针未正确对齐”的陷阱。如果导致的指针没有正确对齐,则通过转换为void *进行强制转换也无法改变这一点。 - Crowman
1
我真的希望这没问题,否则我过去10年写的50%代码都废了。这是在C语言中实现多态性的基础:如果你有一个函数需要一个指向“struct foo”的指针,你同样可以传递一个指向“struct bah”或“struct baz”的指针。 - duncan
2
@duncan:这个问题是为了澄清对此答案的评论。 - jxh
4个回答

4
为了完成分析,有必要查看指针相等性的定义:
如果一个操作数是指向对象类型的指针,另一个操作数是指向限定或非限定版本的void的指针,则前者将被转换为后者的类型。只有当两个指针都是空指针、都是指向同一对象(包括指向对象和其开头的子对象的指针)或函数的指针、都是指向同一数组对象中最后一个元素后面的位置的指针,或者一个是指向一个数组对象的结束位置并且另一个是指向立即在第一个数组对象之后的另一个数组对象的开始位置的指针时,两个指针才相等。因此,这里有一个论据表明它是明确定义的:
(void*)&x == (void*)&x.z (void*)&x.z == &x.z.r (void*)&x == &x.z.r,传递相等性 (struct foo*)(void*)&x == (void*)&x (struct foo*)(void*)&x == &x.z.r,传递相等性
上述步骤的最后一步是初始化p和代码中断言的精髓。

4

您的示例是严格符合规定的。

来自§6.7.2.1 ¶15的第二句话(指向结构体对象的指针,经过适当转换,指向其初始成员...反之亦然。)保证以下相等关系:

(sruct bar *) &x == &(x.z)
(struct foo *) &(x.z) == &(x.z.r)

由于您在struct的开头,因此不会发生填充,根据标准,我理解struct和其第一个元素的地址是相同的。

所以struct foo *p = (void *) &x; 是正确的,就像struct foo *p = (struct foo *) &x; 一样。

在这种特殊情况下,根据§6.7.2.1 ¶15,对齐是保证正确的。并且始终允许通过void *传递指针,但这不是必需的,因为根据§6.3.2.3 ¶7,允许在不涉及对齐问题的情况下将指向不同对象的指针进行转换。

还应该注意到§6.2.3.2 ¶7也说:当将对象的指针转换为字符类型的指针时,结果将指向对象的最低寻址字节,这意味着所有这些指针实际上都指向x.r.z.a的最低寻址字节。因此,您还可以通过指向char的指针传递,因为我们也有:

(char *) &x == (char *) &(x.z) == (char *) &(x.z.r) == (char *) &(x.z.r.a)

void 不是一个字符类型,而“适当转换”可能意味着直接将其强制转换为目标类型。 - M.M
@MattMcNabb:我应该更准确地表达。我的意思是:(char *) &x == (char *) &(x.z) == (char *) &(x.z.r) == (char *) &(x.z.r.a)- 我会试着编辑我的帖子。我已经写过 struct foo *p = (struct foo *) &x; 是正确的了。 - Serge Ballesta

1
IMHO,除非对标准的严格解释有所质疑,否则在同一台编译器上,内存中对象的分配遵循相同的规则:提供适用于任何类型的变量的地址。因为每个结构都从其第一个变量开始,根据传递性质,该结构本身将对适合任何类型的变量的地址进行对齐。后者消除了不同结构具有不同地址的疑虑,因为地址定义遵循相同的规则,在转换之间不能进行任何修改。当然,对于以下结构字段,这并不适用,因为为符合后续字段的对齐要求,可能不是连续的。 如果您操作结构的第一个元素,则保证与结构本身的第一个字段相同。 现在看一下最流行的软件之一:the Independent JPEG group JPEGlib。整个软件在许多处理器和机器上编译,并使用类似于C++管理传递结构的技术,其开头始终相同,但在调用之间保留了许多其他不同的子结构和字段。 此代码适用于从玩具到PC到平板电脑等所有设备的编译和运行...

1
是的,从语言标准来看,您的示例严格符合规定,因此完全合法。这主要来自于您提供的两个引用(重点已突出显示)。第一个引用:
“指向void的指针可以转换为任何对象类型的指针或从任何对象类型的指针转换而来。”
这意味着在您的代码赋值中,我们成功地将结构体baz指针转换为void指针,然后由于两个指针对齐相等,成功地将void指针转换为结构体。如果不是这样的话,我们会由于不符合您提供的6.3.2.3而产生未定义行为。
第二个引用:
“通常情况下,“正确对齐”的概念是可传递的:如果指向类型A的指针对指向类型B的指针来说是正确对齐的,而指向类型B的指针对指向类型C的指针来说也是正确对齐的,则指向类型A的指针对指向类型C的指针来说也是正确对齐的。”
这个更为重要。它并没有声明(也不应该)类型A和类型C必须相同,这反过来又使它们不需要相同。唯一的限制是对齐方式。
就是这些。
当然,这样的操纵出于明显的原因是不安全的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接