Pourquoi dois-je spécifier le type de données à chaque fois dans C?

Comme vous pouvez le voir dans l’extrait de code ci-dessous, j’ai déclaré une variable char et une variable int . Lorsque le code est compilé, il doit identifier les types de données des variables str et i .

Pourquoi ai-je besoin de répéter lors de l’parsing de ma variable que c’est une variable de type chaîne ou entier en spécifiant %s ou %d à scanf ? Le compilateur n’est-il pas assez mature pour l’identifier lorsque j’ai déclaré mes variables?

 #include  int main () { char str [80]; int i; printf ("Enter your family name: "); scanf ("%s",str); printf ("Enter your age: "); scanf ("%d",&i); return 0; } 

Comme il n’y a pas de moyen portable pour un argument de variable, des fonctions telles que scanf et printf de connaître les types d’arguments variables, pas même le nombre d’arguments transmis.

Voir C FAQ: Comment puis-je découvrir le nombre d’arguments avec lesquels une fonction a été appelée?


C’est la raison pour laquelle il doit y avoir au moins un argument fixe pour déterminer le nombre et peut-être les types des arguments variables. Et cet argument (le standard l’appelle parmN , voir C11 ( ISO / IEC 9899: 201x ) §7.16 Arguments de variable ) joue ce rôle spécial et sera transmis à la macro va_start . En d’autres termes, vous ne pouvez pas avoir une fonction avec un prototype comme celui-ci dans la norme C:

 void foo(...); 

La raison pour laquelle le compilateur ne peut pas fournir les informations nécessaires est simple, car le compilateur n’est pas impliqué ici. Le prototype des fonctions ne spécifie pas les types, car ces fonctions ont des types de variables. Ainsi, les types de données réels ne sont pas déterminés au moment de la compilation, mais lors de l’exécution. La fonction prend alors un argument à partir de la stack, après l’autre. Aucune information de type n’est associée à ces valeurs. La seule manière pour la fonction de savoir interpréter les données est d’utiliser les informations fournies par l’appelant, à savoir la chaîne de format.

Les fonctions elles-mêmes ne savent pas quels types de données sont passés, ni ne connaissent le nombre d’arguments transmis. Il est donc impossible à printf décider lui-même.

En C ++, vous pouvez utiliser la surcharge d’opérateur, mais il s’agit d’un mécanisme totalement différent. Car ici le compilateur choisit la fonction appropriée en fonction des types de données et de la fonction surchargée disponible.

Pour illustrer cela, printf , quand il est compilé, ressemble à ceci:

  push value1 ... push valueN push format_ssortingng call _printf 

Et le prototype de printf est le suivant:

 int printf ( const char * format, ... ); 

Il n’y a donc aucune information de type transmise, sauf ce qui est fourni dans la chaîne de format.

Le compilateur peut être intelligent, mais les fonctions printf ou scanf sont stupides – elles ne savent pas quel type de paramètre transmettez-vous pour chaque appel. C’est pourquoi vous devez transmettre %s ou %d chaque fois.

printf n’est pas une fonction insortingnsèque . Cela ne fait pas partie du langage C en soi. Tout ce que le compilateur fait est de générer du code pour appeler printf , en passant tous les parameters. Maintenant que C ne fournit pas de reflection comme mécanisme pour déterminer les informations de type à l’exécution, le programmeur doit explicitement fournir les informations nécessaires.

Le premier paramètre est une chaîne de format . Si vous imprimez un nombre décimal, cela peut ressembler à ceci:

  • "%d" (nombre décimal)
  • "%5d" (nombre décimal rempli à la largeur 5 avec des espaces)
  • "%05d" (nombre décimal "%05d" à la largeur 5 avec des zéros)
  • "%+d" (nombre décimal, toujours avec un signe)
  • "Value: %d\n" (du contenu avant / après le nombre)

etc, voir par exemple Format des espaces réservés sur Wikipedia pour avoir une idée de ce que les chaînes de format peuvent contenir.

En outre, il peut y avoir plus d’un paramètre ici:

"%s - %d" (une chaîne, puis du contenu, puis un nombre)

Le compilateur n’est-il pas assez mûr pour l’identifier lorsque j’ai déclaré ma variable?

Non.

Vous utilisez un langage spécifié il y a plusieurs décennies. Ne vous attendez pas à une esthétique design moderne de C, car ce n’est pas un langage moderne. Les langues modernes auront tendance à échanger une petite quantité d’efficacité dans la compilation, l’interprétation ou l’exécution pour améliorer la facilité d’utilisation ou la clarté. C est une époque où le temps de traitement des ordinateurs était coûteux et l’offre limitée, et sa conception en est la preuve.

C’est aussi la raison pour laquelle C et C ++ restnt les langues de choix lorsque vous vous souciez vraiment d’être rapide, efficace ou proche du métal.

scanf comme prototype int scanf ( const char * format, ... ); dit que stocke les données données selon le format du paramètre dans les emplacements indiqués par les arguments supplémentaires.

Il n’est pas lié au compilateur, il s’agit de syntaxe définie pour scanf . Le format de paramètre est nécessaire pour permettre à scanf connaître la taille à réserver pour les données à saisir.

GCC (et éventuellement d’autres compilateurs C) gardent une trace des types d’arguments, du moins dans certaines situations. Mais le langage n’est pas conçu de cette façon.

La fonction printf est une fonction ordinaire qui accepte des arguments variables. Les arguments variables nécessitent une sorte de schéma d’identification du type à l’exécution, mais dans le langage C, les valeurs ne comportent aucune information de type à l’exécution. (Bien sûr, les programmeurs C peuvent créer des schémas d’exécution en utilisant des structures ou des astuces de manipulation de bits, mais celles-ci ne sont pas intégrées dans le langage.)

Lorsque nous développons une fonction comme celle-ci:

 void foo(int a, int b, ...); 

nous pouvons transmettre “tout” nombre d’arguments supplémentaires après le second, et c’est à nous de déterminer combien il y en a et quels sont leurs types en utilisant une sorte de protocole qui est en dehors du mécanisme de passage de la fonction.

Par exemple, si nous appelons cette fonction comme ceci:

 foo(1, 2, 3.0); foo(1, 2, "abc"); 

il est impossible à l’appelé de distinguer les cas. Il y a juste quelques bits dans une zone de passage de parameters, et nous n’avons aucune idée s’ils représentent un pointeur sur des données de caractère ou un nombre à virgule flottante.

Les possibilités de communication de ce type d’informations sont nombreuses. Par exemple, dans POSIX, la famille de fonctions exec utilise des arguments de variable qui ont tous le même type, char * et un pointeur nul pour indiquer la fin de la liste:

 #include  void my_exec(char *progname, ...) { va_list variable_args; va_start (variable_args, progname); for (;;) { char *arg = va_arg(variable_args, char *); if (arg == 0) break; /* process arg */ } va_end(variable_args); /*...*/ } 

Si l’appelant oublie de passer une terminaison de pointeur nul, le comportement sera indéfini car la fonction continuera à appeler va_arg après avoir consommé tous les arguments. Notre fonction my_exec doit être appelée comme ceci:

 my_exec("foo", "bar", "xyzzy", (char *) 0); 

Le transtypage sur le 0 est requirejs car il n’y a pas de contexte pour qu’il soit interprété comme une constante de pointeur nul: le compilateur n’a aucune idée que le type prévu pour cet argument est un type de pointeur. De plus (void *) 0 n’est pas correct car il sera simplement passé en tant que type void * et non char * , bien que les deux soient presque certainement compatibles au niveau binary, donc cela fonctionnera dans la pratique. Une erreur commune avec ce type de fonction exec est la suivante:

 my_exec("foo", "bar", "xyzzy", NULL); 

où la NULL du compilateur se trouve être définie comme 0 sans aucune dissortingbution (void *) .

Un autre schéma possible consiste à demander à l’appelant de transmettre un numéro indiquant le nombre d’arguments. Bien sûr, ce nombre pourrait être incorrect.

Dans le cas de printf , la chaîne de format décrit la liste des arguments. La fonction l’parsing et extrait les arguments en conséquence.

Comme mentionné au début, certains compilateurs, notamment le compilateur GNU C, peuvent parsingr les chaînes de format au moment de la compilation et effectuer une vérification de type statique par rapport au nombre et aux types d’arguments.

Cependant, notez qu’une chaîne de format peut être autre qu’un littéral et peut être calculée au moment de l’exécution, ce qui est imperméable à de tels schémas de vérification de type. Exemple fictif:

 char *fmt_ssortingng = message_lookup(current_language, message_code); /* no type checking from gcc in this case: fmt_ssortingng could have four conversion specifiers, or ones not matching the types of arg1, arg2, arg3, without generating any diagnostic. */ snprintf(buffer, sizeof buffer, fmt_ssortingng, arg1, arg2, arg3); 

C’est parce que c’est le seul moyen de dire aux fonctions (comme printf scanf ) quel type de valeur vous passez. par exemple-

 int main() { int i=22; printf("%c",i); return 0; } 

ce code imprimera le caractère non entier 22. car vous avez dit à la fonction printf de traiter la variable comme un caractère char.

printf et scanf sont des fonctions d’E / S conçues et définies de manière à recevoir une chaîne de contrôle et une liste d’arguments.

Les fonctions ne connaissent pas le type de paramètre transmis, et Comstackr ne peut pas non plus lui transmettre ces informations.

Comme vous ne spécifiez pas de type de données dans le printf, vous spécifiez le format de données. C’est une distinction importante dans n’importe quelle langue, et elle est doublement importante en C.

Lorsque vous numérisez une chaîne avec %s , vous ne dites pas “parsing une entrée de chaîne pour ma variable de chaîne”. Vous ne pouvez pas dire cela en C car C n’a pas de type de chaîne. La chose la plus proche de C pour une variable chaîne est un tableau de caractères de taille fixe qui contient des caractères représentant une chaîne, la fin de la chaîne étant indiquée par un caractère nul. Donc ce que vous dites vraiment, c’est “voici un tableau pour contenir la chaîne, je promets que c’est assez grand pour l’entrée de chaîne que je veux parsingr”.

Primitif? Bien sûr. C a été inventé il y a plus de 40 ans, quand une machine typique avait au maximum 64 Ko de RAM. Dans un tel environnement, la conservation de la RAM avait une priorité plus élevée que la manipulation sophistiquée de chaînes.

Néanmoins, le scanner %s persiste dans les environnements de programmation plus avancés, où il existe des types de données de type chaîne. Parce qu’il s’agit de numériser, pas de taper.