Si je définis une variable d'un certain type (qui, pour autant que je sache, n'affecte que des données pour le contenu de la variable), comment peut-elle garder trace de quel type de variable il s'agit?
Si je définis une variable d'un certain type (qui, pour autant que je sache, n'affecte que des données pour le contenu de la variable), comment peut-elle garder trace de quel type de variable il s'agit?
Réponses:
Les variables (ou plus généralement: les «objets» au sens de C) ne stockent pas leur type lors de l'exécution. En ce qui concerne le code machine, il n’existe que de la mémoire non typée. Au lieu de cela, les opérations sur ces données interprètent les données comme un type spécifique (par exemple, un flottant ou un pointeur). Les types ne sont utilisés que par le compilateur.
Par exemple, nous pourrions avoir une structure ou une classe struct Foo { int x; float y; };
et une variable Foo f {}
. Comment auto result = f.y;
compiler un accès au champ ? Le compilateur sait qu’il f
s’agit d’un objet de type Foo
et connaît la disposition de Foo
-objects. Selon les détails spécifiques à la plate-forme, cela peut être compilé comme suit: «Placez le pointeur au début de f
, ajoutez 4 octets, puis chargez 4 octets et interprétez ces données comme des valeurs flottantes». Dans de nombreux jeux d'instructions de code machine (y compris x86-64 ) il existe différentes instructions du processeur pour le chargement des flotteurs ou des ints.
Un exemple où le système de types C ++ ne peut pas suivre le type pour nous est une union comme union Bar { int as_int; float as_float; }
. Une union contient jusqu'à un objet de différents types. Si nous stockons un objet dans une union, il s'agit du type actif de l'union. Nous devons seulement essayer de faire sortir ce type du syndicat, toute autre chose serait un comportement indéfini. Soit nous "savons" lors de la programmation du type actif, soit nous pouvons créer une union étiquetée dans laquelle nous stockons une balise type (généralement une énumération) séparément. C'est une technique courante en C, mais étant donné que nous devons maintenir l'union et la balise de type synchronisées, cela est assez sujet aux erreurs. Un void*
pointeur est similaire à une union mais ne peut contenir que des objets pointeur, à l'exception des pointeurs de fonction.
Le C ++ offre deux meilleurs mécanismes pour traiter des objets de types inconnus: nous pouvons utiliser des techniques orientées objet pour effectuer un effacement de type (n'interagissez avec l'objet que par le biais de méthodes virtuelles, de sorte que nous n'avons pas besoin de connaître le type réel), ou utiliser std::variant
, une sorte d'union type-safe.
Il y a un cas où C ++ stocke le type d'un objet: si la classe de l'objet a des méthodes virtuelles (un «type polymorphe», alias interface). La cible d'un appel de méthode virtuelle est inconnue au moment de la compilation et est résolue au moment de l'exécution en fonction du type dynamique de l'objet ("dispatch dynamique"). La plupart des compilateurs implémentent cela en stockant une table de fonction virtuelle («vtable») au début de l'objet. La vtable peut également être utilisée pour obtenir le type de l'objet au moment de l'exécution. Nous pouvons ensuite faire une distinction entre le type statique connu d'une expression au moment de la compilation et le type dynamique d'un objet au moment de l'exécution.
C ++ nous permet d'inspecter le type dynamique d'un objet avec l' typeid()
opérateur qui nous donne un std::type_info
objet. Soit le compilateur connaît le type de l'objet au moment de la compilation, soit il a stocké les informations de type nécessaires à l'intérieur de l'objet et peut les récupérer au moment de l'exécution.
void*
).
typeid(e)
introspecte le type statique de l'expression e
. Si le type statique est un type polymorphe, l'expression sera évaluée et le type dynamique de cet objet est récupéré. Vous ne pouvez pas pointer typeid vers une mémoire de type inconnu et obtenir des informations utiles. Par exemple, le typeid d'un syndicat décrit le syndicat, pas l'objet dans le syndicat. Le typeid d'un void*
est juste un pointeur vide. Et il n'est pas possible de déréférencer un void*
pour obtenir son contenu. En C ++, il n'y a pas de boxe à moins d'être explicitement programmé de cette façon.
L’autre réponse explique bien l’aspect technique, mais j’aimerais ajouter quelques notions générales sur la façon de penser au code machine.
Le code machine après la compilation est assez stupide, et suppose simplement que tout fonctionne comme prévu. Disons que vous avez une fonction simple comme
bool isEven(int i) { return i % 2 == 0; }
Il prend un int et crache un bool.
Une fois que vous l'avez compilé, vous pouvez penser à cela comme à un presse-agrumes automatique:
Il prend des oranges et rend le jus. Reconnaît-il le type d'objets dans lequel il entre? Non, ils sont juste censés être des oranges. Que se passe-t-il si on obtient une pomme au lieu d'une orange? Peut-être que ça va casser. Peu importe, un propriétaire responsable n'essaiera pas de l'utiliser de cette façon.
La fonction ci-dessus est similaire: elle est conçue pour absorber les ints, et elle peut casser ou faire quelque chose de non pertinent si vous mangez quelque chose d'autre. Cela (généralement) n'a pas d'importance, car le compilateur (généralement) vérifie que cela ne se produit jamais - et cela ne se produit jamais dans un code bien formé. Si le compilateur détecte la possibilité qu'une fonction obtienne une valeur typée incorrecte, il refuse de compiler le code et renvoie des erreurs de type.
La mise en garde est qu'il y a quelques cas de code mal formé que le compilateur passera. Les exemples sont:
void*
pas orange*
quand il y a une pomme à l'autre bout du pointeur,Comme indiqué précédemment, le code compilé est semblable à la machine à centrifuger: il ne sait pas ce qu’il traite, il exécute simplement des instructions. Et si les instructions sont fausses, ça casse. C'est pourquoi les problèmes ci-dessus en C ++ entraînent des plantages incontrôlés.
void*
cohortes de C foo*
, les promotions arithmétiques habituelles, le union
type punning, NULL
vs nullptr
, même le fait d' avoir un mauvais pointeur est UB, etc. c'est comme ça.
void*
ne convertit pas implicitement en foo*
, et le union
type punning n'est pas pris en charge (a UB).
Une variable a un certain nombre de propriétés fondamentales dans un langage tel que C:
Dans votre code source , l'emplacement (5) est conceptuel et cet emplacement est désigné par son nom (1). Ainsi, une déclaration de variable est utilisée pour créer l'emplacement et l'espace pour la valeur (6), et dans les autres lignes de source, nous nous référons à cet emplacement et à la valeur qu'il détient en nommant la variable dans une expression.
En simplifiant un peu, une fois que votre programme est traduit en code machine par le compilateur, l'emplacement (5) correspond à un emplacement de mémoire ou de registre du processeur, et toutes les expressions de code source faisant référence à la variable sont traduites en séquences de code machine faisant référence à cette mémoire. ou l'emplacement du registre de la CPU.
Ainsi, lorsque la traduction est terminée et que le programme est exécuté sur le processeur, les noms des variables sont effectivement oubliés dans le code machine et les instructions générées par le compilateur se réfèrent uniquement aux emplacements attribués aux variables (plutôt qu'à leur noms). Si vous déboguez et demandez à déboguer, l'emplacement de la variable associée au nom est ajouté aux métadonnées du programme, même si le processeur voit toujours les instructions de code machine utilisant des emplacements (pas ces métadonnées). (Ceci est une simplification excessive, car certains noms figurent dans les métadonnées du programme aux fins de liaison, de chargement et de recherche dynamique. Néanmoins, le processeur exécute simplement les instructions de code machine auxquelles il est indiqué pour le programme. été convertis en emplacements.)
Il en va de même pour le type, la portée et la durée de vie. Les instructions de code machine générées par le compilateur connaissent la version machine de l'emplacement, qui stocke la valeur. Les autres propriétés, telles que type, sont compilées dans le code source traduit en tant qu'instructions spécifiques permettant d'accéder à l'emplacement de la variable. Par exemple, si la variable en question est un octet de 8 bits signé par opposition à un octet de 8 bits non signé, les expressions du code source faisant référence à la variable seront traduites, par exemple, en charges d'octets signés par rapport aux charges d'octets non signés. selon les besoins pour satisfaire les règles du langage (C). Le type de la variable est ainsi codé dans la traduction du code source en instructions machine, qui ordonnent à la CPU d’interpréter la mémoire ou l’emplacement du registre de la CPU chaque fois qu’elle utilise l’emplacement de la variable.
L'essentiel est que nous devons dire au processeur quoi faire via des instructions (et d'autres instructions) dans le jeu d'instructions de code machine du processeur. Le processeur se souvient très peu de ce qu'il vient de dire ou de ce qu'il vient de dire: il exécute uniquement les instructions données. Il appartient au compilateur ou au programmeur du langage d'assemblage de lui donner un ensemble complet de séquences d'instructions pour manipuler correctement les variables.
Un processeur prend en charge directement certains types de données fondamentaux, tels que byte / word / int / long signed / unsigned, float, double, etc. Le processeur généralement ne se plaindra pas exemple, même s’il s’agit généralement d’une erreur de logique dans le programme. La programmation consiste à informer le processeur de chaque interaction avec une variable.
Au-delà de ces types primitifs fondamentaux, nous devons coder des éléments dans des structures de données et utiliser des algorithmes pour les manipuler en fonction de ces primitives.
En C ++, les objets impliqués dans la hiérarchie de classes pour le polymorphisme ont un pointeur, généralement au début de l'objet, qui fait référence à une structure de données spécifique à la classe, facilitant l'envoi virtuel, la conversion, etc.
En résumé, le processeur ne sait pas ou ne se souvient pas de l'utilisation prévue des emplacements de stockage - il exécute les instructions de code machine du programme lui indiquant comment manipuler le stockage dans les registres de la CPU et dans la mémoire principale. La programmation incombe donc au logiciel (et aux programmeurs) d’utiliser la mémoire de manière judicieuse et de présenter un ensemble cohérent d’instructions de code machine au processeur qui exécute fidèlement le programme dans son ensemble.
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, clang et gcc sont sujettes à supposer que le pointeur unionArray[j].member2
ne peut pas accéder unionArray[i].member1
même si les deux sont issus de la même unionArray[]
.
Si je définis une variable d'un certain type, comment conserve-t-il le type de variable qu'il est?
Il y a deux phases pertinentes ici:
Le compilateur C compile le code C en langage machine. Le compilateur dispose de toutes les informations qu’il peut obtenir de votre fichier source (et des bibliothèques, ainsi que de tout ce dont il a besoin pour faire son travail). Le compilateur C garde une trace de ce qui signifie quoi. Le compilateur C sait que si vous déclarez une variable char
, c’est char.
Pour ce faire, il utilise une "table de symboles" qui répertorie les noms des variables, leur type et d'autres informations. Il s’agit d’une structure de données plutôt complexe, mais vous pouvez l’imaginer comme un simple suivi de la signification des noms lisibles par l’homme. Dans la sortie binaire du compilateur, aucun nom de variable comme celui-ci n'apparaît plus (si nous ignorons les informations de débogage optionnelles pouvant être demandées par le programmeur).
La sortie du compilateur - l'exécutable compilé - est un langage machine, chargé dans la RAM par votre système d'exploitation et exécuté directement par votre processeur. En langage machine, il n'y a pas du tout de notion de "type" - il n'y a que des commandes qui fonctionnent sur un emplacement de la RAM. Les commandes ont en effet un type fixe avec lequel elles fonctionnent (par exemple, il peut y avoir une commande en langage machine "ajoute ces deux entiers 16 bits stockés dans les emplacements de mémoire vive 0x100 et 0x521"), mais il n'y a pas d'information nulle part dans le système les octets à ces emplacements représentent en fait des entiers. Il n'y a aucune protection contre les erreurs de type du tout ici.
char *ptr = 0x123
en C). Je crois que mon utilisation du mot "pointeur" devrait être assez claire dans ce contexte. Sinon, n'hésitez pas à me prévenir et j'ajouterai une phrase à la réponse.
Il y a quelques cas particuliers importants où C ++ stocke un type au moment de l'exécution.
La solution classique est une union discriminée: une structure de données contenant l'un des types d'objet, plus un champ indiquant le type actuel. Une version basée sur un modèle se trouve dans la bibliothèque standard C ++ en tant que std::variant
. Normalement, la balise serait un enum
, mais si vous n'avez pas besoin de tous les éléments de stockage pour vos données, il peut s'agir d'un champ de bits.
L’autre cas courant est le typage dynamique. Lorsque vous avez class
une virtual
fonction, le programme stockera un pointeur sur cette fonction dans une table de fonctions virtuelle , qu'il initialisera pour chaque instance de celle class
-ci lors de sa construction. Normalement, cela signifie une table de fonctions virtuelle pour toutes les instances de classe et chaque instance contenant un pointeur sur la table appropriée. (Cela économise du temps et de la mémoire car la table sera beaucoup plus grande qu'un simple pointeur.) Lorsque vous appelez cette virtual
fonction via un pointeur ou une référence, le programme recherche le pointeur de fonction dans la table virtuelle. (S'il connaît le type exact au moment de la compilation, il peut ignorer cette étape.) Cela permet au code d'appeler l'implémentation d'un type dérivé au lieu de celle de la classe de base.
La chose qui rend cela pertinent ici est la suivante: chacun ofstream
contient un pointeur sur la ofstream
table virtuelle, chacun ifstream
sur la ifstream
table virtuelle, etc. Pour les hiérarchies de classes, le pointeur de la table virtuelle peut servir de balise indiquant au programme le type d'un objet de classe!
Bien que la norme de langage ne dise pas aux concepteurs de compilateurs comment ils doivent implémenter le runtime sous le capot, voici comment vous pouvez vous attendre dynamic_cast
et typeof
travailler.