Lorsque vous apprenez le C++, ou du moins lorsque je l’ai appris par le biais de C++ Primer , les pointeurs étaient appelés "adresses de mémoire" des éléments qu’ils désignaient. Je me demande dans quelle mesure cela est vrai.
Par exemple, deux éléments *p1
et *p2
ont-ils la propriété p2 = p1 + 1
ou p1 = p2 + 1
si et seulement si ils sont adjacents dans la mémoire physique?
Vous devez considérer les pointeurs comme des adresses de virtual memory: les systèmes d’exploitation grand public modernes et les environnements d’exécution placent au moins une couche d’abstraction entre la mémoire physique et ce que vous voyez comme valeur de pointeur.
En ce qui concerne votre déclaration finale, vous ne pouvez pas faire cette hypothèse, même dans un espace d'adressage de mémoire virtuelle. L'arithmétique de pointeur n'est valide que dans des blocs de mémoire contiguës, tels que des tableaux. Et bien qu’il soit permis (en C et C++) d’affecter un pointeur à un point situé après un tableau (ou un scalaire), le comportement sur deferencing un tel pointeur est indéfini. Il est inutile de faire des hypothèses sur la contiguïté dans la mémoire physique dans le contexte de C et C++.
Pas du tout.
C++ est une abstraction du code que votre ordinateur va exécuter. Nous voyons cette fuite d'abstraction à quelques endroits (références de membre de classe nécessitant un stockage, par exemple), mais en général, vous serez mieux loti si vous codez pour l'abstraction et rien d'autre.
Les pointeurs sont des pointeurs. Ils pointent des choses. Seront-ils mis en œuvre comme adresses de mémoire dans la réalité? Peut être. Ils pourraient également être optimisés ou (dans le cas, par exemple, des pointeurs vers les membres), ils pourraient être un peu plus complexes qu'une simple adresse numérique.
Lorsque vous commencez à penser que les pointeurs sont des entiers mappés sur des adresses en mémoire, vous commencez par oublier par exemple que c’est undefined pour tenir un pointeur sur un objet qui n’existe pas (vous ne pouvez pas simplement incrémenter). décrémenter un pointeur bon gré mal gré en une adresse mémoire de votre choix).
Comme de nombreuses réponses l'ont déjà mentionné, il ne faut pas les considérer comme des adresses de mémoire. Découvrez ces réponses et ici pour les comprendre. S'adressant à votre dernière déclaration
* p1 et * p2 ont la propriété p2 = p1 + 1 ou p1 = p2 + 1 si et seulement s'ils sont adjacents dans la mémoire physique
est correct que si p1
et p2
sont du même type ou pointent vers des types de la même taille.
Absolument raison de considérer les pointeurs comme des adresses de mémoire. C'est ce qu'ils sont dans TOUS les compilateurs avec lesquels j'ai travaillé - pour un certain nombre d'architectures de processeurs différentes, fabriquées par un certain nombre de producteurs de compilateurs différents.
Cependant, le compilateur fait une magie intéressante, pour vous aider avec le fait que les adresses de mémoire normales (dans tous les processeurs traditionnels au moins) sont des adresses d'octet, et que l'objet auquel votre pointeur se réfère peut ne pas être exactement un octet. Donc, si nous avons T* ptr;
, ptr++
fera ((char*)ptr) + sizeof(T);
ou ptr + n
sera ((char*)ptr) + n*sizeof(T)
. Cela signifie également que votre p1 == p2 + 1
requiert que p1
et p2
soient du même type T
, car le +1
est en réalité +sizeof(T)*1
.
Il y a UNE exception à la précédente "les pointeurs sont des adresses de mémoire", et ce sont les pointeurs de fonctions membres. Elles sont "spéciales" et, pour le moment, ignorez simplement comment elles sont réellement mises en oeuvre, suffisamment pour dire qu'elles ne sont pas "que des adresses de mémoire".
Le système d’exploitation fournit une abstraction de la machine physique à votre programme (c’est-à-dire que votre programme est exécuté sur une machine virtuelle). Ainsi, votre programme n’a accès à aucune ressource physique de votre ordinateur, qu’il s’agisse de temps CPU, de mémoire, etc. il suffit de demander à l'OS ces ressources.
Dans le cas de la mémoire, votre programme fonctionne dans un espace d'adressage virtuel, défini par le système d'exploitation. Cet espace d'adressage comporte plusieurs régions, telles que pile, segment de mémoire, code, etc. La valeur de vos pointeurs représente les adresses de cet espace d'adressage virtuel. En effet, 2 pointeurs vers des adresses consécutives pointeront vers des emplacements consécutifs dans cet espace adresse.
Toutefois, le système d’exploitation divise cet espace adresse en pages et en segments, qui sont permutés de la mémoire en fonction des besoins. Ainsi, vos pointeurs peuvent pointer ou non sur des emplacements de mémoire physique consécutifs. Il est donc impossible d’indiquer au moment de l’exécution si Vrai ou pas. Cela dépend également de la stratégie utilisée par le système d'exploitation pour la pagination et la segmentation.
En bout de ligne, les pointeurs sont des adresses de mémoire. Cependant, il s’agit d’adresses situées dans un espace de mémoire virtuelle et il appartient au système d’exploitation de décider de la manière dont cela sera mappé sur l’espace de mémoire physique.
En ce qui concerne votre programme, ce n'est pas un problème. Une des raisons de cette abstraction est de faire croire aux programmes qu’ils sont les seuls utilisateurs de la machine. Imaginez le cauchemar que vous auriez à subir si vous deviez prendre en compte la mémoire allouée par d'autres processus lorsque vous écrivez votre programme - vous ne savez même pas quels processus seront exécutés simultanément avec le vôtre. En outre, il s'agit d'une bonne technique pour appliquer la sécurité: votre processus ne peut pas (bien, au moins ne devrait pas pouvoir) accéder de manière malveillante à l'espace mémoire d'un autre processus car il s'exécute dans 2 espaces mémoire (virtuels) différents.
Comme d’autres variables, le pointeur stocke une donnée qui peut être une adresse de mémoire où d’autres données sont stockées.
Ainsi, le pointeur est une variable qui a une adresse et peut contenir une adresse.
Notez que, il n'est pas nécessaire qu'un pointeur contienne toujours une adresse . Il peut contenir un identifiant/adresse non-adresse, etc. Par conséquent, dire un pointeur comme adresse n'est pas une bonne chose.
Concernant votre deuxième question:
L'arithmétique de pointeur est valable pour des blocs de mémoire contigus. Si p2 = p1 + 1
et les deux pointeurs sont du même type, alors p1
et p2
désignent un bloc de mémoire contigu. Ainsi, les adresses que p1
et p2
tiennent sont adjacentes.
Je pense cette réponse a la bonne idée mais une terminologie médiocre. Ce que les pointeurs C fournissent sont exactement le contraire de l’abstraction.
Une abstraction fournit un modèle mental relativement facile à comprendre et à raisonner, même si le matériel est plus complexe et difficile à comprendre ou plus difficile à raisonner.
C pointeurs sont le contraire de cela. Ils tiennent compte de possibles difficultés du matériel, même si le matériel réel est souvent plus simple et plus facile à raisonner. Ils limitent votre raisonnement à ce qui est permis par une union des parties les plus complexes du matériel le plus complexe, quelle que soit la simplicité de votre matériel.
Les pointeurs C++ ajoutent une chose que C n’inclut pas. Cela permet de comparer tous les pointeurs du même type pour l'ordre, même s'ils ne sont pas dans le même tableau. Cela permet un peu plus d'un modèle mental, même s'il ne correspond pas parfaitement au matériel.
À moins que les pointeurs ne soient optimisés par le compilateur, ce sont des entiers qui stockent des adresses de mémoire. Leur longueur dépend de la machine pour laquelle le code est compilé, mais ils peuvent généralement être traités comme des ints.
En fait, vous pouvez vérifier cela en imprimant le nombre réel qui y est stocké avec printf()
.
Attention, cependant, les opérations d'incrémentation/décrémentation du pointeur type *
sont effectuées par la sizeof(type)
. Constatez par vous-même avec ce code (testé en ligne sur Repl.it):
#include <stdio.h>
int main() {
volatile int i1 = 1337;
volatile int i2 = 31337;
volatile double d1 = 1.337;
volatile double d2 = 31.337;
volatile int* pi = &i1;
volatile double* pd = &d1;
printf("ints: %d, %d\ndoubles: %f, %f\n", i1, i2, d1, d2);
printf("0x%X = %d\n", pi, *pi);
printf("0x%X = %d\n", pi-1, *(pi-1));
printf("Difference: %d\n",(long)(pi)-(long)(pi-1));
printf("0x%X = %f\n", pd, *pd);
printf("0x%X = %f\n", pd-1, *(pd-1));
printf("Difference: %d\n",(long)(pd)-(long)(pd-1));
}
Toutes les variables et tous les pointeurs ont été déclarés volatils afin que le compilateur ne les optimise pas. Notez également que j'ai utilisé décrément, car les variables sont placées dans la pile de fonctions.
La sortie était:
ints: 1337, 31337
doubles: 1.337000, 31.337000
0xFAFF465C = 1337
0xFAFF4658 = 31337
Difference: 4
0xFAFF4650 = 1.337000
0xFAFF4648 = 31.337000
Difference: 8
Notez que ce code peut ne pas fonctionner sur tous les compilateurs, spécialement s'ils ne stockent pas les variables dans le même ordre. Cependant, ce qui est important, c’est que les valeurs du pointeur puissent être lues et imprimées et que des décréments d’un décrément puissent/seront décrémentés en fonction de la taille de la variable référencée par le pointeur.
Notez également que &
et *
sont des opérateurs réels pour reference ("récupère l'adresse mémoire de cette variable") et dereference ("récupère le contenu de cette adresse mémoire").
Cela peut également être utilisé pour des astuces intéressantes comme obtenir les valeurs binaires IEEE 754 pour les floats, en convertissant le float*
en tant que int*
:
#include <iostream>
int main() {
float f = -9.5;
int* p = (int*)&f;
std::cout << "Binary contents:\n";
int i = sizeof(f)*8;
while(i) {
i--;
std::cout << ((*p & (1 << i))?1:0);
}
}
Le résultat est:
Binary contents:
11000001000110000000000000000000
Exemple tiré de https://pt.wikipedia.org/wiki/IEEE_754 . Découvrez sur n'importe quel convertisseur.
En quelque sorte, les réponses ne parviennent pas à mentionner une famille de pointeurs spécifique, à savoir les pointeurs vers les membres. Ce sont certainement pas adresses de mémoire.
Les pointeurs sont des adresses de mémoire, mais vous ne devez pas en déduire qu'ils reflètent l'adresse physique. Lorsque vous voyez des adresses telles que 0x00ffb500
, il s’agit d’adresses logiques que le MMU traduira en adresse physique correspondante. C'est le scénario le plus probable, car la mémoire virtuelle est le système de gestion de la mémoire le plus étendu, mais il peut exister des systèmes gérant directement l'adresse physique.
Selon le standard C++ 14, [expr.unary.op]/3:
Le résultat de l'opérateur unaire
&
est un pointeur sur son opérande. L'opérande doit être une valeur ou un identifiant qualifié. Si l'opérande est un identifiant qualifié nommant un membre non statiquem
d'une classeC
de typeT
, le résultat est de type «pointeur sur un membre de la classeC
de typeT
» et constitue unC::m
de conception de valeur. Sinon, si le type de l'expression estT
, le résultat est de type "pointeur sur T" et est une valeur c'est l'adresse de l'objet désigné ou un pointeur sur la fonction désignée. [Remarque: en particulier, l'adresse d'un objet de type “cvT
” est “pointeur sur cvT
” , avec la même qualification cv. —Fin note]
Donc, cela indique clairement et sans ambiguïté que les pointeurs sur le type d'objet (c'est-à-dire un T *
, où T
n'est pas un type de fonction) contiennent des adresses.
"adresse" est défini par [intro.memory]/1:
La mémoire disponible pour un programme C++ consiste en une ou plusieurs séquences d'octets contigus. Chaque octet a une adresse unique.
Une adresse peut donc être tout ce qui sert à identifier de manière unique un octet particulier de la mémoire.
Remarque: Dans la terminologie standard C++, memory ne fait référence qu'à l'espace utilisé. Cela ne signifie pas mémoire physique, mémoire virtuelle ou quelque chose comme ça. La mémoire est un ensemble d'allocations disjoint.
Il est important de garder à l'esprit que, bien que l'un des moyens possibles d'identifier chaque octet en mémoire de manière unique consiste à affecter un entier unique à chaque octet de mémoire physique ou virtuelle, ce n'est pas le seul moyen possible.
Pour éviter d'écrire du code non portable, il convient d'éviter de supposer qu'une adresse est identique à un entier. Les règles de l'arithmétique pour les pointeurs sont différentes des règles de l'arithmétique pour les entiers. De même, nous ne dirions pas que 5.0f
est identique à 1084227584
même s'ils ont des représentations de bits identiques en mémoire (sous IEEE754).
L'exemple particulier que vous donnez:
Par exemple, deux éléments * p1 et * p2 ont-ils la propriété p2 = p1 + 1 ou p1 = p2 + 1 si et seulement s'ils sont adjacents dans la mémoire physique?
échouerait sur les plates-formes ne disposant pas d'un espace d'adressage plat, tel que PIC . Pour accéder à la mémoire physique sur le PIC, vous avez besoin d'une adresse et d'un numéro de banque, mais ce dernier peut être dérivé d'informations extrinsèques telles que le fichier source particulier. Donc, faire de l'arithmétique sur les pointeurs de différentes banques donnerait des résultats inattendus.