Lorsqu'un programme C est en cours d'exécution, les données sont stockées sur le tas ou la pile. Les valeurs sont stockées dans RAM. Mais qu'en est-il des indicateurs de type (par exemple, int
ou char
)? Sont-ils également stockés?
Considérez le code suivant:
char a = 'A';
int x = 4;
J'ai lu que A et 4 sont stockés dans RAM ici. Mais qu'en est-il de a
et x
? Plus confus, comment l'exécution sait-elle que a
est un caractère et x
est un int? Je veux dire, les int
et char
sont-ils mentionnés quelque part dans la RAM?
Disons qu'une valeur est stockée quelque part dans RAM as 10011001; si je suis le programme qui exécute le code, comment saurai-je si ce 10011001 est un char
ou un int
?
Ce que je ne comprends pas, c'est comment l'ordinateur sait, lorsqu'il lit la valeur d'une variable à partir d'une adresse telle que 10001, qu'il s'agisse d'un int
ou char
. Imaginez que je clique sur un programme appelé anyprog.exe
. Immédiatement, le code commence à s'exécuter. Ce fichier exécutable contient-il des informations indiquant si les variables stockées sont de type int
ou char
?
Pour répondre à la question que vous avez posée dans plusieurs commentaires (que je pense que vous devriez modifier dans votre message):
Ce que je ne comprends pas, c'est comment l'ordinateur sait-il quand il lit la valeur d'une variable et l'adresse comme 10001 si est un int ou un char. Imaginez que je clique sur un programme appelé anyprog.exe. Immédiatement, le code commence à s'exécuter. Ce fichier exe contient-il des informations sur si les variables sont stockées comme dans ou car?
int x = 4;
Et supposons qu'il soit stocké dans la RAM:
0x00010004: 0x00000004
La première partie étant l'adresse, la deuxième partie étant la valeur. Lorsque votre programme (qui s'exécute en tant que code machine) s'exécute, tout ce qu'il voit à 0x00010004
est la valeur 0x000000004
. Il ne "connaît" pas le type de ces données et ne sait pas comment elles sont "censées" être utilisées.
Alors, comment votre programme trouve-t-il la bonne chose à faire? Considérez ce code:
int x = 4;
x = x + 5;
Nous avons une lecture et une écriture ici. Lorsque votre programme lit x
dans la mémoire, il trouve 0x00000004
Là. Et votre programme sait ajouter 0x00000005
à elle. Et la raison pour laquelle votre programme "sait" qu'il s'agit d'une opération valide, c'est parce que le compilateur garantit que l'opération est valide via la sécurité de type. Votre compilateur a déjà vérifié que vous pouvez ajouter 4
et 5
ensemble. Ainsi, lorsque votre code binaire s'exécute (l'exe), il n'a pas à effectuer cette vérification. Il exécute simplement chaque étape à l'aveugle, en supposant que tout est OK (de mauvaises choses se produisent quand elles sont en fait, pas OK).
0x00000004: 0x12345678
Même format que précédemment - adresse à gauche, valeur à droite. Quel type est la valeur? À ce stade, vous connaissez autant d'informations sur cette valeur que votre ordinateur lorsqu'il exécute du code. Si je vous disais d'ajouter 12743 à cette valeur, vous pourriez le faire. Vous n'avez aucune idée des répercussions de cette opération sur l'ensemble du système, mais l'ajout de deux chiffres est quelque chose que vous êtes vraiment bon, alors vous pourriez le faire. Cela fait-il de la valeur un int
? Pas nécessairement - Tout ce que vous voyez est deux valeurs 32 bits et l'opérateur d'addition.
char A = 'a';
Comment l'ordinateur sait-il afficher a
dans la console? Eh bien, il y a beaucoup d'étapes à cela. La première consiste à aller à l'emplacement de A
s en mémoire et à le lire:
0x00000004: 0x00000061
La valeur hexadécimale de a
dans ASCII est 0x61, donc ce qui précède pourrait être quelque chose que vous verriez en mémoire. Alors maintenant, notre code machine connaît la valeur entière. Comment le fait-il savoir transformer la valeur entière en un caractère pour l'afficher? Autrement dit, le compilateur s'est assuré de mettre toutes les étapes nécessaires pour effectuer cette transition. Mais votre ordinateur lui-même (ou le programme/exe) n'a aucune idée de ce que le type Cette valeur de 32 bits pourrait être n'importe quoi - int
, char
, la moitié d'un double
, un pointeur, une partie d'un tableau, une partie d'un string
, partie d'une instruction, etc.
Voici une brève interaction que votre programme (exe) peut avoir avec l'ordinateur/le système d'exploitation.
Programme: je veux démarrer. J'ai besoin de 20 Mo de mémoire.
Système d'exploitation: trouve 20 Mo de mémoire libre qui ne sont pas utilisés et les remet
(La note importante est que cela pourrait retourner n'importe quel 20 Mo de mémoire libre, ils n'ont même pas besoin d'être contigus. À ce stade, le programme peut maintenant fonctionner dans la mémoire qu'il a sans parler à l'OS)
Programme: Je vais supposer que le premier point en mémoire est une variable entière 32 bits x
.
(Le compilateur s'assure que l'accès aux autres variables ne touchera jamais cet endroit en mémoire. Il n'y a rien sur le système qui indique que le premier octet est la variable x
, ou que la variable x
est un entier. Une analogie: vous avez un sac. Vous dites aux gens que vous ne mettrez que des boules de couleur jaune dans ce sac. Quand quelqu'un sortira plus tard quelque chose du sac, alors il serait choquant de retirer quelque chose de bleu ou un cube - quelque chose a horriblement mal tourné. Il en va de même pour les ordinateurs: votre programme suppose maintenant que le premier emplacement de mémoire est la variable x et qu'il s'agit d'un entier. Si quelque chose d'autre est écrit sur cet octet de mémoire ou s'il est supposé être autre chose - quelque chose horrible s'est produit. Le compilateur garantit que ce genre de choses ne se produit pas)
Programme: je vais maintenant écrire 2
aux quatre premiers octets où je suppose que x
est à.
Programme: je veux ajouter 5 à x
.
Lit la valeur de X dans un registre temporaire
Ajoute 5 au registre temporaire
Stocke la valeur du registre temporaire dans le premier octet, qui est toujours supposé être x
.
Programme: je vais supposer que le prochain octet disponible est la variable char y
.
Programme: j'écrirai a
dans la variable y
.
Une bibliothèque est utilisée pour trouver la valeur d'octet pour a
L'octet est écrit à l'adresse que le programme suppose être y
.
Programme: je veux afficher le contenu de y
Lit la valeur dans le deuxième emplacement mémoire
Utilise une bibliothèque pour convertir de l'octet en caractère
Utilise des bibliothèques graphiques pour modifier l'écran de la console (définition des pixels du noir au blanc, défilement d'une ligne, etc.)
(Et ça continue d'ici)
Ce sur quoi vous êtes probablement accroché, c'est - que se passe-t-il lorsque le premier point en mémoire n'est plus x
? ou le second n'est plus y
? Que se passe-t-il lorsque quelqu'un lit x
comme char
ou y
comme pointeur? Bref, de mauvaises choses arrivent. Certaines de ces choses ont un comportement bien défini, et certaines ont un comportement non défini. Un comportement indéfini est exactement cela - tout peut arriver, de rien du tout, à planter le programme ou le système d'exploitation. Même un comportement bien défini peut être malveillant. Si je peux changer x
en un pointeur sur mon programme et que votre programme l'utilise comme pointeur, alors je peux amener votre programme à commencer à exécuter mon programme - ce que font exactement les pirates. Le compilateur est là pour vous assurer que nous n'utilisons pas int x
en tant que string
, et des choses de cette nature. Le code machine lui-même ne connaît pas les types et il ne fera que ce que les instructions lui disent de faire. Il existe également une grande quantité d'informations découvertes au moment de l'exécution: quels octets de mémoire le programme est-il autorisé à utiliser? x
commence-t-il au premier octet ou au 12?
Mais vous pouvez imaginer à quel point il serait horrible d'écrire des programmes comme celui-ci (et vous pouvez, dans le langage d'assemblage). Vous commencez par "déclarer" vos variables - vous vous dites que l'octet 1 est x
, l'octet 2 est y
, et pendant que vous écrivez chaque ligne de code, en chargeant et en stockant des registres, vous ( en tant qu'humain) doivent se rappeler lequel est x
et lequel est y
, car le système n'a aucune idée. Et vous (en tant qu'humain) devez vous rappeler quels sont les types x
et y
, car encore une fois - le système n'a aucune idée.
Je pense que votre question principale semble être: "Si le type est effacé au moment de la compilation et non conservé au moment de l'exécution, alors comment l'ordinateur sait-il s'il faut exécuter du code qui l'interprète comme int
ou exécuter du code qui l'interprète comme un char
? "
Et la réponse est… pas l'ordinateur. Cependant, le compilateur ne sait, et il aura simplement mis le bon code dans le binaire en premier lieu. Si la variable était tapée comme char
, alors le compilateur ne mettrait pas le code pour la traiter comme un int
dans le programme, il mettrait le code pour le traiter est un char
.
Il y a sont raisons de conserver le type lors de l'exécution:
+
operator), il n'a donc pas besoin du type d'exécution pour cette raison. Cependant, encore une fois, le type d'exécution est quelque chose de différent du type statique, par exemple en Java, vous pouvez théoriquement effacer les types statiques tout en conservant le type d'exécution pour le polymorphisme. Notez également que si vous décentralisez et spécialisez le code de recherche de type et le placez dans l'objet (ou la classe), vous n'avez pas nécessairement besoin du type d'exécution, par exemple Vtables C++.La seule raison de conserver le type lors de l'exécution en C serait pour le débogage, cependant, le débogage se fait généralement avec la source disponible, et vous pouvez alors simplement rechercher le type dans le fichier source.
L'effacement de type est tout à fait normal. Cela n'affecte pas la sécurité des types: les types sont vérifiés au moment de la compilation, une fois que le compilateur est convaincu que le programme est de type sécurisé, les types ne sont plus nécessaires (pour cette raison). Cela n'a pas d'impact sur le polymorphisme statique (aka surcharge): une fois la résolution de surcharge terminée, et le compilateur a choisi la bonne surcharge, il n'a plus besoin des types. Les types peuvent également guider l'optimisation, mais encore une fois, une fois que l'optimiseur a choisi ses optimisations en fonction des types, il n'en a plus besoin.
La conservation des types lors de l'exécution n'est requise que lorsque vous souhaitez faire quelque chose avec les types lors de l'exécution.
Haskell est l'un des langages à typage statique les plus stricts, les plus rigoureux et les plus sûrs, et les compilateurs Haskell effacent généralement tous les types. (L'exception étant le passage de dictionnaires de méthode pour les classes de type, je crois.)
L'ordinateur ne "sait" pas quelles adresses sont quoi, mais la connaissance de ce qui est intégré dans les instructions de votre programme.
Lorsque vous écrivez un programme C qui écrit et lit une variable char, le compilateur crée du code d'assembly qui écrit ce morceau de données quelque part en tant que char, et il y a un autre code ailleurs qui lit une adresse mémoire et l'interprète comme un char. La seule chose qui lie ces deux opérations ensemble est l'emplacement de cette adresse mémoire.
Quand vient le temps de lire, les instructions ne disent pas "voir quel type de données est là", il dit simplement quelque chose comme "charger cette mémoire comme un flottant". Si l'adresse à lire a été modifiée ou si quelque chose a écrasé cette mémoire avec autre chose qu'un flottant, le CPU se contentera de charger cette mémoire comme un flottant de toute façon, et toutes sortes de choses étranges peuvent se produire en conséquence.
Mauvais temps d'analogie: imaginez un entrepôt d'expédition compliqué, où l'entrepôt est la mémoire et les gens qui choisissent des choses est le CPU. Une partie du "programme" de l'entrepôt place divers articles sur l'étagère. Un autre programme va chercher des articles hors de l'entrepôt et les met dans des boîtes. Quand ils sont retirés, ils ne sont pas contrôlés, ils vont simplement dans la poubelle. Tout l'entrepôt fonctionne grâce à tout ce qui fonctionne en synchronisation, les bons articles étant toujours au bon endroit au bon moment, sinon tout se bloque, comme dans un programme réel.
Ce n'est pas le cas. Une fois que C est compilé en code machine, la machine ne voit qu'un tas de bits. L'interprétation de ces bits dépend des opérations qui sont exécutées sur eux, par opposition à certaines métadonnées supplémentaires.
Les types que vous entrez dans votre code source sont réservés au compilateur. Il prend le type que vous dites que les données sont censées être et, au mieux de ses capacités, essaie de s'assurer que ces données ne sont utilisées que de manière logique. Une fois que le compilateur a fait le meilleur travail possible en vérifiant la logique de votre code source, il le convertit en code machine et supprime les données de type, car le code machine n'a aucun moyen de représenter cela (au moins sur la plupart des machines) .
La plupart des processeurs fournissent des instructions différentes pour travailler avec des données de différents types, donc les informations de type sont généralement "intégrées" au code machine généré. Il n'est pas nécessaire de stocker des métadonnées de type supplémentaires.
Quelques exemples concrets pourraient aider. Le code machine ci-dessous a été généré à l'aide de gcc 4.1.2 sur un système x86_64 exécutant SuSE Linux Enterprise Server (SLES) 10.
Supposons le code source suivant:
int main( void )
{
int x, y, z;
x = 1;
y = 2;
z = x + y;
return 0;
}
Voici la viande du code d'assemblage généré correspondant à la source ci-dessus (en utilisant gcc -S
), Avec des commentaires ajoutés par moi:
main:
.LFB2:
pushq %rbp ;; save the current frame pointer value
.LCFI0:
movq %rsp, %rbp ;; make the current stack pointer value the new frame pointer value
.LCFI1:
movl $1, -12(%rbp) ;; x = 1
movl $2, -8(%rbp) ;; y = 2
movl -8(%rbp), %eax ;; copy the value of y to the eax register
addl -12(%rbp), %eax ;; add the value of x to the eax register
movl %eax, -4(%rbp) ;; copy the value in eax to z
movl $0, %eax ;; eax gets the return value of the function
leave ;; exit and restore the stack
ret
Il y a des trucs supplémentaires qui suivent ret
, mais ce n'est pas pertinent pour la discussion.
%eax
Est un registre de données à usage général de 32 bits. %rsp
Est un registre 64 bits réservé pour enregistrer le pointeur de pile , qui contient l'adresse de la dernière chose insérée dans la pile. %rbp
Est un registre 64 bits réservé à l'enregistrement du pointeur de trame , qui contient l'adresse du courant cadre de pile . Un cadre de pile est créé sur la pile lorsque vous entrez une fonction, et il réserve de l'espace pour les arguments de la fonction et les variables locales. Les arguments et les variables sont accessibles en utilisant les décalages du pointeur de trame. Dans ce cas, la mémoire de la variable x
est de 12 octets "en dessous" de l'adresse stockée dans %rbp
.
Dans le code ci-dessus, nous copions la valeur entière de x
(1, stockée dans -12(%rbp)
) dans le registre %eax
En utilisant l'instruction movl
, qui est utilisé pour copier des mots 32 bits d'un emplacement à un autre. Nous appelons ensuite addl
, qui ajoute la valeur entière de y
(stockée dans -8(%rbp)
) à la valeur déjà dans %eax
. Nous enregistrons ensuite le résultat dans -4(%rbp)
, qui est z
.
Maintenant, changeons cela afin que nous ayons affaire aux valeurs double
au lieu des valeurs int
:
int main( void )
{
double x, y, z;
x = 1;
y = 2;
z = x + y;
return 0;
}
L'exécution de gcc -S
Nous donne à nouveau:
main:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
movq %rax, -24(%rbp) ;; save rax to x
movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
movq %rax, -16(%rbp) ;; save rax to y
movsd -24(%rbp), %xmm0 ;; copy value of x to xmm0 register
addsd -16(%rbp), %xmm0 ;; add value of y to xmm0 register
movsd %xmm0, -8(%rbp) ;; save result to z
movl $0, %eax ;; eax gets return value of function
leave ;; exit and restore the stack
ret
Plusieurs différences. Au lieu de movl
et addl
, nous utilisons movsd
et addsd
(attribuer et ajouter des flotteurs double précision). Au lieu de stocker des valeurs intermédiaires dans %eax
, Nous utilisons %xmm0
.
C'est ce que je veux dire quand je dis que le type est "intégré" au code machine. Le compilateur génère simplement le bon code machine pour gérer ce type particulier.
Historiquement, C considérait la mémoire comme composée d'un certain nombre de groupes d'emplacements numérotés de type unsigned char
(également appelé "octet", bien qu'il n'ait pas toujours besoin d'être de 8 bits). Tout code qui utilise tout ce qui est stocké en mémoire devra savoir dans quel emplacement ou quels emplacements les informations ont été stockées et savoir ce qui doit être fait avec les informations qui s'y trouvent [par ex. "interpréter les quatre octets commençant à l'adresse 123: 456 comme une valeur à virgule flottante 32 bits" ou "stocker les 16 bits inférieurs de la dernière quantité calculée dans deux octets commençant à l'adresse 345: 678]. La mémoire elle-même ne ne savent pas ce que signifient les valeurs stockées dans les emplacements mémoire. Si le code essayait d'écrire de la mémoire en utilisant un type et de le lire comme un autre, les modèles de bits stockés par l'écriture seraient interprétés selon les règles du deuxième type, avec quelles qu'en soient les conséquences.
Par exemple, si le code devait stocker 0x12345678
vers un 32 bits unsigned int
, puis essayez de lire deux _ consécutifs 16 bits unsigned int
valeurs de son adresse et celle ci-dessus, puis en fonction de la moitié de unsigned int
était stocké où, le code pouvait lire les valeurs 0x1234 et 0x5678, ou 0x5678 et 0x1234.
La norme C99, cependant, n'exige plus que la mémoire se comporte comme un ensemble d'emplacements numérotés qui ne savent rien de ce que leurs modèles de bits représentent. Un compilateur est autorisé à se comporter comme si les emplacements de mémoire connaissaient les types de données qui y sont stockés et n'autoriserait que les données écrites en utilisant un type autre que unsigned char
à lire en utilisant l'un ou l'autre type unsigned char
ou le même type que celui avec lequel il a été écrit; les compilateurs sont en outre autorisés à se comporter comme si les emplacements de mémoire avaient le pouvoir et l'inclination de corrompre arbitrairement le comportement de tout programme essayant d'accéder à la mémoire d'une manière contraire à ces règles.
Donné:
unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);
certaines implémentations peuvent imprimer 0x1234, et d'autres peuvent imprimer 0x5678, mais en vertu de la norme C99, il serait légal pour une implémentation d'imprimer "FRINK RULES!" ou faire quoi que ce soit d'autre, sur la théorie qu'il serait légal que les emplacements de mémoire contenant a
incluent du matériel qui enregistre le type utilisé pour les écrire, et que ce matériel réponde à une tentative de lecture non valide dans n'importe quel mode que ce soit, y compris en provoquant "FRINK RULES!" à sortir.
Notez que peu importe si un tel matériel existe réellement - le fait qu'un tel matériel puisse exister légalement rend légal pour les compilateurs de générer du code qui se comporte comme s'il fonctionnait sur un tel système. Si le compilateur peut déterminer qu'un emplacement de mémoire particulier sera écrit comme un type et lu comme un autre, il peut prétendre qu'il s'exécute sur un système dont le matériel pourrait faire une telle détermination, et pourrait répondre avec le degré de capriciosité que l'auteur du compilateur juge approprié .
Le but de cette règle était de permettre aux compilateurs qui savaient qu'un groupe d'octets détenant une valeur d'un certain type contenait une valeur particulière à un moment donné et qu'aucune valeur de ce même type n'avait été écrite depuis, pour déduire que ce groupe d'octets contiendrait toujours cette valeur. Par exemple, un processeur avait lu un groupe d'octets dans un registre, puis voulait plus tard utiliser à nouveau les mêmes informations alors qu'il était encore dans le registre, le compilateur pouvait utiliser le contenu du registre sans avoir à relire la valeur de la mémoire. Une optimisation utile. Pendant environ les dix premières années de la règle, la violer signifierait généralement que si une variable est écrite avec un type autre que celui qui est utilisé pour la lire, l'écriture peut ou non affecter la valeur lue. Un tel comportement peut dans certains cas être désastreux, mais dans d'autres cas peut être inoffensif, surtout si le code qui lit la valeur serait également satisfait de la valeur écrite ou de la valeur détenue avant l'écriture, ou plus encore si la valeur écrite est arrivé à correspondre à la valeur déjà détenue.
Vers 2009, cependant, les auteurs de certains compilateurs comme CLANG ont déterminé que, puisque la norme autorise les compilateurs à faire tout ce qu'ils veulent dans les cas où la mémoire est écrite en utilisant un type et lu comme un autre, les compilateurs devraient déduire que les programmes ne recevront jamais d'entrée qui pourrait provoquer une telle chose. Étant donné que le Standard dit que le compilateur est autorisé à faire tout ce qu'il veut quand une telle entrée non valide est reçue, le code qui n'aurait d'effet que dans les cas où le Standard n'impose aucune exigence peut (et de l'avis de certains auteurs du compilateur, devrait) être omis comme hors de propos. Cela modifie le comportement des violations d'alias de ressembler à de la mémoire qui, étant donné une demande de lecture, peut renvoyer arbitrairement la dernière valeur écrite en utilisant le même type qu'une demande de lecture ou toute valeur plus récente écrite en utilisant un autre type, à être comme une mémoire qui modifiera capricieusement le comportement du programme chaque fois que la norme le lui permettra.
En C, ce n'est pas le cas. D'autres langages (par exemple, LISP, Python) ont des types dynamiques mais C est typé statiquement. Cela signifie que votre programme doit savoir quel type de données sont à interpréter correctement en tant que caractère, entier, etc.
Habituellement, le compilateur s'occupe de cela pour vous, et si vous faites quelque chose de mal, vous obtiendrez une erreur de compilation (ou un avertissement).
Vous devez faire la distinction entre compiletime
et runtime
d'une part et code
et data
d'autre part.
Du point de vue de la machine, il n'y a aucune différence entre ce que vous appelez code
ou instructions
et ce que vous appelez data
. Tout se résume aux chiffres. Mais certaines séquences - ce que nous appellerions code
- font quelque chose que nous trouvons utile, d'autres simplement crash
la machine.
Le travail effectué par le CPU est une simple boucle en 4 étapes:
instruction
)C'est ce qu'on appelle le cycle d'instruction .
J'ai lu que A et 4 sont stockés dans RAM ici. Mais qu'en est-il de a et x?
a
et x
sont des variables, qui sont des espaces réservés pour les adresses, où le programme pourrait trouver le "contenu" des variables. Ainsi, chaque fois que la variable a
est utilisée, il y a effectivement l'adresse du contenu de a
utilisé.
Plus déroutant, comment l'exécution sait-elle que a est un char et x est un int?
L'exécution ne sait rien. D'après ce qui a été dit dans l'introduction, le CPU ne récupère que les données et interprète ces données comme des instructions.
La fonction printf - est conçue pour "savoir" quel type d'entrée vous y mettez, c'est-à-dire que son code résultant donne les bonnes instructions sur la façon de traiter un segment de mémoire spécial. Bien sûr, il est possible de générer une sortie non-sens: en utilisant une adresse, où aucune chaîne n'est stockée avec "% s" dans printf()
, la sortie non-sens sera arrêtée uniquement par un emplacement de mémoire aléatoire, où un 0 (\0
) est.
Il en va de même pour le point d'entrée d'un programme. Sous le C64, il était possible de mettre vos programmes dans (presque) toutes les adresses connues. Les programmes d'assemblage ont été lancés avec une instruction appelée sys
suivie d'une adresse: sys 49152
était un endroit commun pour mettre votre code assembleur. Mais rien ne vous a empêché de charger par exemple données graphiques dans 49152
, entraînant un plantage de la machine après le "démarrage" à partir de ce point. Dans ce cas, le cycle d'instruction a commencé par lire des "données graphiques" et essayer de les interpréter comme du "code" (ce qui, bien sûr, n'avait aucun sens); les effets étaient parfois étonnants;)
Disons qu'une valeur est stockée quelque part dans RAM as 10011001; si je suis le programme qui exécute le code, comment saurai-je si ce 10011001 est un char ou un int?
Comme dit: Le "contexte" - c'est-à-dire les instructions précédentes et suivantes - aide à traiter les données comme nous le voulons. Du point de vue de la machine, il n'y a aucune différence dans aucun emplacement de mémoire. int
et char
n'est que du vocabulaire, ce qui a du sens dans compiletime
; pendant runtime
(au niveau d'un assembly), il n'y a ni char
ni int
.
Ce que je ne comprends pas, c'est comment l'ordinateur sait, lorsqu'il lit la valeur d'une variable à partir d'une adresse comme 10001, qu'il s'agisse d'un int ou d'un char.
L'ordinateur ne sait rien . Le programmeur le fait. Le code compilé génère le contexte , qui est nécessaire pour générer des résultats significatifs pour les humains.
Ce fichier exécutable contient-il des informations indiquant si les variables stockées sont de type int ou char
Oui et Non . Les informations, qu'il s'agisse d'un int
ou d'un char
, sont perdues. Mais d'un autre côté, le contexte (les instructions qui indiquent comment gérer les emplacements de mémoire, où les données sont stockées) est conservé; donc implicitement oui, les "informations" sont implicitement disponibles.
Gardons cette discussion dans la langue C uniquement.
Le programme auquel vous faites référence est écrit dans un langage de haut niveau comme C. L'ordinateur ne comprend que le langage machine. Les langages de niveau supérieur donnent au programmeur la possibilité d'exprimer la logique d'une manière plus conviviale qui est ensuite traduite en code machine que le microprocesseur peut décoder et exécuter. Laissez-nous maintenant discuter du code que vous avez mentionné:
char a = 'A';
int x = 4;
Essayons d'analyser chaque partie:
char/int sont appelés types de données. Ceux-ci indiquent au compilateur d'allouer de la mémoire. Dans le cas de
char
ce sera 1 octet etint
2 octets. (Veuillez noter que cette taille de mémoire dépend à nouveau du microprocesseur).a/x sont appelés identifiants. Maintenant, vous pouvez dire des noms "conviviaux" attribués aux emplacements de mémoire dans la RAM.
= indique au compilateur de stocker "A" à l'emplacement mémoire de
a
et 4 à l'emplacement mémoirex
.
Les identificateurs de type de données int/char ne sont donc utilisés que par le compilateur et non par le microprocesseur pendant l'exécution du programme. Par conséquent, ils ne sont pas stockés en mémoire.
Ma réponse ici est quelque peu simplifiée et ne fera référence qu'à C.
int
ou char
ne sont pas des indicateurs de type pour le CPU; uniquement au compilateur.
L'exe créé par le compilateur aura des instructions pour manipuler int
s si la variable a été déclarée comme int
. De même, si la variable a été déclarée comme char
, l'exe contiendra des instructions pour manipuler un char
.
En C:
int main()
{
int a = 65;
char b = 'A';
if(a == b)
{
printf("Well, what do you know. A char can equal an int.\n");
}
return 0;
}
Ce programme affichera son message, puisque char
et int
ont les valeurs identiques en RAM.
Maintenant, si vous vous demandez comment printf
parvient à afficher 65
pour un int
et A
pour un char
, c'est parce que vous devez spécifier dans la "chaîne de format" comment printf
doit traiter le valeur .
(Par exemple, %c
signifie de traiter la valeur comme char
et %d
signifie traiter la valeur comme un entier; la même valeur de toute façon.)
Réponse courte, le type est codé dans les instructions CPU générées par le compilateur.
Bien que les informations sur le type ou la taille des informations ne soient pas directement stockées, le compilateur garde une trace de ces informations lors de l'accès, de la modification et du stockage des valeurs dans ces variables.
comment l'exécution sait-elle que a est un char et x est un int?
Ce n'est pas le cas, mais lorsque le compilateur produit le code machine, il le sait. Un int
et un char
peuvent être de tailles différentes. Dans une architecture où où un char est de la taille d'un octet et un int est de 4 octets, la variable x
n'est pas dans l'adresse 10001, mais aussi dans 10002, 10003 et 10004. Lorsque le code doit charger la valeur de x
dans un registre CPU, il utilise l'instruction de chargement de 4 octets. Lors du chargement d'un caractère, il utilise l'instruction pour charger 1 octet.
Comment choisir laquelle des deux instructions? Le compilateur décide pendant la compilation, ce n'est pas fait au moment de l'exécution après avoir inspecté les valeurs en mémoire.
Notez également que les registres peuvent être de tailles différentes. Sur les processeurs Intel x86, l'EAX a une largeur de 32 bits, dont la moitié est AX, qui est 16, et AX est divisé en AH et AL, les deux 8 bits.
Donc, si vous souhaitez charger un entier (sur les processeurs x86), vous utilisez l'instruction MOV pour les entiers, pour charger un caractère, vous utilisez l'instruction MOV pour les caractères. Ils sont tous deux appelés MOV, mais ils ont des codes op différents. Être effectivement deux instructions différentes. Le type de la variable est codé dans l'instruction à utiliser.
La même chose se produit avec d'autres opérations. Il existe de nombreuses instructions pour effectuer l'ajout, selon la taille des opérandes, et même s'ils sont signés ou non signés. Voir https://en.wikipedia.org/wiki/ADD_ (x86_instruction) qui répertorie les différents ajouts possibles.
Disons qu'une valeur est stockée quelque part dans RAM as 10011001; si je suis le programme qui exécute le code, comment saurai-je si ce 10011001 est un char ou un int
Tout d'abord, un caractère serait 10011001, mais un entier serait 00000000 00000000 00000000 10011001, car ce sont des tailles différentes (sur un ordinateur avec les mêmes tailles que celles mentionnées ci-dessus). Mais considérons le cas pour signed char
contre unsigned char
.
Ce qui est stocké dans un emplacement mémoire peut être interprété comme vous le souhaitez. Une partie des responsabilités du compilateur C est de s'assurer que ce qui est stocké et lu à partir d'une variable est fait de manière cohérente. Ce n'est donc pas que le programme sache ce qui est stocké dans un emplacement mémoire, mais qu'il accepte au préalable qu'il y lira et écrira toujours le même genre de choses. (sans compter les choses comme les types de casting).
Au niveau le plus bas, dans le CPU physique réel, il n'y a aucun type (en ignorant les unités à virgule flottante). Juste des motifs de bits. Un ordinateur fonctionne en manipulant des motifs de bits, très, très rapidement.
C'est tout ce que le CPU fait, tout ce qu'il peut faire. Il n'y a rien de tel qu'un int ou un char.
x = 4 + 5
S'exécutera en tant que:
L'instruction iadd déclenche un matériel qui se comporte comme si les registres 1 et 2 sont des entiers. S'ils ne représentent pas réellement des nombres entiers, toutes sortes de choses peuvent mal tourner plus tard. Le meilleur résultat est généralement le plantage.
C'est au compilateur de choisir l'instruction correcte en fonction des types donnés dans la source, mais dans le code machine réel exécuté par le CPU, il n'y a aucun type, nulle part.
edit: Notez que le code machine réel ne mentionne en fait 4, ni 5, ni entier n'importe où. c'est juste deux modèles de bits, et une instruction qui prend deux modèles de bits, suppose qu'ils sont des entiers et les ajoute ensemble.
mais pourquoi certains disent ici en cas C #, ce n'est pas l'histoire? j'ai lu d'autres commentaires et ils disent qu'en C # et C++ l'histoire (informations sur les types de données) est différente et même le CPU ne fait pas l'informatique. Des idées à ce sujet?
Dans les langages à vérification de type comme C #, la vérification de type est effectuée par le compilateur. Le code benji a écrit:
int main()
{
int a = 65;
char b = 'A';
if(a == b)
{
printf("Well, what do you know. A char can equal an int.\n");
}
return 0;
}
Refuserait simplement de compiler. De même, si vous essayez de multiplier une chaîne et un entier (j'allais dire ajouter, mais l'opérateur '+' est surchargé de concaténation de chaînes et cela pourrait bien fonctionner).
int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;
Le compilateur refuserait simplement de générer du code machine à partir de ce C #, peu importe combien votre chaîne l'a embrassé.