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:
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 demain()
et en recherchant le code source, alors pourquoi ne peut-il pas trouver le type de Func_i (), qui est explicitement mentionné.
- Si le compilateur peut trouver la valeur retournée par
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_i
est de type float, alors pourquoi suppose-t-il toujoursFunc_i()
qu'il est de type int et donne l'erreur de types en conflit? Pourquoi ne pas forcémentFunc_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.
la source
Func_i
non 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.)Réponses:
Parce que C est un seul passage , statiquement typé , faiblement typé , compilé langue.
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.
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.
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é.
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.
la source
Func_()+1
: ici au moment de la compilation, le compilateur doit connaître le typeFunc_i()
pour générer le code machine approprié. Peut-être que l'assembly ne peut pas gérerFunc_()+1
en 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.int func(...)
... c'est-à-dire qu'elles prennent une liste d'arguments variadiques. Cela signifie que si vous définissez une fonction en tant queint putc(char)
mais oubliez de la déclarer, elle sera plutôt appelée en tant queint putc(int)
(car le caractère passé par une liste d'arguments variadiques est promuint
). 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).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
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:
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.
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 impliciteint Func_i()
. Lorsque le compilateur atteint la définition de la fonctionFunc_i() { ... }
, cela est compatible avec le type. Mais si vous avez écritfloat Func_i() { ... }
à ce stade, vous avez déclaré l'implicitéint Func_i()
et déclaré explicitementfloat 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 estint
par défaut. Même si vous faites cela: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.la source
Unit
fait 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.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 laFunc_i
définition, il s'assure que les signatures correspondent, et si c'est le cas, il place l'assembly pourFunc_i()
à cette adresse et lui dit de renvoyer un entier. Lorsque vous exécutez le programme, le processeur suit ensuite ces instructions avec la valeur3
.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.
Les types de résultats sont facultatifs en C90, ne donnant pas un seul
int
résultat. C'est également vrai pour la déclaration de variable (mais vous devez donner une classe de stockage,static
ouextern
).Ce que le compilateur fait en voyant le
Func_i
est appelé sans déclaration précédente, c'est qu'il y a une déclarationil ne regarde pas plus loin dans le code pour voir avec quelle efficacité il
Func_i
est déclaré. S'ilFunc_i
n'était pas déclaré ou défini, le compilateur ne changerait pas son comportement lors de la compilationmain
. 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.la source
extern int Func_i()
. Ça ne regarde nulle part.-S
(si vous utilisezgcc
) 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).Vous avez écrit dans un commentaire:
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:
il émet du code équivalent à:
Le compilateur fait également une note dans une table interne qui
function #2
est une fonction non encore déclarée nomméeFunc_i
, qui prend un nombre non spécifié d'arguments et retourne un int (par défaut).Plus tard, quand il analyse ceci:
le compilateur recherche
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, 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_i
quand 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:
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.la source
1. call "function #2", put the return-type onto the stack and put the return value on the stack?
printf(..., Func_i()+1);
- le compilateur doit connaître le type deFunc_i
, afin qu'il puisse décider s'il doit émettre uneadd integer
ou uneadd float
instruction. 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.float
les 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.Func_i()
ou / etData_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é.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 deint foo()
. Votre exemple fonctionne fonctionne quandFunc_i()
est déclaréint
car 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 ceint 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 un
double
contient réellement unint
. 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:
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ùx
est 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 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 à 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.
la source
Func_i()
la valeur de retour de a été trouvée?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?foo()
, donc le compilateur sait quoi en faire.