C99标准规定:
When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object
号
考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
", q - p ); |
这显然打破了上述规则,因为p和q指针指向不同的"数组对象",并且根据规则,q - p的差异是未定义的。
但在实践中,为什么这样的事情会导致不明确的行为呢?毕竟,结构成员是按顺序排列的(就像数组元素一样),成员之间有任何潜在的填充。是的,填充的量会随着实现的不同而变化,这会影响计算的结果,但是为什么结果应该是"未定义的"?
我的问题是,我们是否可以假设标准只是"无知"这个问题,或者是否有充分的理由不扩大这一规则?不能将上述规则改为"两者都应指向同一数组对象的元素或同一结构的成员"?
我唯一的怀疑是分段的内存结构,其中的成员可能以不同的段结束。是这样吗?
我也怀疑这就是为什么GCC定义了自己的__builtin_offsetof,以便对offsetof宏有一个"符合标准"的定义。
编辑:
正如已经指出的,标准不允许对空指针进行算术运算。它是一个GNU扩展,只有当GCC通过-std=c99 -pedantic时才会发出警告。我用char *指针替换void *指针。
- 在您的示例中,void上的指针算术无论如何都是被禁止的。如果类型不相同,如何减去它们?
- 您是正确的,标准不允许对空指针进行算术运算,它是GNU扩展。假设两个指针都是char *。
- gcc通过将sizeof(void)处理为1(与char*相同),允许在void*上进行指针运算。所以就你的问题而言,这没有什么不同。
- 这对于允许边界检查实现是必要的,也就是说,afaik是标准委员会(或者至少是C89)允许的意图。我相信实现检查界限可靠地(必须)捕获这种情况(即,它是ub,尽管它在实际中工作)。不过,这样的实现会破坏许多现有的代码。标准对它的对象概念有点含糊不清,这使得很难得到准确的答案。
- @Mafso:在没有别名规则的情况下,每个对象都可以被视为联合体的成员,该联合体包含可以占用空间的每种类型的成员。给定struct {int x, y;} foo;,如果对于某个整数值n,foo.y的地址等于((int*)&foo)+n,那么&foo.x和&foo.y将是从地址&foo开始的整数数组的元素0和n的地址。不幸的是,标准火腿的作者们不遗余力地加入了别名规则,这些规则依赖于他们从未定义过的"对象"的细节,因为语言不需要它们。
同一结构成员的地址之间的减法和关系运算符(类型char*上)定义得很好。
任何对象都可以被视为unsigned char的数组。
引用N1570 6.2.6.1第4段:
Values stored in non-bit-field objects of any other object type
consist of n × CHAR_BIT bits, where n is the size of an object of that
type, in bytes. The value may be copied into an object of type
unsigned char [ n ] (e.g., by memcpy); the resulting set of bytes is
called the object representation of the value.
号
…
My only suspicion are segmented memory architectures where the members
might end up in different segments. Is that the case?
号
不。对于具有分段内存结构的系统,通常编译器会施加一个限制,即每个对象必须适合于一个段。或者它可以允许占用多个段的对象,但是它仍然必须确保指针算术和比较工作正常。
- +我认为答案比我的答案更能说明问题。
- 我不相信。如果一个指向对象的指针总是可以被视为指向最外层封闭对象的指针,例如,struct hack也是合法的…
- @Mafso好吧,在标准中,索引中有条目:struct hack, see flexible array member…
- @用户694733:1999年,灵活的数组成员被添加到标准中,作为对结构黑客的替换,这是有问题的合法性。
- @我知道基思汤普森。我只是不得不这么做。:)
- @Mafso:太糟糕了,标准把零大小数组变成了一种约束冲突。如果它说结构中的零大小数组插入了强制正确对齐所必需的填充,然后在不分配任何内容的情况下生成结果地址,这将提供比灵活数组成员更有用的语义,但在大多数实现中,该功能将不需要任何成本(只需更改最小数组y从1绑定到0)。然后,可以通过说数组在足够大的分配中的最大下标是(len-(size_t)1)来证明结构hack的合理性。
- @Supercat:我们有灵活的数组成员。他们工作。处理好它。
- @KeithThompson:它们只适用于具有分配持续时间的对象;没有标准方法来创建静态或自动持续时间结构,该结构与期望具有FAM结构的函数兼容。
指针算法要求添加或减去的两个指针是同一对象的一部分,因为否则它没有意义。本标准引用部分具体指两个不相关的对象,如int a[b];和int b[5]。指针算法要求知道指针指向的对象的类型(我确信您已经知道了这一点)。
即
1 2
| int a[5];
int *p = &a[1]+1; |
这里,p是通过知道&a[1]是指int对象来计算的,因此增加到4个字节(假设sizeof(int)是4个字节)。
对于结构示例,我认为它不可能被定义为使结构成员之间的指针算术合法。
举个例子,
1 2 3 4 5
| struct test {
int x[5];
char something;
short y[5];
}; |
号
c标准不允许在void指针中使用指针算术(使用gcc -Wall -pedantic test.c编译将捕获这一点)。我认为你使用的GCC假设void*与char*相似,并允许这样做。所以,
等于
1
| printf("%zu", (char*)q - (char*)p ); |
。
如果指针指向同一对象内并且是字符指针(char*或unsigned char*,则指针算法定义得很好。
使用正确的类型,它将是:
1 2 3 4 5
| struct test s = { ... };
int *p = s. x;
short *q = s. y;
printf("%td
", q - p ); |
现在,如何执行q-p?基于sizeof(int)或sizeof(short)?如何计算位于这两个数组中间的char something;的大小?
这就解释了不可能对不同类型的对象执行指针运算。
即使所有成员都是同一类型(因此没有如上所述的类型问题),最好使用标准宏offsetof(来自)来获得与成员之间指针算术效果类似的结构成员之间的差异:
。
所以我认为没有必要用C标准定义结构成员之间的指针算术。
- 回答得好,但是你忘了当指针都是char*类型并且指向同一个对象时是允许的。没有这一点,就不可能定义offsetof。
- 当然。但我不确定我在哪里反驳或暗示?
- 你并没有直接反驳它,但这是一个重要的"漏洞",值得一提,imho。
- 但是,我在逻辑上包括它的地方挣扎着,除了在你的评论之后作为一个不相干的事实。已编辑。谢谢。
- 指向不同对象的指针之间的指针算术和关系运算符(<<=>>=不一定有意义。语言可以使结果不明确而不是不明确的行为,并要求其行为一致(因此,&x < &y && &y < &z表示&x < &z等)。在许多系统中,它实际上是这样工作的。标准未定义这些操作,因为它们很难在某些体系结构上一致地实现,并且因为额外的实现工作不会为您购买任何特别有用的东西。
我相信这个问题的答案比它看起来要简单,操作人员问:
but why should that result be"undefined"?
号
好吧,让我们看一下未定义行为的定义在C99标准草案3.4.3中:
behavior, upon use of a nonportable or erroneous program construct or
of erroneous data, for which this International Standard imposes no
requirements
号
这仅仅是标准没有强制要求的行为,完全符合这种情况,结果将根据体系结构而变化,并且试图指定结果可能很困难,如果不是以可移植的方式不可能的话。这就留下了一个问题,为什么他们会选择未定义的行为,而不是说实现未定义的行为?
很可能是由于未定义的行为限制了创建无效指针的方式数量,这与我们获得了offsetof来删除不相关对象的指针减法的一个潜在需求是一致的。
虽然该标准并未真正定义"无效指针"一词,但我们在国际标准编程语言C的基本原理中得到了很好的描述,在6.3.2.3部分指针中指出(强调我的):
Implicit in the Standard is the notion of invalid pointers. In
discussing pointers, the Standard typically refers to"a pointer to an
object" or"a pointer to a function" or"a null pointer." A special
case in address arithmetic allows for a pointer to just past the end
of an array. Any other pointer is invalid.
号
C99的基本原理还补充说:
Regardless how an invalid pointer is created, any use of it yields
undefined behavior. Even assignment, comparison with a null pointer
constant, or comparison with itself, might on some systems result in
an exception.
号
这强烈地表明指向padding的指针是无效的指针,尽管很难证明padding不是对象,但object的定义是:
region of data storage in the execution environment, the contents of
which can represent values
号
以及注意事项:
When referenced, an object may be interpreted as having a particular
type; see 6.3.2.1.
号
我不知道我们如何解释结构元素之间填充的类型或值,因此它们不是对象,或者至少强烈地表示填充不应该被视为对象。
- 我不知道指向padding的指针如何可能是无效指针。填充不是对象,而是对象的一部分。毕竟,标准保证了填充的存在,只有填充的值是未指定的(6.2.6.1p1)。见基思汤普森的回答。
是的,允许您对结构字节执行指针算术:
N1570-6.3.2.3指针P7:
... When a pointer to an object is converted to a pointer to a character type,
the result points to the lowest addressed byte of the object. Successive increments of the
result, up to the size of the object, yield pointers to the remaining bytes of the object.
号
这意味着,对于程序员来说,无论结构在硬件中是如何实现的,它的字节都应被视为一个连续的区域。
但是,不使用void*指针,这是非标准编译器扩展。如标准段落所述,它仅适用于字符类型指针。
编辑:
正如Mafso在评论中指出的那样,只要减影结果类型ptrdiff_t对结果有足够的范围,以上才是正确的。由于size_t的范围可以大于ptrdiff_t,并且如果结构足够大,地址可能相距太远。
因此,最好对结构构件使用offsetof宏,并从中计算结果。
- +1.我还认为,我引用的规则中的"elements"一词用于区分仅char *指针和与数组元素对应的类型的正确对齐指针。
- 这个答案似乎意味着填充物可以被认为是一个我高度怀疑的目标。
- 这个答案没有抓住要点。1。如果p + n与q比较,这并不意味着q - p被定义。2。重要的是,这里的对象是什么(整个结构或只是成员)。我倾向于用一种说法来解释标准。
- @Shafikyaghmour这个标准中的条款似乎只包括这样可以得到有效指针的事实。是否可以取消对所述指针的引用,在本标准其他地方进行了说明。
- @Mafso你是说(char*)&s + offsetof(s, s.y) == q - p的结果不一定是真的吗?指针算术的规则相当严格,我看不出实现如何在不违反规则的情况下做到这一点。
- 允许ptrdiff_t比size_t小得多(标准没有说明它们之间的关系),所以差异可能只是未定义的(如果定义了q-p,结果就如预期的那样)。(我之前问过一个关于这个的问题。)对于小于2*15的结构,这总是被定义的,并且与这里的问题并不真正相关,我的观点是,"如果定义了p+n,那么p+n-n"的含义是不正确的(在您的帖子中是无声的假设)。
- @Mafso你是对的,关于可能的ptrdiff_t溢出,我已经编辑了答案以适应它。
- 填充指针的指针是否有效?你能证明这一说法是正当的吗?
- 正如我所说,ptrdiff_t问题并不是真正相关的,它只是"p+ndefined=>p+n-ndefined"这个错误含义的反例,你仍然认为这一点。
- @我引用的Shafikyaghmour段落说,您将获得指向对象up to the size of the object字节的指针。sizeof(anyObject)等于对象的整个大小,包括填充。所以在我看来,指针是有效的,从某种意义上说,它是指针算术的有效地址。在某种意义上,取消引用并不一定安全。
- @黑手党你想说什么?如果我们保持在我们类型的限制范围内,那么p+n-n就得到了很好的定义。
- @用户694733:当然它是定义明确的(如果在限制范围内并且p+n是有效的(并且实际使用!),但这是这里的实际点(在6.5.6 p9中定义)。它只为同一数组对象中的指针定义。这里的问题是:物体是什么?成员(转换为char *的指针,在这种情况下,取其差为ub)或包含结构(符合常见解释,但据我所知,标准中没有定义的内容)?
- @Mafso我的解释是,物体是包含结构。3.15将对象描述为"数据存储区域…""…其内容可以表示值"。结构符合此描述。另外,指向数组的原始指针也是通过t struct获得的(使用语法s.x)。如果我们取s的char*指针,6.5.6p7表示对象的ptr可以作为数组第一个成员的ptr。6.3.2.3P7保证,对于指针算术而言,该区域可视为连续的。换句话说,您可以将结构的内存区域视为字节数组。…
- @Mafso…所以似乎没有证据表明6.3.2.3p8和p9不能应用于这种情况。当然,如果x和y是单独的数组,这当然不起作用,但是由于它们包含在同一结构中,前面提到的章节提供了安全保证。标准并没有明确地提到这个案例是UB(不承认这是一个夸伦特),所以没有真正的证据来证明这一点。不好意思看到这面墙的文字:)
- 对象的概念对于别名规则也很有趣,例如关于restrict的这个问题。而且,正如我上面提到的,边界检查实现是下一个问题(例如,这里和这里)。进一步的讨论在聊天中可能更好。
- @Mafso我只浏览了你的链接,但是从答案和他们的评论来看,边界检查似乎在取消引用阶段起作用。无论如何,我会读更多关于这个的文章,你是对的;进一步的讨论应该在聊天中进行。我们现在就把这个留着吧。幸运的是,在可预见的将来,我还没有,也没有,需要访问除通常安全方式以外的结构,所以我并不急于解决这个问题。:)
我应该指出以下几点:
根据C99标准第6.7.2.1节:
在结构对象中,非位字段成员和位字段所在的单位。居住地址的声明顺序会增加。指向结构对象,适当转换,指向其初始成员(或如果该成员是位字段,然后是它所在的单元,反之亦然。可能没有名字在结构对象中填充,但不是在其开始处填充。
成员之间的指针减法结果没有定义太多,因此不可靠(即,当应用相同的算术时,同一结构类型的不同实例之间不保证相同)。