C++ Teil 9 – Zeiger



Das Konzept der Zeiger bzw. Pointer(eng. fĂŒr Zeiger) ist fĂŒr AnfĂ€nger wahrscheinlich der unbeliebteste und fĂŒr einen Profi der mĂ€chtigste Bestandteil von C++.
Viele AnfĂ€nger verstehen sie anfangs nicht und sogar Profis machen manchmal Fehler im Umgang mit ihnen. Nichtsdestotrotz sind Zeiger ein sehr wichtiges Thema, von dem sich kein C++ – Programmierer drĂŒcken kann.

In diesem Abschnitt werde ich eine EinfĂŒhrung in dieses komplexe Gebiet geben.

Arbeitsspeicher

RAM ( Random Access Memory) – Speicher mit wahlfreiem Zugriff, auf Deutsch auch Arbeitsspeicher genannt. Alle Variablen werden im RAM erzeugt. Spricht ein Programmierer vom Speicher, so meint er damit gewöhnlich den Arbeitsspeicher.

Bevor wir lernen was Zeiger sind, ist es wichtig zu wissen wie es im Arbeitsspeicher (RAM) aussieht, wenn wir eine Variable anlegen.
Man kann sich den Arbeitsspeicher als eine Art Tabelle bzw. ein Array vorstellen.

RAMRAM. Der Speicheradressenbereich ist hier ohne Bedeutung.

RAM. Der Speicheradressenbereich ist hier ohne Bedeutung.

Wir wollen sehen was im RAM passiert wenn wir eine long-Variable var deklarieren.
Zuerst schaut das Betriebssystem ob fĂŒr die Variable genĂŒgend Speicher zur VerfĂŒgung steht, denn außer unserem Programm laufen noch dutzende andere Programme im Hintergrund, die auch Speicher brauchen. Wenn genug Speicher da ist, dann reserviert das Betriebssystem 4 KĂ€stchen (4 Byte) fĂŒr uns.

Variable im RAM. Der Speicheradressenbereich ist hier ohne Bedeutung.

Variable im RAM. Der Speicheradressenbereich ist hier ohne Bedeutung.

Das Betriebssystem weiß von den Variablennamen nichts, es arbeitet nur mit den Speicheradressen. Die Variablennamen sind nur fĂŒr den Programmierer da, damit er beim Hantieren mit Speicheradressen nicht verrĂŒckt wird. Sie werden durch den Compiler intern in Speicheradressen umgewandelt.
In C++ kann ein Programmierer direkt auf diese Speicherbereich zugreifen und die Inhalte verÀndern.

Adressoperator

Um die Adresse einer Variablen zu bekommen steht ein Adressoperator & bereit, der direkt vor die Variable geschrieben wird, von der wir die Adresse haben wollen.

Hinweis: Man verwechsle den Adressoperator nicht mit dem UND-Operator. Die Bedeutung folgt immer eindeutig aus dem Zusammenhang.


long var1 = 0;
long var2 = 0;
long var3 = 0;

std::cout << "Adressen: " << &var1 << " " << &var2 << " " << &var3 << std::endl; 

Die Adressen der Variablen werden als hexadezimale Zahlen ausgegeben. Der Abstand zwischen den Adressen entspricht genau der Byte-GrĂ¶ĂŸe des verwendeten Variablentyps, weil die Variablen direkt nacheinander erzeugt wurden.

Zeiger deklarieren

Ein Zeiger ist eine spezielle Variable, die auf eine Speicheradresse zeigt.
Deklariert wird ein Zeiger Àhnlich einer Variable, nur dass ein Stern * vor den Namen steht.

// Schematisch 
Datentyp *Zeigername = Adresse;

// Beispiel in C++
int *zeiger = 0;

Erstellen wir eine Variable und lassen einen Zeiger auf sie zeigen.

int var = 5;  // Variable var erstellt
int *zeiger = &var; // der Zeiger zeigt auf den Speicherblock der Variable var

Man beachte, dass der Datentyp des Zeigers identisch mit dem Datentyp der Variable, auf die gezeigt wird, sein muss.

Unser Zeiger beinhaltet also die Adresse der Variable var, die wir weiter verwenden können.

Lesen & Schreiben

Da die Adresse der Variable bekannt ist, können wir direkt auf den Speicherblock im RAM zugreifen, ohne den Variablennamen var zu benutzen.
Um auf den Inhalt des Speicherblocks zuzugreifen, verwendet man wieder das Sternchen * vor dem Variablennamen. Man bezeichnet das Sternchen in diesem Fall auch als Inhaltsoperator (Es hat hier also eine komplett andere Bedeutung, als oben dargestellt). Mit diesem Operator bekommt man den Inhalt der unter der Zeigeradresse liegt, also den Wert der Variable auf die der Zeiger zeigt.

Lesen:

Datentyp Variablenname = *Zeigername;

Als Beispiel weisen wir den Inhalt auf den der Zeiger zeigt (also die Variable var), einer neuen Variablen zu.

int inhalt_von_var = *zeiger;

Wie man sieht, haben wir den Wert der Variable var bekommen, ohne auf sie selbst zu zugreifen. Kennt man also die Adresse einer Variablen, so kann man ihren Inhalt auslesen. Zumindest dann, wenn die Variable in dem gleichen Programm angelegt wurde, ansonsten greift das Betriebssystem ein und verhindert den Zugriff. Man ĂŒberzeuge sich selbst in dem man einem Zeiger eine beliebige Adresse erstellt und versucht den Wert an diese Adresse auszulesen.

int *zeiger = (int*)98234242;   // eine beligebige Adresse
int inhalt_von_var = *zeiger;  // Laufzeitfehler
std::cout << "inhalt_von_var: " << inhalt_von_var << std::endl;

Lesezugriff verweigert

Lesezugriff verweigert

Als NĂ€chstes werden wir den Wert einer Variablen ĂŒber einen Zeiger Ă€ndern.
Dies geschieht Àhnlich wie beim Lesen mit dem Inhaltsoperator.

*Zeigername = Neuer Wert;

Als Beispiel wird der Wert der Variable auf 10 gesetzt.

int var = 5;
int *zeiger = &var;

std::cout << "var: " << var << std::endl;
std::cout << "zeiger: " << zeiger << std::endl;

// der Variablen var neuen Wert zuweisen
*zeiger = 10;

std::cout << "\nvar: " << var << std::endl;
std::cout << "zeiger: " << zeiger << std::endl;

Zuweisung ĂŒber einen Zeiger

Zuweisung ĂŒber einen Zeiger

Der Wert der Variable wurde verÀndert ohne auf die Variable selbst zu zugreifen. Man beachte, dass die Adresse sich nicht Àndert. Man verÀndert lediglich den Inhalt des Speicherblocks, nicht die Position des Blocks selbst.

Dieser indirekte Zugriff auf Inhalte im Arbeitsspeicher ist ein sehr mÀchtiges Werkzeug, deren Bedeutung man an dieser Stelle vielleicht noch nicht erkennen kann. SpÀtestens wenn es um die dynamische Speicherverwaltung geht, kommt man an dem Zeiger-Konzept nicht vorbei.

Zeigeroperatoren
Operator Name Zweck Beispiel
* Zeigeroperator /
Zeiger-Deklarationsoperator
Deklaration long *z = 0;
& Adressoperator Zugriff auf eine Adresse long *z = &variable;
* Inhaltsoperator Schreib- und Lesezugriff auf den Inhalt // Schreiben
*z = 10;
// Lesen
int var2 = *z;

Zeiger, Arrays und Zeigerarithmetik

Zeiger können nicht nur auf einfachen Variablen zeigen, sondern auf alle Objekte in C++, ob es einfache Variablen, Arrays, Funktionen oder Klassen sind, die wir spÀter kennen lernen werden.
Wir haben im letzten Teil dieser Artikelreihe bereits mit Arrays gearbeitet, deswegen werde ich an dieser Stelle das Zusammenspiel von Feldern und Zeigern beleuchten. Vor allem möchte ich hier auf die Zeigerarithmetik eingehen.
Aber zuerst schauen wir uns an, wie man einen Array-Zeiger erstellt.

// Einen Zeiger auf einen Array erzeugen
long arr[] =  {1,2,4,6,8,10,12,14};
long *ptr = arr;

Wer aufgepasst hat, der wird sich fragen warum denn in der letzten Codezeile kein Adressoperator & verwendet wurde. Dies ist in der Tat etwas verwirrend, denn es handelt sich um einen Sonderfall. Der Feldname in C++ ist als ein Zeiger auf das erste Feldelement definiert.
Man kann sich davon ĂŒberzeugen, wenn man folgende Zeile ausfĂŒhrt.

std::cout << ptr  << std::endl;

Als Resultat bekommt man die Speicheradresse auf das erste Arrayelement geliefert.

Man könnte die Zeigerzuweisung auch mit einem Adressoperator schreiben.

long arr[] =  {1,2,5,7,8,11,12,14,15,17};
long *ptr = &arr[0];

Dies ist eine Eigenart von C/C++ und wir wollen uns hier damit nicht weiter aufhalten.
Ein Array, wie wir es erstellt haben, sieht im Arbeitsspeicher folgendermaßen aus.

Array in RAM

Array in RAM

Wir werden dieses Bild spÀter noch mal verwenden um uns die Funktionsweise der Zeigeroperationen klar zu machen.

Zeigeroperationen

Wie der Name Zeigerarithmetik schon aussagt, kann man mit Zeigern rechnen.
Es sind nur folgende Operationen erlaubt:

Zeigeroperationen
Operation Ergebnis Bedeutung
Zeiger + Integerwert Zeiger Springe Anzahl (Integerwert) Speicherblöcke vorwÀrts
Zeiger – Integerwert Zeiger Springe Anzahl (Integerwert) Speicherblöcke zurĂŒck
Zeiger – Zeiger Integerwert Berechne die Anzahl der Speicherblöcke,
die zwischen den beiden Zeigeradressen liegen

Alle anderen Operationen wie Multiplikation, Division ect. sind nicht erlaubt. FĂŒr die ersten beiden Operationen gibt es die typische C++ Kurzschreibweise: Zeiger++ und Zeiger–, wenn der Integerwert eins betrĂ€gt.

Der Integerwert (das kann eine Zahl oder eine ganzzahlige Variable sein) bei den beiden ersten Operationen gibt die Anzahl der Schritte im Arbeitsspeicherbild an. Das heißt die Adresse des Zeigers wird je nach Datentyp um einen unterschiedlichen Wert erhöht, da die Adressbreite eines Elements von dem Datentyp des Array abhĂ€ngt. Die nachfolgende Grafik sollte dies verdeutlichen.

Zeigeraddition

Zeigeraddition

Die letzte Operation berechnet die Anzahl der Speicherblöcke, die zwischen zwei Zeigern liegen.

Schauen wir uns ein Beispiel an, welches diese Operationen verwendet. Der Code soll die LĂ€nge einer Zeichenkette bestimmen.

// Eine Zeichenkette erstellen
char str[] = "Ich lerne C++";

// Einen Zeiger auf die Zeichenkette erstellen
char *ptr = str;

// Der Zeiger soll zum nÀchsten Speicherblock (Arrayelement springen,
// solange die "String-Ende"-Escape-Sequenz nicht erreicht wurde
while(*ptr != '\0')
{
	// Zum nÀchsten Speicherblock springen
	ptr++;
}

// Die Diffrenez zwischen dem Zeiger auf das letzte und das erste Element 
// liefert die Anzahl der Speicherblöcke dazwischen 
// und somit auch die Anzahl der Buchstaben
int laenge = ptr-str;

// Ergebnis anzeigen
std::cout << "Die Zeichenkette \"" << str << "\" ist " 
		<< laenge << " Zeichen lang." << std::endl;

Wie man sieht kann man im Zusammenhang mit Zeiger auch Vergleichsoperatoren, wie ==, !=, >, < , >= und <= verwenden. Dies waren die Grundlagen, die jeder C++ Programmierer sicher beherrschen soll. SelbstverstĂ€ndlich ist das nicht alles gewesen, was es zum Zeiger-Konzept zu wissen gibt. Es gibt noch weitere Punkte, wie beispielsweise Zeiger auf Zeiger oder Zeiger als Funktionsparameter. Auf einige dieser Punkte werden spĂ€ter eingehen, wenn sie benötigt werden, aber fĂŒr den Anfang sollte das genug sein. Übungsaufgaben:

  1. Beschreiben Sie in wenigen eigenen SĂ€tzen das Zeiger-Konzept.
  2. ErklÀren Sie in eigenen Worten, was der Unterschied zwischen dem Zeigeroperator (*) und dem Inhaltsoperator (*) ist. Woran erkennt man im Code, welcher Operator gerade verwendet wird.
  3. Deklarieren Sie einen Zeiger auf eine Variable und verÀndern Sie den Wert der Variable, ohne den Variablennamen zu verwenden.
  4. Erstellen Sie ein char-Array und lassen sie einen Zeiger ptr darauf zeigen. Erhöhen Sie den Wert des Zeigers um eins und geben Sie den Zeiger auf mit std::cout ausgeben. Was beobachten Sie? Wie erklÀren Sie es?
  5. Erweitern Sie den Code aus der letzten Aufgabe so, dass der Text wiederholt ausgeben wird, wobei bei jeder Ausgabe ein Buchstabe weniger angezeigt wird.




2 Kommentare zu “C++ Teil 9 – Zeiger”

  1. s1ckam 15. Juli 2011 um 09:52 Uhr

    „long var1 = 0;
    long var2 = 0;
    long var3 = 0;

    std::cout << "Adressen: " << &var1 << " " << &var2 << " " << &var3 << std::endl;"

    Da muss ich protestieren, dass stimmt nÀmlich nicht immer. Ich weis nicht genau, wie der VC Compiler das macht, aber wenn man beim Intel Compiler nicht Optimierungsstufe 3 oder kleiner gewÀhlt hat, wird immer die gleiche Addresse ausgegeben. Der Compiler lÀsst die Variable dann einfach auf den Speicherbereich von var1 zeigen. Da sollte man drauf achten, wenn man sich auf die Pointer verlassen will.

    gruß

  2. Maximam 15. Juli 2011 um 10:08 Uhr

    Eigentlich sollte es so etwas nicht geben. Da macht wohl der die Optimierung von Intel Compiler komische Sachen. Der Programmierer sollte sich nur an die Programmiersprachen-Spezifikation halten und darauf verlassen, dass der Compiler sie richtig umsetzt. Wenn der Compiler es nicht tut, dann sucht man sich einen neuen.

    Hast du einen Screenshot?
    Beim VC++ gibt es auch auf volle Optimierungsstufe keine Probleme.

Trackback URI | Kommentare als RSS

Einen Kommentar schreiben

XHTML: Du kannst folgende Tags verwenden: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <sub> <sup>

Hinweis: Ich behalte mir das Recht vor solche Kommentare, die Beleidigungen oder rechtswidrige Inhalte beinhalten erst nach einer Editierung freizugeben oder kommentarlos zu löschen. Ähnliches gilt auch für Kommentare die offensichtlich nur der Suchmaschinenoptimierung dienen.