Як працює метод main () в C?


96

Я знаю, що є два різних підписи для написання основного методу -

int main()
{
   //Code
}

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

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

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


14
Під перевантаженням розуміється наявність двох методів з однаковою назвою в одній програмі. Ви можете будь-коли мати один mainметод в одній програмі C(або, справді, майже будь-якою мовою з такою конструкцією).
Kyle Strand

12
C не має методів; він має функції. Методи - це заднє впровадження об'єктно-орієнтованих "загальних" функцій. Програма викликає функцію з деякими аргументами об'єкта, і система об'єктів вибирає метод (або, можливо, набір методів) на основі їх типів. У C немає цього матеріалу, якщо ви не змоделюєте його самостійно.
Каз

4
Для глибокого обговорення точок входу в програму - не особливо main- я рекомендую класичну книгу Джона Р. Левінса "Посилання та навантажувачі".
Андреас Шпіндлер,

1
У мові C перша форма - int main(void)ні int main()(хоча я ніколи не бачив компілятора, який відхиляє int main()форму).
Кіт Томпсон,

1
@harper: ()Форма застаріла, і незрозуміло, що для неї навіть дозволено main(якщо реалізація спеціально не документує її як дозволену форму). Стандарт C (див. 5.1.2.2.1 запуск програми) не згадує ()форму, яка не зовсім еквівалентна ()формі. Деталі занадто довгі для цього коментаря.
Кіт Томпсон,

Відповіді:


132

Деякі особливості мови 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, завжди. Якщо 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, складається. Потім він перевіряє, чи є кінець тіла потенційно доступним. Якщо так, він вставляє areturn 0;


34

НІ НЕ перевантажує mainнавіть у 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, але в іншому випадку його тип визначається реалізацією. Усі реалізації повинні дозволяти обидва наступних визначення основного:

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

і

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

Стандарт C ++ прямо говорить "Він [основна функція] повинен мати тип повернення типу int, але в іншому випадку його тип визначається реалізацією", і вимагає тих самих двох підписів, що і стандарт C.

У розміщеному середовищі ( середовищі змінного струму, яке також підтримує бібліотеки C) - виклики операційної системи main.

У не-розміщеному середовищі (призначеному для вбудованих програм) ви завжди можете змінити точку входу (або вихід) вашої програми, використовуючи директиви попереднього процесора, такі як

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

Де пріоритет - необов’язковий інтегральний номер.

Запуск Pragma виконує функцію перед основною (у пріоритеті), а pragma-вихід виконує функцію після основної функції. Якщо існує більше однієї директиви запуску, то пріоритет вирішує, який виконати першим.


4
Я не думаю, що ця відповідь насправді відповідає на питання, як насправді компілятор справляється із ситуацією. На мою думку, відповідь @Kaz дає більше розуміння.
Тілман Фогель,

4
Я думаю, що ця відповідь краще відповідає на запитання, ніж відповідь @Kaz. Оригінальне запитання складається під враженням, що відбувається перевантаження оператора, і ця відповідь вирішує, що, показуючи, що замість якогось рішення перевантаження компілятор приймає дві різні підписи. Деталі компілятора цікаві, але не потрібні для відповіді на запитання.
Waleed Khan

1
Для окремо стоячих середовищ ("не розміщених") відбувається набагато більше, ніж просто якась # pragma. Існує переривання скидання від апаратного забезпечення, і це дійсно, де програма починається. Звідти виконуються всі основні установки: стек налаштування, регістри, MMU, відображення пам'яті тощо. Потім відбувається копіювання значень ініціалізації з NVM до статичних змінних зберігання (сегмент .data), а також "нульовий вихід" на всіх статичні змінні зберігання, для яких слід встановити нуль (сегмент .bss). У C ++ викликаються конструктори об'єктів зі статичною тривалістю зберігання. І як тільки все, що зроблено, тоді називається main.
Лундін

8

Немає необхідності в перевантаженні. Так, існує 2 версії, але на той момент можна використовувати лише одну.


5

Це одна з дивних асиметрій та спеціальних правил мови С та С ++.

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

Зауважте також, що навіть у C ++ це не перевантаження ... або програма має першу форму, або вона має другу форму; він не може мати і того, і іншого.


Ви також можете опустити returnтвердження на C (починаючи з C99).
dreamlax

У C ви можете зателефонувати main()і взяти його адресу; С ++ застосовує обмеження, яких С ні.
Джонатан Леффлер

@JonathanLeffler: ти маєш рацію, виправлено. Єдиним забавним щодо основного, що я знайшов у специфікаціях 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
    }

Однак у будь-якому випадку в програмі може бути один і лише один основний (), оскільки це єдиний момент, коли з програми починається її виконання, а отже, вона не може бути більше однієї. (сподіваюся, що це гідно)


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 * "довільна ОС" ". wikipedia
usr2564301

0

Вам не потрібно перевизначати це. Оскільки одночасно буде використовуватися лише один. Так, є 2 різні версії основної функції

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.