Hmmm. Wydaje się, że być może obecnie przyjęta odpowiedź, która wskazuje, że przyjęta wcześniej odpowiedź jest błędna, sama w sobie jest błędna. Tagi na tym pytaniu wskazują, że dotyczy to zarówno C++, jak i C, więc pozostanę przy specyfikacji C++, a nie C99. Niezależnie od wszystkich innych wyjaśnień lub argumentów, podstawową odpowiedzią na to pytanie jest to, że "główny() traktowany jest jako specjalny w sposób określony przez implementację". Wierzę, że odpowiedź Davida jest technicznie poprawniejsza od Hansa, ale wyjaśnię to bardziej szczegółowo ....
Funkcja main() jest zabawna, traktowana przez linker kompilatora z zachowaniem, które nie pasuje do żadnej innej funkcji. Hans ma rację, że w CRT nie ma specjalnego kodu rozpoznającego różne sygnatury main(), ale jego stwierdzenie, że "działa z powodu konwencji wywoływania cdecl" odnosi się tylko do konkretnej platformy (platform), w szczególności Visual Studio. Prawdziwym powodem, dla którego nie ma specjalnego kodu w CRT do rozpoznawania różnych sygnatur funkcji main(), jest to, że nie ma takiej potrzeby. I choć jest to rodzaj dzielenia włosów, to linker, którego zadaniem jest powiązanie kodu startowego w main() w czasie łącza, nie jest to zadanie CRT w momencie uruchamiania.
Większość funkcji funkcji main() jest definiowana przez implementację zgodnie ze specyfikacją C++ (patrz Rozdział 3.6, "Rozpoczęcie i zakończenie"). Jest prawdopodobne, że większość kompilatorów implementacji traktuje main() niejawnie z czymś zbliżonym do zewnętrznego połączenia "C", pozostawiając main() w nieodkształconym stanie, tak że niezależnie od prototypu funkcji, jego symbol linkera jest taki sam. Alternatywnie, linker dla implementacji może być wystarczająco inteligentny, aby przeskanować tabelę symboli, szukając dowolnego, którego ozdobiona nazwa rozwiązuje jakąś formę "[int | void] main (...)" (zauważ, że pustka jako typ powrotu jest sama rzecz implementacyjna, jak sama specyfikacja mówi, że typ powrotu main() musi być "int"). Po znalezieniu takiej funkcji w dostępnych symbolach, linker może po prostu użyć tego, gdzie kod startowy odnosi się do "main()", więc dokładna nazwa symbolu niekoniecznie musi być dopasowana do czegokolwiek; może to być nawet wmain() lub inny, o ile linker wie, jakich odmian szukać, lub kompilator nadaje wszystkie odmiany o tej samej nazwie symbolu.
Należy również zwrócić uwagę na to, że specyfikacja mówi, że main() może nie być przeciążony, więc linker nie powinien "wybierać" między wieloma implementacjami użytkownika różnych form main(). Jeśli znajdzie więcej niż jeden, jest to duplikat błędu symbolu (lub innego podobnego błędu), nawet jeśli lista argumentów nie jest zgodna. I choć wszystkich implementacjach „zastępuje” pozwalają zarówno
int main() { /* ... */ }
i
int main(int argc, char* argv[]) { /* ... */ }
one dopuszczone są również w celu umożliwienia inne listy argumentów, w tym wersji wykazać, że zawiera wskaźnik tablicy ciąg środowisko oraz wszelkie inne wariacja, która ma sens w danej implementacji.
Jak wskazuje Hans, konwencja wywoływania cdecl Visual Studio (i konwencje wywoływania wielu innych kompilatorów) zapewnia strukturę, w której wywołujący może skonfigurować środowisko wywołujące (tj. Stos lub rejestry zdefiniowane przez ABI lub pewną kombinację z dwóch) w taki sposób, że można przekazać zmienną liczbę argumentów, a gdy wywoływany zwraca, wywołujący jest odpowiedzialny za czyszczenie (wyrzucanie używanej przestrzeni argumentu ze stosu, lub w przypadku rejestrów, nic nie jest potrzebne do sprzątania). Ta konfiguracja dobrze pasuje do kodu startowego, przekazując więcej parametrów, niż może być potrzebna, a implementacja main() użytkownika może swobodnie używać lub nie używać żadnego z tych argumentów, tak jak w przypadku wielu platform traktujących różne formy main(), którą wymieniasz w swoim pytaniu.Jednak nie jest to jedyny sposób, w jaki kompilator + linker może osiągnąć ten cel: zamiast tego linker może wybierać między różnymi wersjami kodu startowego w oparciu o definicję twojej głównej(). Mogłoby to pozwolić na szeroki zakres list argumentów main(), które w innym przypadku byłyby niemożliwe przy użyciu modelu czyszczenia wywołującego cdecl. A ponieważ wszystko to jest zdefiniowane przez implementację, jest zgodne z specyfikacją C++, o ile kompilator + linker obsługuje przynajmniej dwie kombinacje przedstawione powyżej (int main()
i int main(int, char**)
).
To prawdopodobnie dokładne 32-bitowe środowiska wykonawcze systemu Windows C. Ale co z innymi platformami, gdzie ABI różni się? Na przykład x64 gdzie parametry są przekazywane w rejestrach nawet dla cdecl. A co z funkcją "main" zgodną z deklaracją taką jak "int main (double d)"? I gdzie w standardzie stwierdza, że wszystkie funkcje "główne" muszą używać '__cdecl', który nie jest nawet częścią standardu? A co z 'WinMain'? –
Cóż, przynajmniej pierwsze zdanie z twojej odpowiedzi jest teraz dokładne. ;-) –
Kwaśne winogrona David? Twoja odpowiedź może być poprawna, po prostu nie znam żadnego kompilatora, który obsługuje rodzaj refleksji, który byłby wymagany, aby to działało. Mogę zaoferować tylko to, co widziałem w ośmiu z nich, które dokładnie studiowałem, wszystkie używały konwencji wywoływania cdecl. Prawdopodobnie dlatego, że jest bardzo łatwy w implementacji, choć nie tak łatwy do wygenerowania kodu. Refleksja jest bardzo rzadką cechą w C. Możesz być może uczynić ją bardziej przekonującą, dając przykład takiego kompilatora? –