【问题标题】:Pointer arithmetic result in pointer to another struct member via previous member address (in the same struct)指针算术通过前一个成员地址(在同一个结构中)导致指向另一个结构成员的指针
【发布时间】:2021-04-04 15:58:46
【问题描述】:

C 标准中关于指针算术结果通过同一结构中的前一个成员地址指向另一个结构成员的指针的观点是什么?


代码 1(无结构),神秘_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(带结构),神秘_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;
}

编译器输出

天箭链接:https://godbolt.org/z/jGoKfETn7

GCC 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?

问题

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

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

【问题讨论】:

  • 不,我不认为这是有保证的。编译器可以在成员之间放置填充等。
  • @cigien 不,这不能回答我的问题。
  • 编译器可能做什么或可能不做什么完全无关紧要。您将其标记为 [language-lawyer],这意味着您想知道该语言所保证的内容。该语言允许编译器添加填充,因此无法保证行为。
  • 您似乎对语言标准的含义有误解。该标准只是对编译器的行为提出了要求。除了这些要求之外,编译器可以做任何它想做的事情并且仍然符合标准。例如如果标准没有对填充强加要求,编译器可以在整数之间插入 1GB 的填充并符合要求。编译器是否这样做是无关紧要的。
  • @AmmarFaizi 您认为“应该”的内容不会对编译器施加限制。

标签: c pointers language-lawyer


【解决方案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,反之亦然。 Per C 2018 6.5.9 6,p1p2 可以比较相等,仅当 twop2 指向)已布置在内存中超过 onep1 指向刚刚超过)。 (一般来说,由于其他原因,指针可以比较相等,但这些情况涉及指向同一个对象、一个结构及其第一个成员、同一个函数等的指针,所有这些都被排除在这个特定的 p1p2.)

因此,如果 two 位于内存中紧随 one 之后,代码 1 中的代码将返回 1,否则返回 2。

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

但是,two 不必立即关注one。这种说法是错误的:

… struct 保证内存布局。

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

注意事项

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

【讨论】:

    【解决方案2】:

    我的理解正确吗?

    没有。

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

    根据 C 标准,神秘_2 是否总是返回 1,因为 p1 == p2 产生 true?

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-09-24
      • 2019-03-18
      • 1970-01-01
      • 2023-03-12
      • 2021-07-26
      • 2014-06-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多