`c = (int *) ((char *) c + 1)` 的作用是什么?

3

我在操作系统课程中遇到了这个问题。 这里 是来自6.828(操作系统)在线课程的代码。它旨在让学习者练习C编程语言中的指针。

#include <stdio.h>
#include <stdlib.h>

void
f(void)
{
    int a[4];
    int *b = malloc(16);
    int *c;
    int i;

    printf("1: a = %p, b = %p, c = %p\n", a, b, c);

    c = a;
    for (i = 0; i < 4; i++)
    a[i] = 100 + i;
    c[0] = 200;
    printf("2: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
       a[0], a[1], a[2], a[3]);

    c[1] = 300;
    *(c + 2) = 301;
    3[c] = 302;
    printf("3: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
       a[0], a[1], a[2], a[3]);

    c = c + 1;
    *c = 400;
    printf("4: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
       a[0], a[1], a[2], a[3]);

    c = (int *) ((char *) c + 1);
    *c = 500;
    printf("5: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n",
       a[0], a[1], a[2], a[3]);

    b = (int *) a + 1;
    c = (int *) ((char *) a + 1);
    printf("6: a = %p, b = %p, c = %p\n", a, b, c);
}

int
main(int ac, char **av)
{
    f();
    return 0;
}

我将它复制到一个文件中,使用gcc进行编译,然后得到了这个输出:

$ ./pointer 
1: a = 0x7ffd3cd02c90, b = 0x55b745ec72a0, c = 0x7ffd3cd03079
2: a[0] = 200, a[1] = 101, a[2] = 102, a[3] = 103
3: a[0] = 200, a[1] = 300, a[2] = 301, a[3] = 302
4: a[0] = 200, a[1] = 400, a[2] = 301, a[3] = 302
5: a[0] = 200, a[1] = 128144, a[2] = 256, a[3] = 302
6: a = 0x7ffd3cd02c90, b = 0x7ffd3cd02c94, c = 0x7ffd3cd02c91

我可以轻松理解1、2、3、4的输出。但是对于5的输出,让我很难理解。特别是为什么a[1]=128144和a[2]=256?
看起来这个输出是下面代码的结果:

c = (int *) ((char *) c + 1);
*c = 500;

我不理解代码 c = (int *) ((char *) c + 1) 的功能。
按照定义,c 是一个指针 int *c。在第5行输出之前,c 通过 c = ac = c + 1 指向数组 a 的第二个地址。现在,(char *) c((char *) c + 1) 的含义是什么,然后再看 (int *) ((char *) c + 1) 的含义是什么?

3
MIT正在教授那些糟糕的代码吗?因为c = (int *) ((char *) c + 1)只是创建了一个指针,这样做存在风险未定义行为,而*c = 500;也是未定义行为。我猜那些不能做的人就去教别人做。 - Andrew Henle
实际上,((char *) c + 1) 指向 a[1] 的第二个字节。*c = 500 覆盖了除了 a[1] 的第一个字节和 a[2] 的第一个字节以外的所有内容。在小端架构中,a[1] 变成了 (400 & 0xFF) | (500 << 8)=128144,而 a[2] 变成了 300 & ~0xFF。但正如前面的评论所说,这是未定义行为。 - dimich
3个回答

2
尽管这在标准中是未定义行为,但在“古老的C语言”中它有明确的含义,并且在您使用的机器/编译器上它显然可以工作。首先,它将 c 转换为(char *),这意味着指针算术运算将以 sizeof(char)(即一个字节)为单位工作,而不是 sizeof(int)。然后添加一个字节。然后将结果转换回(int *)。结果是一个int指针,现在引用比原来高一个字节的地址。由于 c 之前指向 a [1] ,因此之后 * c = 500 将写入 a [1] 的最后三个字节和 a [2] 的第一个字节。在许多机器上(但不是x86),这是一件完全非法的事情。这样的不对齐访问只会使程序崩溃。C标准进一步表示,该代码允许执行任何操作:当编译器看到它时,它可以生成导致崩溃、无操作、写入完全不相关的内存位或导致小侏儒从您的显示器侧面弹出并用木槌击打您的代码。然而,有时在UB的情况下最简单的事情也是明显的事情,这就是其中之一。您的课程材料试图向您展示关于如何在内存中存储数字以及相同的字节可以根据您告诉CPU的方式以不同的方式解释的内容。您应该以这种精神来接受它,而不是作为编写良好C代码的指南。

1
这在任何版本的C语言中都没有定义,无论是古老的还是其他。它只能在OP的机器上运行,因为某个16位微处理器供应商决定允许不对齐访问以保持与8位硬件的兼容性。当时的“古老C”运行在PDP-11上,它肯定不是这样工作的。 - n. m.
@n.1.8e9-我的份在哪里呢m。许多16位处理器没有对齐要求,不仅限于古老的处理器。至于C99有效类型规则如何在您只访问对象的一半时有意义,那就是另一回事了。虽然6.3.2.3中的指针转换规则将所有未对齐访问标记为UB。 - Lundin
1
@Lundin 古老的编程语言确实有对齐要求,这也是 C 语言拥有它们的原因。 - n. m.
我的观点是,PC世界对齐访问=总是UB是一个人为的要求。这使得编译器和程序设计在没有硬件对齐要求的CPU上变得显著而不必要地复杂。 - Lundin
@Lundin 我认为这不是标准或编译器的工作方式,也不应该是这样,但这是离题了。 - n. m.
显示剩余2条评论

1

在第一次输出时,c指向一个随机地址。

c = a; 后,c 指向 a,所以当你改变 c[0]、c[1]、*(c + 2)、3[c] 的值时,a 的值也会相应地改变。

在下一行代码中:

    c = c + 1;

c现在指向a[1],地址为0x7ffd3cd02c94

现在去看你询问的那行:c = (int *) ((char *) c + 1);它会执行以下操作:

  • c转换为指针类型char,仍然指向相同的地址0x7ffd3cd02c94
  • 将指针增加1,因此现在的地址将是0x7ffd3cd02c95
  • 再次将新地址赋值给c(int *)。

在执行该命令之前,c将指向地址:0x7ffd3cd02c94-0x7ffd3cd02c97。但是在此之后,地址将为:0x7ffd3cd02c95-0x7ffd3cd02c98。这就是[5]上的值发生变化的原因 [![enter image description here][1]][1]

现在清楚了为什么你观察到的值发生了变化。

注意:这对于小端系统是正确的。对于大端系统,结果会有些不同。对于一些不允许非对齐访问的嵌入式平台,在该行应该会出现异常。 [1]: https://istack.dev59.com/eU0Tb.webp


0

这是未定义行为的结果。您因为解引用空指针(对于数组a)和数组大小为零(对于数组b)而调用了未定义行为 - 对于这种情况,这相当于c = a; b = 0; c =(int *)((char *)c + 1)。这应该会触发警告,这就是为什么我在上面的示例中还添加了-Wall -pedantic -std=c99。

回答您关于(char *)c和((char *)c + 1)的问题。

(char *)c:由于c是一个指针,c->type是int *(指向int的指针)。这使得c->type具有char *类型。您将数组c中第二个元素的地址赋给a。因此,c->type现在是char *(数组c中第二个元素的地址)。因此,c [0](索引0)是数组c中的第一个元素。

((char *) c + 1) - c + 1 = &c[1]. c[0] + 1 = c[1] (first element of the array c+1).

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