Zeiger

Ein Zeiger ist ein Datum, dessen Inhalt auf eine Adresse (Position) 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 Sternchen vor dem Zeigernamen ist der Zeigeoperator und definiert ptrName als Zeiger.

// Zeiger auf short-Datum, d.h. beim Zugriff auf
// den Speicher werden 2 Bytes ausgelesen
// und als signed short interpretiert
short *MaxValue;
// Zeiger auf long-Datum, es werden 4 Bytes ausgelesen
long  *pCounter;
// Zeiger auf char-Datum, es wird 1 Byte ausgelesen
char  *pKey;

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

short* pVar1, pVar2;

Hier ist nur pVar1 ein Zeiger, da der Zeigeroperator lediglich vor der Variablen pVar1 steht, während pVar2 eine normale short-Variable ist. Schreiben Sie deshalb den Zeigeroperator immer unmittelbar vor dem Zeigernamen. Es hilft, diese Fehlerquelle zu vermeiden.

short *pVar1, *pVar2;

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.

short var;          // Definition short-Variable
short *pVar1;       // Definition short-Zeiger
pVar1 = &var;       // Adresse var im Zeiger ablegen
auto pVar2 = &var;  // Zeiger definieren/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.

// Adresse 0x2000 in char-Zeiger laden
auto pSerialCom = reinterpret_cast<char*>(0x2000);

Und da uninitialisierte Zeiger meistens zu undefiniertem Programmverhalten führen, nochmals eine Wiederholung meines Ratschlags aus dem Kapitel über Variablen: Definieren Sie einen Zeiger ebenfalls an der Stelle im Programm, an der er das erste Mal benötigt wird, und verwenden Sie für den Datentyp des Zeigers die auto Definition. Sie dürfen hierbei sogar das "Sternchen" weglassen, da der rechts von Zuweisungsoperator stehende Ausdruck ein Zeigerdatentyp sein muss (siehe obiges Beispiel).

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(DTYP) 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 ab der Stelle in den Speicher geschrieben, deren Adresse im Zeiger abgelegt ist.

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

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

#include <print>

int main ()
{
    short var1;           // short-Variable
    decltype(var1) var2;  // weitere short-Variable definieren
    // Adresse von var1 im Zeiger pVar ablegen
    auto pVar = &var1;
    
    // var1 indirekt ueber den Zeiger pVar
    // den Wert 0x1234 zuweisen
    *pVar = 0x1234;
    // var1 ueber den Zeiger auslesen, mit 2 multiplizieren
    // und das Ergebnis in var2 ablegen
    var2 = *pVar * 2;
    std::println("var1: {:#x}, var2: {:#x}",var1,var2);
}

var1: 0x1234, var2: 0x2468

Beachten Sie die Definition des Zeigers pVar in Zeile 8. 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 wir im weiteren Verlauf noch sehen.

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. Wird ein Zeiger definiert, dem bei seiner Definition keine Adresse zugewiesen werden kann, sollte dieser Zeiger immer mit einem nullptr initialisiert werden. 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).

int *pInt = nullptr;   // int-Zeiger definieren
int val;               // int-Variable definieren
pInt = &val;           // Adresse im Zeiger ablegen

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.

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

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 als Hex-Zahl ausgegeben.

#include <iostream>

int main()
{
    // int-Datum und int-Zeiger definieren
    int var = 10;
    auto pVar = &var;
    // char-Datum und char-Zeiger definieren
    char letter = 'A';
    auto pLetter = &letter;
    // const char-Zeiger definieren
    const char* pText = "Hallo!";

    // Zeiger ausgeben
    // Gibt Inhalt von pVar aus (die Adresse)
    std::cout << pVar << '\n';
    // ACHTUNG! Gibt den Inhalt ab der Adresse von letter aus
    // bis zufaellig eine 0 im Speicher gefunden wird!!
    std::cout << pLetter << '\n';
    // Gibt "Hallo!" aus
    std::cout << pText << '\n';
}

000000D03FCFF684
A╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠─÷¤?ð
Hallo!

Ausgabe mittels format() und print()

Bei der Ausgabe mithilfe der Bibliotheksfunktionen format() und print()/println() wird bei einem char-Zeiger der Buchstabe bzw. der String ausgegeben. Soll stattdessen der Zeigerinhalt ausgegeben werden, ist wie bei allen anderen Zeigertypen, der Zeiger mittels reinterpret_cast in einen void-Zeiger zu konvertieren.

#include <print>

int main()
{
    // const char-Zeiger definieren
    auto pText = "Hallo!";
    // char Datum und Zeiger definieren
    char cVar = 'a';
    auto pcVar = &cVar;
    // signed char Datum und Zeiger definieren
    signed char scVar = 55;
    auto scPtr = &scVar;
    // int-Datum und int-Zeiger definieren
    int var = 10;
    auto pVar = &var;

    // pText String und dessen Adresse ausgeben
    std::println("pText String \"{}\" liegt auf {}",
                 pText, reinterpret_cast<const void*>(pText));
    // char-Datum und dessen Adresse ausgeben
    std::println("char Datum '{}' liegt auf {}",
                 cVar, reinterpret_cast<void*>(pcVar));
    // signed char-Datum und dessen Adresse ausgeben
    std::println("sigend char Datum {} liegt auf {}",
                 scVar, reinterpret_cast<void*>(scPtr));
    // int-Datum und dessen Adresse ausgeben
    std::println("int Datum {} liegt auf {}",
                 var, reinterpret_cast<void*>(pVar));
}

pText String "Hallo!" liegt auf 0x7ff6dd00e33a
char Datum 'a' liegt auf 0xa55c7ffb1f
sigend char Datum 55 liegt auf 0xa55c7ffb1e
int Datum 10 liegt auf 0xa55c7ffb18

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>(DATUM) Konvertierung notwendig, wobei DTYP der Datentyp des Zielzeigers ist.

// Zeigerdefinitionen
long *pLong;
char *pChar;
// Variablendefinition
long lVar;
// Zuweisung long* an long*
pLong = &lVar;
// Zuweisung long* an char*
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:

#include <print>

int main()
{
    // char-Zeiger Definition
    auto pAny = reinterpret_cast<char*>(0x0100);
    std::print("pAny: {} -> ",reinterpret_cast<void*>(pAny));
    // Inkrementieren des Zeigers
    pAny++;
    std::println("pAny++: {}",reinterpret_cast<void*>(pAny));
    // short-Zeiger Definition
    auto pSome = reinterpret_cast<short*>(0x0208);
    std::print("pSome: {} -> ",reinterpret_cast<void*>(pSome));
    // Subtraktion vom Zeiger
    // pSome gleich 0x0204, unter der Annahme, dass
    // eine short-Variable 2 Byte belegt (2*2Bytes subtr.).
    pSome -= 2;
    std::println("pSome -= 2: {}",reinterpret_cast<void*>(pSome));
    // Aber Achtung!
    // Anweisung erhöht nicht den Zeiger sondern den Inhalt
    // der Speicherstelle auf die pAnother verweist
    int var = 10;
    auto pvar = &var;
    std::println("&var: {}, var: {}",reinterpret_cast<void*>(pvar),var);
    (*pvar)++;
    std::println("&var: {}, var: {}",reinterpret_cast<void*>(pvar),var);
}

pAny: 0x100 -> pAny++: 0x101
pSome: 0x208 -> pSome -= 2: 0x204
&var: 0xaec11ffd44, var: 10
&var: 0xaec11ffd44, var: 11

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.

const und Zeiger

Hier sind 3 Fälle zu unterscheiden:

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.

// 'normale' char-Variable
char nonConst = 'a';
// Zeichenkonstante
const char constChar = 'A';
// Zeiger auf char-Konstante
const decltype(constChar) *pNcPtr1 = &constChar;
// const-Zeiger auf char-Variable
decltype(nonConst) *const pCPtr2 = &nonConst;
// const-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++;

Ein Zeiger auf eine Konstante wurde bisher wie folgt definiert:

const DTYP *ptr;

Das Gleiche kann aber auch durch folgende Definition erreicht werden:

DTYP const *ptr;

Es sollte aber immer nur eine Schreibweise verwendet werden, damit die Sache mit den Zeigern nicht noch komplizierter wird. Laut einer C++-Empfehlung (keine Vorschrift!) ist die erste Schreibweise zu verwenden.

Außer den hier vorgestellten Zeigertypen gibt es zwei weitere Arten von Zeiger: den Funktionszeiger und den Memberzeiger. Diese werden in den entsprechenden Kapiteln später erklärt.

Übungen

zeiger_01

Definieren Sie zwei short-Variablen nVar1 und nVar2 und initialisieren sie mit 10 bzw. 20.

Definieren Sie ein Zeiger für den Zugriff auf die short-Variablen und einen const char-Zeiger.

Geben zur Kontrolle den Inhalt der beiden short-Variablen aus.

Weisen Sie über den short-Zeiger der Variablen nVar2 den Wert von nVar1 zu und geben nVar2 erneut aus.

Geben Sie die Adressen der beiden short-Variablen aus.

Zum Schluss ist dem const char-Zeiger ein String-Literal zuzuweisen und der String über diesen Zeiger auszugeben.

nVar1: 10, nVar2: 20
nVar2 nach Zuweisung ueber Zeiger: 10
nVar1 hat die Adresse: 0x7ff61635d010
nVar2 hat die Adresse: 0x7ff61635d012
pszText zeigt auf String

zeiger_02

Alle numerischen Ausgaben sind Hex-Darstellung mit einem Präfix für die Zahlenbasis auszugeben.

Definieren Sie einen const char-Zeiger und eine long-Variable, die mit dem Hex-Wert 0x12345678L zu initialisieren ist.

Geben Sie den Inhalt der long-Variable aus.

Anschließend ist der Inhalt der long-Variable byteweise auszugeben, so wie unten angegeben.

Verwenden Sie hierzu den const char-Zeiger! Um auf alle Bytes der long-Variablen zuzugreifen, ist die im Zeiger abgelegte Adresse entsprechend zu erhöhen und der Wert dann auszugeben.

Die nachfolgende Ausgabe gilt nur für Prozessoren, bei denen das Low-Byte der long-Variable auf der niedrigeren Adresse liegt. Bei anderen Prozessoren erhalten Sie eine umgekehrte Ausgabe.

Weisen Sie dem const char-Zeiger den String "ABCD" zu.

Geben Sie den String mithilfe des Zeigers aus.

Danach sind die einzelnen Buchstaben des Strings, getrennt durch ein Komma, unter Verwendung des Zeigers auszugeben.

0x12345678 liegt wie folgt im Speicher:
0x78,0x56,0x34,0x12
String ist: ABCD
Als Buchstaben: A,B,C,D