C标准在这种情况下不会对行为施加任何要求,但许多实现都规定了指针算术的行为,超出了标准所需的最低限度,包括这种情况。
在任何符合C实现的情况下,以及几乎所有(如果不是全部)类C方言的实现中,对于任何指针p
,使得*p
或*(p-1)
标识某个对象,将满足以下保证:
- 对于任何等于零的整数值
z
,指针值(p+z)
和(p-z)
在每个方面上都等效于p
,除非它们只有在p
和z
都是常量时才是常量。
- 对于任何等价于
p
的q
,表达式p-q
和q-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”,从而省略后续的空指针检查。这可能导致代码在与实际问题无关的地方出现错误。
near
限定的空指针和far
限定的空指针有不同的表示,但是它会为nullfar
指针使用多个表示吗?如果将nullnear
指针转换为far
指针,然后将其与nullfar
指针进行比较,例如当DS等于0x1234时,会发生什么:(1)0x0000被转换为0x0000:0x0000;(2)0x0000被转换为0x1234:0x0000,但比较运算符检查两个段都为零的情况,或者(3)0x0000被转换为0x1234:0x0000,与0x0000:0x0000不相等。 - supercat