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

时间:2013-10-17 06:20:19

标签: c++ c

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

int main()
{
   //Code
}

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

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

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

9 个答案:

答案 0 :(得分:129)

C语言的一些功能最初只是偶然发生的黑客攻击。

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

程序员注意到他们可以将额外的参数传递给函数,并且在给定的编译器中没有任何不好的事情发生。

如果调用约定是这样的话,就是这种情况:

  1. 调用函数清除参数。
  2. 最左边的参数更接近堆栈的顶部,或者更靠近堆栈帧的底部,因此虚假参数不会使寻址无效。
  3. 遵循这些规则的一组调用约定是基于堆栈的参数传递,其中调用者弹出参数,并从右向左推送:

     ;; 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,以支持如果函数在没有return语句的情况下终止的行为,那么行为就好像{{1}被执行了。这也可以通过代码转换来处理。编译器注意到正在编译名为return 0的函数。然后它检查身体的末端是否可能到达。如果是,则插入main

答案 1 :(得分:34)

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

对于标准C

  

对于托管环境(这是正常环境),C99标准   表示:

     
    

5.1.2.2.1程序启动

         

程序启动时调用的函数名为main。该实现声明此函数没有原型。应该是     使用返回类型int定义并且没有参数:

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

或有两个参数(此处称为argcargv,但可以使用任何名称,因为它们是函数的本地名称     宣布):

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

或等效的; 9)或其他一些实现定义的方式。

         

9)因此,int可以替换为定义为int的typedef名称,或者argv的类型可以写为{{1}和,和     等等。

  

对于标准C ++:

  

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

     

1程序应包含一个名为main的全局函数,它是程序的指定开始。 [...]

     

2实现 不得 预定义主函数。 此功能不得超载。它应该   返回类型为int类型,但其类型是实现定义。   所有实施   应允许以下两个主要定义:

char **argv
     

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

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

托管环境(也支持C库的C环境)中 - 操作系统调用int main(int argc, char* argv[]) { /* ... */ }

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

)更改程序的入口点(或出口)
main

其中priority是可选的整数。

Pragma启动在main(优先级)之前执行该函数,而pragma exit在main函数之后执行该函数。如果有多个启动指令,则优先级决定哪个将首先执行。

答案 2 :(得分:8)

无需重载。是的,有2个版本,但当时只能使用一个版本。

答案 3 :(得分:5)

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

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

另请注意,即使在C ++中,它也不是重载...程序具有第一种形式或者具有第二种形式;它不能兼得。

答案 4 :(得分:4)

main的不同之处并不在于它可以通过多种方式定义,而是只能以两种不同的方式定义

main是用户定义的函数;实现没有声明它的原型。

foobar也是如此,但您可以按照自己喜欢的方式定义具有这些名称的函数。

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

至于编译器如何处理它,它取决于实现。大多数系统可能具有使两种形式有效兼容的调用约定,并且传递给没有参数定义的main的任何参数都被静静地忽略。如果没有,编译器或链接器就不会特别处理main。如果您对系统的工作原理感到好奇,那么您可能会看一些汇编列表。

与C和C ++中的许多内容一样,细节主要是由语言设计者及其前辈的历史和任意决策造成的。

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

答案 5 :(得分:3)

main只是链接器决定的起始地址的名称,其中main是默认名称。程序中的所有函数名称都是函数启动的起始地址。

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

答案 6 :(得分:2)

  

嗯,同一个函数main()的两个不同签名只有在您需要它们时才会显示在图片中,我的意思是如果您的程序在任何实际处理代码之前需要数据,您可以通过使用 -

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

其中变量argc存储传递的数据计数,argv是指向char的指针数组,指向从控制台传递的值。 否则,与

一起使用总是好的
    int main()
    {
       //Code
    }

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

答案 7 :(得分:2)

之前曾问过类似的问题:Why does a function with no parameters (compared to the actual function definition) compile?

排名靠前的答案之一是:

  

在C func()中表示您可以传递任意数量的参数。如果你   不需要参数,那么你必须声明为func(void)

所以,我想这是main的声明方式(如果你可以将术语“声明”应用于main)。事实上你可以这样写:

int main(int only_one_argument) {
    // code
}

它仍然可以编译并运行。

答案 8 :(得分:0)

你不需要覆盖它。因为一次只能使用一个。主要功能有2个不同版本