提问人:Luchian Grigore 提问时间:4/23/2015 更新时间:7/3/2018 访问量:5182
递增 null 指针是否定义明确?
Is incrementing a null pointer well-defined?
问:
在进行指针算术时,有很多未定义/未指定行为的例子 - 指针必须指向同一数组内部(或末尾的数组),或指向同一对象内部,限制何时可以基于上述内容进行比较/操作等。
以下操作是否定义明确?
int* p = 0;
p++;
答:
§5.2.6/1:
操作数对象的值通过添加来修改,除非该对象的类型为 [..]
1
bool
涉及指针的加法表达式在 §5.7/5 中定义:
如果指针操作数和结果都指向 同一个数组对象,或者一个超过数组对象的最后一个元素, 评价不得产生溢出;否则,该行为 未定义。
评论
malloc
1
Foo *p = static_cast<Foo *>(malloc(0));
p
p
对指针的操作(如递增、加法等)通常仅在指针的初始值和结果都指向同一数组的元素(或超过最后一个元素的元素)时才有效。否则,结果是未定义的。标准中有各种条款供各种运营商使用,包括递增和加法。
(有一些例外情况,例如向 NULL 添加零或从 NULL 中减去零才有效,但这不适用于此处)。
NULL 指针不指向任何内容,因此递增它会产生未定义的行为(“否则”子句适用)。
评论
a+3
int a[3]
p[0]
*p
正如哥伦布所说,它是UB。从语言律师的角度来看,这是最终的答案。
但是,我知道所有 C++ 编译器实现都会给出相同的结果:
int *p = 0;
intptr_t ip = (intptr_t) p + 1;
cout << ip - sizeof(int) << endl;
给出 ,表示在 32 位实现上的值为 4,在 64 位实现上的值为 80
p
换一种说法:
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
评论
p2=p+1; if(p==nullptr){...}
if
-fno-delete-null-pointer-checks
p++
int p = foo->bar
sizeof(*p)
offsetof(cFoo, bar)
鉴于您可以递增任何定义明确大小的指针(因此任何不是空指针的指针),并且任何指针的值都只是一个地址(一旦存在,就没有对 NULL 指针进行特殊处理),我想没有理由增加的 null 指针不会(无用地)指向“NULL 之后的一个”est 项。
考虑一下:
// 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 时,null 指针似乎值为零,这无关紧要。null 指针的值取决于平台。在语言中使用零来初始化指针变量是一种语言定义。
评论
事实证明,它实际上是未定义的。有些系统确实如此
int *p = NULL;
if (*(int *)&p == 0xFFFF)
因此,++p 会触发未定义的溢出规则(结果是 sizeof(int *) == 2))。不能保证指针是无符号整数,因此无符号换行规则不适用。
评论
对“未定义行为”的含义的理解似乎很低。
在 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 + 1) - 如果 p 是 null 指针,则 1 不是 null 指针。
评论
回到有趣的 C 时代,如果 p 是指向某物的指针,那么 p++ 实际上是将 p 的大小添加到指针值中,以使 p 指向下一个某物。如果将指针 p 设置为 0,那么 p++ 仍然会通过向其添加 p 的大小来将其指向下一件事。
更重要的是,你可以做一些事情,比如在p中添加或减去数字,以在内存中移动它(p+4将指向p之后的第4个东西)。这些都是有意义的好时光。根据编译器的不同,您可以在内存空间内访问任何您想要的地方。程序运行得很快,即使在慢速硬件上也是如此,因为 C 只是按照你的吩咐去做,如果你太疯狂/草率,就会崩溃。
因此,真正的答案是,将指针设置为 0 是明确定义的,而递增指针是明确定义的。编译器构建者、操作系统开发人员和硬件设计人员会对您施加任何其他限制。
评论
根据 ISO IEC 14882-2011 §5.2.6:
后缀 ++ 表达式的值是其操作数的值。[ 注意:获得的值是 原始值 —尾注 ] 操作数应为可修改的左值。操作数的类型应为 算术类型或指向完整对象类型的指针。
因为 nullptr 是指向完整对象类型的指针。所以我不明白为什么这会是未定义的行为。
如前所述,同一文档在§5.2.6/1中也指出:
如果指针操作数和结果都指向同一数组对象的元素,或者指向过去的元素 数组对象的最后一个元素,求值不得产生溢出;否则,行为是 定义。
这个表达似乎有点模棱两可。在我的解释中,未定义的部分很可能是对对象的评估。我想没有人会不同意这种情况。然而,指针算术似乎只需要一个完整的对象。
当然,后缀 [] 运算符和指向数组对象的指针上的减法或乘法只有在它们实际上指向同一个数组时才被很好地定义。最重要的是,人们可能会认为在 1 个对象中连续定义的 2 个数组可以像单个数组一样进行迭代。
所以我的结论是,操作是明确定义的,但评估不会是。
C 标准要求通过标准定义方法创建的对象不能具有等于空指针的地址。然而,实现可能允许存在不是通过标准定义方式创建的对象,并且标准没有说明此类对象是否可能具有与空指针相同的地址(可能是由于硬件设计问题)。
如果一个实现记录了一个多字节对象的存在,其地址将等于 null,那么在该实现上,say 将使 hold 指向该对象的第一个字节 [这将等于 null 指针],并使其指向第二个字节。但是,除非实现记录了此类对象的存在,或者指定它将执行指针算术,就好像存在此类对象一样,否则没有理由期望任何特定行为。让实现故意捕获对 null 指针执行任何类型的算术运算的尝试,而不是添加或减去零或其他 null 指针可能是一种有用的安全措施,并且出于某些预期的有用目的而增加 null 指针的代码将与它不兼容。更糟糕的是,一些“聪明”的编译器可能会决定,在指针上省略 null 检查,即使它们保持 null 也会递增,从而导致各种破坏接踵而至。char *p = (char*)0;
p
p++
评论
arr + 1
0xFF0 + 1*4 = 0xFF4
arr + 2
arr + 4
arr + 5
arr + 4
arr + 5
0x100 + 5*4 = 0x114
int overflows(int x) { return x + 1 < x;}
return false