Wikipedia · einfach zusammengefasst · Stand
Zeiger (Informatik)
Mit Zeiger (englisch pointer) wird in der Informatik ein Objekt einer Programmiersprache bezeichnet, das eine Speicheradresse zwischenspeichert.
Inhalt6 Abschnitte
Grundprinzip und Bedeutung
Ein Zeiger ist ein Objekt einer Programmiersprache, das eine Speicheradresse zwischenspeichert. Er verweist damit auf einen Ort im Hauptspeicher, an dem beispielsweise eine Variable, ein Objekt oder eine Programmanweisung liegt. Der Zugriff auf den dort gespeicherten Inhalt heißt Dereferenzieren. Enthält etwa die Variable b die Binärzahl 01101101 und die Zeigervariable a die Adresse von b, beispielsweise hexadezimal 1008, dann führt die Dereferenzierung von a zum Wert von b. Im beschriebenen Beispiel passen Adresse und Daten jeweils in ein 32-Bit-Wort.
Zeiger sind eine mögliche und in einigen Programmiersprachen die einzige Umsetzung des Konzepts einer Referenz; deshalb werden sie auch als Referenzvariablen bezeichnet. Sie sind besonders für die dynamische Speicherverwaltung wichtig: Ein Programm kann zur Laufzeit Speicher anfordern, über Zeiger darauf zugreifen und ihn später wieder freigeben. Auch dynamische Datenstrukturen wie verkettete Listen werden gewöhnlich mithilfe von Zeigern aufgebaut.
Zeiger in Programmiersprachen und Typisierung
Zeiger treten vor allem in maschinennahen Sprachen wie Assembler und in Systemprogrammiersprachen wie C und C++ auf. Dort lassen sich Zeiger auf Speicherstellen erzeugen und teilweise rechnerisch verändern. Streng typisierte Sprachen wie Modula-2 oder Ada schränken ihren Gebrauch stark ein. In Java oder Eiffel sind Zeiger intern vorhanden, für Programmierende aber verborgen. Objektorientierte Sprachen verwenden stattdessen alternativ, wie C++, oder ausschließlich, wie Java und Python, Referenzen, die nicht ausdrücklich dereferenziert werden müssen.
Die Typisierung gibt an, ob aus dem Zeigertyp hervorgeht, welchen Datentyp der referenzierte Wert besitzt. Ein typisierter Zeiger darf nur auf Werte seines zugeordneten Typs zeigen. Ein Zeiger auf Integer verweist also auf einen Integer. Dadurch kann der Compiler Verstöße gegen die Typkompatibilität erkennen. In C ist die Typangabe außerdem für Zeigerarithmetik nötig, weil der Compiler die Speichergröße des referenzierten Typs kennen muss, um die Adresse des vorigen oder nächsten Elements zu berechnen.
Untypisierte Zeiger lassen zunächst keinen direkten Zugriff zu: Vor dem Dereferenzieren, Inkrementieren oder Dekrementieren müssen sie in einen typisierten Zeigertyp umgewandelt werden. Beispiele sind void* in C, C++ und D, id in Objective-C sowie POINTER in Pascal. Einige höhere Programmiersprachen besitzen keine untypisierten Zeiger.
In C# und Visual Basic .NET werden die üblichen Aufgaben von Zeigern grundsätzlich durch andere Konzepte wie Delegate übernommen. C# erlaubt jedoch innerhalb eines mit unsafe gekennzeichneten Blocks Zeiger ähnlich wie C++; solcher Code muss speziell kompiliert werden und kann in manchen Fällen die Leistung verbessern. Als Verweistyp kommen dort nur elementare Datentypen, Aufzählungs-, Zeiger- und Strukturtypen infrage. Konvertierungen zwischen Zeigertypen sowie zwischen Zeigern und ganzzahligen Datentypen sind möglich. VB.NET bietet diese Möglichkeit nicht.
Besondere Zeigerarten
Ein Nullzeiger ist ein besonderer Nullwert, der nicht zwingend als numerische 0 dargestellt wird. Er zeigt an, dass eine Zeigervariable auf „nichts“ verweist, und dient häufig als festgelegte Leerstelle. Bei einer einfach verketteten Liste erhält beispielsweise der Folgezeiger des letzten Elements den Nullzeiger, um zu kennzeichnen, dass kein weiteres Element existiert. Rust bildet eine Ausnahme von der verbreiteten Verwendung von Nullzeigern. In Pascal und Object Pascal heißt der Wert nil. In C bezeichnet das Makro NULL aus der Standardbibliothek den Nullzeiger und verdeckt seine interne Darstellung. In C++ ist NULL als Makro für 0 definiert; seit C++11 gibt es zusätzlich die typsichere Konstante nullptr. Donald Knuth verwendet dafür das Symbol Λ, ebenso die Werkzeuge WEB und CWEB. Das Dereferenzieren eines Nullzeigers ist gewöhnlich unzulässig und kann undefiniertes Verhalten, eine Ausnahme, eine Schutzverletzung oder einen Programmabbruch auslösen.
Ein ungültiger Zeiger verweist nicht auf einen gültigen Speicherbereich des erwarteten Typs. Ein uninitialisierter Zeiger wurde vor seiner Verwendung nicht auf einen gültigen Wert gesetzt. Ein wilder Zeiger verweist noch auf eine inzwischen ungültige Speicheradresse. Beim Dereferenzieren können unerwartetes Verhalten oder eine Schutzverletzung entstehen.
Intelligente Zeiger, englisch smart pointers, sind Objekte, die gewöhnliche Zeiger einkapseln und um Verwaltungsfunktionen ergänzen. Ein intelligenter Zeiger kann beispielsweise ein dynamisch angelegtes Speicherobjekt freigeben, sobald die letzte Referenz darauf gelöscht wurde. Zeiger auf COM- oder CORBA-Schnittstellen sind in manchen Sprachen, etwa Object Pascal, als intelligente Zeiger umgesetzt.
Funktions- oder Methodenzeiger zeigen nicht in das Datensegment, sondern auf den Einsprungspunkt einer Funktion im Codesegment. So kann das Ziel eines Funktionsaufrufs erst zur Laufzeit bestimmt werden. Sie werden häufig für Rückruffunktionen, sogenannte Callbacks, eingesetzt und bilden eine Form der späten Bindung.
C++ kennt außerdem Memberzeiger auf Datenmember einer Klasse. Im Artikelbeispiel bezeichnet der Typalias Channel einen Zeiger auf ein uint8_t-Datenmember der Klasse RGB_Pixel. Ein solcher Zeiger kann etwa gezielt auf red, green oder blue gesetzt werden. Wird green ausgewählt, invertiert eine Funktion mit pixel.*channel nur den grünen Kanal jedes Pixels durch die Rechnung 255 − pixel.*channel.
Operationen und Zeigerarithmetik
Zu den grundlegenden Zeigeroperationen gehören:
• Dereferenzieren: Zugriff auf das Objekt, auf das der Zeiger zeigt; bei einem Funktionszeiger kann dies der Aufruf der referenzierten Funktion sein.
• Inkrementieren und Dekrementieren: Versetzen des Zeigers auf das nachfolgende oder vorherige Objekt im Speicher. Intern wird die Größe des referenzierten Objekttyps addiert oder subtrahiert. Daher muss der Typ zur Kompilierzeit bekannt sein.
• Zerstören: Zerstören des referenzierten Objekts, etwa über Destruktoren. Danach wäre es sinnvoll, sämtliche darauf gerichteten Zeigervariablen auf den Nullwert zu setzen, um ungültige Referenzen erkennbar zu machen; allgemein ist dies jedoch nicht immer möglich.
• Vergleichen: Prüfung zweier Zeiger auf Gleichheit oder Ungleichheit. Manche Sprachen erlauben zusätzlich Größer-Kleiner-Vergleiche.
Das Rechnen mit Adressen unter Berücksichtigung der Größe des referenzierten Typs heißt Zeigerarithmetik. Zeiger können erhöht, verringert und voneinander subtrahiert werden. Wird ein Zeiger auf einen vier Byte großen Datentyp um eins erhöht, steigt die gespeicherte Adresse um vier Bytes. Die Differenz zweier Zeiger gibt an, wie viele Objekte des referenzierten Typs zwischen den Adressen Platz finden.
C nutzt Zeigerarithmetik häufig zur Iteration über terminierte Arrays. Im Artikelbeispiel durchläuft ein char**-Zeiger namens cpp das mit NULL abgeschlossene Array argv. Jeder Schritt cpp++ erhöht die Adresse auf einem dargestellten 64-Bit-System um die Größe von char*, dort acht Byte. Beim Aufruf mit ./b foo bar werden drei Argumente ausgegeben; cpp − argv ergibt entsprechend 3. Weil Zeigerarithmetik als fehleranfällig gilt, unterstützen höhere Programmiersprachen sie meist nicht und stellen andere Mittel für dieselben Aufgaben bereit.
Zeiger und Arrays
In C und C++ kann einer Zeigervariablen unmittelbar ein Array zugewiesen werden. Der Zeiger verweist dann auf dessen erstes Element. Bei int triangleNumbers[5] und int* numbersPointer = triangleNumbers zeigt numbersPointer somit auf triangleNumbers[0].
Auch über den Zeiger kann der Index-Operator verwendet werden. Im Beispiel erhalten die fünf Arrayelemente über numbersPointer[0] bis numbersPointer[4] die ersten fünf Dreieckszahlen 0, 1, 3, 6 und 10. Die Dereferenzierung *numbersPointer liefert den Inhalt des ersten Elements, also 0. Der Ausdruck numbersPointer[3] liefert das Element mit Index 3, also 6.
Nutzen, Grenzen und Gefahren
Zeiger können Programme beschleunigen und Speicher sparen. Wenn die benötigte Datenmenge beim Programmstart unbekannt ist, kann zur Laufzeit genau der erforderliche Speicher angefordert und später wieder an das Betriebssystem zurückgegeben werden. In Feldern oder Vektoren lässt sich ein Zeiger auf den Anfang setzen und schrittweise entsprechend der Größe des Datentyps verschieben. Compiler und Programmiersprachen setzen solche Zugriffe teilweise intern automatisch um.
Bei dynamischen Datenstrukturen lassen sich Verweise verändern, beispielsweise zum Sortieren von Listen, ohne die eigentlichen Elemente umzukopieren. Bei einem Funktionsaufruf kann ein Zeiger auf ein Objekt übergeben werden, sodass keine möglicherweise zeitaufwendige Kopie des Objekts nötig ist; dies wird als Referenzparameter verwendet. Mehrere Zeiger können auf dieselbe Variable verweisen, ohne deren Wert mehrfach zu speichern. Bei Zeichenketten ist ein direkter Zugriff auf Speicherinhalte möglich.
Dem stehen erhebliche Risiken gegenüber. Zeiger sind schwer zu erlernen und fehleranfällig. In manchen Sprachen lässt sich zur Laufzeit nicht wirksam prüfen, ob die Daten an der Zieladresse dem erwarteten Typ und den Anforderungen des Programms entsprechen. Fehler können Abstürze, Speicherzugriffsverletzungen, unbemerkte Datenbeschädigungen durch vagabundierende Zeiger, Pufferüberläufe oder Speicherlecks verursachen. Bei einem Speicherleck fordert das Programm fortlaufend Speicher an, ohne ihn wieder freizugeben; im Extremfall kann das Betriebssystem keinen weiteren Speicher liefern.
Viele einzelne, durch Zeiger verbundene Speicherblöcke können besonders bei lange laufenden Prozessen den Adressraum fragmentieren. Dann ist möglicherweise kein ausreichend großer weiterer Bereich verfügbar, obwohl die Summe der belegten Blöcke deutlich kleiner als der insgesamt verfügbare Speicher ist. Weit auseinanderliegende Blöcke verschlechtern außerdem die Nutzung des Prozessor-Caches und können sich beim Paging negativ auswirken. Kompakt gespeicherte Tabellen oder Arrays können deshalb günstiger sein.
Zeiger sind auch ein mögliches Angriffsziel für Malware. Wird ein Zeiger so verändert, dass er auf Schadcode zeigt, kann dieser bei fehlender Kontrolle des reservierten Speicherbereichs ausgeführt werden. Fehlgeleitete Zeiger erleichtern außerdem Pufferüberläufe; dadurch kann insbesondere Programmcode zur Ausführung gelangen, der in Datenvariablen liegt. Wegen solcher Fehler- und Sicherheitsrisiken verzichten manche Programmiersprachen auf offen zugängliche Zeiger oder schränken sie stark ein.