【问题标题】:What C compilers have pointer subtraction underflows?哪些 C 编译器有指针减法下溢?
【发布时间】:2026-02-12 17:40:02
【问题描述】:

所以,正如我从 Michael Burr 的 cmets 到 this answer 了解到的那样,C 标准不支持从经过数组中第一个元素的指针(我想这包括任何分配的内存)的整数减法。

来自the combined C99 + TC1 + TC2 (pdf) 的第 6.5.6 节:

如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

我喜欢指针算术,但这从来都不是我之前担心过的事情。我一直认为:

 int a[1];
 int * b = a - 3;
 int * c = b + 3;

那个c == a

因此,虽然我相信我以前做过这种事情并且没有被咬,但这一定是由于我合作过的各种编译器的好意——他们已经超越了标准要求指针算术按我想象的方式工作。

所以我的问题是,这有多普遍?是否有常用的编译器对我不那么友好?超出数组边界的正确指针算法是事实上的标准吗?

【问题讨论】:

  • 这不是编译器的问题,而是 CPU 架构的问题。那里有一些晦涩的内存模型,您通常不能假设所有系统都具有纯线性内存。只是不要将指针视为内存地址。他们不是。它们有一组单独的限制。

标签: c arrays pointers compiler-construction pointer-arithmetic


【解决方案1】:

MSDOS FAR 指针有这样的问题,通常在实模式中通过“聪明”地使用段寄存器与偏移寄存器的重叠来解决这些问题。那里的效果是 16 位段是左移 4 位,并添加到 16 位偏移量,这给出了一个可以寻址 1MB 的 20 位物理地址,这已经足够了,因为每个人都知道没有人会需要高达 640KB 的 RAM。 ;-)

在保护模式下,段寄存器实际上是内存描述符表的索引。典型的 DOS 扩展运行时通常会安排一些事情,以便可以像在实模式下一样对待许多段,这使得从实模式移植代码变得容易。但它有一些缺陷。首先,分配之前的段不是分配的一部分,因此它的描述符甚至可能无效。

在处于保护模式的 80286 上,仅使用会导致加载无效描述符的值加载段寄存器会导致异常,无论描述符是否实际用于引用内存。

在分配后的一个字节可能会发生类似的问题。指针上的最后一个 ++ 可能已经转移到段寄存器,导致它加载新的描述符。在这种情况下,可以合理地期望内存分配器可以在分配范围的末尾安排 一个 安全描述符,但期望它安排更多的安全描述符是不合理的。

【讨论】:

  • 谢谢你...你会注意到我没有引用这句话或责怪比尔。可能真的没有人认真说过,但这种态度对于不得不支付一大块记忆的人来说并不陌生......
【解决方案2】:

这不是标准的“实现定义”,这是标准的“未定义”。这意味着你不能指望一个支持它的编译器,你不能说,“好吧,这个代码在编译器 X 上是安全的”。通过调用未定义的行为,您的程序是未定义的。

实际的答案不是“我如何(在哪里、何时、在什么编译器上)摆脱这个问题”;实际的答案是“不要这样做”。

【讨论】:

  • 我认为 OP 想知道 为什么 这是真的,就像任何事情一样。如果您从未体验过为 Windows 3.0 开发应用程序的“乐趣”,那么您可能不明白我们今天拥有它是多么容易,这是可以理解的 ;-)
  • 事实上我为 Windows 3.0 编写了程序。那时,文件管理器只允许一种文件类型与一个程序相关联。我编写了一个处理程序,允许用户为每种文件类型添加多个程序;然后用户将文件与该程序相关联,右键单击允许用户从他的自定义程序列表中选择该文件类型。
  • 我只记得必须回到 DOS 进行编译(并运行一个像样的编辑器),因为 MSC 编译器不能在 DOS 机器中可靠地运行。此外,在出现任何错误之后,很有可能退出 Windows 需要三指敬礼,然后 DOS 提示符是之后的第一站……真正的乐趣是在纸上和文本编辑器中设计对话框布局,并拥有坐下来通过编译看看他们是什么样子......
  • 顺便提一下 tpdi,我的评论并不是要特别对你不屑一顾。写段寄存器只是带来了一些潜在的倾向,感觉就像一个脾气暴躁的老家伙今晚小跑着“在雪地里双向上山走了 5 英里”的故事。 ;-)
  • 根本没有采取那种方式,如果我的回答看起来太“哦,是的,我做到了!”,我很抱歉;事实上,我真的忘记了我曾经写过那件事,直到你的评论促使我回忆起它。即使是现在,我也记不起/何时/我写了它。大概在95年之前?我知道我早在学习 C 之前就学会了 8086 asm 9 来编写愚蠢的 TSR)。所以我的评论更多是出于自我惊讶而不是其他任何事情。是的,我记得担心分段内存和偏移量。那时的事情/变得/更复杂了。
【解决方案3】:

另一个原因是有可选的保守垃圾收集器(如 boehm-weiser GC)假定指针始终在分配的范围内,如果不在分配范围内,则允许它们随时释放内存。

有一个流行的商业质量和使用的库确实打破了这一假设,它是来自 HP 的 Judy Trees 库,它使用指针算法来实现非常复杂的哈希结构。

【讨论】:

    【解决方案4】:

    ZETA-C 用于 TI Explorer;指针被实现为数组和索引或置换数组,IIRC,所以你的例子可能不起作用。从zcprim.lisp 中的zcprim>pointer-subtract 开始,找出行为会是什么。不知道这是否符合标准,但我觉得它是正确的。

    【讨论】: