Clarification du pointeur vers le pointeur

142

Je suivais ce tutoriel sur la façon dont un pointeur vers un pointeur .

Permettez-moi de citer le passage pertinent:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Maintenant nous pouvons définir

    int **ipp = &ip1;

et ipppointe vers ip1quel point i. *ippest ip1, et **ippest i, ou 5. Nous pouvons illustrer la situation, avec notre notation familière en forme de boîte et de flèche, comme ceci:

entrez la description de l'image ici

Si alors on dit

    *ipp = ip2;

nous avons changé le pointeur pointé par ipp(c'est-à-dire ip1) pour qu'il contienne une copie de ip2, de sorte qu'il ( ip1) pointe maintenant vers j:

entrez la description de l'image ici


Ma question est la suivante: pourquoi dans la deuxième image, ipppointe toujours ip1mais pas ip2?

Blake
la source
6
s'il vous plaît, ajoutez le type de ipplors de sa définition, afin que votre question soit complète ;-)
zmo
1
Parce que ippmontrant ip1donc *ipp = ip2est le même queip1 = ip2
Grijesh Chauhan
1
Pouvons-nous arrêter d'écrire des astériks aussi loin du type pointu? int **ippest beaucoup moins intuitif que int** ipp, en particulier lorsque **ippsignifie une chose complètement différente en dehors des déclarations de type.
Darkhogg
4
@Darkhogg me int **ippsemble assez intuitif. Cela signifie que je fais **ippun int. Ce qui est vrai.
ithisa
2
@ user54609 significations et intuitivité personnelle à part, considérez ceci: Le type de ippest int**, alors écrivez simplement int**au lieu de compréhensions magiques " impdéréférencement est un int" .
Manu343726

Réponses:

143

Oubliez une seconde l'analogie du pointage. Ce qu'un pointeur contient vraiment, c'est une adresse mémoire. L' &opérateur est "adresse de" - c'est-à-dire qu'il renvoie l'adresse en mémoire d'un objet. L' *opérateur vous donne l'objet auquel un pointeur se réfère, c'est-à-dire que, étant donné un pointeur contenant une adresse, il renvoie l'objet à cette adresse mémoire. Ainsi, lorsque vous le faites *ipp = ip2, ce que vous faites est d' *ippobtenir l'objet à l'adresse contenue dans ipplaquelle se trouve ip1, puis de l'affecter à ip1la valeur stockée dans ip2, qui est l'adresse de j.

Simplement
& -> Adresse de
*-> Valeur à

Robert S. Barnes
la source
14
& et * n'ont jamais été aussi faciles
Ray
7
Je pense que la principale source de confusion est due à l'ambiguïté de l'opérateur *, qui, lors de la déclaration de variable, est utilisé pour indiquer que la variable, en fait, est un pointeur vers un certain type de données. Mais, d'autre part, il est également utilisé dans les instructions pour accéder au contenu de la variable pointée par un pointeur (opérateur de déréférencement).
Lucas A.
43

Parce que vous avez modifié la valeur pointée par ipppas la valeur de ipp. Donc, ipppointe toujours vers ip1(la valeur de ipp), ip1la valeur de s est maintenant la même que ip2la valeur de, donc ils pointent tous les deux vers j.

Ce:

*ipp = ip2;

est le même que:

ip1 = ip2;
Skizz
la source
11
Cela peut valoir la peine de souligner la différence entre int *ip1 = &iet *ipp = ip2;, c'est-à-dire que si vous supprimez le intde la première instruction, les affectations sont très similaires, mais le *fait quelque chose de très différent dans les deux cas.
Crowman
22

Comme la plupart des questions pour débutants dans la balise C, cette question peut être répondue en revenant aux premiers principes:

  • Un pointeur est une sorte de valeur.
  • Une variable contient une valeur.
  • L' &opérateur transforme une variable en pointeur.
  • L' *opérateur transforme un pointeur en variable.

(Techniquement, je devrais dire "lvalue" au lieu de "variable", mais je pense qu'il est plus clair de décrire les emplacements de stockage modifiables comme des "variables".)

Nous avons donc des variables:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

La variable ip1 contient un pointeur. L' &opérateur se transforme ien pointeur et cette valeur de pointeur est affectée ip1. ip1 Contient donc un pointeur versi .

La variable ip2 contient un pointeur. L' &opérateur se transforme jen pointeur et ce pointeur est affecté à ip2. ip2 Contient donc un pointeur vers j.

int **ipp = &ip1;

La variable ippcontient un pointeur. L' &opérateur transforme la variable ip1en pointeur et cette valeur de pointeur est affectée ipp. ippContient donc un pointeur vers ip1.

Résumons l'histoire jusqu'à présent:

  • i contient 5
  • j contient 6
  • ip1 contient "pointeur vers i"
  • ip2 contient "pointeur vers j"
  • ippcontient "pointeur vers ip1"

Maintenant on dit

*ipp = ip2;

L' *opérateur transforme un pointeur en une variable. Nous récupérons la valeur de ipp, qui est "pointeur sur" ip1et la transformons en une variable. Quelle variable?ip1 Bien sûr!

C'est donc simplement une autre façon de dire

ip1 = ip2;

Nous récupérons donc la valeur de ip2. Qu'Est-ce que c'est? "pointeur vers j". Nous attribuons cette valeur de pointeur à ip1, il en ip1va de même pour le "pointeur vers j"

Nous n'avons changé qu'une chose: la valeur de ip1:

  • i contient 5
  • j contient 6
  • ip1contient "pointeur vers j"
  • ip2contient "pointeur vers j"
  • ippcontient "pointeur vers ip1"

Pourquoi fait-il ipptoujours référence ip1et non ip2?

Une variable change lorsque vous lui assignez. Comptez les affectations; il ne peut pas y avoir plus de changements de variables que d'affectations! Vous commencez par attribuer à i, j, ip1, ip2et ipp. Vous attribuez ensuite à *ipp, ce qui, comme nous l'avons vu, signifie la même chose que «attribuer à ip1». Puisque vous n'avez pas attribué à ippune deuxième fois, cela n'a pas changé!

Si vous vouliez changer, ippvous devrez en fait attribuer à ipp:

ipp = &ip2;

par exemple.

Eric Lippert
la source
21

espérons que ce morceau de code peut vous aider.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

il sort:

entrez la description de l'image ici

Michaeltang
la source
12

Mon opinion très personnelle est que les images avec des flèches pointant de cette façon ou qui rendent les pointeurs plus difficiles à comprendre. Cela les fait ressembler à des entités abstraites et mystérieuses. Ils ne sont pas.

Comme tout le reste de votre ordinateur, les pointeurs sont des nombres . Le nom "pointeur" est juste une manière sophistiquée de dire "une variable contenant une adresse".

Par conséquent, permettez-moi de faire bouger les choses en expliquant comment un ordinateur fonctionne réellement.

Nous avons un int, il a le nom iet la valeur 5. Ceci est stocké en mémoire. Comme tout ce qui est stocké en mémoire, il a besoin d'une adresse, sinon nous ne pourrions pas la trouver. Disons qu'il ise termine à l'adresse 0x12345678 et que son copain javec la valeur 6 se termine juste après. En supposant un processeur 32 bits où int vaut 4 octets et les pointeurs sont 4 octets, alors les variables sont stockées dans la mémoire physique comme ceci:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Nous voulons maintenant pointer sur ces variables. Nous créons un pointeur vers int int* ip1, et un int* ip2. Comme tout dans l'ordinateur, ces variables de pointeur sont également allouées quelque part en mémoire. Supposons qu'ils se retrouvent aux adresses adjacentes suivantes en mémoire, immédiatement après j. Nous définissons les pointeurs pour qu'ils contiennent les adresses des variables précédemment allouées: ip1=&i;("copier l'adresse de i dans ip1") et ip2=&j. Ce qui se passe entre les lignes est:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Donc, ce que nous avons obtenu, c'était encore quelques morceaux de mémoire de 4 octets contenant des nombres. Il n'y a pas de flèches mystiques ou magiques en vue.

En fait, rien qu'en regardant une image mémoire, nous ne pouvons pas dire si l'adresse 0x12345680 contient un intouint* . La différence réside dans la manière dont notre programme choisit d'utiliser le contenu stocké à cette adresse. (La tâche de notre programme est en fait juste de dire au CPU quoi faire avec ces nombres.)

Ensuite, nous ajoutons encore un autre niveau d'indirection avec int** ipp = &ip1;. Encore une fois, nous obtenons juste un morceau de mémoire:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

Le modèle semble familier. Encore un autre morceau de 4 octets contenant un nombre.

Maintenant, si nous avions un vidage mémoire de la petite RAM fictive ci-dessus, nous pourrions vérifier manuellement où ces pointeurs pointent. Nous regardons ce qui est stocké à l'adresse de la ippvariable et trouvons le contenu 0x12345680. Quelle est bien sûr l'adresse où ip1est stockée. Nous pouvons aller à cette adresse, vérifier le contenu là-bas et trouver l'adresse de i, puis enfin nous pouvons aller à cette adresse et trouver le numéro 5.

Donc, si nous prenons le contenu de ipp, *ippnous obtiendrons l'adresse de la variable pointeur ip1. En écrivant, *ipp=ip2nous copions ip2 dans ip1, c'est équivalent à ip1=ip2. Dans les deux cas, nous obtiendrions

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Ces exemples ont été donnés pour un processeur big endian)

Lundin
la source
5
Bien que je prenne note de votre point de vue, il est utile de considérer les pointeurs comme des entités abstraites et mystérieuses. Toute implémentation particulière de pointeurs n'est que des chiffres, mais la stratégie d'implémentation que vous esquissez n'est pas une exigence d'une implémentation, c'est juste une stratégie commune. Les pointeurs n'ont pas besoin d'être de la même taille qu'un int, les pointeurs ne doivent pas nécessairement être des adresses dans un modèle de mémoire virtuelle plate, etc. ce ne sont que des détails de mise en œuvre.
Eric Lippert
@EricLippert Je pense que l'on peut rendre cet exemple plus abstrait en n'utilisant pas d'adresses mémoire ou de blocs de données réels. S'il s'agissait d'un tableau indiquant quelque chose comme l' location, value, variableemplacement 1,2,3,4,5et la valeur A,1,B,C,3, l'idée correspondante de pointeurs pourrait être expliquée facilement sans l'utilisation de flèches, qui sont intrinsèquement déroutantes. Quelle que soit l'implémentation choisie, une valeur existe à un endroit donné, et c'est une pièce du puzzle qui devient obscurcie lors de la modélisation avec des flèches.
MirroredFate
@EricLippert D'après mon expérience, la plupart des futurs programmeurs C qui ont des problèmes pour comprendre les pointeurs sont ceux qui ont été nourris de modèles abstraits et artificiels. L'abstraction n'est pas utile, car tout le but du langage C aujourd'hui, c'est qu'il est proche du matériel. Si vous apprenez C mais n'avez pas l'intention d'écrire du code proche du matériel, vous perdez votre temps . Java, etc. est un bien meilleur choix si vous ne voulez pas savoir comment fonctionnent les ordinateurs, mais faites simplement de la programmation de haut niveau.
Lundin
@EricLippert Et oui, diverses implémentations obscures de pointeurs peuvent exister, où les pointeurs ne correspondent pas nécessairement à des adresses. Mais dessiner des flèches ne vous aidera pas non plus à comprendre comment elles fonctionnent. À un moment donné, vous devez abandonner la pensée abstraite et passer au niveau matériel, sinon vous ne devriez pas utiliser C. Il existe de nombreux langages modernes beaucoup plus appropriés destinés à une programmation de haut niveau purement abstraite.
Lundin
@Lundin: Je ne suis pas non plus un grand fan des flèches; la notion de flèche en tant que donnée est délicate. Je préfère y penser abstraitement mais sans flèches. L' &opérateur sur une variable vous donne une pièce de monnaie qui représente cette variable. L' *opérateur sur cette pièce vous rend la variable. Aucune flèche requise!
Eric Lippert
8

Notez les affectations:

ipp = &ip1;

résultats ippà pointer ip1.

donc pour ipppointer vers ip2, nous devrions changer de la même manière,

ipp = &ip2;

ce que nous ne faisons clairement pas. Au lieu de cela, nous modifions la valeur à l'adresse pointée par ipp.
En faisant ce qui suit

*ipp = ip2;

nous remplaçons simplement la valeur stockée dans ip1.

ipp = &ip1, Des moyens *ipp = ip1 = &i,
maintenant, *ipp = ip2 = &j.
Donc, *ipp = ip2c'est essentiellement le même que ip1 = ip2.

Dipto
la source
5
ipp = &ip1;

Aucune attribution ultérieure n'a modifié la valeur de ipp. C'est pourquoi il pointe toujours ip1.

Ce que vous faites avec *ipp, c'est -à -dire avec ip1, ne change pas le fait qui ipppointe vers ip1.

Daniel Daranas
la source
5

Ma question est la suivante: pourquoi dans la deuxième image, ipp pointe toujours vers ip1 mais pas ip2?

vous avez placé de belles photos, je vais essayer de faire de beaux ascii

Comme @ Robert-S-Barnes l'a dit dans sa réponse: oubliez les pointeurs , et ce qui indique quoi, mais pensez en termes de mémoire. Fondamentalement, an int*signifie qu'il contient l'adresse d'une variable et an int**contient l'adresse d'une variable qui contient l'adresse d'une variable. Ensuite, vous pouvez utiliser l'algèbre du pointeur pour accéder aux valeurs ou aux adresses: &foomoyens address of fooet *foomoyens value of the address contained in foo.

Donc, comme les pointeurs concernent la mémoire, la meilleure façon de rendre cela "tangible" est de montrer ce que l'algèbre des pointeurs fait à la mémoire.

Alors, voici la mémoire de votre programme (simplifiée pour les besoins de l'exemple):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

lorsque vous faites votre code initial:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

voici à quoi ressemble votre mémoire:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

là, vous pouvez voir ip1et ip2obtenir les adresses de iet jet ippn'existe toujours pas. N'oubliez pas que les adresses sont simplement des entiers stockés avec un type spécial.

Ensuite, vous déclarez et définissez ippcomme:

int **ipp = &ip1;

alors voici votre souvenir:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

puis, vous modifiez la valeur pointée par l'adresse stockée dans ipp, qui est l'adresse stockée dans ip1:

*ipp = ip2;

la mémoire du programme est

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: comme int*c'est un type spécial, je préfère toujours éviter de déclarer plusieurs pointeurs sur la même ligne, car je pense que la notation int *x;ou int *x, *y;peut être trompeuse. Je préfère écrireint* x; int* y;

HTH

zmo
la source
avec votre exemple, la valeur initiale de ip2ne devrait 3pas être 4.
Dipto
1
oh, je viens de changer la mémoire pour qu'elle corresponde à l'ordre de déclaration. Je suppose que j'ai réglé ça en faisant ça?
zmo
5

Parce que quand tu dis

*ipp = ip2

vous dites «l'objet pointé par ipp» pour indiquer la direction de la mémoire qui ip2pointe.

Vous ne dites pas ippau point ip2.

Diego R. Alcantara
la source
4

Si vous ajoutez l'opérateur de déréférencement *au pointeur, vous redirigez du pointeur vers l'objet pointé.

Exemples:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

Par conséquent:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.
moooeeeep
la source
3

Si vous voulez ipppointer du doigt ip2, vous devez le dire ipp = &ip2;. Cependant, cela laisserait ip1toujours pointer i.

Andrejovich
la source
3

Au tout début que vous définissez,

ipp = &ip1;

Maintenant, déréférencer comme,

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 
Sunil Bojanapally
la source
3

Considérez chaque variable représentée comme ceci:

type  : (name, adress, value)

donc vos variables doivent être représentées comme ceci

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Comme la valeur de ippest &ip1ainsi l'inctruction:

*ipp = ip2;

modifie la valeur de l'addition &ip1à la valeur de ip2, ce qui signifie ip1est modifiée:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Mais ippencore:

(ipp, &ipp, &ip1)

Donc, la valeur de ippstill, &ip1ce qui signifie qu'il pointe toujours ip1.

rullof
la source
1

Parce que vous changez le pointeur de *ipp. Ça veut dire

  1. ipp (nom variable) ---- allez à l'intérieur.
  2. à l'intérieur se ipptrouve l'adresse de ip1.
  3. maintenant *ipp, allez à (adresse de l'intérieur) ip1.

Maintenant nous en sommes ip1. *ipp(ie ip1) = ip2.
ip2contient l'adresse du contenu j.so ip1sera remplacé par contenir de l'ip2 (c'est-à-dire l'adresse de j), NOUS NE CHANGEONS PAS LE ippCONTENU. C'EST TOUT.

user3286725
la source
1

*ipp = ip2; implique:

Affectez ip2à la variable pointée par ipp. Donc c'est équivalent à:

ip1 = ip2;

Si vous souhaitez que l'adresse de ip2soit stockée ipp, faites simplement:

ipp = &ip2;

Maintenant , les ipppoints à ip2.

Rikayan Bandyopadhyay
la source
0

ipppeut contenir la valeur (c'est-à-dire pointer vers) un pointeur vers un objet de type pointeur . Quand tu fais

ipp = &ip2;  

puis le ipp contient l' adresse de la variable (pointeur)ip2 , qui est ( &ip2) de type pointeur vers pointeur . Maintenant, la flèche de la ippdeuxième photo pointera vers ip2.

Wiki dit:
L' *opérateur est un opérateur de déréférence qui opère sur une variable de pointeur, et renvoie une valeur l (variable) équivalente à la valeur à l'adresse du pointeur. C'est ce qu'on appelle le déréférencement du pointeur.

Appliquer l' *opérateur sur le ippdéréfrence à une valeur l de pointeur sur leint type. La valeur l déréférencée*ipp est de type pointeur versint , elle peut contenir l'adresse d'un inttype data. Après la déclaration

ipp = &ip1;

ippcontient l'adresse de ip1et *ippcontient l'adresse de (pointant vers) i. Vous pouvez dire que *ippc'est un alias de ip1. Tous les deux**ipp et *ip1sont des alias pour i.
En faisant

 *ipp = ip2;  

*ippet les ip2deux pointent vers le même emplacement mais ipppointent toujours vers ip1.

En *ipp = ip2;fait, il copie le contenu de ip2(l'adresse de j) vers ip1(comme *ippun alias pour ip1), créant en fait les deux pointeurs ip1et ip2pointant vers le même objet ( j).
Ainsi, dans la deuxième figure, la flèche de ip1et ip2pointe vers jwhile ipppointe toujours vers ip1car aucune modification n'est effectuée pour changer la valeur deipp .

piratages
la source