问题描述
#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);
}
最近在一个问题中发布在 SO 上。问题本身并不重要,重要的是(对我而言)是 gcc 和 clang 警告使用 isFlag
作为 printf()
的参数,neither of them 警告如何使用地址 0 或空指针被写入。即使指定了 -O3
,也可以确保内联 isEven()
函数,并且当我还指定 -Wall -Wextra
时。
这种情况不应该有警告吗?
解决方法
取消引用空指针是未定义的行为。不需要为未定义的行为发出诊断(错误或警告)。因此,从标准的角度来看,不为您的示例生成任何警告是完全可以的。
毫无疑问,让编译器检测到它会很有用,可能无法在所有情况下都检测到。
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 type ‘int’,but argument 2 has type ‘int *’ [-Wformat=]
14 | printf("%d",isFlag);
| ~^ ~~~~~~
| | |
| int int *
| %ls
test.c:4:17: warning: null pointer dereference [-Wnull-dereference]
4 | *isFlag = 1;
| ~~~~~~~~^~~
-Wnull-取消引用
如果编译器检测到由于取消引用空指针而触发错误或未定义行为的路径,则发出警告。
此选项仅在 -fdelete-null-pointer-checks 为
active,这是通过大多数目标中的优化启用的。这
警告的精确度取决于所使用的优化选项。
这仅仅是因为编译器不够聪明,无法做到这一点。有可能让它们变得如此聪明,但最有可能的是,编译器构造函数根本不认为值得付出努力。因为归根结底,编写正确的代码是程序员的责任。
事实上,他们太聪明了。正如 OznOg 在下面的评论中提到的,优化器正在使用这样的东西来使代码更快。但是标准并没有要求编译器为此发出警告,C 有一种非常注重程序员的心态。它根本不像 Java 那样牵着你的手。
此外,这种警告很容易产生大量误报。在您的代码中,它相当琐碎,但可以轻松想象带有分支的更复杂的示例。喜欢这个代码:
int *q = malloc(*q);
int *p = 0;
if(n%2 == 0) p = q;
*p = 42;
在最后一行,如果 NULL
是奇数,我们将写入 n
,如果 q
是偶数,我们将写入 n
。而 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;
但不是这个:
int x = 0;
int y = 1/x;
检测这些错误的一种方法是使用 --fsanitize=address
进行编译。您不会在编译时发现它,而是在运行时发现它。
这种情况不应该有警告吗?
我正要说一些关于单独编译的内容,等,但我发现 GCC 8.4 也没有诊断出 main()
中那个指针的取消引用,即使有标志 { {1}}。
应该吗?没有要求它这样做。行为未定义,但它不是约束违规,因此 C 不需要实现来诊断它。如果编译器确实诊断出这一点——也许有些人确实做到了——那肯定会有所帮助,但这归结为实现质量问题。