96

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

int main()
{
   //Code
}

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

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

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

4

9 回答 9

135

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;

于 2013-10-17T06:50:07.557 回答
34

main即使在 C++中也没有重载。主函数是程序的入口点,应该只存在一个定义。

对于标准 C

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

5.1.2.2.1 程序启动

程序启动时调用的函数名为main. 实现没有声明这个函数的原型。它应定义为返回类型int且不带参数:

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

或带有两个参数(这里称为argcand argv,尽管可以使用任何名称,因为它们对于声明它们的函数是本地的):

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 类型的返回类型,否则它的类型是实现定义的。所有实现都应允许以下两种 main 定义:

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

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

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

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

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

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

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

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

于 2013-10-17T06:23:13.617 回答
8

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

于 2013-10-17T06:22:38.840 回答
5

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

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

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

于 2013-10-17T06:34:01.563 回答
4

不同寻常main的不是它可以用一种以上的方式来定义,而是它只能用两种不同的方式之一来定义。

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

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

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

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

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

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

于 2013-10-23T06:49:20.360 回答
3

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

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

于 2013-10-17T06:24:17.113 回答
2

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

排名最高的答案之一是:

在 Cfunc()中意味着您可以传递任意数量的参数。如果你不想要任何参数,那么你必须声明为func(void)

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

int main(int only_one_argument) {
    // code
}

它仍然会编译和运行。

于 2013-10-23T22:32:37.660 回答
2

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

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

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

    int main()
    {
       //Code
    }

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

于 2013-10-17T13:14:08.193 回答
0

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

于 2013-10-25T13:52:31.130 回答