web-dev-qa-db-fra.com

Que signifie "déréférencer" un pointeur?

S'il vous plaît inclure un exemple avec l'explication.

473
asir

Revoir la terminologie de base

C’est généralement ​​assez - à moins que vous ne programmiez Assembly - d’envisager un pointeur contenant un adresse mémoire numérique, 1 se rapportant au deuxième octet de la mémoire du processus, 2 au troisième, 3 au quatrième, etc.

  • Qu'est-il arrivé à 0 et au premier octet? Eh bien, nous y reviendrons plus tard - voir pointeurs nuls ci-dessous.
  • Pour une définition plus précise de ce que les pointeurs stockent, ainsi que des liens entre mémoire et adresses, voir "Pour en savoir plus sur les adresses de mémoire et sur les raisons pour lesquelles vous n'avez probablement pas besoin de savoir".

Lorsque vous voulez accéder aux données/valeurs dans la mémoire pointées par le pointeur, le contenu de l'adresse avec cet index numérique, alors vous déréférencement ​​ le pointeur.

Différents langages informatiques ont différentes notations pour indiquer au compilateur ou à l'interprète que vous êtes maintenant intéressé par la valeur indiquée - je me concentre ci-dessous sur C et C++.

Un scénario de pointeur

Considérons en C, étant donné un pointeur tel que p ci-dessous ...

_const char* p = "abc";
_

... quatre octets avec les valeurs numériques utilisées pour coder les lettres "a", "b", "c" et un octet 0 indiquant la fin des données textuelles sont stockés quelque part en mémoire et l'adresse numérique de celle-ci les données sont stockées dans p.

Par exemple, si le littéral chaîne se trouvait à l'adresse 0x1000 et p un pointeur 32 bits à 0x2000, le contenu de la mémoire serait le suivant:

_Memory Address (hex)    Variable name    Contents
1000                                     'a' == 97 (ASCII)
1001                                     'b' == 98
1002                                     'c' == 99
1003                                     0
...
2000-2003               p                1000 hex
_

Notez qu'il n'y a pas de nom de variable/identifiant pour l'adresse 0x1000, mais nous pouvons indirectement faire référence au littéral de chaîne en utilisant un pointeur stockant son adresse: p.

Déréférencement du pointeur

Pour faire référence aux caractères indiqués par p, nous déréférencons p en utilisant l'une de ces notations (encore une fois, pour C):

_assert(*p == 'a');  // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
                     // p and 1 times the size of the things to which p points:
                     // In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b');  // Another notation for p[1]
_

Vous pouvez également déplacer les pointeurs dans les données pointées, en les déréférencant au fur et à mesure:

_++p;  // Increment p so it's now 0x1001
assert(*p == 'b');  // p == 0x1001 which is where the 'b' is...
_

Si vous avez des données sur lesquelles vous pouvez écrire, vous pouvez faire les choses suivantes:

_int x = 2;
int* p_x = &x;  // Put the address of the x variable into the pointer p_x
*p_x = 4;       // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4
_

Au-dessus, vous devez avoir su au moment de la compilation que vous auriez besoin d'une variable appelée x, et le code demande au compilateur de déterminer où il devrait être stocké, en s'assurant que l'adresse sera disponible via _&x_.

Déréférencement et accès à un membre de données de structure

En C, si vous avez une variable qui est un pointeur sur une structure avec des membres de données, vous pouvez accéder à ces membres en utilisant l'opérateur de déréférencement _->_:

_typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159;  // Dereference and access data member x.d_
(*p).d_ *= -1;    // Another equivalent notation for accessing x.d_
_

Types de données multi-octets

Pour utiliser un pointeur, un programme informatique doit également avoir une idée du type de données pointé - si ce type de données nécessite plus d'un octet à représenter, il pointe normalement sur l'octet de plus petit numéro des données.

Alors, regardons un exemple un peu plus complexe:

_double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3);  // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4);  // Actually looks at bytes from address p + 1 * sizeof(double)
                       // (sizeof(double) is almost always eight bytes)
assert(++p);           // Advance p by sizeof(double)
assert(*p == 13.4);    // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8;       // Change sizes[3] from 19.4 to 29.8
                       // Note: earlier ++p and + 2 here => sizes[3]
_

Pointeurs vers mémoire allouée dynamiquement

Parfois, vous ne savez pas combien de mémoire vous aurez besoin jusqu'à ce que votre programme soit exécuté et que vous voyez quelles données lui sont envoyées ... vous pouvez alors allouer dynamiquement de la mémoire en utilisant malloc. Il est courant de stocker l'adresse dans un pointeur ...

_int* p = malloc(sizeof(int)); // Get some memory somewhere...
*p = 10;            // Dereference the pointer to the memory, then write a value in
fn(*p);             // Call a function, passing it the value at address p
(*p) += 3;          // Change the value, adding 3 to it
free(p);            // Release the memory back to the heap allocation library
_

En C++, l'allocation de mémoire se fait normalement avec l'opérateur new et la désallocation avec delete:

_int* p = new int(10); // Memory for one int with initial value 10
delete p;

p = new int[10];      // Memory for ten ints with unspecified initial value
delete[] p;

p = new int[10]();    // Memory for ten ints that are value initialised (to 0)
delete[] p;
_

Voir aussi pointeurs intelligents C++ ci-dessous.

Perdre et fuir des adresses

Souvent, un pointeur peut être la seule indication de l'emplacement de certaines données ou de la mémoire tampon dans la mémoire. Si l'utilisation continue de ces données/mémoire tampon est nécessaire, ou si vous avez la possibilité d'appeler free() ou delete pour éviter de perdre de la mémoire, le programmeur doit alors opérer sur une copie du pointeur ...

_const char* p = asprintf("name: %s", name);  // Common but non-Standard printf-on-heap

// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
    if (!isprint(*q))
        *q = '_';

printf("%s\n", p); // Only q was modified
free(p);
_

... ou orchestrez soigneusement l'inversion de tout changement ...

_const size_t n = ...;
p += n;
...
p -= n;  // Restore earlier value...
_

Pointeurs intelligents C++

En C++, il est recommandé d’utiliser des objets pointeur intelligent pour stocker et gérer les pointeurs, en les désallocant automatiquement lorsque les destructeurs des pointeurs intelligents s’exécutent. Depuis C++ 11, la bibliothèque standard en fournit deux, unique_ptr lorsqu'il existe un seul propriétaire pour un objet alloué ...

_{
    std::unique_ptr<T> p{new T(42, "meaning")};
    call_a_function(p);
    // The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete
_

... et shared_ptr pour la propriété d'actions (avec décompte de références ) ...

_{
    std::shared_ptr<T> p(new T(3.14, "pi"));
    number_storage.may_add(p); // Might copy p into its container
} // p's destructor will only delete the T if number_storage didn't copy
_

Null pointeurs

En C, NULL et _0_ - et également en C++ nullptr - peuvent être utilisés pour indiquer qu'un pointeur ne contient pas l'adresse de mémoire d'une variable et ne doit pas être déréférencé ou utilisé en arithmétique de pointeur. Par exemple:

_const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
char c;
while ((c = getopt(argc, argv, "f:")) != EOF)
    switch (c) {
      case f: p_filename = optarg; break;
    }
if (p_filename)  // Only NULL converts to false
    ...   // Only get here if -f flag specified
_

En C et C++, tout comme les types numériques intégrés, les valeurs par défaut ne sont pas nécessairement _0_, ni bools à false, les pointeurs ne sont pas toujours définis sur NULL. Toutes ces variables sont définies sur 0/false/NULL quand il s’agit de static variables ou de variables membres directes ou indirectes d’objets statiques ou de leurs bases, ou sont initialisées à zéro (par exemple new T(); et new T(x, y, z); effectue une initialisation à zéro sur les membres de T, y compris les pointeurs, alors que _new T;_ ne le fait pas).

De plus, lorsque vous affectez _0_, NULL et nullptr à un pointeur, les bits du pointeur ne sont pas nécessairement tous réinitialisés: le pointeur ne peut pas contenir "0" au niveau matériel, ou reportez-vous à l'adresse 0 dans votre espace d'adressage virtuel. Le compilateur est autorisé à stocker autre chose là-bas s’il en a la raison, mais quoi qu’il en soit - si vous comparez le pointeur à _0_, NULL, nullptr ou à un autre pointeur qui n’a été attribué à aucun de ces éléments, la comparaison doit fonctionner comme prévu. Donc, sous le code source au niveau du compilateur, "NULL" est potentiellement un peu "magique" dans les langages C et C++ ...

Plus d'informations sur les adresses mémoire et pourquoi vous n'avez probablement pas besoin de savoir

Plus strictement, les pointeurs initialisés stockent un motif binaire identifiant soit NULL, soit une adresse mémoire (souvent virtuelle ).

Le cas simple est celui où il s'agit d'un décalage numérique dans l'espace d'adressage virtuel complet du processus; dans des cas plus complexes, le pointeur peut être relatif à une zone de mémoire spécifique, que la CPU peut sélectionner en fonction des registres de "segments" de la CPU ou d'une forme quelconque de segment id codé dans la configuration binaire, et/ou regardant à des endroits différents instructions du code machine utilisant l’adresse.

Par exemple, un _int*_ correctement initialisé pour pointer sur une variable int pourrait - après avoir converti en un _float*_ - accéder à une valeur de la mémoire "GPU" tout à fait distincte de la int variable, une fois convertie en un pointeur de fonction, il peut s’agir d’une mémoire distincte contenant les codes machine de la fonction.

Les langages de programmation 3GL tels que C et C++ ont tendance à masquer cette complexité, telle que:

  • Si le compilateur vous donne un pointeur sur une variable ou une fonction, vous pouvez la déréférencer librement (à condition que la variable ne soit pas détruite/désallouée entre-temps) et que le problème du compilateur soit de savoir si, par exemple. un registre de CPU particulier doit être préalablement restauré, ou une instruction de code machine distincte doit être utilisée

  • Si vous obtenez un pointeur sur un élément d'un tableau, vous pouvez utiliser l'arithmétique de pointeur pour vous déplacer n'importe où dans le tableau, ou même pour former une adresse légale à la fin du tableau qu'il est légal de comparer avec d'autres pointeurs sur des éléments. dans le tableau (ou qui ont été déplacés de la même manière par arithmétique de pointeur vers la même valeur au-delà de la fin); encore une fois en C et C++, c'est au compilateur de s'assurer que cela "fonctionne"

  • Fonctions spécifiques du système d'exploitation, par exemple le mappage de mémoire partagée peut vous donner des pointeurs et ils "fonctionneront" dans la plage d'adresses qui leur convient

  • Les tentatives visant à déplacer des pointeurs légaux au-delà de ces limites, ou à convertir des nombres arbitraires en pointeurs, ou à utiliser des pointeurs transtypés en types non liés, ont généralement [comportement indéfini] , bibliothèques et applications de niveau, mais le code des systèmes d’exploitation, des pilotes de périphérique, etc. peut devoir s’appuyer sur un comportement laissé indéfini par C ou C++, qui est néanmoins bien défini par son matériel spécifique.

666
Tony Delroy

Déréférencer un pointeur signifie obtenir la valeur stockée dans l'emplacement de mémoire indiqué par le pointeur. L'opérateur * est utilisé pour cela, et s'appelle l'opérateur de déréférencement.

int a = 10;
int* ptr = &a;

printf("%d", *ptr); // With *ptr I'm dereferencing the pointer. 
                    // Which means, I am asking the value pointed at by the pointer.
                    // ptr is pointing to the location in memory of the variable a.
                    // In a's location, we have 10. So, dereferencing gives this value.

// Since we have indirect control over a's location, we can modify its content using the pointer. This is an indirect way to access a.

 *ptr = 20;         // Now a's content is no longer 10, and has been modified to 20.
87
Mahesh

Un pointeur est une "référence" à une valeur ... tout comme un numéro d'appel de bibliothèque est une référence à un livre. "Déréférencement" du numéro d'appel passe physiquement et récupère ce livre.

int a=4 ;
int *pA = &a ;
printf( "The REFERENCE/call number for the variable `a` is %p\n", pA ) ;

// The * causes pA to DEREFERENCE...  `a` via "callnumber" `pA`.
printf( "%d\n", *pA ) ; // prints 4.. 

Si le livre n'est pas là, le bibliothécaire commence à crier, ferme la bibliothèque et deux ou trois personnes sont sur le point de rechercher la cause d'une personne qui va trouver un livre qui n'y est pas.

16
bobobobo

En termes simples, déréférencer signifie accéder à la valeur d'un certain emplacement de mémoire par rapport auquel ce pointeur est pointé.

11
Fahad Naeem

Code et explication de Bases du pointeur :

L'opération de déréférence commence au pointeur et suit sa flèche pour accéder à sa pointe. L’objectif peut être de regarder l’état pointee ou de changer l’état pointee. L'opération de déréférence sur un pointeur ne fonctionne que si le pointeur a une pointee - la pointee doit être allouée et le pointeur doit être défini pour pointer sur elle. L'erreur la plus courante dans le code du pointeur est de ne pas configurer la pointee. Le crash le plus courant à cause de cette erreur dans le code est une opération de déréférence ayant échoué. Dans Java, le déréférencement incorrect sera signalé poliment par le système d'exécution. Dans les langages compilés tels que C, C++ et Pascal, le déréférencement incorrect plante parfois, et parfois la mémoire corrompue de manière subtile et aléatoire. Les bugs de pointeur dans les langages compilés peuvent être difficiles à détecter pour cette raison.

void main() {   
    int*    x;  // Allocate the pointer x
    x = malloc(sizeof(int));    // Allocate an int pointee,
                            // and set x to point to it
    *x = 42;    // Dereference x to store 42 in its pointee   
}
7
atp

Je pense que toutes les réponses précédentes sont fausses, car elles indiquent que déréférencer signifie accéder à la valeur réelle. Wikipedia donne la bonne définition à la place: https://en.wikipedia.org/wiki/Dereference_operator

Il fonctionne sur une variable de pointeur et renvoie une valeur l équivalente à celle de l'adresse du pointeur. Cela s'appelle "déréférencer" le pointeur.

Cela dit, nous pouvons déréférencer le pointeur sans jamais accéder à la valeur indiquée. Par exemple:

char *p = NULL;
*p;

Nous avons déréférencé le pointeur NULL sans accéder à sa valeur. Ou on pourrait faire:

p1 = &(*p);
sz = sizeof(*p);

Encore une fois, déréférencement, mais jamais accéder à la valeur. Un tel code NE plantera PAS: le blocage se produit lorsque vous accédez réellement aux données par un pointeur non valide. Cependant, malheureusement, selon la norme, la déréférencement d'un pointeur non valide est un comportement indéfini (à quelques exceptions près), même si vous n'essayez pas de toucher aux données réelles.

Donc, en bref: déréférencer le pointeur signifie lui appliquer l'opérateur de déréférence. Cet opérateur ne fait que renvoyer une valeur l pour votre utilisation future.

2
stsp