Comment fonctionne la méthode main () en C?

96

Je sais qu'il y a deux signatures différentes pour écrire la méthode principale -

int main()
{
   //Code
}

ou pour gérer l'argument de la ligne de commande, nous l'écrivons comme-

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

Dans C++Je sais, nous pouvons surcharger une méthode, mais Ccomment le compilateur gère-t-il ces deux signatures de mainfonction différentes?

Ritesh
la source
14
La surcharge se réfère à avoir deux méthodes avec le même nom dans le même programme. Vous ne pouvez avoir qu'une seule mainméthode dans un seul programme dans C(ou, vraiment, dans à peu près n'importe quel langage avec une telle construction).
Kyle Strand
13
C n'a pas de méthodes; il a des fonctions. Les méthodes sont l'implémentation back-end de fonctions "génériques" orientées objet. Le programme appelle une fonction avec des arguments d'objet, et le système d'objets choisit une méthode (ou peut-être un ensemble de méthodes) en fonction de leurs types. C n'a rien de tout cela à moins que vous ne le simuliez vous-même.
Kaz
4
Pour une discussion approfondie sur les points d'entrée du programme - pas particulièrement main- je recommande le livre classique de John R. Levines "Linkers & Loaders".
Andreas Spindler
1
En C, le premier formulaire est int main(void), non int main()(bien que je n'ai jamais vu un compilateur qui rejette le int main()formulaire).
Keith Thompson
1
@harper: Le ()formulaire est obsolescent, et il n'est pas clair qu'il soit même autorisé main(à moins que l'implémentation ne le documente spécifiquement comme un formulaire autorisé). Le standard C (voir 5.1.2.2.1 Démarrage du programme) ne mentionne pas le ()formulaire, qui n'est pas tout à fait équivalent au ()formulaire. Les détails sont trop longs pour ce commentaire.
Keith Thompson

Réponses:

133

Certaines des fonctionnalités du langage C ont commencé comme des hacks qui ont fonctionné.

Les signatures multiples pour les listes d'arguments principales et de longueur variable font partie de ces fonctionnalités.

Les programmeurs ont remarqué qu'ils peuvent passer des arguments supplémentaires à une fonction, et rien de mauvais ne se produit avec leur compilateur donné.

C'est le cas si les conventions d'appel sont telles que:

  1. La fonction appelante nettoie les arguments.
  2. Les arguments les plus à gauche sont plus proches du haut de la pile, ou de la base du cadre de la pile, de sorte que des arguments faux n'invalident pas l'adressage.

Un ensemble de conventions d'appel qui obéit à ces règles est le passage de paramètre basé sur la pile, par lequel l'appelant affiche les arguments, et ils sont poussés de droite à gauche:

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

Dans les compilateurs où ce type de convention d'appel est le cas, rien de spécial ne doit être fait pour prendre en charge les deux types de main, voire des types supplémentaires. mainpeut être une fonction d'aucun argument, auquel cas il est inconscient des éléments qui ont été poussés sur la pile. Si c'est une fonction de deux arguments, alors il trouve argcet argvcomme les deux éléments de pile supérieurs. S'il s'agit d'une variante à trois arguments spécifique à la plate-forme avec un pointeur d'environnement (une extension commune), cela fonctionnera également: il trouvera ce troisième argument comme troisième élément à partir du haut de la pile.

Ainsi, un appel fixe fonctionne dans tous les cas, permettant à un seul module de démarrage fixe d'être lié au programme. Ce module pourrait être écrit en C, comme une fonction ressemblant à ceci:

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

En d'autres termes, ce module de démarrage appelle simplement un main à trois arguments, toujours. Si main ne prend aucun argument, ou seulement int, char **, cela fonctionne correctement, ainsi que s'il ne prend aucun argument, en raison des conventions d'appel.

Si vous deviez faire ce genre de chose dans votre programme, ce serait non portable et considéré comme un comportement indéfini par ISO C: déclarer et appeler une fonction d'une manière et la définir d'une autre. Mais l'astuce de démarrage d'un compilateur n'a pas besoin d'être portable; il n'est pas guidé par les règles des programmes portables.

Mais supposons que les conventions d'appel soient telles que cela ne peut pas fonctionner de cette façon. Dans ce cas, le compilateur doit traitermain spécialement. Lorsqu'il remarque qu'il compile la mainfonction, il peut générer du code compatible avec, par exemple, un appel à trois arguments.

C'est-à-dire que vous écrivez ceci:

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

Mais lorsque le compilateur le voit, il effectue essentiellement une transformation de code afin que la fonction qu'il compile ressemble plus à ceci:

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

sauf que les noms __argc_ignore n'existent pas littéralement. Aucun de ces noms n'est introduit dans votre portée, et il n'y aura aucun avertissement concernant les arguments inutilisés. La transformation de code oblige le compilateur à émettre du code avec la liaison correcte qui sait qu'il doit nettoyer trois arguments.

Une autre stratégie d'implémentation consiste pour le compilateur ou peut-être l'éditeur de liens à générer la __startfonction (ou quel que soit son nom), ou au moins en sélectionner une parmi plusieurs alternatives précompilées. Des informations peuvent être stockées dans le fichier objet sur lequel des formulaires pris en charge de mainest utilisé. L'éditeur de liens peut consulter ces informations et sélectionner la version correcte du module de démarrage qui contient un appel maincompatible avec la définition du programme. Les implémentations C n'ont généralement qu'un petit nombre de formes prises en charge, maindonc cette approche est faisable.

Les compilateurs pour le langage C99 doivent toujours traiter mainspécialement, dans une certaine mesure, pour prendre en charge le hack que si la fonction se termine sans returninstruction, le comportement est comme si elle return 0était exécutée. Ceci, encore une fois, peut être traité par une transformation de code. Le compilateur remarque qu'une fonction appelée mainest en cours de compilation. Ensuite, il vérifie si l'extrémité du corps est potentiellement accessible. Si tel est le cas, il insère unreturn 0;

Kaz
la source
34

Il n'y a PAS de surcharge de main même en C ++. La fonction principale est le point d'entrée d'un programme et une seule définition doit exister.

Pour la norme C

Pour un environnement hébergé (c'est normal), la norme C99 dit:

5.1.2.2.1 Démarrage du programme

La fonction appelée au démarrage du programme est nommée main. L'implémentation ne déclare aucun prototype pour cette fonction. Il doit être défini avec un type de retour de intet sans paramètres:

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

ou avec deux paramètres (appelés ici argcet argv, bien que tous les noms puissent être utilisés, car ils sont locaux à la fonction dans laquelle ils sont déclarés):

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

ou équivalent; 9) ou d'une autre manière définie par l'implémentation.

9) Ainsi, intpeut être remplacé par un nom typedef défini comme int, ou le type de argvpeut être écrit comme char **argv, et ainsi de suite.

Pour le C ++ standard:

3.6.1 Fonction principale [basic.start.main]

1 Un programme doit contenir une fonction globale appelée main, qui est le début désigné du programme. [...]

2 Une implémentation ne doit pas prédéfinir la fonction principale. Cette fonction ne doit pas être surchargée . Il doit avoir un type de retour de type int, mais sinon, son type est défini par l'implémentation. Toutes les implémentations doivent permettre les deux définitions suivantes de main:

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

et

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

Le standard C ++ dit explicitement "Elle [la fonction principale] doit avoir un type de retour de type int, mais sinon son type est défini par l'implémentation", et nécessite les deux mêmes signatures que le standard C.

Dans un environnement hébergé ( environnement AC qui prend également en charge les bibliothèques C) - les appels du système d'exploitation main.

Dans un environnement non hébergé (destiné aux applications embarquées), vous pouvez toujours modifier le point d'entrée (ou de sortie) de votre programme en utilisant les directives du pré-processeur comme

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

Où la priorité est un nombre entier facultatif.

Le démarrage de Pragma exécute la fonction avant que le main (par priorité) et la sortie de pragma exécute la fonction après la fonction principale. S'il y a plus d'une directive de démarrage, la priorité décide de celle qui s'exécutera en premier.

Sadique
la source
4
Je ne pense pas que cette réponse réponde réellement à la question de savoir comment le compilateur gère réellement la situation. La réponse donnée par @Kaz donne plus d'informations, à mon avis.
Tilman Vogel
4
Je pense que cette réponse répond mieux à la question que celle de @Kaz. La question d'origine est sous l'impression que la surcharge d'opérateurs se produit, et cette réponse résout cela en montrant qu'au lieu d'une solution de surcharge, le compilateur accepte deux signatures différentes. Les détails du compilateur sont intéressants mais pas nécessaires pour répondre à la question.
Waleed Khan
1
Pour les environnements autonomes («non hébergés»), il se passe bien plus que de simples #pragma. Il y a une interruption de réinitialisation du matériel et c'est vraiment là que le programme commence. À partir de là, toutes les configurations fondamentales sont exécutées: pile de configuration, registres, MMU, mappage de la mémoire, etc. variables de stockage statiques qui doivent être définies sur zéro (segment .bss). En C ++, les constructeurs d'objets avec une durée de stockage statique sont appelés. Et une fois que tout cela est fait, alors main est appelée.
Lundin
8

Il n'y a pas besoin de surcharge. Oui, il existe 2 versions, mais une seule peut être utilisée à la fois.

user694733
la source
5

C'est l'une des étranges asymétries et règles spéciales du langage C et C ++.

À mon avis, cela n'existe que pour des raisons historiques et il n'y a pas de réelle logique sérieuse derrière cela. Notez que mainc'est spécial aussi pour d'autres raisons (par exemple mainen C ++ ne peut pas être récursif et vous ne pouvez pas prendre son adresse et en C99 / C ++, vous êtes autorisé à omettre un finalreturn instruction ).

Notez également que même en C ++, ce n'est pas une surcharge ... soit un programme a la première forme, soit la deuxième forme; il ne peut pas avoir les deux.

6502
la source
Vous pouvez également omettre l' returninstruction en C (depuis C99).
dreamlax
En C, vous pouvez appeler main()et prendre son adresse; C ++ applique des limites que C n'applique pas.
Jonathan Leffler
@JonathanLeffler: vous avez raison, fixe. La seule chose amusante à propos de la principale que j'ai trouvée dans les spécifications C99 en plus de la possibilité d'omettre la valeur de retour est que comme la norme est libellée IIUC, vous ne pouvez pas passer une valeur négative argclors de la récurrence (5.1.2.2.1 ne spécifie pas de limitations sur argcet argvs'appliquent uniquement à l'appel initial à main).
6502
4

Ce qui est inhabituel au sujet mainn'est pas qu'elle peut être définie dans plus d'une façon, il est qu'il peut seulement être défini dans l' une de deux façons différentes.

mainest une fonction définie par l'utilisateur; l'implémentation ne déclare pas de prototype pour cela.

La même chose est vraie pour fooou bar, mais vous pouvez définir des fonctions avec ces noms comme vous le souhaitez.

La différence est qu'elle mainest appelée par l'implémentation (l'environnement d'exécution), pas seulement par votre propre code. L'implémentation n'est pas limitée à la sémantique ordinaire des appels de fonction C, elle peut donc (et doit) traiter quelques variations - mais elle n'est pas obligée de gérer une infinité de possibilités. Le int main(int argc, char *argv[])formulaire permet les arguments de ligne de commande, et int main(void)en C ouint main() en C ++ est juste une commodité pour les programmes simples qui n'ont pas besoin de traiter les arguments de ligne de commande.

Quant à la façon dont le compilateur gère cela, cela dépend de l'implémentation. La plupart des systèmes ont probablement des conventions d'appel qui rendent les deux formes effectivement compatibles, et tous les arguments passés à un maindéfini sans paramètre sont discrètement ignorés. Sinon, il ne serait pas difficile pour un compilateur ou un éditeur de liens de traiter mainspécialement. Si vous êtes curieux de savoir comment cela fonctionne sur votre système , vous pouvez consulter certaines listes d'assemblages.

Et comme beaucoup de choses en C et C ++, les détails sont en grande partie le résultat de l'histoire et des décisions arbitraires prises par les concepteurs des langages et leurs prédécesseurs.

Notez que C et C ++ autorisent tous les deux d'autres définitions définies par l'implémentation pour main- mais il y a rarement de bonnes raisons de les utiliser. Et pour les implémentations autonomes (comme les systèmes embarqués sans OS), le point d'entrée du programme est défini par l'implémentation et n'est même pas nécessairement appelé main.

Keith Thompson
la source
3

Le mainest juste un nom pour une adresse de départ décidée par l'éditeur de liens où mainest le nom par défaut. Tous les noms de fonction d'un programme sont des adresses de départ où la fonction démarre.

Les arguments de fonction sont poussés / sautés sur / à partir de la pile, donc s'il n'y a pas d'arguments spécifiés pour la fonction, il n'y a pas d'arguments poussés / sautés sur / hors de la pile. C'est ainsi que main peut fonctionner à la fois avec ou sans arguments.

AndersK
la source
2

Eh bien, les deux signatures différentes de la même fonction main () n'apparaissent que lorsque vous le souhaitez, je veux dire si votre programme a besoin de données avant tout traitement réel de votre code, vous pouvez les transmettre via l'utilisation de -

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

où la variable argc stocke le nombre de données passées et argv est un tableau de pointeurs vers char qui pointe vers les valeurs transmises depuis la console. Sinon c'est toujours bon d'accompagner

    int main()
    {
       //Code
    }

Cependant, dans tous les cas, il peut y avoir un et un seul main () dans un programme, car c'est le seul point où à partir d'un programme commence son exécution et donc il ne peut pas y en avoir plus d'un. (j'espère que c'est digne)

manish
la source
2

Une question similaire a été posée auparavant: Pourquoi une fonction sans paramètres (par rapport à la définition de fonction réelle) se compile-t-elle?

L'une des réponses les mieux classées était:

En C func()signifie que vous pouvez passer n'importe quel nombre d'arguments. Si vous ne voulez aucun argument, vous devez déclarer commefunc(void)

Donc, je suppose que c'est comment mainest déclaré (si vous pouvez appliquer le terme "déclaré" à main). En fait, vous pouvez écrire quelque chose comme ceci:

int main(int only_one_argument) {
    // code
}

et il sera toujours compilé et exécuté.

varepsilon
la source
1
Excellente observation! Il semble que l'éditeur de liens pardonne assez main, car il y a un problème non encore mentionné: encore plus d' arguments pour main! «Unix (mais pas Posix.1) et Microsoft Windows» ajoutent char **envp(je me souviens que DOS l'a également autorisé, n'est-ce pas?), Et Mac OS X et Darwin ajoutent encore un autre pointeur char * «d'informations arbitraires fournies par le système d'exploitation». wikipedia
usr2564301
0

Vous n'avez pas besoin de remplacer cela, car une seule sera utilisée à la fois Oui, il existe 2 versions différentes de la fonction principale

Gautam
la source