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

Is incrementing a null pointer well-defined?

提问人:Luchian Grigore 提问时间:4/23/2015 更新时间:7/3/2018 访问量:5182

问:

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

以下操作是否定义明确?

int* p = 0;
p++;
C++ 指针 language-lawyer

评论

8赞 Borgleader 4/23/2015
我很好奇为什么你认为它不会......
3赞 chris 4/23/2015
@ddriver,作为未定义的行为,它不必知道。可以假设您遵守规则并且不会产生 UB。
4赞 chris 4/23/2015
@ddriver,好吧,想象一下,如果你溢出0x1000,实现陷阱。您有一个位于 0xFF0 的 3 个四字节整数的数组。 将实现为 .同样,对于通过 .现在想象一下.过去0x1000有陷阱,但这没关系,因为它是未定义的行为。将数组放在更远的内存中是不行的,以免陷阱。现在想象一下它在0x100。 实现为 。它超出了范围,但没有陷阱。这也是可以的,因为它是未定义的行为,但“有效”arr + 10xFF0 + 1*4 = 0xFF4arr + 2arr + 4arr + 5arr + 4arr + 50x100 + 5*4 = 0x114
7赞 CodesInChaos 4/23/2015
@ddriver 优化编译器喜欢假设“UB永远不会发生”。因此,他们可以自由地假设调用 UB 的代码是无法访问的,并继续利用该矛盾销毁所有内容。
5赞 Voo 4/24/2015
@ddriver 如果你认为 UB 只有在根据所使用的架构“有意义”时才能产生问题,那么你至少落后于编译器十年。示例:x86 有 2s 补码算术,所以应该总是给你正确的结果,对吧?在现代 gccs 上,您很有可能将函数优化为 .int overflows(int x) { return x + 1 < x;}return false

答:

36赞 Columbo 4/23/2015 #1

§5.2.6/1:

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

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

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

评论

6赞 user3528438 4/23/2015
我很好奇标准中是如何定义“数组对象”的。是否考虑数组对象的返回值?malloc
0赞 Lingxi 4/23/2015
如何保证指针和整数之间的映射具有相应的指针表示形式,否则是实现定义的?1
1赞 Columbo 4/23/2015
@Lingxi 这句话的第一部分说你可以转换它。生成的值是 impemention 定义的,但操作的合法性是有保证的,不是吗?(实际上,它应该是 sizeof(int),忘记调整它 - 尽管如此,从 1 转换为它应该是有效的。
1赞 supercat 4/23/2015
@Columbo:实现可以合法地指定,在转换为指针时,它识别为从来都不是指针到整数转换的结果的任何整数值都可能产生陷阱表示形式。
0赞 jxh 4/24/2015
如果假设对它的调用导致持有非 NULL 指针,那么 UB 也会增加它。Foo *p = static_cast<Foo *>(malloc(0));pp
13赞 Peter 4/23/2015 #2

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

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

NULL 指针不指向任何内容,因此递增它会产生未定义的行为(“否则”子句适用)。

评论

2赞 Columbo 4/23/2015
它们可以指向一个对象,也可以指向一个对象过去的一个字节。
1赞 dhein 4/23/2015
@Columbo:你指的是什么?
0赞 CodesInChaos 4/23/2015
@Zaibis 可能是指向数组末尾的一个元素的指针有效的规则,即当您有一个 时,尽管没有指向有效元素,但该规则是有效的。但是谈论“一个字节”有点奇怪。a+3int a[3]
0赞 Marc van Leeuwen 4/24/2015
你把零加错了;在加法运算中,添加零时不会出现异常,对于无效的指针值,可能会导致 UB。这就是为什么在某些情况下不严格等同于(当该位置的内容未实际使用时)的原因之一,例如请参阅此答案p[0]*p
2赞 Peter 4/24/2015
你说的对 C 是正确的,但对 C++ 不是真的,Marc。C++98 的第一句话 - 第 5.7 节,第 8 段说“如果将值 0 添加到指针值或从指针值中减去,则结果等于原始指针值。(我现在手头没有最新版本的 C++ 标准来检查部分编号)。安德鲁·科尼格(Andrew Koenig)有一篇文章讨论了为什么,drdobbs.com/cpp/why-does-c-allow-arithmetic-on-null-poin/......
0赞 Serge Ballesta 4/23/2015 #3

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

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

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

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

给出 ,表示在 32 位实现上的值为 4,在 64 位实现上的值为 80p

换一种说法:

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

评论

7赞 CodesInChaos 4/23/2015
我不会相信现代优化编译器。例如,它很有可能决定在条件中永远不会得到满足(因为 p==null 会导致第一个语句的 UB)并删除整个语句。p2=p+1; if(p==nullptr){...}if
0赞 Serge Ballesta 4/23/2015
正如我在第一行所说,它绝对UB。但是我找不到程序、编译器和参数的示例来展示您给出的问题。
0赞 MSalters 4/24/2015
@SergeBallesta:GCC,甚至默认(这就是为什么有一个标志)-fno-delete-null-pointer-checks
0赞 CodesInChaos 4/24/2015
@MSalters我不确定当前的编译器是否检测为 UB,或者它们是否仅在您取消引用指针时才这样做。但是,无论他们现在是否检测到它,从优化取消引用案例步骤到“优化”此案例也只是一小步。p++
0赞 MSalters 4/24/2015
@CodesInChaos:在最低级别检测它会简单得多。GCC 案例因在相当于 .在硬件级别,两者都只是指针增量。一个添加字节,另一个.int p = foo->barsizeof(*p)offsetof(cFoo, bar)
-2赞 Microprocessor Cat 4/23/2015 #4

鉴于您可以递增任何定义明确大小的指针(因此任何不是空指针的指针),并且任何指针的值都只是一个地址(一旦存在,就没有对 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 指针的值取决于平台。在语言中使用零来初始化指针变量是一种语言定义。

评论

1赞 supercat 4/23/2015
一个好的实现应该阻止任何在运行时从空指针中添加或减去任何整数的尝试(“十亿美元错误”的大部分危害源于平台的失败)。很少有平台需要这种行为,在实践中,这种行为几乎总是伴随着杂散的内存访问。空指针上的隐含算术唯一有用的情况是完全在编译时可解析为两个指针之间的差值的情况下,这两个指针与公共基的位移恒定。
3赞 Luchian Grigore 4/24/2015
你认为它是正确的,因为它是直观的或适用于你的系统是错误的。C++ 由标准描述的语言规则控制。有些规则是违反直觉的,但它们背后的原因是它允许实现执行某些优化,否则这是不可能的。
0赞 gnasher729 4/24/2015
@LuchianGrigore:还有一个问题是,如果递增空指针不是未定义的行为,那么它必须以某种方式定义。好吧,我不想负责定义递增空指针的作用。微处理器 Cat 声称结果被定义为“如果你递减它,就会产生一个空指针的东西”。
0赞 Microprocessor Cat 4/24/2015
我想知道为什么这个答案被否决了。是代码特别不正确,还是我的结论?
0赞 Fattie 4/24/2015
微观 - 你的答案是关于你对如何或什么是明智的想法。(你可能是完全正确的,也可能不是完全正确的。这个问题与“什么是明智的”完全无关。这只是一个“语言法”问题。
-1赞 Joshua 4/24/2015 #5

事实证明,它实际上是未定义的。有些系统确实如此

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

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

评论

0赞 Joshua 4/24/2015
它将 p 的值转换为整数。需要奇怪的表达式来防止编译器生成代码以将 NULL 替换为 0。实际的按位值与此处相关。
2赞 Peter 4/24/2015
这不是递增 NULL。它正在重新分配指针的值。
16赞 gnasher729 4/24/2015 #6

对“未定义行为”的含义的理解似乎很低。

在 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 指针。

评论

1赞 Fattie 4/24/2015
这似乎是页面上唯一的答案,它实际上知道“未定义的行为”意味着什么。
5赞 Tony Delroy 4/24/2015
这种咆哮甚至没有试图解决那个东西 - 啊 - 它叫什么......哦,是的 - 问题,这不是“未定义的行为如何表现出来”,而是问题中的代码是否具有未定义的行为。 (公平地说,它最终确实达成了一个讨论,该讨论的前提是增加未定义的空指针而不说明或证明这一点。这个答案最好移到一个适当的问题上......
-1赞 The Software Barbarian 4/24/2015 #7

回到有趣的 C 时代,如果 p 是指向某物的指针,那么 p++ 实际上是将 p 的大小添加到指针值中,以使 p 指向下一个某物。如果将指针 p 设置为 0,那么 p++ 仍然会通过向其添加 p 的大小来将其指向下一件事。

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

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

评论

2赞 Kos 4/24/2015
难道不是相反吗?C 标准没有定义它,但编译器供应商可以自由地为特定平台定义它。
0赞 The Software Barbarian 4/24/2015
我只记得我以前的K&R在谈到指针时说的话,这就是它们应该工作的方式。如果编译器供应商让它不能直观地工作,那么我可能不会使用那个编译器,除非我的胳膊被粗暴地扭曲了。:-)
1赞 Kos 4/24/2015
OTOH,当时你还没有从编译器那里得到这种级别的优化。权衡,权衡......
0赞 Luchian Grigore 4/24/2015
问题是关于 C++ 的。“真实答案”是标准定义或未定义的。
0赞 The Software Barbarian 4/30/2015
据我了解,C 中的指针和 C++ 中的指针的定义是相同的。无论如何,没有指针算术差异。分配指向 0 的指针是合法的 - 例如,您的代码实际上可以使用它来检测链的末端。递增指针应该会把你带到数组中的下一个东西,不要对指针值的解释做出任何假设。优化是该死的,如果我编写一个算法来使用指针在对象列表周围反弹以特定方式工作,最好不要让法律代码停止工作。
0赞 laurisvr 5/20/2015 #8

根据 ISO IEC 14882-2011 §5.2.6

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

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

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

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

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

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

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

0赞 supercat 7/3/2018 #9

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

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