Pourquoi l'argv principal C / C ++ est-il déclaré comme "char * argv []" plutôt que simplement "char * argv"?

21

Pourquoi est-il argvdéclaré comme "un pointeur vers un pointeur vers le premier index du tableau", plutôt que d'être simplement "un pointeur vers le premier index du tableau" ( char* argv)?

Pourquoi la notion de "pointeur à pointeur" est-elle requise ici?

un utilisateur
la source
4
"un pointeur pour pointer vers le premier index du tableau" - Ce n'est pas une description correcte de char* argv[]ou char**. C'est un pointeur vers un pointeur sur un personnage; en particulier, le pointeur externe pointe vers le premier pointeur d'un tableau et les pointeurs internes pointent vers les premiers caractères des chaînes terminées par zéro. Il n'y a aucun indice impliqué ici.
Sebastian Redl
12
Comment obtiendriez-vous le deuxième argument s'il ne s'agissait que de char * argv?
gnasher729
15
Votre vie deviendra plus facile lorsque vous placerez l'espace au bon endroit. char* argv[]met l'espace au mauvais endroit. Disons char *argv[], et maintenant il est clair que cela signifie "l'expression *argv[n]est une variable de type char". Ne vous laissez pas prendre à essayer de déterminer ce qu'est un pointeur et ce qui est un pointeur vers un pointeur, etc. La déclaration vous indique quelles opérations vous pouvez effectuer sur cette chose.
Eric Lippert
1
Comparez mentalement char * argv[]à la construction C ++ similaire std::string argv[], et il pourrait être plus facile d'analyser. ... Ne commencez pas à l' écrire de cette façon!
Justin Time - Rétablissez Monica le
2
@EricLippert notez que la question inclut également C ++, et là vous pouvez avoir par exemple char &func(int);qui ne fait pas &func(5)avoir de type char.
Ruslan

Réponses:

59

Argv est essentiellement comme ceci:

entrez la description de l'image ici

À gauche se trouve l'argument lui-même - ce qui est réellement passé comme argument à main. Cela contient l'adresse d'un tableau de pointeurs. Chacun de ces points pointe vers un endroit en mémoire contenant le texte de l'argument correspondant qui a été passé sur la ligne de commande. Ensuite, à la fin de ce tableau, il est garanti qu'il y a un pointeur nul.

Notez que le stockage réel pour les arguments individuels est au moins potentiellement alloué séparément les uns des autres, de sorte que leurs adresses en mémoire peuvent être organisées de manière assez aléatoire (mais selon la façon dont les choses se trouvent écrites, elles peuvent également être dans un seul bloc contigu de mémoire - vous ne savez tout simplement pas et ne devriez pas vous en soucier).

Jerry Coffin
la source
52
Quel que soit le moteur de mise en page qui a dessiné ce diagramme pour vous, il y a un bug dans leur algorithme de minimisation des croisements!
Eric Lippert
43
@EricLippert Pourrait être intentionnel de souligner que les pointes peuvent ne pas être contiguës ni en ordre.
jamesdlin
3
Je dirais que c'est intentionnel
Michael
24
C'était certainement intentionnel - et je suppose que Eric a probablement compris cela, mais (correctement, l'OMI) pensait que le commentaire était drôle de toute façon.
Jerry Coffin
2
@JerryCoffin, on pourrait également souligner que même si les arguments réels étaient contigus en mémoire, ils peuvent avoir des longueurs arbitraires, de sorte que l'on aurait toujours besoin de pointeurs distincts pour chacun d'eux pour pouvoir accéder argv[i]sans parcourir tous les précédents.
ilkkachu
22

Parce que c'est ce que fournit le système d'exploitation :-)

Votre question est un peu un problème d'inversion poulet / œuf. Le problème n'est pas de choisir ce que vous voulez en C ++, le problème est de savoir comment vous dites en C ++ ce que le système d'exploitation vous donne.

Unix transmet un tableau de "chaînes", chaque chaîne étant un argument de commande. En C / C ++, une chaîne est un "char *", donc un tableau de chaînes est char * argv [], ou char ** argv, selon le goût.

passant
la source
13
Non, c'est exactement "le problème de choisir ce que vous voulez en C ++". Windows, par exemple, fournit la ligne de commande sous la forme d'une chaîne unique, et pourtant les programmes C / C ++ reçoivent toujours leur argvtableau - le runtime se charge de tokeniser la ligne de commande et de construire le argvtableau au démarrage.
Joker_vD
14
@Joker_vD Je pense que d'une manière tordue, il s'agit de ce que l'OS vous donne. Plus précisément: je suppose que C ++ l'a fait de cette façon parce que C l'a fait de cette façon, et C l'a fait de cette façon parce qu'à l'époque C et Unix étaient si inextricablement liés et Unix l'a fait de cette façon.
Daniel Wagner
1
@DanielWagner: Oui, cela vient de l'héritage Unix de C. Sous Unix / Linux, un minimum _startappelant maindoit simplement passer mainun pointeur vers le argvtableau existant en mémoire; c'est déjà dans le bon format. Le noyau le copie de l'argument argv vers l' execve(const char *filename, char *const argv[], char *const envp[])appel système qui a été effectué pour démarrer un nouvel exécutable. (Sous Linux, argv [] (le tableau lui-même) et argc sont sur la pile à l'entrée de processus. Je suppose que la plupart des Unix sont les mêmes, car c'est un bon endroit pour cela.)
Peter Cordes
8
Mais le point de Joker ici est que les normes C / C ++ laissent à l'implémentation la provenance des arguments; ils ne doivent pas nécessairement provenir directement du système d'exploitation. Sur un système d'exploitation qui transmet une chaîne plate, une bonne implémentation C ++ devrait inclure la création de jetons, au lieu de définir argc=2et de transmettre la chaîne plate entière. (Suivre la lettre de la norme n'est pas suffisant pour être utile ; cela laisse intentionnellement beaucoup de place aux choix d'implémentation.) Bien que certains programmes Windows veuillent traiter spécialement les guillemets, les implémentations réelles fournissent un moyen d'obtenir la chaîne plate, aussi.
Peter Cordes
1
La réponse de Basile est à peu près cette correction + @ Joker et mes commentaires, avec plus de détails.
Peter Cordes
15

Tout d'abord, en tant que déclaration de paramètre, char **argvest identique à char *argv[]; ils impliquent tous deux un pointeur vers (un tableau ou un ensemble d'un ou plusieurs possibles) pointeur (s) vers des chaînes.

Ensuite, si vous n'avez que "pointer to char" - par exemple juste char *- alors pour accéder au nième élément, vous devrez scanner les n-1 premiers éléments pour trouver le début du nième élément. (Et cela imposerait également l'exigence que chacune des chaînes soit stockée de manière contiguë.)

Avec le tableau de pointeurs, vous pouvez directement indexer le nième élément - donc (bien que pas strictement nécessaire - en supposant que les chaînes sont contiguës), il est généralement beaucoup plus pratique.

Pour illustrer:

./programme bonjour le monde

argc = 3
argv[0] --> "./program\0"
argv[1] --> "hello\0"
argv[2] --> "world\0"

Il est possible que, dans un tableau de caractères fourni par le système d'exploitation:

            "./program\0hello\0world\0"
argv[0]      ^
argv[1]                 ^
argv[2]                        ^

si argv n'était qu'un "pointeur sur char", vous pourriez voir

       "./program\0hello\0world\0"
argv    ^

Cependant (bien que probable par la conception de l'os), il n'y a aucune garantie réelle que les trois chaînes "./program", "hello" et "world" sont contiguës. En outre, ce type de "pointeur unique vers plusieurs chaînes contiguës" est une construction de type de données plus inhabituelle (pour C), en particulier par rapport au tableau de pointeurs vers la chaîne.

Erik Eidt
la source
si au lieu de, argv --> "hello\0world\0"vous avez argv --> index 0 of the array(bonjour), tout comme un tableau normal. pourquoi est-ce pas faisable? alors vous continuez à lire les argcheures du tableau . alors vous passez argv lui-même et non un pointeur sur argv.
un utilisateur le
@auser, c'est ce que argv -> "./program\0hello\0\world\0" est: un pointeur vers le premier caractère (c'est-à-dire le ".") Si vous passez ce pointeur devant le premier \ 0, alors vous avoir un pointeur sur "bonjour \ 0", puis sur "world \ 0". Après les temps d'argc (frapper \ 0 "), vous avez terminé. Bien sûr, cela peut fonctionner, et comme je l'ai dit, une construction inhabituelle.
Erik Eidt
Vous avez oublié de dire que dans votre exemple argv[4]estNULL
Basile Starynkevitch
3
Il y a une garantie (au moins initialement) argv[argc] == NULL. Dans ce cas argv[3], non argv[4].
Miral
1
@Hill, oui, merci car j'essayais d'être explicite sur les terminateurs de caractères nuls (et j'ai raté celui-ci).
Erik Eidt
13

Pourquoi l'argv principal C / C ++ est déclaré comme "char * argv []"

Une réponse possible est que la norme C11 n1570 (au §5.1.2.2.1 Démarrage du programme ) et la norme C ++ 11 n3337 (au §3.6.1 fonction principale ) l' exigent pour les environnements hébergés (mais notez que la norme C mentionne également §5.1.2.1 environnements autonomes ) Voir aussi ceci .

La question suivante est pourquoi les normes C et C ++ ont-elles choisi maind'avoir une telle int main(int argc, char**argv)signature? L'explication est largement historique: C a été inventé avec Unix , qui a un shell qui fait un globbing avant de faire fork(qui est un appel système pour créer un processus) et execve(qui est l'appel système pour exécuter un programme), et qui execvetransmet un tableau des arguments de programme de chaîne et est lié à celui maindu programme exécuté. En savoir plus sur la philosophie Unix et sur les ABI .

Et C ++ s'est efforcé de suivre les conventions de C et d'être compatible avec lui. Il ne pouvait pas définir mainl'incompatibilité avec les traditions C.

Si vous avez conçu un système d'exploitation à partir de zéro (ayant toujours une interface de ligne de commande) et un langage de programmation pour lui à partir de zéro, vous serez libre d'inventer différentes conventions de démarrage de programme. Et d'autres langages de programmation (par exemple Common Lisp ou Ocaml ou Go) ont des conventions de démarrage de programme différentes.

En pratique, mainest invoqué par un code crt0 . Notez que sous Windows, la globalisation peut être effectuée par chaque programme dans l'équivalent de crt0, et certains programmes Windows peuvent démarrer via le point d'entrée WinMain non standard . Sous Unix, la globalisation est effectuée par le shell (et crt0adapte l'ABI, et la disposition initiale de la pile d'appels qu'il a spécifiée, aux conventions d'appel de votre implémentation C).

Basile Starynkevitch
la source
12

Plutôt que de le considérer comme un "pointeur à pointeur", il est utile de le considérer comme un "tableau de chaînes", avec []dénotation tableau et char*dénotation chaîne. Lorsque vous exécutez un programme, vous pouvez lui passer un ou plusieurs arguments de ligne de commande et ceux-ci sont reflétés dans les arguments à main: argcest le nombre d'arguments et argvvous permet d'accéder aux arguments individuels.

casablanca
la source
2
+1 ceci! Dans de nombreux langages - bash, PHP, C, C ++ - argv est un tableau de chaînes. Vous devez penser à cela lorsque vous voyez char **ou char *[], ce qui est le même.
rexkogitans
1

Dans de nombreux cas, la réponse est "parce que c'est une norme". Pour citer la norme C99 :

- Si la valeur d'argc est supérieure à zéro, les membres du tableau argv [0] à argv [argc-1] inclus doivent contenir des pointeurs vers des chaînes , qui reçoivent des valeurs définies par l'implémentation par l'environnement hôte avant le démarrage du programme.

Bien sûr, avant qu'il ne soit normalisé, il était déjà utilisé par K&R C dans les premières implémentations Unix, dans le but de stocker des paramètres de ligne de commande (quelque chose dont vous devez vous soucier dans le shell Unix, comme /bin/bashou /bin/shpas dans les systèmes embarqués). Pour citer la première édition du «langage de programmation C» de K&R (p. 110) :

Le premier (appelé conventionnellement argc ) est le nombre d'arguments de ligne de commande avec lesquels le programme a été appelé ; le second ( argv ) est un pointeur vers un tableau de chaînes de caractères contenant les arguments, un par chaîne.

Sergiy Kolodyazhnyy
la source