我们可以用几种方式编写 main 函数,
int main()
int main(int argc,char *argv[])
int main(int argc,char *argv[],char * environment)
运行时 CRT 函数如何知道应该调用哪个 main。请注意这里,我不是在询问是否支持 Unicode。
接受的答案不正确,CRT 中没有特殊代码来识别 main() 声明的类型。
由于 cdecl 调用约定,它可以工作。它指定参数从右到左压入堆栈,并且调用者在调用后清理堆栈。因此 CRT 只需将所有参数传递给 main() 并在 main() 返回时再次弹出它们。您唯一需要做的就是在 main() 函数声明中以正确的顺序指定参数。argc 参数必须是第一个,它是堆栈顶部的参数。argv 必须是第二个,等等。省略一个参数没有区别,只要你也省略所有后面的参数。
这也是 printf() 函数可以工作的原因,它具有可变数量的参数。一个参数在一个已知的位置,第一个。
通常,编译器/链接器需要识别main
您正在使用的特定形式,然后包含代码以将其从系统启动函数适应您的 C 或 C++main
函数。
确实,特定平台上的特定编译器可以在不这样做的情况下逃脱,使用 Hans 在他的回答中描述的方法。但是,并非所有平台都使用堆栈来传递参数,并且可以编写具有不兼容参数列表的符合 C 和 C++ 实现。对于这种情况,编译器/链接器需要确定main
调用哪种形式。
嗯。似乎当前接受的答案(表明先前接受的答案不正确)本身就是不正确的。这个问题的标签表明它适用于 C++ 和 C,所以我会坚持 C++ 规范,而不是 C99。不管所有其他解释或论据如何,这个问题的主要答案是“main() 以实现定义的方式被特殊对待”。我相信大卫的答案在技术上比汉斯的更正确,但我会更详细地解释它......
main() 函数是一个有趣的函数,由编译器和链接器处理,其行为与其他函数不匹配。Hans 是正确的,CRT 中没有特殊代码来识别 main() 的不同签名,但他断言它“由于 cdecl 调用约定而工作”仅适用于特定平台,尤其是 Visual Studio。CRT 中没有识别 main() 不同签名的特殊代码的真正原因是没有必要。尽管这有点麻烦,但链接器的工作是在链接时将启动代码绑定到 main() 中,而不是 CRT 在启动时的工作。
根据 C++ 规范(参见第 3.6 节“开始和终止”),main() 函数的大部分处理方式是实现定义的。很可能大多数实现的编译器隐含地处理 main() 类似于外部“C”链接的东西,使 main() 处于非修饰状态,因此无论其函数原型如何,其链接器符号都是相同的。或者,实现的链接器可以足够聪明地扫描符号表,寻找任何其修饰名称解析为某种形式的“[int|void] main(...)”(请注意,作为返回类型的 void 是本身是特定于实现的东西,因为规范本身说 main() 的返回类型必须是'int')。一旦在可用符号中找到这样的函数,链接器就可以简单地使用启动代码引用“main()”的那个函数,所以确切的符号名称不一定要匹配任何特定的东西;它甚至可以是 wmain() 或其他,只要链接器知道要查找哪些变体,或者编译器为所有变体赋予相同的符号名称即可。
另外需要注意的关键是规范说 main() 可能不会被重载,因此链接器不应该在不同形式的 main() 的多个用户实现之间“挑选”。如果它找到多个,即使参数列表不匹配,这也是重复符号错误(或其他类似错误)。尽管所有实现“应”允许两者
int main() { /* ... */ }
和
int main(int argc, char* argv[]) { /* ... */ }
它们还被允许允许其他参数列表,包括您显示的包含环境字符串数组指针的版本,以及在任何给定实现中有意义的任何其他变体。
正如 Hans 所指出的,Visual Studio 编译器的 cdecl 调用约定(以及许多其他编译器的调用约定)提供了一个框架,其中调用者可以设置调用环境(即堆栈,或 ABI 定义的寄存器,或两者的某种组合)这样可以传递可变数量的参数,当被调用者返回时,调用者负责清理(将使用的参数空间从堆栈中弹出,或者在寄存器的情况下,不需要做任何清理)。这种设置很适合启动代码传递比可能需要更多的参数,并且用户的 main() 实现可以自由使用或不使用任何这些参数,就像许多平台对各种形式的处理的情况一样您在问题中列出的 main() 。然而,这不是编译器+链接器可以实现此目标的唯一方法:相反,链接器可以根据您的 main() 定义在不同版本的启动代码之间进行选择。这样做将允许使用 cdecl caller-cleanup 模型不可能的各种 main() 参数列表。而且由于所有这些都是实现定义的,因此根据 C++ 规范它是合法的,只要编译器+链接器至少支持上面显示的两种组合(int main()
和int main(int, char**)
)。
C 99 标准(5.1.2.2.1 程序启动)说,实现不强制main() 函数的原型,并且程序可以将其定义为:
1) int main(void);
2) int main(int argc, char *argv[]);
或以语义上等同于 2) 的方式,例如
2') int main(int argc, char **argv);
或以其他实现定义的方式。它不要求原型:
3) int main(int argc, char *argv[],char * envp[]);
将具有预期的行为 - 尽管该原型必须编译,因为任何原型都必须编译。3) 受到 GCC 和 Microsoft C 以及其他编译器的支持。(注意提问者的第三个原型有 char *envp 而不是 char *envp[],无论是偶然还是因为他/她有其他编译器)。
GCC 和 Microsoft C 都将使用任何原型编译 main(),因为它们应该这样做。它们解析您实际指定的原型并生成汇编语言以正确方式使用参数(如果有)。因此,例如,它们每个都会为程序生成预期的行为:
#include <stdio.h>
void main(double d, char c)
{
printf("%lf\n",d);
putchar(c);
}
如果您能找到一种将 double 和 char 直接传递给程序的方法,而不是通过字符串数组。
这些观察结果可以通过启用实验程序的汇编语言列表来验证。
编译器的标准 CRT 如何允许我们调用生成的 main() 实现的问题与如何将 main() 定义给编译器的问题不同。
对于 GCC 和 MS C,main() 可以以任何我们喜欢的方式定义。然而,在每种情况下,实现的标准 CRT,AFIK,仅支持将参数传递给 main(),而不是按照 3)。所以 1) - 2') 通过忽略多余的参数也将具有预期的行为,除了提供我们自己的非标准运行时之外,我们没有其他选择。
Hans Passant 的回答似乎偶然地误导了argc告诉函数以与 printf() 的第一个参数相同的方式消耗多少后续参数。如果argc存在,它仅表示作为第二个参数argv传递的数组中的元素数。它不指示传递给 main() 的参数数量。GCC 和 MS C 都通过解析您编写的原型来确定期望的参数 - 本质上是编译器对任何函数所做的事情,除了那些被定义为采用可变数量的参数的函数,如 printf()。
main() 不接受可变数量的参数。它接受您在定义中指定的参数,并且通常编译器的标准 CRT 假定它们是 (int, char *[], char *[])。
首先, main
函数在 GCC 中被专门处理(例如GCC 4.7 的源代码树的main_identifier_node
in 文件)gcc/c-family/c-common.c
C11 和 C++11 标准对此有特定的措辞和规范。
然后,C 调用 ABI 约定通常使得额外的参数不会造成太大的伤害。
所以你可以把它想象成语言规范和编译器都有关于“重载”的特定内容main
。
我什至认为这main
可能不是一个普通的功能。我相信标准中的某些词——我现在没有——可能会被理解为禁止获取其地址或递归main
。
在实践中,main
由一些汇编代码调用,这些代码编译成crt*.o
由gcc
. 用于gcc -v
了解更多正在发生的事情。