【问题标题】:How does the main() method work in C?main() 方法在 C 中是如何工作的?
【发布时间】:2013-10-25 12:30:10
【问题描述】:

我知道写main方法有两个不同的签名-

int main()
{
   //Code
}

或者为了处理命令行参数,我们把它写成-

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

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

【问题讨论】:

  • 重载是指在同一个程序中有两个同名的方法。C(或,实际上,几乎可以用任何具有这种结构的语言)。
  • C 没有方法;它有功能。方法是面向对象的“通用”函数的后端实现。程序调用带有一些对象参数的函数,对象系统根据它们的类型选择一个方法(或者可能是一组方法)。除非你自己模拟,否则 C 没有这些东西。
  • 关于程序入口点的深入讨论 - 不是特别main - 我推荐 John R. Levines 的经典书籍“Linkers & Loaders”。
  • 在 C 中,第一种形式是 int main(void),而不是 int main()(尽管我从未见过拒绝 int main() 形式的编译器)。
  • @harper:() 表单已经过时,甚至 main 是否允许使用它也不清楚(除非实现明确将其记录为允许的表单)。 C 标准(请参阅 5.1.2.2.1 程序启动)没有提及 () 形式,它与 () 形式相当等效。此评论的详细信息太长。

标签: c++ c


【解决方案1】:

C 语言的一些功能最初只是碰巧起作用的 hack。

main 的多个签名以及可变长度参数列表是这些功能之一。

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

如果调用约定如下:

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

【讨论】:

    【解决方案2】:

    即使在 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 的类型可以写为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 库的 C 环境)中 - 操作系统调用 main

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

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

    其中priority是一个可选的整数。

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

    【讨论】:

    • 我不认为,这个答案实际上回答了编译器如何实际处理这种情况的问题。在我看来,@Kaz 给出的答案提供了更多洞察力。
    • 我认为这个答案比@Kaz 更好地回答了这个问题。最初的问题是在发生运算符重载的印象下,这个答案通过显示编译器接受两个不同的签名而不是一些重载解决方案来解决这个问题。编译器细节很有趣,但不是回答问题所必需的。
    • 对于独立环境(“非托管”),除了一些#pragma 之外,还有很多事情要做。硬件有一个复位中断,这就是程序真正开始的地方。从那里开始执行所有基本设置:设置堆栈、寄存器、MMU、内存映射等。然后将初始化值从 NVM 复制到静态存储变量(.data 段),以及所有的“零输出”应设置为零的静态存储变量(.bss 段)。在 C++ 中,调用具有静态存储持续时间的对象的构造函数。一旦所有这些都完成了,就会调用 main。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 您也可以在 C 中省略 return 语句(自 C99 起)。
      • 在C中,你可以调用main()并获取它的地址; C++ 应用了 C 没有的限制。
      • @JonathanLeffler:你是对的,固定的。除了可以省略返回值之外,我在 C99 规范中发现的关于 main 的唯一有趣的事情是,由于标准的措辞是 IIUC,因此您不能在递归时将负值传递给 argc(5.1.2.2.1 不会指定对argcargv 的限制仅适用于对main 的初始调用)。
      【解决方案5】:

      main 的不同寻常之处不在于它可以以多种方式定义,而在于它只能以两种不同方式之一来定义。

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

      foobar 也是如此,但您可以使用这些名称以任何您喜欢的方式定义函数。

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

      至于编译器如何处理,这取决于实现。大多数系统可能具有使这两种形式有效兼容的调用约定,并且传递给未定义参数的main 的任何参数都会被忽略。如果不是,编译器或链接器将不难处理main。如果您对它在您的系统上的工作原理感到好奇,您可以查看一些程序集列表。

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

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

          好吧,同一函数 main() 的两个不同签名仅在您需要时才会出现,我的意思是,如果您的程序在对代码进行任何实际处理之前需要数据,您可以通过使用传递它们 -

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

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

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

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

          【讨论】:

            【解决方案8】:

            之前有人问过类似的问题: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
            }
            

            它仍然会编译和运行。

            【讨论】:

            • 出色的观察!似乎链接器对main 非常宽容,因为还有一个问题尚未提及:甚至main更多 参数! “Unix(但不是 Posix.1)和 Microsoft Windows”添加 char **envp(我记得 DOS 也允许这样做,不是吗?),Mac OS X 和 Darwin 添加了另一个“任意操作系统提供的信息”字符* 指针。 wikipedia
            【解决方案9】:

            你不需要覆盖这个。因为一次只会使用一个。是的,有两个不同版本的 main 函数

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2015-08-12
              • 1970-01-01
              • 1970-01-01
              • 2013-09-26
              • 1970-01-01
              • 2013-10-24
              • 2012-10-10
              • 2010-10-17
              相关资源
              最近更新 更多