简体   繁体   English

当我在C语言中忽略函数的参数时会发生什么?

[英]What happens when I leave out an argument to a function in C?

First of all, I know this way of programming is not good practice. 首先,我知道这种编程方式不是一种好习惯。 For an explanation of why I'm doing this, read on after the actual question. 有关我为什么这样做的解释,请在实际问题之后继续阅读。

When declaring a function in C like this: 像这样在C中声明一个函数时:

int f(n, r) {…}

The types of r and n will default to int . rn的类型将默认为int The compiler will likely generate a warning about it, but let's choose to ignore that. 编译器可能会对此发出警告,但让我们选择忽略它。

Now suppose we call f but, accidentally or otherwise, leave out an argument: 现在假设我们调用f但是偶然地或以其他方式忽略了一个参数:

f(25);

This will still compile just fine (tested with both gcc and clang). 这样仍然可以正常编译(使用gcc和clang进行测试)。 However there is no warning from gcc about the missing argument. 但是,gcc没有提供有关缺少参数的警告。

So my question is: 所以我的问题是:

  1. Why does this not produce a warning (in gcc) or error? 为什么这不会产生警告(以gcc表示)或错误?
  2. What exactly happens when it is executed? 执行该命令到底会发生什么? I assume I'm invoking undefined behaviour but I'd still appreciate an explanation. 我假设我正在调用未定义的行为,但是我仍然希望得到一个解释。

Note that it does not work the same way when I declare int f(int n, int r) {…} , neither gcc nor clang will compile this . 请注意,当我声明int f(int n, int r) {…}时,它的工作方式不同,gcc和clang 都不会编译它

Now if you're wondering why I would do such a thing, I was playing Code Golf and tried to shorten my code which used a recursive function f(n, r) . 现在,如果你想知道我为什么会做这样的事情,我是打码高尔夫球场 ,并试图缩短我的代码,它使用了递归函数f(n, r) I needed a way to call f(n, 0) implicitly, so I defined F(n) { return f(n, 0) } which was a little too many bytes for my taste. 我需要一种隐式调用f(n, 0) ,因此我定义了F(n) { return f(n, 0) } ,这对我来说有点太多了。 So I wondered whether I could just omit this parameter. 所以我想知道是否可以忽略此参数。 I can't, it still compiles but no longer works. 我不能,它仍然可以编译,但不再起作用。

While optimizing this code, it was pointed out to me that I could just leave out a return at the end of my function – no warning from gcc about this either. 在优化此代码时,向我指出,我可以在函数末尾留下一个return -gcc对此也没有警告。 Is gcc just too tolerant? gcc太宽容了吗?

  1. You don't get any diagnostics from the compiler because you are not using modern "prototyped" function declarations. 您没有从编译器获得任何诊断信息,因为您没有使用现代的“原型化”函数声明。 If you had written 如果你写过

     int f(int n, int r) {…} 

    then a subsequent f(25) would have triggered a diagnostic. 那么随后的f(25) 触发诊断。 With the compiler on the computer I'm typing this on, it's actually a hard error. 使用计算机上的编译器输入时,实际上是一个硬错误。

    "Old-style" function declarations and definitions intentionally cause the compiler to relax many of its rules, because the old-style code that they exist for backward compatibility with would do things like this all the dang time. “旧式”函数的声明和定义有意使编译器放宽其许多规则,因为为向后兼容而存在的旧式代码将在整个过程中保持这种状态。 Not the thing you were trying to do, hoping that f(25) would somehow be interpreted as f(25, 0) , but, for instance, f(25) where the body of f never looks at the r argument when its n argument is 25. 不是您要尝试执行的操作,而是希望f(25)会以某种方式解释为f(25, 0) ,但是,例如f(25) ,其中f的主体在n时从不看r参数参数是25。

  2. The pedants commenting on your question are pedantically correct when they say that literally anything could happen (within the physical capabilities of the computer, anyway; "demons will fly out of your nose" is the canonical joke, but it is, in fact, a joke). 当评论者说您可能发生任何事情时,他们在评论您的问题时是正确的(反正在计算机的物理功能范围内;“恶魔会飞出您的鼻子”是典型的笑话,但实际上,这是一个笑话)。玩笑)。 However, it is possible to describe two general classes of things that are what usually happens. 但是,可以描述通常发生的两种一般事物。

    With older compilers, what usually happens is, code is generated for f(25) just as it would have been if f only took one argument. 对于较旧的编译器,通常会发生的情况是,为f(25)生成代码,就像f仅接受一个参数那样。 That means the memory or register location where f will look for its second argument is uninitialized, and contains some garbage value. 这意味着f将在其中寻找第二个参数的内存或寄存器位置未初始化,并且包含一些垃圾值。

    With newer compilers, on the other hand, the compiler is liable to observe that any control-flow path passing through f(25) has undefined behavior, and based on that observation, assume that all such control-flow paths are never taken , and delete them. 另一方面,对于较新的编译器,编译器可能会观察到通过f(25)任何控制流路径都具有未定义的行为,并根据该观察结果, 假设所有此类控制流路径都不会被采用 ,并且删除它们。 Yes, even if it's the only control-flow path in the program. 是的,即使它是程序中唯一的控制流路径。 I have actually witnessed Clang spit out main: ret for a program all of whose control-flow paths had undefined behavior! 我实际上已经亲眼目睹了Clang吐出main: ret退出程序的所有控制流路径都具有未定义的行为!

  3. GCC not complaining about f(n, r) { /* no return statement */ } is another case like (1), where the old-style function definition relaxes a rule. GCC不抱怨f(n, r) { /* no return statement */ }是另一种情况,如(1),其中旧式函数定义放宽了规则。 void was invented in the 1989 C standard; void是在1989 C标准中发明的; prior to that, there was no way to say explicitly that a function does not return a value. 在此之前,没有办法明确说一​​个函数不返回值。 So you don't get a diagnostic because the compiler has no way of knowing that you didn't mean to do that. 因此,您不会得到诊断,因为编译器无法知道您不是故意这样做的。

    Independently of that, yes, GCC's default behavior is awfully permissive by modern standards. 独立的,是的,GCC的默认行为按现代标准非常宽松。 That's because GCC itself is older than the 1989 C standard and nobody has reexamined its default behavior in a long time. 这是因为GCC本身早于1989 C标准,并且长期以来没有人重新检查其默认行为。 For new programs, you should always use -Wall , and I recommend also at least trying -Wextra , -Wpedantic , -Wstrict-prototypes , and -Wwrite-strings . 对于新程序,应该始终使用-Wall ,并且我还建议至少尝试-Wextra-Wpedantic-Wstrict-prototypes-Wwrite-strings In fact, I recommend going through the "Warning Options" section of the manual and experimenting with all of the additional warning options. 实际上,我建议您阅读本手册的“警告选项”部分,并尝试所有其他警告选项。 (Note however that you should not use -std=c11 , because that has a nasty tendency to break the system headers. Use -std=gnu11 instead.) (但是请注意,你应该使用-std=c11 ,因为有一个讨厌的倾向,打破系统的头。使用-std=gnu11代替。)

First off, the C standard doesn't distinguish between warnings and errors. 首先,C标准无法区分警告和错误。 It only talks about "diagnostics". 它只谈论“诊断”。 In particular, a compiler can always produce an executable (even if the source code is completely broken) without violating the standard. 特别是,编译器始终可以在不违反标准的情况下生成可执行文件(即使源代码已完全损坏)。 1 1个

The types of r and n will default to int . rn的类型将默认为int

Not anymore. 不再。 Implicit int has been gone from C since 1999. (And your test code requires C99 because for (int i = 0; ... isn't valid in C90). 自1999年以来,隐式int就不再使用C。(并且您的测试代码要求使用C99,因为for (int i = 0; ...在C90中无效)。

In your test code gcc does issue a diagnostic for this: 在您的测试代码中,gcc会为此发出诊断:

.code.tio.c: In function ‘f’:
.code.tio.c:2:5: warning: type of ‘n’ defaults to ‘int’ [-Wimplicit-int]

It's not valid code, but gcc still produces an executable (unless you enable -Werror ). 这不是有效的代码,但是gcc仍会生成一个可执行文件(除非启用-Werror )。

If you add the required types ( int f(int n, int r) ), it uncovers the next issue: 如果添加所需的类型( int f(int n, int r) ),它将发现下一个问题:

.code.tio.c: In function ‘main’:
.code.tio.c:5:3: error: too few arguments to function ‘f’

Here gcc somewhat arbitrarily decided not to produce an executable. 在这里,gcc任意决定不产生可执行文件。

Relevant quotes from C99 (and probably C11 too; this text hasn't changed in the n1570 draft ): C99的相关引号(可能还有C11; n1570草案中的此文本未更改):

6.9.1 Function definitions 6.9.1函数定义

Constraints 约束条件

[...] [...]

  1. If the declarator includes an identifier list, each declaration in the declaration list shall have at least one declarator, those declarators shall declare only identifiers from the identifier list, and every identifier in the identifier list shall be declared. 如果声明者包括一个标识符列表,则声明列表中的每个声明应至少具有一个声明者,这些声明者应仅声明标识符列表中的标识符,并且应声明标识符列表中的每个标识符。

Your code violates a constraint (your function declarator includes an identifier list, but there is no declaration list), which requires a diagnostic (such as the warning from gcc). 您的代码违反了约束(您的函数声明器包括标识符列表,但没有声明列表),该约束需要诊断(例如来自gcc的警告)。

Semantics 语义学

  1. [...] If the declarator includes an identifier list, the types of the parameters shall be declared in a following declaration list. [...]如果声明符包括标识符列表,则应在以下声明列表中声明参数的类型。

Your code violates this shall rule, so it has undefined behavior. 您的代码违反本法应当裁定,所以它不确定的行为。 This applies even if the function is never called! 即使从未调用该函数也是如此!

6.5.2.2 Function calls 6.5.2.2函数调用

Constraints 约束条件

[...] [...]

  1. If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. 如果表示被调用函数的表达式的类型包括原型,则参数的数量应与参数的数量一致。 [...] [...]

Semantics 语义学

[...] [...]

  1. [...] If the number of arguments does not equal the number of parameters, the behavior is undefined. [...]如果参数数量不等于参数数量,则行为未定义。 [...] [...]

The actual call also has undefined behavior if the number of arguments passed doesn't match the number of parameters the function has. 如果传递的参数数量与函数具有的参数数量不匹配,则实际调用也具有未定义的行为。

As for omitting return : This is actually valid as long as the caller doesn't look at the returned value. 至于省略return :这实际上是有效的,只要调用者不查看返回的值即可。

Reference (6.9.1 Function definitions, Semantics): 参考(6.9.1函数定义,语义):

  1. If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined. 如果到达终止函数的} ,并且调用者使用了函数调用的值,则该行为未定义。

1 The sole exception seems to be the #error directive, about which the standard says: 1唯一的例外似乎是#error指令,标准规定:

The implementation shall not successfully translate a preprocessing translation unit containing a #error preprocessing directive unless it is part of a group skipped by conditional inclusion. 该实现不应成功地转换包含#error预处理指令的预处理转换单元,除非它是被有条件包含跳过的组的一部分。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM