为什么C语言的主函数可以有或没有参数?

12

只是想知道为什么这样

int main(void){}

编译和链接

这个也是:

int main(int argc, char **argv){}
为什么不需要强制选择其中之一呢?
gcc甚至可以使用一个参数进行编译和链接:
int main(int argc){}

但使用 -Wall 选项会发出这个警告:

smallest_1.5.c:3:1: warning: ‘main’ takes only zero or two arguments [-Wmain]

我不是在问"为什么他们允许这样做?",而是在问"调用者和链接器如何处理main函数的多个可能性?"


10
简短回答是“因为标准规定如此”。 - Jeffery Thomas
@JefferyThomas 我的问题不是关于“为什么允许它”,而是“它是如何工作的”?调用程序和链接器是如何同时处理两种定义的呢? - Scooter
2
因为调用代码可以在寄存器或堆栈中传递参数。两个参数的主函数使用它们,而零个参数的主函数不对它们进行任何操作。就是这么简单。链接甚至都没有涉及到。 - Jens
1
非常相关的问题 - edmz
5个回答

9

我以下是从Linux的角度出发。

main函数在标准定义中非常特殊(对于托管的C11实现)。最近的编译器(包括GCCClang/LLVM)也明确知道它,它们有特定的代码来处理main(并提供此警告)。顺便说一下,GCC(通过GNU libc头文件和function attributes的帮助)也有printf的特殊代码。您还可以使用MELT添加自己的自定义功能属性到GCC中。

对于链接器来说,main通常是一个常规符号,但它从crt0调用(使用gcc -v编译您的代码以了解其真正含义)。顺便说一下,ld(1)链接器(以及ELF文件,例如可执行文件目标文件)没有类型或函数签名的概念,只处理名称(这就是为什么C++编译器会进行一些名称重整的原因)。

这里的ABI调用约定是这样定义的,即将未使用的参数传递给函数(例如main甚至open(2)...)不会造成任何损害(几个参数在寄存器中传递)。详细信息请阅读x86-64 System V ABI

还可以参考此答案中的参考文献。

最后,你真的应该将你的main定义为int main(int argc, char**argv),除此之外不要添加任何内容,并且希望你通过它们处理程序参数(至少按照GNU编码标准规定处理--help--version)。在Linux上,我讨厌那些不这样做的程序(我咒骂他们的程序员),所以处理--help--version

2
因为调用代码可以在寄存器或堆栈上传递参数。两个参数的 main 函数使用它们,而零个参数的 main 函数不对它们做任何操作。就是这么简单。链接甚至没有参与其中。
如果您担心被调用代码中的堆栈调整,那么 main 函数只需要确保在返回时堆栈指针相同(甚至在这种情况下通常也无关紧要,例如当 ABI 指定调用者负责堆栈管理时)。

1
程序启动时调用的函数名为main。实现不为此函数声明原型。应使用返回类型为int且无参数定义该函数: ``` int main(void) { /* ... */ } ``` 或使用两个参数(这里称为argc和argv,但可以使用任何名称,因为它们是在声明它们的函数中本地的): ``` int main(int argc, char *argv[]) { /* ... */ } ``` 或等效方式;或以其他某种实现定义的方式。
关于参数:
第一个参数计算提供给程序的参数数量,第二个参数是指向这些参数的字符串的指针数组。这些参数由命令行解释器传递给程序。 因此,两种可能性如下处理: 1. 如果没有声明参数:不需要输入任何参数。 2. 如果在`main()`中有参数,则应该: - `argc`大于零。 - `argv [argc]`是一个空指针。 - `argv [0]`到`argv [argc-1]`是指向字符串的指针,其含义将由程序确定。 - `argv [0]`将是包含程序名称的字符串,如果不可用则为空字符串。 `argv`的其余元素表示提供给程序的参数。 在只支持单大小写字符的情况下,这些字符串的内容将以小写形式提供给程序。

在堆栈中,它们将被放置在返回地址和保存的基指针上方,就像任何其他堆栈帧一样。

在机器级别上:

它们将通过寄存器传递,具体取决于实现方式。


但是C语言不允许函数重载。 - el.pescado - нет войне
@el.pescado 提醒您,我已经写过“人们可以考虑main()函数……”。不过,这是一个好问题! - Ziezi
然而,在这种泛化中存在陷阱。函数重载通常通过将函数签名追加到符号名称(例如,在C ++中进行名称缠绕)来工作,以便两个具有相同名称的函数分配不同的符号。 在这种情况下,它可以工作,因为没有发生这样的事情-任何形式的“main”都具有相同的符号名称(在大多数情况下为“main”或“_main”)。 - el.pescado - нет войне
此外,尽管任何形式的“main”都可能有效,但你只能有一个。 - el.pescado - нет войне
@el.pescado 谢谢你的见解!我将对答案的引言进行改述。 - Ziezi

1
使其工作与二进制格式可执行文件和操作系统的加载器有关。链接器不关心(好吧,它稍微关心一下:它需要标记入口点),唯一的调用程序是加载器。
任何系统的加载器都必须知道如何将支持的二进制格式带入内存并分支到入口点。这在系统和二进制格式上略有不同。
如果您有关于特定操作系统/二进制格式的问题,您可能需要澄清。

1
简短回答:如果您不使用参数,则可以以两种方式声明不带参数的主函数:
int main(void)

或者

int main()

第一个意味着main是一个没有参数的函数。第二个意味着main是一个带任意数量参数的函数。
由于你不访问参数,两者都可以。任何编译器都有“特殊”代码来检查main的参数是错误的。(但: main必须返回一个值。)

TIL。有趣的是这也是C与C ++不同的另一个方面,即在定义中带参数的函数仍然可以在声明中没有任何参数地声明。 https://softwareengineering.stackexchange.com/a/287002/331025 - Vishal Sharma

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