指针算术导致通过前一个成员地址(在同一结构中)指向另一个结构成员的指针

时间:2021-04-04 15:58:46

标签: c pointers language-lawyer

关于指针算术结果通过同一结构中的前一个成员地址指向另一个结构成员的指针,C 标准有何看法?


代码 1(无结构),mystery_1

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)怎么办?


代码 2(带结构),mystery_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

海湾合作委员会 10.2

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

Clang 11.0.1

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 中,返回值未确定,因为无法保证堆栈上局部变量的内存布局。
  • 在代码 2 中,返回值被确定并明确定义为 1,因为 p1 == p2 为 true,因为 struct 保证了内存布局。所以 my_var.one 的下一个地址是 my_var.two,编译器不允许假设 p1p2 因为它们的出处不同。

问题

  • 我的理解是否正确?
  • 根据 C 标准,mystery_2 是否总是返回 1,因为 p1 == p2 为 true?
  • mystery_2 中,编译器是否允许假设 p1 != p2,因此函数返回 0?

问题

我与某人讨论了结构体案例 (mystery_2),他们说:

p1 指向(过去)一,而 p2 指向二。在 C 规范中,这些被视为不同的“对象”。然后规范继续定义指向不同对象的指针可能比较不同,即使两个指针具有完全相同的位模式

2 个答案:

答案 0 :(得分:1)

<块引用>

我的理解正确吗?

没有

您对局部变量的看法是正确的;但不适用于结构示例。

<块引用>

根据 C 标准,当 p1 == p2 为真时,mystery_2 是否总是返回 1?

没有。 C 标准不能保证这一点。因为 onetwo 之间可以有填充。

实际上,在这个示例中,任何编译器都没有理由在它们之间插入填充。 而且您几乎总是可以期望 mystery_2 返回 1。但这不是 C 标准所要求的,因此病态编译器可以在 onetwo 之间插入填充,这将是完美的有效。

关于填充:唯一的保证是结构的第一个成员之前不能有任何填充。因此,指向结构的指针和指向其第一个成员的指针保证相同。没有任何其他保证。

注意:您应该使用 uinptr_t 来存储指针值(unsigned long 不能保证能够保存指针值)。

答案 1 :(得分:1)

指针算术的两个基础是,根据 C 2018 6.5.6 8:

  • 指向数组元素的指针可以调整(通过整数的加减法)指向数组的任何元素或指向数组末尾(最后一个元素之后的元素)。 C 标准未定义的算术运算。
  • 对于指针算法,单个对象的作用就像一个对象的数组。

因此 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,p1p2 仅在 twop2 指向)已在内存中的布局超出 {{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 可能会产生两个不同的值,尽管将它们转换回指针会导致比较为相等的指针。