为什么gcc和clang不会警告写入地址0?

5
以下错误代码:
#include <stdio.h>
#include <string.h>

void isEven (int *isFlag, int num) {
    if (num % 2 == 0) {
        *isFlag = 1;
    } else {
        *isFlag = 0;
    }
}

int main() {
    int num = 4;
    int *isFlag = 0;
    isEven(isFlag, num);
    printf("%d", isFlag);
}  

最近在一个问题中发布了这篇文章。问题本身并不重要,重要的是,尽管gcc和clang警告使用isFlag作为printf()的参数,但它们两者都没有警告写入地址0或空指针。即使使用了-O3确保isEven()函数被内联,并且我还指定了-Wall -Wextra

在这种情况下,难道不应该有一个警告吗?


2
NULL(即(void *)0)可能不是0的确切int表示形式。 - Govind Parmar
3
为什么要这样做?调用函数时不需要检查参数的使用方式。而编译器也没有义务检查函数中是否有isFlag值的测试。并且假设该函数在另一个编译单元中,因此逻辑不能轻松地进行检查。 - Weather Vane
3
在一些处理器上,地址 0 是有效的。 - Fiddling Bits
1
@klutt 你是对的。我原本想说的是在那个地址上可能有实际的内存(例如Flash,RAM)。 - Fiddling Bits
@FiddlingBits:也许吧,但不是在我的机器上的默认目标或者在GodBolt上。另外,看看klutt说了什么。 - einpoklum
显示剩余7条评论
3个回答

5

对空指针进行解引用操作是未定义行为。对于未定义行为,没有要求发出诊断(错误或警告)。因此,从标准的角度来看,对于您的示例不产生任何警告是完全可以的。

毫无疑问,让编译器检测空指针解引用问题很有用,但在某些情况下可能无法检测到。 gcc确实有一个-Wnull-dereference选项,可以检测到您的示例并产生以下结果:

$ gcc -O3 -Wall -Wextra -Wnull-dereference -fsanitize=address test.c
test.c: In function ‘main’:
test.c:14:14: warning: format ‘%d’ expects argument of typeint’, but argument 2 has typeint *’ [-Wformat=]
   14 |     printf("%d", isFlag);
      |             ~^   ~~~~~~
      |              |   |
      |              int int *
      |             %ls
test.c:4:17: warning: null pointer dereference [-Wnull-dereference]
    4 |         *isFlag = 1;
      |         ~~~~~~~~^~~

来自gcc文档

-Wnull-dereference
如果编译器检测到通过解引用空指针触发错误或未定义行为的路径,则发出警告。 此选项仅在-fdelete-null-pointer-checks激活时才生效,大多数目标都启用了该选项,这由优化控制。 警告的精度取决于所使用的优化选项。


嗯...但是这个标志既不在-Wall中,也不在-Wextra中,这不奇怪吗? - einpoklum
-Wall-Wextra 中包含的内容在不同版本的 gcc 中可能不一致。正如文档所述,-Wnull-reference 仅在启用优化时包含(在我的命令行中,实际上是 -O3 启用了它)。 - P.P
我正要说这个。通过静态分析检测错误需要抽象执行的类型,这种类型优化器可以执行以便检测到它。 - Clifford
1
@SergeyA 6.5.3.2地址和间接运算符/4指出:如果将无效值分配给指针,则一元*运算符的行为是未定义的。脚注102进一步澄清了无效指针:*在一元运算符解引用指针的无效值中,包括空指针[..]**。 - P.P
@P.P,不是的。唯一讨论空指针间接引用的时间是在注释2到9.3.4.3(使用最新标准)中,但这不是规范性的,并且已经多次讨论过了。请参见https://stackoverflow.com/questions/1110111/what-part-of-dereferencing-null-pointers-causes-undesired-behavior/1110370#1110370以获取更长的写作。 - SergeyA
显示剩余3条评论

0
"它的原因很简单,编译器还不够智能。虽然有可能让它们变得更加智能,但大多数情况下,编译器构造者认为这并不值得努力。因为最终,编写正确的代码是程序员的责任。"
"实际上,它们已经非常聪明了。正如OznOg在评论中提到的,优化器正在使用这样的技术来使代码更快。但标准并不要求编译器对此发出警告,并且C语言具有非常强烈的专注于程序员的思维方式。它不像Java那样会给你指引。"
"此外,这种类型的警告很容易产生许多误报。在您的代码中,这相当简单,但人们可以很容易地想象出更复杂的分支示例。就像这段代码:"
int *q = malloc(*q);
int *p = 0;
if(n%2 == 0) p = q;
*p = 42;

在最后一行,如果n是奇数,我们将写入NULL,如果n是偶数,则写入q。而q是一个有效的地址。好吧,前提是malloc成功了。这也应该生成警告吗?

对于gcc和可能的其他编译器,有一个选项可以给你这个功能。然而,它不是标准要求的。而且不启用的原因基本上与标准不要求的原因相同。

在这种情况下,正确的处理方式是在函数中添加一个空指针检查。像这样:
void isEven (int *isFlag, int num) {
    if(!isFlag) {
         /* Handle error. Maybe error message and/or exit */
    } else if (num % 2 == 0) {
        *isFlag = 1;
    } else {
        *isFlag = 0;
    }
}

C语言编译器通常不会记住数值。例如,以下代码将生成警告:warning: division by zero

int y = 1/0;

but not this:

int x = 0;
int y = 1/x;

检测这些错误的一种方法是使用--fsanitize=address编译。您不会在编译期间发现这个错误,而是在运行时发现。


1
编译器非常智能,当启用优化时,它会删除对printf的调用,因为它“看到”了未定义的行为。https://godbolt.org/z/T5eh91dj8 - OznOg
@OznOg 确实。我会澄清一下。 - klutt
这只是因为编译器还不够智能化。但至少在简单的情况下,它们是可以做到的。请参见 P.P 的回答。 - einpoklum
@einpoklum,是的,但不够智能化来解决OP在问题中提出的情况。 - klutt
@klutt:不,它足够聪明以应对我在问题中提出的情况。我们只需要给它正确的开关即可。 - einpoklum

0
在这种情况下,难道不应该有一个警告吗?
我本来想说一些关于分离编译等方面的话,但是我发现即使使用标志-Wall -Wextra -pedantic,GCC 8.4也没有诊断main()中指针的解引用。
它应该吗?没有这个要求。行为是未定义的,但它不是一个约束违规,因此C语言不要求实现对其进行诊断。如果编译器确实能够诊断这个问题,那肯定会很有帮助,但这归结为实现质量问题。

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