考虑非常简单:
int main(void) {
return 0;
}
我编译了它(使用mingw32-gcc)并将其作为main.exe foo bar
执行。
现在,我曾预料到由明确声明为没有 life 参数的主函数引起的某种崩溃或错误。缺乏错误导致了这个问题,这实际上是四个问题。
为什么这样做? 答案:因为标准是这样说的!
输入参数是否被忽略,或者是使用argc& amp; argv默默地? 答案:在这种特殊情况下,堆栈已准备就绪。
如何验证以上内容? 答案:请参阅rascher的回答。
这个平台是否依赖? 答案:是,否。
答案 0 :(得分:20)
我不知道您的问题的跨平台答案。但这让我很好奇。那么我们该怎么办?看看堆栈!
第一次迭代:
test.c的
int main(void) {
return 0;
}
test2.c中
int main(int argc, char *argv[]) {
return 0;
}
现在看看汇编输出:
$ gcc -S -o test.s test.c
$ cat test.s
.file "test.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
movl $0, %eax
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits
这里没有什么令人兴奋的。除了一件事:两个C程序具有相同的汇编输出!
这基本上是有道理的;我们从来没有真正必须从main()的堆栈中推送/弹出任何东西,因为它是调用堆栈中的第一个东西。
然后我写了这个程序:
int main(int argc, char *argv[]) {
return argc;
}
它的主题:
main:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
popl %ebp
ret
这告诉我们“argc”位于8(%ebp)
现在又有两个C程序:
int main(int argc, char *argv[]) {
__asm__("movl 8(%ebp), %eax\n\t"
"popl %ebp\n\t"
"ret");
/*return argc;*/
}
int main(void) {
__asm__("movl 8(%ebp), %eax\n\t"
"popl %ebp\n\t"
"ret");
/*return argc;*/
}
我们从上面偷了“return argc”代码并将其粘贴到这两个程序的asm中。当我们编译并运行它们,然后调用echo $?
(它回应前一个进程的返回值)时,我们得到了“正确”的答案。因此,当我运行“./test a b c d”时,$?
为两个程序提供“5” - 即使只有一个定义了argc / argv。这告诉我,在我的平台上,argc肯定会放在堆栈上。我敢打赌,类似的测试会证实这是argv。
在Windows上试试这个!
答案 1 :(得分:10)
来自C99标准:
5.1.2.2.1程序启动
程序启动时调用的函数名为main。实施宣布否 这个功能的原型。它应该使用返回类型int和no来定义 参数:
int main(void) { /* ... */ }
或有两个参数(这里称为argc和argv,但可能有任何名称 使用,因为它们是声明它们的函数的本地函数):
int main(int argc,char * argv []){/ * ... * /}
或同等的;或者以其他一些实现定义的方式。
答案 2 :(得分:5)
在经典C中,你可以做类似的事情:
void f() {}
f(5, 6);
没有什么可以阻止你调用具有不同数量参数的函数,因为它的定义假定。 (现代编译器自然会认为这是一个令人震惊的错误,并且强烈反对实际编译代码。)
您的main()
功能也会发生同样的情况。 C运行时库将调用
main(argc, argv);
但是你的函数不准备接收这两个参数的事实对调用者没有任何影响。
答案 3 :(得分:3)
在大多数编译器中,__ argc和__argv作为运行时库中的全局变量存在。这些值是正确的。
在Windows上,如果入口点具有UTF-16签名,它们将不正确,这也是在该平台上获取正确命令参数的唯一方法。在这种情况下它们将是空的,但这不是你的情况,并且有两个widechar替代变量。
答案 4 :(得分:2)
有一些注意事项要做。
标准基本上说明了最主要的是:一个不带参数的函数,一个带两个参数的函数,或者其他什么!
例如,请参阅我对this question的回答。
但你的问题指向其他事实。
为什么这样做?答:因为 标准是这样说的!
这是不正确的。它的工作原因有其他原因。它的工作原理是调用约定。
这些约定可以是:在栈上推送参数,调用者负责清理堆栈。因此,在实际的asm代码中,被调用者可以完全忽略堆栈中的内容。电话看起来像
push value1
push value2
call function
add esp, 8
(英特尔的例子,只是为了保持主流)。
在堆栈上推送参数的功能是完全无趣的,一切都会正常工作!即使调用约定不同,这确实是正确的,例如。
li $a0, value
li $a1, value
jal function
如果函数考虑了寄存器$ a0和$ a1,则不会改变任何内容。
所以被调用者可以忽略没有伤害参数,cn相信它们不存在,或者它可以知道它们存在,但更喜欢忽略它们(相反,如果被调用者从堆栈或寄存器获取值,则会出现问题,虽然来电者没有通过任何事情。)
这就是事情有效的原因。
从C的角度来看,如果我们在一个系统中,启动代码使用两个参数(int和char **)调用main并且期望一个int返回值,那么“right”原型将是
int main(int argc, char **argv) { }
但我们现在假设我们不使用这些论点。
说int main(void)
或int main()
更正确(仍然在同一系统中,实现使用两个args调用main并且期望一个int返回值,如前所述)?
事实上,标准并未说明我们必须做什么。正确的“原型”表明我们有两个参数仍然是之前显示的那个。
但从逻辑的角度来看,正确的说法(我们知道它)但我们对它们不感兴趣的方式是
int main() { /* ... */ }
在this answer我已经展示了如果我们将参数传递给声明为int func()
的函数会发生什么,如果我们将参数传递给声明为int func(void)
的函数会发生什么。
在第二种情况下,我们有一个错误,因为(void)
明确表示该函数没有参数。
使用main
我们无法收到错误,因为我们没有真正的原型强制参数,但值得注意的是gcc -std=c99 -pedantic
没有给int main()
或int main(void)
发出警告{1}},这意味着1)即使使用std
标志,gcc也不符合C99,或者2)两种方式都符合标准。更可能的是选项2。
一个是明确的标准兼容(int main(void)
),另一个是int main(int argc, char **argv)
,但没有明确说出参数,因为我们对它们不感兴趣。
int main(void)
即使在存在论证时也能正常工作,因为我以前所写过的。但它表示,主要没有争论。虽然在很多情况下,如果我们可以写int main(int argc, char **argv)
,那么它就是假的,而int main()
必须是首选。
另一个有趣的事情是,如果我们说main在实现需要返回值的系统上没有返回值(void main()
),我们会得到一个警告。这是因为调用者希望它对它执行某些操作,因此如果我们不返回值,则它是“未定义的行为”(这并不意味着在return
情况下放置明确的main
,但声明main
返回int)。
在许多启动代码中,我看到主要是通过以下方式之一调用的:
retval = main(_argc, _argv);
retval = main(_argc, _argv, environ);
retval = main(_argc, _argv, environ, apple); // apple specific stuff
但是可能存在以不同方式调用main的启动代码,例如retval = main()
;在这种情况下,为了显示这一点,我们可以使用int main(void)
,另一方面,使用int main(int argc, char **argv)
会编译,但如果我们实际使用参数会使程序崩溃(因为检索到的值将是垃圾)。
这个平台是否依赖?
调用main的方式是依赖于平台(特定于实现),如标准所允许的那样。 “假定的”主要原型是一种结果,如前所述,如果我们知道有传入的论点但我们不会使用它们,我们应该使用int main()
作为较长的int main(int argc, char **argv)
的短格式,而int main(void)
意味着不同的东西:即主要没有参数(在我们正在考虑的系统中是错误的)
答案 5 :(得分:1)
为什么会起作用:通常,函数参数在特定位置传递(通常是寄存器或堆栈)。没有参数的函数永远不会检查它们,所以它们的内容是无关紧要的。这取决于调用和命名约定,但请参阅#4。
通常会准备好堆栈。在运行时库(如DOS)解析argv的平台上,如果没有使用argv,编译器可能会选择不链接代码,但这是很少有人认为必要的复杂性。在其他平台上,在程序加载之前,由exec()准备argv。
依赖于平台,但在Linux系统上,您实际上可以检查/ proc / PID / cmdline中的argv内容,无论它们是否被使用。许多平台还提供单独的调用来查找参数。
根据Tim Schaeffer引用的标准,main不需要接受参数。在大多数平台上,参数本身仍然存在,但是没有参数的main()永远不会知道它们。
答案 6 :(得分:1)
非常同意 Greg Hewgill 的回答,但我想补充几句。
我认为这也可以归因于调用公约_cdecl。因为_cdecl,调用者负责清理栈,所以当调用者声明main usemain(argc, argv);
时,被调用者(你)定义了像int main(){...}
或int main(int argc, char *argv[]){...}
这样的main函数,它不会重要的是,调用者推送两个参数然后自己弹出它(在汇编代码中add esp, $value
),只是在第一种方式中,你浪费了参数:argc 和 argv。
顺便说一句,为了深入理解 Greg Hewgill 的回答,您可能需要一些有关链接器和加载器的知识。