web-dev-qa-db-fra.com

Pourquoi * Déclaration * des données et des fonctions nécessaires dans la langue C, lorsque la définition est écrite à la fin du code source?

Considérez le code "C" suivant:

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i() est défini à la fin du code source et aucune déclaration n'est fournie avant son utilisation dans main(). Au moment même où le compilateur voit Func_i() in main(), il sort de la fonction main() et découvre Func_i(). Le compilateur trouve en quelque sorte la valeur renvoyée par Func_i() et la donne à printf(). Je sais aussi que le compilateur ne peut pas trouver le type de retour de Func_i(). Il faut par défaut (devines?) Type de retour de Func_i() doit être int. C'est-à-dire que si le code avait float Func_i() alors le compilateur donnerait l'erreur: Types de conflit pour Func_i().

De la discussion ci-dessus, nous voyons que:

  1. Le compilateur peut trouver la valeur renvoyée par Func_i().

    • Si le compilateur peut trouver la valeur renvoyée par Func_i() en sortant de la fonction main() et sur la recherche du code source, pourquoi ne peut-il pas trouver le type de func_i (), qui est explicitement mentionné.
  2. Le compilateur doit savoir que Func_i() est de type float - c'est pourquoi il donne l'erreur de types conflictuels.

  • Si le compilateur sait que Func_i est de type float, alors pourquoi assume-t-il toujours Func_i() pour être de type INT et donne l'erreur de types conflictuels? Pourquoi ne faites-la pas avec force Func_i() pour être de type flotteur.

J'ai le même doute avec la déclaration de variable . Considérez le code "C" suivant:

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

Le compilateur donne l'erreur: 'data_i' non déclaré (première utilisation dans cette fonction).

  • Lorsque le compilateur voit Func_i(), il passe au code source pour trouver la valeur renvoyée par Func_ (). Pourquoi le compilateur ne peut-il pas faire la même chose pour la variable data_i?

Edit :

Je ne connais pas les détails du fonctionnement intérieur du compilateur, de l'assembleur, du processeur, etc. L'idée de base de ma question est que si je dis (écrire) la valeur de retour de la fonction dans le code source enfin, après l'utilisation. de cette fonction, la langue "C" permet à l'ordinateur de trouver cette valeur sans donner d'erreur. Maintenant, pourquoi l'ordinateur ne peut-il pas trouver le type de la même manière. Pourquoi le type de données_i ne peut-il pas être trouvé que la valeur de retour Func_i () a été trouvée. Même si j'utilise l'instruction extern data-type identifier;, je ne dis pas la valeur d'être renvoyée par cet identifiant (fonction/variable). Si l'ordinateur peut trouver cette valeur, alors pourquoi ne peut-il pas trouver le type. Pourquoi avons-nous besoin de la déclaration avant du tout?

Merci.

15
user106313

Parce que c est un simple passe, typée statiquement, faiblement typée, compilé langue.

  1. simple passe signifie que le compilateur n'a pas l'air de voir la définition d'une fonction ou d'une variable. Étant donné que le compilateur ne regarde pas l'avenir, la déclaration d'une fonction doit venir avant l'utilisation de la fonction, sinon le compilateur ne sait pas quelle est sa signature de type. Cependant, la définition de la fonction peut être plus tard dans le même fichier, soit même dans un fichier différent. Voir point n ° 4.

    La seule exception est l'artefact historique que les fonctions et les variables non déclarées sont présumées de type "INT". La pratique moderne consiste à éviter une frappe implicite en déclarant toujours des fonctions et des variables explicitement.

  2. typographié statiquement signifie que toutes les informations de type sont calculées au moment de la compilation. Ces informations sont ensuite utilisées pour générer du code de machine exécutant au moment de l'exécution. Il n'y a pas de concept en C de la dactylographie du temps d'exécution. Une fois un int, toujours un int, une fois un flotteur, toujours un flotteur. Cependant, ce fait est quelque peu obscurci par le point suivant.

  3. faiblement typée signifie que le compilateur C génère automatiquement un code à convertir entre les types numériques sans nécessiter le programmateur de spécifier explicitement les opérations de conversion. En raison de la typographie statique, la même conversion sera toujours effectuée de la même manière à chaque fois par le biais du programme. Si une valeur de flotteur est convertie en une valeur INT à un endroit donné dans le code, une valeur de flotteur sera toujours convertie en une valeur INT à cet endroit dans le code. Cela ne peut pas être changé au moment de l'exécution. La valeur elle-même peut changer d'une seule exécution du programme à l'autre, bien sûr, et les états conditionnels peuvent modifier quelles sections de code sont exécutées dans quel ordre, mais une seule section de code donnée sans appels de fonctions ou de conditionnels effectuera toujours l'exact. mêmes opérations chaque fois que cela est exécuté.

  4. compilé signifie que le processus d'analyse du code source lisible par l'homme et de le transformer en instructions lisibles par la machine est entièrement effectué avant que le programme ne fonctionne. Lorsque le compilateur compilait une fonction, il n'a aucune connaissance de ce qu'elle rencontrera plus loin dans un fichier source donné. Cependant, une fois la compilation (et l'assemblage, la liaison, etc.) ont terminé, chaque fonction de l'exécutable fini contient des pointeurs numériques aux fonctions qu'il appellera lorsqu'elle sera exécutée. C'est pourquoi Main () peut appeler une fonction plus bas dans le fichier source. Au fil du temps () est effectivement exécuté, il contiendra un pointeur à l'adresse de Func_i ().

    Le code de la machine est très, très spécifique. Le code d'ajout de deux entiers (3 + 2) est différent de celui pour ajouter deux flotteurs (3.0 + 2.0). Ce sont tous les deux différents de l'ajout d'un Int à un flotteur (3 + 2.0), et ainsi de suite. Le compilateur détermine à chaque point dans une fonction Quelle opération exacte doit être effectuée à ce stade et génère un code qui effectue cette opération exacte. Une fois que cela a été fait, il ne peut pas être changé sans recompiler la fonction.

Mettre tous ces concepts ensemble, la raison que la principale () ne peut pas "voir" plus bas pour déterminer le type de func_i () est que l'analyse de type se produit au tout début du processus de compilation. À ce stade, seule la partie du fichier source jusqu'à la définition de la principale () a été lue et analysée, et la définition de Func_i () n'est pas encore connue du compilateur.

La raison que la principale () peut "voir" où func_i () est de appel C'est que l'appel se passe au moment de l'exécution, après compilation a déjà résolu tous les noms et types de tous les identificateurs, L'assemblage a déjà converti toutes les fonctions au code de la machine et la liaison a déjà inséré l'adresse correcte de chaque fonction dans chaque endroit où elle est appelée.

Bien sûr, j'ai laissé la plupart des détails de Gory. Le processus réel est beaucoup, beaucoup plus compliqué. J'espère que j'ai fourni assez d'un aperçu de haut niveau pour répondre à vos questions.

De plus, rappelez-vous que ce que j'ai écrit ci-dessus s'applique spécifiquement à C.

Dans d'autres langues, le compilateur peut effectuer plusieurs passes via le code source, et le compilateur pourrait donc récupérer la définition de Func_i () sans qu'il soit prédeclamé.

Dans d'autres langues, fonctions et/ou variables peuvent être dactylographiques dynamiquement, une seule variable peut être maintenue, ou une seule fonction pourrait être transmise ou retour, un entier, un flotteur, une chaîne, un tableau ou un objet à des moments différents.

Dans d'autres langues, la saisie peut être plus forte, nécessitant une conversion de point flottant sur entier pour être explicitement spécifiée. Dans encore d'autres langues, la typing peut être plus faible, permettant la conversion de la chaîne "3.0" au flotteur 3.0 à l'entiers 3 à effectuer automatiquement.

Et dans d'autres langues, le code peut être interprété d'une ligne à la fois, ou compilé au code d'octet puis interprété, ou juste à temps compilé, ou dans une grande variété d'autres régimes d'exécution.

26
Clement Cherlin

Premièrement, vos programmes sont valables pour la norme C90, mais pas pour ceux suivants. Int implicite (permettant de déclarer une fonction sans donner son type de retour) et une déclaration implicite de fonctions (permettant d'utiliser une fonction sans le déclarer) ne sont plus valables.

Deuxièmement, cela ne fonctionne pas comme vous le pensez.

  1. Le type de résultat est facultatif en C90, sans donner un moyen de résultat int. Qu'il est également vrai pour la déclaration variable (mais vous devez donner une classe de stockage, static ou extern).

  2. Ce que le compilateur fait lorsque vous voyez le Func_i Est appelé sans déclaration précédente, suppose qu'il y a une déclaration

    extern int Func_i();
    

    cela ne ressemble pas plus loin dans le code pour voir à quel point Func_i est déclaré. Si Func_i N'a pas été déclaré ou défini, le compilateur ne changerait pas son comportement lors de la compilation main. La déclaration implicite n'est que pour la fonction, il n'y en a pas à variable.

    Notez que la liste des paramètres vides dans la déclaration ne signifie pas que la fonction ne prend pas de paramètres (vous devez spécifier (void) Pour cela), cela signifie que le compilateur n'a pas à vérifier les types de Les paramètres et seront les mêmes conversions implicites appliquées aux arguments passés aux fonctions variadiques.

10
AProgrammer

Vous avez écrit dans un commentaire:

L'exécution est effectuée ligne par ligne. Le seul moyen de trouver la valeur renvoyée par Func_i () est de sauter de la principale

C'est une idée fausse: l'exécution n'est pas la ligne de ligne. compilation est effectué ligne par ligne et la résolution de nom est effectuée pendant la compilation, et elle ne résout que les noms, et non les valeurs de retour.

Un modèle conceptuel utile est-ce: lorsque le compilateur lit la ligne:

  printf("func:%d",Func_i());

il émet un code équivalent à:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

Le compilateur fait également une note dans une table interne que function #2 est une fonction pas encore déclarée nommée Func_i, qui prend un nombre non spécifié d'arguments et renvoie un INT (la valeur par défaut).

Plus tard, quand il analyse ceci:

 int Func_i() { ...

le compilateur lève les yeux Func_i Dans le tableau mentionné ci-dessus et vérifie si les paramètres et le type de retour correspondent. S'ils ne le font pas, il s'arrête avec un message d'erreur. S'ils le font, il ajoute l'adresse actuelle à la table de fonction interne et passe à la ligne suivante.

Donc, le compilateur n'a pas "l'air" pour Func_i Quand il a analysé la première référence. Il a simplement pris une note dans une table, la poursuite de la ligne suivante. Et à la fin du fichier, il possède un fichier d'objet et une liste d'adresses de saut.

Plus tard, la liaison prend tout cela et remplace tous les pointeurs sur "fonction n ° 2" avec l'adresse de saut réelle, alors il émet quelque chose comme:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

Beaucoup plus tard, lorsque le fichier exécutable est exécuté, l'adresse de saut est déjà résolue et l'ordinateur peut simplement passer à l'adresse 0x1215. Aucun nom de recherche requis.

Disclaimer: Comme je l'ai dit, c'est un modèle conceptuel et le monde réel est plus compliqué. Les compilateurs et les liaiseurs font toutes sortes d'optimisations folles aujourd'hui. Ils puissantes "sauter un bas" à rechercher Func_i, bien que j'en doute. Mais les langues C sont définies d'une manière que vous pourrait écrire un compilateur super simple comme ça. Donc, la plupart du temps, c'est un modèle très utile.

7
nikie

C et un certain nombre d'autres langues qui nécessitent des déclarations ont été conçues à une époque lorsque l'heure du processeur et la mémoire étaient chères. Le développement de C et Unix s'est rendu de pair pendant un certain temps, et ce dernier n'avait pas de mémoire virtuelle avant la comparution de 3BSD en 1979. Sans la pièce supplémentaire de travail, les compilateurs avaient tendance à être Un seul passage Affaires parce qu'ils n'avaient pas besoin de la capacité de conserver une certaine représentation de l'ensemble du fichier en mémoire de la même manière.

Les compilateurs à simple passe sont, comme nous, sellés avec une incapacité à voir dans le futur. Cela signifie que les seules choses qu'ils peuvent savoir soient sûrement ce qu'ils ont été informés explicitement avant la compilation de la ligne de code. C'est clair à l'une de nous que Func_i() est déclarée plus tard dans le fichier source, mais le compilateur, qui fonctionne sur un petit morceau de code à la fois, n'a aucune idée de son arrivée.

Au début de C (AT & T, K & R, C89), l'utilisation d'une fonction foo() Avant la déclaration a abouti à un de facto ou de déclaration implicite de int foo(). Votre exemple fonctionne fonctionne lorsque Func_i() est déclaré int car il correspond à ce que le compilateur a déclaré en votre nom. Le remplacer à tout autre type entraînera un conflit car il ne correspond plus plus ce que le compilateur a choisi en l'absence d'une déclaration explicite. Ce comportement a été supprimé en C99, où l'utilisation d'une fonction non déclarée est devenue une erreur.

Alors qu'en est-il des types de retour?

La convention appelante pour le code objet dans la plupart des environnements nécessite de connaître uniquement l'adresse de la fonction appelée, qui est relativement facile pour les compilateurs et les liaiseurs à traiter. L'exécution saute au début de la fonction et revient quand elle revient. Toute autre chose, notamment les arrangements d'arguments de passage et de valeur de retour, est entièrement déterminé par l'appelant et la callee dans un arrangement appelé a convention appelante . Tant que les deux partagent le même ensemble de conventions, il devient possible pour un programme d'appeler des fonctions dans d'autres fichiers d'objet, qu'ils ont été compilés dans n'importe quelle langue qui partage ces conventions. (En informatique scientifique, vous rencontrez beaucoup de C appelant Fortran et vice versa, et la capacité de le faire provient d'une convention appelante.)

Une autre caractéristique du c'était que les prototypes comme nous le connaissons maintenant n'existaient pas. Vous pouvez déclarer le type de retour d'une fonction (par exemple, int foo()), mais pas ses arguments (c'est-à-dire int foo(int bar) n'était pas une option). Cela existait parce que, comme indiqué ci-dessus, le programme a toujours été bloqué dans une convention appelante pouvant être déterminée par les arguments. Si vous avez appelé une fonction avec le mauvais type d'arguments, il s'agissait d'une ordures dans la situation des ordures.

Étant donné que le code d'objet a la notion d'un retour mais pas un type de retour, un compilateur doit connaître le type de retour pour faire face à la valeur renvoyée. Lorsque vous exécutez des instructions sur la machine, tout simplement des bits et le processeur ne se soucient pas de savoir si la mémoire où vous essayez de comparer un double a réellement un int dedans. Cela fait simplement ce que vous demandez, et si vous le cassez, vous possédez les deux morceaux.

Considérez ces bits de code:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

Le code sur la gauche compile jusqu'à un appel à foo() suivi en copiant le résultat fourni via la convention d'appel/retour dans où x est stocké. C'est le cas facile.

Le code sur la droite montre une conversion de type et c'est pourquoi les compilateurs doivent connaître le type de retour d'une fonction. Les numéros de point flottant ne peuvent pas être largués dans la mémoire dans laquelle autre code s'attendra à voir un int car il n'y a pas de conversion magique qui a lieu. Si le résultat final doit être un entier, il doit y avoir des instructions qui guident le processeur pour effectuer la conversion avant le stockage. Sans connaître le type de retour de foo() À l'avance, le compilateur n'aurait aucune idée que le code de conversion est nécessaire.

Les compilateurs multiples permettent toutes sortes de choses, dont l'une est la possibilité de déclarer des variables, des fonctions et des méthodes après leur utilisation pour la première fois. Cela signifie que lorsque le compilateur se contente de compiler le code, il a déjà vu l'avenir et sait quoi faire. Java, par exemple, Mandates Multi-Pass en vertu du fait que sa syntaxe permet une déclaration après utilisation.

5
Blrfl