- Zeiger (Informatik)
-
Mit Zeiger (auch engl. Pointer) wird in der Informatik ein spezieller Wert bezeichnet, dessen Bedeutung eine Speicheradresse ist. An dieser Adresse können entweder Daten, wie Variablen oder Objekte, aber auch Programmcode, stehen. Durch Dereferenzierung des Zeigers ist es möglich, auf die Daten oder den Code zuzugreifen.
Zeiger werden unter anderem dazu verwendet, dynamischen Speicher zu verwalten. So werden bestimmte Datenstrukturen, zum Beispiel verkettete Listen, in der Regel mit Hilfe von Zeigern implementiert.
Ein Zeiger ist ein Spezialfall und in einigen Programmiersprachen die einzige Implementierungsmöglichkeit des Konzepts einer Referenz.
Zeiger in Programmiersprachen
Zeiger kommen vor allem in maschinennahen Programmiersprachen wie z. B. Assembler, C oder C++ vor, während man den Gebrauch in streng typisierten Sprachen wie Modula-2 oder Ada stark einschränkt und sie in Sprachen wie Java oder Eiffel zwar intern vorhanden, aber für den Programmierer vollständig verborgen (opak) sind. Mit erstgenannten Sprachen ist es möglich, Zeiger auf beliebige Stellen im Speicher zu erzeugen oder mit ihnen zu rechnen.
Manche Programmiersprachen schränken den Gebrauch von Zeigern ein, weil Programmierern bei der Arbeit mit Zeigern leicht schwerwiegende Programmierfehler unterlaufen (die so eine Ursache für Pufferüberläufe oder Speicherzugriffsverletzungen (SIGSEGVs) und daraus folgend Abstürze bei zum Beispiel in C und C++ geschriebenen Programmen darstellen).
In objektorientierten Sprachen tritt an die Stelle der Zeiger alternativ (C++) oder ausschließlich (Java, Python) die Referenz, die im Gegensatz zu Pointern nicht ausdrücklich dereferenziert werden muss.
In der typsicheren Sprache C# kommen Zeiger „im Grunde nach“ nicht vor. Alle Funktionalitäten, die Zeiger bieten, wurden durch sichere Konzepte wie Delegate ersetzt. Es ist jedoch möglich, unsicheren Code zu deklarieren (der auch speziell kompiliert werden muss), um Zeiger wie in C++ nutzen zu können.[1] Damit kann in manchen Fällen bessere Performance erreicht werden oder es wird möglich auf die Windows-API-Funktionen zuzugreifen. Vom Gebrauch ist jedoch stark abzuraten. Innerhalb der .NET-Framework stellt unsicherer Code ein Sicherheitsrisiko dar, weil dieser angelegte Speicher nicht vom Garbage Collector bereinigt werden kann und weil unsicherer Code von der Common Language Runtime generell nicht überprüft wird.
Typisierte Zeiger
In den meisten höheren Programmiersprachen werden Zeiger direkt mit Datentypen assoziiert. So kann ein "Zeiger auf ein Objekt vom Typ Integer" normalerweise auch nur auf ein Objekt vom Typ "Integer" verweisen. Der Datentyp des Zeigers selbst bestimmt sich also durch den Typ, auf den er verweist. In der Programmiersprache C ist dies eine Voraussetzung zur Realisierung der Zeigerarithmetik (s. u.), denn nur durch das Wissen um die Speichergröße des assoziierten Typs kann die Adresse des Vorgänger- oder Nachfolgeelementes berechnet werden. Darüber hinaus ermöglicht die Typisierung von Zeigern dem Compiler, Verletzungen der Typkompatibilität zu erkennen.
Untypisierte Zeiger
Diese Zeiger sind mit keinem Datentyp verbunden. Sie können nicht dereferenziert, inkrementiert oder dekrementiert werden, sondern müssen vor dem Zugriff in einen typisierten Zeigertyp umgewandelt werden.
Beispiele dafür sind der Typ void* in C und C++, in Objective-C vom Typ id oder POINTER in Pascal.
In höheren Programmiersprachen existieren zum Teil keine untypisierten Zeiger.
Leerzeiger (Nullzeiger)
Der Nullzeiger ist ein Zeiger mit einem speziellen, dafür reservierten Wert (sog. Nullwert, nicht zwingend numerisch 0), der anzeigt, dass auf nichts verwiesen wird. Nullzeiger werden in fast allen Sprachen sehr häufig verwendet, da man mittels des Nullzeigers eine „designierte Leerstelle“ kennzeichnet. Zum Beispiel wird eine einfach verkettete Liste meist so implementiert, dass das letzte Element auf den Nullzeiger als Folgeelement verweist. Er kennzeichnet in diesem Fall also das Ende der Liste. In Pascal-basierten Sprachen wie Delphi bzw. Object Pascal heißt der Nullzeiger beispielsweise
nil
(lateinisch: „nichts“ oder Akronym für „not in list“). In C kennzeichnet das in der Standardbiliothek enthaltene Präprozessor-MakroNULL
den Nullzeiger und verdeckt die interne Repräsentation. C++ nutzt vorzugsweise die numerische0
anstelle desNULL
-Makros [2] Für den kommenden C++-Standard C++0x soll die Konstantenullptr
eingeführt werden. Die Nullreferenz in Python heißtNone
.Intern werden Nullzeiger auf unterschiedliche Arten repräsentiert, weshalb man sich nach Möglichkeit nie um den tatsächlichen Wert kümmert, sondern ihn einfach als Indikator benutzt, dass der Zeiger auf keinen benutzbaren Inhalt verweist. Die logische Folge ist, dass ein Nullzeiger nicht dereferenziert werden darf. Ein solcher Umgang mit dem Nullzeiger kann undefiniertes Verhalten hervorrufen. Auf vielen modernen Betriebssystemen wird das Programm mit einer Schutzverletzung abgebrochen.
Uninitialisierte Zeiger
Falls eine Zeigervariable dereferenziert wird, die nicht auf einen gültigen Speicherbereich des entsprechenden Typs zeigt, kann es ebenfalls zu unerwartetem Verhalten kommen. So kann eine Situation auftreten, wenn eine Variable vor ihrer Benutzung nicht auf einen gültigen Wert initialisiert wurde, oder wenn sie noch auf eine Speicheradresse verweist, die nicht mehr gültig ist (wilder Zeiger). Zeigt der Zeiger nicht auf eine gültige Speicheraddresse, kann es wie beim Nullzeiger zu einer Schutzverletzung kommen.
Zeigeroperationen
- Dereferenzieren: auf das Objekt, auf welches der Zeiger zeigt, zugreifen. Im Falle eines Funktionszeigers wird dies in den meisten Fällen einem tatsächlichem Aufruf des referenzierten Programmfragmentes entsprechen.
- Inkrementieren/Dekrementieren: den Zeiger auf das Objekt versetzen, das sich im Speicher hinter/vor dem ursprünglichem Objekt befindet. Intern wird dies durch Addition oder Subtraktion der Objektgröße realisiert. Diese ist dem Compiler nur bekannt, wenn der Typ des referenzierten Objekts während der Kompilierzeit klar gekennzeichnet ist. Der so entstehende Zeigerwert kann natürlich auch sofort der enthaltenden Variable zugewiesen werden, falls es diese gibt. Die Prä- und Post-Inkrementierungsoperatoren in C leisten das beispielsweise.
- Zerstören: des referenzierten Objektes (siehe Konstruktor / Destruktor). Es bietet sich nach Aufruf des Destruktors an, alle Variablen, die Zeiger auf das zerstörte Objekt enthalten, auf den Nullwert zu setzen, um später erkennen zu können, dass kein gültiges Objekt mehr referenziert wird. Dies ist im Allgemeinen jedoch nicht möglich.
- Vergleichen: mit anderen Zeigern oder mit NULL.
Zeigerarithmetik
Das Erhöhen oder Verringern eines Zeigers um einen festen Wert oder das Subtrahieren zweier Zeiger wird als Zeigerarithmetik bezeichnet.
Da diese Operationen sehr fehleranfällig sind, werden sie in höheren Programmiersprachen meist nicht unterstützt, wobei selbstverständlich dort wiederum andere Methoden gegeben sind, um die gleiche Funktionalität zu implementieren.
Eigenschaften von Zeigern auf Daten
Vorteile
Die Verwendung von Zeigern kann in bestimmten Fällen den Programmablauf beschleunigen oder helfen, Speicherplatz zu sparen:
- Ist die von einem Programm im Speicher zu haltende Datenmenge am Programmstart unbekannt, so kann genau so viel Speicher alloziert werden, wie benötigt wird (Dynamische Speicherverwaltung).
- Es ist möglich, während des Programmablaufs nicht mehr benötigten Speicher wieder an das Betriebssystem zurückzugeben.
- Bei der Verwendung von Feldern bzw. Vektoren kann man mittels Zeigern schnell innerhalb des Feldes springen und navigieren. Mittels Zeigerinkrement wird dabei durch ein Feld hindurchgelaufen. Anstatt einen Index zu verwenden und so die Feldelemente über diesen anzusprechen, setzt man zu Beginn des Ablaufs einen Zeiger auf den Anfang des Feldes und inkrementiert diesen Zeiger bei jedem Durchlauf. Diese Art des Zugriffs auf Felder wird in vielen Programmiersprachen und Compilern an manchen Stellen intern automatisch so umgesetzt.
- Verweise auf Speicherbereiche können geändert werden, z. B. zur Sortierung von Listen, ohne die Elemente umkopieren zu müssen (dynamische Datenstrukturen).
- Bei Funktionsaufrufen kann durch die Übergabe eines Zeigers auf ein Objekt vermieden werden, das Objekt selbst zu übergeben, was eine in bestimmten Fällen sehr zeitaufwändige Anfertigung einer Kopie des Objektes erfordern würde (Referenzparameter).
- Anstatt Variablen jedes Mal zu kopieren und so jedes Mal erneut Speicherplatz zur Verfügung zu stellen, kann man in manchen Fällen einfach mehrere Zeiger auf dieselbe Variable verweisen lassen.
- Bei Zeichenketten können direkt Speicherinhalte angesprochen werden, ohne über Objekte und Funktionen gehen zu müssen.
Nachteile und Gefahren
Es gibt Sprachen, die bewusst auf den Einsatz von Zeigern verzichten (s. o.). Dies hat vor allem folgende Gründe:
- Der Umgang mit Zeigern ist schwierig zu erlernen, kompliziert und fehleranfällig. Vor allem im Sinne von Zeigern zu denken, bereitet Programmieranfängern anfangs oft Schwierigkeiten. Auch bei erfahrenen Programmierern kommen Flüchtigkeitsfehler im Umgang mit Zeigern noch relativ häufig vor.
- Im Allgemeinen ist keine Datentyp-Kontrolle möglich, das heißt, beim Ausführen kann nicht kontrolliert werden, was für Daten an der Zieladresse stehen, und ob diese den Erwartungen (Spezifikationen) des Programmablauf entsprechen
- Programmierfehler bei der Arbeit mit Zeigern können schwere Folgen haben. So kommt es z. B. zu Programmabstürzen, unbemerkter Beschädigung von Daten (durch vagabundierende Zeiger), Pufferüberläufen oder "verlorenen" Speicherbereichen (Speicherlecks: Das Programm fordert ständig mehr Speicher an, der anderen Programmen nicht mehr zur Verfügung steht, bis im Extremfall das Betriebssystem nicht mehr genügend liefern kann).
- Setzen sich Datenstrukturen aus Zeigern zusammen, die auf einzelne kleine Speicherblöcke verweisen, kann dies insbesondere bei Prozessen, die sehr lange laufen, zur Fragmentierung des Adressraumes führen, so dass der Prozess keinen weiteren Speicher anfordern kann, obwohl die Summe der allozierten Speicherblöcke wesentlich geringer als der verfügbare Speicher ist.
- Die Effizienz des Prozessor-Caches leidet darunter, wenn eine Datenstruktur auf viele Speicherblöcke verweist, die im Adressraum weit auseinander liegen. Daher kann es sinnvoll sein, stattdessen Arrays zu verwenden, weil diese eine kompakte Darstellung im Speicher haben.
- Letzteres kann sich auch negativ im Zusammenhang mit Paging auswirken.
- Nicht zuletzt ist ein Pointer eine typische Ansatzstelle von Malware: Das Schadprogramm braucht nur eine Stelle zu ändern, um auf den eigenen Programmcode zu verlinken: Gibt es keine saubere Kontrolle des für das Programm reservierten Speicherbereichs, kann der auch beliebig anderswo liegen. Außerdem sind über fehlgeleitete Pointer auch Pufferüberläufe einfach zu erzeugen. Im Besonderen können so in Datenvariablen liegende Programmcodes zur Ausführung gelangen, das ist also eine typische Erstinfektionstechnologie.
Intelligente Zeiger
Als Intelligente Zeiger (smart pointers) werden Objekte bezeichnet, die einfache Zeiger einkapseln und mit zusätzlichen Funktionen und Eigenschaften ausstatten. Z. B. könnte ein "smart pointer" ein dynamisch alloziertes Speicherobjekt freigeben, sobald die letzte Referenz darauf gelöscht wird.
Zeiger auf eine COM- oder CORBA-Schnittstelle sind in manchen Programmiersprachen (z. B. Delphi) als Intelligenter Zeiger implementiert.
Funktionszeiger (Methodenzeiger)
Funktionszeiger bilden eine besondere Klasse von Zeigern. Sie zeigen nicht auf einen Bereich im Datensegment, sondern auf den Einsprungspunkt einer Funktion im Codesegment des Speichers. Damit ist es möglich, benutzerdefinierte Funktionsaufrufe, deren Ziel erst zur Laufzeit bestimmt wird, zu realisieren. Funktionszeiger kommen häufig in Verbindung mit Rückruffunktionen (callback function) zum Einsatz und stellen eine Form der späten Bindung dar.
Siehe auch: Methodenzeiger
Weblinks
Fußnoten und Einzelnachweise
- ↑ MSDN über unsicheren Code und Zeiger in C#
- ↑ Bjarne Stroustrup: C++ Style and Technique FAQ
Kategorien:- Programmierung
- Programmiersprachelement
Wikimedia Foundation.