取消引用指向数组的 NULL 指针在 C 中是否有效?

Nek*_*eko 0 c arrays null pointers dereference

这种行为是否已定义?

volatile long (*volatile ptr)[1] = (void*)NULL;
volatile long v = (long) *ptr;

printf("%ld\n", v);
Run Code Online (Sandbox Code Playgroud)

它起作用是因为通过取消对数组的指针的引用,我们正在接收一个数组本身,然后该数组衰减为指向它的第一个元素的指针。

更新演示:https : //ideone.com/DqFF6T

此外,GCC 甚至将下一个代码视为常量表达式:

volatile long (*ptr2)[1] = (void*)NULL;
enum { this_is_constant_in_gcc = ((void*)ptr2 == (void*)*ptr2) };
printf("%d\n", this_is_constant_in_gcc);
Run Code Online (Sandbox Code Playgroud)

基本上,在编译时取消引用 ptr2;

Mar*_*lli 6

这个:

long (*ptr)[1] = NULL;
Run Code Online (Sandbox Code Playgroud)

正在声明一个指向“1 的数组long”(更准确地说,类型为long int (*)[1])的指针,初始值为NULL。一切正常,任何指针都可以NULL

然后,这个:

long v = (long) *ptr;
Run Code Online (Sandbox Code Playgroud)

正在取消引用NULL指针,这是未定义的行为。所有的赌注都关了,如果你的程序没有崩溃,下面的语句可以打印任何值或真正做任何其他事情。

让我再澄清一次:未定义的行为意味着任何事情都可能发生。没有解释为什么在调用未定义行为后会发生任何奇怪的事情,也不需要解释。编译器可以很好地发出 16 位实模式 x86 程序集,生成一个删除整个主文件夹的二进制文件,发出 Apollo 11 指导计算机程序集代码,或其他任何东西。这不是一个错误。它完全符合标准。


您的代码似乎可以工作的唯一原因是 GCC纯粹出于巧合决定执行以下操作(Godbolt 链接):

long (*ptr)[1] = NULL;
Run Code Online (Sandbox Code Playgroud)

导致NULL-dereference 永远不会真正发生。这很可能是解引用ptr¯\_(?)_/¯ 中未定义行为的结果


有趣的是,我之前在评论中说:

取消引用NULL是无效的,并且基本上总是会导致分段错误。

但是当然,因为“基本上总是”是错误的未定义行为。我认为这是我第一次看到空指针取消引用不会导致 SIGSEGV。


Joh*_*ger 5

这种行为是否已定义?

不是。

long (*ptr)[1] = NULL;
long v = (long) *ptr;

printf("%ld\n", v);
Run Code Online (Sandbox Code Playgroud)

它起作用是因为通过取消对数组的指针的引用,我们正在接收一个数组本身,然后该数组衰减为指向它的第一个元素的指针。

不,您将类型与价值混淆了。*ptr第二行中的表达式确实具有type long[1],但是无论数据类型如何,并且无论将应用于结果的自动转换如何,计算该表达式都会产生未定义的行为。

规范的相关部分是第 6.5.2.3/4 段

一元 * 运算符表示间接。如果操作数指向一个函数,则结果是一个函数指示符;如果它指向一个对象,则结果是一个指定该对象的左值。如果操作数的类型为“指向类型的指针”,则结果的类型为“类型”。如果为指针分配了无效值,则一元 * 运算符的行为未定义。

脚注继续澄清

[...] 一元 * 运算符取消引用指针的无效值包括空指针 [...]

从经验意义上讲,它可能对您“有效”,但从语言的角度来看,任何输出或根本没有输出都是一致的结果。

更新:

有趣的是,明确取地址的答案*ptr与假设数组衰减将克服取消引用的不确定性的答案不同。该标准规定,作为一种特殊情况,当一元运算&符的操作数是一元运算符的结果时,这两个运算*符都不求值。如果满足所有相关约束,结果就好像它们都被完全省略了,只是它永远不是左值。

因此,这是可以的:

long (*ptr)[1] = NULL;
long v = (long) &*ptr;

printf("%ld\n", v);
Run Code Online (Sandbox Code Playgroud)

在许多实现中,它会可靠地打印 0,但请注意,C 没有指定它必须为 0。

这里的关键区别在于,在这种情况下,*不评估操作(根据规范)。在*原始代码中的操作评价,尽管事实上,如果该指针值是有效的,所得到的阵列将被转换右回指针(不同类型的,但相同的位置)。这确实显示了明显的捷径,实现可采取与原代码,他们可能会采取它,如果他们愿意的话,不考虑是否ptr的值是有效的,因为如果是有效的,那么他们可以为所欲为。

  • 不,@NekoNeko。暂时抛开标准明确指出空指针属于“无效值”类别这一事实,您仍然会混淆类型和值。如果指针有效,则该指针将指向一个对象,但并非所有指针值都有效,特别是空指针*根据定义*不指向任何对象。 (2认同)
  • @NekoNeko 不,它不会到处打印 0。这是未定义的行为。*任何*兼容的编译器都可以在取消引用“NULL”后很好地打印“Hello World!”。 (2认同)