【问题标题】:Is the behavior of subtracting two NULL pointers defined?是否定义了减去两个 NULL 指针的行为?
【发布时间】:2011-12-29 00:21:05
【问题描述】:

如果两个非 void 指针变量都是 NULL 值,是否定义了它们的差异(根据 C99 和/或 C++98)?

例如,假设我有一个如下所示的缓冲区结构:

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

说,ex.buf 指向一个数组或一些 malloc 的内存。如果我的代码始终确保 pwritepread 指向该数组内或超过它,那么我相当有信心 ex.pwrite - ex.pread 将始终被定义。但是,如果 pwritepread 都为 NULL 会怎样。我可以期望减去两者被定义为(ptrdiff_t)0 还是严格兼容的代码需要测试指针是否为 NULL?请注意,我感兴趣的唯一情况是 both 指针为 NULL(表示缓冲区未初始化的情况)。鉴于满足上述假设,原因与完全兼容的“可用”功能有关:

size_t buf_avail(const struct s_buf *b)
{     
    return b->pwrite - b->pread;
}

【问题讨论】:

  • 您是否尝试过多次执行该操作?
  • 什么意思?我知道这个操作的结果在 95%(假设 5% 是 AS/400)的实现中为 0,不会发生任何不好的事情。我对实现细节不感兴趣。我的问题与一些特定的标准定义有关。
  • Hunter McMillen:这是不好的方法——“我将指针存储在 int 中,但什么也没发生。我检查了不同的计算机和编译器,但什么也没发生。然后是 64 位计算机”。如果某些东西现在可以工作,但依赖于未定义的行为,那么它将来可能无法工作。
  • 我赞扬您确保您的代码保证按照相关标准工作,而不是仅仅注意到它恰好在您测试的平台上工作。
  • @TobySpeight:8086 编译器对near 限定的空指针与far 限定的空指针有不同的表示,但它会为空far 指针使用多种表示吗?如果空 near 指针转换为 far 指针,则该指针又与空 far 指针进行比较,例如DS 等于 0x1234,会发生什么: (1) 0x0000 被转换为 0x0000:0x0000; (2) 0x0000 被转换为 0x1234:0x0000,但比较运算符检查双段为零的情况,或者 (3) 0x0000 被转换为 0x1234:0x0000,比较不等于 0x0000:0x0000。

标签: c++ c c99 c89


【解决方案1】:

C 标准对这种情况下的行为没有强加任何要求,但许多实现确实在许多情况下指定了指针算术的行为,超出了标准要求的最低限度,包括这个。

在任何符合 C 的实现以及几乎所有(如果不是全部)类 C 方言的实现上,以下保证将适用于任何指针 p,这样 *p*(p-1) 就可以识别某个对象:

  • 对于任何等于零的整数值z,指针值(p+z)(p-z) 将在各个方面都等效于p,除非它们只有在p 和@987654329 @ 是不变的。
  • 对于任何与p 等效的q,表达式p-qq-p 都将产生零。

为所有指针值(包括 null)提供此类保证可能会消除对用户代码中某些 null 检查的需要。此外,在大多数平台上,为所有指针值生成支持此类保证而不考虑它们是否为空的代码将比专门处理空值更简单和更便宜。但是,某些平台可能会捕获使用空指针执行指针运算的尝试,即使在加零或减零时也是如此。在这样的平台上,必须添加到指针操作以维护保证的编译器生成的空值检查的数量在许多情况下将大大超过用户生成的空值检查的数量,因此可以省略。

如果有一个实现,维护保证的成本很高,但很少有程序会从中受益,那么允许它捕获“null+zero”计算是有意义的,并要求这种实现的用户代码包括手动空值检查,这些保证可能使不必要的。预计此类津贴不会影响其他 99.44% 的实施,在这些实施中,维持担保的价值将超过成本。这样的实现应该坚持这样的保证,但他们的作者不应该需要标准的作者告诉他们。

C++ 的作者已经决定,符合标准的实现必须不惜一切代价维护上述保​​证,即使在它们可能会大大降低指针算术性能的平台上也是如此。他们判断,即使在维护成本高昂的平台上,保证的价值也会超过成本。这种态度可能受到了将 C++ 视为比 C 更高级别语言的愿望的影响。AC 程序员可能会知道特定目标平台何时会以不寻常的方式处理像 (null+zero) 这样的情况,但 C++ 程序员没想到会关心这些事情。因此,保证一致的行为模型被认为是值得的。

当然,如今关于“定义”什么的问题很少与平台可以支持的行为有任何关系。相反,现在流行的编译器——以“优化”的名义——要求程序员手动编写代码来处理平台以前可以正确处理的极端情况。例如,如果应该输出从地址p 开始的n 字符的代码写成:

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

较旧的编译器会生成可靠地不输出任何内容的代码, 没有副作用,如果 p==NULL 和 n==0,不需要特殊情况 n==0。在 然而,较新的编译器必须添加额外的代码:

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

优化器可能会或可能无法摆脱。未能包含额外的代码可能会导致一些编译器认为由于 p“不可能为空”,任何后续的空指针检查都可能被省略,从而导致代码在与实际“问题”无关的位置中断。

【讨论】:

    【解决方案2】:

    在 C99 中,这是技术上未定义的行为。 C99 §6.5.6 说:

    7) 对于这些运算符,指向不是元素的对象的指针 array 的行为与指向长度为 1 的数组的第一个元素的指针相同 对象的类型作为其元素类型。

    [...]

    9) 当两个指针相减时,都指向同一个数组对象的元素, 或者数组对象的最后一个元素;结果是 两个数组元素的下标。 [...]

    §6.3.2.3/3 说:

    值为 0 的整数常量表达式,或这种类型的表达式 void *,称为空指针常量。55) 如果将空指针常量转换为指针类型,则保证生成的指针称为空指针比较不等于指向任何对象或函数的指针。

    所以由于空指针不等于任何对象,它违反了 6.5.6/9 的前提条件,所以它是未定义的行为。但实际上,我敢打赌,几乎每个编译器都会返回 0 的结果而不会产生任何不良副作用。

    在 C89 中,它也是未定义的行为,尽管标准的措辞略有不同。

    另一方面,C++03 确实在这种情况下定义了行为。该标准对减去两个空指针有一个特殊的例外。 C++03 §5.7/7 说:

    如果将值 0 添加到指针值或从指针值中减去值,则结果比较等于原始指针值。如果两个指针指向同一个对象,或者都指向同一个数组末尾的后一个,或者都为空,两个指针相减,结果比较等于转换为类型ptrdiff_t的值0。

    C++11(以及 C++14 的最新草案,n3690)与 C++03 的措辞相同,只是在 std::ptrdiff_t 的位置上进行了细微的更改 ptrdiff_t

    【讨论】:

    • 由于完整性,这是目前最好的答案。
    • 这似乎是标准中的一个疏忽,应该用“9”来纠正)当两个指针相减时,如果它们相等,则结果为零。否则,两者都应指向相同的元素数组对象..."
    • @R..,为了消除歧义,应该说“比较相等”不是吗?因为两个空指针可能不包含相同的值,所以它们“不”相等。
    • 看起来latest draft of the upcoming C1X standard也有相同的语言。我希望这实际上是一个疏忽,并且语言委员会会修复它。
    • 两个空指针是相同的,因为比较相等。当然,它们可能没有相同的表示形式
    【解决方案3】:

    编辑:这个答案只对C有效,我回答的时候没看到C++标签。

    不,指针运算只允许用于指向同一对象内的指针。由于根据 C 标准空指针的定义不指向任何对象,这是未定义的行为。

    (虽然,我猜任何合理的编译器都只会返回0,但谁知道呢。)

    【讨论】:

    • 这是不正确的。参见 5.7 [expr.add] / 7:“如果两个指针指向同一个对象,或者都指向同一个数组末尾的后一位置,或者 都为空,然后将两个指针相减,则结果比较等于值 0 转换为类型 std::ptrdiff_t。"
    • 这在 C++ 的特定上下文中是不正确的。标记的其他语言呢?
    • 哇,没想到这个蹩脚的问题会触及 C/C++ 规范的差异。
    • @Jens:常见问题解答和网站管理员鼓励我们编辑答案,如果我们可以改进它们。你现在的回答比以前好。我无意冒犯,老实说,鉴于该网站的政策,我认为您不符合被冒犯。见:stackoverflow.com/privileges/edit
    • 詹斯,通常我会同意你的看法。我尽量从不编辑某人的答案,而是用评论指出我的不同意见 - 更少的羽毛被激怒,他们可能会学到一些东西。或者也许我错了,我的编辑会适得其反。但在这种情况下,我认为约翰的编辑是合理的,因为您的答案是最高评价的,但显然不是 100% 正确。有必要阻止人们在不考虑替代方案的情况下“堆积”到看起来正确的答案。
    【解决方案4】:

    我在 C++ 标准 (5.7 [expr.add] / 7) 中找到了这个:

    如果两个指针 [...] 都为空,并且这两个指针是 减去,结果比较等于值 0 转换为 输入 std::ptrdiff_t

    正如其他人所说,C99 要求 2 个指针之间的加法/减法必须属于同一个数组对象。 NULL 不指向一个有效的对象,这就是为什么你不能在减法中使用它。

    【讨论】:

    • +1:很有趣,所以 C++ 明确定义了这种行为,而 C 没有。
    猜你喜欢
    • 2017-02-20
    • 2019-09-08
    • 2017-02-17
    • 2011-09-04
    • 1970-01-01
    • 2015-08-18
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    相关资源
    最近更新 更多