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

Pointer arithmetic result in pointer to another struct member via previous member address (in the same struct)

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;
}

编译器输出

神马 link: 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    , %eax # return 1, as compiler must consider the memory order of struct members
        ret

叮当声 11.0.1

mystery_1:                              # @mystery_1
        movl    , %eax # return 2, while gcc returns 0 (fine as no guarantee)
        retq
mystery_2:                              # @mystery_2
        movl    , %eax # return 1, as compiler must consider the memory order of struct members
        retq

我的理解

问题

问题

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

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

Is my understanding correct?

没有

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

According to C standard, does mystery_2 always return 1 as p1 == p2 yields true?

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

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

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

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

根据 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 指向)已在内存中布局超过 onep1 点刚好超出)。 (一般来说,指针可以出于其他原因比较相等,但这些情况涉及指向同一对象、结构及其第一个成员、同一函数等的指针,所有这些都被排除在这个特定的 p1p2.)

因此,代码 1 中的代码将 return 1 如果 two 刚好在 one 之后布局在内存中,否则为 2。

代码2中也是如此,定义了指针运算&my_var.one + 1,结果p1比较等于p2当且仅当成员two 在记忆中紧跟成员one

但是,two 不必紧跟在 one 之后。这种说法是错误的:

… struct guarantees the memory layout.

C 标准允许实现在结构成员之间放置填充。常见的 C 实现不会为 struct { int one, two; } 执行此操作,因为它不需要对齐(一旦 one 对齐,紧随其后的地址也适合 int,因此没有填充需要),但 C 标准不保证它。

备注

<stdint.h> 中声明的

uintptr_t 是将指针转换为整数的更好选择。但是,该标准仅保证 (uintptr_t) px == (uintptr_t) py 意味着 px == py,而不保证 px == py 意味着 (uintptr_t) px == (uintptr_t) py。换句话说,将指向同一对象的两个指针转换为 uintptr_t 可能会产生两个不同的值,尽管将它们转换回指针将导致指针比较相等。