在执行指针算术时,有很多未定义/未指定行为的示例 - 指针必须指向同一个数组(或一个超过结尾),或者在同一个对象内,限制何时可以进行比较/操作以上等等。
以下操作是否定义明确?
int* p = 0;
p++;
答案 0 :(得分:37)
§5.2.6/ 1:
操作数对象的值通过向其添加
1
来修改,除非该对象的类型为bool
[..]
涉及指针的附加表达式在§5.7/ 5中定义:
如果指针操作数和结果都指向了元素 相同的数组对象,或者超过数组对象的最后一个元素, 评估不得产生溢出; 否则,行为 未定义。
答案 1 :(得分:15)
似乎很少理解什么"未定义的行为"手段。
在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不是空指针。
答案 2 :(得分:13)
指针上的操作(如递增,添加等)通常仅在指针的初始值和结果指向同一数组的元素(或指向最后一个元素的一个元素)时才有效。否则结果是未定义的。标准中有各种条款,供各种运营商说明,包括递增和添加。
(有一些例外,例如将0添加到NULL或从NULL中减去0是有效的,但这不适用于此处。)
NULL指针不指向任何内容,因此递增它会产生未定义的行为(“otherwise”子句适用)。
答案 3 :(得分:1)
事实证明它实际上是未定义的。有这样的系统
int *p = NULL;
if (*(int *)&p == 0xFFFF)
因此,++ p将使未定义的溢出规则跳闸(结果是sizeof(int *)== 2))。指针不保证是无符号整数,因此无符号包装规则不适用。
答案 4 :(得分:0)
正如科伦坡所说,它是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
答案 5 :(得分:0)
来自 ISO IEC14882-2011§5.2.6:
后缀++表达式的值是其操作数的值。 [注意:获得的价值是 原始值-end note]操作数应是可修改的左值。操作数的类型应为 算术类型或指向完整对象类型的指针。
因为nullptr是指向完整对象类型的指针。所以我不明白为什么这会是未定义的行为。
如前所述,同一文件也在§5.2.6/ 1 中说明:
如果指针操作数和结果都指向同一个数组对象的元素,或者一个过去的元素 数组对象的最后一个元素,评估不应产生溢出;否则,行为是 未定义。
这个表达似乎有些含糊不清。在我的解释中,未定义的部分可能很好地是对象的评估。而且我认为没有人会不同意这种情况。然而,指针算术似乎只需要一个完整的对象。
当然,postfix []运算符以及指向数组对象的减法或乘法只有很好地定义,如果它们实际上指向同一个数组。最重要的是因为人们可能会想到在1个对象中连续定义的2个数组,可以像单个数组一样进行迭代。
所以我的结论是操作定义明确,但评估不会。
答案 6 :(得分:0)
C标准要求通过标准定义的方法创建的对象不能具有等于空指针的地址。实现可能允许存在不是通过标准定义的方式创建的对象,但是标准对此内容没有任何说明(例如由于硬件设计问题)该对象是否具有与空指针相同的地址
如果某个实现记录了一个多字节对象的存在,该对象的地址将比较为null,则在该实现上说char *p = (char*)0;
将使p
持有一个指向该对象的第一个字节的指针[它等于一个空指针],而p++
将使其指向第二个字节。但是,除非实现记录了此类对象的存在,或者指定其将执行指针算术就好像此类对象存在,否则没有理由期望任何特定行为。使实现有意地捕获对空指针执行除加或减零或其他空指针以外的任何算术运算的尝试可能是一种有用的安全措施,并且出于某些预期的有用目的而使空指针递增的代码将与之不兼容。更糟糕的是,某些“聪明”的编译器可能会决定,即使在指针保持零的情况下也会递增的情况下,它们可以省略空检查,从而导致各种形式的破坏。
答案 7 :(得分:-1)
回到有趣的C天,如果p是某个东西的指针,p ++实际上是将p的大小添加到指针值,使p指向下一个东西。如果你将指针p设置为0,那么通过向它添加p的大小,p +仍然可以将它指向下一个东西。
更重要的是,你可以做一些事情,比如在p中添加或减去数字以便通过记忆移动它(p + 4指向过去p的第4个点)。这些都是有意义的好时光。根据编译器的不同,您可以在内存空间中找到您想要的任何位置。程序运行速度很快,即使在慢速硬件上也是如此,因为C只是做了你告诉它的事情,如果你太疯狂/马虎就崩溃了。
所以真正的答案是将指针设置为0是明确定义的,并且指针的增量是明确定义的。编译器构建器,操作系统开发人员和硬件设计人员会对您施加任何其他约束。
答案 8 :(得分:-2)
鉴于您可以递增任何定义良好的指针(所以任何不是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无关时,空指针看起来具有零值。空指针的值是平台相关的。在语言中使用零来初始化指针变量是一个语言定义。)