Wie C-Zeiger tatsächlich funktionieren: Speicheradressen erklärt

6

Die meisten Entwickler behandeln den Speicher wie eine Blackbox. Sie gehen davon aus, dass Variablen nur Namen sind, die mit Werten verknüpft sind. Das funktioniert so lange, bis Sie Code schreiben müssen, der Daten direkt auf Maschinenebene manipuliert. Dann treffen Sie Zeiger.

Eine normale Variable ist einfach. Es handelt sich um einen Speicherblock mit einem Namen, den Sie in Ihrem Code verwenden. Deklarieren Sie eine Ganzzahl i, und der Compiler reserviert vier Bytes. Du siehst ich. Das Gerät sieht eine Adresse. Der Wert befindet sich in diesen vier Bytes.

Zeiger brechen dieses einfache Modell.

Ein Zeiger enthält keinen Wert. Es enthält eine Adresse. Es zeigt auf eine andere Variable. Insbesondere wird der Speicherort dieser Variablen gespeichert. Diese Unterscheidung ist entscheidend. Wenn Sie die beiden verwechseln, stürzt Ihr Programm ab. Hart.

Der Aufbau ist zweischichtig. Erstens gibt es die Zeigervariable selbst. Es enthält eine numerische Adresse. Zweitens gibt es die Zielvariable. Die Adresse im Zeiger führt Sie zu den eigentlichen Daten. Sobald Sie diese Trennung verstanden haben, erschließen Sie erhebliche Möglichkeiten in der Systemprogrammierung.

Betrachten Sie dieses Standard-C-Beispiel:

„c

int main() {
int i, j;
int p; / ein Zeiger auf eine Ganzzahl */

p = &i;
*p = 5;
j = ich;

printf(“%d %d %d\n”, i, j, *p);
0 zurückgeben;
}

Die erste Zeile deklariert zwei Standard-Ganzzahlen, i und j. Die nächste Zeile, „int p“, deklariert einen Zeiger. Der Stern ist der Schlüssel. Es teilt dem Compiler mit, dass p * keine Ganzzahl ist. Es ist ein Zeiger auf eine Ganzzahl. Sie können dies mit Floats, Strukturen, Zeichen oder allem anderen tun. Die Syntax bleibt gleich.

Dann kommt der Teil, der Anfänger aus der Fassung bringt: „p = &i;“.

In C ist das kaufmännische Und-Zeichen & der Adressoperator. „&i“ gibt nicht den Wert von i zurück. Es gibt die Speicheradresse zurück, an der i lebt. Die Zuweisung „p = &i“ kopiert diese Adresse in den Zeiger p. Jetzt zeigt p auf i.

Wenn Sie diesen Schritt überspringen, enthält p Müll. Es enthält eine zufällige, undefinierte Adresse. Eine spätere Dereferenzierung führt wahrscheinlich zu einem Segmentierungsfehler. Das Programm stürzt ab, weil Sie es angewiesen haben, auf Speicher zuzugreifen, der ihm nicht gehört.

Visualisierung hilft. Stellen Sie sich die Erinnerung als ein Gitter vor.

Vor der Initialisierung sind i, j und p vorhanden, enthalten jedoch unbekannte Werte. Die ganzen Zahlen sind Kästchen mit Fragezeichen. Sie könnten alles enthalten. Der Zeiger p ist ein Kreis. Es hat Pfeile, die in zufällige Richtungen zeigen. Es weiß noch nicht, wohin es gehen soll.

Nachdem Sie „p = &i“ ausgeführt haben, ändern sich die Pfeile. Sie alle weisen direkt auf i hin. p weiß jetzt genau, wo i wohnt.

Der Zusammenhang ist klar. p enthält den Standort. i enthält die Daten. Sie verwenden p, um i zu erreichen.

Mit diesem Mechanismus verwaltet C den Speicher effizient. Es ermöglicht Funktionen, als Referenz übergebene Variablen zu ändern. Es ermöglicht eine dynamische Speicherzuweisung. Damit können Sie komplexe Datenstrukturen wie verknüpfte Listen und Bäume erstellen.

Aber es birgt auch Risiken. Eine falsche Adresse und Sie lesen oder schreiben ins Leere.

Das Sternchen in „p = 5“ macht etwas anderes als das in „int p“. Das zweite Sternchen war Teil der Typdefinition. Die Variable wurde als Zeiger deklariert. Das erste Sternchen ist der Dereferenzierungsoperator. Es bedeutet „gehen Sie zu der in p gespeicherten Adresse und ändern Sie dort den Wert.“

p = 5“ fügt also die Zahl 5 an den Speicherort ein, auf den p zeigt. Da p auf i zeigt, wird i zu 5. Der Variablen j wird dann der Wert von i * zugewiesen. Die Ausgabe gibt 5, 5 und 5 aus. Alle drei spiegeln die gleiche zugrunde liegende Änderung wider.

Um zu verstehen, wie C-Zeiger funktionieren, müssen Sie Ihre Denkweise ändern. Hören Sie auf, Variablen als Container zu betrachten. Betrachten Sie sie als Standorte. Der Zeiger ist die Karte. Die Variable ist das Ziel. Wenn die Karte falsch ist, ist man am Ende verloren. Und bei der Low-Level-Programmierung ist es teuer, verloren zu gehen.

Der Indirektionsoperator erklärt

Wenn Sie „p“ die Adresse „i“ zuweisen, geben Sie diesem einzelnen Speicherblock im Wesentlichen zwei verschiedene Namen. Die Variable „i“ verweist immer noch direkt darauf, aber „p“ dient jetzt als Handle, als Referenzpunkt, um zu denselben Daten zu gelangen.

In C definiert diese Dualität, wie Zeigervariablen funktionieren. Der Zeiger selbst, „p“, ist einfach ein Container, der eine Adresse enthält. Es enthält nicht den tatsächlichen Wert, an dem Sie interessiert sind. Stattdessen enthält es den Ort, an dem dieser Wert lebt. Hier kommt der Indirektionsoperator „*“ ins Spiel.

Mit „p“ können Sie auf die Daten unter der in „p“ gespeicherten Adresse zugreifen. Betrachten Sie es als Dereferenzierung. Wenn Sie „ p = 5“ schreiben, ändern Sie nicht die Adresse in „p“. Sie gehen zu der Stelle, auf die „p“ zeigt, und überschreiben den dort vorhandenen Wert mit der Zahl 5.

Diese Unterscheidung ist entscheidend für das Verständnis der Speicherverwaltung. „p“ sagt Ihnen, wo Sie suchen müssen. „p“ sagt Ihnen, was* Sie sich ansehen müssen. Die Verwechslung der beiden führt zu Fehlern, die schwer zu verfolgen sind, da Sie am Ende möglicherweise eine Adresse anstelle der Daten ändern oder versuchen, an einen zufälligen Speicherort zu schreiben, weil „p“ nicht korrekt initialisiert wurde.

Sobald Sie sich mit diesem mentalen Modell vertraut gemacht haben, hören Zeiger auf, abstrakte Syntax zu sein, und werden zu einem direkten Draht zum Gedächtnis. Sie können Variablen über verschiedene Bereiche hinweg ändern, große Strukturen als Referenz übergeben, ohne sie zu kopieren, und dynamische Datenstrukturen erstellen, die je nach Bedarf wachsen und schrumpfen. Es ist mächtiges Zeug, wenn man aufhört, sich mit der Syntax herumzuschlagen.

Der folgende Codeausschnitt veranschaulicht genau dieses Verhalten und zeigt, wie sich der Wert von „i“ nur ändert, weil „p“ verwendet wird, um ihn zu erreichen.

„c
int i = 10;
int p = &i;
p = 5; // ich bin jetzt 5

Der Wert von „i“ wird sofort aktualisiert. Die Adresse wurde nicht verschoben. Nur der Inhalt hat.

Wie Zeiger Werte und Adressen speichern

Es scheint recht einfach zu sein, aber die Mechanik kann einen zum Stolpern bringen. Da der Variablen „i“ der Wert 5 zugewiesen ist und der Zeiger „p“ die Adresse von „i“ enthält, gibt die Dereferenzierung von „p“ („*p“) 5 zurück. Wenn Sie „j = i;“ ausführen, kopieren Sie diesen Wert. Also wird „j“ zu 5.

Führen Sie jetzt die Anweisung „printf“ aus. Sie erhalten „5 5 5“.

Dies geschieht aufgrund der dualen Natur von Zeigern. Stellen Sie sich vor, dass sie zwei unterschiedliche Rollen haben. Erstens speichert die Zeigervariable selbst eine Speicheradresse. Zweitens verweist es auf einen bestimmten Datentyp, der sich an dieser Adresse befindet.

In unserem Beispiel:
p ist der Zeiger. Es enthält den Speicherort.
*p ist der Wert. Es handelt sich um die tatsächlichen Daten, die an dieser Adresse gespeichert sind.

Eine Verwechslung der beiden führt zu Fehlern. Wenn Sie die Adresse als Daten behandeln, erhalten Sie Müllwerte. Wenn Sie die Daten als Adresse behandeln, stürzen Sie Ihr Programm ab. Zu wissen, mit wem man es zu tun hat, verändert alles.

Warum ist diese Unterscheidung für die alltägliche Codierung wichtig? Denn wenn Sie den Unterschied zwischen dem Speicherort der Daten und dem Inhalt der Daten verstehen, können Sie den Speicher direkt manipulieren. Dies ist bei dynamischen Strukturen nützlich, aber gefährlich, wenn Sie den Schritt verpassen. Eine falsche Bewegung und das System versucht, von einem nicht vorhandenen Ort zu lesen.

Sie beginnen zu verstehen, warum C gleichermaßen mächtig und gefährlich bleibt.