“int main;”是一个有效的C/C++程序吗?

114
我之所以这样问是因为我的编译器认为如此,尽管我并不这样认为。 echo 'int main;' | cc -x c - -Wall
echo 'int main;' | c++ -x c++ - -Wall Clang没有发出任何警告或错误,gcc只发出温和的警告:'main' is usually a function [-Wmain],但仅在编译为C时才会发出。指定-std=似乎无关紧要。
否则,它可以编译和链接良好。但在执行时,它会立即终止,并显示SIGBUS(对我而言)。
阅读(优秀)答案中的What should main() return in C and C++?和快速搜索语言规范,我确信需要一个main 函数。但是来自gcc的-Wmain的措辞(‘main’ is usually a function)(以及这里的错误缺乏)似乎可能表明另外一种情况。

但是为什么呢?这是某种奇怪的边缘情况或“历史”用途吗?有人知道是什么原因吗?

我想说的是,在托管环境中,我真的认为这应该是一个错误,不是吗?


6
要使gcc成为(大多数)标准兼容的编译器,您需要使用“gcc -std=c99 -pedantic ...”。 - pmg
3
@pmg无论是否使用-pedantic或任何-std,都是相同的警告。我的系统c99也可以编译这个文件而不出现警告或错误... - Geoff Nixon
3
很遗憾,如果你足够“聪明”,可以创建编译器认可但没有意义的东西。在这种情况下,你正在链接 C 运行库调用名为 main 的变量,这不太可能起作用。如果你使用“正确”的值初始化 main,它实际上可能会返回... - Mats Petersson
7
即使这是有效的,这也是一件糟糕的事情(指难以阅读的代码)。顺便说一句,在托管实现和独立实现中可能会有所不同(独立实现不知道main)。 - Basile Starynkevitch
1
为了更多有趣的时光,请尝试main=195; - geometrian
显示剩余5条评论
9个回答

97

由于问题被双重标记为C和C ++,因此C ++和C的解释不同:

  • C ++使用名称修饰(name mangling)来帮助链接器区分不同类型的文本相同符号,例如全局变量xyz和独立的全局函数xyz(int)。但是,名称main永远不会被修饰。
  • C不使用名称修饰,因此程序可能会通过提供一种类型的符号来替换不同类型的符号,并使程序成功链接。

这就是问题所在:链接器期望找到符号main,并且它确实找到了。它将该符号“连接”起来,就好像它是一个函数,因为它不知道更好的方法。将控制权传递给main的运行时库部分要求链接器提供main,因此链接器会给出符号main,从而完成链接阶段。当然,在运行时此过程失败,因为main不是一个函数。

这里是同样问题的另一个例子:

文件x.c:

#include <stdio.h>
int foo(); // <<== main() expects this
int main(){
    printf("%p\n", (void*)&foo);
    return 0;
}

文件 y.c:

int foo; // <<== external definition supplies a symbol of a wrong kind

编译中:

gcc x.c y.c

这段代码可以编译,可能会运行,但由于向链接器提供的符号类型与编译器承诺的符号类型不同,因此它是未定义行为。

至于警告消息,我认为它是合理的:C允许您构建没有main函数的库,因此如果您需要定义一个变量main以某种未知的原因,编译器就会释放该名称main供其他用途使用。


3
C++编译器对main函数的处理方式不同。即使没有extern "C",它的名称也不会被混淆。我猜这是因为否则它需要发射自己的extern "C" main函数,以确保链接。 - UldisK
2
实际上,C++和C的结果并不不同,正如在这里指出的那样 - 在C++中,main不受名称混淆的影响(看起来是这样),无论它是否是一个函数。 - Geoff Nixon
4
我认为你对问题的解释过于狭隘了:除了在帖子标题中提出的问题,OP明显还寻求一个解释为什么他的程序首先编译成功(“即使我不这样认为,我的编译器似乎是这样认为的”),以及为什么将main定义为除函数外的其他内容可能有用。答案为两个部分都提供了解释。 - Sergey Kalinichenko
1
符号main不受名称混淆的影响是无关紧要的。C++标准中没有提到名称混淆。名称混淆是一个实现问题。 - David Hammen
@UldisK:我认为许多C++实现都有一个名为“main”的库函数,它执行任何需要在用户提供的“main”第一条语句之前发生的C++特定操作,然后调用用户提供的“main”函数,该函数将被赋予其他名称。一些实现可能会代替编译器生成对启动例程的调用,以便在“main”函数中执行任何用户提供的代码之前执行,但实际上没有任何理由在此类代码执行之前激活用户提供的“main”函数的堆栈帧。 - supercat
显示剩余5条评论

30

main不是保留字,而是预定义标识符,类似于cinendlnpos等。因此,您可以声明一个名为main的变量,对其进行初始化然后输出其值。

当然:

  • 警告很有用,因为这种做法容易出错;
  • 您可以编写一个没有main()函数的源文件(库)。

编辑

一些参考资料:

  • main不是保留字(C++11):

    函数main不得在程序内使用。 main的连接(3.5)是实现定义的。定义main为已删除或声明maininlinestaticconstexpr的程序是非法的。 名称main不受保留。
    [示例:成员函数、类和枚举可以称为main,其他名称空间中的实体也可以。 ——结束示例]

    C++11 -[basic.start.main]3.6.1.3

    [2.11/3] [...] 一些标识符保留供C++实现和标准库使用(17.6.4.3.2),不得以其他方式使用;无需进行诊断。

    [17.6.4.3.2/1] 某些名称和函数签名集始终保留给实现:

    • 每个包含双下划线__或以下划线后跟大写字母开头的名称(2.12)都保留供实现任何用途。
    • 以下划线开头的每个名称都被保留给实现,作为全局命名空间中的名称。
  • 编程语言中的保留字

    程序员不能重新定义保留字,但预定义的可以在某种程度上被覆盖。这就是main的情况:在使用该标识符的声明的范围内,其含义可以被重新定义。


我猜我被这个事实迷住了(因为它非常容易出错),所以为什么这是一个警告而不是错误,以及为什么编译为C时只是一个警告。当然,你可以编译没有main()函数的程序,但你不能将其链接为一个程序。这里发生的是一个“有效”的程序被链接而没有main(),只有一个main - Geoff Nixon
7
cinendl 不在默认命名空间中,它们位于 std 命名空间中。 nposstd::basic_string 的成员。 - AnotherParker
1
main 是一个保留作为全局名称的关键字。你提到的其他事物和 main 都不是预定义的。 - Potatoswatter
1
请参阅C++14 §3.6.1和C11 §5.1.2.2.1,了解main允许的限制。C++规定:“实现不得预定义main函数”,而C则规定:“实现不为此函数声明原型。” - Potatoswatter
@manlio:请澄清你引用的内容。至于普通 C,引用是错误的。所以我猜它是C++标准中的任何一个,是吗? - dhein

19

“int main;”是一个合法的C/C++程序吗?

这需要视不同情况而定。

“int main;”是一个合法的C程序吗?

是。在自由站式实现中,可以接受这样的程序。在自由站式环境中,“main”并不一定具有特殊含义。

但在主机化环境下,它是不合法的。

“int main;”是一个合法的C++程序吗?

同上。

为什么会崩溃?

这个程序不一定在你的环境中有意义。在自由站式环境中,程序启动和结束以及“main”的含义都是由实现定义的。

为什么编译器会警告我?

编译器可以根据自己的喜好对你发出警告,只要它不拒绝符合规范的程序。另一方面,警告已足够诊断非符合性程序。由于这个编译单元不能成为有效的主机化程序的一部分,诊断信息是正当的。

gcc是自由站式环境还是主机化环境?

两者都可以。

gcc文档记录了-ffreestanding编译选项。加上它,警告就会消失。当构建内核或固件等项目时,可能需要使用它。

g++没有文档记录这样的编译选项。在此程序中提供它似乎没有任何效果。可以安全地假定g++提供的环境是主机化的。在这种情况下缺少诊断信息是一个错误。


17

这是一个警告,因为从技术上讲它并不被禁止。启动代码将使用"main"的符号位置,并带有三个标准参数(argc、argv和envp)跳转到它。它不能在链接时检查它是否实际上是一个函数,甚至不能检查它是否具有那些参数。这也是为什么int main(int argc, char **argv)能够工作-编译器不知道envp参数,它恰巧没有被使用,它是调用者清理。

开个玩笑,你可以做一些像这样的事情

int main = 0xCBCBCBCB;

在x86计算机上,忽略警告和类似的内容,它不仅可以编译,而且实际上也可以运行。

有人使用类似于这种技术编写了一个可在多个架构上直接运行的可执行文件(类似),链接如下:http://phrack.org/issues/57/17.html#article。 它也被用于赢得IOCCC比赛 - http://www.ioccc.org/1984/mullender/mullender.c


1
“这是一个警告,因为在C++中它在技术上并不被禁止。” - Cheers and hth. - Alf
3
“the three standard arguments (argc, argv and envp)”可能是在讨论Posix标准中的内容。这里需要翻译的是“三个标准参数(argc、argv和envp)”。 - Cheers and hth. - Alf
在我的系统上(Ubuntu 14/x64),以下代码可以使用gcc编译通过:int main __attribute__ ((section (".text")))= 0xC3C3C3C3; - csharpfolk
@Cheersandhth.-Alf 前两个是标准的,第三个是 POSIX。 - dascandy

9

这是一个有效的程序吗?

不是。

由于没有可执行部分,因此它不是一个程序。

它是否可以编译?

是的。

它能与有效的程序一起使用吗?

可以。

并非所有编译代码都需要是可执行的才能算有效。例如静态和动态库。

您实际上已经构建了一个目标文件。虽然它不是一个有效的可执行文件,但另一个程序可以通过在运行时加载该文件中的对象 main 来链接到它。

这应该是一个错误吗?

传统上,C++允许用户做一些似乎没有有效用途但符合语言语法的事情。

我的意思是,当然,这可能会被重新分类为错误,但为什么呢?如果警告可以发挥作用,那么将其视为错误有什么意义呢?

只要理论上存在这种功能在实际代码中使用的可能性,那么根据该语言,具有非函数对象名为main的情况极不可能导致错误。


1
@ChrisStratton:我认为Keith的论点是,链接失败是因为符号被多重定义了...因为“有效程序”除非定义了一个main函数,否则就不是有效程序。 - Ben Voigt
@BenVoigt 但如果它出现在库中,那么链接不会(并且可能无法)失败,因为在程序链接时,int main;定义将不可见。 - user743382
@hvd:为什么不行呢?它既没有显式的静态可见性,也没有隐式的静态可见性(例如在C++中的const int main;)。特别是如果没有其他符号提供,链接器可能会丢弃包含该定义的整个编译单元。 - Ben Voigt
@hvd:动态加载是一个有趣的讨论,但是除了链接之外,没有其他解释基思的评论的方式。"如果您在链接时不指定库"不符合"How can... link to it?"的要求。此外,这个问题是关于程序而不是库的。 - Ben Voigt
@BenVoigt 是的,我认为Keith Thompson没有注意到这个答案确实提到了在运行时加载库,所以他的评论对这个答案并不真正适用。但是当你说这个问题是关于程序时,你可能是有道理的。 - user743382
显示剩余6条评论

6
我想通过引用实际语言标准来补充已经给出的答案。
“int main;”是有效的C程序吗?
简短回答(我的意见):只有在您的实现使用“自由执行环境”时才有效。
以下所有引用都来自C11 5.环境
一个实现将C源文件转换并在两个数据处理系统环境中执行C程序,这些环境将称为翻译环境和执行环境[...]
5.1.2 执行环境
定义了两个执行环境:自由和托管。在两种情况下,当执行环境调用指定的C函数时,程序启动就会发生。
5.1.2.1 自由环境
在自由环境中(在其中C程序可以在没有操作系统的任何优势的情况下执行),在程序启动时调用的函数的名称和类型是实现定义的。

5.1.2.2 托管环境

不需要提供托管环境,但如果存在,则应符合以下规范。

5.1.2.2.1 程序启动

程序启动时调用的函数名为 main。[...] 它应定义为返回类型为 int,没有参数 [...] 或带有两个参数 [...] 或等效的其他实现定义方式。

从中可以观察到以下内容:

  • C11 程序可以具有独立运行环境或托管执行环境,并且有效。
  • 如果它具有独立运行环境,则无需存在主函数。
  • 否则,必须有一个返回类型为 int 的主函数。
在独立执行环境中,我认为如果没有像5.1.2所需的函数,不允许启动,则它是有效的程序。 在托管执行环境中,虽然您的代码引入了一个名为“main”的对象,但它无法提供返回值,因此从这个意义上说,我认为它不是有效的程序,尽管人们也可以像以前一样争论,如果该程序不是用于执行(例如可能只想提供数据),那么它就不允许执行这样的操作。

“int main;”是有效的C++程序吗?

简短回答(我的观点):只有当您的实现使用“独立执行环境”时。

C++14的引用

3.6.1 主函数

一个程序应该包含一个名为main的全局函数,它是程序的指定起点。在自由环境下是否需要定义一个main函数是由实现定义的。它应该有一个返回类型为int的返回类型,但其类型是由实现定义的。名称main没有被保留。与C11标准相比,在自由执行环境中应用更少的限制,因为根本没有提到启动函数,而对于托管执行环境,情况基本与C11相同。对于托管案例,我认为您的代码不是有效的C++14程序,但我确定它适用于自由案例。由于我的答案只考虑了执行环境,因此我认为dasblinkenlicht的答案也适用,因为发生在翻译环境中的名称重整发生在此之前。在这里,我不确定上面的引号是否被严格遵守。

4
我的观点是,在托管环境中,我认为这应该是一个错误。
错误在于您没有指定一个名为“main”的函数,该函数返回int类型,并试图在托管环境中使用程序。
假设您有一个编译单元,定义了一个名为“main”的全局变量。在独立环境中,这可能是合法的,因为在独立环境中,什么构成程序是由实现决定的。
假设您有另一个编译单元,定义了一个名为“main”的全局函数,该函数返回int类型且不带参数。这正是托管环境中程序所需的内容。
如果您只在独立环境中使用第一个编译单元,并且只在托管环境中使用第二个编译单元,则一切都很好。如果您在一个程序中同时使用两者怎么办?在C++中,您违反了一个定义规则。这是未定义行为。在C中,您违反了规定所有对单个符号的引用必须一致的规则;如果它们不一致,则是未定义行为。未定义行为是实现开发人员的“免责”卡。实现对未定义行为做出的任何响应都符合标准。实现不必警告或检测未定义行为。
如果您只使用其中一个编译单元,但使用了错误的编译单元(这就是您所做的),情况就不那么清晰了。在C中,情况很明确。在托管环境中未以两种标准形式之一定义函数“main”是未定义行为。假设您根本没有定义“main”。编译器/链接器不必对此错误发出任何警告。他们投诉是对他们的好意。C程序编译和链接没有错误是您的问题,而不是编译器的问题。
在C++中情况不太清楚,因为在托管环境中未定义函数“main”是一个错误,而不是未定义行为(换句话说,必须进行诊断)。然而,C++中的一个定义规则意味着链接器可能相当愚蠢。链接器的工作是解析外部引用,由于一个定义规则,链接器不必知道这些符号的含义。您提供了一个名为“main”的符号,链接器期望看到一个名为“main”的符号,因此从链接器的角度来看,一切都很好。

4

对于C语言来说,目前它是实现定义行为。

正如ISO/IEC9899所说:

5.1.2.2.1 程序启动

1 在程序启动时调用的函数名为main。实现对该函数不声明原型。它应该定义为int类型,并且没有参数:

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

或者有两个参数(这里称为argc和argv,尽管可以使用任何名称,因为它们是在声明它们的函数内部本地的):

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

或等效方式;或以其他实现定义的方式。


3

不,这不是一个有效的程序。

对于C++,最近通过defect report 1886: Language linkage for main()明确指出了这一点,其中写道:

似乎没有任何限制可以为main()提供显式的语言链接,但它可能应该是非法的或有条件支持的。

解决方案的一部分包括以下更改:

在全局范围内声明变量main或使用C语言链接声明名称main(在任何命名空间中)的程序是非法的。

我们可以在最新的C++ draft standard N4527中找到这个措辞,这是C++1z草案。

现在,最新版本的clang和gcc都会将其作为错误报告(请参见实时演示):

error: main cannot be declared as global variable
int main;
^

在这个缺陷报告之前,这是一种未定义的行为,不需要诊断。另一方面,格式不正确的代码需要进行诊断,编译器可以将其作为警告或错误。

感谢更新!很高兴看到编译器诊断现在已经开始使用。然而,我必须说我发现C++标准中的更改令人困惑。(有关背景,请参见上面关于main()名称重整的评论。)我理解禁止main()具有显式链接规范的基本原理,但我不理解它要求main()具有_C++链接_。当然,标准并没有直接解决如何处理ABI链接/名称重整的问题,但实际上(比如说,使用Itanium ABI),这将使main()重整为_Z4mainv。我错过了什么? - Geoff Nixon
我认为超级猫的评论已经涵盖了这一点。如果实现在调用用户定义的main函数之前自己做了一些事情,那么它很容易选择调用一个被混淆的名称。 - Shafik Yaghmour

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