Теперь я ожидал какого-то сбоя или ошибки, вызванной основной функцией, явно объявленной как лишенная параметров life. Отсутствие ошибок привело к этому вопросу, и это действительно четыре вопроса.
Ответ 1
Я не знаю кросс-платформенного ответа на ваш вопрос. Но мне стало любопытно. Так что же нам делать? Посмотрите на стек!
Для первой итерации:
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;
}
И его asm:
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!
Ответ 6
Есть несколько заметок.
Стандарт в основном говорит, что, скорее всего, главное: функция, не принимающая никаких аргументов, или функция, принимающая два аргумента или что-то еще!
См. например, мой ответ на этот вопрос.
Но ваш вопрос указывает на другие факты.
Почему это работает? Ответ: Потому что стандарт говорит так!
Это неверно. Он работает по другим причинам. Он работает из-за вызывающих соглашений.
Этими конвенциями могут быть: аргументы помещаются в стек, а вызывающий отвечает за очистку стека. Из-за этого, в фактическом коде asm, вызываемый может полностью игнорировать то, что находится в стеке. Вызов выглядит как
push value1
push value2
call function
add esp, 8
(примеры intel, просто чтобы остаться в мейнстриме).
Что делает функция с аргументами, нажимаемыми на стек, совершенно неинтересно, все будет работать нормально! И это действительно так, даже если соглашение вызова отличается, например.
li $a0, value
li $a1, value
jal function
Если функция учитывает регистры $a0 и $a1 или нет, ничего не меняет.
Таким образом, вызываемый может игнорировать аргументы без вреда, cn полагает, что они не существуют, или они могут знать, что они существуют, но предпочитают игнорировать их (напротив, было бы проблематично, если вызываемый получает значения из стека или регистров, в то время как вызывающий абонент ничего не пропускал).
Вот почему все работает.
С точки зрения C, если мы находимся в системе, где код запуска вызывает main с двумя аргументами (int и char **) и ожидает значение возврата int, "правильный" прототип будет
int main(int argc, char **argv) { }
Но предположим теперь, что мы не используем эти аргументы.
Правильнее сказать int main(void)
или int main()
(все еще в той же системе, где реализация вызывает main с двумя аргументами и ожидает значение int, как было сказано ранее)?
Действительно, стандарт не говорит, что мы должны делать. Правильный "prototype", который говорит, что у нас есть два аргумента, все еще тот, что был показан ранее.
Но с логической точки зрения, правильный способ сказать, что есть аргументы (мы знаем это), но мы их не интересуемся, это
int main() { /* ... */ }
В этот ответ Я показал, что происходит, если мы передаем аргументы функции, объявленной как int func()
, и что произойдет, если мы передадим аргументы объявленной функции как int func(void)
.
Во втором случае мы имеем ошибку, так как (void)
явно говорит, что функция не имеет аргументов.
С main
мы не сможем получить ошибку, так как у нас нет реального прототипа для аргументов, но стоит отметить, что gcc -std=c99 -pedantic
не дает никаких предупреждений для int main()
или для int main(void)
, и это означают, что 1) gcc не совместим с C99 даже с флагом std
, или 2) оба способа являются стандартными. Скорее всего, это вариант 2.
Один из них явно соответствует стандарту (int main(void)
), другой действительно int main(int argc, char **argv)
, но без явного указания аргументов, поскольку мы их не интересуем.
int main(void)
работает даже тогда, когда существуют аргументы, из-за того, что я написал ранее. Но в нем говорится, что главное не принимает аргументов. Хотя во многих случаях, если мы можем написать int main(int argc, char **argv)
, тогда это значение false, а int main()
должно быть предпочтительным.
Еще одна интересная вещь: если мы говорим, что main не возвращает значение (void main()
) в системе, где реализация ожидает возвращаемого значения, мы получаем предупреждение. Это связано с тем, что вызывающий абонент ожидает, что он что-то сделает с ним, так что это "поведение w90 > ", если мы не вернем значение (это не означает, что явное return
в случае main
, но объявив main
как возвращающий int).
Во многих кодах запуска я видел, как main вызывается одним из следующих способов:
retval = main(_argc, _argv);
retval = main(_argc, _argv, environ);
retval = main(_argc, _argv, environ, apple); // apple specific stuff
Но могут существовать коды запуска, которые называет основной по-разному, например. retval = main()
; в этом случае, чтобы показать это, мы можем использовать int main(void)
, а с другой стороны, с помощью int main(int argc, char **argv)
будет компилироваться, но сделать сбой программы, если мы фактически используем аргументы (так как извлеченные значения будут мусорными).
Является ли эта платформа зависимой?
Как называется основной, зависит от платформы (специфическая реализация), как это допускается стандартами. "Предполагаемый" главный прототип - это консенсус и, как уже было сказано, если мы знаем, что есть аргументы, но мы их не будем использовать, мы должны использовать int main()
как короткую форму для более длинного int main(int argc, char **argv)
, тогда как int main(void)
означает что-то другое: ie main не принимает никаких аргументов (что неверно в системе, о которой мы думаем)