首先,我知道这种编程方式不是很好的做法。为了解释我为什么要这样做,请在实际问题之后继续阅读。
在C中声明一个函数时:
int f(n, r) {…}
r
和n
的类型默认为int
。编译器可能会生成关于它的警告,但让我们选择忽略它。
现在假设我们打电话给f
,但是,不小心或其他情况下,遗漏了一个论点:
f(25);
这仍将编译just fine(使用gcc和clang进行测试)。但是gcc没有关于缺失参数的警告。
所以我的问题是:
请注意,当我声明int f(int n, int r) {…}
,gcc和clang will compile this时,它的工作方式不同。
现在,如果您想知道为什么我会这样做,我正在玩Code Golf并尝试缩短使用递归函数f(n, r)
的{{3}}。我需要一种隐式调用f(n, 0)
的方法,所以我定义了F(n) { return f(n, 0) }
,这对我来说有点太多字节。所以我想知道我是否可以省略这个参数。我不能,它仍然编译但不再有效。
在优化此代码时,有人向我指出,我可以在功能结束时省略return
- gcc也没有发出关于此问题的警告。 gcc是否过于宽容?
答案 0 :(得分:4)
您没有从编译器获得任何诊断信息,因为您没有使用现代"原型"功能声明。如果你写了
int f(int n, int r) {…}
然后后续的f(25)
将触发诊断。使用计算机上的编译器我打开它,实际上是一个很难的错误。
"旧式"函数声明和定义有意地导致编译器放松它的许多规则,因为它们为了向后兼容而存在的旧式代码会在所有的时间内完成这样的事情。不是你想要做的事情,希望f(25)
以某种方式被解释为f(25, 0)
,但是,例如,f(25)
f
的身体从未看过r
n
参数为25时的f(25)
参数。
当他们说字面上任何可能发生时(在计算机的物理能力范围内,无论如何;"恶魔将飞出你的鼻子"是一个规范的笑话,但实际上它是一个笑话)。但是,可以描述通常发生的两类一般事物。
对于较旧的编译器,通常会发生的事情是,为f
生成代码,就像f
仅使用一个参数一样。这意味着f(25)
将查找其 second 参数的内存或寄存器位置未初始化,并包含一些垃圾值。
另一方面,对于较新的编译器,编译器可能会发现任何通过main: ret
的控制流路径都有未定义的行为,并且基于该观察,假设所有这样的控制 - 永远不会采用流路径,并删除它们。是的,即使它是程序中唯一的控制流路径。我实际上目睹过Clang吐出f(n, r) { /* no return statement */ }
一个程序,其控制流路径都有未定义的行为!
GCC不抱怨void
是另一种情况,如(1),其中旧式函数定义放宽了规则。 -Wall
是1989年C标准发明的;在此之前,没有办法明确表示函数不返回值。因此,您无法获得诊断,因为编译器无法知道您并不意味着这样做。
独立于此,是的,GCC的默认行为 是非常宽松的现代标准。这是因为GCC本身比1989 C标准更早,并且没有人在很长一段时间内重新检查其默认行为。对于新程序,您应始终使用-Wextra
,我建议您至少尝试-Wpedantic
,-Wstrict-prototypes
,-Wwrite-strings
和-std=c11
。事实上,我建议通过"警告选项"本手册的一部分,并使用所有附加警告选项进行试验。 (但请注意,您应不使用-std=gnu11
,因为这样会破坏系统标题。请改用leveldb
。)
答案 1 :(得分:2)
首先,C标准不区分警告和错误。它只涉及“诊断”。特别是,编译器总是可以生成可执行文件(即使源代码完全被破坏)而不违反标准。 1
r
和n
的类型默认为int
。
不再。自1999年以来,隐式int
已从C中消失。(并且您的测试代码需要C99,因为for (int i = 0; ...
在C90中无效。)
在您的测试代码中,gcc会为此发出诊断:
.code.tio.c: In function ‘f’:
.code.tio.c:2:5: warning: type of ‘n’ defaults to ‘int’ [-Wimplicit-int]
这不是有效的代码,但gcc仍会生成可执行文件(除非您启用-Werror
)。
如果添加所需类型(int f(int n, int r)
),则会显示下一个问题:
.code.tio.c: In function ‘main’:
.code.tio.c:5:3: error: too few arguments to function ‘f’
这里gcc有点随意决定不生成可执行文件。
来自C99的相关引文(也可能是C11; n1570 draft中此文字未发生变化):
6.9.1功能定义
<强>约束强>
[...]
- 如果声明者包含标识符列表,则声明列表中的每个声明都应该是 至少有一个声明者,那些声明者只能声明来自的标识符 标识符列表,标识符列表中的每个标识符都应声明。
醇>
您的代码违反了约束(您的函数声明符包含标识符列表,但没有声明列表),这需要诊断(例如来自gcc的警告)。
<强>语义强>
- [...]如果 声明者包含一个标识符列表,参数的类型应在一个声明中声明 以下声明清单。
醇>
您的代码违反了此 规则,因此它具有未定义的行为。即使从未调用该函数,这也适用!
6.5.2.2函数调用
<强>约束强>
[...]
- 如果表示被调用函数的表达式具有包含原型的类型,则 参数的数量应与参数的数量一致。 [...]
醇><强>语义强>
[...]
- [...]如果参数的数量不等于参数的数量,则 行为未定义。 [...]
醇>
如果传递的参数数量与函数的参数数量不匹配,实际调用也会有未定义的行为。
省略return
:只要调用者没有查看返回的值,这实际上是有效的。
参考(6.9.1函数定义,语义):
- 如果到达终止函数的
醇>}
,则使用函数调用的值 调用者,行为未定义。
1 唯一的例外似乎是#error
指令,标准说明了该指令:
实施不能成功翻译预处理翻译单元 包含
#error
预处理指令,除非它是跳过的组的一部分 条件包含。