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:
- 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 ist vom 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.
#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:
- Der Zeiger ist konstant.
- Das, worauf der Zeiger verweist, ist konstant.
- 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.
// '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