Pourquoi la * déclaration * des données et fonctions est-elle nécessaire en langage C, alors que la définition est écrite à la fin du code source?


15

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()dans main(), il sort du main()et découvre Func_i(). Le compilateur trouve en quelque sorte la valeur retournée par Func_i()et la donne à printf(). Je sais également que le compilateur ne peut pas trouver le type de retour de Func_i(). Par défaut, il prend (devine?) Le type de retour de Func_i()be int. C'est-à-dire que si le code avait float Func_i()alors le compilateur donnerait l'erreur: Types en conflit pourFunc_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 retournée par Func_i()en sortant de main()et en recherchant le code source, alors pourquoi ne peut-il pas trouver le type de Func_i (), qui est explicitement mentionné.
  2. Le compilateur doit savoir qu'il Func_i()est de type float - c'est pourquoi il donne l'erreur de types en conflit.

  • Si le compilateur sait qu'il Func_iest de type float, alors pourquoi suppose-t-il toujours Func_i()qu'il est de type int et donne l'erreur de types en conflit? Pourquoi ne pas forcément Func_i()être de type float.

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).

  • Quand le compilateur voit Func_i() , il descend dans le code source pour trouver la valeur retournée par Func_ (). Pourquoi le compilateur ne peut-il pas faire de même pour la variable Data_i?

Éditer:

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

Je vous remercie.


7
Le compilateur ne "trouve" pas la valeur renvoyée par Func_i. cela se fait au moment de l'exécution.
James McLeod

26
Je n'ai pas déçu, mais la question est basée sur de graves malentendus sur le fonctionnement des compilateurs, et vos réponses dans les commentaires suggèrent que vous avez encore des obstacles conceptuels à surmonter.
James McLeod

4
Notez que le premier exemple de code n'a pas été valide, conforme au code standard au cours des quinze dernières années; C99 a fait l'absence d'un type de retour dans les définitions de fonction et la déclaration implicite de Func_inon valide. Il n'y a jamais eu de règle pour déclarer implicitement des variables non définies, de sorte que le deuxième fragment était toujours mal formé. (Oui, les compilateurs acceptent toujours le premier échantillon car il était valide, s'il était bâclé, sous C89 / C90.)
Jonathan Leffler

19
@ user31782: Conclusion à la question: Pourquoi la langue X fait-elle / requiert-elle Y? Parce que c'est le choix que les designers ont fait. Vous semblez dire que les concepteurs de l'une des langues les plus réussies auraient jamais dû faire des choix différents il y a des décennies plutôt que d'essayer de comprendre ces choix dans le contexte où ils ont été faits. La réponse à votre question: pourquoi avons-nous besoin d'une déclaration préalable? a été donné: Parce que C utilise un compilateur en un seul passage. La réponse la plus simple à la plupart de vos questions complémentaires est Parce qu'alors ce ne serait pas un compilateur en un seul passage.
Mr.Mindor

4
@ user31782 Vous voulez vraiment, vraiment lire le livre du dragon pour comprendre comment les compilateurs et les processeurs fonctionnent réellement - il est tout simplement impossible de distiller toutes les connaissances requises dans une seule réponse SO (ou même 100 à cela). Excellent livre pour quiconque s'intéresse aux compilateurs.
Voo

Réponses:


26

Parce que C est un seul passage , statiquement typé , faiblement typé , compilé langue.

  1. Le passage unique signifie que le compilateur ne regarde pas vers l'avant pour voir la définition d'une fonction ou d'une variable. Comme le compilateur ne regarde pas vers l'avant, la déclaration d'une fonction doit précéder 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, ou même dans un fichier différent. Voir point # 4.

    La seule exception est l'artefact historique selon lequel les fonctions et variables non déclarées sont présumées être de type "int". La pratique moderne consiste à éviter le typage implicite en déclarant toujours explicitement les fonctions et les variables.

  2. Le type statique 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 machine qui s'exécute au moment de l'exécution. Il n'y a aucun concept en C de typage au moment de l'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. Faible type signifie que le compilateur C génère automatiquement du code pour convertir entre les types numériques sans exiger du programmeur qu'il spécifie explicitement les opérations de conversion. En raison du typage statique, la même conversion sera toujours effectuée de la même manière à chaque fois dans le programme. Si une valeur flottante est convertie en une valeur int à un endroit donné du code, une valeur flottante sera toujours convertie en une valeur int à cet endroit du code. Cela ne peut pas être modifié au moment de l'exécution. La valeur elle-même peut changer d'une exécution du programme à la suivante, bien sûr, et les instructions conditionnelles peuvent changer les sections de code qui sont exécutées dans quel ordre, mais une seule section de code donnée sans appels de fonction ou conditions effectuera toujours exactement mêmes opérations chaque fois qu'il 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 machine est entièrement exécuté avant l'exécution du programme. Lorsque le compilateur compile une fonction, il n'a aucune connaissance de ce qu'il rencontrera plus loin dans un fichier source donné. Cependant, une fois la compilation (et l'assemblage, la liaison, etc.) terminée, chaque fonction de l'exécutable terminé contient des pointeurs numériques vers les fonctions qu'elle appellera lors de son exécution. C'est pourquoi main () peut appeler une fonction plus bas dans le fichier source. Au moment où main () est réellement exécuté, il contiendra un pointeur vers l'adresse de Func_i ().

    Le code machine est très, très spécifique. Le code pour ajouter deux entiers (3 + 2) est différent de celui pour ajouter deux flottants (3.0 + 2.0). Celles-ci sont toutes deux différentes de l'ajout d'un int à un flottant (3 + 2.0), etc. Le compilateur détermine pour chaque point d'une fonction quelle opération exacte doit être effectuée à ce point et génère un code qui exécute cette opération exacte. Une fois cela fait, il ne peut pas être modifié sans recompiler la fonction.

En rassemblant tous ces concepts, la raison pour laquelle main () 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 main () a été lue et analysée, et la définition de Func_i () n'est pas encore connue du compilateur.

La raison pour laquelle main () peut "voir" où Func_i () doit l' appeler est que l'appel se produit au moment de l'exécution, après que la compilation a déjà résolu tous les noms et types de tous les identificateurs, l'assembly a déjà converti tous les fonctions au code machine, et la liaison a déjà inséré l'adresse correcte de chaque fonction à chaque endroit où elle est appelée.

J'ai, bien sûr, omis la plupart des détails sanglants. Le processus réel est beaucoup, beaucoup plus compliqué. J'espère avoir fourni suffisamment de vues d'ensemble de haut niveau pour répondre à vos questions.

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

Dans d'autres langages, le compilateur peut effectuer plusieurs passages dans le code source, et donc le compilateur pourrait récupérer la définition de Func_i () sans qu'elle soit prédéclarée.

Dans d'autres langages, les fonctions et / ou les variables peuvent être typées dynamiquement, de sorte qu'une seule variable peut contenir, ou une seule fonction peut être passée ou renvoyée, un entier, un flottant, une chaîne, un tableau ou un objet à différents moments.

Dans d'autres langues, le typage peut être plus fort, nécessitant une conversion explicite de virgule flottante en entier. Dans d'autres langues encore, la frappe peut être plus faible, ce qui permet d'effectuer automatiquement la conversion de la chaîne "3.0" en float 3.0 en entier 3.

Et dans d'autres langues, le code peut être interprété une ligne à la fois, ou compilé en octet-code puis interprété, ou juste à temps compilé, ou soumis à une grande variété d'autres schémas d'exécution.


1
Merci pour une réponse tout-en-un. Votre réponse et celle de Nikie est ce que je voulais savoir. Par exemple Func_()+1: ici au moment de la compilation, le compilateur doit connaître le type Func_i()pour générer le code machine approprié. Peut-être que l'assembly ne peut pas gérer Func_()+1en appelant le type au moment de l'exécution, ou c'est possible, mais cela ralentira le programme au moment de l'exécution. Je pense que c'est suffisant pour moi pour l'instant.
user106313

1
Détail important des fonctions implicitement déclarées de C: elles sont supposées être de type int func(...)... c'est-à-dire qu'elles prennent une liste d'arguments variadiques. Cela signifie que si vous définissez une fonction en tant que int putc(char)mais oubliez de la déclarer, elle sera plutôt appelée en tant que int putc(int)(car le caractère passé par une liste d'arguments variadiques est promu int). Ainsi, alors que l'exemple de l'OP s'est avéré fonctionner parce que sa signature correspondait à la déclaration implicite, il est compréhensible pourquoi ce comportement a été découragé (et des avertissements appropriés ont été ajoutés).
uliwitness

37

Une contrainte de conception du langage C était qu'il était censé être compilé par un compilateur en un seul passage, ce qui le rend approprié pour les systèmes très limités en mémoire. Par conséquent, le compilateur ne connaît à tout moment que les éléments mentionnés précédemment. Le compilateur ne peut pas avancer dans la source pour trouver une déclaration de fonction, puis revenir pour compiler un appel à cette fonction. Par conséquent, tous les symboles doivent être déclarés avant d'être utilisés. Vous pouvez pré-déclarer une fonction comme

int Func_i();

en haut ou dans un fichier d'en-tête pour aider le compilateur.

Dans vos exemples, vous utilisez deux fonctionnalités douteuses du langage C à éviter:

  1. Si une fonction est utilisée avant d'être correctement déclarée, elle est utilisée comme une «déclaration implicite». Le compilateur utilise le contexte immédiat pour comprendre la signature de la fonction. Le compilateur ne parcourra pas le reste du code pour déterminer quelle est la véritable déclaration.

  2. Si quelque chose est déclaré sans type, le type est considéré comme étant int. C'est par exemple le cas pour les variables statiques ou les types de retour de fonction.

Donc, dans printf("func:%d",Func_i()), nous avons une déclaration implicite int Func_i(). Lorsque le compilateur atteint la définition de la fonction Func_i() { ... }, cela est compatible avec le type. Mais si vous avez écrit float Func_i() { ... }à ce stade, vous avez déclaré l'implicité int Func_i()et déclaré explicitement float Func_i(). Puisque les deux déclarations ne correspondent pas, le compilateur vous donne une erreur.

Éliminer certaines idées fausses

  • Le compilateur ne trouve pas la valeur renvoyée par Func_i. L'absence d'un type explicite signifie que le type de retour est intpar défaut. Même si vous faites cela:

    Func_i() {
        float f = 42.3;
        return f;
    }

    alors le type sera int Func_i(), et la valeur de retour sera tronquée silencieusement!

  • Le compilateur finit par connaître le type réel de Func_i, mais il ne connaît pas le type réel lors de la déclaration implicite. Ce n'est que lorsqu'il atteint plus tard la vraie déclaration qu'il peut savoir si le type déclaré implicitement était correct. Mais à ce stade, l'assembly de l'appel de fonction peut déjà avoir été écrit et ne peut pas être modifié dans le modèle de compilation C.


3
@ user31782: L'ordre du code est important au moment de la compilation, mais pas au moment de l'exécution. Le compilateur est hors de vue lorsque le programme s'exécute. Au moment de l'exécution, la fonction aura été assemblée et liée, son adresse aura été résolue et coincée dans l'espace réservé d'adresse de l'appel. (C'est un peu plus complexe que cela, mais c'est l'idée de base.) Le processeur peut se ramifier en avant ou en arrière.
Blrfl

20
@ user31782: Le compilateur n'imprime pas la valeur. Votre compilateur n'exécute pas le programme !!
Courses de légèreté avec Monica

1
@LightnessRacesinOrbit Je le sais. J'ai par erreur écrit le compilateur dans mon commentaire ci-dessus parce que j'ai oublié le processeur de noms .
user106313

3
@Carcigenicate C était fortement influencé par le langage B, qui n'avait qu'un seul type: un type numérique intégral de largeur de mot qui pouvait également être utilisé pour les pointeurs. C a initialement copié ce comportement, mais il est maintenant complètement interdit depuis la norme C99. Unitfait un bon type par défaut du point de vue de la théorie des types, mais échoue dans les aspects pratiques de la programmation proche des systèmes métalliques pour laquelle B puis C ont été conçus.
amon

2
@ user31782: Le compilateur doit connaître le type de la variable afin de générer l'assembly correct pour le processeur. Lorsque le compilateur trouve l'implicite Func_i(), il génère et enregistre immédiatement du code pour que le processeur passe à un autre emplacement, puis reçoive un entier, puis continue. Lorsque le compilateur trouve plus tard la Func_idéfinition, il s'assure que les signatures correspondent, et si c'est le cas, il place l'assembly pour Func_i()à cette adresse et lui dit de renvoyer un entier. Lorsque vous exécutez le programme, le processeur suit ensuite ces instructions avec la valeur 3.
Mooing Duck

10

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

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

  1. Les types de résultats sont facultatifs en C90, ne donnant pas un seul intrésultat. C'est également vrai pour la déclaration de variable (mais vous devez donner une classe de stockage, staticou extern).

  2. Ce que le compilateur fait en voyant le Func_iest appelé sans déclaration précédente, c'est qu'il y a une déclaration

    extern int Func_i();

    il ne regarde pas plus loin dans le code pour voir avec quelle efficacité il Func_iest déclaré. S'il Func_in'était pas déclaré ou défini, le compilateur ne changerait pas son comportement lors de la compilation main. La déclaration implicite est uniquement pour la fonction, il n'y en a pas pour la variable.

    Notez que la liste de paramètres vide 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 paramètres et sera le même conversions implicites qui sont appliquées aux arguments passés aux fonctions variadiques.


Si le compilateur peut trouver la valeur retournée par Func_i () en sortant du main () et en recherchant le code source, alors pourquoi ne peut-il pas trouver le type de Func_i (), qui est explicitement mentionné.
user106313

1
@ user31782 S'il n'y avait pas de déclaration précédente de Func_i, lorsque vous voyez que Func_i est utilisé dans une expression d'appel, se comporte comme s'il y en avait une extern int Func_i(). Ça ne regarde nulle part.
AProgrammer

1
@ user31782, le compilateur ne saute nulle part. Il émettra du code pour appeler cette fonction; la valeur retournée sera déterminée au moment de l'exécution. Eh bien, dans le cas d'une fonction aussi simple qui est présente dans la même unité de compilation, la phase d'optimisation peut aligner la fonction, mais ce n'est pas quelque chose auquel vous devez penser lorsque vous considérez les règles du langage, c'est une optimisation.
AProgrammer

10
@ user31782, vous avez de sérieux malentendus sur le fonctionnement des programmes. Tellement sérieux que je ne pense pas que p.se soit un bon endroit pour les corriger (peut-être le chat, mais je n'essaierai pas de le faire).
AProgrammer

1
@ user31782: Écrire un petit extrait et le compiler avec -S(si vous utilisez gcc) vous permettra de regarder le code assembleur généré par le compilateur. Ensuite, vous pouvez avoir une idée de la façon dont les valeurs de retour sont gérées au moment de l'exécution (en utilisant normalement un registre de processeur ou de l'espace sur la pile du programme).
Giorgio

7

Vous avez écrit dans un commentaire:

L'exécution se fait ligne par ligne. La seule façon de trouver la valeur retournée par Func_i () est de sauter hors du principal

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

Un modèle conceptuel utile est le suivant: Lorsque le compilateur lit la ligne:

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

il émet du 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 qui function #2est une fonction non encore déclarée nommée Func_i, qui prend un nombre non spécifié d'arguments et retourne un int (par défaut).

Plus tard, quand il analyse ceci:

 int Func_i() { ...

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

Ainsi, le compilateur n'a pas "recherché" Func_iquand il a analysé la première référence. Il a simplement fait une note dans un tableau, puis a analysé la ligne suivante. Et à la fin du fichier, il a un fichier objet et une liste d'adresses de saut.

Plus tard, l'éditeur de liens prend tout cela et remplace tous les pointeurs vers la "fonction # 2" par l'adresse de saut réelle, il émet donc 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. Aucune recherche de nom n'est requise.

Avertissement : Comme je l'ai dit, c'est un modèle conceptuel et le monde réel est plus compliqué. Les compilateurs et les éditeurs de liens font toutes sortes d'optimisations folles aujourd'hui. Ils pourraient même "sauter un haut" pour chercher Func_i, bien que j'en doute. Mais les langages C sont définis de manière à ce que vous puissiez écrire un compilateur super simple comme ça. Donc, la plupart du temps, c'est un modèle très utile.


Merci pour votre réponse. Le compilateur ne peut-il pas émettre le code:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313

1
(Suite) Aussi: Et si vous écriviez printf(..., Func_i()+1);- le compilateur doit connaître le type de Func_i, afin qu'il puisse décider s'il doit émettre une add integerou une add floatinstruction. Vous trouverez peut - être des cas particuliers où le compilateur peut aller sans l'information de type, mais le compilateur doit travailler pour tous les cas.
nikie

4
@ user31782: En règle générale, les instructions machine sont très simples: ajoutez deux registres d'entiers 32 bits. Chargez une adresse mémoire dans un registre entier de 16 bits. Aller à une adresse. De plus, il n'y a pas de types : vous pouvez facilement charger un emplacement de mémoire qui représente un nombre flottant 32 bits dans un registre entier 32 bits et faire de l'arithmétique avec. (Cela n'a que rarement du sens.) Donc non, vous ne pouvez pas émettre directement de code machine comme ça. Vous pouvez écrire un compilateur qui fait toutes ces choses avec des vérifications d'exécution et des données de type supplémentaires sur la pile. Mais ce ne serait pas un compilateur C.
nikie

1
@ user31782: Depends, IIRC. floatles valeurs peuvent vivre dans un registre FPU - alors il n'y aurait aucune instruction du tout. Le compilateur ne fait que suivre quelle valeur est stockée dans quel registre pendant la compilation, et émet des trucs comme "ajouter la constante 1 au registre FP X". Ou il pourrait vivre sur la pile, s'il n'y a pas de registres libres. Ensuite, il y aurait une instruction "augmenter le pointeur de pile de 4", et la valeur serait "référencée" comme quelque chose comme "pointeur de pile - 4". Mais toutes ces choses ne fonctionnent que si les tailles de toutes les variables (avant et après) sur la pile sont connues au moment de la compilation.
nikie

1
De toute la discussion que je suis parvenu à cette compréhension: Pour que le compilateur crée un code d'assembly plausible pour toute instruction incluant Func_i()ou / et Data_i, il doit déterminer leurs types; il n'est pas possible en langage assembleur d'appeler le type de données. J'ai besoin d'étudier les choses en détail moi-même pour être assuré.
user106313

5

C et un certain nombre d'autres langages qui nécessitent des déclarations ont été conçus à une époque où le temps processeur et la mémoire étaient chers. Le développement de C et d'Unix est allé de pair pendant un certain temps, et ce dernier n'a pas eu de mémoire virtuelle jusqu'à ce que 3BSD apparaisse en 1979. Sans l'espace supplémentaire pour travailler, les compilateurs avaient tendance à être des affaires à passage unique parce qu'ils ne le faisaient pas nécessitent la possibilité de garder une représentation de l'ensemble du fichier en mémoire à la fois.

Les compilateurs monopasse sont, comme nous, aux prises avec une incapacité à voir l'avenir. Cela signifie que les seules choses qu'ils peuvent savoir avec certitude sont ce qui leur a été dit explicitement avant la compilation de la ligne de code. Il est clair pour nous deux que cela Func_i()est déclaré plus tard dans le fichier source, mais le compilateur, qui opère sur un petit morceau de code à la fois, n'a aucune idée de son arrivée.

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

Qu'en est-il des types de retour?

La convention d'appel pour le code objet dans la plupart des environnements nécessite de connaître uniquement l'adresse de la fonction appelée, ce qui est relativement facile à gérer pour les compilateurs et les éditeurs de liens. L'exécution saute au début de la fonction et revient lorsqu'elle revient. Tout le reste, notamment les arrangements de passage d'arguments et une valeur de retour, est entièrement déterminé par l'appelant et l'appelé dans un arrangement appelé convention d'appel . 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 objets, qu'elles aient été compilées dans un langage qui partage ces conventions. (Dans le calcul scientifique, vous rencontrez beaucoup de C appelant FORTRAN et vice versa, et la capacité de le faire vient d'avoir une convention d'appel.)

Une autre caractéristique du début du C était que les prototypes tels que nous les 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 que ce int foo(int bar)n'était pas une option). Cela a existé parce que, comme indiqué ci-dessus, le programme a toujours respecté une convention d'appel qui pourrait être déterminée par les arguments. Si vous avez appelé une fonction avec le mauvais type d'arguments, il s'agissait d'une situation garbage in, garbage out.

Étant donné que le code objet a la notion de retour mais pas de type de retour, un compilateur doit connaître le type de retour pour gérer la valeur renvoyée. Lorsque vous exécutez des instructions machine, ce ne sont que des bits et le processeur ne se soucie pas de savoir si la mémoire où vous essayez de comparer undouble contient réellement un int. Il fait juste ce que vous demandez, et si vous le cassez, vous possédez les deux pièces.

Considérez ces bits de code:

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

Le code à gauche se compile en un appel à foo() suivi en copiant le résultat fourni via la convention d'appel / retour dans n'importe où xest stocké. Voilà le cas facile.

Le code à droite montre une conversion de type et c'est pourquoi les compilateurs doivent connaître le type de retour d'une fonction. Les nombres à virgule flottante ne peuvent pas être sauvegardés dans la mémoire où un autre code s'attendra à voir unint 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 à l' foo()avance le type de retour , le compilateur n'aurait aucune idée que le code de conversion est nécessaire.

Les compilateurs à passes multiples permettent toutes sortes de choses, dont la possibilité de déclarer des variables, des fonctions et des méthodes après leur première utilisation. Cela signifie que lorsque le compilateur se met à compiler le code, il a déjà vu l'avenir et sait quoi faire. Java, par exemple, rend obligatoire le multi-passage du fait que sa syntaxe permet la déclaration après utilisation.


Merci pour votre réponse (+1). Je ne connais pas les détails du fonctionnement interne du compilateur, de l'assembleur, du processeur, etc. L'idée de base de ma question est que si je dis (écris) la valeur de retour de la fonction dans le code source enfin, après l'utilisation de cette fonction, le langage permet à l' ordinateur de trouver cette valeur sans donner d'erreur. Maintenant, pourquoi l'ordinateur ne trouve-t-il pas le type de la même manière. Pourquoi le type de Data_i ne peut-il pas être trouvé car Func_i()la valeur de retour de a été trouvée?
user106313

Je ne suis toujours pas satisfait. double foo(); int x; x = foo();donne simplement l'erreur. Je sais que nous ne pouvons pas faire ça. Ma question est que dans l'appel de fonction, le processeur ne trouve que la valeur de retour; pourquoi ne trouve-t-il pas aussi le type de retour?
user106313

1
@ user31782: Cela ne devrait pas. Il y a un prototype pour foo(), donc le compilateur sait quoi en faire.
Blrfl

2
@ user31782: Les processeurs n'ont aucune notion de type de retour.
Blrfl

1
@ user31782 Pour la question du temps de compilation: Il est possible d'écrire un langage dans lequel toutes ces analyses de type peuvent être effectuées au moment de la compilation. C n'est pas un tel langage. Le compilateur C ne peut pas le faire car il n'est pas conçu pour le faire. Aurait-il pu être conçu différemment? Bien sûr, mais cela prendrait beaucoup plus de puissance de traitement et de mémoire pour le faire. En fin de compte, ce n'était pas le cas. Il a été conçu de la manière la plus adaptée aux ordinateurs de l'époque.
Mr.Mindor
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.