Benutzer:Gleu/pointer

aus Wikipedia, der freien Enzyklopädie
Der Zeiger a zeigt auf Variable b. Die Variable b enthält eine Nummer (hexadezimal 01101101) und die Variable a enthält die Speicheradresse von b (hexadezimal 1008). In diesem Fall passen die Adresse und die Daten in ein 32-bit-Wort.

Mit Zeiger (auch engl. Pointer) wird in der Informatik eine spezielle Variable bezeichnet, die eine Speicheradresse enthält. An dieser Adresse können entweder Daten, wie Variablen oder Objekte, aber auch Programmcode, stehen.

Mit dem Begriff Zeiger wird fälschlicherweise häufig auch der Datentyp des Zeigers bezeichnet, hier muss allerdings richtig vom Zeigertyp gesprochen werden. Der Zeigertyp beschreibt welchen Typ die dereferenzierten Daten haben.

Es wird zwischen zwei Zugriffsverfahren unterschieden:

  • Wird auf den Wert des Zeigers, somit die gespeicherten Adresse, zugegriffen, so spricht man auch vom Zugriff auf die Adresse des referenzierten Elementes. Im nebenstehenden Beispiel erhielte man so die Hexadezimalzahl 1008.
  • Wird über den Zeiger auf den Wert des verwiesenen Elements zugegriffen, so nennt man diese Operation Dereferenzierung. Im nebenstehenden Beispiel erhielte man so die Zahl 17.

Zeiger werden weiterhin 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.

Verwendung

Zeiger sind eine sehr dünne Abstraktionsschicht zu den Adressierungsmöglichkeiten, die moderne Rechnerarchitekturen bereitstellen. Speicher im System wird mit Adressen, im einfachsten Fall numerische Indizes, angesprochen. Um den Speicher zu beschreiben, oder um auf gespeicherte Werte zuzugreifen, stellt die CPU Operationen zur Auflösungen von Adressen zur Verfügung. Pointer sind eine Abbildung dieser Funktionalität in Programmiersprachen.

Es gibt typisierte und untypisierte Zeiger. Typisierte Zeiger werden dazu verwendet die gespeicherten Daten zu interpretieren, da Datentypen unterschiedlichen Speicherbedarf haben (zum Beispiel char 8 Bit und int 32 Bit). Mit dem Typ wird die Interpretation und die Speichergröße der Daten festgelegt. Mit dem Wissen um die Größe des assoziierten Typs kann der Zeiger dereferenziert werden und auf den Wert an der Speicherstelle zugegriffen werden. Um Adressarithmetik durchzuführen können auch die Adressen 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 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. Untypisierte Zeiger sind schwer zu lesen und zu verstehen. In höheren Programmiersprachen existieren zum Teil keine untypisierten Zeiger, da die selben Aufgaben mit Polymorphie einfacher zu erledigen sind.

Bei Funktionsaufrufen kann statt einer Variable, ein Pointer darauf übergeben werden(Call by reference). Dies hat zur Folge, dass keine Kopie der Variable im Stack erzeugt werden muss, sondern, dass auf den Ursprungsdaten gearbeitet wird. Bei großen Variablen wie Arrays kann CPU Zeit und Speicher gespart werden, da nur ein Zeiger und nicht die komplette Struktur kopiert werden muss.

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 häufig verwendet, da mittels des Nullzeigers eine designierte Leerstelle gekennzeichnet wird. 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 kann. Wird es dennoch versucht gibt es undefiniertes Verhalten oder das Betriebssystemen bricht das Programm mit einer Schutzverletzung ab.

Zeigeroperationen

Da im Zeiger nur Adressen, also ganze Zahlen gespeichert werden, sind theoretisch alle Operationen für Integer möglich. Jedoch sind nicht alle sinnvoll und es werden nur folgende unterstützt:

  • Zuweisung: Der Zeiger wird mit der Adresse einer Variablen, ein Objekt oder eine Funktion initialisiert.
  • Dereferenzierung: Der Zugriff auf die Variable oder das Objekt, auf das der Zeiger zeigt, wird Dereferenzierung genannt.
  • Inkrementieren/Dekrementieren: Die Speicheradresse, die im Pointer gespeichert ist, wird um die Speichergröße des Pointertyps erhöht oder vermindert. Der Anwender muss selber überprüfen ob der Pointer auf einen gültigen Wert zeigt, oder nicht.
  • Erzeugen/Zerstören:: Es wird ein Objekt des Pointertyps im Heap erzeugt oder zerstört. Diese Operationen ist für dynamische Speichererstellung wichtig.
  • Vergleichen:: Zeiger können mit anderen Zeigern verglichen werden. Dies kann zum Beispiel dazu verwendet werden Iterationen zu stoppen. Ein Sonderfall ist der Vergleich mit dem Nullpointer um einen Pointer auf Existenz zu überprüfen.

Zeigerarithmetik

Das Erhöhen oder Verringern eines Zeigers um einen konstanten Wert oder das Subtrahieren zweier Zeiger wird als Zeigerarithmetik bezeichnet. Diese Operationen werden zum Beispiel dazu verwendet in großen Arrays zu navigieren. Mit Hilfe von Speicherarithmetik kann ein enormer Performancegewinn erzielt werden, jedoch ist Zeigearithmetik ist kompliziert, fehleranfällig und schlecht zu lesen. Bei unsauberer Programmierung kann es zu Pufferüberläufen kommen, oder Daten können ungewollt durch vagabundierende Zeiger manipuliert werden.

Verwendung in Datenstrukturen

Bei der Erstellung von dynamischen Datenstrukturen wie Listen, Bäumen und Warteschlangen werden Pointer verwendet um die Strukturen zu kontrollieren.

Einfach verkettete Liste.png

Bei einer einfach verketten Liste wird eine Datenstruktur definiert, die neben den eigentlichen Nutzdaten einen Zeiger auf das folgende Element enthält. Im Bild werden die Folgepointer mit blauen Pfeilen dargestellt. Der rote Pfeil entspricht einem Zeiger auf das erste Element und kann somit als Zeiger auf die gesamte Liste interpretiert werden. Wenn auf das nächste Element zugegriffen werden soll, muss der Zeiger dereferenziert werden. Wenn das Listenende erreicht ist, zeigt der Folgepointer auf den Nullzeiger, hier mit NIL gekennzeichnet.

Da der Speicher dynamisch allokiert wird, kann genau so viel Speicher bereitgestellt werden, wie benötigt (Dynamische Speicherverwaltung). Wenn sich die Anforderungen während des Programmablaufs ändern, kann Speicher angefordert oder freigegeben werden. Es ist darauf zu achten, dass der nicht mehr benötigte Speicher wieder freigegeben wird, da es sonst zu Speicherlöchern kommen kann. Des weiteren muss der Pointer, der auf den freigegebenen Speicher zeigt wieder auf den Nullzeiger gesetzt werden, da es sonst möglich ist, dass das Programm nicht mehr gültigen Speicher manipuliert (wilder Zeiger). Dies führt zu undefinierten Verhalten, oder zu einer Schutzverletzung.

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

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 und 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.

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.

Beispiel C

/* Basics */
int a = 100;
int *ptr = NULL;  /* Null Pointer */
ptr = &a;         /* ptr zeigt auf a */
*ptr = 10;        /* a ist 10 */


/* Adressarithmetik */ 
int array[5];      /* Deklariert ein Array von 5 im Speicher aufeinander folgender Integers */
ptr = array;       /* Arrays koennen wie Pointer verwendet werden */
                   /* ptr zeigt auf array [0] */
for (int i = 0; i < 5; ++i) {
   *ptr = i;       /* Speicher an Stelle ptr ist i */
   ptr++;          /* Speicher wird um eine Stelle inkrementiert */ 
}               

/* Der Inhalt des Arrays ist:
array [0] = 0
array [1] = 1
array [2] = 2
array [3] = 3
array [4] = 4
*/

Abschließende Betrachtung

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 allokiert 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 hindurch gelaufen. 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). Auf diese Weise können In-place Algorithmen sehr einfach realisiert werden.
  • Bei Funktionsaufrufen wird statt der Instanz von einer Variable, ein Pointer auf die Variable übergeben (Call by reference). Dies hat zur Folge, dass keine Kopie der Variable im Stack erzeugt werden muss, sondern, dass auf den Ursprungsdaten gearbeitet wird. Vor allem bei großen Variablen wie Arrays kann sehr einfach CPU Zeit gespart werden.
  • Anstatt Variablen jedes Mal zu kopieren und so jedes Mal erneut Speicherplatz zu allokieren, kann können mehrere Zeiger auf dieselbe Variable verweisen.

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.
  • Sicherheitsprobleme wie Pufferüberläufe
  • Unbemerkte Beschädigung von Daten durch vagabundierende Zeiger
  • 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.
  • Falls eine Zeigervariable dereferenziert wird, die nicht auf einen gültigen Speicherbereich des entsprechenden Typs zeigt, kann es zu unerwartetem Verhalten kommen. So kann eine Situation auftreten, wenn eine Variable vor ihrer Benutzung nicht auf eine gültige Adresse 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.
  • Untypisierte Zeiger sind schwer zu lesen und zu verstehen

Intelligente Zeiger (smart pointers)

Als Intelligente Zeiger (smart pointers) werden Objekte bezeichnet, die einfache Zeiger einkapseln und mit zusätzlichen Funktionen und Eigenschaften ausstatten. Zum Beispiel könnte ein smart pointer ein dynamisch alloziertes Speicherobjekt freigeben, sobald die letzte Referenz darauf gelöscht wird. Mit Hilfe von Smart Pointern können die meisten Gefahren bei der Verwendung von Zeigern vermieden werden.

Zeiger auf eine COM- oder CORBA-Schnittstelle sind in manchen Programmiersprachen (z. B. Delphi) als Intelligenter Zeiger implementiert.

Arbeiten mit Zeigern[2] [3]

  • Alle Zeiger werden explizit mit einem Wert instantiiert, und sei es mit dem Nullzeiger.
  • Für jede Allokierung gibt es das als Gegenteil das Löschen.
  • Nachdem ein Zeiger gelöscht ist ist er explizit auf auf den Nullzeiger zu setzen.
  • Vor Gebrauch wird jeder Zeiger überprüft, ob er auf den Nullzeiger steht.
  • Um keine manuelle Speicherverwaltung zu betreiben sollen Smart Pointer verwendet werden.
  • Der Allokierer eines Zeigers ist für die Löschung verantwortlich.

Weblinks

Referenzen

  1. MSDN über unsicheren Code und Zeiger in C#
  2. http://cplus.about.com/od/learning1/ss/pointers_8.htm Some Rules with Pointers
  3. http://stackoverflow.com/questions/1721862/dos-and-donts-while-using-pointers DO’s and Donts while using pointers