指针比较在 C 中如何工作?可以比较不指向同一个数组的指针吗?

Shi*_*sui 35 c pointers heap-memory memory-layout undefined-behavior

在 K&R(C 编程语言第 2 版)第 5 章中,我阅读了以下内容:

首先,在某些情况下可以比较指针。如果pq指向同一个数组的成员,则关系一样==!=<>=,等正常工作。

这似乎意味着只能比较指向同一数组的指针。

但是,当我尝试此代码时

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);
Run Code Online (Sandbox Code Playgroud)

1 被打印到屏幕上。

首先,我认为我会得到 undefined 或某种类型或错误,因为ptpx没有指向同一个数组(至少在我的理解中)。

也是pt > px因为两个指针都指向存储在栈上的变量,栈向下增长,所以内存地址t大于x?为什么pt > px是真的?

当 malloc 被引入时,我变得更加困惑。 同样在第 8.7 章的 K&R 中写到以下内容:

然而,仍然存在一种假设,即sbrk可以有意义地比较指向由 返回的不同块的指针。仅允许在数组内进行指针比较的标准并不能保证这一点。因此,这个版本的malloc仅在一般指针比较有意义的机器之间是可移植的。

将指向堆上 malloced 空间的指针与指向堆栈变量的指针进行比较,我没有任何问题。

例如,下面的代码工作正常,1并被打印:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);
Run Code Online (Sandbox Code Playgroud)

根据我对编译器的实验,我认为任何指针都可以与任何其他指针进行比较,而不管它们分别指向何处。此外,我认为两个指针之间的指针算术很好,无论它们分别指向哪里,因为算术只是使用指针存储的内存地址。

尽管如此,我对我在 K&R 中阅读的内容感到困惑。

我问的原因是因为我的教授。实际上使它成为一个考试问题。他给出了以下代码:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}
Run Code Online (Sandbox Code Playgroud)

这些评估是什么:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

答案是010

(我的教授确实在考试中包含免责声明,即这些问题是针对 Ubuntu Linux 16.04、64 位版本编程环境的)

(编者注:如果 SO 允许更多标签,那么最后一部分将保证和可能的。如果问题/类的重点是特定的低级操作系统实现细节,而不是可移植的 C。)

dbu*_*ush 34

根据C11 标准,关系运算符<<=>>=只能用于指向同一数组或结构对象的元素的指针。这在第 6.5.8p5 节中有详细说明:

比较两个指针时,结果取决于所指向对象在地址空间中的相对位置。如果指向对象类型的两个指针都指向同一个对象,或者都指向同一个数组对象的最后一个元素之后的一个,则它们比较相等。如果指向的对象是同一个聚合对象的成员,则指向后面声明的结构成员的指针比较大于指向结构中前面声明的成员的指针,指向下标值较大的数组元素的指针比较大于指向同一数组元素的指针具有较低的下标值。指向同一联合对象成员的所有指针比较相等。

请注意,任何不满足此要求的比较都会调用未定义的行为,这意味着(除其他外)您不能依赖结果是可重复的。

在您的特定情况下,对于两个局部变量的地址之间以及本地地址和动态地址之间的比较,该操作似乎“有效”,但是通过对您的代码进行看似无关的更改,结果可能会发生变化甚至用不同的优化设置编译相同的代码。对于未定义的行为,仅仅因为代码可能崩溃或生成错误并不意味着它

例如,在 8086 实模式下运行的 x86 处理器具有使用 16 位段和 16 位偏移量来构建 20 位地址的分段内存模型。因此,在这种情况下,地址不会完全转换为整数。

然而,相等运算符==!=没有这个限制。它们可以在任何两个指向兼容类型的指针或 NULL 指针之间使用。因此==!=在您的两个示例中使用或将生成有效的 C 代码。

但是,即使使用==和 ,!=您也可以获得一些意想不到但仍然定义明确的结果。请参阅无关指针的相等比较能否评估为真?有关这方面的更多详细信息。

关于你的教授给出的考试问题,它做出了一些有缺陷的假设:

  • 存在一个平面内存模型,其中地址和整数值之间存在 1 对 1 的对应关系。
  • 转换后的指针值适合整数类型。
  • 在执行比较时,实现只是将指针视为整数,而没有利用未定义行为赋予的自由。
  • 使用堆栈并且局部变量存储在那里。
  • 堆用于从中提取分配的内存。
  • 堆栈(以及局部变量)出现在比堆(因此分配的对象)更高的地址处。
  • 该字符串常量出现在比堆低的地址处。

如果您要在不满足这些假设的体系结构和/或编译器上运行此代码,那么您可能会得到非常不同的结果。

此外,这两个示例在调用 时也表现出未定义的行为strcpy,因为右操作数(在某些情况下)指向单个字符而不是空终止字符串,导致函数读取超出给定变量的边界。

  • @Shisui:TL:DR:它不是可移植的 C,尽管它恰好在 x86-64 Linux 上运行良好。然而,对比较结果做出假设是疯狂的。如果您不在主线程中,您的线程堆栈将使用“malloc”从操作系统获取更多内存的相同机制进行动态分配,因此没有理由假设您的本地变量(线程堆栈)位于上面`malloc` 动态分配存储。 (4认同)
  • @Shisui 即使如此,你仍然不应该依赖结果。编译器在优化方面可能会“非常”激进,并且会利用未定义的行为作为这样做的机会。使用不同的编译器和/或不同的优化设置可能会生成不同的输出。 (3认同)
  • @Shisui:它通常会在具有平面内存模型的机器上工作,例如 x86-64。此类系统的某些编译器甚至可能在其文档中定义行为。但如果不是,那么由于编译时可见的 UB,可能会发生“疯狂”行为。(实际上,我认为没有人想要这样,所以这不是主流编译器寻找和“尝试破坏”的东西。) (2认同)
  • @PeterCordes:需要的是将行为的各个方面识别为“可选定义”,以便实现可以在闲暇时定义它们或不定义它们,但如果它们不这样做,则必须以可测试的方式(例如预定义的宏)进行指示。此外,与其将优化效果可观察到的任何情况描述为“未定义行为”,不如说优化器可以将行为的某些方面视为“不可观察”,如果它们表明它们这样做。例如,给定 `int x,y;`,一个实现...... (2认同)

ζ--*_*ζ-- 12

比较指向相同类型的两个不同数组的指针的主要问题是数组本身不需要放置在特定的相对位置——一个可能在另一个之前和之后结束。

首先,我认为我会得到 undefined 或某种类型或错误,因为 pt 和 px 没有指向同一个数组(至少在我的理解中)。

不,结果取决于实施和其他不可预测的因素。

也是pt>px,因为两个指针都指向存储在栈上的变量,栈向下增长,所以t的内存地址大于x的内存地址?这就是为什么 pt>px 是真的?

不一定有 stack。当它存在时,它不需要向下生长。它可以长大。它可能以某种奇怪的方式不连续。

此外,我认为两个指针之间的指针算术很好,无论它们分别指向哪里,因为算术只是使用指针存储的内存地址。

让我们看看第 85 页上的C 规范§6.5.8,它讨论了关系运算符(即您正在使用的比较运算符)。请注意,这不适用于直接!===比较。

比较两个指针时,结果取决于所指向对象在地址空间中的相对位置。... 如果指向的对象是同一聚合对象的成员, ... 指向具有较大下标值的数组元素的指针比较大于指向具有较低下标值的同一数组元素的指针。

在所有其他情况下,行为是未定义的。

最后一句话很重要。虽然我减少了一些不相关的情况以节省空间,但有一种情况对我们很重要:两个数组,不是同一结构/聚合对象1 的一部分,我们正在比较指向这两个数组的指针。这是未定义的行为

虽然您的编译器只是插入了某种 CMP(比较)机器指令来对指针进行数字比较,但您在这里很幸运,但 UB 是一个非常危险的野兽。实际上任何事情都可能发生——您的编译器可以优化整个函数,包括可见的副作用。它可能会产生鼻妖。

1 可以比较指向属于同一结构的两个不同数组的指针,因为这属于两个数组属于同一聚合对象(结构)的子句。


mev*_*ets 6

然后问什么

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
Run Code Online (Sandbox Code Playgroud)

评价为。答案是 0、1 和 0。

这些问题简化为:

  1. 是堆在堆栈之上还是之下。
  2. 是程序字符串文字部分上方还是下方的堆。
  3. 与[1]相同。

而这三个问题的答案都是“实现定义”。你教授的问题是假的;他们以传统的 unix 布局为基础:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel
Run Code Online (Sandbox Code Playgroud)

但是一些现代的 unice(和替代系统)不符合这些传统。除非他们以“截至 1992 年”开头;确保在 eval 上给出 -1。

  • 未定义实现,*未定义!* 这样想,前者可能因实现而异,但实现应该记录如何决定行为。后者意味着行为可能以任何方式变化,并且实现不必告诉您蹲下:-) (3认同)

归档时间:

查看次数:

5731 次

最近记录:

4 年,5 月 前