我知道有两种不同的签名来编写主方法 -
int main()
{
//Code
}
Run Code Online (Sandbox Code Playgroud)
或者为了处理命令行参数,我们将其写为 -
int main(int argc, char * argv[])
{
//code
}
Run Code Online (Sandbox Code Playgroud)
在C++
我知道我们可以重载的方法,但是在C
编译器如何处理这两个不同的签名main
功能?
Kaz*_*Kaz 130
C语言的一些功能最初只是偶然发生的黑客攻击.
主要以及可变长度参数列表的多个签名是这些功能之一.
程序员注意到他们可以将额外的参数传递给函数,并且在给定的编译器中没有任何不好的事情发生.
如果调用约定是这样的话,就是这种情况:
遵守这些规则的一组调用约定是基于堆栈的参数传递,其中调用者弹出参数,并且它们被从右向左推送:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
Run Code Online (Sandbox Code Playgroud)
在这种类型的调用约定的编译器中,没有什么特别需要支持这两种main
甚至是其他种类.main
可以是没有参数的函数,在这种情况下,它不会被推到堆栈上的项目.如果它是两个参数的函数,那么它会找到argc
并argv
作为两个最顶层的堆栈项.如果它是具有环境指针(公共扩展名)的特定于平台的三参数变体,那么它也将起作用:它将发现第三个参数作为堆栈顶部的第三个元素.
因此,固定呼叫适用于所有情况,允许将单个固定启动模块链接到程序.该模块可以用C语言编写,其功能类似于:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
Run Code Online (Sandbox Code Playgroud)
换句话说,这个启动模块总是调用一个三参数main.如果main不接受任何参数,或者只接受int, char **
它,它恰好工作正常,以及由于调用约定它不需要参数.
如果你在你的程序中做这种事情,它将是不可移植的并且被ISO C认为是未定义的行为:以一种方式声明和调用函数,并在另一种方式中定义它.但编译器的启动技巧不一定是可移植的; 它不受便携式程序规则的指导.
但是假设调用约定是这样的,它不能以这种方式工作.在这种情况下,编译器必须main
特别处理.当它注意到它正在编译main
函数时,它可以生成与三参数调用兼容的代码.
也就是说,你写这个:
int main(void)
{
/* ... */
}
Run Code Online (Sandbox Code Playgroud)
但是当编译器看到它时,它基本上执行代码转换,以便它编译的函数看起来更像这样:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
Run Code Online (Sandbox Code Playgroud)
除了名字__argc_ignore
不存在.您的范围中没有引入此类名称,并且不会对未使用的参数发出任何警告.代码转换使编译器发出具有正确链接的代码,该链接知道它必须清理三个参数.
另一种实现策略是编译器或者链接器自定义生成__start
函数(或其任何调用的函数),或者至少从几个预编译的备选项中选择一个.可以在目标文件中存储关于main
正在使用哪种支持形式的信息.链接器可以查看此信息,并选择正确版本的启动模块,该模块包含main
与程序定义兼容的调用.C实现通常只有少量支持的形式,main
因此这种方法是可行的.
C99语言的编译器总是要在main
某种程度上特别对待支持黑客,如果函数在没有return
语句的情况下终止,那么行为就好像return 0
被执行了一样.这也可以通过代码转换来处理.编译器注意到main
正在编译一个被调用的函数.然后它检查身体的末端是否可能到达.如果是这样,它会插入一个return 0;
Sad*_*que 34
main
即使在C++中也没有超载.主要功能是程序的入口点,只应存在单个定义.
对于标准C.
对于托管环境(这是正常环境),C99标准说:
5.1.2.2.1程序启动
在程序启动时调用的函数被命名
main
.该实现声明此函数没有原型.它应定义为返回类型int
且没有参数:Run Code Online (Sandbox Code Playgroud)int main(void) { /* ... */ }
或者有两个参数(这里称为
argc
和argv
,虽然可以使用任何名称,因为它们是声明它们的函数的本地名称):Run Code Online (Sandbox Code Playgroud)int main(int argc, char *argv[]) { /* ... */ }
或同等学历; 9)或以其他一些实现定义的方式.
9)因此,
int
可以替换为定义为的typedef名称int
,或者argv
可以写为的类型char **argv
,依此类推.
对于标准C++:
3.6.1主要功能[basic.start.main]
1程序应包含一个名为main的全局函数,它是程序的指定开始.[...]
2实现不应预定义主要功能.此功能不应过载.它应该具有int类型的返回类型,否则其类型是实现定义的.所有实现都应允许以下两个主要定义:
Run Code Online (Sandbox Code Playgroud)int main() { /* ... */ }
和
Run Code Online (Sandbox Code Playgroud)int main(int argc, char* argv[]) { /* ... */ }
C++标准明确地说"它[主函数]应该具有int类型的返回类型,但是其类型是实现定义的",并且需要与C标准相同的两个签名.
在托管环境(也支持C库的AC环境)中 - 操作系统调用main
.
在非托管环境(一个用于嵌入式应用程序)中,您始终可以使用预处理器指令(如预处理器指令)更改程序的入口点(或出口)
#pragma startup [priority]
#pragma exit [priority]
Run Code Online (Sandbox Code Playgroud)
优先级是可选的整数.
Pragma启动在main(优先级)和pragma exit执行主函数之后执行函数之前执行函数.如果有多个启动指令,则优先级决定哪个将首先执行.
这是C和C++语言的奇怪的不对称性和特殊规则之一.
在我看来,它只是出于历史原因而存在,并且它背后没有真正严重的逻辑.请注意,main
由于其他原因,这也是特殊的(例如,main
在C++中不能递归,你不能使用它的地址,在C99/C++中你可以省略最后的return
语句).
另请注意,即使在C++中,它也不是一个重载...程序具有第一个表单或者具有第二个表单; 它不能兼得.
不同寻常的main
不是它可以用不止一种方式定义,而是它只能用两种不同方式之一来定义。
main
是一个用户定义的函数;该实现没有为其声明原型。
foo
对于or也是如此bar
,但是您可以以任何您喜欢的方式使用这些名称定义函数。
不同之处在于它main
是由实现(运行时环境)调用的,而不仅仅是由您自己的代码调用。该实现不限于普通的 C 函数调用语义,因此它可以(并且必须)处理一些变化 - 但不需要处理无限多种可能性。该int main(int argc, char *argv[])
形式允许使用命令行参数,int main(void)
在 C 或int main()
C++ 中只是为了方便不需要处理命令行参数的简单程序。
至于编译器如何处理这个,取决于实现。大多数系统可能具有使两种形式有效兼容的调用约定,并且传递给main
不带参数的定义的任何参数都会被悄悄忽略。如果不是,编译器或链接器进行特殊处理并不困难main
。如果您好奇它如何在您的系统上工作,您可以查看一些程序集列表。
与 C 和 C++ 中的许多事情一样,细节很大程度上是历史和语言设计者及其前辈做出的任意决定的结果。
请注意,C 和 C++ 都允许其他实现定义的定义main
-- 但很少有任何充分的理由使用它们。对于独立实现(例如没有操作系统的嵌入式系统),程序入口点是实现定义的,甚至不一定称为main
.