增加一个空指针定义良好?
在做指针算术的时候,有很多未定义/未指定行为的例子 – 指针必须指向同一个数组(或者结束时),或者在同一个对象中,限制什么时候可以根据上面的方法进行比较/操作等等
以下操作是否定义良好?
int* p = 0; p++;
§5.2.6/ 1:
除非对象的types为
bool
[…],否则操作数对象的值将被修改。
涉及指针的附加expression式在第5.7.5节中定义:
如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不会产生溢出; 否则,行为是不确定的。
对于“未定义的行为”的含义,似乎还不甚了解。
在C,C ++和Objective-C等相关语言中,有四种行为:有语言标准定义的行为。 有实现定义的行为,这意味着语言标准明确表示实现必须定义行为。 没有明确的行为,语言标准说有几种行为是可能的。 还有没有定义的行为, 语言标准对结果没有任何说明 。 由于语言标准没有对结果做任何说明,任何事情都可能发生在未定义的行为上。
这里有些人认为“未定义的行为”是指“不好的事情发生”。 那是错的 这意味着“任何事情都可能发生”,包括“可能发生的事情”,而不是“不好的事情必须发生”。 实际上,这意味着“testing你的程序时没有什么不好的事情发生,但是一旦它被运送给顾客,所有的事情都会崩溃”。 由于任何事情都可能发生,编译器实际上可以假设代码中没有未定义的行为 – 因为它是真的,或者是错误的,在这种情况下会发生任何事情,这意味着无论发生什么,因为编译器的错误假设仍然是正确。
有人声称,当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不是空指针。
对指针的操作(如递增,join等)通常只在指针的初始值和结果指向同一数组的元素(或超过最后一个元素的元素)时才有效。 否则结果是不确定的。 这个标准中有各种各样的条款,包括增加和增加。
(有几个例外,例如将NULL加到NULL或从NULL减去零,但这不适用于此)。
一个NULL指针不指向任何东西,所以递增它给出未定义的行为(“否则”子句适用)。
事实certificate,这实际上是不确定的。 有这样的系统
int *p = NULL; if (*(int *)&p == 0xFFFF)
因此,++ p会跳过未定义的溢出规则(原来sizeof(int *)== 2))。 指针不保证是无符号整数,所以无符号换行规则不适用。
正如Columbo所说,这是UB。 从语言律师的angular度来看,这是一个明确的答案。
但是我知道的所有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 :
后缀++expression式的值是其操作数的值。 [注意:获得的值是原始值的一个副本 – 结束注释]操作数应该是一个可修改的左值。 操作数的types应为算术types或指向完整对象types的指针。
由于nullptr是一个指向完整对象types的指针。 所以我不明白为什么这是未定义的行为。
如前所述,同一文件在第5.2.6 / 1节中也提到:
如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不会产生溢出; 否则,行为是不确定的。
这个expression似乎有点模棱两可。 在我的解释中,未定义的部分很可能是对象的评估。 而且我认为没有人会不同意这种情况。 但是,指针算术似乎只需要一个完整的对象。
当然postfix []运算符和指向数组对象的减法运算或乘法运算都是很好定义的,如果它们实际上指向同一个数组的话。 非常重要,因为人们可能会认为在1个对象中连续定义的2个数组可以像遍历一个数组一样迭代。
所以我的结论是,手术是明确的,但评估不会。
回到有趣的C日,如果p是指向某事物的指针,p ++实际上将p的大小加到指针值上,使p指向下一个东西。 如果将指针p设置为0,那么p ++仍然会将它指向下一个东西,方法是将p的大小加上它。
更重要的是,你可以做一些事情,比如从p中增加或者减去数字,以便通过记忆来移动它(p + 4将指向p的第四个东西)。这些都是合理的。 根据编译器的不同,你可以在你的内存空间中去任何你想要的地方。 即使在慢速的硬件上,程序也运行得很快,因为C只是做了你告诉它的事情,如果你太疯狂/马虎,就会崩溃。
所以真正的答案是将指针设置为0是明确定义的,并且递增指针是明确的。 编译器构build者,操作系统开发人员和硬件devise人员将为您提供任何其他限制。
假设你可以递增任何指定的大小的指针(所以任何不是空指针的东西),而且任何指针的值只是一个地址(一旦存在就没有特殊的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已经完成了math,但它等于j,所以它是一个空指针。
因此,我认为这是明确的,没用的。
(当printfed时null指针的值为零,null指针的值是平台相关的,在语言中使用零来初始化指针variables是一种语言定义。