C指针算术运算用于数组

3
我正在阅读K&R中有关数组算术的部分,发现了一些奇怪的东西。我为了上下文贴出了整个段落,但我主要关注粗体部分:
如果p和q指向同一个数组的成员,则像==、!=、<、>=等关系运算符可以正常工作。例如,如果p指向数组的早期成员,而q指向数组的后期成员,则p < q为真。任何指针都可以与零有意义地进行相等或不相等的比较。但是,对于不指向同一数组成员的指针进行算术或比较的行为是未定义的。(有一个例外:可以在指针算术中使用数组结尾后的第一个元素的地址。)
我在这里得到了一些答案(C pointer arithmetic for arrays),但我对以下内容表示怀疑:
由于以下代码似乎可以在没有抛出任何异常或错误的情况下使用解引用和比较,因此我对此存有疑虑:
#include <stdio.h>
    
int main() {
    int a[5] = { 1, 2, 3, 4, 5 };
    int b[5] = { 1, 2, 3, 4, 5 };
    int *p = &a[7];
    int *q = &b[3];
    printf("%d\n", p);
    printf("%d\n", q);
    printf("%d\n", q > p); // relational from different arrays
    printf("%d", *p);      // dereferencing also seems to work
}

有人可以帮忙吗?

代码应该抛出一个错误。


8
“但是对于不指向同一数组成员的指针进行算术或比较的行为是未定义的。” 未定义意味着未定义。在一个编译器中有效的代码在另一个编译器中可能无法正常工作...一个样本大小不足以作出评估的依据。 - Fe2O3
代码应该抛出一个错误。这是错误的假设。大多数情况下,检查未定义行为的工作应该由程序员完成,而不是编译器或操作系统。什么是未定义行为以及它是如何工作的? - Lundin
这段代码不能保证一定能够正常工作。虽然有可能会,但并不是必然的。 - Promitheas Nikou
代码应该抛出一个错误。--> “应该抛出一个错误”意味着存在某些代码应该出错的规范。然而,行为未定义。没有任何“应该”与UB做任何事情。 - chux - Reinstate Monica
2个回答

4

你的代码在多个地方存在未定义行为,但是C语言没有定义在出现未定义行为时会发生什么:任何事情都可能发生。没有异常或错误被抛出,程序可能会崩溃、产生意外结果或似乎工作并产生预期结果...一切皆有可能,无法预料。

这些地方存在未定义行为:

  • int *p = &a[7]; 你计算了一个不存在于数组a末尾之后的元素的地址,而不是紧接在末尾之后的元素。
  • printf("%d\n", p); 你传递了一个指向int的指针,而printf期望的是一个int。你应该写成printf("%p\n", (void *)p);
  • printf("%d\n", q); 同上
  • printf("%d\n", q > p); 使用未初始化为有效表达式的p的值
  • printf("%d", *p); 解引用无效指针p
请注意,C标准比K&R书更精确地比较指针的相等性:
6.5.8关系运算符
5 对于这些运算符,指向不是数组元素的对象的指针与指向长度为1且元素类型为该对象类型的数组的第一个元素的指针行为相同。
6 当比较两个指针时,结果取决于所指对象在地址空间中的相对位置。如果两个指向对象类型的指针都指向同一对象,或者都指向同一数组对象的最后一个元素之后的位置,则它们相等。如果所指对象是同一聚合对象的成员,则声明在结构中较晚的结构成员的指针比先前声明的成员的指针大,并且具有较大下标值的数组元素的指针比具有较小下标值的相同数组的元素的指针大。所有指向同一联合对象的成员的指针都相等。如果表达式P指向数组对象的元素,并且表达式Q指向同一数组对象的最后一个元素,则指针表达式Q+1比P大。在所有其他情况下,行为未定义。
6.5.9 相等运算符
6 ... 如果一个操作数是指针,而另一个是空指针常量,则将空指针常量转换为指针的类型。如果一个操作数是指向对象类型的指针,而另一个是指向void类型的限定或非限定版本的指针,则将前者转换为后者的类型。
7 两个指针相等的条件是:它们都是空指针,它们都是指向同一个对象(包括指向对象及其子对象的指针),或者函数;它们都是指向同一个数组对象中最后一个元素之后的位置;或者其中一个是指向一个数组对象的结束位置,而另一个是指向地址空间中紧随第一个数组对象之后的不同数组对象的起始位置。
这是一个具有确定行为的修改版本。
#include <stdio.h>
    
int main() {
    int a[5] = { 1, 2, 3, 4, 5 };
    int b[5] = { 1, 2, 3, 4, 5 };
    int *p = &a[3];
    int *q = &a[5];
    int *r = &b[0];

    // printing pointer values
    printf("p:      %p\n", (void *)p);
    printf("q:      %p\n", (void *)q);
    printf("r:      %p\n", (void *)r);

    // p, q and r can be compared to 0
    printf("p == 0: %d\n", p == 0);  // outputs 0
    printf("q != 0: %d\n", q != 0);  // outputs 1
    printf("!r:     %d\n", !r);      // outputs 0

    // p and q can be compared for equality and order
    printf("p < q:  %d\n", p < q);  // outputs 1
    printf("p != q: %d\n", p != q); // outputs 1

    // p and r can only be compared for equality
    printf("p != r: %d\n", p != r); // outputs 1

    // q and r can only be compared for equality but the result is unspecified:
    //   it is compiler dependent depends on the memory layout
    printf("q != r: %d\n", q != r); // may output 1 or 0
    return 0;
}

  1. 为什么这被认为是未定义行为,因为最终我们比较不同的内存地址,无论指针是否属于同一数组?// 这取决于编译器,取决于内存布局 printf("q != r: %d\n", q != r); // 可能输出1或0
  2. 为什么这里取决于编译器?请说明有关内存布局的内容。
  3. 上述规则的基础是什么?您可以分享C标准书的链接吗?谢谢!
- Azim Qadri

1
你正在处理未定义行为。 这意味着你不能依靠它 - 它可能在一天工作得很好,而在另一天根本不起作用。 但无论如何 - 你都不能信任它。
这就是为什么未定义行为如此危险。有时它可能一直运行良好,直到某一天毫无预警地失败。

我认为 UB 源于编译器不需要执行一项或另一项操作。因此,它不是“一天可以工作,但另一天不行”,而是一个编译器会执行某些操作,而另一个编译器将生成其他代码,产生不同的结果。因为标准并没有要求编译器执行一项或另一项操作。 - mmixLinus
@mmixLinus 尽管许多观察到的未定义行为可能依赖于编译器,但对于给定的编译,未定义行为仍然可能“一天工作,但另一天不工作”。 - chux - Reinstate Monica
这是对C标准所谓的未定义行为的错误描述。例如,调用操作系统例程被C标准定义为未定义行为,但我们期望该行为由操作系统文档定义,您可以依赖它,并且它不会在一天工作并在下一天失败。 C标准对未定义行为的定义是中立的 - 它既不好也不坏,既不“非常危险”,也不一定有益;它只是标准没有指定的东西。 - Eric Postpischil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接