以下错误代码:
#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()
的参数,但它们都没有警告如何写入地址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;
| ~~~~~~~~^~~
来自gcc文档:
-Wnull取消引用
如果编译器检测到由于取消引用null指针而触发错误或未定义行为的路径,则发出警告。只有当-fdelete null指针检查为活动,这是由大多数目标中的优化启用的。这个警告的精度取决于所使用的优化选项。
这只是因为编译器不够聪明。可以让它们变得如此聪明,但最有可能的是,编译器构造函数认为这不值得付出努力。因为归根结底,编写正确的代码是程序员的责任。
事实上,他们非常聪明。正如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
有一个编译器选项,可能还有其他编译器选项。但是,这不是标准所要求的。默认情况下不启用的原因与标准不要求的原因基本相同
在这种情况下,正确的处理方法是在函数中添加一个null检查。像这样:
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()
中该指针的取消引用,即使有标志-Wall -Wextra -pedantic
也是如此。
应该吗?没有要求它这样做。行为是未定义的,但它不是违反约束的,所以C不需要实现来诊断它。如果编译器确实诊断出了这一点——也许有些编译器也诊断出了——这肯定会很有帮助,但归根结底是实现质量问题。