C99中的func()和func(void)有什么区别?

73

void func()

实际上,参数为空表示接受任何参数。

void func(void)不接受任何参数。

但在标准C99中,我找到了以下内容:

6.7.5.3 函数声明符(包括原型)
14 标识符列表仅声明函数参数的标识符。函数声明符中的空列表是该函数定义的一部分,指定该函数没有参数。 函数声明符中的空列表不是该函数定义的一部分,指定未提供有关参数数量或类型的信息。

根据标准,func()func(void)是相同的吗?


2
我不知道标准是怎样的,但实际上显然不是这样的。 - Stargateur
1
我认为标准很清楚地表明它们是不同的:未提供信息与不应提供参数不同。 - Margaret Bloom
@Margaret,在粗体部分,如果声明符是定义的一部分,则表示没有参数。 - liusrichard
作为函数原型,void funct()void func(void)是不同的。但当它们作为定义的一部分时,它们是相同的。void func() { ... }void func(void) { ... }都不带参数。 - haccks
可能是重复的问题,参考in c: func(void) vs. func() - Yousha Aleayoub
4个回答

77

简而言之

在声明中,

void func1();     // obsolescent
void func2(void);

行为差异很大。第一个声明的函数没有任何原型 - 它可以接受任意数量的参数!而后者声明了一个带有原型的函数,该函数不带参数且不接受任何参数。

定义

void func1() { }     // obsolescent

void func2(void) { }
  • 前者声明并定义了一个没有参数和原型的函数func1

  • 后者声明并定义了一个没有参数但有原型的函数func2

这两个函数的行为不同,因为当使用错误数量的参数调用具有原型的函数时,C编译器必须打印诊断消息,但是在调用没有原型的函数时,它不需要这样做。

也就是说,给定上述定义:

func1(1, 2, 3); // need not produce a diagnostic message
func2(1, 2, 3); // must always produce a diagnostic message 
                // as it is a constraint violation

然而,在严格遵守规范的程序中,这两个调用都是非法的,因为它们明确是未定义行为,根据6.5.2.2p6

此外,空括号被认为是一种过时的特性:

使用带有空括号的函数声明符(不是原型格式参数类型声明符)是一种过时的特性。

使用具有单独的参数标识符和声明列表的函数定义(不是原型格式参数类型和标识符声明符)是一种过时的特性。

详细说明

有两个相关但不同的概念:参数和参数值。

  • 参数是传递到函数中的值。

  • 参数是函数内的名称/变量,在函数输入时将其设置为参数值的值

在以下摘录中:

int foo(int n, char c) {
    ...
}

...

    foo(42, ch);

nc是参数。 42ch是参数的实际值。

引用摘录仅涉及函数的参数,但未提及函数的原型或参数。


声明 void func1() 表示函数 func1 可以被调用并带有任意数量的参数,即没有关于参数数量的信息被指定(在 C99 中,作为一个单独的声明,它被指定为“没有参数规范的函数”),而声明 void func2(void) 则表示函数 func2 根本不接受任何参数

你提问中的引语意味着,在函数定义中,void func1()void func2(void) 都表明它们没有参数,即当进入函数时,没有将变量名设置为参数值的情况。 void func() {}void func(); 相对比,前者声明了 func 确实不带参数,而后者是一个函数 func 的声明,其中既没有指定参数,也没有指定它们的类型(没有原型的声明)。

但是,它们在定义方面仍然有所不同。

  • 定义 void func1() {} 没有声明原型,而 void func2(void) {} 声明了原型,因为()不是参数类型列表,而(void)是一个参数类型列表(6.7.5.3.10):

    如果参数列表中只有一个类型为 void 的未命名参数,则指定函数没有参数。

    进一步信息请参考6.9.1.7

    如果声明符包括参数类型列表, 那么该列表同时也指定所有参数的类型;这样的声明符也作为同一翻译单位中对该函数以后调用的函数原型。 如果声明符包括标识符列表,则在以下的声明列表中必须声明参数的类型。 在两种情况下,每个参数的类型都要按照6.7.5.3的说明进行调整,针对参数类型列表;生成的类型应为对象类型。

    func1函数定义的声明符中没有参数类型列表,因此该函数没有原型。

  • void func1() { ... } 仍可用任意数量的参数调用,而使用任何参数调用void func2(void) { ... }会导致编译时错误(6.5.2.2):

    如果表示调用的函数的表达式具有包括原型的类型,则参数数量必须与参数数量相同。每个参数的类型都应该是其对应参数的未限定类型的对象可赋值的类型。

    (强调我的)

    这是一个约束条件,根据标准规定,符合要求的实现必须显示关于此问题的至少一条诊断消息。但由于func1没有原型,符合要求的实现不需要生成任何诊断信息。


然而,如果参数的数量与实参的数量不相等,则行为未定义6.5.2.2p6

如果表示调用函数的表达式具有不包括原型的类型,[...] 如果参数的数量与实参的数量不相等,则行为未定义。

因此,理论上符合 C99 标准的编译器也可以在这种情况下报错或发出警告。StoryTeller 提供了clang 可能会诊断此问题的证据;但是,我的 GCC 似乎没有这样做(这可能也是为了与一些旧的晦涩代码兼容所必需的):
void test() { }

void test2(void) { }

int main(void) {
    test(1, 2);
    test2(1, 2);
}

当使用gcc -std=c99 test.c -Wall -Werror编译上述程序时,输出如下:
test.c: In function ‘main’:
test.c:7:5: error: too many arguments to function ‘test2’
     test2(1, 2);
     ^~~~~
test.c:3:6: note: declared here
 void test2(void) { }
      ^~~~~

也就是说,在没有原型声明的函数(test)中,参数根本不会被与函数参数进行任何检查,而GCC认为为有原型声明的函数(test2)指定任何参数是编译时错误;任何符合规范的实现都必须将其诊断为约束违规。

6.9.1.13已经明确说明:“这两个定义之间的区别在于,第一种形式作为原型声明,强制将后续调用函数的参数进行转换,而第二种形式则不会。”(这两个定义都是关于同一个带有参数列表和标识符列表的函数声明。空列表必须是标识符列表,相应的参数列表只需使用void即可) - TonioElGringo
我没有找到C规范支持 _function definition_ 作为 void func1() { } 是一个不赞成的特性。也许您认为 6.11.6函数声明符 适用于 _函数定义_? - chux - Reinstate Monica
1
哦,你提到的「6.11.7 函数定义」和它的「分离参数标识符和声明列表」链接并不适用于 void func1() { }。但它却适用于 void func3(a,b) int a; int b; { } - chux - Reinstate Monica
1
或者,在函数定义中也有一个 declarator 函数,因此适用于6.11.6。 - Antti Haapala -- Слава Україні
3
标准对于如果以foo(5)的形式调用int foo() {...};不会有任何要求,但是一些实现可以并且确实定义了这样的调用具有有用的行为,特别是如果函数的代码使用了内联汇编或其他实现定义的扩展。将这样的调用视为约束违规将阻止此类实现提供有用的功能。 - supercat

21
重要部分已经用粗体突出:

6.7.5.3 函数声明符(包括原型)14 标识符列表仅声明函数参数的标识符。在函数声明符中,如果空列表是该函数定义的一部分,则指定该函数没有参数。但是,在不是该函数定义的一部分的函数声明符中,空列表指定未提供有关参数数量或类型的信息。

因此,在带有函数体的函数中,当参数列表为空时,它们是相同的。但如果只是函数的声明,则不同。

void function1(); // No information about arguments
void function2(void); // Function with zero arguments

void function3() {
    // Zero arguments
}

void function4(void) {
    // Zero arguments
}

4
“@usr what does it mean?” 的意思是“@usr 这是什么意思?”。 - Mats
5
引用段落表明这个含义出现在定义中,而不是声明中。你不能对标准提出异议。 - StoryTeller - Unslander Monica
3
如果一个定义没有参数,那么它就没有参数;-) 我不确定引用部分是否与问题直接相关。即使函数定义为 int func() {..}(没有原型),在这种情况下仍然可以接受参数,该定义也充当声明 - P.P
4
我感觉我在重复自己。但是我会再试一次:如果没有先前的声明,则定义将起到声明的作用。如果该定义没有参数,则它“接受未指定(但不是无限)数量的参数”。(顺便说一下,如果某个函数被定义为int fun() {},那么很明显它没有参数 - 我能看得出来,因为我不是盲人。但这并不能反驳我所说的。也许你可以解释一下“参数”和“参数值”之间的区别。) - P.P
7
这个答案是错误的。在参数方面,它们是相同的,但是 () 不指定原型,因此函数3没有原型 - 它也没有任何参数,但是不会检查参数数量或它们的类型。 - Antti Haapala -- Слава Україні
显示剩余23条评论

10
根据标准,func()和func(void)是相同的吗?
不。func(void)表示函数根本不带任何参数;而func()表示函数带有未指定数量的参数。两者都是有效的,但是func()样式已经过时,不应该再使用。
这是早期C的产物。 C99将其标记为已过时。 6.11.6 Function declarators
使用空括号(而不是原型格式的参数类型声明符)的函数声明符是一个已过时的特性。
截至C11,它仍然是已过时的,并未从标准中删除。

1
希望它在2倍速中被移除。 - 2501
但根据[6.7.5](http://port70.net/~nsz/c/c99/n1256.html#6.7.5.3p14)的规定,似乎是一样的。 - liusrichard
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - supercat
并不是说不提供这种语义的编译器就必须接受这种语法——只是需要有某种标准语法,实现可以随意支持或不支持以指示旧语义。有一个新的语法实际上可以促进依赖于语义的代码迁移到新系统,如果希望简化这种迁移的编译器编写者使用语法来模拟旧的调用约定。 - supercat

6
函数定义中的空参数列表表示该函数既没有原型也没有任何参数。
C11 §6.9.1/7 功能定义( ongoing 强调在引用中):
声明符在函数定义中指定正在定义的函数的名称及其参数的标识符。如果声明符包括参数类型列表,则列表还指定了所有参数的类型;这样的声明符也用作在同一翻译单元中对同一函数的后续调用的函数原型。
问题问道:
根据标准,func() 和 func(void) 是相同的吗?
不是。 void func() 和 void func(void) 之间的本质区别在于它们的调用。
C11 §6.5.2.2/2 函数调用(在约束部分内):
如果表示被调用函数的表达式具有包含原型的类型,则参数数量应与参数个数相同。每个参数的类型都应该是其对应参数的未修饰版本的类型可以赋值给一个对象的值。
注意,参数≠参数。函数可能不包含参数,但它可能有多个参数。
由于使用空参数定义的函数不会引入原型,因此不会针对其调用进行检查,因此在理论上它可以提供任意数量的参数。
然而,以至少一个参数调用这种函数在技术上是未定义行为(UB)(请参见Antti Haapala's comments)。
C11 §6.5.2.2/6 函数调用(在语义部分内):
如果参数的数量与参数的数量不相等,则行为是未定义的。
因此,区别很微妙:
当使用void定义函数时,如果参数的数量与参数(以及其类型)不匹配,则由于约束违规(§6.5.2.2/2),它将无法编译。这种情况需要符合标准的编译器发出诊断消息。
如果使用空参数进行定义,则它可能会编译,也可能不会(符合标准的编译器没有要求发出诊断消息),但是调用这样的函数是未定义的行为。
例如:
#include <stdio.h>

void func1(void) { puts("foo"); }
void func2()     { puts("foo"); }

int main(void)
{
    func1(1, 2); // constraint violation, it shouldn't compile
      func2(3, 4); // may or may not compile, UB when called
    return 0;
}

请注意,优化编译器可能会在这种情况下削减参数。例如,这就是Clang如何根据SysV ABI调用约定在x86-64上使用-01编译上述代码(不包括func1的调用)的方式。
main:                                   # @main
        push    rax          ; align stack to the 16-byte boundary
        call    func2        ; call func2 (no arguments given)
        xor     eax, eax     ; set zero as return value
        pop     rcx          ; restore previous stack position (RSP)
        ret

1
附录J.2. 未定义行为:“对于在作用域中没有函数原型的函数调用,参数数量不等于参数数量(6.5.2.2)。”,因此在严格符合规范的程序中不允许。 - Antti Haapala -- Слава Україні

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