是否定义了减去两个NULL指针的行为?


78

如果两个非空指针变量都被赋值,是否定义了两个非空指针变量(根据C99和/或C ++ 98)的区别NULL

例如,假设我有一个看起来像这样的缓冲区结构:

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

说,ex.buf指向数组或某些已分配的内存。如果我的代码始终确保pwritepread指向该数组或指向该数组的一个数组,那么我很有信心ex.pwrite - ex.pread将始终对其进行定义。但是,如果pwritepread都为NULL。我是否可以期望将两者定义相减,(ptrdiff_t)0还是严格遵从的代码需要测试指针是否为NULL?请注意,我唯一感兴趣的情况是两个指针均为NULL(表示缓冲区未初始化的情况)。在满足上述假设的前提下,原因与完全兼容的“可用”功能有关:

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

1
您是否尝试过多次手术?
亨特·麦克米伦

10
你什么意思?我知道一个事实,即在95%的实现上,此操作的结果为0(假设5%为AS / 400),并且不会发生任何不良情况。我对实施细节不感兴趣。我的问题与一些特定的标准定义有关。
约翰·卢布斯

8
Hunter McMillen:这是一种不好的方法-“我将指针存储在int中,什么也没发生。我在不同的计算机和编译器上检查,什么也没发生。然后出现了64位计算机”。如果某些方法现在可以使用,但依赖于未定义的行为,则将来可能无法使用。
Maciej Piechotka

3
我赞扬你确保你的代码是保障相关标准,而不是仅仅注意到,它发生在工作你测试平台的工作。
大卫·史瓦兹

1
@TobySpeight:一个8086编译器对于near-qualified空指针与-qualified空指针会有不同的表示形式far,但是它将为空far指针使用多种表示形式吗?如果将空near指针转换为far与空far指针进行比较的指针,例如DS等于0x1234,则会发生以下情况:(1)将0x0000视为0x0000:0x0000;(2)0x0000转换为0x1234:0x0000,但是比较运算符检查两个段为零的情况,或者(3)0x0000转换为0x1234:0x0000,比较不等于0x0000:0x0000。
超级猫

Answers:


100

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

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

[...]

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

§6.3.2.3/ 3说:

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

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

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

另一方面,C ++ 03在此实例中确实定义了行为。该标准是减去两个空指针的特殊例外。C ++ 03§5.7/ 7说:

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

C ++ 11(以及C ++ 14,n3690的最新草案)具有相同的措辞,C ++ 03,与只是微小的变化std::ptrdiff_t来代替ptrdiff_t


14
由于完整性,这是当前的最佳答案。
John Dibling

这似乎是对标准的疏忽,应通过“ 9)减去两个指针,如果它们相等,则结果为零。否则,两个指针都应指向同一数组对象的元素...”
R .. GitHub停止帮助ICE,

@R ..,要消除歧义,应该说“比较相等”,不是吗?因为两个空指针可能不包含相同的值,所以它们“不相等”。
詹斯·古斯特

看起来即将发布的C1X标准最新草案也具有相同的语言。我希望这实际上是一个疏忽,并且语言委员会会对此进行纠正。
亚当·罗森菲尔德

通过比较相等,两个空指针具有相同的。当然,它们可能没有相同的表示形式
R .. GitHub停止帮助ICE,

36

我在C ++标准(5.7 [expr.add] / 7)中发现了这一点:

如果两个指针都为空,并且两个指针相减,则结果比较等于转换为std :: ptrdiff_t类型的值0

就像其他人所说的那样,C99要求在同一数组对象的2个指针之间进行加/减。NULL并不指向有效的对象,这就是为什么您不能在减法中使用它的原因。


4
+1:有趣,因此C ++明确定义了这种行为,而C没有。
奥利弗·查尔斯沃思

23

编辑:此答案仅对C有效,当我回答时我没有看到C ++标记。

不,指针算术仅适用于指向同一对象内的指针。由于根据C标准的定义,空指针不指向任何对象,因此这是未定义的行为。

(尽管,我猜想任何合理的编译器都将返回0它,但是谁知道呢。)


5
这是不正确的。参见5.7 [expr.add] / 7:“如果两个指针指向同一对象,或者两个指针指向同一数组的末尾,或者两个指针都为null,并且两个指针相减,则结果等于0转换为类型std::ptrdiff_t。”
CB Bailey

1
在C ++的特定上下文中,这是不正确的。其他标记的语言呢?
John Dibling

8
哇,从来没有想过这个la脚的问题会触及C / C ++规范的差异。
约翰·吕布斯

3
@Jens:FAQ和站点管理员鼓励我们编辑答案,如果我们可以改善它们的话。您的答案现在比以前更好。我无意冒犯,老实说,鉴于该站点的政策,我认为您不应该受到冒犯。请参阅:stackoverflow.com/privileges/edit
John Dibling

2
詹斯,通常我会同意你的看法。我从不尝试编辑某人的答案,而是通过评论指出我的异议-减少了羽毛的松动,他们可能会学到一些东西。或者,也许我错了,而我的编辑会适得其反。但是在这种情况下,我认为John的编辑是合理的,因为您的回答是最高评价,但显然不是100%正确。有必要避免人们“堆砌”正确的答案,而不考虑其他选择。
Mark Ransom

0

在这种情况下,C标准对行为没有任何要求,但是许多实现在许多情况下确实指定了指针算术的行为,超出了标准(包括该标准)所要求的最低要求。

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

  • 对于z等于零的任何整数值,指针值(p+z)(p-z)在各个方面都将等效于p,不同之处在于,只有当pz都恒定时,它们才会是恒定的。
  • 对于q等于的任何p表达式,p-qq-p都将产生零。

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

如果存在这样一种实现,即维持保证的成本会很高,但是很少有程序会从中获得任何好处,那么允许它捕获“空+零”计算并要求用户代码这样的实现包括手动进行null检查,以确保可以不必进行担保。预计这一津贴不会影响其余99.44%的实施,因为这些实施的担保价值将超过成本。这样的实现应该维护这样的保证,但是他们的作者不需要标准的作者告诉他们。

C ++的作者已经决定,即使在可能严重降低指针算术性能的平台上,兼容的实现也必须不惜一切代价维护上述保​​证。他们认为,即使在维护成本很高的平台上,担保的价值也将超过成本。这种态度可能受到了将C ++视为比C语言更高级的语言的渴望的影响。可以期望AC程序员知道特定的目标平台何时以异常的方式处理类似(null + zero)的情况,但是C ++程序员没想到自己会关心这些事情。因此,保证一致的行为模型是值得的。

当然,如今有关“定义的”的问题很少与平台可以支持的行为有关。取而代之的是,现在,对于编译器而言,以“优化”的名义,要求程序员手动编写代码来处理平台以前可以正确处理的极端情况,已成为一种时尚。例如,假设应该n从地址开始输出字符的代码p写为:

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”,因此可以省略任何后续的null指针检查,从而导致代码陷入与实际“问题”无关的位置。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.