Zeiger auf Zeigerklärung

142

Ich habe diesem Tutorial gefolgt, wie ein Zeiger auf einen Zeiger funktioniert funktioniert.

Lassen Sie mich die relevante Passage zitieren:


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

Jetzt können wir einstellen

    int **ipp = &ip1;

und ippzeigt auf ip1welche zeigt auf i. *ippist ip1, und **ippist ioder 5. Wir können die Situation mit unserer bekannten Box-Pfeil-Notation wie folgt veranschaulichen:

Geben Sie hier die Bildbeschreibung ein

Wenn dann sagen wir

    *ipp = ip2;

Wir haben den Zeiger, auf den ipp(das heißt ip1) zeigt ip2, so geändert , dass er eine Kopie von enthält , sodass er ( ip1) jetzt auf Folgendes zeigt j:

Geben Sie hier die Bildbeschreibung ein


Meine Frage ist: Warum zeigt im zweiten Bild immer ippnoch, ip1aber nicht ip2?

Blake
quelle
6
Bitte fügen Sie den Typ ippbei der Definition hinzu, damit Ihre Frage vollständig ist ;-)
zmo
1
Da ippzu zeigen , ip1so *ipp = ip2ist die gleiche wieip1 = ip2
Grijesh Chauhan
1
Können wir bitte aufhören, Sternchen zu schreiben, die so weit vom spitzen Typ entfernt sind? int **ippist weit weniger intuitiv als int** ipp, insbesondere wenn es sich um **ippeine völlig andere Sache außerhalb von Typdeklarationen handelt.
Darkhogg
4
@ Darkhogg int **ippscheint mir ziemlich intuitiv zu sein. Es bedeutet, ich mache **ippeine int. Was wahr ist.
Ithisa
2
@ user54609 Bedeutungen und persönliche Intuitivität abgesehen, bedenken Sie Folgendes: Die Art von ippist int**, also schreiben Sie einfach int**statt magischer " impDereferenzierung ist ein int" Verständnis .
Manu343726

Antworten:

143

Vergessen Sie für eine Sekunde die Zeigeanalogie. Was ein Zeiger wirklich enthält, ist eine Speicheradresse. Dies &ist der Operator "Adresse von", dh er gibt die Adresse im Speicher eines Objekts zurück. Der *Operator gibt Ihnen das Objekt an, auf das sich ein Zeiger bezieht, dh wenn ein Zeiger eine Adresse enthält, gibt er das Objekt an dieser Speicheradresse zurück. Wenn Sie dies tun *ipp = ip2, rufen Sie *ippdas Objekt an der Adresse ab, in ippder es sich befindet, ip1und weisen es dann ip1dem Wert zu, in ip2dem sich die Adresse befindet j.

Einfach
& -> Adresse von
*-> Wert bei

Robert S. Barnes
quelle
14
& und * waren noch nie so einfach
Ray
7
Ich glaube, die Hauptursache für Verwirrung ist die Mehrdeutigkeit des Operators *, der während der Variablendeklaration verwendet wird, um anzuzeigen, dass die Variable tatsächlich ein Zeiger auf einen bestimmten Datentyp ist. Andererseits wird es auch in Anweisungen verwendet, um auf den Inhalt der Variablen zuzugreifen, auf die ein Zeiger zeigt (Dereferenzierungsoperator).
Lucas A.
43

Weil Sie den Wert geändert haben, auf den ippnicht der Wert von zeigt ipp. Zeigt also ippimmer noch auf ip1(den Wert von ipp), ip1ist der Wert jetzt der gleiche wie ip2der Wert von, also zeigen beide auf j.

Dies:

*ipp = ip2;

ist das gleiche wie:

ip1 = ip2;
Skizz
quelle
11
Es kann sinnvoll sein, auf den Unterschied zwischen int *ip1 = &iund hinzuweisen *ipp = ip2;, dh wenn Sie die intAnweisung aus der ersten Anweisung entfernen, sehen die Zuweisungen sehr ähnlich aus, aber *in beiden Fällen geschieht etwas ganz anderes.
Crowman
22

Wie die meisten Anfängerfragen im C-Tag kann diese Frage beantwortet werden, indem auf die ersten Prinzipien zurückgegriffen wird:

  • Ein Zeiger ist eine Art Wert.
  • Eine Variable enthält einen Wert.
  • Der &Operator verwandelt eine Variable in einen Zeiger.
  • Der *Operator verwandelt einen Zeiger in eine Variable.

(Technisch sollte ich "lvalue" anstelle von "variable" sagen, aber ich denke, es ist klarer, veränderbare Speicherorte als "Variablen" zu beschreiben.)

Wir haben also Variablen:

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

Variable ip1 enthält einen Zeiger. Der &Operator verwandelt sich iin einen Zeiger und dieser Zeigerwert wird zugewiesen ip1. So ip1 enthält einen Zeiger aufi .

Variable ip2 enthält einen Zeiger. Der &Operator verwandelt sich jin einen Zeiger und dieser Zeiger wird zugewiesen ip2. So ip2 enthält einen Zeiger auf j.

int **ipp = &ip1;

Variable ippenthält einen Zeiger. Der &Operator verwandelt eine Variable ip1in einen Zeiger und dieser Zeigerwert wird zugewiesen ipp. So ippenthält einen Zeiger aufip1 .

Fassen wir die bisherige Geschichte zusammen:

  • i enthält 5
  • j enthält 6
  • ip1 enthält "Zeiger auf i"
  • ip2 enthält "Zeiger auf j"
  • ippenthält "Zeiger auf ip1"

Jetzt sagen wir

*ipp = ip2;

Der *Operator wandelt einen Zeiger wieder in eine Variable um. Wir holen den Wert von ipp, der "Zeiger auf" ist, ip1und verwandeln ihn in eine Variable. Welche Variable? ip1Natürlich!

Daher ist dies einfach eine andere Art zu sagen

ip1 = ip2;

Also holen wir den Wert von ip2. Was ist es? "Zeiger auf j". Wir weisen diesen Zeigerwert zu ip1, also ip1ist er jetzt "Zeiger auf j".

Wir haben nur eines geändert: den Wert von ip1:

  • i enthält 5
  • j enthält 6
  • ip1enthält "Zeiger auf j"
  • ip2enthält "Zeiger auf j"
  • ippenthält "Zeiger auf ip1"

Warum zeigt ippimmer noch auf ip1und nicht ip2?

Eine Variable ändert sich, wenn Sie sie zuweisen. Zähle die Aufgaben; Es kann nicht mehr Änderungen an Variablen geben als Zuweisungen! Sie beginnen mit der Zuordnung zu i, j, ip1, ip2und ipp. Sie weisen dann zu *ipp, was, wie wir gesehen haben, dasselbe bedeutet wie "zuweisen zu ip1". Da Sie kein ippzweites Mal zugewiesen haben, hat sich nichts geändert!

Wenn Sie sich ändern ippmöchten, müssen Sie tatsächlich Folgendes zuweisen ipp:

ipp = &ip2;

zum Beispiel.

Eric Lippert
quelle
21

Ich hoffe, dieser Code kann helfen.

#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);
}

es gibt aus:

Geben Sie hier die Bildbeschreibung ein

michaeltang
quelle
12

Meine ganz persönliche Meinung ist, dass Bilder mit Pfeilen in diese Richtung zeigen oder dass Zeiger schwerer zu verstehen sind. Es lässt sie wie abstrakte, mysteriöse Wesen erscheinen. Sie sind nicht.

Wie alles in Ihrem Computer sind Zeiger Zahlen . Der Name "Zeiger" ist nur eine ausgefallene Art zu sagen "eine Variable, die eine Adresse enthält".

Lassen Sie mich deshalb die Dinge aufrühren, indem ich erkläre, wie ein Computer tatsächlich funktioniert.

Wir haben eine int, es hat den Namen iund den Wert 5. Dies ist im Speicher gespeichert. Wie alles, was im Speicher gespeichert ist, benötigt es eine Adresse, sonst könnten wir sie nicht finden. Nehmen wir an, er ilandet an der Adresse 0x12345678 und sein Freund jmit dem Wert 6 endet direkt danach. Angenommen, eine 32-Bit-CPU mit int 4 Bytes und Zeigern 4 Bytes, werden die Variablen wie folgt im physischen Speicher gespeichert:

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

Nun wollen wir auf diese Variablen zeigen. Wir erstellen einen Zeiger auf int ,, int* ip1und einen int* ip2. Wie alles im Computer werden auch diese Zeigervariablen irgendwo im Speicher zugeordnet. Nehmen wir an, sie landen unmittelbar danach an den nächsten benachbarten Adressen im Speicher j. Wir setzen die Zeiger so, dass sie die Adressen der zuvor zugewiesenen Variablen enthalten: ip1=&i;("kopiere die Adresse von i in ip1") und ip2=&j. Was zwischen den Zeilen passiert, ist:

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)

Was wir also bekamen, waren noch einige 4-Byte-Speicherblöcke, die Zahlen enthielten. Es sind keine mystischen oder magischen Pfeile in Sicht.

Wenn wir uns nur einen Speicherauszug ansehen, können wir nicht sagen, ob die Adresse 0x12345680 ein intoder enthältint* . Der Unterschied besteht darin, wie unser Programm die unter dieser Adresse gespeicherten Inhalte verwendet. (Die Aufgabe unseres Programms besteht eigentlich nur darin, der CPU mitzuteilen, was mit diesen Zahlen zu tun ist.)

Dann fügen wir noch eine weitere Indirektionsebene mit hinzu int** ipp = &ip1;. Wieder bekommen wir nur einen Teil der Erinnerung:

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

Das Muster kommt mir bekannt vor. Noch ein Teil von 4 Bytes, der eine Zahl enthält.

Wenn wir nun einen Speicherauszug des obigen fiktiven kleinen RAM hätten, könnten wir manuell überprüfen, wohin diese Zeiger zeigen. Wir schauen uns an, was unter der Adresse der ippVariablen gespeichert ist, und finden den Inhalt 0x12345680. Welches ist natürlich die Adresse, wo ip1gespeichert wird. Wir können zu dieser Adresse gehen, den Inhalt dort überprüfen und die Adresse von finden i, und schließlich können wir zu dieser Adresse gehen und die Nummer 5 finden.

Wenn wir also den Inhalt von ipp nehmen, erhalten *ippwir die Adresse der Zeigervariablen ip1. Durch das Schreiben *ipp=ip2kopieren wir ip2 in ip1, es entspricht ip1=ip2. In jedem Fall würden wir bekommen

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

(Diese Beispiele wurden für eine Big-Endian-CPU angegeben.)

Lundin
quelle
5
Obwohl ich Ihren Standpunkt verstehe, ist es wertvoll, Zeiger als abstrakte, mysteriöse Einheiten zu betrachten. Jede bestimmte Implementierung von Zeigern besteht nur aus Zahlen, aber die von Ihnen skizzierte Implementierungsstrategie ist keine Voraussetzung für eine Implementierung, sondern nur eine gemeinsame Strategie. Zeiger müssen nicht die gleiche Größe wie ein int haben, Zeiger müssen keine Adressen in einem flachen virtuellen Speichermodell sein und so weiter. Dies sind lediglich Implementierungsdetails.
Eric Lippert
@EricLippert Ich denke, man kann dieses Beispiel abstrakter machen, indem man keine tatsächlichen Speicheradressen oder Datenblöcke verwendet. Wenn es sich um eine Tabelle handelte, location, value, variablein der 1,2,3,4,5angegeben wurde A,1,B,C,3, wo sich Position und Wert befanden, konnte die entsprechende Idee von Zeigern leicht erklärt werden, ohne dass Pfeile verwendet wurden, die von Natur aus verwirrend sind. Unabhängig von der gewählten Implementierung ist an einer bestimmten Stelle ein Wert vorhanden, und dies ist ein Teil des Puzzles, der beim Modellieren mit Pfeilen verschleiert wird.
MirroredFate
@EricLippert Nach meiner Erfahrung sind die meisten potenziellen C-Programmierer, die Probleme haben, Zeiger zu verstehen, diejenigen, denen abstrakte, künstliche Modelle zugeführt wurden. Abstraktion ist nicht hilfreich, da der gesamte Zweck der heutigen C-Sprache darin besteht, dass sie nahe an der Hardware liegt. Wenn Sie C lernen, aber keinen Code in der Nähe der Hardware schreiben möchten, verschwenden Sie Ihre Zeit . Java usw. ist eine viel bessere Wahl, wenn Sie nicht wissen möchten, wie Computer funktionieren, sondern nur High-Level-Programmierung durchführen möchten.
Lundin
@EricLippert Und ja, es können verschiedene obskure Implementierungen von Zeigern existieren, bei denen die Zeiger nicht unbedingt Adressen entsprechen. Aber das Zeichnen von Pfeilen hilft Ihnen auch nicht zu verstehen, wie diese funktionieren. Irgendwann muss man das abstrakte Denken verlassen und sich auf die Hardware-Ebene begeben, sonst sollte man nicht C verwenden. Es gibt viele weitaus geeignetere, moderne Sprachen, die für die rein abstrakte Programmierung auf hoher Ebene gedacht sind.
Lundin
@Lundin: Ich bin auch kein großer Fan von Pfeildiagrammen; Die Vorstellung eines Pfeils als Daten ist schwierig. Ich denke lieber abstrakt darüber nach, aber ohne Pfeile. Der &Operator für eine Variable gibt Ihnen eine Münze, die diese Variable darstellt. Der *Operator auf dieser Münze gibt Ihnen die Variable zurück. Keine Pfeile erforderlich!
Eric Lippert
8

Beachten Sie die Aufgaben:

ipp = &ip1;

Ergebnisse ippzu zeigen ip1.

Um ippdarauf hinzuweisen ip2, sollten wir uns auf ähnliche Weise ändern:

ipp = &ip2;

was wir eindeutig nicht tun. Stattdessen ändern wir den Wert an der Adresse, auf die verwiesen wird ipp.
Durch das Folgende

*ipp = ip2;

Wir ersetzen nur den in gespeicherten Wert ip1.

ipp = &ip1bedeutet *ipp = ip1 = &i,
jetzt , *ipp = ip2 = &j.
Also, *ipp = ip2ist im Wesentlichen das gleiche wie ip1 = ip2.

Dipto
quelle
5
ipp = &ip1;

Keine spätere Zuordnung hat den Wert von geändert ipp. Deshalb weist es immer noch darauf hin ip1.

Was Sie damit tun *ipp, dh mit ip1, ändert nichts an der Tatsache, dass ippdarauf hingewiesen wird ip1.

Daniel Daranas
quelle
5

Meine Frage ist: Warum zeigt ipp im zweiten Bild immer noch auf ip1, aber nicht auf ip2?

Wenn du schöne Bilder platziert hast, werde ich versuchen, schöne ASCII-Kunst zu machen:

Wie @ Robert-S-Barnes in seiner Antwort sagte: Vergessen Sie Zeiger und was auf was hinweist, aber denken Sie in Bezug auf das Gedächtnis. Grundsätzlich int*bedeutet ein , dass es die Adresse einer Variablen int**enthält und ein die Adresse einer Variablen enthält, die die Adresse einer Variablen enthält. Dann können Sie die Algebra des Zeigers verwenden, um auf die Werte oder Adressen zuzugreifen: &foomeans address of foound *foomeans value of the address contained in foo.

Da es bei Zeigern um den Umgang mit dem Gedächtnis geht, besteht der beste Weg, dies tatsächlich "greifbar" zu machen, darin, zu zeigen, was die Zeigeralgebra mit dem Gedächtnis tut.

Hier ist also der Speicher Ihres Programms (vereinfacht für den Zweck des Beispiels):

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

wenn Sie Ihren Anfangscode machen:

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

So sieht dein Gedächtnis aus:

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

dort können Sie sehen , ip1und ip2die Adressen bekommen iund jundipp immer noch nicht existiert. Vergessen Sie nicht, dass Adressen einfach Ganzzahlen sind, die mit einem speziellen Typ gespeichert sind.

Dann deklarieren und definieren Sie ippwie:

int **ipp = &ip1;

Also hier ist deine Erinnerung:

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

und dann ändern Sie den Wert, auf den die Adresse zeigt, in ippder die Adresse gespeichert ist ip1:

*ipp = ip2;

Der Speicher des Programms ist

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

NB: int*Da es sich um einen speziellen Typ handelt, ziehe ich es vor, immer zu vermeiden, mehrere Zeiger in derselben Zeile zu deklarieren, wie ich das int *x;oder denkeint *x, *y; Notation irreführend sein kann. Ich schreibe lieberint* x; int* y;

HTH

zmo
quelle
mit Ihrem Beispiel Anfangswert ip2soll 3nicht 4.
Dipto
1
Oh, ich habe gerade den Speicher so geändert, dass er der Reihenfolge der Deklaration entspricht. Ich denke, ich habe das behoben?
Zmo
5

Denn wenn du sagst

*ipp = ip2

Sie sagen das 'Objekt, auf das gezeigt wird ipp', um die Richtung des Gedächtnisses anzuzeigen, ip2das zeigt.

Du sagst nicht ippzu zeigen ip2.

Diego R. Alcantara
quelle
4

Wenn Sie den Dereferenzierungsoperator hinzufügen * dem Zeiger , leiten Sie vom Zeiger zum Objekt weiter, auf das gezeigt wird.

Beispiele:

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`

Deshalb:

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

Wenn Sie wollen , würde ippzu zeigen ip2, würden Sie zu sagen haben ipp = &ip2;. Dies würde jedoch ip1immer noch darauf hinweisen i.

Andrejovich
quelle
3

Ganz am Anfang hast du gesetzt,

ipp = &ip1;

Jetzt dereferenziere es als,

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

Betrachten Sie jede Variable wie folgt dargestellt:

type  : (name, adress, value)

Ihre Variablen sollten also so dargestellt werden

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

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

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

Wie der Wert von ippist &ip1so die Anweisung:

*ipp = ip2;

ändert den Wert an der Addess &ip1in den Wert von ip2, was bedeutet, dass ip1geändert wird:

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

Aber ipptrotzdem:

(ipp, &ipp, &ip1)

Der Wert von ippstill zeigt &ip1also immer noch auf ip1.

rullof
quelle
1

Weil Sie den Zeiger von ändern *ipp. Es bedeutet

  1. ipp (variabler Name) ---- geh rein.
  2. innen ippist die Adresse vonip1 .
  3. jetzt *ippgeh also zu (adresse von innen) ip1.

Jetzt sind wir bei ip1. *ipp(ie ip1) = ip2.
ip2Adresse von. jenthalten ip1Inhalt wird durch Inhalt von ip2 (dh Adresse von j) ersetzt, WIR ÄNDERN DEN INHALT NICHT ipp. DAS IST ES.

3286725
quelle
1

*ipp = ip2; impliziert:

Weisen Sie ip2der Variablen zu, auf die von zeigt ipp. Das entspricht also:

ip1 = ip2;

Wenn Sie möchten, dass die Adresse von ip2gespeichert wird ipp, gehen Sie einfach wie folgt vor:

ipp = &ip2;

Zeigt jetzt ippauf ip2.

Rikayan Bandyopadhyay
quelle
0

ippkann den Wert eines Zeigers auf ein Objekt vom Zeigertyp enthalten (dh auf einen Zeiger zeigen) . Wenn Sie das tun

ipp = &ip2;  

dann ippenthält das die Adresse der Variablen (Zeiger)ip2 , die &ip2vom Typ Zeiger auf Zeiger ist ( ) . Jetzt zeigt der Pfeil ippim zweiten Bild aufip2 .

Wiki sagt:
Der *Operator ist ein Dereferenzierungsoperator, der mit einer Zeigervariablen arbeitet und einen l-Wert zurückgibt (Variable) , der dem Wert an der Zeigeradresse entspricht. Dies wird als Dereferenzierung des Zeigers bezeichnet.

Anwenden des *Operators auf ippDereferenzierung auf einen l-Wert des Zeigers auf denint Typ. Der dereferenzierte l-Wert *ippist vom Typ Zeiger aufint , er kann die Adresse eines intTyps Daten enthalten. Nach der Aussage

ipp = &ip1;

ipphält die Adresse von ip1und *ipphält die Adresse von (zeigt auf) i. Man kann sagen, das *ippist ein Alias ​​von ip1. Beide **ippund *ip1sind Alias ​​für i.
Indem ich es tue

 *ipp = ip2;  

*ippund ip2beide zeigen auf dieselbe Position, zeigen aber ippimmer noch auf ip1.

Was *ipp = ip2;tatsächlich bedeutet, ist, dass es den Inhalt von ip2(die Adresse von j) nach kopiert ip1(wie *ippes ein Alias ​​für ist ip1), wodurch tatsächlich beide Zeiger erstellt werden ip1und ip2auf dasselbe Objekt gezeigt wird ( j).
In der zweiten Abbildung zeigt der Pfeil von ip1und ip2auf, jwährend er ippnoch zeigt, ip1da keine Änderung vorgenommen wird, um den Wert von zu ändernipp .

Haccks
quelle