正在递增定义良好的空指针

Is incrementing a null pointer well-defined?

本文关键字:空指针 定义      更新时间:2023-10-16

在进行指针运算时,有很多未定义/未指定行为的例子-指针必须指向同一数组内(或一个超过末尾的数组),或指向同一对象内,对何时可以基于以上内容进行比较/操作的限制,等等。

以下操作是否定义明确?

int* p = 0;
p++;

§5.2.6/1:

通过向操作数对象添加1来修改操作数对象的值,除非对象的类型为bool[..]

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

如果指针操作数和结果都指向相同的数组对象或经过数组对象的最后一个元素的一个数组对象,评估不得产生溢出否则,行为未定义

人们似乎对"未定义行为"的含义理解甚少。

在C、C++和Objective-C等相关语言中,有四种行为:有由语言标准定义的行为。有实现定义的行为,这意味着语言标准明确规定实现必须定义行为。存在未指明的行为,语言标准规定有几种行为是可能的。还有一种未定义的行为,语言标准对结果没有任何说明。因为语言标准没有说明结果,任何事情都可能发生在未定义的行为中。

这里的一些人认为"未定义的行为"意味着"发生了不好的事情"。这是错误的。它的意思是"任何事情都可能发生",包括"坏的事情可能发生"而不是"坏的东西必须发生"。在实践中,这意味着"当你测试你的程序时,没有什么不好的事情发生,但一旦它被交付给客户,一切都会变得一团糟"。由于任何事情都可能发生,编译器实际上可以假设代码中没有未定义的行为,因为它要么是真的,要么是假的,在这种情况下,任何事情都有可能发生,这意味着由于编译器错误的假设而发生的任何事情都是正确的。

有人声称,当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检查,只返回1000000/x。这意味着当您调用f(0)而不是返回0时,程序将崩溃。

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

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

(有几个例外,比如将零添加到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

来自ISO IEC 14882-2011§5.2.6:

后缀++表达式的值是其操作数的值。[注:获得的值是原始值--尾注]操作数应为可修改的左值。操作数的类型应为算术类型或指向完整对象类型的指针。

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

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

如果指针操作数和结果都指向同一数组对象的元素,或者一个过去数组对象的最后一个元素,则评估不应产生溢出;否则,行为为未定义。

这个表达式似乎有点模棱两可。在我的解释中,未定义的部分很可能是对对象的评估。我想没有人会不同意这种情况。然而,指针算法似乎只需要一个完整的对象。

当然,指向数组对象的指针上的后缀[]运算符和减法或乘法只有在它们实际上指向同一数组的情况下才能得到很好的定义。最重要的是,人们可能会认为在一个对象中连续定义的两个数组可以像单个数组一样迭代。

因此,我的结论是,操作是明确的,但评估不会。

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

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

事实证明它实际上是未定义的。有些系统是真正的

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

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

回到有趣的C时代,如果p是一个指向某个东西的指针,p++有效地将p的大小添加到指针值中,使p指向下一个东西。如果你将指针p设置为0,那么通过将p的大小添加到它上,p++仍然会将它指向下一个东西。

更重要的是,你可以做一些事情,比如从p中加或减数字,让它在记忆中移动(p+4会指向p后的第4位)。这些都是有意义的美好时光。根据编译器的不同,您可以在内存空间中任意访问。程序运行得很快,甚至在慢硬件上也是如此,因为C只是按照你的要求去做,如果你太疯狂/太草率,它就会崩溃。

因此,真正的答案是,将指针设置为0是定义良好的,而递增指针是定义明确的。编译器构建者、操作系统开发人员和硬件设计师会对您施加任何其他约束。

假设您可以递增任何定义良好大小的指针(因此任何不是空指针的指针),并且任何指针的值都只是一个地址(一旦存在空指针,就没有特殊的处理方法),我想递增的空指针没有理由不(无用地)指向"一个在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));

j也做了数学运算,但它等于j,所以它是一个空指针。

因此,我认为这是定义明确的,只是没用。

(打印时,空指针的值为零是不相关的。空指针的取决于平台。在语言中使用零初始化指针变量是一种语言定义。)