Wie viele andere Sprachen hat auch C++ Zeiger. Du weißt bereits, dass Referenzen und Zeiger ähnlich sind, aber stell sie dir als eine Ebene näher an der Funktionsweise deines Computers vor. Zeiger sind Variablen, die Objektadressen speichern. Sie werden verwendet, um direkt mit Objekten zu interagieren, was dynamische Speicherzuweisung und effiziente Datenmanipulation in C++ ermöglicht.
Wenn du neu bei Zeigern bist, können sie etwas mysteriös wirken, aber sobald du dich an sie gewöhnt hast, sind sie ganz unkompliziert.
Sie sind ein entscheidender Teil von C++, also nimm dir etwas Zeit, um sie wirklich zu verstehen. Die schlichte Variante in diesem Konzept wird auch dumb pointer oder raw pointer genannt. Mit modernem C++ gibt es außerdem smart pointer; der einfache Typ ist überhaupt nicht clever und du musst die ganze Arbeit selbst erledigen.
Bevor wir ins Detail gehen, lohnt es sich zu verstehen, wofür Zeiger verwendet werden. Zeiger sind eine Möglichkeit, die Adresse eines Objekts mit anderen Teilen unseres Programms zu teilen, was aus zwei wichtigen Gründen nützlich ist:
Eine Zeigerdeklaration in C++ besteht darin, den Datentyp anzugeben, auf den der Zeiger zeigt, gefolgt von einem Sternchen (*) und dem Namen des Zeigers.
Wenn Zeiger deklariert werden, werden sie nicht automatisch initialisiert.
Ohne ausdrückliche Zuweisung enthält ein Zeiger normalerweise einen unbestimmten Wert, der oft als „Mülladresse“ bezeichnet wird.
Manche Compiler initialisieren Zeiger zwar möglicherweise auf nullptr, aber dieses Verhalten ist nicht bei allen Compilern garantiert, also solltest du dich nicht darauf verlassen.
Am besten initialisierst du Raw Pointer ausdrücklich und prüfst vor der Verwendung, ob sie nicht null sind, um mögliche Probleme zu vermeiden.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Um die Adresse einer Variable einem Zeiger zuzuweisen, verwendest du den Adressoperator (&).
Ein Zeiger wird mit dem Indirektionsoperator (*) dereferenziert.
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Achtung: Das Dereferenzieren muss ausdrücklich erfolgen, während Referenzen einfach wie ein Alias funktionieren.
Zeigerarithmetik ermöglicht es dir, Rechenoperationen mit Zeigern durchzuführen, was besonders nützlich ist, wenn du mit Arrays arbeitest. Wenn du eine Ganzzahl zu einem Zeiger addierst, zeigt er auf ein anderes Element.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
Zeigerarithmetik in C++ kann leicht zu undefiniertem Verhalten führen, wenn du nicht sorgfältig damit umgehst. Undefiniertes Verhalten kann sich in unerwarteten Programmergebnissen, Abstürzen oder sogar Sicherheitslücken zeigen. Ein berüchtigtes Beispiel für die Folgen von undefiniertem Verhalten war die Explosion der Ariane 5 1996, bei der eine Software-Ausnahme, verursacht durch die Umwandlung einer 64-Bit-Gleitkommazahl in eine 16-Bit-Ganzzahl mit Vorzeichen, zu einem katastrophalen Fehlschlag führte.
In C++ wird der Operator -> verwendet, um über einen Zeiger auf ein Objekt auf dessen Member zuzugreifen.
Er ist eine Kurzschreibweise, die den Zugriff auf Member von Objekten vereinfacht, auf die Zeiger zeigen.
Wenn ptr zum Beispiel ein Zeiger auf ein Objekt mit einer Membervariable x ist, kannst du statt (*ptr).x direkt ptr->x verwenden.
Dieser Operator verbessert die Lesbarkeit des Codes und reduziert die Ausführlichkeit, wenn du mit Zeigern auf Objekte arbeitest.
Hier ist ein kurzes Beispiel mit einem struct Superhero, das eine Membervariable superpower hat.
Die main-Funktion erstellt einen Zeiger dianaPrince auf ein Superhero-Objekt (das Wonder Woman darstellt).
Mit dem Operator -> wird auf die Membervariable superpower zugegriffen, die Wonder Womans ikonischen „Lasso of Truth“ zeigt.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
Zeiger und Referenzen ermöglichen beide den indirekten Zugriff auf Objekte, unterscheiden sich aber in ihren Möglichkeiten und in Sicherheitsaspekten. Zeiger bieten die Flexibilität, ihr Zielobjekt zu ändern, und können auf null gesetzt werden. Diese Flexibilität birgt jedoch Risiken, etwa das Dereferenzieren von Nullzeigern oder das Erzeugen von dangling Pointern. Referenzen hingegen können nicht null sein und sind bei ihrer Erstellung an gültige Objekte gebunden, wodurch diese Risiken vermieden werden. Da Referenzen sicherer sind, solltest du sie Zeigern vorziehen, es sei denn, du brauchst die zusätzlichen Funktionen, die Zeiger bieten.
In C++ wird new verwendet, um zur Laufzeit dynamisch Speicher für Objekte auf dem Heap zu reservieren.
Es gibt einen Zeiger auf die reservierte Speicheradresse zurück und ermöglicht Objekten eine Lebensdauer, die nicht an einen bestimmten Gültigkeitsbereich gebunden ist.
Wichtig ist jedoch, new immer mit delete zu kombinieren, um den Speicher ordnungsgemäß freizugeben.
Dynamische Speicherzuweisung solltest du vorsichtig einsetzen, um häufige Probleme wie Speicherlecks und den Zugriff auf freigegebenen Speicher zu vermeiden.
Zeiger können auch auf einen speziellen Wert gesetzt werden, den Nullzeiger, der anzeigt, dass sie auf keine gültige Adresse zeigen. Nullzeiger werden häufig verwendet, um Zeiger zu initialisieren, bevor ihnen gültige Werte zugewiesen werden.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Ein Zeiger sollte entweder die Adresse eines gültigen Objekts enthalten oder auf nullptr gesetzt sein.
So musst du Zeiger nur auf null testen und kannst davon ausgehen, dass jeder Nicht-null-Zeiger gültig ist.
Wenn ein Objekt zerstört wird, bleiben alle Zeiger auf das zerstörte Objekt dangling (sie werden nicht automatisch auf nullptr gesetzt).
Es liegt in deiner Verantwortung, diese Fälle zu erkennen und dafür zu sorgen, dass diese Zeiger anschließend auf nullptr gesetzt werden.
In älterem Code kannst du auf zwei Alternativen zu nullptr stoßen.
Erstens wird das Literal 0 speziell als Nullwert für Zeiger interpretiert, obwohl es das einzige Szenario ist, in dem ein ganzzahliges Literal einem Zeiger zugewiesen werden kann.
Zweitens ist das preprocessor macro NULL, von C übernommen und im Header <cstddef> definiert, eine weitere Darstellung eines Nullzeigers, obwohl seine Verwendung in modernem C++-Code weniger üblich ist.