关于指针算术结果通过同一结构中的前一个成员地址指向另一个结构成员的指针,C 标准有何看法?
int mystery_1(void)
{
int one = 1, two = 2;
int *p1 = &one + 1;
int *p2 = &two;
unsigned long i1 = (unsigned long) p1;
unsigned long i2 = (unsigned long) p2;
if (i1 == i2)
return p1 == p2;
return 2;
}
从代码 1 中,我知道结果是不确定的,因为无法保证堆栈上的局部变量如何放置。
如果我像这样使用结构体(代码 2)怎么办?
int mystery_2(void)
{
struct { int one, two; } my_var = {
.one = 1, .two = 2
};
int *p1 = &my_var.one + 1;
int *p2 = &my_var.two;
unsigned long i1 = (unsigned long) p1;
unsigned long i2 = (unsigned long) p2;
if (i1 == i2)
return p1 == p2;
return 2;
}
Godbolt 链接:https://godbolt.org/z/jGoKfETn7
mystery_1:
xorl %eax, %eax # return 0, while clang returns 2 (fine as no guarantee)
ret
mystery_2:
movl $1, %eax # return 1, as compiler must consider the memory order of struct members
ret
mystery_1: # @mystery_1
movl $2, %eax # return 2, while gcc returns 0 (fine as no guarantee)
retq
mystery_2: # @mystery_2
movl $1, %eax # return 1, as compiler must consider the memory order of struct members
retq
1
,因为 p1 == p2
为 true,因为 struct 保证了内存布局。所以 my_var.one
的下一个地址是 my_var.two
,编译器不允许假设 p1
和 p2
因为它们的出处不同。mystery_2
是否总是返回 1,因为 p1 == p2
为 true?mystery_2
中,编译器是否允许假设 p1 != p2
,因此函数返回 0?我与某人讨论了结构体案例 (mystery_2
),他们说:
p1
指向(过去)一,而 p2
指向二。在 C 规范中,这些被视为不同的“对象”。然后规范继续定义指向不同对象的指针可能比较不同,即使两个指针具有完全相同的位模式
答案 0 :(得分:1)
我的理解正确吗?
没有
您对局部变量的看法是正确的;但不适用于结构示例。
<块引用>根据 C 标准,当 p1 == p2 为真时,mystery_2 是否总是返回 1?
没有。 C 标准不能保证这一点。因为 one
和 two
之间可以有填充。
实际上,在这个示例中,任何编译器都没有理由在它们之间插入填充。
而且您几乎总是可以期望 mystery_2
返回 1。但这不是 C 标准所要求的,因此病态编译器可以在 one
和 two
之间插入填充,这将是完美的有效。
关于填充:唯一的保证是结构的第一个成员之前不能有任何填充。因此,指向结构的指针和指向其第一个成员的指针保证相同。没有任何其他保证。
注意:您应该使用 uinptr_t
来存储指针值(unsigned long
不能保证能够保存指针值)。
答案 1 :(得分:1)
指针算术的两个基础是,根据 C 2018 6.5.6 8:
因此 int *p1 = &one + 1;
具有定义的行为。
关于:
unsigned long i1 = (unsigned long) p1;
unsigned long i2 = (unsigned long) p2;
由于它不是这个问题的重点,让我们假设实现定义的指针到 unsigned long
的转换产生唯一标识指针值的唯一值。 (也就是说,将任何地址转换为 unsigned long
只会为该地址生成一个值,而将该值转换回指针会重现该地址。C 标准不保证这一点。)
那么,如果 i1 == i2
,它意味着 p1 == p2
,反之亦然。根据 C 2018 6.5.9 6,p1
和 p2
仅在 two
(p2
指向)已在内存中的布局超出 {{1 }} (其中 one
点刚好超出)。 (通常,由于其他原因,指针可以比较相等,但这些情况涉及指向同一对象、结构及其第一个成员、同一函数等的指针,所有这些都被排除在这个特定的 {{1} } 和 p1
。)
因此,如果 p1
位于内存中的 p2
之后,则代码 1 中的代码将返回 1,否则返回 2。
代码 2 中也是如此。定义了指针算法 two
,结果 one
比较等于 &my_var.one + 1
当且仅当成员 p1
立即跟随内存中的成员 p2
。
但是,two
不必紧跟在 one
之后。这个说法是错误的:
... struct 保证内存布局。
C 标准允许实现在结构成员之间放置填充。常见的 C 实现不会对 two
执行此操作,因为对齐不需要它(一旦 one
对齐,紧随其后的地址也适合 struct { int one, two; }
对齐,因此没有填充需要),但 C 标准不保证。
one
中声明的 int
是将指针转换为整数的更好选择。但是,该标准仅保证 uintptr_t
隐含 <stdint.h>
,而不保证 (uintptr_t) px == (uintptr_t) py
隐含 px == py
。换句话说,将指向同一对象的两个指针转换为 px == py
可能会产生两个不同的值,尽管将它们转换回指针会导致比较为相等的指针。