C++ Tutorial

Zeiger

Ein Zeiger ist eine Variable, deren Inhalt auf eine Adresse im Speicher verweist. D.h., über einen Zeiger wird indirekt auf den Speicher zugegriffen.

Zeigerdefinition

Ein Zeiger wird wie folgt definiert:

DTYP *ptrName;

Der Datentyp DTYP legt fest, wie die Daten zu interpretieren sind, die ab der im Zeiger enthaltenen Adresse im Speicher liegen. Das kleine Sternchen vor dem Zeigernamen ist der Zeigeroperator und definiert ptrName als Zeiger.

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

Adressberechnung

Nachdem ein Zeiger definiert ist, kann ihm eine Adresse, z.B. die eines Datums, zugewiesen werden. Um die Adresse eines Datums zu erhalten, ist vor dem Namen des Datums der Adressoperator & anzugeben. Hierbei muss der Zeiger den gleichen Datentyp besitzen wie das Datum.

1: short var;           // Definition short-Variable
2: short *pVar1;        // Definition short-Zeiger
3: pVar1 = &var;        // Adresse von var im Zeiger ablegen
4: auto pVar2 = &var;   // Zeiger definieren und initialisieren

Liegt die Variable var z.B. auf der Speicheradresse 0x1F00, dann enthalten nach den Zuweisungen die Zeiger pVar1 und pVar2 den Wert 0x1F00.

Soll ein Zeiger auf eine feste Adresse verweisen, ist dem Zeiger ein Literal oder eine benannte Konstante zuzuweisen. In diesem Fall ist eine Typkonvertierung mittels reinterpret_cast<DTYP>(ADR) durchzuführen (reinterpret_cast wird später genauer erklärt). Innerhalb der spitzen Klammer von reinterpret_cast ist der Datentyp des Zeigers anzugeben (einschließlich des Sternchens), dem der Wert zugewiesen werden soll.

1: // Adresse 0x2000 in char-Zeiger laden
2: 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 dereferenzierte Zeiger links vom Zuweisungsoperator, wird zunächst der rechts vom Zuweisungsoperator stehende Ausdruck berechnet und das Ergebnis dann in die Speicherstelle geschrieben, deren Adresse im Zeiger abgelegt ist.

1: // Zeiger mit Adresse 0x8100 laden
2: auto pMemory = reinterpret_cast<long*>(0x8100);
3: // Wert 0x00001234 ab Adresse 0x8100 ablegen
4: *pMemory = 0x1234L;

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

1: short var1 = 0x1234;    // short-Variable
2: decltype(var1) var2;    // weitere short-Variable definieren
3:
4: auto pVar = &var1;      // Zeiger mit Adresse von var1 laden
5: // Speicher auslesen, dessen Adresse im Zeiger abgelegt
6: // ist (hier also var1) und mit 2 multiplizieren.
7: // Nach der Zuweisung hat var2 den doppelten Wert von var1
8: // also 0x2468
9: var2 = *pVar * 2;

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

void-Zeiger

Ein Zeiger vom Datentyp void* ist ein Zeiger, der an keinen Datentyp gebunden ist. Soll über einen solchen void-Zeiger auf Daten zugegriffen werden, ist dieser zuerst in einen typisierten Zeiger (char*, short* usw.) zu konvertieren. Wozu void-Zeiger nützlich sind, werden Sie im weiteren Verlauf noch sehen.

nullptr

nullptr ist ein Literal für einen Zeiger, dessen Inhalt nicht definiert ist, d.h., die in ihm abgelegte Adresse ist nicht gültig. Wird ein Zeiger definiert, dem bei seiner Definition keine Adresse zugewiesen werden kann, sollte dieser Zeiger immer mit einem nullptr initialisiert werden. Und in diesem Fall ist der Datentyp des Zeigers explizit anzugeben, da der nullptr im Prinzip 'typlos' ist. (Intern besitzt der Zeiger den Datentyp std::nullptr_t, aber mit diesem Datentyp kann in der Regel nicht gearbeitet werden):

1: int main()
2: {
3:    int *pInt = nullptr;   // int-Zeiger definieren
4:    int val;               // int-Variable definieren
5:    pInt = &val;           // Adresse im Zeiger ablegen
6: }

String-Literale und Zeiger

Wie bekannt, haben String-Literale die Form:

"Dies ist ein String-Literal"

Der Datentyp des String-Literals ist standardgemäß const char[n] (siehe Kapitel Konstanten) und damit kann ein String-Literal einem const char-Zeiger zugewiesen werden.

1: // Definition des const char-Zeigers
2: const char *pText = nullptr;
3: // Zeiger die Adresse eines String-Literals zuweisen
4: pText = "Mein String-Literal";
5: // Weiteres String-Literal dem Zeiger zuweisen
6: pText = "Ein anderes String-Literal";

Operationen mit Zeiger

Einem Zeiger kann nur ein Zeiger mit dem gleichen Datentyp zugewiesen werden. Soll ein Datum mit einem abweichenden Datentyp zugewiesen werden, ist eine reinterpret_cast<DTYP>(DAT) Konvertierung notwendig, wobei DTYP vom Datentyp des Zeigers ist.

1: // Zeigerdefinitionen
2: long *pLong;
3: char *pChar;
4: // Variablendefinition
5: long lVar;
6:
7: // Zuweisung long* an long*
8: pLong = &lVar;
9: // Zuweisung long* an char*
10: pChar = reinterpret_cast<decltype(pChar)>(&lVar);

Auch dieses Beispiel verwendet den Spezifizierer decltype zur Typkonvertierung. Ganz gleich, welchen Datentyp der Zeiger pChar besitzt, reinterpret_cast liefert immer den richtigen Zeiger-Datentyp zurück.

Für arithmetische Operationen mit Zeiger gelten einige Besonderheiten:

  • Es sind nur die Operationen Addition und Subtraktion zugelassen, wobei einer der Operanden ein Integer-Datentyp sein muss (Ausnahme siehe nächsten Punkt). Das Ergebnis besitzt den Datentyp des Zeigers.
  • Zwei Zeiger können subtrahiert aber nicht addiert werden. Das Ergebnis der Subtraktion besitzt den Datentyp size_t.
  • Eine Addition des Wertes X auf einen Zeiger vom Typ DTYP* erhöht den Inhalt des Zeigers um X*sizeof(DTYP) (siehe Beispiel). Für die Subtraktion gilt Entsprechendes.
1: // char-Zeiger Definition
2: auto pAny = reinterpret_cast<char*>(0x0100);
3: // Inkrementieren des Zeigers
4: // pAny danach 0x0101(Größe char-Variable: 1 Byte)
5: pAny++;
6:
7: // short-Zeiger Definition
8: auto pSome = reinterpret_cast<short*>(0x0208);
9: // Subtraktion vom Zeiger
10: // pSome enthält den Wert 0x0204, unter der Annahme, dass
11: // eine short-Variable 2 Byte belegt (2*2Bytes subtr.).
12: pSome -= 2;
13: // Aber Achtung!
14: // Anweisung erhöht nicht den Zeiger sondern den Inhalt
15: // de Speicherstelle, die durch pAnother adressiert wird
16: (*pAnother)++;

Außer Addition und Subtraktion sind noch Vergleichsoperationen mit Zeiger erlaubt, d.h. mit dem GLEICH-Operator == kann z.B. geprüft werden, ob ein Zeiger auf eine bestimmte Adresse verweist. Dabei ist zu beachten, dass beide Operanden den gleichen Datentyp besitzen.

Ausgabe von Zeigern

Ausgabe mittels cout

Wird ein char-Zeiger in der cout-Anweisung ausgegeben, wird davon ausgegangen, dass der Zeiger auf einen String verweist und der String ausgegeben. Bei allen anderen Zeigern wird der Zeigerinhalt ausgegeben.

1: // int-Datum und int-Zeiger definieren
2: int var = 10;
3: auto pVar = &var;
4: // char-Zeiger definieren
5: char *pText = "Hallo!";
6: // Zeiger ausgeben
7: std::cout << pVar << '\n';
8: std::cout << pText << '\n';

0xe7c6bffa4c
Hallo!

Ausgabe mittels format() und print()

Bei der Ausgabe mithilfe der Bibliotheksfunktionen format() und print() wird bei einem char-Zeiger ebenfalls der String ausgegeben. Soll stattdessen der Zeigerinhalt ausgegeben werden, ist im Platzhalter der Formatspezifizierer :p anzugeben.

Alle anderen Zeigertypen müssen für die Ausgabe in einen void-Zeiger konvertiert werden.

1: // char-Zeiger definieren
2: auto pText = "Hallo!";
3: // String ausgeben, auf den der Zeiger pText verweist
4: std::println("Textausgabe ueber Zeiger: {}", pText);
5: // Nun den Inhalt des Zeigers (Adresse) ausgeben
6: std::println("Inhalt des Zeigers: {:p}", pText);
7: // Speicher ausgeben, der durch pText adressiert wird
8: std::println("Datenausgabe ueber Zeiger: {:#x}",*pText);
9: // int-Datum und int-Zeiger definieren
10: int var = 10;
11: auto pVar = &var;
12: std::println("Inhalt von {:p} ist {}",
13:               reinterpret_cast<void*>(pVar), *pVar);

Textausgabe ueber Zeiger: Hallo!
Inhalt des Zeigers: 0x7ff74ee3d050
Datenausgabe ueber Zeiger: 0x48
Inhalt von 0xc0ff5ff5fc ist 10

const und Zeiger

Hier sind 3 Fälle zu unterscheiden:

1.) Der Zeiger ist konstant.
2.) Das, worauf der Zeiger verweist, ist konstant.
3.) Sowohl der Zeiger wie auch das, worauf er verweist, ist konstant.

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

Dieser 'komplizierte' Sachverhalt lässt sich am besten merken, wenn die Zeigerdefinition von rechts nach links gelesen wird. So bedeutet z.B. die Anweisung

const char *pcPtr;

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

char *const pcPtr;

dass pcPtr ein konstanter Zeiger auf ein char ist.

Nachfolgend ist zu jedem Fall ein Beispiel aufgeführt.

1: // 'normale' char-Variable
2: char nonConst = 'a';
3: // Zeichenkonstante
4: const char constChar = 'A';
5: // Zeiger auf char-Konstante
6: const decltype(constChar) *pNcPtr1 = &constChar;
7: // const-Zeiger auf char-Variable
8: decltype(nonConst) *const pCPtr2 = &nonConst;

9: // const-Zeiger auf char-Konstante
10: const decltype(constChar) *const pCCPtr3 = &constChar;
11: // Nicht erlaubt, da Zeiger auf Konstante verweist
12: *pNcPtr1 = 'B';
13: // Ok da Zeiger nicht konstant ist
14: pNcPtr1++;
15: // Ok da Zeiger auf char-Variable
16: *pCPtr2 = 'B';
17: // Nicht erlaubt, da Zeiger konstant ist
18: pCPtr2++;
19: // Nicht erlaubt, da Zeiger auf Konstante verweist
20: *pCCPtr3 = 'B';
21: // Nicht erlaubt, da auch Zeiger konstant ist
22: pCCPtr3++;;


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz