为什么C标准允许这段代码编译而没有报错?

5

这是一段看起来没有错误被接受的代码:

#include <stdio.h>
#include <string.h>

int main() {
    if (strcmp(1, 2))
        printf(3);
}

使用 clang -std=c11 -Weverything 编译会产生4个警告:

badstrcmp.c:5:16: warning: incompatible integer to pointer conversion passing 'int' to parameter of type 'const char *' [-Wint-conversion]
    if (strcmp(1, 2))
               ^
/usr/include/string.h:77:25: note: passing argument to parameter '__s1' here
int      strcmp(const char *__s1, const char *__s2);
                            ^
badstrcmp.c:5:19: warning: incompatible integer to pointer conversion passing 'int' to parameter of type 'const char *' [-Wint-conversion]
    if (strcmp(1, 2))
                  ^
/usr/include/string.h:77:43: note: passing argument to parameter '__s2' here
int      strcmp(const char *__s1, const char *__s2);
                                              ^
badstrcmp.c:6:16: warning: incompatible integer to pointer conversion passing 'int' to parameter of type 'const char *' [-Wint-conversion]
        printf(3);
               ^
/usr/include/stdio.h:259:36: note: passing argument to parameter here
int      printf(const char * __restrict, ...) __printflike(1, 2);
                                       ^
badstrcmp.c:6:16: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
        printf(3);
               ^
badstrcmp.c:6:16: note: treat the string as an argument to avoid this
        printf(3);
               ^
               "%s",
4 warnings generated.

我的问题是为什么C标准允许编译它?这种问题应该被诊断为错误,并拒绝代码。为什么C标准允许程序被翻译?


3
我喜欢clang的幽默感:“警告:格式字符串不是字符串字面量(可能存在安全隐患)” - chqrlie
4
如果你写垃圾内容,就会得到垃圾结果。 - Ed Heal
1
我想象一下,如果你为一个嵌入式系统编写代码,你可以传递硬编码地址 strcmp(0x345625,0x432252),这可能意味着某些含义。 - Jean-François Fabre
2
在嵌入式系统中,必须允许使用硬编码内存位置。允许这样的东西并不是“弱点”,而是C语言的特性:贴近底层。 - Weather Vane
1
C语言起源于一种无类型的、面向字节的语言(https://www.bell-labs.com/usr/dmr/www/chist.html),旨在与几十年前的代码保持向后兼容。使用“-Wall -Werror”编译选项(+可能还有一些降低这个问题的其他选项)可以解决这个问题。 - Petr Skocik
显示剩余12条评论
5个回答

6
你的问题似乎基于一个无效的前提,即C标准以某种方式“允许此代码编译”。实际上,C标准没有“允许或不允许代码编译”的概念。
如果代码无效,则标准要求编译器通过诊断消息告诉您。标准不要求编译器拒绝编译您的代码。它们仍可以以某种实现定义的方式进行编译。
根据C标准,你的代码明显无效。标准C语言不允许隐式整数到指针的转换。你的编译器显然已经通过诊断消息告诉了你。这足以满足标准的要求。
之后一切都将不确定。你的编译器可能会将其编译成“某物”,但这不是符合C程序。它的行为未由该语言定义。
至于您收到的诊断消息的格式(以及它们是否为“警告”或“错误”)-这是一个问题向您的编译器。在C中,这是一个实现质量问题。C标准与此无关。
你可以使用-pedantic-errors标志要求clang将C语言约束违规报告为“错误”。虽然并非完美,但它将使编译器拒绝编译您的代码(如果这是您想要的)。

1
“C标准没有所谓的“允许或不允许代码编译”的概念”:实际上,它确实有,但只适用于非常特定的情况:*4.符合性 4.实现不得成功转换包含#error预处理指令的预处理翻译单元,除非它是被条件性包含跳过的组成部分。” - chqrlie

5

代码应该被拒绝。

这是一个很大的假设。

实际上,引用标准:

只要仍然正确翻译出有效程序,实现可以产生任意数量的诊断。它也可以成功地翻译无效程序。

(5.1.1.3 note 9)

标准没有对编译器在遇到错误时必须执行的操作进行重大限制,原因是在许多情况和许多平台上,“错误”实际上可能是由实现定义的明确定义的扩展行为。从向开发人员提供有用信息的角度来看,在遇到第一个非常微小但仍然符合语法规则的困难点时,继续翻译并在程序的后续部分中捕获其他错误或信息可能会更好,而不是放弃。

它也不愿过多假设“翻译”究竟包含哪些内容(翻译阶段7基本上只是“这里发生了一些事情”)。例如,在严格标准版本的语言中,没有“机器码”的概念(除了像J.5.7这样的扩展),因此标准不能禁止编译器发出它。

4
一方面,标准明确允许整数转换为指针类型(参见C99标准):

6.3.2.3 指针 ... (5) 整数可以转换为任何指针类型。除非在先前规定的情况下,否则结果是实现定义的,可能不会对齐正确,可能不指向所引用类型的实体,并且可能是一个陷阱表示。56)

另一方面,标准要求在这种情况下使用显式转换:

6.5.4 强制类型转换运算符… (3) 涉及指针的转换,除了符合6.5.16.1约束的情况外,都必须通过明确的强制类型转换来指定

因此,printf(3) 明显是不正确的,但它是否产生错误或警告似乎在这种情况下取决于编译器。

绝对不是真的。你的引用只是说语言在适当的上下文中支持整数到指针的转换(例如通过显式转换),但标准明确禁止 隐式整数到指针的转换。6.5.4/3“涉及指针的转换,除了在6.5.16.1的约束允许的情况下,必须通过显式转换来指定。” - AnT stands with Russia
@AnT:好观点。我想知道为什么6.3.2.3(5)没有至少在脚注中提到这一部分(有脚注,但只涉及地址空间)。而且我尝试的编译器实际上接受它而不需要显式转换。有什么想法吗? - Stephan Lechner
实际上,在6.3的一开始就写道:“该子句指定了对这种隐式转换所需的结果,以及由强制转换操作(显式转换)产生的结果。”该句的目的是强调6.3关于转换的结果。 6.3并不意味着允许这些转换隐式发生。 - AnT stands with Russia
然后它说,“6.3.1.8中的列表总结了大多数普通运算符执行的转换;根据6.5中每个运算符的讨论需要进行补充。”也就是说,您应该阅读每个单独运算符的描述,以了解它可以/不能隐式执行的操作。 - AnT stands with Russia

3
如果您不想编译这样的代码,请使用-Werror作为附加标志,这会将每个警告转换为编译错误,您的代码将无法编译。

3
当然,我使用-Werror以及一个经过精心筛选的-Wno-xxx列表...但是不对这些问题进行错误诊断有什么用呢?委员会上的人是想让编程语言成为新手的雷区吗? - chqrlie
1
@chqrlie,C语言和汇编语言都是一个雷区,但正是由于它们的存在,程序员的生活才变得更加容易。 - Weather Vane
不是-Werror,而是-pedantic-errors - AnT stands with Russia

3
#include <stdio.h>
#include <string.h>

int main() {
    if (strcmp(1, 2))
        printf(3);
}

由于传递了错误类型的参数(且没有隐式转换可用),因此对strcmpprintf的调用是约束违规。C标准要求针对任何违反约束或语法规则的程序至少发出一条诊断消息。就标准而言,警告是完全有效的诊断消息。
以下是标准的说法(引用C11标准N1570草案第5.1.1.3节):
“符合规范的实现应该产生至少一个诊断消息(以实现定义的方式标识),如果预处理翻译单元或翻译单元包含任何语法规则或约束的违反,则即使行为也明确指定为未定义或实现定义,也应该发出诊断消息。在其他情况下不需要产生诊断消息。”
标准实际上只有一个情况需要拒绝源文件,那就是它包含一个#error指令。第4段,第4节:
实现不应成功地翻译包含#error预处理指令的预处理翻译单元,除非它是被条件包含跳过的一部分。
我个人更喜欢gcc和clang拒绝违反语法规则或约束的程序,但正如我所说,标准允许非致命警告。如果您希望拒绝此类程序,可以使用各种选项,例如-std=c11 -pedantic-errors。请注意,gcc和clang默认情况下并不完全符合C标准,但这些非致命警告不是该不符合标准的例子。

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