Was bedeutet "Dereferenzieren" eines Zeigers?

540

Bitte fügen Sie der Erklärung ein Beispiel bei.

asir
quelle
Dies kann Ihnen helfen: stackoverflow.com/questions/2795575/…
Harry Joy
24
int *p;würde einen Zeiger auf eine ganze Zahl definieren und *pdiesen Zeiger dereferenzieren, was bedeutet, dass er tatsächlich die Daten abruft, auf die p zeigt.
Peyman
4
Binky's Pointer Fun ( cslibrary.stanford.edu/104 ) ist ein großartiges Video über Zeiger, die Dinge klären könnten. @ Erik- Du rockst, weil du den Link zur Stanford CS Library erstellt hast. Es gibt so viele Leckereien dort ...
Templatetypedef
6
Harrys Antwort ist das Gegenteil von hilfreich hier.
Jim Balter

Antworten:

731

Überprüfung der grundlegenden Terminologie

Es ist normalerweise gut genug - es sei denn, Sie programmieren Assembly -, sich einen Zeiger vorzustellen, der eine numerische Speicheradresse enthält, wobei 1 auf das zweite Byte im Speicher des Prozesses verweist, 2 auf das dritte, 3 auf das vierte und so weiter.

  • Was ist mit 0 und dem ersten Byte passiert? Nun, wir werden später darauf zurückkommen - siehe Nullzeiger unten.
  • Eine genauere Definition dessen, was Zeiger speichern und wie Speicher und Adressen zusammenhängen, finden Sie am Ende dieser Antwort unter "Weitere Informationen zu Speicheradressen und warum Sie dies wahrscheinlich nicht wissen müssen" .

Wenn Sie auf die Daten / Werte im Speicher zugreifen möchten, auf die der Zeiger zeigt - den Inhalt der Adresse mit diesem numerischen Index -, dereferenzieren Sie den Zeiger.

Verschiedene Computersprachen haben unterschiedliche Notationen, um dem Compiler oder Interpreter mitzuteilen, dass Sie jetzt am (aktuellen) Wert des Objekts interessiert sind - ich konzentriere mich unten auf C und C ++.

Ein Zeigerszenario

Betrachten Sie in C einen Zeiger wie den pfolgenden ...

const char* p = "abc";

... vier Bytes mit den numerischen Werten, die zum Codieren der Buchstaben 'a', 'b', 'c' und eines 0-Bytes zum Bezeichnen des Endes der Textdaten verwendet werden, werden irgendwo im Speicher gespeichert und die numerische Adresse davon Daten werden in gespeichert p. Auf diese Weise codiert C Text im Speicher und wird als ASCIIZ bezeichnet .

Wenn sich das Zeichenfolgenliteral beispielsweise an der Adresse 0x1000 und pein 32-Bit-Zeiger an der Adresse 0x2000 befindet, lautet der Speicherinhalt wie folgt:

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

Beachten Sie, dass es für die Adresse 0x1000 keinen Variablennamen / Bezeichner gibt. Wir können jedoch indirekt auf das Zeichenfolgenliteral verweisen, indem wir einen Zeiger verwenden, in dem die Adresse gespeichert ist : p.

Dereferenzieren des Zeigers

Um auf die Zeichen zu verweisen, auf die pverwiesen wird, dereferenzieren wir pmit einer dieser Notationen (wiederum für 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]

Sie können auch Zeiger durch die Daten verschieben, auf die verwiesen wird, und sie anschließend dereferenzieren:

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

Wenn Sie Daten haben, in die geschrieben werden kann, können Sie Folgendes tun:

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

Oben müssen Sie zur Kompilierungszeit gewusst haben, dass Sie eine aufgerufene Variable benötigen x, und der Code fordert den Compiler auf, festzulegen , wo sie gespeichert werden soll, um sicherzustellen, dass die Adresse über verfügbar ist &x.

Dereferenzieren und Zugreifen auf ein Strukturdatenelement

Wenn Sie in C eine Variable haben, die ein Zeiger auf eine Struktur mit Datenelementen ist, können Sie mit dem ->Dereferenzierungsoperator auf diese Elemente zugreifen :

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_

Multi-Byte-Datentypen

Um einen Zeiger zu verwenden, benötigt ein Computerprogramm auch einen Einblick in den Datentyp, auf den verwiesen wird. Wenn dieser Datentyp mehr als ein Byte zur Darstellung benötigt, zeigt der Zeiger normalerweise auf das Byte mit der niedrigsten Nummer in den Daten.

Betrachten wir also ein etwas komplexeres Beispiel:

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)
++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]

Zeiger auf dynamisch zugewiesenen Speicher

Manchmal wissen Sie nicht, wie viel Speicher Sie benötigen, bis Ihr Programm ausgeführt wird und Sie sehen, welche Daten darauf geworfen werden. Dann können Sie mithilfe von Speicher dynamisch Speicher zuweisen malloc. Es ist üblich, die Adresse in einem Zeiger zu speichern ...

int* p = (int*)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

In C ++ erfolgt die Speicherzuweisung normalerweise mit dem newOperator und die Freigabe mit 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;

Siehe auch C ++ - Smartpointer unten.

Adressen verlieren und verlieren

Oft ist ein Zeiger der einzige Hinweis darauf, wo sich Daten oder Puffer im Speicher befinden. Wenn die fortlaufende Verwendung dieser Daten / Puffer oder die Fähigkeit zum Aufrufen free()oder deleteVermeiden eines Speicherverlusts erforderlich ist, muss der Programmierer eine Kopie des Zeigers bearbeiten ...

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

... oder die Umkehrung von Änderungen sorgfältig orchestrieren ...

const size_t n = ...;
p += n;
...
p -= n;  // Restore earlier value...
free(p);

C ++ Smart Pointer

In C ++ empfiehlt es sich, Smart Pointer- Objekte zum Speichern und Verwalten der Zeiger zu verwenden und diese automatisch freizugeben, wenn die Destruktoren der Smart Pointer ausgeführt werden. Seit C ++ 11 bietet die Standardbibliothek zwei, unique_ptrwenn es einen einzelnen Eigentümer für ein zugewiesenes Objekt gibt ...

{
    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

... und shared_ptrfür den Aktienbesitz (unter Verwendung der Referenzzählung ) ...

{
    auto p = std::make_shared<T>(3.14, "pi");
    number_storage1.may_add(p); // Might copy p into its container
    number_storage2.may_add(p); // Might copy p into its container    } // p's destructor will only delete the T if neither may_add copied it

Nullzeiger

In C NULLund 0- und zusätzlich in C ++ nullptr- kann angegeben werden, dass ein Zeiger derzeit nicht die Speicheradresse einer Variablen enthält und nicht dereferenziert oder in der Zeigerarithmetik verwendet werden sollte. Zum Beispiel:

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

In C und C ++, ebenso wie integrierte numerische Typen standardmäßig nicht unbedingt 0, noch boolszu false, Zeiger sind nicht immer auf NULL. Alle diese sind auf 0 / false / NULL gesetzt , wenn sie staticVariablen oder (nur C ++) direkt oder indirekt Elementvariablen von statischen Objekten oder ihre Basen oder Null Initialisierung durchlaufen (zB new T();und new T(x, y, z);führt Null-Initialisierung auf T-Mitgliedern einschließlich Zeiger, während new T;nicht).

Wenn Sie und einem Zeiger zuweisen 0, werden die Bits im Zeiger nicht unbedingt alle zurückgesetzt: Der Zeiger enthält möglicherweise nicht "0" auf Hardwareebene oder verweist auf die Adresse 0 in Ihrem virtuellen Adressraum. Der Compiler wird zum Speichern von etwas erlaubt sonst da , wenn sie Grund zu sein , hat aber was immer es tut - wenn Sie zusammen kommen und vergleichen Sie den Zeiger auf , , oder ein anderer Zeiger, der alle diejenigen, die Vergleichs muss wie erwartet zugewiesen wurde. Unterhalb des Quellcodes auf Compilerebene ist "NULL" in den Sprachen C und C ++ möglicherweise etwas "magisch" ...NULLnullptr0NULLnullptr

Mehr über Speicheradressen und warum Sie es wahrscheinlich nicht wissen müssen

Streng genommen speichern initialisierte Zeiger ein Bitmuster, das entweder eine NULLoder eine (häufig virtuelle ) Speicheradresse identifiziert .

Der einfache Fall ist, dass dies ein numerischer Versatz im gesamten virtuellen Adressraum des Prozesses ist. In komplexeren Fällen kann der Zeiger relativ zu einem bestimmten Speicherbereich sein, den die CPU basierend auf CPU- "Segment" -Registern oder einer Art von Segment-ID auswählen kann, die im Bitmuster codiert ist, und / oder abhängig von der Maschinencode-Anweisungen unter Verwendung der Adresse.

Beispielsweise kann eine int*ordnungsgemäß initialisierte Datei, die auf eine intVariable verweist, nach dem Umwandeln in einen float*Zugriffsspeicher im "GPU" -Speicher ganz anders sein als der Speicher, in dem sich die intVariable befindet, und nach dem Umwandeln und Verwenden als Funktionszeiger möglicherweise weiter verweisen Opcodes für bestimmte Speicher-Haltemaschinen für das Programm (mit dem numerischen Wert des int*effektiv zufälligen, ungültigen Zeigers innerhalb dieser anderen Speicherbereiche).

3GL-Programmiersprachen wie C und C ++ neigen dazu, diese Komplexität zu verbergen, so dass:

  • Wenn der Compiler Ihnen einen Zeiger auf eine Variable oder Funktion gibt, können Sie diese frei dereferenzieren (solange die Variable nicht zerstört / freigegeben wurde) und es ist das Problem des Compilers, ob z. B. ein bestimmtes CPU-Segmentregister im Voraus wiederhergestellt werden muss oder a Es wird eine bestimmte Anweisung für den Maschinencode verwendet

  • Wenn Sie einen Zeiger auf ein Element in einem Array erhalten, können Sie mithilfe der Zeigerarithmetik eine beliebige Stelle im Array verschieben oder sogar eine Adresse nach dem Ende des Arrays bilden, die mit anderen Zeigern auf Elemente verglichen werden kann im Array (oder die in ähnlicher Weise durch Zeigerarithmetik auf denselben Wert nach dem Ende verschoben wurden); Auch in C und C ++ muss der Compiler sicherstellen, dass dies "einfach funktioniert".

  • Bestimmte Betriebssystemfunktionen, z. B. die Zuordnung von gemeinsam genutztem Speicher, können Ihnen Zeiger geben, und sie funktionieren "nur" innerhalb des für sie sinnvollen Adressbereichs

  • Versuche, legale Zeiger über diese Grenzen hinaus zu verschieben oder beliebige Zahlen in Zeiger umzuwandeln oder Zeiger zu verwenden, die in nicht verwandte Typen umgewandelt wurden, weisen normalerweise ein undefiniertes Verhalten auf . Sie sollten daher in Bibliotheken und Anwendungen höherer Ebenen vermieden werden, aber Code für Betriebssysteme, Gerätetreiber usw. Möglicherweise müssen Sie sich auf ein Verhalten verlassen, das vom C- oder C ++ - Standard nicht definiert wurde und das dennoch durch die spezifische Implementierung oder Hardware gut definiert ist.

Tony Delroy
quelle
ist p[1] und *(p + 1) identisch ? Das heißt, erzeugt p[1] und *(p + 1)generiert die gleichen Anweisungen?
Pacerier
2
@Pacerier: ab 6.5.2.1/2 im N1570-Entwurf C-Standard (zuerst online gefunden) "Die Definition des Indexoperators [] lautet, dass E1 [E2] identisch ist mit (* ((E1) + (E2))" ). " - Ich kann mir keinen Grund vorstellen, warum ein Compiler sie in einem frühen Stadium der Kompilierung nicht sofort in identische Darstellungen konvertieren würde und danach dieselben Optimierungen anwendet, aber ich sehe nicht, wie jemand definitiv beweisen kann, dass der Code identisch ist ohne jeden jemals geschriebenen Compiler zu überblicken.
Tony Delroy
3
@Honey: Der Wert 1000 hex ist zu groß, um in einem einzelnen Byte (8 Bit) Speicher zu codieren: Sie können nur vorzeichenlose Zahlen von 0 bis 255 in einem Byte speichern. Sie können also einfach nicht 1000 hex bei "nur" der Adresse 2000 speichern. Stattdessen würde ein 32-Bit-System 32 Bit - das sind vier Bytes - mit Adressen von 2000 bis 2003 verwenden. Ein 64-Bit-System würde 64 verwenden Bits - 8 Bytes - von 2000 bis 2007. In beiden Fällen plautet die Basisadresse von nur 2000: Wenn Sie einen anderen Zeiger pdarauf hätten, müssten 2000 in seinen vier oder acht Bytes gespeichert werden. Ich hoffe, das hilft! Prost.
Tony Delroy
1
@TonyDelroy: Wenn eine Union uein Array enthält arr, erkennen sowohl gcc als auch clang, dass der lvalue u.arr[i]möglicherweise auf denselben Speicher wie andere Union-Mitglieder zugreift, erkennen jedoch nicht, dass lvalue dies *(u.arr+i)möglicherweise tut. Ich bin nicht sicher, ob die Autoren dieser Compiler der Meinung sind, dass der letztere UB aufruft oder dass der erstere UB aufruft, aber sie sollten es trotzdem sinnvoll verarbeiten, aber sie sehen die beiden Ausdrücke eindeutig als unterschiedlich an.
Supercat
3
Ich habe selten Zeiger und deren Verwendung in C / C ++ gesehen, die so kurz und einfach erklärt wurden.
KayleeFrye_onDeck
102

Das Dereferenzieren eines Zeigers bedeutet, dass der Wert abgerufen wird, der an dem Speicherort gespeichert ist, auf den der Zeiger zeigt. Der Operator * wird dazu verwendet und als Dereferenzierungsoperator bezeichnet.

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.
Mahesh
quelle
15
Ein Zeiger zeigt nicht auf einen Wert , sondern auf ein Objekt .
Keith Thompson
51
@KeithThompson Ein Zeiger zeigt nicht auf ein Objekt, sondern auf eine Speicheradresse, in der sich ein Objekt (möglicherweise ein Grundelement) befindet.
mg30rg
4
@ mg30rg: Ich bin mir nicht sicher, welchen Unterschied du machst. Ein Zeigerwert ist eine Adresse. Ein Objekt ist per Definition ein "Bereich der Datenspeicherung in der Ausführungsumgebung, dessen Inhalt Werte darstellen kann". Und was meinst du mit "primitiv"? Der C-Standard verwendet diesen Begriff nicht.
Keith Thompson
6
@KeithThompson Ich habe kaum darauf hingewiesen, dass Sie der Antwort keinen Mehrwert hinzugefügt haben, sondern nur die Terminologie ausgewählt haben (und das auch falsch gemacht haben). Der Zeigerwert ist sicherlich eine Adresse, so "zeigt" er auf eine Speicheradresse. Das Wort "Objekt" in unserer OOP-gesteuerten Welt kann irreführend sein, da es als "Klasseninstanz" interpretiert werden kann (ja, mir war nicht bewusst, dass die Frage mit [C] und nicht mit [C ++] gekennzeichnet ist), und ich habe das Wort verwendet "primitiv" wie im Gegenteil von "copmlex" (Datenstruktur wie eine Struktur oder Klasse).
mg30rg
3
Lassen Sie mich zu dieser Antwort hinzufügen, dass der Array-Indexoperator []auch einen Zeiger dereferenziert ( a[b]definiert als bedeutet *(a + b)).
cmaster - wieder Monica
20

Ein Zeiger ist eine "Referenz" auf einen Wert. Ähnlich wie eine Bibliotheksrufnummer eine Referenz auf ein Buch ist. Durch "Dereferenzieren" der Rufnummer wird das Buch physisch durchlaufen und abgerufen.

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.. 

Wenn das Buch nicht da ist, fängt der Bibliothekar an zu schreien, schließt die Bibliothek und ein paar Leute sind bereit, die Ursache zu untersuchen, warum eine Person ein Buch findet, das nicht da ist.

Bobobobo
quelle
17

In einfachen Worten bedeutet Dereferenzierung den Zugriff auf den Wert von einem bestimmten Speicherort aus, auf den dieser Zeiger zeigt.

Fahad Naeem
quelle
7

Code und Erklärung aus den Zeigergrundlagen :

Die Dereferenzierungsoperation beginnt am Zeiger und folgt seinem Pfeil, um auf seinen Zeiger zuzugreifen. Das Ziel kann sein, den Spitzenstatus zu betrachten oder den Spitzenstatus zu ändern. Die Dereferenzierungsoperation für einen Zeiger funktioniert nur, wenn der Zeiger einen Pointee hat - der Pointee muss zugewiesen und der Zeiger muss so eingestellt sein, dass er darauf zeigt. Der häufigste Fehler im Zeigercode ist das Vergessen, den Pointee einzurichten. Der häufigste Laufzeitabsturz aufgrund dieses Fehlers im Code ist eine fehlgeschlagene Dereferenzierungsoperation. In Java wird die falsche Dereferenzierung vom Laufzeitsystem höflich gekennzeichnet. In kompilierten Sprachen wie C, C ++ und Pascal stürzt die falsche Dereferenzierung manchmal ab und manchmal beschädigt sie den Speicher auf subtile, zufällige Weise.

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   
}
atp
quelle
Sie müssen tatsächlich Speicher zuweisen, auf den x zeigen soll. Ihr Beispiel hat ein undefiniertes Verhalten.
Peyman
3

Ich denke, alle vorherigen Antworten sind falsch, da sie besagen, dass Dereferenzierung den Zugriff auf den tatsächlichen Wert bedeutet. Wikipedia gibt stattdessen die richtige Definition an: https://en.wikipedia.org/wiki/Dereference_operator

Es arbeitet mit einer Zeigervariablen und gibt einen l-Wert zurück, der dem Wert an der Zeigeradresse entspricht. Dies wird als "Dereferenzieren" des Zeigers bezeichnet.

Das heißt, wir können den Zeiger dereferenzieren, ohne jemals auf den Wert zuzugreifen, auf den er zeigt. Zum Beispiel:

char *p = NULL;
*p;

Wir haben den NULL-Zeiger dereferenziert, ohne auf seinen Wert zuzugreifen. Oder wir könnten tun:

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

Wieder Dereferenzierung, aber niemals Zugriff auf den Wert. Ein solcher Code stürzt NICHT ab: Der Absturz tritt auf, wenn Sie tatsächlich mit einem ungültigen Zeiger auf die Daten zugreifen . Leider ist das Dereferenzieren eines ungültigen Zeigers gemäß dem Standard ein undefiniertes Verhalten (mit wenigen Ausnahmen), selbst wenn Sie nicht versuchen, die tatsächlichen Daten zu berühren.

Kurz gesagt: Dereferenzieren des Zeigers bedeutet, den Dereferenzierungsoperator darauf anzuwenden. Dieser Operator gibt nur einen l-Wert für Ihre zukünftige Verwendung zurück.

stsp
quelle
Nun, Sie haben einen NULL-Zeiger dereferenziert, was zu einem Segmentierungsfehler führen würde.
Arjun Gaur
Darüber hinaus haben Sie nach "Dereferenzierungsoperator" und nicht nach "Dereferenzierung eines Zeigers" gesucht, was tatsächlich bedeutet, den Wert abzurufen / auf einen Wert an einem Speicherort zuzugreifen, auf den ein Zeiger zeigt.
Arjun Gaur
Hast du es versucht? Ich tat. Folgendes stürzt nicht ab: `#include <stdlib.h> int main () {char * p = NULL; * p; return 0; } `
stsp
1
@stsp Wenn der Code jetzt nicht abstürzt, bedeutet dies nicht, dass er in Zukunft oder auf einem anderen System nicht mehr funktioniert.
1
*p;verursacht undefiniertes Verhalten. Obwohl Sie Recht haben, dass die Dereferenzierung nicht auf den Wert an sich zugreift , *p; greift der Code auf den Wert zu.
MM