C++ Kurs

Zeiger

Die Themen:

Zeigerdefinition

Sehen wir uns zuerst einmal an, was eigentlich das Besondere an einem Zeiger ist. Ein Zeiger ist zunächst einmal eine Variable. Aber anstelle eines 'normalen Wertes' enthält er einen Verweis auf eine Adresse einer Speicherstelle, eines Peripheriebausteins oder einer Variable. Über diesen im Zeiger abgelegten Verweis kann dann z.B. eine bestimmte Stelle im Speicher, ein Register in einem Peripheriebaustein oder der Inhalt einer Variable ausgelesen oder beschrieben werden. D.h. über einen Zeiger kann damit direkt auf den Speicher oder die Peripherie zugegriffen werden.

Wofür ein solcher Zeiger eingesetzt wird, auch wenn Sie 'nur' PC Programme erstellen und mit dem physikalischen Speicher eigentliches nichts zu tun haben wollen, sehen wir uns in diesem Kapitel an.

Ein Zeiger wird wie folgt definiert:

DATENTYP *NAME;

Der DATENTYP bei der Zeigerdefinition gibt an, wie die Daten zu interpretieren sind, deren Adresse im Zeiger abgelegt ist. Das kleine Sternchen vor dem Zeigernamen definiert NAME als Zeiger und wird als Zeigeroperator bezeichnet. Ohne dieses Sternchen wäre dies nur eine 'normale' Variablendefinition.

PgmHeader
short *pMaxValue;
long  *pCounter;
char  *pKey;
Achtung

Aber Achtung bei der Mehrfach-Definition von Zeigern! Für den Compiler spielt es keine Rolle, ob der Zeigeroperator unmittelbar hinter dem Datentyp, zwischen Leerzeichen oder unmittelbar vor dem Zeigernamen steht. Gewöhnen Sie sich aber trotzdem an, den Zeigeroperator immer direkt vor dem Zeigernamen zu setzen. Sehen Sie sich dazu folgendes Beispiel an, bei dem zwei Zeiger definiert werden sollten:

short* pVar1, pVar2;

Hier ist nur pVar1 als Zeiger definiert, da der Zeigeroperator nur vor der Variablen pVar1 steht, während pVar2 eine gewöhnliche short Variable ist. Schreiben Sie deshalb den Zeigeroperator immer unmittelbar vor dem Zeigernamen (es hilft diese Fehlerquelle zu vermeiden):

short *pVar1, *pVar2;
Achtung

Und da uninitialisierte Zeiger immer zu undefiniertem Programmverhalten führen, nochmals eine Wiederholung meines Ratschlags aus dem Kapitel über Variablen: definieren Sie ebenfalls Zeiger an der Stelle im Programm, an der sie das erste Mal verwendet werden und verwenden Sie für den Datentyp eines Zeigers die auto-Definition. Sie dürfen hierbei sogar das "Sternchen" weglassen, da der rechts von Zuweisungsoperator stehende Ausdruck ein Zeigerdatentyp sein muss.

auto pVar = ...;  // hier folgt noch der Initialisierungsausdruck

Adressberechnung

Nach dem ein Zeiger definiert ist, kann ihm eine Adresse, z.B. die einer Variablen, zugewiesen werden. Um die Adresse einer Variable zu erhalten, ist vor dem Variablennamen den Adressoperator & anzugeben. Hierbei muss der Zeiger den gleichen Datentyp besitzen wie die Variable, deren Adresse im Zeiger abgelegt werden soll. Sind die Datentypen nicht identisch, so meldet der Compiler beim Übersetzen des Programms einen Fehler.

Beispiel:

PgmHeader
short var1 = ...;     // Definition der short Variablen
short *pVar;          // Definition des short Zeigers
decltype(var1) *pVar1; // Alternativ und typsicherer
...
pVar = &var1;      // Adresse von var1 im Zeiger ablegen
auto ptr = &var1;  // Zeiger definieren und initialisieren

Liegt die Variable var1 z.B. auf der Speicheradresse 0x1F00, dann enthält nach der obigen Zuweisung der Zeiger pVar den Wert 0x1F00.

Soll ein Zeiger auf eine bestimmte Adresse im Speicher verweisen, so kann dem Zeiger auch ein Literal oder eine benannte Konstante zugewiesen werden. In diesem Fall muss aber eine entsprechende Typkonvertierung mittels reinterpret_cast<...> durchgeführt werden (reinterpret_cast wird später noch genauer erklärt). Innerhalb der spitzen Klammer des reinterpret_cast Ausdrucks ist der Datentyp des Zeigers anzugeben (einschließlich des Sternchens), dem der Wert zugewiesen werden soll.

PgmHeader
// Zeiger mit der Adresse 0x2000 laden
auto pSerialCom = reinterpret_cast<char*>(0x2000);

Zeigerzugriffe

Um auf den Inhalt der Speicherstelle zuzugreifen, deren Adresse im Zeiger abgelegt ist, ist vor dem Zeigernamen der Dereferenzierungsoperator * (wiederum das Sternchen) anzugeben. Die Anzahl der Bytes, die bei einem solchen Zugriff transferiert werden, ist vom Datentyp des Zeigers abhängig. Es werden immer sizeof(DATENTYP) Bytes übertragen, also bei einem char* in der Regel 1 Byte und bei einem long* 4 Bytes.

Steht der Zeiger links vom Zuweisungsoperator, so wird zunächst der rechts vom Zuweisungsoperator stehende Ausdruck berechnet und das Ergebnis dann in die Speicherstelle geschrieben, deren Adresse im Zeiger abgelegt ist.

PgmHeader
// Zeiger mit Adresse 0x8100 laden
auto pMemory = reinterpret_cast<long*>(0x8100);

// Wert 0x00001234 ab Adresse 0x8100 ablegen
*pMemory = 0x1234;

Steht dagegen der Zeiger rechts vom Zuweisungsoperator, so wird der Inhalt des Speichers ab der Adresse ausgelesen, die im Zeiger abgelegt ist.

PgmHeader
short var1 = 0x1234;     // short Variable definieren und initialisieren
decltype(var1) var2;     // weitere short Variable definieren

auto pVar = &var1;       // Zeiger mit Adresse von var laden
// Speicherstelle auslesen, deren Adresse im Zeiger abgelegt
// ist (hier also var1) und den Wert mit 2 multiplizieren.
// Nach der Zuweisung hat var2 den doppelten Wert von var1
// also 0x2468

var2 = *pVar * 2;

Beachten Sie auch nochmals die Definition des Zeigers pVar. Der Datentyp des Zeigers wird mittels auto aus dem Datentyp der Variablen var1 bestimmt. D.h. wenn sich der Datentyp der Variablen var1 ändert, so ändert sich automatisch auch der Datentyp des Zeigers pVar.

void Zeiger

Eine Besonderheit im Zusammenhang mit Zeigern spielt der Datentyp void. Ein Zeiger vom Datentyp void* ist ein Zeiger der an keinen bestimmten Datentyp gebunden ist. Soll über einen solchen void-Zeiger auf Daten zugegriffen werden, so muss dieser zuerst in einen entsprechenden typisierten Zeiger (char*, short* usw.) konvertiert werden, damit die Anzahl der zu übertragenden Bytes vom Compiler berechnet werden kann. Wozu void-Zeiger letztendlich nützlich sind, werden wir uns im weiteren Verlaufe des Kurses noch ansehen.

HinweisEs gibt außer den hier vorgestellten Zeigertypen noch zwei weitere Arten von Zeigern: den Funktionszeiger und den Memberzeiger. Diese werden in den entsprechenden Kapiteln später erklärt.

Der nullptr

Der nullptr ist ein Literal für einen Zeiger, dessen Inhalt nicht definiert ist, d.h. die in ihm abgelegte Adresse ist nicht gültig. In der Vergangenheit wurden solche nicht-initialisierte Zeiger mit der Ganzzahl-Konstante NULL belegt, welche intern als 0 definiert war. Diese Vorgehensweise ist überholt! Wenn ein Zeiger definiert wird, dem bei seiner Definition keine Adresse zugewiesen werden kann, so sollte dieser Zeiger immer mit einem nullptr initialisiert werden. Und in diesem Fall sollten Sie stets den Datentyp des Zeigers explizit angegeben, da der nullptr im Prinzip 'typlos' ist. (Intern hat der Zeiger den Datentyp std::nullptr_t, aber damit können wir in der Regel nicht weiterarbeiten):

PgmHeader
int main()
{
   int *pInt = nullptr;      // int-Zeiger definieren
   ...
   int val;                  // int-Variable definieren
   pInt = &val;              // Zeiger eine Adresse zuweisen
   ...
}
HinweisAus Kompatibilitätsgründen ist es jedoch weiterhin erlaubt, einem Zeiger die Konstante NULL zuzuweisen. Dies sollte jedoch in Zukunft vermeiden werden.

String-Literale und Zeiger

Wie bereits bekannt, haben String-Literale die folgende Form:

"Dies ist ein String-Literal"

Der Datentyp des String-Literals ist standardgemäß const char[n] (siehe Kapitel 'Kostanten, Zeichen- und String-Literale') und damit kann ein String-Literal einem const char* zugewiesen werden.

PgmHeader
// Definition des const char-Zeigers

const char *pText = nullptr;
...
// Dem const char-Zeiger die Adresse eines String-Literals zuweisen
pText = "Mein String-Literal";
...
// Weiteres String-Literal dem Zeiger zuweisen
pText = "Ein anderes String-Literal";

Aus Gründen der Abwärtskompatibilität mit bestehendem C++ Code wurde im C++ Standard jedoch auch die Zuweisung eines String-Literals an ein char* (also ohne const davor) zugelassen. Dies sollte aber nach Möglichkeit vermeiden werden.

AchtungDer folgende Code mag auf einigen System funktionieren, erzeugt jedoch lt. C++ Standard ein undefiniertes Verhalten:

char *pText = "String-Literal";
*pText = 'A';

Hier wird dem char-Zeiger pText die Adresse eines String-Literals (das ja konstante Zeichen besitzt) zugewiesen. Anschließend wird dann über diesen Zeiger versucht, das erste Zeichen im String-Literal zu verändern, was nun je nach System bis zum Absturz des Rechners führen kann. Bei Anwendung der korrekten Anweisung const char *pText wird schon vom Compiler ein Fehler gemeldet wenn versucht wird, die Zeichen im String-Literal zu ändern.

Operationen mit Zeigern

Die einfachste Operation mit Zeigern ist die Zuweisung. Hierbei ist aber stets darauf zu achten, dass einem Zeiger nur ein anderer Zeiger des gleichen Datentyps zugewiesen werden kann. Der Adressoperator & liefert z.B. einen solchen Zeiger vom Typ <Datentyp der Variablen>*. Soll eine Zuweisung mit einem anderen Datentyp erfolgen, sei es eine Konstante oder ein Zeiger mit einem abweichenden Datentyp, so ist dieser erst mittels reinterpret_cast<DTYP>(...) entsprechend zu konvertieren. DTYP ist der Datentyp des Ziels, also z.B. ein char*.

PgmHeader
// Zeigerdefinitionen
long *pLong;
char *pChar;
// Variablendefinition
long lVar;

// Zuweisung long* an long*
pLong = &lVar;
// Zuweisung long* an char*
pChar = reinterpret_cast<decltype(pChar)>(pLong);

Und auch in diesem Beispiel machen wir uns wieder den Operator decltype zu Nutze. Ganz gleich welchen Datentyp der Zeiger pChar annimmt, reinterpret_cast liefert immer den richtig Zeiger-Datentyp zurück.

Für arithmetische Operationen mit Zeigervariablen gelten einige Besonderheiten:

  1. Es sind nur die Operationen Addition und Subtraktion (einschl. deren Kurzschreibweisen) zugelassen, wobei einer der Operanden ein Ganzzahl-Datentyp sein muss (Ausnahme siehe Punkt 2). Alle anderen Operationen führen zu einem Übersetzungsfehler. Das Ergebnis besitzt den Datentyp des Zeigers.
  2. Zwei Zeiger können subtrahiert aber nicht addiert werden. Das Ergebnis der Subtraktion besitzt den Datentyp size_t.
  3. Eine Addition des Wertes X auf einen Zeiger vom Typ DTYP* erhöht den Inhalt des Zeigers (d.h. die in ihm abgelegte Adresse) um X*sizeof(DTYP) (siehe Beispiele). Für die Subtraktion gilt entsprechendes. Einer der Gründe für dieses Verhalten erfahren Sie im Kapitel über Felder.
PgmHeader
// char Zeiger Definition
auto pAny = reinterpret_cast<char*>(0x0100);
// Inkrementieren des Zeigers
// pAny enthält danach den Wert 0x0101 (Größe char-Variable: 1 Byte)

pAny++;

// short Zeiger Definition
auto pSome = reinterpret_cast<short*)>(0x0208);
// Subtraktion vom Zeiger
// pSome enthält danach den Wert 0x0204, unter der Annahme, dass
// eine short-Variable 2 Byte belegt (2*2Bytes subtrahieren).

pSome -= 2;

// Aber Achtung!
// Die nachfolgende Anweisung erhöht nicht den Zeiger sondern den
// Inhalt der Speicherstelle die durch pAnother adressiert wird

(*pAnother)++;

Außer Addition und Subtraktion sind nur noch Vergleichsoperationen mit Zeigern erlaubt, d.h. es kann z.B. mit dem GLEICH-Operator == abfragt werden, ob ein Zeiger eine bestimmte Adresse enthält. Dabei ist aber wiederum zu beachten, dass beide Operanden des Operators immer vom gleichen Datentyp sein müssen.

Zeiger in cout-Anweisungen

Steht in einer cout-Anweisung als auszugebendes Datum ein Zeiger, so wird in der Regel der Inhalt des Zeigers, d.h. die in ihm abgelegte Adresse, ausgegeben. Soll stattdessen der Inhalt der Speicherstelle, die durch den Zeiger adressiert wird, ausgegeben werden, so muss der Zeiger dereferenziert werden (*-Operator).

Eine Ausnahme davon bilden alle Typen von char-Zeigern (siehe auch nachfolgendes Beispiel). Bei char-Zeigern innerhalb einer cout-Anweisung wird davon ausgegangen, dass der Zeiger auf einen String zeigt und somit der String auszugeben ist. Sie wissen doch noch: Strings sind Zeichenketten die mit einer binären 0 abgeschlossen sind. Fehlt aus irgendeinem Grund die abschließende binäre 0 im String oder zeigt der char-Zeiger nicht auf einen String, so wird ab der im Zeiger abgelegten Adresse der Speicherinhalt so lange als Zeichen ausgegeben, bis zufällig eine 0 im Speicher gefunden wird. Soll der Inhalt des char-Zeigers ausgegeben werden, d.h. die in ihm enthaltene Adresse, so ist der Zeiger in einen anderen Datentyp zu konvertieren. Dieser Datentyp muss dann aber selbstverständlich groß genug sein (in Bezug auf die Anzahl der Bytes), um die im Zeiger abgelegte Adresse vollständig aufnehmen zu können. Im Beispiel wird der char-Zeiger dazu in einen void-Zeiger konvertiert. Um innerhalb der cout-Anweisung auf ein einzelnes Zeichen im String zuzugreifen, der über den char-Zeiger adressiert wird, ist der Zeiger zu dereferenzieren. Der Wert wird dann standardmäßig als ASCII--Zeichen dargestellt. Für eine numerische Darstellung ist wiederum eine Typkonvertierung notwendig. Im Beispiel wird dazu der Datentyp int verwendet.

PgmHeader
#include <iostream>
using namespace std;

// char-Zeiger mit Adresse des Strings "ABCD" initialisieren
const char *pText = "ABCD";

// main() Funktion
int main()
{
   // Alle numerischen Ausgaben in Hex mit Zahlenbasis
   cout.setf(ios::showbase);
   cout << hex;

   // String ausgeben, auf den der Zeiger pText verweist
   cout << "Textausgabe ueber Zeiger: " << pText << endl;
   // Nun den Inhalt des Zeigers (Adresse) ausgeben
   cout << "Inhalt des Zeigers: " << static_cast<const void*>(pText) << endl;
   // Speicherinhalt ausgeben, der durch pText adressiert wird
   cout << "Datenausgabe ueber Zeiger: " << static_cast<int>(*pText) << endl;
}
Programmausgabe Textausgabe über Zeiger: ABCD
 Inhalt des Zeigers: 013A2124
 Datenausgabe über Zeiger: 0x41
HinweisMehr zu der im Beispiel verwendeten Typkonvertierung static_cast<...> gleich im nächsten Kapitel.

const und Zeiger

Wie bereits weiter vorne im Kurs weiter ausgeführt, werden für nicht veränderbare Werte in einem Programm Konstanten verwendet. Und dies gilt auch für Zeiger. Nur ist die Sache hier etwas komplizierter, oder scheint auf den ersten Blick jedenfalls so.

Bei Zeigern müssen 3 Fälle unterscheiden:

  1. Der Zeiger ist konstant
  2. Das, worauf der Zeiger zeigt, ist konstant
  3. Sowohl der Zeiger als auch das, worauf er zeigt ist, konstant.

Sehen wir uns die entsprechenden Zeiger-Definitionen einmal an:

Zeigerdefinition Bedeutung
const DTYP *ptr; Zeiger ptr zeigt auf eine Konstante vom Typ DTYP; der Zeiger kann verändert werden.
DTYP *const ptr; Zeiger ptr zeigt auf Variable vom Typ DTYP; der Zeiger selbst ist konstant.
const DTYP *const ptr;   Zeiger ptr zeigt auf eine Konstante vom Typ DTYP; der Zeiger selbst ist ebenfalls konstant.

Nachfolgend ist zu jedem Fall ein entsprechendes Beispiel aufgeführt. Sehen Sie sich genau an, wie ein Zeiger auf eine Konstante definiert wird und wie ein konstanter Zeiger. Sie können sich diesen 'komplizierten' Sachverhalt am besten merken, wenn Sie eine Zeigerdefinition von rechts nach links lesen. So bedeutet z.B. die Anweisung

const char *pcPtr;

dass pcPtr ein Zeiger auf ein char ist das konstant ist. Oder die Anweisung

char* const pcPtr;

dass pcPtr ein konstanter Zeiger auf ein char ist.

PgmHeader
// 'normale' char Variable
char nonConst = 'a';
// Zeichenkonstante
const char constChar = 'A';
// Zeiger auf char-Konstante
const decltype(constChar) *pNcPtr1 = &constChar;
// Konst-Zeiger auf char-Variable
decltype(nonConst) *const pCPtr2 = &nonConst;
// Konst-Zeiger auf char-Konstante
const decltype(constChar) *const pCCPtr3 = &constChar;
// Nicht erlaubt, da Zeiger auf Konstante verweist
*pNcPtr1 = 'B';
// Ok da Zeiger nicht konstant ist
pNcPtr1++;
// Ok da Zeiger auf char-Variable
*pCPtr2 = 'B';
// Nicht erlaubt, da Zeiger konstant ist
pCPtr2++;
// Nicht erlaubt, da Zeiger auf Konstante verweist
*pCCPtr3 = 'B';
// Nicht erlaubt, da auch Zeiger konstant ist
pCCPtr3++;

HinweisZum Abschluss noch etwas zur Verwirrung. Wie Sie ja bereits wissen, wird ein Zeiger auf einen konstanten Wert wie folgt definiert:

const DTYP* ptr;

Das Gleiche erreichen Sie aber auch durch folgende Definition:

DTYP const* ptr;

Sie sollten in Ihren Programmen aber immer nur eine Schreibweise einsetzen damit die Sache mit den Zeigern nicht noch komplizierter wird. Laut einer C++ Empfehlung (keine Vorschrift!) sollten Sie die erste Schreibweise verwenden.

Beipiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.