Come funziona il metodo main () in C?


96

So che ci sono due firme diverse per scrivere il metodo principale -

int main()
{
   //Code
}

o per gestire l'argomento della riga di comando, lo scriviamo come-

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

In C++So che possiamo sovraccaricare un metodo, ma Ccome fa il compilatore gestire queste due diverse firme di mainfunzione?


14
Il sovraccarico si riferisce alla presenza di due metodi con lo stesso nome nello stesso programma. Puoi avere un solo mainmetodo in un singolo programma in C(o, in realtà, praticamente in qualsiasi lingua con un tale costrutto).
Kyle Strand

12
C non ha metodi; ha funzioni. I metodi sono l'implementazione di back-end di funzioni "generiche" orientate agli oggetti. Il programma chiama una funzione con alcuni argomenti oggetto e il sistema a oggetti sceglie un metodo (o forse un insieme di metodi) in base ai loro tipi. C non ha niente di tutto questo a meno che tu non lo simuli da solo.
Kaz

4
Per una discussione approfondita sui punti di ingresso del programma - non in particolare main- consiglio il classico libro di John R. Levines "Linkers & Loaders".
Andreas Spindler

1
In C, il primo modulo è int main(void), no int main()(anche se non ho mai visto un compilatore che rifiuti il int main()modulo).
Keith Thompson

1
@harper: il ()modulo è obsoleto e non è chiaro se sia consentito main(a meno che l'implementazione non lo documenti specificamente come un modulo consentito). Lo standard C (vedere 5.1.2.2.1 Avvio del programma) non menziona il ()form, che non è del tutto equivalente al ()form. I dettagli sono troppo lunghi per questo commento.
Keith Thompson

Risposte:


132

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:

  1. La funzione chiamante pulisce gli argomenti.
  2. 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;


34

Non vi è alcun sovraccarico mainanche in C ++. La funzione principale è il punto di ingresso di un programma e dovrebbe esistere una sola definizione.

Per Standard C

Per un ambiente ospitato (quello normale), lo standard C99 dice:

5.1.2.2.1 Avvio del programma

La funzione chiamata all'avvio del programma è denominata main. L'implementazione non dichiara alcun prototipo per questa funzione. Deve essere definito con un tipo di ritorno inte senza parametri:

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

o con due parametri (qui indicati come argce argv, sebbene sia possibile utilizzare qualsiasi nome, poiché sono locali rispetto alla funzione in cui sono dichiarati):

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

o equivalente; 9) o in un altro modo definito dall'implementazione.

9) Quindi, intpuò essere sostituito da un nome typedef definito come int, oppure il tipo di argvpuò essere scritto come char **argv, e così via.

Per C ++ standard:

3.6.1 Funzione principale [basic.start.main]

1 Un programma deve contenere una funzione globale chiamata main, che è l'inizio designato del programma. [...]

2 Un'implementazione non deve predefinire la funzione principale. Questa funzione non deve essere sovraccaricata . Deve avere un tipo di ritorno di tipo int, ma per il resto il suo tipo è definito dall'implementazione. Tutte le implementazioni devono consentire entrambe le seguenti definizioni di main:

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

e

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

Lo standard C ++ dice esplicitamente "[La funzione principale] deve avere un tipo di ritorno di tipo int, ma altrimenti il ​​suo tipo è definito dall'implementazione" e richiede le stesse due firme dello standard C.

In un ambiente ospitato ( ambiente AC che supporta anche le librerie C), il sistema operativo chiama main.

In un ambiente non ospitato (uno destinato alle applicazioni incorporate) puoi sempre modificare il punto di ingresso (o uscita) del tuo programma utilizzando le direttive del pre-processore come

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

Dove la priorità è un numero intero opzionale.

Pragma startup esegue la funzione prima della main (per priorità) e pragma exit esegue la funzione dopo la funzione main. Se è presente più di una direttiva di avvio, la priorità decide quale verrà eseguita per prima.


4
Non credo che questa risposta risponda effettivamente alla domanda su come il compilatore gestisce effettivamente la situazione. La risposta data da @Kaz fornisce maggiori informazioni, secondo me.
Tilman Vogel

4
Penso che questa risposta risponda meglio alla domanda di quella di @Kaz. La domanda originale ha l'impressione che si stia verificando un sovraccarico di operatori e questa risposta risolve che mostrando che invece di una soluzione di sovraccarico il compilatore accetta due firme diverse. I dettagli del compilatore sono interessanti ma non necessari per rispondere alla domanda.
Waleed Khan

1
Per gli ambienti indipendenti ("non ospitati"), c'è molto di più che un semplice #pragma. C'è un interrupt di ripristino dall'hardware ed è proprio lì che inizia il programma. Da lì, vengono eseguite tutte le impostazioni fondamentali: stack di installazione, registri, MMU, mappatura della memoria ecc. Quindi si verifica la copia dei valori di inizializzazione da NVM alle variabili di archiviazione statiche (segmento .data), nonché "zero-out" su tutti variabili di archiviazione statica che devono essere impostate su zero (segmento .bss). In C ++ vengono chiamati i costruttori di oggetti con durata di archiviazione statica. E una volta fatto tutto ciò, viene chiamato main.
Lundin

8

Non è necessario sovraccaricare. Sì, ci sono 2 versioni, ma solo una può essere utilizzata alla volta.


5

Questa è una delle strane asimmetrie e regole speciali del linguaggio C e C ++.

Secondo me esiste solo per ragioni storiche e non c'è una vera logica seria dietro di essa. Nota che mainè speciale anche per altri motivi (ad esempio mainin C ++ non può essere ricorsivo e non puoi prenderne l'indirizzo e in C99 / C ++ puoi omettere returnun'istruzione finale ).

Nota anche che anche in C ++ non è un sovraccarico ... o un programma ha la prima forma o ha la seconda forma; non può avere entrambi.


Puoi anche omettere l' returnistruzione in C (da C99).
dreamlax

In C, puoi chiamare main()e prendere il suo indirizzo; C ++ applica limiti che C non fa.
Jonathan Leffler

@JonathanLeffler: hai ragione, risolto. L'unica cosa divertente di main che ho trovato nelle specifiche C99 oltre alla possibilità di omettere il valore di ritorno è che poiché lo standard è formulato IIUC non puoi passare un valore negativo argcquando si ricorre (5.1.2.2.1 non specifica limitazioni su argce si argvapplicano solo alla chiamata iniziale a main).
6502

4

Ciò che è insolito mainnon è che può essere definito in più di un modo, è che può essere definito solo in uno dei due modi diversi.

mainè una funzione definita dall'utente; l'implementazione non dichiara un prototipo per esso.

La stessa cosa è vera per fooo bar, ma puoi definire funzioni con quei nomi come preferisci.

La differenza è che mainviene richiamato dall'implementazione (l'ambiente di runtime), non solo dal proprio codice. L'implementazione non è limitata alla semantica delle chiamate di funzione C ordinaria, quindi può (e deve) gestire alcune variazioni, ma non è necessario per gestire infinite possibilità. Il int main(int argc, char *argv[])modulo consente gli argomenti della riga di comando e int main(void)in C o int main()in C ++ è solo una comodità per i programmi semplici che non hanno bisogno di elaborare gli argomenti della riga di comando.

Per quanto riguarda il modo in cui il compilatore gestisce questo, dipende dall'implementazione. La maggior parte dei sistemi ha probabilmente convenzioni di chiamata che rendono le due forme effettivamente compatibili e qualsiasi argomento passato a un maindefinito senza parametri viene tranquillamente ignorato. In caso contrario, non sarebbe difficile per un compilatore o un linker trattare in modo mainspeciale. Se sei curioso di come funziona sul tuo sistema , potresti guardare alcuni elenchi di assembly.

E come molte cose in C e C ++, i dettagli sono in gran parte il risultato della storia e di decisioni arbitrarie prese dai progettisti dei linguaggi e dai loro predecessori.

Si noti che sia C che C ++ consentono entrambi altre definizioni definite dall'implementazione per main, ma raramente ci sono buone ragioni per usarle. E per le implementazioni indipendenti (come i sistemi embedded senza OS), il punto di ingresso del programma è definito dall'implementazione e non è necessariamente nemmeno chiamato main.


3

Il mainè solo un nome per un indirizzo di partenza deciso dal linker dove mainè il nome predefinito. Tutti i nomi di funzione in un programma sono indirizzi iniziali in cui inizia la funzione.

Gli argomenti della funzione vengono inseriti / estratti dallo stack, quindi se non ci sono argomenti specificati per la funzione non ci sono argomenti inseriti / estratti dallo stack. È così che main può funzionare sia con che senza argomenti.


2

Bene, le due diverse firme della stessa funzione main () vengono visualizzate solo quando le vuoi così, voglio dire se il tuo programma ha bisogno di dati prima di qualsiasi elaborazione effettiva del tuo codice puoi passarle tramite l'uso di -

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

dove la variabile argc memorizza il conteggio dei dati passati e argv è un array di puntatori a char che punta ai valori passati dalla console. Altrimenti va sempre bene

    int main()
    {
       //Code
    }

Comunque in ogni caso può esserci una e una sola main () in un programma, perché è l'unico punto da cui un programma inizia la sua esecuzione e quindi non può essere più di uno. (spero sia degno)


2

Una domanda simile è stata posta prima: perché una funzione senza parametri (rispetto alla definizione di funzione effettiva) viene compilata?

Una delle risposte migliori è stata:

In C func()significa che puoi passare qualsiasi numero di argomenti. Se non vuoi argomenti, devi dichiarare comefunc(void)

Quindi, immagino sia come mainviene dichiarato (se puoi applicare il termine "dichiarato" a main). In effetti puoi scrivere qualcosa del genere:

int main(int only_one_argument) {
    // code
}

e verrà comunque compilato ed eseguito.


1
Ottima osservazione! Sembra che il linker sia abbastanza indulgente main, poiché c'è un problema non ancora menzionato: ancora più argomenti per main! "Unix (ma non Posix.1) e Microsoft Windows" aggiungono char **envp(ricordo che anche DOS lo consentiva, non è vero?), E Mac OS X e Darwin aggiungono ancora un altro puntatore char * "informazioni fornite dal sistema operativo arbitrario". wikipedia
usr2564301

0

Non è necessario sovrascriverlo. Perché verrà utilizzato solo uno alla volta. Sì, ci sono 2 diverse versioni della funzione principale

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.