main()方法如何在C中工作?


96

我知道有两种不同的签名可以编写main方法-

int main()
{
   //Code
}

或用于处理命令行参数,我们将其写为-

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

C++我知道我们可以重载的方法,但是在C编译器如何处理这两个不同的签名main功能?


14
重载是指在同一程序中有两个具有相同名称的方法main在一个程序中C,您只能拥有一种方法(或者,实际上,几乎任何具有这种构造的语言都可以)。
Kyle Strand

12
C没有方法;它具有功能。方法是面向对象的“通用”功能的后端实现。程序调用带有某些对象参数的函数,然后对象系统根据其类型选择一个方法(或一组方法)。除非您自己模拟C,否则C没有任何这些东西。
卡兹(Kaz)

4
对于程序入口点的深入讨论(不是特别地)main,我推荐John R. Levines的经典著作“ Linkers&Loaders”。
安德里亚斯·斯平德勒

1
在C语言中,第一种形式int main(void)不是,int main()(尽管我从未见过拒绝该int main()形式的编译器)。
基思·汤普森

1
@harper:该()表格已过时,并且尚不清楚是否允许使用main(除非实现明确将其记录为允许的表格)。C标准(请参阅5.1.2.2.1程序启动)没有提及该()表单,该表单与该表单并不完全相同()。详细信息对此评论过长。
基思·汤普森

Answers:


132

C语言的某些功能起初只是一些hack,而这些hack恰好起作用了。

主功能列表和变长参数列表的多个签名是这些功能之一。

程序员注意到,他们可以将额外的参数传递给函数,并且给定的编译器也不会发生任何不良情况。

如果调用约定如下:

  1. 调用函数清除参数。
  2. 最左边的参数更靠近堆栈的顶部或堆栈框架的底部,因此伪参数不会使寻址无效。

遵循这些规则的一组调用约定是基于堆栈的参数传递,由此调用者弹出参数,并将它们从右向左推送:

 ;; 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

在这种调用约定是这种情况的编译器中,不需要做特别的事情就可以支持这两种main,甚至是其他两种。main可以是不带参数的函数,在这种情况下,它可以忽略被压入堆栈的项目。如果它是两个参数的函数,那么它将找到argcargv作为两个最顶层的堆栈项。如果它是具有环境指针(公共扩展)的特定于平台的三参数变体,那么它也将起作用:它将从堆栈顶部找到第三个参数作为第三个元素。

因此,固定调用适用于所有情况,从而允许将单个固定启动模块链接到程序。该模块可以用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));
}

换句话说,此启动模块始终始终调用三个参数的main。如果main不带参数或仅int, char **带参数,则由于调用约定,碰巧也可以正常工作,也可以不带参数。

如果要在程序中执行此类操作,则ISO C将是不可移植的,并且将其视为未定义的行为:以一种方式声明和调用函数,然后以另一种方式定义函数。但是,编译器的启动技巧不必具有可移植性。它不受可移植程序规则的指导。

但是,假设调用约定是如此,以至于它不能以这种方式工作。在这种情况下,编译器必须main特别对待。当它注意到正在编译main函数时,它可以生成与例如三个参数调用兼容的代码。

就是说,你这样写:

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

但是,当编译器看到它时,它实际上执行了代码转换,因此编译的函数看起来更像这样:

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

只是名称__argc_ignore实际上并不存在。不会将此类名称引入您的范围,并且不会对未使用的参数发出任何警告。代码转换使编译器发出具有正确链接的代码,该链接知道必须清除三个参数。

另一种实现策略是让编译器或链接器自定义生成__start函数(或所谓的函数),或者至少从几个预编译的替代方法中选择一个。可以将有关使用哪种受支持形式的信息存储在目标文件中main。链接器可以查看此信息,并选择包含main与程序定义兼容的调用的启动模块的正确版本。C实现通常只有少量受支持的形式,main因此这种方法是可行的。

C99语言的编译器在main某种程度上始终必须特别对待,以支持这样的hack:如果函数在没有return声明的情况下终止,则行为就好像return 0已执行一样。同样,这可以通过代码转换来解决。编译器注意到main正在调用一个函数。然后,它检查身体的末端是否可能触及。如果是这样,它将插入一个return 0;


34

main即使在C ++中也没有重载。主要功能是程序的入口点,并且仅应存在一个定义。

对于标准C

对于托管环境(这是正常的环境),C99标准规定:

5.1.2.2.1程序启动

程序启动时调用的函数名为main。该实现没有为此函数声明任何原型。它的返回类型应为int且不带参数:

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

或带有两个参数(尽管可以使用任何名称,因为它们在声明它们的函数中是局部的,所以在这里称为argcargv,):

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

或同等学历; 9)或其他实现定义的方式。

9)因此,int可以用定义为的typedef名称替换intargv也可以将其写为char **argv,等等。

对于标准C ++:

3.6.1主要功能[basic.start.main]

1程序应包含一个称为main的全局功能,这是程序的指定启动位置。[...]

2实现不得预定义主要功能。此功能不得重载。它的返回类型应该是int类型,否则它的类型是实现定义的。所有实现都应允许以下两个main定义:

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

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

C ++标准明确指出“ [[主函数]应该具有int类型的返回类型,但否则其类型是实现定义的”,并且需要与C标准相同的两个签名。

托管环境(也支持C库的AC环境)中,操作系统调用main

非托管环境(用于嵌入式应用程序的环境)中,您始终可以使用预处理器指令(例如,

#pragma startup [priority]
#pragma exit [priority]

其中优先级是一个可选的整数。

编译指示启动在主函数之前执行功能(优先级),编译指示退出在主函数之后执行功能。如果有多个启动指令,则优先级决定哪个将首先执行。


4
我不认为,此答案实际上回答了编译器实际上如何处理这种情况的问题。在我看来,@ Kaz给出的答案提供了更多的见识。
Tilman Vogel 2013年

4
我认为这个答案比@Kaz更好地回答了这个问题。最初的问题是在出现运算符重载的印象,此答案通过显示编译器接受两个不同的签名而不是某些重载解决方案来解决。编译器的细节很有趣,但不一定要回答这个问题。
Waleed Khan 2013年

1
对于独立的环境(“非托管”环境),正在进行的工作远不止一些#pragma。硬件会产生一个复位中断,这才是程序开始的地方。从那里开始,执行所有基本设置:设置堆栈,寄存器,MMU,内存映射等。然后将初始化值从NVM复制到静态存储变量(.data段),并对所有值进行“零输出”应该设置为零(.bss段)的静态存储变量。在C ++中,将调用具有静态存储持续时间的对象的构造函数。完成所有操作后,将调用main。
伦丁

8

不需要过载。是的,有2个版本,但当时只能使用一个。


5

这是C和C ++语言奇怪的不对称性和特殊规则之一。

我认为它的存在仅出于历史原因,其背后并没有真正的严肃逻辑。请注意,main由于其他原因,它也是特殊的(例如,main在C ++中不能递归,并且不能使用其地址;在C99 / C ++中,可以省略最终return语句)。

还要注意,即使在C ++中,它也不是重载……程序具有第一种形式或第二种形式;它不能两者兼有。


您也可以省略returnC中的语句(自C99起)。
dreamlax

在C中,您可以调用main()并获取其地址;C ++会应用C不会的限制。
乔纳森·莱夫勒

@JonathanLeffler:你是正确的,固定的。除了可以忽略返回值外,我在C99规范中发现的关于main的唯一有趣的事情是,由于标准用IIUC表示,因此递归时不能传递负值argc(5.1.2.2.1并未指定限制argc并且argv仅适用于对的初始调用main
6502,

4

main唯一不同的是,它不能以多种方式定义,而是只能以两种不同方式之一进行定义。

main是用户定义的函数;该实现未为其声明原型。

同样的事情是真实的foo还是bar,但你可以定义这些名称的功能你喜欢的任何方式。

区别在于,它main是由实现(运行时环境)调用的,而不仅仅是由您自己的代码调用的。该实现方式不仅限于普通的C函数调用语义,因此它可以(而且必须)处理一些变体-但不需要处理无限多种可能性。该int main(int argc, char *argv[])格式允许使用命令行参数,对于不需要处理命令行参数的简单程序,使用int main(void)C或int main()C ++只是一种方便。

至于编译器如何处理它,取决于实现。大多数系统可能具有使两种形式有效兼容的调用约定,并且传递给main不带参数的定义的任何参数都将被静默忽略。如果没有,那么对编译器或链接器进行main特殊处理就不会困难。如果您好奇它如何在您的系统上工作,可以查看一些程序集清单。

就像C和C ++中的许多东西一样,细节在很大程度上是历史的结果以及语言设计者及其前身做出的任意决定。

请注意,C和C ++都允许使用-的其​​他实现定义的定义,main但是很少有充分的理由使用它们。对于独立的实现(例如,没有OS的嵌入式系统),程序入口点是实现定义的,甚至不必调用main


3

main仅仅是通过在连接器决定一个起始地址的名称main是默认名称。程序中的所有函数名称都是函数开始的起始地址。

函数参数是从堆栈中压入/弹出的,因此,如果没有为函数指定参数,则不会在堆栈上压入/弹出的参数。这就是main可以在有或没有参数的情况下工作的方式。


2

好吧,只有在需要时,同一函数main()的两个不同签名才会出现在图片中,也就是说,如果您的程序在进行代码的任何实际处理之前需要数据,则可以使用-

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

其中,变量argc存储传递的数据计数,而argv是指向char的指针数组,该指针指向从控制台传递的值。否则,与它搭配总是很好

    int main()
    {
       //Code
    }

但是,在任何情况下,一个程序中只能有一个main(),因为这是程序从其开始执行的唯一点,因此不能超过一个。(希望它值得)


2

之前有人问过类似的问题:为什么没有参数的函数(与实际函数定义相比)会编译?

排名最高的答案之一是:

C func()表示您可以传递任意数量的参数。如果您不想要任何参数,则必须声明为func(void)

因此,我想这main就是声明的方式(如果您可以将术语“ declared”应用于main)。实际上,您可以这样写:

int main(int only_one_argument) {
    // code
}

并且仍将编译并运行。


1
极好的观察!它出现的链接是相当宽容的main,因为有没有提及的一个问题:甚至更多的参数main!“添加了Unix(但不是Posix.1)和Microsoft Windows” char **envp(我记得DOS也允许这样做,不是吗?),Mac OS X和Darwin添加了另一个“操作系统提供的任意信息” char *指针。维基百科
usr2564301 2013年

0

您不需要覆盖此功能,因为一次只能使用一个。是的,主要功能有2个不同的版本

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.