我知道写主要方法有两种不同的签名-
int main()
{
//Code
}
或者为了处理命令行参数,我们将其写成-
int main(int argc, char * argv[])
{
//code
}
在C++
中,我知道我们可以重载一个方法,但在C
中,编译器如何处理main
函数的这两个不同签名?
我知道写主要方法有两种不同的签名-
int main()
{
//Code
}
或者为了处理命令行参数,我们将其写成-
int main(int argc, char * argv[])
{
//code
}
在C++
中,我知道我们可以重载一个方法,但在C
中,编译器如何处理main
函数的这两个不同签名?
C语言的一些特性最初只是碰巧起作用的黑客攻击.
main的多个签名以及可变长度的参数列表就是这些特性之一.
程序员注意到,他们可以向函数传递额外的参数,并且给定的编译器不会发生任何不好的事情.
如果调用约定满足以下条件,则会出现这种情况:
遵循这些规则的一组调用约定是基于堆栈的参数传递,调用者通过该协议弹出参数,并将其从右向左推:
;; 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
可以是一个无参数的函数,在这种情况下,它对推到堆栈上的项不起作用.如果它是两个参数的函数,那么它会发现argc
和argv
是最上面的两个堆栈项.如果它是一个平台特定的三参数变量,带有一个环境指针(一个公共扩展名),那么它也会工作:它会发现第三个参数是堆栈顶部的第三个元素.
因此,固定调用适用于所有情况,允许单个固定启动模块链接到程序.该模块可以用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));
}
换句话说,这个start模块总是调用一个三参数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
函数(或者不管它叫什么),或者至少从几个预编译的备选方案中 Select 一个.可以将关于正在使用main
的哪种支持形式的信息存储在对象文件中.链接器可以查看此信息,并 Select 包含与程序定义兼容的main
调用的启动模块的正确版本.C实现通常只有少量支持的main
形式,所以这种方法是可行的.
C99语言的编译器总是需要在一定程度上特别处理main
,以支持这样一种攻击:如果函数在没有return
语句的情况下终止,行为就好像执行了return 0
.这同样可以通过代码转换来处理.编译器注意到正在编译一个名为main
的函数.然后判断身体的末端是否有可能到达.如果是,则插入一个return 0;