Zeiger (Computerprogrammierung) - Pointer (computer programming)

Zuweisungsanweisungen und Zeigervariablen zählen für mich zu den „wertvollsten Schätzen“ der Informatik.

Donald Knuth , Structured Programming, mit go to Statements

Zeiger a , der auf die der Variable b zugeordnete Speicheradresse zeigt . In diesem Diagramm verwendet die Rechenarchitektur den gleichen Adressraum und Daten primitive beide für Zeiger und nicht-Zeiger; diese Notwendigkeit sollte nicht der Fall sein.

In der Informatik ist ein Zeiger in vielen Programmiersprachen ein Objekt , das eine Speicheradresse speichert . Dies kann der Wert eines anderen Werts sein, der sich im Computerspeicher befindet , oder in einigen Fällen der der speicherabgebildeten Computerhardware . Ein Zeiger verweist auf eine Stelle im Speicher, und das Erhalten des an dieser Stelle gespeicherten Werts wird als Dereferenzieren des Zeigers bezeichnet. Als Analogie könnte eine Seitenzahl im Index eines Buches als Zeiger auf die entsprechende Seite angesehen werden; die Dereferenzierung eines solchen Zeigers würde erfolgen, indem man zu der Seite mit der angegebenen Seitennummer blättert und den auf dieser Seite gefundenen Text liest. Das tatsächliche Format und der Inhalt einer Zeigervariablen hängen von der zugrunde liegenden Computerarchitektur ab .

Mit Zeiger deutlich verbessert die Leistung für sich wiederholende Vorgänge, wie durchqueren iterable Datenstrukturen (zB Strings , Lookup - Tabellen , Steuertabellen und Baumstrukturen). Insbesondere ist es oft viel billiger, Zeiger zu kopieren und zu dereferenzieren, als die Daten, auf die die Zeiger zeigen, zu kopieren und darauf zuzugreifen.

Zeiger werden auch verwendet, um die Adressen von Einstiegspunkten für aufgerufene Unterprogramme in der prozeduralen Programmierung und für die Laufzeitverknüpfung mit Dynamic Link Libraries (DLLs) zu halten . In der objektorientierten Programmierung werden Zeiger auf Funktionen zum Binden von Methoden verwendet , oft unter Verwendung virtueller Methodentabellen .

Ein Zeiger ist eine einfache, mehr konkrete Umsetzung des abstraktere Referenzdatentypen . Mehrere Sprachen, insbesondere Low-Level-Sprachen , unterstützen eine Art von Zeigern, obwohl einige mehr Einschränkungen bei der Verwendung haben als andere. Während "Zeiger" verwendet wurde, um allgemein auf Referenzen zu verweisen, trifft es eher auf Datenstrukturen zu, deren Schnittstelle explizit die Manipulation des Zeigers erlaubt (arithmetisch über Zeigerarithmetik ) als Speicheradresse, im Gegensatz zu einemmagischen Cookieoder einerFähigkeit,die dies nicht zulässt. Da Zeiger sowohl einen geschützten als auch einen ungeschützten Zugriff auf Speicheradressen ermöglichen, sind insbesondere im letzteren Fall mit ihrer Verwendung Risiken verbunden. Primitive Zeiger werden oft in einem Format ähnlich einemIntegergespeichert; der Versuch, einen solchen Zeiger zu dereferenzieren oder "nachzuschlagen", dessen Wert keine gültige Speicheradresse ist, könnte jedoch zumAbsturzeines Programms führen(oder ungültige Daten enthalten). Um dieses potenzielle Problem zu mildern, werden Zeiger ausGründen derTypsicherheitals separater Typ betrachtet, der durch den Datentyp parametrisiert wird, auf den sie zeigen, selbst wenn die zugrunde liegende Darstellung eine ganze Zahl ist. Es können auch andere Maßnahmen ergriffen werden (wie zum BeispielValidierungundGrenzüberprüfung), um zu überprüfen, ob die Zeigervariable einen Wert enthält, der sowohl eine gültige Speicheradresse ist als auch innerhalb des numerischen Bereichs liegt, den der Prozessor adressieren kann.

Geschichte

1955 erfand die sowjetische Informatikerin Kateryna Juschtschenko die Programmiersprache Address , die indirekte Adressierung und Adressen höchsten Ranges – analog zu Zeigern – ermöglichte. Diese Sprache war auf den Computern der Sowjetunion weit verbreitet. Außerhalb der Sowjetunion war es jedoch unbekannt, und normalerweise wird Harold Lawson die Erfindung des Zeigers im Jahr 1964 zugeschrieben. Im Jahr 2000 wurde Lawson der Computer Pioneer Award der IEEE verliehen "[f]or inventing the pointer variable and Introduction dieses Konzepts in PL/I und bietet damit erstmals die Möglichkeit, Linked Lists allgemein und flexibel zu behandeln". Hochsprache". Sein wegweisendes Papier zu den Konzepten erschien in der CACM-Ausgabe vom Juni 1967 mit dem Titel: PL/I List Processing. Nach Angaben des Oxford English Dictionary tauchte das Wort Pointer erstmals in gedruckter Form als Stack-Pointer in einem technischen Memorandum der System Development Corporation auf .

Formale Beschreibung

In der Informatik ist ein Zeiger eine Art Referenz .

Ein Datenprimitiv (oder einfach nur primitiv ) ist ein beliebiges Datum, das unter Verwendung eines Speicherzugriffs aus dem Computerspeicher gelesen oder in diesen geschrieben werden kann (beispielsweise sind sowohl ein Byte als auch ein Wort Primitive).

Ein Datenaggregat (oder einfach Aggregat ) ist eine Gruppe von Grundelementen, die im Speicher logisch zusammenhängen und die kollektiv als ein Datum betrachtet werden (zum Beispiel könnte ein Aggregat aus 3 logisch zusammenhängenden Bytes bestehen, deren Werte die 3 Koordinaten von a . darstellen Punkt im Raum). Wenn ein Aggregat vollständig aus dem gleichen Primitivtyp besteht, kann das Aggregat als Array bezeichnet werden ; in gewissem Sinne, ein Multi-Byte - Wort primitiv ist ein Array von Bytes, und einige Programme verwenden Worte auf diese Weise.

Im Kontext dieser Definitionen ist ein Byte das kleinste Primitiv; jede Speicheradresse spezifiziert ein anderes Byte. Die Speicheradresse des Anfangsbytes eines Datums wird als Speicheradresse (oder Basisspeicheradresse ) des gesamten Datums betrachtet.

Ein Speicherzeiger (oder nur Zeiger ) ist ein Grundelement, dessen Wert als Speicheradresse verwendet werden soll; man sagt, dass ein Zeiger auf eine Speicheradresse zeigt . Es wird auch gesagt, dass ein Zeiger auf ein Datum [im Speicher] zeigt, wenn der Wert des Zeigers die Speicheradresse des Datums ist.

Allgemeiner gesagt ist ein Zeiger eine Art Referenz , und es wird gesagt, dass ein Zeiger auf ein Datum verweist, das irgendwo im Speicher gespeichert ist ; um dieses Datum zu erhalten, muss der Zeiger dereferenziert werden . Das Merkmal, das Zeiger von anderen Referenzarten unterscheidet, besteht darin, dass der Wert eines Zeigers als Speicheradresse interpretiert werden soll, was ein eher untergeordnetes Konzept ist.

Referenzen dienen als Indirektionsebene: Der Wert eines Zeigers bestimmt, welche Speicheradresse (dh welches Datum) in einer Berechnung verwendet werden soll. Da Indirektion ein grundlegender Aspekt von Algorithmen ist, werden Zeiger in Programmiersprachen oft als grundlegender Datentyp ausgedrückt ; in statisch (oder stark ) getippt Programmiersprachen, die Art bestimmt , einen Zeigers die Art des Datums , an dem der Zeiger zeigt.

Architektonische Wurzeln

Zeiger sind eine sehr dünne Abstraktion zusätzlich zu den Adressierungsfunktionen, die die meisten modernen Architekturen bieten . Im einfachsten Schema wird jeder Speichereinheit im System eine Adresse oder ein numerischer Index zugewiesen, wobei die Einheit typischerweise entweder ein Byte oder ein Wort ist – je nachdem, ob die Architektur byte- oder wortadressierbar ist – wandelt effektiv den gesamten Speicher in ein sehr großes Array um . Das System würde dann auch eine Operation bereitstellen, um den in der Speichereinheit an einer gegebenen Adresse gespeicherten Wert abzurufen (normalerweise unter Verwendung der Universalregister der Maschine ).

Im Normalfall ist ein Zeiger groß genug, um mehr Adressen aufzunehmen, als im System Speichereinheiten vorhanden sind. Dadurch besteht die Möglichkeit, dass ein Programm versucht, auf eine Adresse zuzugreifen, die keiner Speichereinheit entspricht, entweder weil nicht genügend Speicher installiert ist (dh außerhalb des verfügbaren Speicherbereichs) oder die Architektur solche Adressen nicht unterstützt. Der erste Fall kann bei bestimmten Plattformen wie der Intel x86- Architektur als Segmentierungsfehler (segfault) bezeichnet werden. Der zweite Fall ist in der aktuellen Implementierung von AMD64 möglich , wo Zeiger 64 Bit lang sind und Adressen sich nur auf 48 Bit erstrecken. Zeiger müssen bestimmten Regeln (kanonischen Adressen) entsprechen. Wenn also ein nicht-kanonischer Zeiger dereferenziert wird, löst der Prozessor einen allgemeinen Schutzfehler aus .

Andererseits haben einige Systeme mehr Speichereinheiten als Adressen vorhanden sind. In diesem Fall wird ein komplexeres Schema wie Speichersegmentierung oder Paging verwendet, um unterschiedliche Teile des Speichers zu unterschiedlichen Zeiten zu verwenden. Die letzten Inkarnationen der x86-Architektur unterstützen bis zu 36 Bit physikalischer Speicheradressen, die über den PAE- Paging-Mechanismus auf den 32-Bit-linearen Adressraum abgebildet wurden . Somit kann jeweils nur auf 1/16 des möglichen Gesamtspeichers zugegriffen werden. Ein weiteres Beispiel in derselben Computerfamilie war der 16-Bit- geschützte Modus des 80286- Prozessors, der zwar nur 16 MB physischen Speicher unterstützte, aber auf bis zu 1 GB virtuellen Speicher zugreifen konnte, aber die Kombination aus 16-Bit-Adresse und Segment Register machten den Zugriff auf mehr als 64 KB in einer Datenstruktur umständlich.

Um eine konsistente Schnittstelle bereitzustellen, stellen einige Architekturen speicherabgebildete E/A bereit , die es einigen Adressen ermöglicht, sich auf Speichereinheiten zu beziehen, während andere auf Geräteregister anderer Geräte im Computer verweisen . Es gibt analoge Konzepte wie Datei-Offsets, Array-Indizes und Verweise auf entfernte Objekte, die zum Teil denselben Zwecken wie Adressen für andere Objekttypen dienen.

Verwendet

Zeiger werden in Sprachen wie PL/I , C , C++ , Pascal , FreeBASIC und implizit in den meisten Assemblersprachen ohne Einschränkungen direkt unterstützt . Sie werden hauptsächlich zum Erstellen von Referenzen verwendet , die wiederum für den Aufbau fast aller Datenstrukturen grundlegend sind , sowie zum Übergeben von Daten zwischen verschiedenen Teilen eines Programms.

In funktionalen Programmiersprachen, die stark auf Listen angewiesen sind, werden Datenreferenzen abstrakt verwaltet, indem primitive Konstrukte wie cons und die entsprechenden Elemente car und cdr verwendet werden , die man sich als spezialisierte Zeiger auf die erste und zweite Komponente einer cons-Zelle vorstellen kann. Dies führt zu einigen der idiomatischen "Geschmacksrichtungen" der funktionalen Programmierung. Durch Strukturieren von Daten in solchen cons-lists erleichtern diese Sprachen rekursive Mittel zum Aufbauen und Verarbeiten von Daten – zum Beispiel durch rekursiven Zugriff auf die Kopf- und Endelemente von Listen von Listen; zB "das Auto von der CD von der CD nehmen". Im Gegensatz dazu, Speicherverwaltung basierend auf Zeiger dereferenzieren in gewissen Annäherung einer Array von Speicheradressen erleichtert die Behandlung von Variablen als Schlitze , in die Daten zugewiesen werden können imperativ .

Beim Umgang mit Arrays beinhaltet die kritische Nachschlageoperation typischerweise eine als Adressenberechnung bezeichnete Stufe , die das Konstruieren eines Zeigers auf das gewünschte Datenelement in dem Array beinhaltet. In anderen Datenstrukturen, wie beispielsweise verknüpften Listen , werden Zeiger als Referenzen verwendet, um ein Stück der Struktur explizit an ein anderes zu binden.

Zeiger werden verwendet, um Parameter als Referenz zu übergeben. Dies ist nützlich, wenn der Programmierer möchte, dass die Änderungen einer Funktion an einem Parameter für den Aufrufer der Funktion sichtbar sind. Dies ist auch nützlich, um mehrere Werte von einer Funktion zurückzugeben.

Zeiger können auch verwendet werden, um dynamische Variablen und Arrays im Speicher zuzuweisen und freizugeben. Da eine Variable oft überflüssig wird, nachdem sie ihren Zweck erfüllt hat, ist es eine Verschwendung von Speicher, sie zu behalten, und daher empfiehlt es sich, die Zuordnung (unter Verwendung der ursprünglichen Zeigerreferenz) aufzuheben, wenn sie nicht mehr benötigt wird. Andernfalls kann es zu einem Speicherverlust kommen (wo verfügbarer freier Speicher allmählich oder in schweren Fällen schnell abnimmt, da sich zahlreiche redundante Speicherblöcke ansammeln).

C-Zeiger

Die grundlegende Syntax zum Definieren eines Zeigers ist:

int *ptr;

Dies wird ptrals Bezeichner eines Objekts des folgenden Typs deklariert :

  • Zeiger, der auf ein Objekt vom Typ zeigt int

Dies wird normalerweise prägnanter ausgedrückt als " ptrist ein Zeiger auf int".

Da die Sprache C keine implizite Initialisierung für Objekte mit automatischer Speicherdauer vorsieht, sollte oft darauf geachtet werden, dass die Adresse, auf die ptrverweist, gültig ist; deshalb ist es manchmal vor, dass ein Zeiger explizit auf den initialisierten vorgeschlagen wird Null - Zeiger - Wert, der in C mit dem standardisierten Makro traditionell angegeben wird NULL:

int *ptr = NULL;

Die Dereferenzierung eines Null-Zeigers in C führt zu undefiniertem Verhalten , das katastrophal sein kann. Die meisten Implementierungen halten jedoch einfach die Ausführung des fraglichen Programms an, normalerweise mit einem Segmentierungsfehler .

Das unnötige Initialisieren von Zeigern könnte jedoch die Programmanalyse behindern und dadurch Fehler verbergen.

Sobald ein Zeiger deklariert wurde, besteht der nächste logische Schritt darin, auf etwas zu zeigen:

int a = 5;
int *ptr = NULL;

ptr = &a;

Dies weist den Wert der Adresse von azu zu ptr. Wenn beispielsweise aam Speicherplatz 0x8130 gespeichert wird , ist der Wert von ptr0x8130 nach der Zuweisung. Um den Zeiger zu dereferenzieren, wird wieder ein Sternchen verwendet:

*ptr = 8;

Dies bedeutet, dass Sie den Inhalt von ptr(der 0x8130 ist) nehmen, diese Adresse im Speicher "lokalisieren" und ihren Wert auf 8 setzen. Wenn aspäter erneut darauf zugegriffen wird, ist ihr neuer Wert 8.

Dieses Beispiel kann klarer sein, wenn der Speicher direkt untersucht wird. Angenommen, es abefindet sich bei Adresse 0x8130 im Speicher und ptrbei 0x8134; Nehmen Sie auch an, dass dies ein 32-Bit-Computer ist, sodass ein int 32 Bit breit ist. Folgendes würde sich im Speicher befinden, nachdem der folgende Codeausschnitt ausgeführt wurde:

int a = 5;
int *ptr = NULL;
Die Anschrift Inhalt
0x8130 0x00000005
0x8134 0x00000000

(Der hier gezeigte NULL-Zeiger ist 0x00000000.) Durch Zuweisen der Adresse von aan ptr:

 ptr = &a;

ergibt folgende Speicherwerte:

Die Anschrift Inhalt
0x8130 0x00000005
0x8134 0x00008130

Dann durch Dereferenzieren ptrdurch Codieren:

 *ptr = 8;

der Computer nimmt den Inhalt von ptr(der 0x8130 ist), 'lokalisieren' diese Adresse und weist diesem Speicherort 8 zu, was den folgenden Speicher ergibt:

Die Anschrift Inhalt
0x8130 0x00000008
0x8134 0x00008130

Offensichtlich wird der Zugriff aden Wert 8 ergeben, da der vorherige Befehl den Inhalt von amittels des Zeigers geändert hat ptr.

Verwendung in Datenstrukturen

Beim Einrichten von Datenstrukturen wie Listen , Warteschlangen und Bäumen sind Zeiger erforderlich, um die Implementierung und Steuerung der Struktur zu verwalten. Typische Beispiele für Zeiger sind Startzeiger, Endzeiger und Stapelzeiger . Diese Zeiger können entweder absolut (die tatsächliche physikalische Adresse oder eine virtuelle Adresse im virtuellen Speicher ) oder relativ (ein Offset von einer absoluten Startadresse ("Basis") sein, die normalerweise weniger Bits als eine vollständige Adresse verwendet, aber normalerweise ein zusätzliches benötigt Rechenoperation zu lösen).

Relative Adressen sind eine Form der manuellen Speichersegmentierung und teilen viele ihrer Vor- und Nachteile. Ein 2-Byte-Offset, der eine 16-Bit-Ganzzahl ohne Vorzeichen enthält, kann verwendet werden, um eine relative Adressierung für bis zu 64 KiB (2 16 Byte) einer Datenstruktur bereitzustellen . Dies kann leicht auf 128, 256 oder 512 KiB erweitert werden, wenn die Adresse, auf die gezeigt wird, gezwungen wird, an einer Halbwort-, Wort- oder Doppelwortgrenze ausgerichtet zu werden (wobei jedoch eine zusätzliche bitweise Operation "nach links verschieben" erforderlich ist - um 1, 2 oder 3 Bit – um den Offset um den Faktor 2, 4 oder 8 anzupassen, bevor er zur Basisadresse addiert wird). Im Allgemeinen sind solche Schemata jedoch sehr mühsam, und aus Gründen der Bequemlichkeit für den Programmierer werden absolute Adressen (und darunter ein flacher Adressraum ) bevorzugt.

Ein Ein-Byte-Offset, wie beispielsweise der hexadezimale ASCII- Wert eines Zeichens (zB X'29'), kann verwendet werden, um auf einen alternativen Integer-Wert (oder Index) in einem Array (zB X'01') zu zeigen. Auf diese Weise können Zeichen sehr effizient von ' Rohdaten ' in einen verwendbaren sequentiellen Index und dann ohne Nachschlagetabelle in eine absolute Adresse übersetzt werden .

C-Arrays

In C ist die Array-Indizierung formal durch Zeigerarithmetik definiert; das heißt, die Sprachspezifikation erfordert, array[i]dass äquivalent zu *(array + i). Daher kann man sich Arrays in C als Zeiger auf aufeinanderfolgende Speicherbereiche (ohne Lücken) vorstellen, und die Syntax für den Zugriff auf Arrays ist identisch mit der, die zum Dereferenzieren von Zeigern verwendet werden kann. Ein Array arraykann beispielsweise wie folgt deklariert und verwendet werden:

int array[5];      /* Declares 5 contiguous integers */
int *ptr = array;  /* Arrays can be used as pointers */
ptr[0] = 1;        /* Pointers can be indexed with array syntax */
*(array + 1) = 2;  /* Arrays can be dereferenced with pointer syntax */
*(1 + array) = 2;  /* Pointer addition is commutative */
array[2] = 4;      /* Subscript operator is commutative */

Dies weist einen Block von fünf ganzen Zahlen zu und benennt den Block array, der als Zeiger auf den Block fungiert. Eine andere übliche Verwendung von Zeigern besteht darin, auf dynamisch zugewiesenen Speicher von malloc zu zeigen, der einen aufeinanderfolgenden Speicherblock von nicht weniger als der angeforderten Größe zurückgibt, der als Array verwendet werden kann.

Während die meisten Operatoren für Arrays und Zeiger gleichwertig sind, unterscheidet sich das Ergebnis des sizeofOperators. In diesem Beispiel sizeof(array)wird ausgewertet zu 5*sizeof(int)(der Größe des Arrays), während sizeof(ptr)ausgewertet wird zu sizeof(int*), der Größe des Zeigers selbst.

Standardwerte eines Arrays können wie folgt deklariert werden:

int array[5] = {2, 4, 3, 1, 5};

Wenn arraysich der Speicher auf einem 32-Bit- Little-Endian- Rechner ab Adresse 0x1000 im Speicher befindet, enthält der Speicher Folgendes (Werte sind hexadezimal , wie die Adressen):

0 1 2 3
1000 2 0 0 0
1004 4 0 0 0
1008 3 0 0 0
100C 1 0 0 0
1010 5 0 0 0

Hier sind fünf Ganzzahlen dargestellt: 2, 4, 3, 1 und 5. Diese fünf Ganzzahlen belegen jeweils 32 Bit (4 Byte), wobei das niedrigstwertige Byte zuerst gespeichert wird (dies ist eine Little-Endian- CPU-Architektur ) und nacheinander gespeichert ab Adresse 0x1000.

Die Syntax für C mit Zeigern lautet:

  • array bedeutet 0x1000;
  • array + 1bedeutet 0x1004: das "+1" bedeutet, die Größe von 1 hinzuzufügen int, was 4 Bytes beträgt;
  • *arraybedeutet, den Inhalt von array. Betrachten Sie den Inhalt als Speicheradresse (0x1000), suchen Sie den Wert an dieser Stelle (0x0002);
  • array[i]bedeutet Elementnummer i, 0-basiert, von arraydenen übersetzt wird in *(array + i).

Das letzte Beispiel ist der Zugriff auf den Inhalt von array. Brechen sie ab:

  • array + ider Speicherplatz des (i) -ten Elements von ist array, beginnend bei i=0;
  • *(array + i) nimmt diese Speicheradresse und dereferenziert sie, um auf den Wert zuzugreifen.

C verknüpfte Liste

Nachfolgend finden Sie eine Beispieldefinition einer verknüpften Liste in C.

/* the empty linked list is represented by NULL
 * or some other sentinel value */
#define EMPTY_LIST  NULL

struct link {
    void        *data;  /* data of this link */
    struct link *next;  /* next link; EMPTY_LIST if there is none */
};

Diese zeigerrekursive Definition ist im Wesentlichen die gleiche wie die referenzrekursive Definition aus der Programmiersprache Haskell :

 data Link a = Nil
             | Cons a (Link a)

Nilist die leere Liste und Cons a (Link a)ist eine Contra- Zelle vom Typ amit einem anderen Link ebenfalls vom Typ a.

Die Definition mit Referenzen ist jedoch typgeprüft und verwendet keine potenziell verwirrenden Signalwerte. Aus diesem Grund werden Datenstrukturen in C in der Regel über Wrapper-Funktionen behandelt , die sorgfältig auf Korrektheit geprüft werden.

Pass-by-Adresse mit Zeigern

Zeiger können verwendet werden, um Variablen anhand ihrer Adresse zu übergeben, wodurch ihr Wert geändert werden kann. Betrachten Sie beispielsweise den folgenden C- Code:

/* a copy of the int n can be changed within the function without affecting the calling code */
void passByValue(int n) {
    n = 12;
}

/* a pointer m is passed instead. No copy of the value pointed to by m is created */
void passByAddress(int *m) {
    *m = 14;
}

int main(void) {
    int x = 3;

    /* pass a copy of x's value as the argument */
    passByValue(x);
    // the value was changed inside the function, but x is still 3 from here on

    /* pass x's address as the argument */
    passByAddress(&x);
    // x was actually changed by the function and is now equal to 14 here

    return 0;
}

Dynamische Speicherzuweisung

In einigen Programmen hängt der erforderliche Speicher davon ab, was der Benutzer eingeben kann. In solchen Fällen muss der Programmierer Speicher dynamisch zuweisen. Dies geschieht, indem Speicher auf dem Heap statt auf dem Stack zugewiesen wird , wo normalerweise Variablen gespeichert werden (Variablen können auch in den CPU-Registern gespeichert werden, aber das ist eine andere Sache). Die dynamische Speicherzuweisung kann nur über Zeiger erfolgen, und Namen (wie bei allgemeinen Variablen) können nicht angegeben werden.

Zeiger werden verwendet, um die Adressen von dynamisch zugewiesenen Speicherblöcken zu speichern und zu verwalten . Solche Blöcke werden verwendet, um Datenobjekte oder Arrays von Objekten zu speichern. Die meisten strukturierten und objektorientierten Sprachen bieten einen Speicherbereich, der als Heap oder freier Speicher bezeichnet wird und aus dem Objekte dynamisch zugewiesen werden.

Der folgende C-Beispielcode veranschaulicht, wie Strukturobjekte dynamisch zugewiesen und referenziert werden. Die Standard-C-Bibliothek stellt die Funktion malloc()zur Zuweisung von Speicherblöcken aus dem Heap bereit . Es nimmt die Größe eines zuzuweisenden Objekts als Parameter und gibt einen Zeiger auf einen neu zugewiesenen Speicherblock zurück, der zum Speichern des Objekts geeignet ist, oder es gibt einen Nullzeiger zurück, wenn die Zuweisung fehlgeschlagen ist.

/* Parts inventory item */
struct Item {
    int         id;     /* Part number */
    char *      name;   /* Part name   */
    float       cost;   /* Cost        */
};

/* Allocate and initialize a new Item object */
struct Item * make_item(const char *name) {
    struct Item * item;

    /* Allocate a block of memory for a new Item object */
    item = malloc(sizeof(struct Item));
    if (item == NULL)
        return NULL;

    /* Initialize the members of the new Item */
    memset(item, 0, sizeof(struct Item));
    item->id =   -1;
    item->name = NULL;
    item->cost = 0.0;

    /* Save a copy of the name in the new Item */
    item->name = malloc(strlen(name) + 1);
    if (item->name == NULL) {
        free(item);
        return NULL;
    }
    strcpy(item->name, name);

    /* Return the newly created Item object */
    return item;
}

Der folgende Code veranschaulicht, wie Speicherobjekte dynamisch freigegeben werden, dh an den Heap oder freien Speicher zurückgegeben werden. Die Standard-C-Bibliothek stellt die Funktion free()zum Freigeben eines zuvor zugewiesenen Speicherblocks und zum Zurückgeben an den Heap bereit .

/* Deallocate an Item object */
void destroy_item(struct Item *item) {
    /* Check for a null object pointer */
    if (item == NULL)
        return;

    /* Deallocate the name string saved within the Item */
    if (item->name != NULL) {
        free(item->name);
        item->name = NULL;
    }

    /* Deallocate the Item object itself */
    free(item);
}

Speicherabgebildete Hardware

Auf einigen Computerarchitekturen können Zeiger verwendet werden, um Speicher oder speicherabgebildete Geräte direkt zu manipulieren.

Das Zuweisen von Adressen zu Pointern ist ein unschätzbares Werkzeug bei der Programmierung von Mikrocontrollern . Unten ist ein einfaches Beispiel, das einen Zeiger vom Typ int deklariert und auf eine hexadezimale Adresse initialisiert, in diesem Beispiel die Konstante 0x7FFF:

int *hardware_address = (int *)0x7FFF;

Mitte der 80er Jahre war die Verwendung des BIOS für den Zugriff auf die Videofunktionen von PCs langsam. Anwendungen, die anzeigeintensiv waren, wurden normalerweise verwendet, um direkt auf den CGA- Videospeicher zuzugreifen, indem die hexadezimale Konstante 0xB8000 in einen Zeiger auf ein Array von 80 vorzeichenlosen 16-Bit-Int-Werten umgewandelt wurde. Jeder Wert bestand aus einem ASCII- Code im Low-Byte und einer Farbe im High-Byte. Um also den Buchstaben 'A' in Zeile 5, Spalte 2 in hellem Weiß auf Blau zu setzen, würde man Code wie den folgenden schreiben:

#define VID ((unsigned short (*)[80])0xB8000)

void foo(void) {
    VID[4][1] = 0x1F00 | 'A';
}

Verwendung in Steuertabellen

Steuertabellen , die verwendet werden, um den Programmfluss zu steuern , verwenden normalerweise in großem Umfang Zeiger. Die Zeiger, die normalerweise in einen Tabelleneintrag eingebettet sind, können beispielsweise verwendet werden, um die Einstiegspunkte zu auszuführenden Unterprogrammen zu halten, basierend auf bestimmten Bedingungen, die in demselben Tabelleneintrag definiert sind. Die Zeiger können jedoch einfach Indizes auf andere separate, aber verbundene Tabellen sein, die ein Array der tatsächlichen Adressen oder die Adressen selbst (je nach verfügbaren Programmiersprachenkonstrukten) umfassen. Sie können auch verwendet werden, um auf frühere Tabelleneinträge zu verweisen (wie bei der Schleifenverarbeitung) oder um einige Tabelleneinträge zu überspringen (wie bei einem Schalter oder "frühen" Verlassen einer Schleife). Für diesen letzteren Zweck kann der "Zeiger" einfach die Tabelleneintragsnummer selbst sein und kann durch einfache Arithmetik in eine tatsächliche Adresse umgewandelt werden.

Getippte Zeiger und Guss

In vielen Sprachen haben Zeiger die zusätzliche Einschränkung, dass das Objekt, auf das sie zeigen, einen bestimmten Typ hat . Zum Beispiel kann ein Zeiger so deklariert werden, dass er auf eine ganze Zahl zeigt ; Die Sprache versucht dann, den Programmierer daran zu hindern, auf Objekte zu verweisen, die keine Ganzzahlen sind, wie beispielsweise Gleitkommazahlen , und eliminiert einige Fehler.

Zum Beispiel in C

int *money;
char *bags;

moneywäre ein Integer-Zeiger und bagsein char-Zeiger. Folgendes würde eine Compiler-Warnung von "Zuweisung von inkompatiblem Zeigertyp" unter GCC ergeben

bags = money;

weil moneyund bagswurden mit verschiedenen Typen deklariert. Um die Compiler - Warnung unterdrücken, muss deutlich gemacht werden , dass Sie tatsächlich wollen , um die Zuordnung machen von typecasting es

bags = (char *)money;

was besagt, dass der Integer-Zeiger von moneyin einen char-Zeiger umgewandelt und an zugewiesen werden soll bags.

Ein Entwurf des C-Standards aus dem Jahr 2005 verlangt, dass das Umwandeln eines Zeigers, der von einem Typ auf einen anderen Typ abgeleitet wurde, die Ausrichtungsgenauigkeit für beide Typen beibehalten sollte (6.3.2.3 Zeiger, Abs. 7):

char *external_buffer = "abcdef";
int *internal_data;

internal_data = (int *)external_buffer;  // UNDEFINED BEHAVIOUR if "the resulting pointer
                                         // is not correctly aligned"

In Sprachen, die Zeigerarithmetik zulassen, berücksichtigt die Arithmetik auf Zeigern die Größe des Typs. Das Hinzufügen einer ganzen Zahl zu einem Zeiger erzeugt beispielsweise einen weiteren Zeiger, der auf eine Adresse zeigt, die um diese Zahl mal die Größe des Typs höher ist. Dies ermöglicht es uns, die Adresse von Elementen eines Arrays eines bestimmten Typs leicht zu berechnen, wie im obigen Beispiel für C-Arrays gezeigt wurde. Wenn ein Zeiger eines Typs in einen anderen Typ einer anderen Größe umgewandelt wird, sollte der Programmierer erwarten, dass die Zeigerarithmetik anders berechnet wird. Wenn das moneyArray in C beispielsweise bei 0x2000 beginnt und sizeof(int)4 Byte groß ist, während sizeof(char)es 1 Byte ist, money + 1zeigt es auf 0x2004, bags + 1würde aber auf 0x2001 zeigen. Andere Risiken des Castings umfassen den Verlust von Daten, wenn "breite" Daten an "enge" Stellen geschrieben werden (zB bags[0] = 65537;), unerwartete Ergebnisse beim Bit-Verschieben von Werten und Vergleichsprobleme, insbesondere bei Werten mit Vorzeichen und ohne Vorzeichen.

Obwohl es im Allgemeinen unmöglich ist, zur Kompilierzeit zu bestimmen, welche Umwandlungen sicher sind, speichern einige Sprachen Laufzeittypinformationen, die verwendet werden können, um zu bestätigen, dass diese gefährlichen Umwandlungen zur Laufzeit gültig sind. Andere Sprachen akzeptieren lediglich eine konservative Annäherung an sichere Besetzungen oder gar keine.

Wert von Zeigern

In C und C++ ist das Ergebnis des Vergleichs zwischen Zeigern undefiniert. In diesen Sprachen und LLVM wird die Regel so interpretiert, dass "nur weil zwei Zeiger auf dieselbe Adresse zeigen, sie nicht gleich sind und austauschbar verwendet werden können", der Unterschied zwischen den Zeigern als ihre Herkunft bezeichnet wird . Obwohl die Umwandlung in einen Integer-Typ, wie z. B. uintptr_teinen Vergleich, bietet, ist die Umwandlung selbst implementierungsdefiniert. Darüber hinaus wird die weitere Konvertierung in Bytes und Arithmetik Optimierer abschrecken, die versuchen, die Verwendung von Zeigern zu verfolgen, ein Problem, das in der akademischen Forschung noch geklärt wird.

Zeiger sicherer machen

Da ein Zeiger einem Programm den Versuch ermöglicht, auf ein möglicherweise nicht definiertes Objekt zuzugreifen, können Zeiger die Ursache für eine Vielzahl von Programmierfehlern sein . Die Nützlichkeit von Zeigern ist jedoch so groß, dass es schwierig sein kann, Programmieraufgaben ohne sie auszuführen. Folglich haben viele Sprachen Konstrukte entwickelt, die einige der nützlichen Funktionen von Zeigern ohne ihre Fallstricke bereitstellen sollen , die manchmal auch als Zeigergefahren bezeichnet werden . In diesem Zusammenhang werden Zeiger, die den Speicher direkt adressieren (wie in diesem Artikel verwendet), als . bezeichnetraw Pointer s, im Gegensatz zuSmart Pointernoder anderen Varianten.

Ein Hauptproblem bei Zeigern besteht darin, dass sie, solange sie direkt als Zahl manipuliert werden können, dazu gebracht werden können, auf ungenutzte Adressen oder auf Daten zu zeigen, die für andere Zwecke verwendet werden. Viele Sprachen, einschließlich der meisten funktionalen Programmiersprachen und neueren imperativer Sprachen wie Java , ersetzen Zeiger durch einen undurchsichtigeren Referenztyp, der normalerweise einfach als Referenz bezeichnet wird und nur verwendet werden kann, um auf Objekte zu verweisen und nicht als Zahlen manipuliert werden, was dies verhindert Art des Fehlers. Als Sonderfall wird die Array-Indizierung behandelt.

Ein Zeiger, dem keine Adresse zugewiesen ist, wird als Wildzeiger bezeichnet . Jeder Versuch, solche nicht initialisierten Zeiger zu verwenden, kann zu unerwartetem Verhalten führen, entweder weil der Anfangswert keine gültige Adresse ist oder weil seine Verwendung andere Teile des Programms beschädigen kann. Das Ergebnis ist oft ein Segmentierungsfehler , eine Speicherverletzung oder eine wilde Verzweigung (bei Verwendung als Funktionszeiger oder Verzweigungsadresse).

In Systemen mit expliziter Speicherzuweisung ist es möglich, einen Dangling-Pointer zu erzeugen, indem die Zuordnung des Speicherbereichs aufgehoben wird, auf den er zeigt. Diese Art von Zeiger ist gefährlich und subtil, da ein freigegebener Speicherbereich die gleichen Daten wie vor der Freigabe enthalten kann, aber dann neu zugeordnet und durch nicht verwandten Code überschrieben werden kann, der dem früheren Code unbekannt ist. Sprachen mit Garbage Collection verhindern diese Art von Fehler, da die Zuordnung automatisch aufgehoben wird, wenn keine Referenzen mehr im Gültigkeitsbereich vorhanden sind.

Einige Sprachen, wie C++ , unterstützen intelligente Zeiger , die eine einfache Form der Referenzzählung verwenden , um die Zuweisung von dynamischem Speicher zu verfolgen und zusätzlich als Referenz zu fungieren. In Abwesenheit von Referenzzyklen, bei denen ein Objekt indirekt über eine Folge von intelligenten Zeigern auf sich selbst verweist, beseitigen diese die Möglichkeit von hängenden Zeigern und Speicherlecks. Delphi- Strings unterstützen die Referenzzählung nativ.

Die Programmiersprache Rust führt einen Borrow-Checker , Pointer-Lifetimes und eine Optimierung basierend auf optionalen Typen für Null-Pointer ein , um Pointer-Bugs zu beseitigen, ohne auf Garbage Collection zurückzugreifen .

Spezielle Arten von Zeigern

Arten definiert durch Wert

Null Zeiger

Ein Nullzeiger hat einen reservierten Wert, um anzuzeigen, dass der Zeiger nicht auf ein gültiges Objekt verweist. Nullzeiger werden routinemäßig verwendet, um Bedingungen wie das Ende einer Liste unbekannter Länge oder das Versäumnis, eine Aktion auszuführen , darzustellen ; Diese Verwendung von NULL- Zeigern kann mit NULL-fähigen Typen und mit dem Wert Nothing in einem Optionstyp verglichen werden .

Baumelnder Zeiger

Ein baumelnder Zeiger ist ein Zeiger, der nicht auf ein gültiges Objekt zeigt und folglich ein Programm zum Absturz bringen oder sich seltsam verhalten kann. In den Programmiersprachen Pascal oder C können Zeiger, die nicht speziell initialisiert sind, auf unvorhersehbare Adressen im Speicher zeigen.

Der folgende Beispielcode zeigt einen baumelnden Zeiger:

int func(void) {
    char *p1 = malloc(sizeof(char)); /* (undefined) value of some place on the heap */
    char *p2;       /* dangling (uninitialized) pointer */
    *p1 = 'a';      /* This is OK, assuming malloc() has not returned NULL. */
    *p2 = 'b';      /* This invokes undefined behavior */
}

Hier p2kann auf eine beliebige Stelle im Speicher verweisen, sodass das Ausführen der Zuweisung *p2 = 'b';einen unbekannten Speicherbereich beschädigen oder einen Segmentierungsfehler auslösen kann .

Wilder Zweig

Wird ein Zeiger als Adresse des Einstiegspunkts zu einem Programm oder Start einer Funktion verwendet, die nichts zurückliefert und auch entweder nicht initialisiert oder beschädigt ist, wird bei einem Aufruf oder Sprung zu dieser Adresse dennoch ein " wilder Zweig" “ soll eingetreten sein. Mit anderen Worten, ein wilder Zweig ist ein Funktionszeiger, der wild ist (dangling).

Die Folgen sind normalerweise unvorhersehbar und der Fehler kann sich auf verschiedene Weise präsentieren, je nachdem, ob der Zeiger eine "gültige" Adresse ist oder nicht und ob (zufällig) ein gültiger Befehl (Opcode) an dieser Adresse vorhanden ist oder nicht. Die Erkennung einer wilden Verzweigung kann eine der schwierigsten und frustrierendsten Debugging-Übungen darstellen, da ein Großteil der Beweise möglicherweise bereits vorher oder durch die Ausführung einer oder mehrerer unangemessener Anweisungen an der Verzweigungsstelle zerstört wurde. Falls vorhanden, kann ein Befehlssatzsimulator normalerweise nicht nur eine wilde Verzweigung erkennen, bevor sie wirksam wird, sondern auch eine vollständige oder teilweise Verfolgung ihrer Geschichte liefern.

Arten definiert durch Struktur

Autorelativer Zeiger

Ein autorelativer Zeiger ist ein Zeiger, dessen Wert als Offset von der Adresse des Zeigers selbst interpretiert wird; Wenn also eine Datenstruktur ein autorelatives Zeigerelement aufweist, das auf einen Teil der Datenstruktur selbst zeigt, kann die Datenstruktur im Speicher verschoben werden, ohne den Wert des autorelativen Zeigers aktualisieren zu müssen.

Das zitierte Patent verwendet auch den Begriff selbstrelativer Zeiger , um dasselbe zu bedeuten. Die Bedeutung dieses Begriffs wurde jedoch auf andere Weise verwendet:

  • bedeutet einen Offset von der Adresse einer Struktur und nicht von der Adresse des Zeigers selbst;
  • bedeutet einen Zeiger, der seine eigene Adresse enthält, was nützlich sein kann, um in einem beliebigen Speicherbereich eine Sammlung von Datenstrukturen zu rekonstruieren, die aufeinander zeigen.

Basierender Zeiger

Ein basierter Zeiger ist ein Zeiger, dessen Wert ein Offset vom Wert eines anderen Zeigers ist. Dies kann zum Speichern und Laden von Datenblöcken verwendet werden, wobei dem Basiszeiger die Adresse des Blockanfangs zugewiesen wird.

Arten, die durch Verwendung oder Datentyp definiert sind

Mehrfache Umleitung

In einigen Sprachen kann ein Zeiger auf einen anderen Zeiger verweisen, was mehrere Dereferenzierungsoperationen erfordert, um zum ursprünglichen Wert zu gelangen. Obwohl jede Indirektionsebene zu Leistungskosten führen kann, ist sie manchmal notwendig, um ein korrektes Verhalten für komplexe Datenstrukturen bereitzustellen . In C ist es beispielsweise typisch, eine verkettete Liste in Form eines Elements zu definieren , das einen Zeiger auf das nächste Element der Liste enthält:

struct element {
    struct element *next;
    int            value;
};

struct element *head = NULL;

Diese Implementierung verwendet einen Zeiger auf das erste Element in der Liste als Ersatz für die gesamte Liste. Wenn am Anfang der Liste ein neuer Wert hinzugefügt wird, headmuss er so geändert werden, dass er auf das neue Element zeigt. Da C-Argumente immer als Wert übergeben werden, ermöglicht die Verwendung der doppelten Indirektion die korrekte Implementierung der Einfügung und hat den wünschenswerten Nebeneffekt, dass Sonderfallcode eliminiert wird, um Einfügungen am Anfang der Liste zu behandeln:

// Given a sorted list at *head, insert the element item at the first
// location where all earlier elements have lesser or equal value.
void insert(struct element **head, struct element *item) {
    struct element **p;  // p points to a pointer to an element
    for (p = head; *p != NULL; p = &(*p)->next) {
        if (item->value <= (*p)->value)
            break;
    }
    item->next = *p;
    *p = item;
}

// Caller does this:
insert(&head, item);

In diesem Fall, wenn der Wert von itemkleiner als der von ist head, wird der Anrufer headrichtig auf die Adresse des neuen Elements aktualisiert.

Ein grundlegendes Beispiel ist das Argument argv für die Hauptfunktion in C (und C++) , das im Prototyp als angegeben wird char **argv– dies liegt daran, dass die Variable argvselbst ein Zeiger auf ein Array von Strings (ein Array von Arrays) *argvist ein Zeiger auf die 0-te Zeichenfolge (nach Konvention der Name des Programms) und **argvist das 0-te Zeichen der 0-ten Zeichenfolge.

Funktionszeiger

In einigen Sprachen kann ein Zeiger auf ausführbaren Code verweisen, dh er kann auf eine Funktion, Methode oder Prozedur zeigen. Ein Funktionszeiger speichert die Adresse einer aufzurufenden Funktion. Obwohl diese Funktion verwendet werden kann, um Funktionen dynamisch aufzurufen, ist sie häufig eine beliebte Technik von Viren- und anderen bösartigen Software-Autoren.

int sum(int n1, int n2) {   // Function with two integer parameters returning an integer value
    return n1 + n2;
}

int main(void) {
    int a, b, x, y;
    int (*fp)(int, int);    // Function pointer which can point to a function like sum
    fp = &sum;              // fp now points to function sum
    x = (*fp)(a, b);        // Calls function sum with arguments a and b
    y = sum(a, b);          // Calls function sum with arguments a and b
}

Zurück-Zeiger

In doppelt verketteten Listen oder Baumstrukturen 'zeigt' ein Zurück-Zeiger, der auf einem Element gehalten wird, auf das Element zurück, das auf das aktuelle Element verweist. Diese sind nützlich für die Navigation und Manipulation auf Kosten einer höheren Speichernutzung.

Simulation mit einem Array-Index

Es ist möglich, das Zeigerverhalten mit Hilfe eines Indexes auf ein (normalerweise eindimensionales) Array zu simulieren.

In erster Linie für Sprachen , die Zeiger explizit nicht unterstützen , aber nicht unterstützen Arrays, das Array kann und verarbeitet wird gedacht , als ob es der gesamte Speicherbereich (im Rahmen des jeweiligen Array) und jeder Index es war , kann als gleichwertig betrachtet werden in ein Allzweckregister in Assembler (das auf die einzelnen Bytes zeigt, aber dessen tatsächlicher Wert relativ zum Anfang des Arrays ist, nicht seiner absoluten Adresse im Speicher). Unter der Annahme der Array ist, sagen wir, eine zusammenhängende 16 - Megabyte - Zeichendatenstruktur , einzelne Bytes (oder eine Zeichenfolge von zusammenhängenden Bytes innerhalb des Arrays) können direkt adressiert und mit dem Namen des Arrays manipulierten mit einem 31 - Bit - unsigned integer als simulierte Zeiger (Dies ist dem oben gezeigten Beispiel für C-Arrays sehr ähnlich ). Zeigerarithmetik kann durch Addieren oder Subtrahieren vom Index simuliert werden, mit minimalem zusätzlichem Overhead im Vergleich zu echter Zeigerarithmetik.

Es ist sogar theoretisch möglich, unter Verwendung der obigen Technik zusammen mit einem geeigneten Befehlssatzsimulator jeden Maschinencode oder das Zwischenprodukt ( Bytecode ) eines beliebigen Prozessors/einer anderen Sprache in einer anderen Sprache zu simulieren , die überhaupt keine Zeiger unterstützt (zum Beispiel Java / JavaScript ). Um dies zu erreichen, kann der Binärcode anfänglich in zusammenhängende Bytes des Arrays geladen werden, damit der Simulator vollständig innerhalb des Speichers desselben Arrays "liest", interpretiert und verarbeitet. Falls erforderlich, vollständig zu vermeiden Pufferüberlauf - Probleme, die Überprüfung Grenzen können in der Regel für den Compiler actioned werden (oder wenn nicht, Hand in den Simulator codiert).

Unterstützung in verschiedenen Programmiersprachen

Ada

Ada ist eine stark typisierte Sprache, bei der alle Zeiger typisiert sind und nur sichere Typkonvertierungen zulässig sind. Alle Zeiger sind standardmäßig mit initialisiert null, und jeder Versuch, über einen nullZeiger auf Daten zuzugreifen, führt zum Auslösen einer Ausnahme . Zeiger in Ada werden als Zugriffstypen bezeichnet . Ada 83 erlaubte keine Arithmetik auf Zugriffstypen (obwohl viele Compiler-Hersteller dies als nicht standardmäßige Funktion vorgesehen haben), aber Ada 95 unterstützt "sichere" Arithmetik auf Zugriffstypen über das Paket System.Storage_Elements.

BASIC

Mehrere alte Versionen von BASIC für die Windows-Plattform unterstützten STRPTR(), um die Adresse einer Zeichenfolge zurückzugeben, und VARPTR(), um die Adresse einer Variablen zurückzugeben. Visual Basic 5 hatte auch Unterstützung für OBJPTR(), um die Adresse einer Objektschnittstelle zurückzugeben, und für einen ADDRESSOF-Operator, um die Adresse einer Funktion zurückzugeben. Die Typen von all diesen sind Ganzzahlen, aber ihre Werte entsprechen denen, die von Zeigertypen gehalten werden.

Neuere BASIC- Dialekte wie FreeBASIC oder BlitzMax haben jedoch erschöpfende Zeigerimplementierungen. In FreeBASIC wird die Arithmetik von ANYZeigern (äquivalent zu C void*) so behandelt, als ob der ANYZeiger eine Bytebreite hätte. ANYZeiger können nicht wie in C dereferenziert werden. Auch das Umwandeln zwischen ANYZeigern und Zeigern eines anderen Typs erzeugt keine Warnungen.

dim as integer f = 257
dim as any ptr g = @f
dim as integer ptr i = g
assert(*i = 257)
assert( (g + 4) = (@f + 1) )

C und C++

In C und C++ sind Zeiger Variablen, die Adressen speichern und null sein können . Jeder Zeiger hat einen Typ, auf den er zeigt, aber man kann frei zwischen Zeigertypen umwandeln (aber nicht zwischen einem Funktionszeiger und einem Objektzeiger). Ein spezieller Zeigertyp, der „void-Zeiger“ genannt wird, erlaubt das Zeigen auf ein beliebiges (Nicht-Funktions-)Objekt, ist jedoch dadurch eingeschränkt, dass es nicht direkt dereferenziert werden kann (es soll gecastet werden). Die Adresse selbst kann oft direkt manipuliert werden, indem ein Zeiger auf und von einem ganzzahligen Typ ausreichender Größe umgewandelt wird, obwohl die Ergebnisse implementierungsdefiniert sind und tatsächlich undefiniertes Verhalten verursachen können; während frühere C-Standards keinen ganzzahligen Typ hatten, der garantiert groß genug war, gibt C99 den uintptr_t typedef- Namen an , der in definiert ist <stdint.h>, aber eine Implementierung muss ihn nicht bereitstellen.

C++ unterstützt C-Zeiger und C-Typecasting vollständig. Es unterstützt auch eine neue Gruppe von Typumwandlungsoperatoren, um einige unbeabsichtigte gefährliche Umwandlungen zur Kompilierzeit abzufangen. Seit C++11 bietet die C++-Standardbibliothek auch intelligente Zeiger ( unique_ptr, shared_ptrund weak_ptr), die in manchen Situationen als sicherere Alternative zu primitiven C-Zeigern verwendet werden können. C++ unterstützt auch eine andere Form der Referenz, die sich ganz von einem Zeiger unterscheidet und einfach als Referenz oder Referenztyp bezeichnet wird .

Zeigerarithmetik , d. h. die Möglichkeit, die Zieladresse eines Zeigers mit arithmetischen Operationen (sowie Größenvergleichen) zu ändern, wird durch den Sprachstandard so eingeschränkt, dass er innerhalb der Grenzen eines einzelnen Array-Objekts (oder direkt danach) bleibt, und wird andernfalls undefiniertes Verhalten aufrufen . Durch Hinzufügen oder Subtrahieren eines Zeigers wird dieser um ein Vielfaches der Größe seines Datentyps verschoben . Wenn Sie beispielsweise 1 zu einem Zeiger auf 4-Byte-Ganzzahlen hinzufügen, wird die Byte-Adresse des Zeigers, auf die gezeigt wird, um 4 erhöht oft das gewünschte Ergebnis. Zeigerarithmetik kann nicht mit voidZeigern durchgeführt werden, da der void-Typ keine Größe hat, und daher kann die angegebene Adresse nicht hinzugefügt werden, obwohl gcc und andere Compiler Bytearithmetik void*als eine nicht standardmäßige Erweiterung durchführen und sie so behandeln, als wäre es char *.

Die Pointer-Arithmetik bietet dem Programmierer eine einzige Möglichkeit, mit verschiedenen Typen umzugehen: Addieren und Subtrahieren der Anzahl der benötigten Elemente anstelle des tatsächlichen Offsets in Bytes. (Zeigerarithmetik mit char *Zeigern verwendet Byte-Offsets, da sizeof(char)per Definition 1 ist.) Insbesondere deklariert die C-Definition explizit, dass die Syntax a[n], die das n-te Element des Arrays aist, äquivalent zu ist *(a + n), was der Inhalt des Elements ist, auf das verwiesen wird durch a + n. Dies impliziert, dass n[a]das äquivalent zu ist a[n], und man kann zB schreiben a[3]oder 3[a]genauso gut auf das vierte Element eines Arrays zugreifen a.

Obwohl die Zeigerarithmetik leistungsfähig ist, kann sie eine Quelle von Computerfehlern sein . Es neigt dazu, unerfahrene Programmierer zu verwirren und sie in verschiedene Kontexte zu zwingen: Ein Ausdruck kann ein gewöhnlicher arithmetischer oder ein zeigerarithmetischer sein, und manchmal ist es leicht, einen mit dem anderen zu verwechseln . Als Reaktion darauf erlauben viele moderne High-Level-Computersprachen (zum Beispiel Java ) keinen direkten Zugriff auf den Speicher unter Verwendung von Adressen. Außerdem behebt der sichere C-Dialekt Cyclone viele der Probleme mit Zeigern. Siehe Programmiersprache C für weitere Diskussion.

Der voidZeiger , oder void*wird in ANSI C und C++ als generischer Zeigertyp unterstützt. Ein Zeiger auf voidkann die Adresse eines beliebigen Objekts (nicht einer Funktion) speichern und wird in C bei der Zuweisung implizit in einen anderen Objektzeigertyp konvertiert, muss jedoch explizit umgewandelt werden, wenn er dereferenziert wird. K&R C verwendet char*für den Zweck des „typagnostischen Zeigers“ (vor ANSI C).

int x = 4;
void* p1 = &x;
int* p2 = p1;       // void* implicitly converted to int*: valid C, but not C++
int a = *p2;
int b = *(int*)p1;  // when dereferencing inline, there is no implicit conversion

C++ erlaubt keine implizite Konvertierung void*in andere Zeigertypen, auch nicht in Zuweisungen. Dies war eine Designentscheidung, um unvorsichtige und sogar unbeabsichtigte Umwandlungen zu vermeiden, obwohl die meisten Compiler nur Warnungen und keine Fehler ausgeben, wenn sie auf andere Umwandlungen stoßen.

int x = 4;
void* p1 = &x;
int* p2 = p1;                     // this fails in C++: there is no implicit conversion from void*
int* p3 = (int*)p1;               // C-style cast
int* p4 = static_cast<int*>(p1);  // C++ cast

In C++ gibt es keine void&(Referenz auf void) zum Komplementieren void*(Zeiger auf void), da Referenzen sich wie Aliase auf die Variablen verhalten, auf die sie zeigen, und es kann niemals eine Variable vom Typ geben void.

Übersicht über die Syntax der Zeigerdeklaration

Diese Zeigerdeklarationen decken die meisten Varianten von Zeigerdeklarationen ab. Natürlich ist es möglich, Tripelzeiger zu haben, aber die Grundprinzipien hinter einem Tripelzeiger existieren bereits in einem Doppelzeiger.

char cff [5][5];    /* array of arrays of chars */
char *cfp [5];      /* array of pointers to chars */
char **cpp;         /* pointer to pointer to char ("double pointer") */
char (*cpf) [5];    /* pointer to array(s) of chars */
char *cpF();        /* function which returns a pointer to char(s) */
char (*CFp)();      /* pointer to a function which returns a char */
char (*cfpF())[5];  /* function which returns pointer to an array of chars */
char (*cpFf[5])();  /* an array of pointers to functions which return a char */

() und [] haben eine höhere Priorität als *.

C#

In der Programmiersprache C# werden Zeiger nur unter bestimmten Bedingungen unterstützt: Jeder Codeblock, der Zeiger enthält, muss mit dem unsafeSchlüsselwort gekennzeichnet werden. Solche Blöcke erfordern normalerweise höhere Sicherheitsberechtigungen, um ausgeführt zu werden. Die Syntax ist im Wesentlichen dieselbe wie in C++, und die Adresse, auf die verwiesen wird, kann entweder verwalteter oder nicht verwalteter Speicher sein. Zeiger auf verwalteten Speicher (jeder Zeiger auf ein verwaltetes Objekt) müssen jedoch mit dem fixedSchlüsselwort deklariert werden , wodurch verhindert wird, dass der Garbage Collector das gezeigte Objekt als Teil der Speicherverwaltung verschiebt, während sich der Zeiger im Gültigkeitsbereich befindet, wodurch die Zeigeradresse gültig bleibt.

Eine Ausnahme hiervon ist die Verwendung der IntPtrStruktur, die ein sicher verwaltetes Äquivalent zu int*ist und keinen unsicheren Code erfordert. Dieser Typ wird häufig zurückgegeben, wenn Methoden aus dem verwendet werden System.Runtime.InteropServices, zum Beispiel:

// Get 16 bytes of memory from the process's unmanaged memory
IntPtr pointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(16);

// Do something with the allocated memory

// Free the allocated memory
System.Runtime.InteropServices.Marshal.FreeHGlobal(pointer);

Das .NET-Framework enthält viele Klassen und Methoden in den Systemund System.Runtime.InteropServices-Namespaces (z. B. die MarshalKlasse), die .NET-Typen (z. B. System.String) in und von vielen nicht verwalteten Typen und Zeigern (z. B. LPWSTRoder void*) konvertieren , um die Kommunikation mit nicht verwaltetem Code zu ermöglichen . Die meisten dieser Methoden haben dieselben Sicherheitsberechtigungsanforderungen wie nicht verwalteter Code, da sie beliebige Stellen im Speicher beeinflussen können.

COBOL

Die Programmiersprache COBOL unterstützt Zeiger auf Variablen. Primitive oder Gruppen-(Datensatz-)Datenobjekte, die innerhalb LINKAGE SECTIONeines Programms deklariert sind, sind von Natur aus zeigerbasiert, wobei der einzige Speicher, der innerhalb des Programms zugewiesen wird, Platz für die Adresse des Datenelements (typischerweise ein einzelnes Speicherwort) ist. Im Programmquellcode werden diese Datenelemente wie jede andere WORKING-STORAGEVariable verwendet, aber auf ihren Inhalt wird implizit indirekt über ihre LINKAGEZeiger zugegriffen .

Speicherplatz für jedes Datenobjekt, auf das gezeigt wird, wird typischerweise dynamisch unter Verwendung externer CALLAnweisungen oder über eingebettete erweiterte Sprachkonstrukte wie EXEC CICSoder -Anweisungen zugewiesenEXEC SQL .

Erweiterte Versionen von COBOL bieten auch mit USAGE IS POINTERKlauseln deklarierte Zeigervariablen . Die Werte solcher Zeigervariablen werden unter Verwendung von SETund SET ADDRESS-Anweisungen festgelegt und geändert .

Einige erweiterte Versionen von COBOL bieten auch PROCEDURE-POINTERVariablen, die die Adressen von ausführbarem Code speichern können .

PL/I

Die PL/I- Sprache bietet volle Unterstützung für Zeiger auf alle Datentypen (einschließlich Zeiger auf Strukturen), Rekursion , Multitasking , String-Handling und umfangreiche integrierte Funktionen . PL/I war ein ziemlicher Sprung nach vorne im Vergleich zu den Programmiersprachen seiner Zeit. PL/I-Zeiger sind nicht typisiert, und daher ist kein Casting für die Dereferenzierung oder Zuweisung von Zeigern erforderlich. Die Deklarationssyntax für einen Zeiger ist DECLARE xxx POINTER;, die einen Zeiger mit dem Namen "xxx" deklariert. Zeiger werden mit BASEDVariablen verwendet. Eine basierende Variable kann mit einem Standard-Locator ( DECLARE xxx BASED(ppp);oder ohne ( DECLARE xxx BASED;), wobei xxx eine basierende Variable ist, die eine Elementvariable, eine Struktur oder ein Array sein kann, und ppp der Standardzeiger ist, deklariert werden). Eine solche Variable kann ohne expliziten Zeigerverweis ( ) xxx=1;adressiert werden oder kann mit einem expliziten Verweis auf den Standardlokator (ppp) oder auf einen anderen Zeiger ( qqq->xxx=1;) adressiert werden .

Zeigerarithmetik ist nicht Teil des PL/I-Standards, aber viele Compiler erlauben Ausdrücke der Form ptr = ptr±expression. IBM PL/I hat auch die eingebaute Funktion PTRADD, um die Arithmetik durchzuführen. Zeigerarithmetik wird immer in Bytes durchgeführt.

IBM Enterprise PL/I-Compiler verfügen über eine neue Form von typisierten Zeigern, die als HANDLE.

D

Die Programmiersprache D ist eine Ableitung von C und C++, die C-Zeiger und C-Typecasting vollständig unterstützt.

Eiffel

Die objektorientierte Sprache Eiffel verwendet Wert- und Referenzsemantik ohne Zeigerarithmetik. Trotzdem werden Zeigerklassen bereitgestellt. Sie bieten Pointer-Arithmetik, Typecasting, explizite Speicherverwaltung, Schnittstellen zu Nicht-Eiffel-Software und andere Funktionen.

Fortran

Fortran-90 führte eine stark typisierte Zeigerfunktion ein. Fortran-Zeiger enthalten mehr als nur eine einfache Speicheradresse. Sie kapseln auch die untere und obere Grenze von Array-Dimensionen, Strides (z. B. um beliebige Array-Abschnitte zu unterstützen) und andere Metadaten. Ein Assoziationsoperator , =>wird verwendet, um a POINTEReiner Variablen TARGETzuzuordnen, die ein Attribut hat. Die Fortran-90- ALLOCATEAnweisung kann auch verwendet werden, um einen Zeiger einem Speicherblock zuzuordnen. Der folgende Code kann beispielsweise verwendet werden, um eine verknüpfte Listenstruktur zu definieren und zu erstellen:

type real_list_t
  real :: sample_data(100)
  type (real_list_t), pointer :: next => null ()
end type

type (real_list_t), target :: my_real_list
type (real_list_t), pointer :: real_list_temp

real_list_temp => my_real_list
do
  read (1,iostat=ioerr) real_list_temp%sample_data
  if (ioerr /= 0) exit
  allocate (real_list_temp%next)
  real_list_temp => real_list_temp%next
end do

Fortran-2003 fügt Unterstützung für Prozedurzeiger hinzu. Außerdem unterstützt Fortran-2003 als Teil der C-Interoperabilitätsfunktion intrinsische Funktionen zum Konvertieren von C-Zeigern in Fortran-Zeiger und zurück.

gehen

Go hat Hinweise. Seine Deklarationssyntax entspricht der von C, ist jedoch umgekehrt geschrieben und endet mit dem Typ. Im Gegensatz zu C verfügt Go über eine Garbage Collection und verbietet Zeigerarithmetik. Referenztypen wie in C++ existieren nicht. Einige eingebaute Typen, wie Maps und Channels, sind geboxt (dh intern sind sie Zeiger auf veränderliche Strukturen) und werden mit der makeFunktion initialisiert . Bei einem Ansatz für eine einheitliche Syntax zwischen Zeigern und Nicht-Zeigern wurde der Pfeiloperator ( ->) weggelassen: Der Punktoperator eines Zeigers bezieht sich auf das Feld oder die Methode des dereferenzierten Objekts. Dies funktioniert jedoch nur mit 1 Indirektionsebene.

Java

In Java gibt es keine explizite Darstellung von Zeigern . Stattdessen werden komplexere Datenstrukturen wie Objekte und Arrays mithilfe von Referenzen implementiert . Die Sprache bietet keine expliziten Zeigermanipulationsoperatoren. Es ist immer noch möglich , Code zu versuchen , einen NULL - Verweis (Null - Zeiger) dereferenzieren jedoch, die Ergebnisse in einer Laufzeit Ausnahme geworfen. Der von nicht referenzierten Speicherobjekten belegte Speicherplatz wird zur Laufzeit automatisch durch die Garbage Collection wiederhergestellt .

Modula-2

Zeiger werden ähnlich wie in Pascal implementiert, ebenso wie VARParameter in Prozeduraufrufen. Modula-2 ist noch stärker typisiert als Pascal, mit weniger Möglichkeiten, dem Typsystem zu entkommen. Einige der Varianten von Modula-2 (wie Modula-3 ) beinhalten die Garbage Collection.

Oberon

Ähnlich wie bei Modula-2 sind Zeiger verfügbar. Es gibt immer noch weniger Möglichkeiten, das Typsystem zu umgehen, und so sind Oberon und seine Varianten in Bezug auf Zeiger immer noch sicherer als Modula-2 oder seine Varianten. Wie bei Modula-3 ist die Garbage Collection Teil der Sprachspezifikation.

Pascal

Im Gegensatz zu vielen Sprachen, die über Zeiger verfügen, erlaubt Standard- ISO- Pascal nur Zeiger, auf dynamisch erstellte Variablen zu verweisen, die anonym sind, und erlaubt ihnen nicht, auf statische oder lokale Standard-Variablen zu verweisen. Es hat keine Zeigerarithmetik. Zeiger müssen auch einen zugeordneten Typ haben und ein Zeiger auf einen Typ ist nicht kompatibel mit einem Zeiger auf einen anderen Typ (zB ein Zeiger auf ein char ist nicht kompatibel mit einem Zeiger auf eine ganze Zahl). Dies trägt dazu bei, die Typsicherheitsprobleme zu beseitigen, die anderen Zeigerimplementierungen innewohnen, insbesondere denen, die für PL/I oder C verwendet werden . Es beseitigt auch einige Risiken, die durch baumelnde Zeiger verursacht werden , aber die Möglichkeit, referenzierten Raum mit der disposeStandardprozedur dynamisch loszulassen (die den gleichen Effekt wie die freeBibliotheksfunktion in C hat ) bedeutet, dass das Risiko baumelnder Zeiger nicht vollständig ist eliminiert.

In einigen kommerziellen und Open-Source-Compiler-Implementierungen von Pascal (oder Derivaten) – wie Free Pascal , Turbo Pascal oder Object Pascal in Embarcadero Delphi – darf ein Zeiger jedoch auf statische oder lokale Standardvariablen verweisen und kann von einem Zeigertyp in . umgewandelt werden Ein weiterer. Darüber hinaus ist die Pointer-Arithmetik uneingeschränkt: Das Addieren oder Subtrahieren eines Pointers verschiebt ihn um diese Anzahl von Bytes in beide Richtungen, aber die Verwendung der Standardprozeduren Incoder Decverschiebt den Pointer um die Größe des Datentyps, auf den er deklariert ist. Unter dem Namen wird auch ein untypisierter Zeiger bereitgestellt Pointer, der mit anderen Zeigertypen kompatibel ist.

Perl

Die Programmiersprache Perl unterstützt Zeiger, obwohl sie selten verwendet werden, in Form der Pack- und Unpack-Funktionen. Diese sind nur für einfache Interaktionen mit kompilierten OS-Bibliotheken gedacht. In allen anderen Fällen verwendet Perl Referenzen , die typisiert sind und keinerlei Zeigerarithmetik zulassen. Sie werden verwendet, um komplexe Datenstrukturen aufzubauen.

Siehe auch

Verweise

Externe Links