不带"void"的int main()在ISO C中是否有效且可移植?

26

C标准规定了主机实现中main的两种定义形式:

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

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

它可以用与上述方式"等价"的方式来定义(例如,您可以更改参数名称,将int替换为定义为int的typedef名称,或将char *argv []编写为char **argv)。

它还可以以"某种其他实现定义的方式"定义 - 这意味着像int main(int argc,char * argv [],char * envp)这样的东西是有效的如果实现对其进行了记录。

“以某种其他实现定义的方式”条款不在1989/1990标准中; 它是由1999年标准添加的(但早期的标准允许扩展,因此实现仍然可以允许其他形式)。

我的问题是:鉴于当前(2011年)ISO C标准,是否存在以下形式的定义

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

适用于所有托管实现且可移植的吗?

(请注意,我并不涉及C ++中的void main或使用没有括号的int main()。这只涉及ISO C中int main(void)int main()之间的区别。)


相关:int main()和int main(void)之间的区别? - RobertS supports Monica Cellio
3个回答

27

不可以。

根据标准的规范措辞,使用空括号而没有 void 关键字的定义不是必须接受的形式之一,严格来讲这种程序的行为是未定义的。

参考文献: N1570 第5.1.2.2.1节。(2011年发布的ISO C标准不免费提供,其措辞与N1570草案相同。)

第1段说:

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.

在约束之外使用“shall”这个词意味着任何违反它的程序都有未定义的行为。因此,例如,如果我写:

double main(unsigned long ocelots) { return ocelots / 3.14159; }

一个符合规范的编译器不一定需要输出诊断信息,但也不需要编译程序或者如果编译了程序,则不需要以任何特定的方式运行它。
如果 int main() 等同于 int main(void),那么它将是有效且可移植到任何符合规范的托管实现。但它们并不等价。
int main(void) { }

提供了一个声明(在本例中为原型)和一个定义。通过使用 void 关键字,声明指定函数没有参数。定义也指定了同样的内容。

如果我改为以下写法:

int main() { }

那么我使用的是旧式声明和定义。(这些声明和定义已经过时,但它们仍然是语言定义的一部分,并且所有符合标准的编译器仍必须支持它们。)

作为一个声明,它没有指定函数期望的参数数量或类型。作为一个定义,它不定义参数,但编译器不需要使用该信息来诊断不正确的调用。

DR #317 包括 C 标准委员会在 2006 年做出的裁决,即带有 () 的定义不能提供等同于带有 (void) 的原型(感谢 hvd 找到该参考文献)。

C 允许递归调用 main。假设我写:

int main(void) {
    if (0) {
        main(42);
    }
}

可见的原型int main(void)指定了main不接受任何参数。试图传递一个或多个参数的调用会违反约束条件,需要在编译时进行诊断。

或者假设我写:

int main() {
    if (0) {
        main(42);
    }
}

如果执行调用main(42),它将具有未定义的行为--但它不违反约束,并且不需要诊断。由于受到if (0)保护,因此从未发生调用,也从未实际发生未定义的行为。如果我们假设int main()是有效的,则该程序必须被任何符合规范的编译器接受。但正因为如此,它证明了int main()并非等同于int main(void),因此不受5.1.2.2.1的覆盖。
结论:根据标准的措辞,实现可以记录int main() { }是允许的。如果没有记录,仍然可以接受它而不会抱怨。但符合规范的编译器也可能拒绝int main() { },因为它不是标准允许的形式之一,其行为因此未定义。
但仍有一个未解决的问题:这是否是标准作者的意图?
在1989年 ANSI C 标准发布之前,不存在 void 关键字。 ANSI 之前的(K&R)C 程序将会定义 main 为以下之一:
main()

或者作为
int main()

ANSI标准的一个主要目标是在不破坏现有的ANSI之前的代码的情况下添加新功能(包括原型)。声称int main()不再有效将违反该目标。
我怀疑C标准的作者们并没有打算使int main()无效。但是,按照所写的标准并没有体现出这种意图;它至少允许符合C编译器拒绝int main()
实际上,你几乎肯定可以使用它。我尝试过的每个C编译器都会接受。
int main() { return 0; }

不抱怨,行为相当于

int main(void) { return 0; }

但出于各种原因:

  • 遵循标准的文字和意图;
  • 避免使用过时的特性(未来的标准可能会删除旧式函数定义);
  • 保持良好的编码习惯(对于实际被其他函数调用的函数而言,()(void)之间的区别非常重要,不仅仅是main函数)。

我建议始终编写int main(void)而不是int main()。 它更清晰地表明了意图,并且您可以100%确定您的 编译器将接受它,而不是99.9%。


3
我非常想补充一些东西;因为我在那个时代成长,那时编译器不够兼容,而针对它们编写的教程也很少;但你已经充分涵盖了这个领域 :) - Edwin Buck
5
我更愿意采用这个解释,即“int main()”是被允许的,而标准存在缺陷。 - M.M
5
另外,我对你所提出的“避免使用过时的特性(未来的标准可能会删除旧式函数定义)”的论点非常怀疑。即使未来的标准删除了旧式函数定义,那么这个未来的标准肯定会像C++一样将()等同于(void)。争辩未来的标准可能不会这样做就像争辩未来的标准可能使所有整数文字都是基数11一样。当然,从技术上讲我们不能排除这种可能性,但我们都知道这是不可能发生的。 - user743382
8
重新表述我的早期评论:你的回答显示了int main()作为定义与int main(void)作为声明不等同,但仍然等效,因此对于“int main()int main(void)作为声明不等同”的论点并没有被证明或反驳。是的,int main() { if (0) { main(42); } }不需要诊断,但这是因为声明与int main(void)不同,并且仍存在一个可能合法的论点认为定义是等效的。 - user743382
2
文本“它应该定义为返回类型为int且没有参数的形式:int main(void) { /* ... */ }”显然有缺陷。不清楚代码示例是否是唯一允许的形式,还是它只是允许形式的一个示例;在这种情况下,int main() {}也符合文本中的标准。 - M.M
显示剩余29条评论

15
一个强有力的迹象表明,无论标准是否准确提供使其有效的措辞,int main()都是有效的,这是因为在标准中偶尔会使用int main()而没有任何人提出反对意见。虽然示例不具规范性,但它们确实表示意图。

6.5.3.4 The sizeof and _Alignof operators

8 EXAMPLE 3 In this example, the size of a variable length array is computed and returned from a function:

#include <stddef.h>

size_t fsize3(int n)
{
      char b[n+3];       // variable length array
      return sizeof b;   // execution time sizeof
}

int main()
{
      size_t size;
      size = fsize3(10); // fsize3 returns 13
      return 0;
}

6.7.6.3 Function declarators (including prototypes)

20 EXAMPLE 4 The following prototype has a variably modified parameter.

void addscalar(int n, int m,
      double a[n][n*m+300], double x);

int main()
{
      double b[4][308];
      addscalar(4, 2, b, 2.17);
      return 0;
}

void addscalar(int n, int m,
      double a[n][n*m+300], double x)
{
      for (int i = 0; i < n; i++)
            for (int j = 0, k = n*m+300; j < k; j++)
                  // a is a pointer to a VLA with n*m+300 elements
                  a[i][j] += x;
}
关于标准的实际规范文本,我认为“等效”的含义被过度解读了。应该很清楚,
int main (int argc, char *argv[]) {
    (void) argc; (void) argv;
    return 0;
}

是有效的,并且

int main (int x, char *y[]) {
    (void) argc; (void) argv;
    return 0;
}

是无效的。尽管标准在规范文本中明确指出可以使用任何名称,这意味着 int main (int argc, char *argv[])int main (int x, char *y[]) 在 5.1.2.2.1 的目的上被视为等效。单词“等效”的严格英语含义并不是它的实际含义。

Keith Thompson 在他的回答中提出了一个相对宽松的解释。

同样有效但更加宽泛的解释允许 int main()int main(void)int main() 都将 main 定义为返回 int 并且不带参数的函数。

目前,标准和任何官方 DRs 都没有回答哪种解释是预期的,因此这个问题无法回答,但示例强烈暗示了最后一种解释。


这些例子是一个很好的论据,并且倾向于证实意图是允许int main(){}(更准确地说,要求符合规范的实现接受int main(){})。我仍然认为标准的规范措辞使int main(){}不是严格符合规范的,但这取决于“等效”的解释。 - Keith Thompson
除了对未定义值的引用之外,你最后一个示例有什么无效之处? - cat
1
@cat 正是因为 (void)argc;(void)argv; 是无效的,因为 argcargv 还没有被声明。这就是重点:从过于严格的意义上讲,int main(int x, char *y[]) 不等同于 int main(int argc, char *argv[]) - user743382

8
是的。
int main() { /* ... */ }

等同于

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

N1570 5.1.2.2.1/1

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.

6.7.6.3/14

标识符列表仅声明函数参数的标识符。在函数声明符中为空列表的函数定义指定该函数没有参数。不是函数定义的函数声明符中的空列表指定没有提供有关参数数量或类型的信息。

(强调我的)

正如标准明确指出的那样,定义int main() { /* ... */ }确实指定了函数main没有参数。我们都清楚,这个函数定义确实指定了函数main的返回类型为int。由于5.1.2.2.1不要求声明main必须具有原型,因此我们可以安全地断言定义int main() { /* ... */ }满足标准所规定的所有要求(它[主函数]应以int类型的返回类型和没有参数的形式定义,或者[其他一些形式])。

然而,在您的代码中永远不应使用int main() {},因为“使用带有空括号的函数声明符(不是原型格式的参数类型声明符)是一种过时的特性”(6.11.6),并且由于这种定义形式不包括函数原型声明符,编译器不会检查参数的数量和类型是否正确。

N1570 6.5.2.2/8

不执行其他隐式转换;特别地,在不包括函数原型声明符的函数定义中,不会将参数的数量和类型与参数进行比较。

(强调我的)


1
你应该添加5.1.2.2.1中包含“或等效”的字眼。这允许例如int main(int c, char **v) {…,如果你认为“或等效”适用于它们之前的两种情况,那么你就有了一个强有力的论据(连同你引用的6.7.6.3:14)。 - Pascal Cuoq
1
这个答案明显缺少重要细节。Keith Thompson的答案已经展示了int main()int main(void)之间的具体区别。 - user743382
3
@hvd,实际上,我认为这个回答提出了一个强有力的观点,即Keith Thompson回答中使用的句子“If we assume that int main() is valid, then this program must be accepted by any conforming compiler.”是错误的。因为“在函数声明符的空列表中,该函数的定义部分指定函数没有参数”。 - Pascal Cuoq
3
@PascalCuoq 的说法是正确的,虽然我同意标准可以更清晰明确。请参阅 DR #317 获取官方解释。 - user743382
1
@KeithThompson 是的,这个问题已经被 hvd 挖掘出来并得到了令人信服的解释。我仍然不相信仅仅因为 int main() 允许编写无效的递归调用,就应该被视为禁止,但是 hvd 在您的答案下面写了一条评论,我没有什么可补充的。 - Pascal Cuoq
显示剩余7条评论

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