在C23中,int main()和int main(void)这两个原型是等价的吗?

17
C23在函数声明符中引入了新的语义:
6.7.6.3 函数声明符
[...]
13 对于没有参数类型列表的函数声明符:效果就好像它被声明为一个参数类型列表,其中包含关键字 void。函数声明符为函数提供了原型。
这似乎意味着使用空参数列表编写的函数定义可以等效地写为 () 或 (void)。
然而,对于 main 函数,这种等价性似乎并不保证。

5.1.2.2.1 Program startup

The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:

int main(void) { /* ... */ }

or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):

int main(int argc, char *argv[]) { /* ... */ }

or equivalent or in some other implementation-defined manner.

这似乎不能保证int main() { /* ... */ }main的有效定义,或者说等效是否包含这种变体?
这让我感到困扰,C17中使用语法int main()的两个示例(在6.5.3.4和6.7.6.3中)在最新的C23草案中已经改为使用int main(void)

1
在许多传统的Unix系统上,你也可以将main声明为int main(int argc, char **argv, char **envp),其中envp是一个以NULL结尾的环境变量定义数组。我刚在我的Linux系统上尝试了一下,它仍然有效。不确定ANSI C对此有何说法。它可能被认为是非标准的。 - undefined
6
@TomKarzes:这个由或以其他实现定义的方式来处理。 - undefined
3
C11 §6.7.6.3 p14中的旧措辞是:“在函数声明符中的空列表,作为该函数定义的一部分,指定该函数没有参数。”因此,将main的定义写为int main() { /*code here*/ }至少从C11开始就等同于int main(void) { /*code here*/ } - undefined
@user3386109:GCC或clang在使用-std=c11时并不是这样处理的。https://godbolt.org/z/5zf1dr1dK显示GCC在`int main(){}定义之后接受main(123),即使在-Wall -Wextra -pedantic下也没有警告。Clang会发出警告,但不会报错,消息中说这只是在C2x中才是错误。GCC的-std=c23`会报错。标准中的这个条款只是在讨论是否有可以访问以读取传递的任何参数的命名变量。否则,GCC和Clang将偏离C11标准,可能是有意为之,但是C23又有什么改变呢? - undefined
@PeterCordes 你误解了我的评论。我是在回答提问者提出的问题。问题明确询问了"int main() {/*...*/}是否是main的有效定义"。我在评论中特别强调了定义。请注意,我的评论中也强调了定义。你的评论是关于一个空参数列表的定义是否作为一个原型。在C11中,答案是“不是”。在C23中,答案是“是”。 - undefined
@user3386109:哦,你只是指在有效定义和与调用者(如_start)兼容的情况下,希望调用一个接受0或2个参数的主函数。而不是指定义作为声明的方式,这部分在C23之前是不等效的。 - undefined
5个回答

17
在C17和早期版本的标准中,int main() { … }没有为main()提供原型,但在其他方面与int main(void) { … }等效。
在C23中,int main() { … }main()提供了原型,并且在拼写上完全等效于int main(void) { … }
这个区别只有在递归调用main()时才会有影响——这在C中是允许的,但在C++中是不允许的。在C17或更早的版本中,像main(23, "elephants");这样的递归调用是允许的,因为没有为main()指定原型(假设在递归调用之前可见main()的定义)。而使用int main(void),这是不允许的,因为作用域中有一个指定“无参数”的原型。
注意在 C和C++中main()应该返回什么?中所说的内容。这个问题有广泛的讨论,包括C17和之前的标准在它们的(非规范性)示例中使用了int main()int main(void)。它还指出了Microsoft对Windows系统的规定以及Annex J“常见扩展”中提到的内容(两者都支持int main(int argc, char **argv, char **envp))。甚至Apple还为main()提供了第四个可选参数-int main(int argc, char **argv, char **envp, char **apple),它的行为类似于argvenvp。我需要尽快更新我的C23答案。

嗯,根据我阅读的"C11 §6.7.6.3 14"中的内容,一个函数声明中的空列表表示该函数没有参数。所以,像foo() { .... }这样的函数定义后面的foo(23, "elephants");是无效的调用。我认为int main() { ... }和后面的main(23, "elephants");也是无效的。 - undefined
1
旁注:为什么在C语言中可以递归调用main函数,但在C++中却不行?虽然并不是非常有用,但我可以设计一个C程序:#include <stdio.h> int main(int argc,char **argv) { printf("%s: %s\n",argv[0],argv[argc - 1]); if (argc > 2) main(argc - 1,argv); return 0; } 编译并运行它:./program hello world goodbye galaxy,然后得到:./program: galaxy ./program: goodbye ./program: world ./program: hello 为什么在C++中不能这样做呢?这似乎是一条任意的规则。这个规则的理由是什么? - undefined
2
@CraigEstey 更确切地说是一个“过时”的规则。Cfront时代的C++实现在main函数的开头注入代码来运行全局构造函数,而且这段代码不是幂等的,所以递归调用main会再次运行全局构造函数,从而引发滑稽的情况。我想这个规则至今仍然存在,主要是因为WG21中没有人认为这个问题足够重要,值得去改变。 - undefined
1
@CraigEstey:MinGW在汇编的main:顶部即使在C模式下也会调用__main初始化函数,我认为这是为了避免将libc的初始化工作从DLL的加载钩子中运行。假设至少在C模式下是幂等的,可能会有一个已初始化标志的静态检查。在gcc汇编输出中,哪个库有__main函数引用 / GCC在MinGW上如何实现__attribute__((constructor))(GCC即使在C模式下也支持静态构造函数,所以可能不是libc的初始化)。 - undefined
1
@PeterCordes: 他们无法将libc的内容放在DLL的加载钩子中,因为有-static的存在。并不存在EXE的加载钩子。至于为什么他们没有将那个调用放在crt0.o(或其道义等效物)中,我不知道原因。 - undefined
显示剩余2条评论

3
所有标准中的代码片段都被视为示例,因此不具有规范性。
规范要求是文本所说的:“[main]应该以int类型的返回值和无参数的方式进行定义[,或者……]”。在C2023中,int main() { ... }以int类型的返回值和无参数的方式定义了main,因此满足了要求。
正如在问题的评论中指出的那样,C2011中的语言意味着定义int main() { ... }将在该标准下定义main为int类型的返回值和无参数,尽管声明int main();不会声明main为无参数。我无法从这台计算机方便地检查C1999或更早的版本。

关于“标准中的所有代码片段都被视为示例”的问题:示例中的代码是非规范性的,因为示例本身就是非规范性的。标准中还有一些不在示例中的代码片段,用于解释说明,并且它们是标准规范部分的重要组成部分。我们不能说5.1.2.2.1中的int main(void) { /* ... */ }是示例代码而不是解释性代码,因为那么int main(int argc, char *argv[]) { /* ... */ }也将是非规范性的,而标准规范中没有任何内容说明第一个参数的类型是int,第二个参数的类型是char *[] - undefined
1
@zwol:关于“不,真的,所有代码片段都是非规范示例”的说法:这种断言没有依据,而且是不可能的,因为标准在很多情况下都使用了代码片段,这些代码片段对于标准来说是至关重要的,比如在6.7.2 2节中列举类型说明符的组合时。 - undefined
@EricPostpischil 我已经无法访问1990年代的DR讨论和回复档案,即使我有时间也无法翻找。你可以选择不相信我,但我所说的是真实的。 - undefined
所以6.4.2.2 1的规范文本是:“标识符__func__应当被翻译器隐式声明,就好像在每个函数定义的左花括号之后立即出现了声明,其中function-name是词法封闭函数的名称。”而不是“标识符__func__应当被翻译器隐式声明,就好像在每个函数定义的左花括号之后立即出现了声明static const char __func__[] = " function-name ";,其中function-name是词法封闭函数的名称。” - undefined
6.10.6 2说:“...指令应采用以下形式之一,其含义在其他地方描述:开关:其中之一。” 这个与5.1.2.2.1中的main规范非常相似;它指定了代码可能具有的形式,并使用冒号引入这些形式的显示。如果从中删除代码片段,它就成了语法错误的胡言乱语。 - undefined
显示剩余13条评论

3
首先,让我们考虑根据C17标准如何声明函数main。C17标准的第6.7.6.3节“函数声明符(包括原型)”中提到:

14 标识符列表仅声明函数的参数标识符。在函数声明符的定义中,如果使用了空列表,则表示该函数没有参数。在函数声明符的非定义部分中使用空列表,则表示没有提供有关参数数量或类型的信息。

也就是说,当函数具有空的标识符列表时,可以在函数定义中使用空括号。但是相对于函数main,标准要求必须使用参数类型列表来声明它。
根据C语法:

5 如果在声明“T D1”中,D1的形式为

D ( parameter-type-list )

or

D ( identifier-listopt )

如果在声明“T D”中为ident指定的类型是“derived-declarator-type-list T”,那么为ident指定的类型是“derived-declarator-type-list返回未限定版本的T的函数”。
正如所见,参数类型列表可能是可选的。可选的是标识符列表。
进一步(第10页):
10 作为列表中唯一项的类型为void的未命名参数的特殊情况指定函数没有参数。
这意味着没有参数的main函数应该用包含类型为void的未命名参数的参数列表声明。
根据C17标准(5.1.2.2.1程序启动):
1 在程序启动时调用的函数名为main。实现不为此函数声明原型。它应该定义为返回类型为int且没有参数的函数。
int main(void) { /* ... */ }

或者使用两个参数(在这里称为argc和argv,尽管可以使用任何名称,因为它们是在声明它们的函数内部局部的):
int main(int argc, char *argv[]) { /* ... */ }

或等效;10)或以某种其他实现定义的方式。
现在让我们阅读一下详细解释了“等效”一词的脚注10:
10)因此,int可以被定义为int的typedef名称替换,或者argv的类型可以写为char ** argv,等等。
没有提到函数main的函数声明可以用空的标识符列表重写的情况。
标准要求函数main必须使用函数原型进行声明。上面提供的引用中给出了main的这种声明的示例。
来自C17标准(6.9.1函数定义)
语义学 7 在函数定义中,声明符指定了正在定义的函数的名称和其参数的标识符。如果声明符包括参数类型列表,则该列表还指定了所有参数的类型;这样的声明符还用作同一翻译单元中对同一函数的后续调用的函数原型。如果声明符包括标识符列表,参数的类型应在随后的声明列表中声明。无论哪种情况,每个参数的类型都会根据6.7.6.3中的参数类型列表进行调整;结果类型应为完整的对象类型。
请注意,在C标准中,动词“shall”的意思如下:
符合性 1 在本文档中,“shall”被解释为对实现或程序的要求;相反,“shall not”被解释为禁止。
C23标准相对于main的声明有什么变化?没有!如果你阅读C23标准草案的5.1.2.2.1程序启动部分,你会发现在关于main的声明上使用了相同的文本,没有参数。
是的,现在在C23标准中,函数声明中带有空括号的函数声明符具有另一种含义。参数类型列表是可选的。函数声明符中排除了标识符列表。但是main的声明满足与C17标准相同的要求。它应该按照C23标准的要求声明,如下所示:
int main( void )

即使脚注与C17标准中的相同,也没有一个词能够像这样声明函数main
int main()

在C23标准的5.1.2.2.1程序启动部分中。
在C23标准中,与C17标准相同,保留了使用空参数列表声明main的形式。
@zwol错误地声称与main的声明相关的“标准中的所有代码片段都被视为示例,因此不具有规范性。” C17标准和C23标准的5.1.2.2.1程序启动部分都对如何声明主函数进行了规范描述。
你应该注意到,这将是从之前的C标准到C23标准对main的规范描述的重大变化。而C23标准应该在其描述中反映出这一点,即“就本草案(C23)而言,已应用了哪些文件”。然而,在这份文件清单中,并未提及对函数main声明要求的更改。我只找到了以下与之相关的文件:“N2432删除对带有标识符列表的函数定义的支持”。
如果我接受我是错误的,那么可以得出结论,C23标准存在缺陷,因为这一部分是从C17标准转移而来,没有任何改动,但其含义却发生了变化。至少在C23标准中,应该在这一部分添加一个注释或脚注,以阐明与之前的C标准的变化。

脚注10并不确切地阐述了“等同”一词的含义,至少它是“等同”的一个例子。除此之外,大部分回答都是不错的。 - undefined
@chux-ReinstateMonica 无论如何,等价性并不意味着在C17中用空标识符列表声明main函数。它应该使用包括空参数列表的参数列表进行声明。 - undefined
1
关于“Further (p. #10):” / 10 **The special case of an unnamed parameter of type void as the only item in the list specifies that the function has no parameters.**” / “That is the function main without parameters shall be declared with a parameter list that contains unnamed parameter of type void.”这段话说的是,如果声明符具有D(void)的形式,则表示函数没有参数,而不是如果函数没有参数,则其声明符必须具有D(void)的形式,并且它没有提到main必须具有什么形式。 - undefined
@EricPostpischil 我不能同意你的观点。函数的声明中明确指出了应该如何声明函数。函数应该使用参数类型列表进行声明。 - undefined
1
关于“明确显示函数应如何声明”的问题:明确指出“或等效”。 - undefined
显示剩余11条评论

1
在C23中,int main()int main(void)是等效的原型吗?
是的,对于函数声明来说是等效的。
C23草案N1570对此进行了修订规定:
对于没有参数类型列表的函数声明符:其效果就好像它被声明为一个参数类型列表,其中只包含关键字void。函数声明符为函数提供了原型。C23 § 6.7.6.3 13
我理解这适用于所有函数,包括main()
关于main的规范并没有限制这一点。

-1
在C标准中,规范的内容有:
- 对于所有函数声明符,6.7.6.3是规范的。 - 在所有C系统中,C程序的入口点始终是一个函数(参见5.1.2)。 - 如果`main`是C程序的入口点,则根据C23 6.7.6.3,`int main()`和`int main(void)`是等效的。
值得注意的是:
  • 例子,包括代码示例都不是规范的。注释和脚注也不是。这些都是信息性部分的例子。详细信息请查看: C++标准核心语言规范中的注释和示例是否非规范的?

  • 第5.1.2.2.1章节只对严格符合规范的托管系统程序具有规范性。对于一般的C程序没有任何相关性。

  • 正如@zwol在答案中指出的那样,第5.1.2.2.1章节中具有规范性的部分(针对托管系统程序)是文本“它应该定义为返回类型为int且没有参数”,而不是示例。

  • 此外,第5.1.2.2.1章节中还有规范性文本“或等效”。这始终是为了涵盖诸如int main (int foo, char* bar[])int32_t main (void)等情况的。在这种情况下,int == int32_t,等等。等效示例只是编码风格或命名的变化。

    恰好在C23中,“或等效”部分也适用于int main (void)int main (),因为它们是等效的形式。

  • 第5.1.2.2.1章节还有文本“或以其他实现定义的方式”。
    类似地,对于独立系统的第5.1.2.1章节:“在程序启动时调用的函数的名称和类型是实现定义的”。

    这意味着只要函数遵守了函数声明的规则,它可以被命名为任何名称,并以任何方式声明,只要这由实现(编译器)记录。然而,依赖于这种方式的程序将不再是严格符合规范的(C17或C23 4 §5),因为它依赖于实现定义的方式;因此,独立程序永远不能严格符合规范。

    然而,在符合规范的程序(托管/独立)中,可以创建一个具有任何名称、返回类型或参数的函数作为程序的入口点,只要编译器记录并支持这样做。

    但是,具有格式int function_name()的程序入口函数仍然必须遵守6.7.6.3。

常见的符合托管系统功能的示例(实现定义):
int WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd);

常见的符合自立系统函数的示例(实现定义):
void main (void);

即使例子不是规范的,它们的存在也会强烈暗示给我一种意图和期望,即在没有非常强烈和令人信服的理由的情况下,实现应该以所示的方式运行。 - undefined

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