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

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

例如,假设我有一个这样的缓冲结构:

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;
}
3341 次浏览

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

不,指针算法只允许指向同一对象的指针使用。因为根据 C 标准空指针的定义,它不指向任何对象,所以这是一个未定义行为。

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

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

如果两个指针[ ... ]都为空,则两个指针为 减去0后,结果与转换为 类型 std: : ptrdiff _ t

正如其他人所说,C99需要在同一数组对象的两个指针之间进行加减运算。NULL 不指向一个有效的对象,这就是为什么你不能在减法中使用它。

在 c99中,它在技术上是未定义行为的。 c996.5.6表示:

7)为了这些操作符的目的,指向一个对象的指针不是 属性的长度为1的数组的第一个元素的指针的行为相同 类型作为对象的元素类型。

[...]

9)当减去两个指针时,两个指针都应指向同一数组对象的元素, 或数组对象的最后一个元素后面的一个; 结果是 两个数组元素的下标

6.32.3/3说:

值为0的整数常量表达式,或者将此类表达式强制转换为 如果一个空指针常量被转换成指针类型,那么得到的指针(称为 空指针)保证不等于指向任何对象或函数的指针。

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

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

另一方面,C + + 03在这个实例中确实定义了行为。该标准对于减去两个空指针有一个特殊的例外。C + + 035.7/7表示:

如果在指针值中加入或减去值0,则结果与原始指针值相等。如果两个指针指向同一个对象,或者两个指针都指向同一个数组的末尾,或者两个指针都指向同一个数组的末尾,那么这两个指针都是 null,并且减去这两个指针,结果就等于转换为 ptrdiff_t类型的值0。

C + + 11(以及 C + + 14,n3690的最新草案)与 C + + 03有相同的措辞,只是略微改变了 std::ptrdiff_t而不是 ptrdiff_t

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

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

  • 对于任何等于零的整数值 z,指针值 (p+z)(p-z)在任何方面都等价于 p,除了它们只有在 pz都是常数时才是常数。
  • 对于任何等价于 pq,表达式 p-qq-p都将得到零。

对所有指针值(包括 null)保持这样的保证,可能会消除在用户代码中进行一些 null 检查的需要。此外,在大多数平台上,生成支持所有指针值的这种保证的代码,而不考虑它们是否为 null,这比特别处理 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”,任何后续的 null 指针检查都可能被忽略,从而导致代码在一个与实际“问题”无关的地方中断。