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:
- Funkcja wywołująca czyści argumenty.
- 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;
mainmetodę w jednym programie wC(a właściwie w prawie każdym języku z taką konstrukcją).