Alcune delle caratteristiche del linguaggio C sono nate come hack che hanno funzionato.
Le firme multiple per gli elenchi di argomenti principali e di lunghezza variabile sono una di queste caratteristiche.
I programmatori hanno notato che possono passare argomenti extra a una funzione e non succede niente di male con il compilatore dato.
Questo è il caso se le convenzioni di chiamata sono tali che:
- La funzione chiamante pulisce gli argomenti.
- Gli argomenti più a sinistra sono più vicini all'inizio dello stack o alla base dello stack frame, in modo che gli argomenti spuri non invalidino l'indirizzamento.
Un insieme di convenzioni di chiamata che obbedisce a queste regole è il passaggio di parametri basato sullo stack in base al quale il chiamante inserisce gli argomenti e questi vengono spinti da destra a sinistra:
;; 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
Nei compilatori in cui è presente questo tipo di convenzione di chiamata, non è necessario fare nulla di speciale per supportare i due tipi di main, o anche altri tipi. mainpuò essere una funzione di nessun argomento, nel qual caso è ignaro degli elementi che sono stati inseriti nella pila. Se è una funzione di due argomenti, trova argce argvcome i due elementi dello stack più in alto. Se si tratta di una variante a tre argomenti specifica della piattaforma con un puntatore all'ambiente (un'estensione comune), anche questo funzionerà: troverà quel terzo argomento come terzo elemento in cima allo stack.
E così una chiamata fissa funziona per tutti i casi, consentendo di collegare un unico modulo di avvio fisso al programma. Quel modulo potrebbe essere scritto in C, come una funzione simile a questa:
/* 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));
}
In altre parole, questo modulo di avvio chiama sempre un main di tre argomenti, sempre. Se main non accetta argomenti, o solo int, char **, funziona bene, così come se non accetta argomenti, a causa delle convenzioni di chiamata.
Se dovessi fare questo genere di cose nel tuo programma, sarebbe un comportamento non portabile e considerato indefinito da ISO C: dichiarare e chiamare una funzione in un modo e definirla in un altro. Ma il trucco di avvio di un compilatore non deve essere portabile; non è guidato dalle regole per i programmi portatili.
Ma supponiamo che le convenzioni di chiamata siano tali da non poter funzionare in questo modo. In tal caso, il compilatore deve trattare in modo mainspeciale. Quando si accorge che sta compilando la mainfunzione, può generare codice compatibile, ad esempio, con una chiamata a tre argomenti.
Vale a dire, scrivi questo:
int main(void)
{
/* ... */
}
Ma quando il compilatore lo vede, esegue essenzialmente una trasformazione del codice in modo che la funzione che compila sia più simile a questa:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
tranne che i nomi __argc_ignorenon esistono letteralmente. Nessun nome di questo tipo viene introdotto nel tuo ambito e non ci sarà alcun avviso sugli argomenti inutilizzati. La trasformazione del codice fa sì che il compilatore emetta codice con il collegamento corretto che sa che deve pulire tre argomenti.
Un'altra strategia di implementazione è che il compilatore o forse il linker generi in modo personalizzato la __startfunzione (o qualunque cosa venga chiamata), o almeno ne selezioni una da diverse alternative precompilate. Le informazioni potrebbero essere memorizzate nel file oggetto su quale delle forme supportate di mainviene utilizzata. Il linker può guardare queste informazioni e selezionare la versione corretta del modulo di avvio che contiene una chiamata maincompatibile con la definizione del programma. Le implementazioni C di solito hanno solo un piccolo numero di forme supportate, mainquindi questo approccio è fattibile.
I compilatori per il linguaggio C99 devono sempre trattare in modo mainspeciale, in una certa misura, per supportare l'hack che se la funzione termina senza returnun'istruzione, il comportamento è come se return 0fosse eseguito. Questo, ancora una volta, può essere trattato con una trasformazione del codice. Il compilatore nota che mainsi sta compilando una funzione chiamata . Quindi verifica se l'estremità del corpo è potenzialmente raggiungibile. In tal caso, inserisce un filereturn 0;
mainmetodo in un singolo programma inC(o, in realtà, praticamente in qualsiasi lingua con un tale costrutto).