null指针间接引用数组类型是否UB?

4

考虑以下代码:

#include <stdio.h>

int *f(int (*p)[2])
{
    return *p; //Possible UB here?
}

int main()
{
    printf("%p", f(NULL));
}

将间接应用于空指针是否会创建UB?

也许不会,因为数组类型的lvalue被转换回指针,并且实际未访问任何对象值。在这种情况下哪种说法是正确的?

编辑:我很清楚什么是UB。我只想知道使用标准文件证明或解释上述代码为什么是或不是UB。


3
是的,对空指针进行解引用操作会导致未定义行为。然而,未定义行为的定义是其结果没有任何限制。因此,特定的编译器可能会执行您所描述的操作,也可能不会。 - Peter
1
数组类型的左值...嗯...那是什么? - Sourav Ghosh
1
@Peter 当然是的。*p 是一个数组而不是指针。你可以通过尝试分配它并阅读产生的错误,或使用 sizeof 来检查它。它也是一个左值。 - 2501
1
@SouravGhosh 正是它所说的。 - 2501
1
@SouravGhosh 我是在回应你的引用。我很惊讶你不知道什么是数组或lvalue。 - 2501
显示剩余12条评论
3个回答

3

如我在评论中所说,是的。任何对空指针的引用都会导致未定义的行为。

你需要意识到的是,未定义的行为意味着标准对结果没有任何要求或限制。

这意味着当代码行为未定义时,实现可以自由地表现出你所描述的行为 - 或者不是这样。它不是在这种方式下被要求表现或不表现。

编译器的行为对于确定什么是未定义行为和什么不是未定义行为并不相关。


谢谢你的回答,但我当然知道什么是未定义行为。我只是想要使用标准文献来解释这种行为。为什么“将具有类型‘‘类型数组’’的表达式转换为具有类型‘‘类型指针’’的表达式”这一事实不能使其变得明确定义(例如)?为什么没有“数组类型的lvalue”这样的东西? - AnArrayOfFunctions
2
这是解除引用导致未定义行为。将结果转换为另一种类型并不改变产生未定义行为的事实。 - Peter
@Peter:请看一下我的回答,并注意标准中明确允许取消引用空指针,前提是只取其地址。 - Serge Ballesta

0

NULL是一个空指针常量,试图解引用一个(任何)空指针(无效内存)将导致UB。

因此,理论上,我们不能解引用包含NULL的任何指针。

在这里,p是一个指针,p == NULL*p是一种尝试解引用的方法。因此,它会调用未定义行为

值得一提的是,NULL的主要用途之一是提供一个有效的值来检查并停止解引用一个持有NULL的指针。


0

首先简短回答:许多人认为这显然是未定义行为,即使我认为意图是清晰的,我也找不到标准中允许表达式的参考。因此,根据标准,行为是未定义的。

但是如下所述,将指向数组的指针解引用等同于将指针强制转换为数组的第一个元素。而且标准通过解释说明该强制转换完全被定义,因为如果指针指向真正的数组,则在数组地址处的内容就是数组的第一个元素。如果指针为空,则明确允许将空指针转换为另一类型的空指针。因此,只需替换该行:

return *p;

因为标准没有明确指定应该如何处理:

return (int *) p; // no UB here even if p is null!

这可以用于指向任何类型的数组,包括多维数组:解引用可以安全地替换为对立即底层子数组的转换。


这是一个有趣的边界情况。在我看来,标准并不清楚它是否属于未定义行为。以下是一些提示,可能表明它是从C99的草案n1256或C11的草案n1570中,6.5.3.2地址和间接运算符(所有强调都是我的):

§4一元*运算符表示间接寻址...如果操作数具有“指向类型”的类型,则结果具有“类型”类型。如果已将无效值分配给指针,则一元*运算符的行为是未定义的

关于该部分的注释坚持认为:

通过一元*运算符对指针进行取消引用的无效值之一是空指针...


但这并不是很清楚,因为数组是一种派生类型,是一个不可修改的lvalue,并且只能在两个上下文中使用:

  • 它可以被转换(衰减)为其基础类型的指针

  • 它可以与[]后缀运算符一起使用,以构建对其元素之一的lvalue

使用*p [i]肯定会导致UB,因为我们首先在空指针上进行算术运算,然后解引用结果。这里毫无疑问

但在所示代码中(return *p;),我们处于第一个上下文中,这意味着我们只将数组转换为指针。同一段落中的同一注释(on same paragraph)说:

因此,&*E等同于E(即使E是空指针)...

由于p是指向数组的指针,因此应该应用多维数组的语义。同一标准的第6.5.2.1节“数组下标”明确说明了多维数组的情况:

§3 连续的下标运算符指定多维数组对象的一个元素。如果 E 是一个 n 维数组(n≥2),其维度为 i×j×...×k,则 E(用作非左值)被转换为一个指向具有维度 j×...×k 的 (n-1) 维数组的指针。如果对此指针显式地或隐式地应用一元 * 运算符,结果就是指向的 (n-1) 维数组。
在我看来,这清楚地说明了 *p 就是 (int *) p,因此当函数 f 收到空指针时,需要返回一个空指针。
但是这里引用的第一条评论让人认为任何应用于空指针的 * 运算符都会导致未定义行为。同一评论的第二部分证明了这是错误的,但是评论并不是规范性的。因此,为了避免被未来版本的优化编译器积极追踪可能的未定义行为而受到伤害,我会将其视为未定义行为,并且永远不会在实际代码中使用它,即使我真的认为它是允许的。

注意:我知道注释不是规范的一部分,但它们存在是为了帮助我们理解标准。所以当一个注释明确说&*E等同于E(即使E是一个空指针)时,这实际上意味着只要结果仍然用于地址,对空指针应用运算符 * 不一定会导致未定义行为。


感谢您的关注,这是一个有趣的案例,特别是因为6.5.3.2/4非常清晰,脚注也非常清晰,但两者却完全相互矛盾。人们可能会将脚注解读为“如果运算符紧接着&运算符,则不会真正应用运算符”(反之亦然)。实际上,通过在f()中进行空指针检查,可以轻松解决这个问题。 - Peter - Reinstate Monica
1
p 是指向数组的指针,而不是数组本身。当你将 NULL 传递给 f 时,评估 *p 将会导致空指针引用。 - Virgile
1
@Peter:即使某些事情在标准中未定义,实现也可以选择支持它。众所周知,GCC支持许多扩展功能。当标准在某一点上不是非常明确时,每个实现都必须决定它要做什么。C语言没有参考实现,因此标准有时会含糊不清... - Serge Ballesta
@SergeBallesta:问题可能应该分成几个子部分:一个理智的“正常”编译器是否有理由将某些内容视为UB,一个晦涩难懂的编译器是否有理由将某些内容视为UB,以及编译器可能将某些内容视为UB的危险程度是否足够高,程序员应该避免它,并且应该检测到它的消毒编译器。这个答案似乎回答了第一个问题。 - supercat
@SergeBallesta:值得注意的是,C89标准的作者们认为没有理由避免某些行为的答案是“否”和“是”,因为他们并不希望编译器会特意利用未定义行为。后来的标准版本则基于这样一种观念,即某些事情“本来就是”未定义行为,即使程序员和编译器编写者都认为它们只是因为标准的粗心而“技术上”成为未定义行为。 - supercat
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接