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

时间:2011-11-14 21:11:47

标签: c++ c c99 c89

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

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

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

说,ex.buf指向数组或某些malloc内存。如果我的代码始终确保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;
}

4 个答案:

答案 0 :(得分:96)

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

  

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

     

[...]

     

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

§6.3.2.3/ 3说:

  

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

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

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

另一方面,

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

  

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

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

答案 1 :(得分:36)

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

  

如果两个指针都是空的,那么两个指针都是   减去,结果比较等于转换为0的值0   输入std :: ptrdiff_t

正如其他人所说,C99要求两个指针之间的加/减是同一个数组对象。 NULL不指向有效对象,这就是您不能在减法中使用它的原因。

答案 2 :(得分:23)

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

不,指针算法仅允许指向同一对象内的指针。由于C标准的定义,空指针不指向任何对象,因此这是未定义的行为。

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

答案 3 :(得分:1)

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

在任何符合C的实现,以及几乎所有(如果不是全部)类C语言的实现中,以下保证将适用于任何指针p,以便*p或{{1}识别一些对象:

  • 对于等于零的任何整数值*(p-1),指针值z(p+z)将在(p-z)的各个方向上等效,除了它们只是常量如果pp都是常量。
  • 对于等同于z的任何q,表达式pp-q都会产生零。

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

如果有一项实施,其中维持担保的成本会很高,但很少有任何计划会从中获得任何好处,那么允许它陷入" null + zero"计算,并要求此类实现的用户代码包括保证可能不必要的手动空检查。这种补贴预计不会影响其他99.44%的实施,其中维持担保的价值将超过成本。这样的实现应该支持这样的保证,但是他们的作者不应该要求标准的作者告诉他们。

C ++的作者已经决定,符合要求的实现必须不惜任何代价维护上述保​​证,即使在可能会严重降低指针算法性能的平台上也是如此。他们认为,即使在维持昂贵的平台上,担保的价值也会超过成本。这种态度可能受到将C ++视为高级语言的愿望的影响。可以预期AC程序员知道特定目标平台何时会以不寻常的方式处理类似(null + 0)的情况,但是C ++程序员预计不会关注这些事情。因此,保证一致的行为模型被认为是值得的。

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

p

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

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

优化器可能会或可能无法摆脱。如果不包含额外的代码可能会导致某些编译器认为由于p&#34;可能不为空&#34;,可能会省略任何后续的空指针检查,从而导致代码在无关的位置中断实际问题&#34;。