递增空指针是否定义明确?

Is incrementing a null pointer well-defined?

在进行指针运算时,有很多 undefined/unspecified 行为的示例 - 指针必须指向同一个数组(或末尾后的一个)或同一个对象,限制何时可以做comparisons/operations基于以上等

下面的操作是否定义明确?

int* p = 0;
p++;

§5.2.6/1:

The value of the operand object is modified by adding 1 to it, unless the object is of type bool [..]

涉及指针的加法表达式在 §5.7/5 中定义:

If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

指针上的操作(如递增、添加等)通常仅在指针的初始值和结果都指向同一数组的元素(或指向最后一个元素之后的元素)时才有效。否则结果未定义。标准中有各种条款供各种运算符使用,包括递增和添加。

(有一些例外情况,例如向 NULL 添加零或从 NULL 中减去零是有效的,但这不适用于此处)。

NULL 指针不指向任何东西,因此递增它会产生未定义的行为("otherwise" 子句适用)。

正如 Columbo 所说,它是 UB。从语言律师的角度来看,这是最终的答案。

然而,我所知道的所有 C++ 编译器实现都会给出相同的结果:

int *p = 0;
intptr_t ip = (intptr_t) p + 1;

cout << ip - sizeof(int) << endl;

给出 0,这意味着 p 在 32 位实现上具有值 4,在 64 位实现上具有值 8

换句话说:

int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour 
int *p2 = (int *) ip;      // formally UB
p++;               // formally UB
assert ( p2 == p) ;  // works on all major implementation

鉴于您可以递增任何定义明确的大小的指针(因此任何不是 void 指针的指针),并且任何指针的值只是一个地址(一旦 NULL 指针出现,就没有特殊处理)存在),我想没有理由为什么递增的空指针不会(无用地)指向“NULL 之后的第一个”项。

考虑一下:

// These functions are horrible, but they do return the 'next'
// and 'prev' items of an int array if you pass in a pointer to a cell.
int *get_next(int *p) { return p+1; }
int *get_prev(int *p) { return p-1; }

int *j = 0;

int *also_j = get_prev(get_next(j));

also_j 已对其进行数学运算,但它等于 j,因此它是一个空指针。

因此,我建议它定义明确,只是没用。

(并且空指针在 printfed 时显示为零值是无关紧要的。空指针的 value 是平台相关的。在语言中使用零来初始化指针变量是一种语言定义。)

原来它实际上是未定义的。有些系统是正确的

int *p = NULL;
if (*(int *)&p == 0xFFFF)

因此,++p 会触发未定义的溢出规则(结果是 sizeof(int *) == 2))。不能保证指针是无符号整数,因此无符号换行规则不适用。

似乎对"undefined behaviour"的理解程度很低。

在 C、C++ 和 Objective-C 等相关语言中,有四种行为: 有语言标准定义的行为。有实现定义的行为,这意味着语言标准明确表示实现必须定义行为。存在未指定的行为,语言标准表示可能存在多种行为。并且存在未定义的行为,语言标准没有说明任何结果。因为语言标准对结果没有任何说明,所以任何事情都可能发生在未定义的行为上。

这里有些人认为 "undefined behaviour" 意味着 "something bad happens"。那是错误的。它的意思是 "anything can happen",其中包括 "something bad can happen",而不是 "something bad must happen"。实际上它意味着 "nothing bad happens when you test your program, but as soon as it is shipped to a customer, all hell breaks loose"。由于任何事情都可能发生,编译器实际上可以假设您的代码中没有未定义的行为 - 因为它要么为真,要么为假,在这种情况下任何事情都可能发生,这意味着由于编译器的错误假设而发生的任何事情仍然是正确的。

有人说当p指向3个元素的数组,计算出p+4,不会有什么不好的事情发生。错误的。您的优化编译器来了。说这是你的代码:

int f (int x)
{
    int a [3], b [4];
    int* p = (x == 0 ? &a [0] : &b [0]);
    p + 4;
    return x == 0 ? 0 : 1000000 / x;
}

如果 p 指向 a [0],则计算 p + 4 是未定义的行为,但如果它指向 b [0],则不是。因此允许编译器假定 p 指向 b [0]。因此允许编译器假定 x != 0,因为 x == 0 会导致未定义的行为。因此,允许编译器删除 return 语句中的 x == 0 检查,只删除 return 1000000 / x。这意味着当您调用 f (0) 而不是 returning 0 时您的程序会崩溃。

另一个假设是如果你递增一个空指针然后再次递减它,结果还是一个空指针。又错了。除了递增空指针可能会在某些硬件上崩溃的可能性之外,这又如何:由于递增空指针是未定义的行为,编译器会检查指针是否为空,并且仅在指针不是空指针时递增指针,所以 p + 1 又是一个空指针。通常它会对递减做同样的事情,但作为一个聪明的编译器,它注意到如果结果是空指针,p + 1 总是未定义的行为,因此可以假设 p + 1 不是空指针,因此可以省略空指针检查。这意味着如果 p 是空指针,则 (p + 1) - 1 不是空指针。

回到有趣的 C 时代,如果 p 是指向某物的指针,p++ 会有效地将 p 的大小添加到指针值,以使 p 指向下一个某物。如果您将指针 p 设置为 0,那么按理说 p++ 仍会通过将 p 的大小添加到它来指向下一件事。

更重要的是,您可以做一些事情,例如从 p 中添加或减去数字以在内存中移动它(p+4 将指向 p 之后的第 4 个东西)。这些都是有意义的好时光。根据编译器的不同,你可以在你的记忆中去任何你想去的地方space。程序 运行 速度很快,即使在较慢的硬件上也是如此,因为 C 只是按照您的要求执行,如果您执行得太快就会崩溃 crazy/sloppy。

所以真正的答案是将指针设置为 0 是明确定义的,递增指针也是明确定义的。编译器构建者、os 开发人员和硬件设计人员对您施加了任何其他限制。

来自 ISO IEC 14882-2011 §5.2.6:

The value of a postfix ++ expression is the value of its operand. [ Note: the value obtained is a copy of the original value —end note ] The operand shall be a modifiable lvalue. The type of the operand shall be an arithmetic type or a pointer to a complete object type.

因为 nullptr 是指向完整对象类型的指针。所以我不明白为什么这是未定义的行为。

如前所述,同一文件在§5.2.6/1中也指出:

If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

这个表述好像有点含糊。在我的解释中,未定义的部分很可能是对象的评估。我认为没有人会不同意这种情况。然而,指针运算似乎只需要一个完整的对象。

当然,后缀 [] 运算符和数组对象指针上的减法或乘法只有定义明确,如果它们实际上指向同一个数组。最重要的是因为人们可能会认为在 1 个对象中连续定义的 2 个数组可以像单个数组一样进行迭代。

所以我的结论是操作定义明确,但评估不是。

C 标准要求通过标准定义方式创建的任何对象都不能具有等于空指针的地址。然而,实现可能允许存在不是通过标准定义的方式创建的对象,并且标准没有说明这样的对象是否可能具有与空指针相同的地址(可能是由于硬件设计问题) .

如果一个实现记录了一个多字节对象的存在,其地址比较等于 null,那么在该实现上说 char *p = (char*)0; 会使 p 持有指向第一个字节的指针该对象[比较等于一个空指针],p++ 会使它指向第二个字节。然而,除非实现记录了这样一个对象的存在,或者指定它将执行指针运算,就好像这样一个对象存在一样,否则没有理由期望任何特定的行为。除了添加或减去零或其他空指针之外,让实现故意捕获对空指针执行任何类型的算术的尝试可能是一种有用的安全措施,并且为了某些预期有用的目的而增加空指针的代码将与其不兼容。更糟糕的是,某些 "clever" 编译器可能会决定在指针会增加的情况下忽略空值检查,即使它们保持空值,也会导致各种破坏。