两个空指针相减的行为是否被定义?

80

如果两个非空指针变量均具有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?请注意,我感兴趣的唯一情况是两个指针都为NULL(表示未初始化缓冲区的情况)。这是因为在前面满足的假设条件下,“available”函数需要完全符合规范:

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

1
你尝试过多次执行该操作吗? - Hunter McMillen
10
你的意思是什么?我知道事实上在95%的实现中,这个操作的结果是0(假设5%是AS/400),不会发生任何问题。我对具体的实现细节不感兴趣,我的问题涉及一些特定的标准定义。 - John Luebs
8
Hunter McMillen:这是一个糟糕的方法 - “我将指针存储在int中,什么也没有发生。我在不同的计算机和编译器上进行了检查,什么也没有发生。然后出现了64位计算机”。如果某些东西现在可以工作,但依赖于未定义的行为,那么它将来可能无法工作。 - Maciej Piechotka
3
我赞扬你确保你的代码符合相关标准,而不仅仅是在你测试的平台上能够正常运行。 - David Schwartz
1
@TobySpeight:一个8086编译器将对near限定的空指针和far限定的空指针有不同的表示,但是它会为null far指针使用多个表示吗?如果将null near指针转换为far指针,然后将其与null far指针进行比较,例如当DS等于0x1234时,会发生什么:(1)0x0000被转换为0x0000:0x0000;(2)0x0000被转换为0x1234:0x0000,但比较运算符检查两个段都为零的情况,或者(3)0x0000被转换为0x1234:0x0000,与0x0000:0x0000不相等。 - supercat
显示剩余2条评论
4个回答

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

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

[...]

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

而§6.3.2.3/3则说:

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

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

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

C ++ 03在这种情况下确实具有定义的行为。标准针对两个空指针的减法做了特殊例外。 C ++ 03 §5.7/7说:

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

C++11(以及最新的草案C++14,n3690)与C++03完全相同,只有一个小改变:ptrdiff_t被替换为std::ptrdiff_t


15
鉴于完整性,目前这是最好的答案。 - John Dibling
@R..,为了消除歧义,应该说“比较相等”,对吧?因为两个空指针可能不包含相同的值,所以它们“不相等”。 - Jens Gustedt
看起来,即将发布的C1X标准的最新草案中也有相同的语言。希望这只是一个疏忽,语言委员会能够修复它。 - Adam Rosenfield
两个空指针由于比较相等而具有相同的。当然,它们可能没有相同的表示 - R.. GitHub STOP HELPING ICE
在C++11和最新的C++14草案中,仍然可以减去两个空指针(null)。(只是向大家保证,因为我看不到撤销这一点的理由。) - CTMacUser
显示剩余2条评论

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

如果两个指针[...]都是 null,并且这两个指针相减,结果等于转换为类型 std::ptrdiff_t 的值 0。

正如其他人所说,C99要求对两个指针进行加法/减法时必须是同一数组对象。NULL 没有指向有效的对象,因此不能在减法中使用它。

4
有趣的是,C++明确定义了这种行为,而C没有。 - Oliver Charlesworth

24

编辑: 这个回答仅适用于 C 语言,我在回答时没有看到 C++ 标签。

不,指针算术运算只允许对指向同一对象的指针进行。由于按照 C 标准的定义,空指针不指向任何对象,因此这是未定义的行为。

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


5
这是不正确的。请看5.7 [expr.add] / 7:“如果两个指针指向同一对象或都指向同一个数组的末尾或都为null,并且这两个指针进行减法运算,结果将等于转换为类型std::ptrdiff_t的值0。” - CB Bailey
1
这在 C++ 的特定上下文中是不正确的。那么对于其他标记的语言呢? - John Dibling
8
哇,从没想过这个无聊的问题会涉及到C/C++规范的差异。 - John Luebs
3
@Jens:FAQ和网站管理员鼓励我们编辑答案,如果能让它们变得更好。你的答案现在比以前要好。我并没有想要冒犯你,说实话,考虑到这个网站的政策,我认为你有点过于敏感了。请参见:http://stackoverflow.com/privileges/edit - John Dibling
2
Jens,通常情况下我会同意你的观点。我尽量避免编辑别人的答案,而是通过评论指出我的异议-这样可以减少争执,并且他们可能会学到一些东西。或者也有可能我错了,我的编辑反而会适得其反。但在这种情况下,我认为John的编辑是合理的,因为你的答案虽然评分很高,但显然并不完全正确。有必要防止人们在考虑其他选择之前就盲目追随一个看似正确的答案。 - Mark Ransom
显示剩余10条评论

0

C标准在这种情况下不会对行为施加任何要求,但许多实现都规定了指针算术的行为,超出了标准所需的最低限度,包括这种情况。

在任何符合C实现的情况下,以及几乎所有(如果不是全部)类C方言的实现中,对于任何指针p,使得*p*(p-1)标识某个对象,将满足以下保证:

  • 对于任何等于零的整数值z,指针值(p+z)(p-z)在每个方面上都等效于p,除非它们只有在pz都是常量时才是常量。
  • 对于任何等价于pq,表达式p-qq-p都将产生零。

对于所有指针值(包括 null),这样的保证可能会消除用户代码中某些 null 检查的需要。此外,在大多数平台上,生成代码以不考虑它们是否为 null 来维护所有指针值的这种保证通常比特殊处理 null 更简单、更便宜。然而,在某些平台上,即使添加或减去零,使用 null 指针进行指针算术的尝试也可能会触发陷阱。在这种平台上,为了维护保证,必须添加许多编译器生成的 null 检查到指针操作中,这种数量在许多情况下远远超过可以省略的用户生成的 null 检查。

如果有一种实现,维护这些保证的成本很高,但几乎没有任何程序从中受益,那么允许它陷入“null+zero”计算并要求这种实现的用户代码包括手动 null 检查是有意义的。这种容忍预计不会影响其他 99.44% 的实现,其中维护保证的价值将超过成本。这样的实现应该维护这样的保证,但其作者不应该需要标准的作者告诉他们这一点。

C++的作者们决定,符合规范的实现必须不惜一切代价保持上述保证,即使在这些平台上会严重降低指针算术的性能。他们认为,即使在维护这些保证的成本很高的平台上,这些保证的价值也超过了成本。这种态度可能受到将C++视为比C更高级语言的愿望的影响。可以期望C程序员知道特定目标平台是否以不寻常的方式处理类似(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“不可能为null”,从而省略后续的空指针检查。这可能导致代码在与实际问题无关的地方出现错误。


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