Je sais que les pointeurs contiennent des adresses. Je sais que les types de pointeurs sont "généralement" connus en fonction du "type" de données vers lesquelles ils pointent. Mais, les pointeurs sont toujours des variables et les adresses qu'ils détiennent doivent avoir un "type" de données. Selon mes informations, les adresses sont au format hexadécimal. Mais, je ne sais toujours pas quel "type" de données est cet hexadécimal. (Notez que je sais ce qu'est un hexadécimal, mais quand vous dites 10CBA20
, par exemple, est cette chaîne de caractères? des entiers? quelle? Quand je veux accéder à l'adresse et la manipuler .. elle-même, j'ai besoin de connaître son type. C'est pourquoi je demande.)
Le type d'une variable de pointeur est .. pointeur.
Les opérations que vous êtes formellement autorisé à faire en C sont de le comparer (à d'autres pointeurs ou à la valeur spéciale NULL/zéro), d'ajouter ou de soustraire des entiers ou de le convertir en d'autres pointeurs.
ne fois que vous acceptez un comportement indéfini, vous pouvez voir quelle est réellement la valeur. Ce sera généralement un mot machine, le même genre de chose qu'un entier, et peut généralement être casté sans perte vers et depuis un type entier. (Beaucoup de code Windows le fait en masquant les pointeurs dans les typedefs DWORD ou HANDLE).
Il existe des architectures où les pointeurs ne sont pas simples car la mémoire n'est pas plate. DOS/8086 "près" et "loin"; Différents espaces mémoire et code de PIC.
Vous compliquez trop les choses.
Les adresses ne sont que des entiers, point final. Idéalement, il s'agit du numéro de la cellule de mémoire référencée (en pratique, cela devient plus compliqué en raison des segments, de la mémoire virtuelle, etc.).
La syntaxe hexadécimale est une fiction complète qui n'existe que pour la commodité des programmeurs. 0x1A et 26 sont exactement le même nombre d'exactement le même type, et ce n'est pas non plus ce que l'ordinateur utilise - en interne, l'ordinateur utilise toujours 00011010 (une série de signaux binaires).
Le fait qu'un compilateur vous permette de traiter les pointeurs comme des nombres dépend de la définition du langage - les langages de "programmation système" sont traditionnellement plus transparents sur la façon dont les choses fonctionnent sous le capot, tandis que les langages "de haut niveau" essaient plus souvent de cacher le métal nu au programmeur - mais cela ne change rien au fait que les pointeurs sont des nombres, et généralement le type de nombre le plus courant (celui avec autant de bits que l'architecture de votre processeur).
Un pointeur est juste cela - un pointeur. Ce n'est pas autre chose. N'essayez pas de penser que c'est autre chose.
Dans les langages comme C, C++ et Objective-C, les pointeurs de données ont quatre types de valeurs possibles:
Il existe également des pointeurs de fonction, qui identifient une fonction, ou sont des pointeurs de fonction nuls, ou ont une valeur indéterminée.
D'autres pointeurs sont "pointeur vers membre" en C++. Ce sont très certainement des adresses mémoire pas! Au lieu de cela, ils identifient un membre de l'instance any d'une classe. Dans Objective-C, vous avez des sélecteurs, qui sont quelque chose comme "pointeur vers une méthode d'instance avec un nom de méthode et des noms d'argument donnés". Comme un pointeur de membre, il identifie les méthodes all de toutes les classes tant qu'elles se ressemblent.
Vous pouvez étudier comment un compilateur spécifique implémente des pointeurs, mais c'est une question entièrement différente.
Un pointeur est un modèle de bits adressant (identifiant de manière unique à des fins de lecture ou d'écriture) un mot de stockage en RAM. Pour des raisons historiques et conventionnelles, l'unité de mise à jour est de huit bits, connue en anglais comme un "octet" ou en français, plutôt plus logiquement, comme un octet. C'est omniprésent mais pas inhérent; d'autres tailles ont existé.
Si je me souviens bien, il y avait un ordinateur qui utilisait un mot 29 bits; non seulement ce n'est pas une puissance de deux, mais c'est même le premier. Je pensais que c'était SILLIAC mais l'article pertinent de Wikipedia ne le supporte pas. CAN BUS utilise des adresses 29 bits mais par convention, les adresses réseau ne sont pas appelées pointeurs même lorsqu'elles sont fonctionnellement identiques.
Les gens continuent d'affirmer que les pointeurs sont des entiers. Ce n'est ni intrinsèque ni essentiel, mais si nous interprétons les modèles de bits sous forme d'entiers, la qualité utile de l'ordinalité émerge, permettant une mise en œuvre très directe (et donc efficace sur un petit matériel) de constructions comme "chaîne" et " tableau ". La notion de mémoire contiguë dépend de la contiguïté ordinale et un positionnement relatif est possible; la comparaison d'entiers et les opérations arithmétiques peuvent être appliquées de manière significative. Pour cette raison, il existe presque toujours une forte corrélation entre la taille de Word pour l'adressage de stockage et l'ALU (la chose qui fait des mathématiques entières).
Parfois, les deux ne correspondent pas. Dans les premiers PC, le bus d'adresse avait une largeur de 24 bits.
Fondamentalement, chaque ordinateur moderne est une machine à pousser les bits. Habituellement, il pousse des bits dans des grappes de données, appelées octets, mots, dwords ou qwords.
Un octet est composé de 8 bits, un mot 2 octets (ou 16 bits), un mot dword 2 (ou 32 bits) et un qword 2 mots (ou 64 bits). Ce ne sont pas les seuls moyens d'organiser les bits. Des manipulations sur 128 bits et 256 bits se produisent également, souvent dans les instructions SIMD.
Les instructions d'assemblage fonctionnent sur les registres et les adresses de mémoire fonctionnent généralement sous l'une des formes ci-dessus.
Les ALU (unités arithmétiques logiques) fonctionnent sur de tels faisceaux de bits comme s'ils représentaient des entiers (généralement le format Complément de Deux), et les FPU comme s'ils avaient des valeurs à virgule flottante (généralement de style IEEE 754 float
et double
). Les autres parties agiront comme s'il s'agissait de données groupées de certains formats, caractères, entrées de table, instructions CPU ou adresses.
Sur un ordinateur 64 bits typique, des paquets de 8 octets (64 bits) sont des adresses. Nous affichons ces adresses de manière conventionnelle au format hexadécimal (comme 0xabcd1234cdef5678
), mais ce n'est qu'un moyen simple pour les humains de lire les modèles de bits. Chaque octet (8 bits) est écrit en deux caractères hexadécimaux (de manière équivalente, chaque caractère hexadécimal - 0 à F - représente 4 bits).
Ce qui se passe réellement (pour un certain niveau de fait), c'est qu'il y a des bits, généralement stockés dans un registre ou stockés dans des emplacements adjacents dans une banque de mémoire, et nous essayons simplement de les décrire à un autre humain.
Suivre un pointeur consiste à demander au contrôleur mémoire de nous donner quelques données à cet endroit. Vous demanderiez généralement au contrôleur de mémoire un certain nombre d'octets à un certain emplacement (enfin, implicitement une plage d'emplacements, généralement contigus), et il est fourni via divers mécanismes dans lesquels je n'entrerai pas.
Le code spécifie généralement une destination pour les données à extraire - un registre, une autre adresse mémoire, etc. - et généralement c'est une mauvaise idée de charger des données à virgule flottante dans un registre en attendant un entier, ou vice versa.
Le type de données en C/C++ est quelque chose dont le compilateur garde la trace et il change le code généré. Habituellement, il n'y a rien d'intrinsèque dans les données qui le rend en fait d'un type quelconque. Juste une collection de bits (arrangés en octets) qui sont manipulés de manière entière (ou flottante ou de type adresse) par le code.
Il y a des exceptions à cela. Il existe des architectures où certaines choses sont différentes sorte de bits. L'exemple le plus courant est les pages d'exécution protégées - alors que les instructions indiquant au CPU ce qu'il faut faire sont des bits, au moment de l'exécution, les pages (mémoire) contenant le code à exécuter sont marquées spécialement, ne peuvent pas être modifiées et vous ne pouvez pas exécuter les pages qui ne sont pas marquées comme pages d'exécution.
Il existe également des données en lecture seule (parfois stockées dans ROM qui ne peuvent pas être écrites physiquement!), Des problèmes d'alignement (certains processeurs ne peuvent pas charger les double
s de la mémoire à moins qu'ils ne soient alignés en particulier ou des instructions SIMD qui nécessitent un certain alignement), et des myriades d'autres bizarreries d'architecture.
Même le niveau de détail ci-dessus est un mensonge. Les ordinateurs ne poussent pas "vraiment" les bits, ils poussent vraiment les tensions et le courant. Ces tensions et courants ne font parfois pas ce qu'ils sont "censés" faire au niveau de l'abstraction des bits. Les puces sont conçues pour détecter la plupart de ces erreurs et les corriger sans que l'abstraction de niveau supérieur n'en soit consciente.
Même c'est un mensonge.
Chaque niveau d'abstraction masque celui ci-dessous et vous permet de penser à résoudre des problèmes sans avoir à garder à l'esprit les diagrammes de Feynman afin d'imprimer "Hello World"
.
Ainsi, à un niveau d'honnêteté suffisant, les ordinateurs Push bits, et ces bits sont donnés un sens par la façon dont ils sont utilisés.
Les gens ont beaucoup fait pour savoir si les pointeurs sont des entiers ou non. Il existe en fait des réponses à ces questions. Cependant, vous allez devoir faire un pas dans le pays du cahier des charges, ce qui n'est pas pour les âmes sensibles. Nous allons jeter un oeil à la spécification C, ISO/IEC 9899: TC2
6.3.2.3 Pointeurs
Un entier peut être converti en n'importe quel type de pointeur. Sauf indication contraire, le résultat est défini par l'implémentation, peut ne pas être correctement aligné, peut ne pas pointer vers une entité du type référencé et peut être une représentation d'interruption.
Tout type de pointeur peut être converti en un type entier. Sauf indication contraire, le résultat est défini par l'implémentation. Si le résultat ne peut pas être représenté dans le type entier, le comportement n'est pas défini. Le résultat n'a pas besoin d'être dans la plage de valeurs de tout type entier.
Maintenant, pour cela, vous allez avoir besoin de connaître certains termes courants des spécifications. "implémentation définie" signifie que chaque compilateur est autorisé à le définir différemment. En fait, un compilateur peut même le définir de différentes manières en fonction des paramètres de votre compilateur. Un comportement indéfini signifie que le compilateur est autorisé à faire absolument n'importe quoi, de donner une erreur de temps de compilation à des comportements inexplicables, à fonctionner parfaitement.
De cela, nous pouvons voir que la forme de stockage sous-jacente n'est pas spécifiée, à part qu'il peut être une conversion en un type entier. À vrai dire, pratiquement tous les compilateurs sous le soleil représentent des pointeurs sous le capot comme des adresses entières (avec une poignée de cas spéciaux où il pourrait être représenté comme 2 entiers au lieu de 1), mais la spécification autorise absolument tout, comme représenter adresses sous forme de chaîne de 10 caractères!
Si nous avançons rapidement hors de C et regardons la spécification C++, nous obtenons un peu plus de clarté avec reinterpret_cast
, mais c'est une langue différente, donc sa valeur pour vous peut varier:
ISO/IEC N337: Projet de spécification C++ 11 (je n'ai que le projet en main)
5.2.10 Réinterpréter le casting
Un pointeur peut être explicitement converti en n'importe quel type intégral suffisamment grand pour le contenir. La fonction de mappage est définie par l'implémentation. [Remarque: Il est destiné à ne pas surprendre ceux qui connaissent la structure d'adressage de la machine sous-jacente. —Fin note] Une valeur de type std :: nullptr_t peut être convertie en type intégral; la conversion a la même signification et la même validité qu'une conversion de (void *) 0 en type intégral. [Remarque: Un reinterpret_cast ne peut pas être utilisé pour convertir une valeur de n'importe quel type en type std :: nullptr_t. —Fin note]
Une valeur de type intégral ou de type énumération peut être explicitement convertie en pointeur. Un pointeur converti en un entier de taille suffisante (s'il en existe sur l'implémentation) et renvoyé au même type de pointeur aura sa valeur d'origine; les mappages entre pointeurs et entiers sont par ailleurs définis par l'implémentation. [Remarque: à l'exception de ce qui est décrit au 3.7.4.3, le résultat d'une telle conversion ne sera pas une valeur de pointeur dérivée en toute sécurité. —Fin note]
Comme vous pouvez le voir ici, avec quelques années de plus à son actif, C++ a constaté qu'il était sûr de supposer qu'un mappage vers des nombres entiers existait, donc il n'est plus question de comportement indéfini (bien qu'il y ait une contradiction intéressante entre les parties 4 et 5 avec la phrase "s'il en existe sur la mise en œuvre")
Maintenant, que devez-vous retirer de cela?
Le meilleur pari: lancer un (char *). Les spécifications C et C++ sont pleines de règles spécifiant le conditionnement des tableaux et des structures, et les deux autorisent toujours le transtypage de n'importe quel pointeur en char *. char est toujours de 1 octet (non garanti en C, mais en C++ 11, il est devenu une partie obligatoire du langage, donc il est relativement sûr de supposer qu'il est de 1 octet partout). Cela vous permet de faire une certaine arithmétique des pointeurs au niveau octet par octet sans avoir besoin de connaître réellement les représentations spécifiques à l'implémentation des pointeurs.
Sur la plupart des architectures, le type de pointeur cesse d'exister une fois qu'il a été traduit en code machine (à l'exception peut-être des "gros pointeurs"). Par conséquent, un pointeur vers un int
ne pourrait pas être distingué d'un pointeur vers un double
, du moins en soi. *
[*] Bien que vous puissiez toujours faire des suppositions en fonction des types d'opérations que vous lui appliquez.
Une chose importante à comprendre à propos de C et C++ est ce que sont réellement les types. Tout ce qu'ils font, c'est d'indiquer au compilateur comment interpréter un ensemble de bits/octets. Commençons par le code suivant:
int var = -1337;
Selon l'architecture, un entier reçoit généralement 32 bits d'espace pour stocker cette valeur. Cela signifie que l'espace en mémoire où var est stocké ressemblera à "11111111 11111111 11111010 11000111" ou en hexadécimal "0xFFFFFAC7". C'est ça. C'est tout ce qui est stocké à cet endroit. Tous les types font dire au compilateur comment interpréter ces informations. Les pointeurs ne sont pas différents. Si je fais quelque chose comme ça:
int* var_ptr = &var; //the ampersand is telling C "get the address where var's value is located"
Ensuite, le compilateur obtiendra l'emplacement de var, puis stockera cette adresse de la même manière que le premier extrait de code enregistre la valeur -1337. Il n'y a aucune différence dans la façon dont ils sont stockés, juste dans la façon dont ils sont utilisés. Peu importe que j'ai fait de var_ptr un pointeur vers un int. Si vous le vouliez, vous pourriez le faire.
unsigned int var2 = *(unsigned int*)var_ptr;
Cela copiera la valeur hexadécimale ci-dessus de var (0xFFFFFAC7) dans l'emplacement qui stocke la valeur de var2. Si nous devions ensuite utiliser var2, nous trouverions que la valeur serait 4294965959. Les octets dans var2 sont les mêmes que var, mais la valeur numérique diffère. Le compilateur les a interprétés différemment parce que nous lui avons dit que ces bits représentent un long non signé. Vous pouvez également faire de même pour la valeur du pointeur.
unsigned int var3 = (unsigned int)var_ptr;
Vous finiriez par interpréter la valeur qui représente l'adresse de var comme un entier non signé dans cet exemple.
J'espère que cela clarifie les choses pour vous et vous donne un meilleur aperçu du fonctionnement de C. Veuillez noter que vous NE DEVRIEZ PAS faire l'une des choses folles que j'ai faites dans les deux lignes ci-dessous dans le code de production réel. C'était juste pour la démonstration.
Les types se combinent.
En particulier, certains types se combinent, presque comme s'ils étaient paramétrés avec des espaces réservés. Les types de tableau et de pointeur sont comme ceci; ils ont un tel espace réservé, qui est le type de l'élément du tableau ou la chose pointée, respectivement. Les types de fonctions sont également comme ceci; ils peuvent avoir plusieurs espaces réservés pour les paramètres et un espace réservé pour le type de retour.
Une variable qui est déclarée contenir un pointeur sur char a le type "pointeur sur char". Une variable qui est déclarée contenir un pointeur vers un pointeur vers int a le type "pointeur vers un pointeur vers int".
Un type (valeur de) "pointeur vers pointeur vers int" peut être changé en "pointeur vers int" par une opération de déréférencement. Ainsi, la notion de type n'est pas seulement des mots mais une construction mathématiquement significative, dictant ce que nous pouvons faire avec des valeurs du type (telles que la déréférence, ou passer comme paramètre ou assigner à une variable; elle détermine également la taille (nombre d'octets) de opérations d'indexation, d'arithmétique et d'incrémentation/décrémentation).
P.S. Si vous voulez approfondir les types, essayez ce blog: http://www.goodmath.org/blog/2015/05/13/expressions-and-arity-part-1/
Entier.
L'espace d'adressage dans un ordinateur est numéroté séquentiellement, en commençant à 0, et incrémente de 1. Ainsi, un pointeur contiendra un nombre entier qui correspond à une adresse dans l'espace d'adressage.