Как работает метод main () в C?

96

Я знаю, что есть две разные подписи для написания основного метода -

int main()
{
   //Code
}

или для обработки аргумента командной строки мы пишем его как-

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

В C++Я знаю , что мы можем перегрузить метод, но Cкак компилятор обрабатывать эти две различные сигнатуры mainфункции?

Ритеш
источник
14
Под перегрузкой подразумевается наличие двух методов с одинаковыми именами в одной программе. У вас может быть только один mainметод в одной программе C(или, на самом деле, практически на любом языке с такой конструкцией).
Кайл Стрэнд
13
У C нет методов; у него есть функции. Методы - это внутренняя реализация объектно-ориентированных «универсальных» функций. Программа вызывает функцию с некоторыми аргументами объекта, и объектная система выбирает метод (или, возможно, набор методов) на основе их типов. В C нет ничего из этого, если вы не моделируете это самостоятельно.
Kaz
4
Для глубокого обсуждения точек входа в программу - не особенно main- я рекомендую классическую книгу Джона Р. Левинеса "Linkers & Loaders".
Андреас Шпиндлер
1
В C первая форма - int main(void)not int main()(хотя я никогда не видел компилятора, который отклоняет int main()форму).
Кейт Томпсон
1
@harper: ()форма устарела, и неясно, разрешено ли она вообще main(если реализация специально не документирует ее как разрешенную форму). В стандарте C (см. 5.1.2.2.1 Запуск программы) ()форма не упоминается , что не совсем эквивалентно ()форме. Детали слишком длинные для этого комментария.
Кейт Томпсон

Ответы:

133

Некоторые особенности языка C начинались как хитрости, которые просто так сработали.

Одной из таких функций является наличие нескольких подписей для основных списков аргументов и списков аргументов переменной длины.

Программисты заметили, что они могут передавать функции дополнительные аргументы, и с их компилятором ничего плохого не происходит.

Это так, если соглашения о вызовах таковы, что:

  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может быть функцией без аргументов, и в этом случае она не обращает внимания на элементы, которые были помещены в стек. Если это функция двух аргументов, то он находит 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));
}

Другими словами, этот стартовый модуль всегда вызывает основную функцию с тремя аргументами. Если 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;

Каз
источник
34

Нет НИКАКОЙ перегрузки mainдаже в C ++. Основная функция - это точка входа в программу, и должно существовать только одно определение.

Для стандарта C

Для размещенной среды (это нормальная) стандарт C99 говорит:

5.1.2.2.1 Запуск программы

Называется функция, вызываемая при запуске программы main. Реализация не объявляет прототипа для этой функции. Он должен быть определен с возвращаемым типом intи без параметров:

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

или с двумя параметрами (называемыми здесь argcи 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.

В размещенной среде ( среда AC, которая также поддерживает библиотеки C) - вызывает операционная система main.

В среде без хоста (предназначенной для встраиваемых приложений) вы всегда можете изменить точку входа (или выхода) своей программы, используя директивы препроцессора, такие как

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

Где приоритет - это необязательное целое число.

Запуск Pragma выполняет функцию перед основной (по приоритету), а программа exit выполняет функцию после основной функции. Если имеется более одной директивы запуска, то приоритет решает, какая из них будет выполнена первой.

Sadique
источник
4
Я не думаю, что этот ответ действительно отвечает на вопрос, как компилятор на самом деле справляется с ситуацией. На мой взгляд, ответ @Kaz дает больше информации.
Тилман Фогель
4
Я думаю, что этот ответ лучше отвечает на вопрос, чем ответ @Kaz. В исходном вопросе создается впечатление, что происходит перегрузка оператора, и этот ответ решает эту проблему, показывая, что вместо некоторого решения по перегрузке компилятор принимает две разные сигнатуры. Детали компилятора интересны, но не являются необходимыми для ответа на вопрос.
Waleed Khan
1
В автономных средах («не размещенных») происходит гораздо больше, чем просто #pragma. Имеется прерывание сброса от оборудования, и именно здесь начинается программа. Оттуда выполняются все основные настройки: стек настройки, регистры, MMU, отображение памяти и т. Д. Затем происходит копирование значений инициализации из NVM в статические переменные хранилища (сегмент .data), а также «обнуление» для всех статические переменные хранилища, которые должны быть установлены в ноль (сегмент .bss). В C ++ вызываются конструкторы объектов со статической продолжительностью хранения. И как только все это будет сделано, вызывается main.
Lundin
8

Нет необходимости в перегрузке. Да, существует 2 версии, но одновременно можно использовать только одну.

пользователь694733
источник
5

Это одна из странных асимметрий и особых правил языков C и C ++.

На мой взгляд, он существует только по историческим причинам и в этом нет никакой серьезной логики. Обратите внимание, что это mainявляется особенным и по другим причинам (например, mainв C ++ не может быть рекурсивного, и вы не можете взять его адрес, а в C99 / C ++ вам разрешено опустить последний returnоператор).

Заметьте также, что даже в C ++ это не перегрузка ... либо программа имеет первую форму, либо вторую форму; он не может иметь обоих.

6502
источник
Вы также можете опустить returnоператор в C (начиная с C99).
dreamlax
В C вы можете позвонить main()и взять его адрес; C ++ применяет ограничения, которых нет в C.
Джонатан Леффлер
@JonathanLeffler: ты прав, исправлено. Единственная забавная вещь о main, которую я обнаружил в спецификациях C99 в дополнение к возможности опустить возвращаемое значение, заключается в том, что, поскольку стандарт сформулирован как IIUC, вы не можете передавать отрицательное значение argcпри рекурсии (5.1.2.2.1 не указывает ограничений на argcи argvприменяется только к первоначальному вызову main).
6502
4

Необычность mainзаключается не в том, что его можно определить более чем одним способом, а в том, что его можно определить только одним из двух разных способов.

mainэто функция, определяемая пользователем; реализация не объявляет для него прототип.

То же самое верно и для fooили bar, но вы можете определять функции с этими именами как хотите.

Разница в том, что mainвызывается реализацией (средой выполнения), а не только вашим собственным кодом. Реализация не ограничивается обычной семантикой вызова функций C, поэтому она может (и должна) иметь дело с несколькими вариациями, но не требуется обрабатывать бесконечно много возможностей. int main(int argc, char *argv[])Форма позволяет аргументы командной строки, и int main(void)в C или int main()в C ++ это просто удобство для простых программ , которые не нужны для аргументов командной строки процесса.

Что касается того, как компилятор это обрабатывает, это зависит от реализации. Большинство систем, вероятно, имеют соглашения о вызовах, которые обеспечивают эффективную совместимость двух форм, и любые аргументы, переданные в объект, mainопределенный без параметров, незаметно игнорируются. В противном случае компилятору или компоновщику не составит труда обработать это mainспециально. Если вам интересно, как это работает в вашей системе , вы можете посмотреть некоторые списки сборок.

И, как и многие вещи в C и C ++, детали во многом являются результатом истории и произвольных решений, принятых разработчиками языков и их предшественниками.

Обратите внимание, что и C, и C ++ допускают другие определения, определяемые реализацией, mainно редко есть веские причины для их использования. А для автономных реализаций (таких как встроенные системы без ОС) точка входа в программу определяется реализацией и даже не вызывается main.

Кейт Томпсон
источник
3

Это mainпросто имя для начального адреса, определяемого компоновщиком, где mainимя по умолчанию. Все имена функций в программе - это начальные адреса, с которых функция запускается.

Аргументы функции выталкиваются / выталкиваются в / из стека, поэтому, если для функции не указаны аргументы, нет никаких аргументов, выталкиваемых / выталкиваемых в / из стека. Таким образом main может работать как с аргументами, так и без них.

АндерсК
источник
2

Что ж, две разные сигнатуры одной и той же функции main () появляются на картинке только тогда, когда они вам нужны, поэтому я имею в виду, что если вашей программе нужны данные перед любой фактической обработкой вашего кода, вы можете передать их с помощью -

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

где переменная argc хранит количество переданных данных, а argv - это массив указателей на char, который указывает на переданные значения из консоли. В противном случае всегда хорошо идти с

    int main()
    {
       //Code
    }

Однако в любом случае в программе может быть один и только один main (), поскольку это единственная точка, с которой программа начинает свое выполнение, и, следовательно, их не может быть больше одного. (надеюсь, что это достойно)

маниакальный
источник
2

Подобный вопрос задавался раньше: почему функция без параметров (по сравнению с фактическим определением функции) компилируется?

Один из самых популярных ответов был:

В C func()означает, что вы можете передавать любое количество аргументов. Если вам не нужны аргументы, вы должны объявить какfunc(void)

Итак, я предполагаю, что это то, как mainобъявляется (если вы можете применить термин «объявленный» main). На самом деле вы можете написать примерно так:

int main(int only_one_argument) {
    // code
}

и он все равно будет компилироваться и запускаться.

варепсилон
источник
1
Замечательное наблюдение! Похоже, компоновщик довольно снисходителен main, поскольку есть еще не упомянутая проблема: еще больше аргументов в пользу main! Добавляют «Unix (но не Posix.1) и Microsoft Windows» char **envp(я помню, что DOS тоже позволяла это, не так ли?), А Mac OS X и Darwin добавляют еще один указатель char * на «произвольную информацию, предоставляемую ОС». википедия
usr2564301
0

Вам не нужно переопределять это. Потому что одновременно будет использоваться только одна. Да, есть 2 разные версии основной функции

гаутам
источник