Jak działa metoda main () w C?

96

Wiem, że istnieją dwa różne podpisy do napisania głównej metody -

int main()
{
   //Code
}

lub do obsługi argumentu wiersza poleceń, piszemy go jako-

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

W C++Wiem, że możemy przeciążyć metodę, ale w Cjaki sposób kompilator obsługiwać te dwa różne podpisy mainfunkcji?

Ritesh
źródło
14
Przeciążanie odnosi się do posiadania dwóch metod o tej samej nazwie w tym samym programie. Możesz mieć tylko jedną mainmetodę w jednym programie w C(a właściwie w prawie każdym języku z taką konstrukcją).
Kyle Strand
13
C nie ma metod; ma funkcje. Metody są wewnętrzną implementacją „ogólnych” funkcji zorientowanych obiektowo. Program wywołuje funkcję z pewnymi argumentami obiektowymi, a system obiektowy wybiera metodę (lub może zestaw metod) na podstawie ich typów. C nie ma żadnego z tych elementów, chyba że sam to zasymulujesz.
Kaz
4
Do głębokiej dyskusji na temat punktów wejścia do programu - nie szczególnie main- polecam klasyczną książkę Johna R. Levinesa „Linkers & Loaders”.
Andreas Spindler
1
W C pierwsza forma to int main(void)nie int main()(chociaż nigdy nie widziałem kompilatora, który odrzuca int main()formularz).
Keith Thompson
1
@harper: ()Formularz jest przestarzały i nie jest jasne, czy jest nawet dozwolony main(chyba że implementacja wyraźnie udokumentuje go jako dozwolony formularz). Standard C (patrz 5.1.2.2.1 Uruchamianie programu) nie wspomina o ()formie, która nie jest całkowicie równoważna z ()formularzem. Szczegóły są za długie dla tego komentarza.
Keith Thompson

Odpowiedzi:

133

Niektóre cechy języka C zaczęły się od hacków, które po prostu zadziałały.

Wiele podpisów dla głównych list argumentów oraz list argumentów o zmiennej długości jest jedną z tych funkcji.

Programiści zauważyli, że mogą przekazywać dodatkowe argumenty do funkcji i nic złego się nie dzieje z ich kompilatorem.

Dzieje się tak, jeśli konwencje wywoływania są takie, że:

  1. Funkcja wywołująca czyści argumenty.
  2. Najbardziej lewe argumenty znajdują się bliżej wierzchołka stosu lub podstawy ramki stosu, tak więc fałszywe argumenty nie unieważniają adresowania.

Jednym z zestawów konwencji wywoływania, które są zgodne z tymi regułami, jest przekazywanie parametrów na podstawie stosu, w którym wywołujący pobiera argumenty i są one przesuwane od prawej do lewej:

 ;; 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

W kompilatorach, w których występuje taka konwencja wywoływania, nie trzeba nic robić, aby wspierać te dwa rodzaje main, a nawet dodatkowe. mainmoże być funkcją bez argumentów, w którym to przypadku jest nieświadoma elementów, które zostały odłożone na stos. Jeśli jest to funkcja dwóch argumentów, to znajduje argci argvjako dwa najwyższe elementy stosu. Jeśli jest to wariant trójargumentowy specyficzny dla platformy ze wskaźnikiem środowiskowym (typowe rozszerzenie), to również zadziała: ten trzeci argument znajdzie jako trzeci element od góry stosu.

Tak więc stałe wywołanie działa we wszystkich przypadkach, umożliwiając połączenie jednego, stałego modułu startowego z programem. Ten moduł można zapisać w C, jako funkcję podobną do tej:

/* 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));
}

Innymi słowy, ten moduł startowy zawsze wywołuje trzyargumentowy main. Jeśli main nie przyjmuje argumentów lub tylko int, char **, zdarza się, że działa dobrze, a także jeśli nie przyjmuje żadnych argumentów, ze względu na konwencje wywoływania.

Gdybyś miał zrobić tego rodzaju rzecz w swoim programie, byłoby to nieprzenoszalne i uważane za niezdefiniowane zachowanie przez ISO C: deklarowanie i wywoływanie funkcji w jeden sposób i definiowanie jej w inny. Ale sztuczka startowa kompilatora nie musi być przenośna; nie kierują się zasadami programów przenośnych.

Ale przypuśćmy, że konwencje wywoływania są takie, że nie może działać w ten sposób. W takim przypadku kompilator musi traktować mainspecjalnie. Kiedy zauważy, że kompiluje mainfunkcję, może wygenerować kod, który jest zgodny, powiedzmy, z wywołaniem z trzema argumentami.

To znaczy, piszesz to:

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

Ale kiedy kompilator to zobaczy, zasadniczo przeprowadza transformację kodu, aby funkcja, którą kompiluje, wyglądała bardziej tak:

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

poza tym, że nazwy __argc_ignorenie istnieją dosłownie. Żadne takie nazwy nie są wprowadzane do twojego zakresu i nie będzie żadnego ostrzeżenia o nieużywanych argumentach. Transformacja kodu powoduje, że kompilator emituje kod z poprawnym powiązaniem, który wie, że musi wyczyścić trzy argumenty.

Inna strategia implementacji polega na tym, że kompilator lub konsolidator generuje niestandardowo __startfunkcję (lub jakkolwiek to się nazywa) lub przynajmniej wybiera jedną z kilku wstępnie skompilowanych alternatyw. W pliku obiektowym mogą być przechowywane informacje o tym, która z obsługiwanych form mainjest używana. Konsolidator może przejrzeć te informacje i wybrać poprawną wersję modułu startowego, który zawiera wywołanie mainzgodne z definicją programu. Implementacje C mają zwykle tylko niewielką liczbę obsługiwanych form, mainwięc takie podejście jest wykonalne.

Kompilatory języka C99 zawsze muszą main, do pewnego stopnia, specjalnie traktować , aby wspierać hack, że jeśli funkcja kończy się bez returninstrukcji, zachowanie jest tak, jakby return 0zostało wykonane. To znowu może być potraktowane przez transformację kodu. Kompilator zauważa, że mainkompilowana jest wywołana funkcja . Następnie sprawdza, czy koniec treści jest potencjalnie osiągalny. Jeśli tak, wstawiareturn 0;

Kaz
źródło
34

Nie ma żadnego przeciążenia mainnawet w C ++. Funkcja główna to punkt wejścia do programu i powinna istnieć tylko jedna definicja.

W przypadku standardu C

W przypadku środowiska hostowanego (to normalne) standard C99 mówi:

5.1.2.2.1 Uruchomienie programu

Nazwa funkcji wywoływana podczas uruchamiania programu main. Implementacja nie deklaruje żadnego prototypu dla tej funkcji. Powinien być określony z typem zwracanym inti bez parametrów:

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

lub z dwoma parametrami (określanymi tutaj jako argci argv, chociaż można używać dowolnych nazw, ponieważ są one lokalne dla funkcji, w której zostały zadeklarowane):

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

lub odpowiednik; 9) lub w inny sposób określony w implementacji.

9) W ten sposób intmożna go zastąpić nazwą typu zdefiniowanego jako int, lub typ argvmożna zapisać jako char **argvi tak dalej.

Dla standardowego C ++:

3.6.1 Główna funkcja [basic.start.main]

1 Program powinien zawierać globalną funkcję zwaną main, która jest wyznaczonym początkiem programu. […]

2 Implementacja nie określa z góry głównej funkcji. Ta funkcja nie powinna być przeciążona . Powinien mieć zwracany typ typu int, ale poza tym jego typ jest zdefiniowany w implementacji. Wszystkie implementacje pozwalają na obie z następujących definicji main:

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

i

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

Standard C ++ wyraźnie mówi, że „To [główna funkcja] powinna mieć zwracany typ typu int, ale poza tym jego typ jest zdefiniowany w implementacji” i wymaga tych samych dwóch podpisów, co standard C.

W środowisku hostowanym ( środowisko AC, które obsługuje również biblioteki C) - wywołuje system operacyjny main.

W środowisku niehostowanym (przeznaczonym dla aplikacji wbudowanych) zawsze możesz zmienić punkt wejścia (lub wyjścia) programu za pomocą dyrektyw preprocesora, takich jak

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

Gdzie priorytet jest opcjonalną liczbą całkowitą.

Pragma startup wykonuje funkcję przed main (priorytetowo), a pragma exit wykonuje funkcję po funkcji main. Jeśli jest więcej niż jedna dyrektywa startowa, priorytet decyduje, która zostanie wykonana jako pierwsza.

Sadique
źródło
4
Nie sądzę, ta odpowiedź faktycznie odpowiada na pytanie, jak kompilator faktycznie radzi sobie z tą sytuacją. Odpowiedź udzielona przez @Kaz daje moim zdaniem więcej wglądu.
Tilman Vogel,
4
Myślę, że ta odpowiedź lepiej odpowiada na pytanie niż ta, którą napisał @Kaz. Pierwotne pytanie dotyczy wrażenia, że ​​występuje przeciążenie operatora, a ta odpowiedź rozwiązuje ten problem, pokazując, że zamiast jakiegoś rozwiązania przeciążającego kompilator akceptuje dwa różne podpisy. Szczegóły kompilatora są interesujące, ale nie są konieczne, aby odpowiedzieć na pytanie.
Waleed Khan
1
W przypadku środowisk wolnostojących („niehostowanych”) dzieje się o wiele więcej niż tylko #pragma. Jest przerwanie resetowania ze sprzętu i tak naprawdę zaczyna się program. Stamtąd wykonywane są wszystkie podstawowe ustawienia: stos konfiguracji, rejestry, MMU, mapowanie pamięci itp. Następnie następuje kopiowanie wartości inicjalizacyjnych z NVM do statycznych zmiennych pamięci (segment .data), a także „zerowanie” wszystkich statyczne zmienne pamięci, które powinny być ustawione na zero (segment .bss). W C ++ wywoływane są konstruktory obiektów ze statycznym czasem trwania. A kiedy wszystko jest zrobione, nazywa się main.
Lundin,
8

Nie ma potrzeby przeciążania. Tak, istnieją 2 wersje, ale w danym momencie można używać tylko jednej.

user694733
źródło
5

Jest to jedna z dziwnych asymetrii i specjalnych reguł języka C i C ++.

Moim zdaniem istnieje tylko ze względów historycznych i nie kryje się za tym żadna poważna logika. Zauważ, że mainjest to szczególne również z innych powodów (na przykład mainw C ++ nie może być rekurencyjne i nie możesz wziąć jego adresu, aw C99 / C ++ możesz pominąć końcowe returnstwierdzenie).

Zauważ też, że nawet w C ++ nie jest to przeciążenie ... albo program ma pierwszą formę, albo ma drugą postać; nie może mieć obu.

6502
źródło
Możesz pominąć returninstrukcję również w C (od C99).
dreamlax
W C możesz zadzwonić main()i odebrać jego adres; C ++ stosuje ograniczenia, których nie ma w C.
Jonathan Leffler
@JonathanLeffler: masz rację, naprawiono. Jedyną zabawną rzeczą dotyczącą main, jaką znalazłem w specyfikacjach C99, oprócz możliwości pominięcia wartości zwracanej, jest to, że ponieważ standard ma sformułowanie IIUC, nie można przekazać wartości ujemnej argcpodczas rekurencji (5.1.2.2.1 nie określa ograniczeń argci argvmają zastosowanie tylko do pierwszego wezwania do main).
6502
4

Niezwykłe mainnie jest to, że można go zdefiniować na więcej niż jeden sposób, ale to, że można go zdefiniować tylko na jeden z dwóch różnych sposobów.

mainjest funkcją zdefiniowaną przez użytkownika; implementacja nie deklaruje jej prototypu.

To samo dotyczy foolub bar, ale możesz definiować funkcje o tych nazwach w dowolny sposób.

Różnica polega na tym, że mainjest wywoływana przez implementację (środowisko wykonawcze), a nie tylko przez Twój własny kod. Implementacja nie ogranicza się do zwykłej semantyki wywołań funkcji C, więc może (i musi) radzić sobie z kilkoma odmianami - ale nie jest wymagana do obsługi nieskończenie wielu możliwości. int main(int argc, char *argv[])Forma pozwala na argumenty wiersza polecenia, a int main(void)w C lub int main()C ++ jest tylko wygoda dla prostych programów, które nie muszą przetwarzać argumenty wiersza polecenia.

Jeśli chodzi o to, jak kompilator to obsługuje, zależy to od implementacji. Większość systemów prawdopodobnie ma konwencje wywoływania, które sprawiają, że te dwie formy są efektywnie kompatybilne, a wszelkie argumenty przekazywane do mainzdefiniowanego bez parametrów są po cichu ignorowane. Jeśli nie, nie byłoby trudno kompilatorowi lub linkerowi traktować w mainspecjalny sposób. Jeśli jesteś ciekawy, jak to działa w twoim systemie , możesz spojrzeć na niektóre zestawienia.

I podobnie jak wiele rzeczy w C i C ++, szczegóły są w dużej mierze wynikiem historii i arbitralnych decyzji podjętych przez projektantów języków i ich poprzedników.

Zauważ, że zarówno C, jak i C ++ zezwalają na inne definicje zdefiniowane przez implementację main- ale rzadko istnieje dobry powód, aby ich używać. W przypadku wdrożeń wolnostojących (takich jak systemy wbudowane bez systemu operacyjnego) punkt wejścia programu jest zdefiniowany w ramach implementacji i niekoniecznie jest nawet wywoływany main.

Keith Thompson
źródło
3

To maintylko nazwa adresu początkowego ustalona przez konsolidatora, gdzie mainjest nazwą domyślną. Wszystkie nazwy funkcji w programie są adresami początkowymi, od których zaczyna się funkcja.

Argumenty funkcji są wypychane / zdejmowane na / ze stosu, więc jeśli nie ma żadnych argumentów określonych dla funkcji, nie ma żadnych argumentów umieszczanych / zdejmowanych ze stosu. W ten sposób main może działać zarówno z argumentami, jak i bez nich.

AndersK
źródło
2

Cóż, dwa różne sygnatury tej samej funkcji main () pojawiają się na obrazie tylko wtedy, gdy ich potrzebujesz, więc mam na myśli, że jeśli twój program potrzebuje danych przed jakimkolwiek faktycznym przetwarzaniem twojego kodu, możesz je przekazać za pomocą -

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

gdzie zmienna argc przechowuje liczbę przekazanych danych, a argv jest tablicą wskaźników do znaku char, która wskazuje na przekazane wartości z konsoli. W przeciwnym razie zawsze dobrze jest iść

    int main()
    {
       //Code
    }

Jednak w każdym przypadku może istnieć jedna i tylko jedna funkcja main () w programie, ponieważ jest to jedyny punkt, w którym program rozpoczyna wykonywanie, a zatem nie może być więcej niż jeden. (mam nadzieję, że to jest warte)

manish
źródło
2

Podobne pytanie zostało zadane wcześniej: dlaczego funkcja bez parametrów (w porównaniu z rzeczywistą definicją funkcji) jest kompilowana?

Jedną z najwyżej ocenianych odpowiedzi była:

W C func()oznacza, że ​​możesz przekazać dowolną liczbę argumentów. Jeśli nie chcesz żadnych argumentów, musisz zadeklarować jakofunc(void)

Więc myślę, że tak mainjest deklarowane (jeśli można zastosować termin „zadeklarowany” do main). Właściwie możesz napisać coś takiego:

int main(int only_one_argument) {
    // code
}

i nadal będzie się kompilować i działać.

varepsilon
źródło
1
Doskonała obserwacja! Wygląda na to, że linker jest dość wyrozumiały main, ponieważ jest jeszcze nie wspomniany problem: jeszcze więcej argumentów za main! Dodaje się „Unix (ale nie Posix.1) i Microsoft Windows” char **envp(pamiętam, że DOS również na to pozwalał, prawda?), A Mac OS X i Darwin dodają kolejny wskaźnik char * do „dowolnych informacji dostarczonych przez system operacyjny”. wikipedia
usr2564301
0

Nie musisz tego nadpisywać. Ponieważ tylko jeden będzie używany na raz. Tak, są 2 różne wersje funkcji głównej

gautam
źródło