【问题标题】:Pointer difference across members of a struct?结构成员之间的指针差异?
【发布时间】:2014-12-29 23:00:12
【问题描述】:

C99 标准规定:

当两个指针相减时,两个指针都指向同一个数组对象的元素,或者指向数组对象最后一个元素的元素

考虑以下代码:

struct test {
    int x[5];
    char something;
    short y[5];
};

...

struct test s = { ... };
char *p = (char *) s.x;
char *q = (char *) s.y;
printf("%td\n", q - p);

这显然违反了上述规则,因为pq 指针指向不同的“数组对象”,并且根据规则,q - p 的区别是未定义的。

但在实践中,为什么这样的事情会导致未定义的行为?毕竟,结构成员是按顺序排列的(就像数组元素一样),成员之间有任何潜在的填充。确实,填充量会因实现而异,这会影响计算的结果,但为什么该结果应该是“未定义的”?

我的问题是,我们可以假设标准只是“无知”这个问题,还是有充分的理由不扩大这条规则?上述规则不能改写为“都应该指向同一个数组对象的元素或同一个结构的成员”吗?

我唯一的怀疑是分段内存架构,其中成员可能最终位于不同的段中。是这样吗?

我还怀疑这就是 GCC 定义自己的 __builtin_offsetof 的原因,以便对 offsetof 宏进行“符合标准”的定义。

编辑:

正如已经指出的,标准不允许对 void 指针进行算术运算。它是一个 GNU 扩展,仅当 GCC 通过-std=c99 -pedantic 时才会触发警告。我正在用char * 指针替换void * 指针。

【问题讨论】:

  • 并且在您的示例中,无论如何都禁止对 void 进行指针运算。如果类型不一样,你怎么能减去它们?
  • 您是正确的,标准不允许对 void 指针进行算术,它是 GNU 扩展。想象一下,两个指针都是char *
  • gcc 允许在 void* 上进行指针运算,方法是将 sizeof(void) 视为 1(与 char* 相同)。因此,就您的问题而言,这没有区别。
  • 这是允许边界检查实现所必需的,也就是说,afaik,标准委员会的意图(或者至少是 C89 的意图)允许它。我相信实现检查边界可以(必须)可靠地捕捉到这种情况(也就是说,它是 UB,尽管它在现实中有效)。不过,这样的实现会破坏很多现有的代码。而且标准对对象的概念有点模糊,因此很难得到准确的答案。
  • @mafso:在没有别名规则的情况下,每个对象都可以被视为联合的成员,该联合包含可能占用空间的每种类型的成员。给定struct {int x, y;} foo;,如果对于某个整数值nfoo.y 的地址等于((int*)&foo)+n,那么&foo.x&foo.y 将是整数数组中元素0 和n 的地址从地址&foo 开始。不幸的是,标准的作者粗心地引入了别名规则,这些规则依赖于他们从未定义过的“对象”的细节,因为语言不需要它们。

标签: c pointers c99 ptrdiff-t


【解决方案1】:

是的,您可以对结构字节执行指针算术运算:

N1570 - 6.3.2.3 指针 p7:

...当指向对象的指针转换为指向字符类型的指针时, 结果指向对象的最低寻址字节。 的连续增量 结果,直到对象的大小,产生指向对象剩余字节的指针。

这意味着对于程序员来说,结构的字节应该被看作是一个连续的区域,不管它是如何在硬件中实现的。

虽然没有void* 指针,但这是非标准的编译器扩展。正如标准段落中提到的,它仅适用于字符类型指针。

编辑:

正如 mafso 在 cmets 中指出的那样,只要减法结果的类型 ptrdiff_t 具有足够的结果范围,上述内容才是正确的。由于size_t 的范围可以大于ptrdiff_t,并且如果结构足够大,则地址可能相距太远。

因此,最好在结构成员上使用offsetof 宏并从中计算结果。

【讨论】:

  • +1,我也相信我引用的规则中的“元素”一词用于区分单纯的char *指针和与数组元素对应的类型的正确对齐的指针。
  • 这个答案似乎暗示填充可以被认为是我高度怀疑的对象。
  • 这个答案没有抓住重点。 1. 如果p + n 比较等于q,这并不意味着定义了q - p。 2.重要的是,这里的对象是什么(整个结构或只是成员)。我倾向于以后者的方式解释标准。
  • @ShafikYaghmour 标准中的这个条款似乎只涵盖了您可以通过这种方式获得有效指针的事实。是否可以取消引用所述指针在标准的其他地方都有介绍。
  • ptrdiff_t 被允许比size_t 小得多(标准没有说明它们的关系),所以有可能只是未定义差异(如果q-p已定义,结果如预期)。 (前段时间我有asked a question。)对于小于 2**15 的结构,它总是被定义,它与这里的问题并没有真正的关系,我的意思是,暗示“如果p+n 是已定义,p+n-n" 也是不正确的(在您的帖子中默默假设)。
【解决方案2】:

我应该指出以下几点:

来自 C99 标准,第 6.7.2.1 节:

在结构对象中,非位域成员和位域所在的单元 驻留的地址按照它们被声明的顺序增加。一个指针 适当转换的结构对象指向其初始成员(或者如果该成员是 位域,然后到它所在的单元),反之亦然。 可能有未命名的 在结构对象内填充,但不在其开头。

成员之间的指针减法的结果与其说是不确定的不如说是不可靠的(即,当应用相同的算术时,不能保证在相同结构类型的不同实例之间是相同的)。

【讨论】:

    【解决方案3】:

    指针算术要求被相加或相减的两个指针是同一个对象的一部分,否则它没有意义。 标准的引用部分特指两个不相关的对象,例如int a[b];int b[5]。指针算法需要知道指针指向的对象的类型(我相信你已经知道了)。

    int a[5];
    int *p = &a[1]+1; 
    

    这里的p 是通过知道&a[1] 引用int 对象并因此增加到4 个字节(假设sizeof(int) 是4)来计算的。

    来到 struct 示例,我认为它不可能被定义为使 struct 成员之间的指针算术合法。

    我们举个例子,

    struct test {
        int x[5];
        char something;
        short y[5];
    };
    

    C 标准不允许使用 void 指针进行指针运算(使用 gcc -Wall -pedantic test.c 编译会发现这一点)。我认为您正在使用 gcc,它假定 void* 类似于 char* 并允许它。 所以,

    printf("%zu\n", q - p);
    

    等价于

    printf("%zu", (char*)q - (char*)p);
    

    如果指针指向同一个对象并且是字符指针(char*unsigned char*),则指针算术定义良好。

    使用正确的类型,它将是:

    struct test s = { ... };
    int *p = s.x;
    short *q = s.y;
    printf("%td\n", q - p);
    

    现在,如何执行q-p?基于sizeof(int)sizeof(short) ?这两个数组中间的char something;的大小如何计算?

    这应该说明不可能对不同类型的对象执行指针运算。

    即使所有成员都是相同类型(因此没有上述类型问题),那么最好使用标准宏offsetof(来自<stddef.h>)来获得具有相似效果的结构成员之间的差异作为成员之间的指针运算:

    printf("%zu\n", offsetof(struct test, y) - offsetof(struct test, x));
    

    所以我认为没有必要通过 C 标准定义结构成员之间的指针运算。

    【讨论】:

    • 很好的答案,但是你忘记了当指针都是char* 类型并且指向同一个对象时是允许的。没有它,就不可能定义offsetof
    • 当然。但我不确定我在哪里反驳或暗示?
    • 您并没有直接反驳它,但恕我直言,这是一个重要的“漏洞”。
    • TBH,除了在您发表评论后作为不相交的事实之外,我在逻辑上可以将其包含在内的地方很挣扎。已编辑。谢谢。
    • 指向不同对象的指针之间的指针算术和关系运算符(<<=>>=)没有必然没有意义。语言可以使结果未指定而不是未定义行为,并要求其行为一致(因此&x < &y && &y < &z 暗示&x < &z,等等)。在许多系统上,它确实以这种方式工作。该标准未定义此类操作,因为它们可能难以在某些架构上一致实施,而且额外的实施工作不会为您带来任何特别有用的东西。
    【解决方案4】:

    我相信这个问题的答案比看起来更简单,OP 问道:

    但为什么结果应该是“未定义”?

    好吧,让我们看看未定义行为的定义在草案C99标准部分3.4.3

    行为,在使用不可移植或错误的程序结构或 错误数据,本国际标准对此没有规定 要求

    这只是标准没有强加要求的行为,它完全适合这种情况,结果会因架构而异,尝试指定结果可能会很困难,如果不是不可能的话方式。这就留下了一个问题,为什么他们会选择未定义的行为,而不是说实现未指定的行为?

    很可能是未定义的行为来限制创建无效指针的方式的数量,这与我们提供offsetof 以消除一个潜在需求这一事实一致用于不相关对象的指针减法。

    虽然标准并没有真正定义术语无效指针,但我们在 Rationale for International Standard—Programming Languages—C 中得到了很好的描述,在 6.3.2.3 部分 Pointers 说(强调我的) :

    标准中隐含的是无效指针的概念。在 在讨论指针时,该标准通常指的是“指向 对象”或“指向函数的指针”或“空指针”。一个特别的 地址算术中的大小写允许指向刚刚结束的指针 的一个数组。 任何其他指针都无效。

    C99 的基本原理进一步补充道:

    不管无效指针是如何创建的,对它的任何使用都会产生 未定义的行为。偶数赋值,与空指针比较 常数,或与自身比较,在某些系统上可能会导致 例外。

    这强烈地向我们表明,指向 padding 的指针将是一个 无效指针,尽管很难证明 padding 不是一个objectobject的定义说:

    执行环境中的数据存储区域,内容为 可以表示值

    和注释:

    当被引用时,一个对象可能被解释为具有特定的 类型;见 6.3.2.1。

    我不明白我们如何推断结构元素之间的 typevalue 填充,因此它们不是 objects em> 或至少是强烈表示 padding 并不意味着被视为 对象

    【讨论】:

    • 我看不出指向填充的指针如何成为无效指针。填充不是对象,而是对象的一部分。毕竟,标准保证填充将存在,只有它的值是未指定的(6.2.6.1p1)。见基思汤普森answer
    【解决方案5】:

    相同结构的成员地址之间的减法和关系运算符(类型为char*)定义良好。

    任何对象都可以被视为unsigned char的数组。

    引用N1570 6.2.6.1 第4段:

    存储在任何其他对象类型的非位域对象中的值 由 n × CHAR_BIT 位组成,其中 n 是该对象的大小 类型,以字节为单位。该值可以复制到类型的对象中 unsigned char [ n ](例如,通过 memcpy);结果的字节集是 称为值的对象表示。

    ...

    我唯一的怀疑是分段内存架构,其中成员 最终可能会出现在不同的细分市场。是这样吗?

    没有。对于具有分段内存架构的系统,通常编译器会施加限制,即每个对象必须适合单个段。或者它可以允许占用多个段的对象,但它仍然必须确保指针算术和比较正常工作。

    【讨论】:

    • +1 我认为答案比我的答案更能说明问题。
    • 我不相信。如果指向对象的指针始终可以被视为指向最外层封闭对象的指针,例如,struct hack 也是合法的......
    • @mafso 好吧,在标准中,索引中有 is 条目:struct hack, see flexible array member...
    • @user694733:灵活的数组成员在 1999 年被添加到标准中,作为 struct hack 的替换,其合法性值得怀疑。
    • @KeithThompson 我知道。我不得不这样做。 :)
    猜你喜欢
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多