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
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 ptr
als Bezeichner eines Objekts des folgenden Typs deklariert :
- Zeiger, der auf ein Objekt vom Typ zeigt
int
Dies wird normalerweise prägnanter ausgedrückt als " ptr
ist 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 ptr
verweist, 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 a
zu zu ptr
. Wenn beispielsweise a
am Speicherplatz 0x8130 gespeichert wird , ist der Wert von ptr
0x8130 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 a
später erneut darauf zugegriffen wird, ist ihr neuer Wert 8.
Dieses Beispiel kann klarer sein, wenn der Speicher direkt untersucht wird. Angenommen, es a
befindet sich bei Adresse 0x8130 im Speicher und ptr
bei 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 a
an ptr
:
ptr = &a;
ergibt folgende Speicherwerte:
Die Anschrift Inhalt 0x8130 0x00000005 0x8134 0x00008130
Dann durch Dereferenzieren ptr
durch 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 a
den Wert 8 ergeben, da der vorherige Befehl den Inhalt von a
mittels 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 array
kann 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 sizeof
Operators. 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 array
sich 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 + 1
bedeutet 0x1004: das "+1" bedeutet, die Größe von 1 hinzuzufügenint
, was 4 Bytes beträgt; -
*array
bedeutet, den Inhalt vonarray
. Betrachten Sie den Inhalt als Speicheradresse (0x1000), suchen Sie den Wert an dieser Stelle (0x0002); -
array[i]
bedeutet Elementnummeri
, 0-basiert, vonarray
denen übersetzt wird in*(array + i)
.
Das letzte Beispiel ist der Zugriff auf den Inhalt von array
. Brechen sie ab:
-
array + i
der Speicherplatz des (i) -ten Elements von istarray
, 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)
Nil
ist die leere Liste und Cons a (Link a)
ist eine Contra- Zelle vom Typ a
mit 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;
money
wäre ein Integer-Zeiger und bags
ein char-Zeiger. Folgendes würde eine Compiler-Warnung von "Zuweisung von inkompatiblem Zeigertyp" unter GCC ergeben
bags = money;
weil money
und bags
wurden 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 money
in 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 money
Array in C beispielsweise bei 0x2000 beginnt und sizeof(int)
4 Byte groß ist, während sizeof(char)
es 1 Byte ist, money + 1
zeigt es auf 0x2004, bags + 1
wü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_t
einen 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 p2
kann 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, head
muss 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 item
kleiner als der von ist head
, wird der Anrufer head
richtig 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 argv
selbst ein Zeiger auf ein Array von Strings (ein Array von Arrays) *argv
ist ein Zeiger auf die 0-te Zeichenfolge (nach Konvention der Name des Programms) und **argv
ist 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 = ∑ // 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 null
Zeiger 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 ANY
Zeigern (äquivalent zu C void*
) so behandelt, als ob der ANY
Zeiger eine Bytebreite hätte. ANY
Zeiger können nicht wie in C dereferenziert werden. Auch das Umwandeln zwischen ANY
Zeigern 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_ptr
und 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 void
Zeigern 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 a
ist, ä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 void
Zeiger , oder void*
wird in ANSI C und C++ als generischer Zeigertyp unterstützt. Ein Zeiger auf void
kann 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 unsafe
Schlü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 fixed
Schlü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 IntPtr
Struktur, 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 System
und System.Runtime.InteropServices
-Namespaces (z. B. die Marshal
Klasse), die .NET-Typen (z. B. System.String
) in und von vielen nicht verwalteten Typen und Zeigern (z. B. LPWSTR
oder 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 SECTION
eines 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-STORAGE
Variable verwendet, aber auf ihren Inhalt wird implizit indirekt über ihre LINKAGE
Zeiger zugegriffen .
Speicherplatz für jedes Datenobjekt, auf das gezeigt wird, wird typischerweise dynamisch unter Verwendung externer CALL
Anweisungen oder über eingebettete erweiterte Sprachkonstrukte wie EXEC CICS
oder -Anweisungen zugewiesenEXEC SQL
.
Erweiterte Versionen von COBOL bieten auch mit USAGE
IS
POINTER
Klauseln deklarierte Zeigervariablen . Die Werte solcher Zeigervariablen werden unter Verwendung von SET
und SET
ADDRESS
-Anweisungen festgelegt und geändert .
Einige erweiterte Versionen von COBOL bieten auch PROCEDURE-POINTER
Variablen, 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 BASED
Variablen 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 POINTER
einer Variablen TARGET
zuzuordnen, die ein Attribut hat. Die Fortran-90- ALLOCATE
Anweisung 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 make
Funktion 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 VAR
Parameter 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 dispose
Standardprozedur dynamisch loszulassen (die den gleichen Effekt wie die free
Bibliotheksfunktion 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 Inc
oder Dec
verschiebt 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
- PL/I List Processing Paper aus der Juni 1967 Ausgabe von CACM
- cdecl.org Ein Tool zum Konvertieren von Zeigerdeklarationen in einfaches Englisch
- Über IQ.com Ein Leitfaden für Anfänger, der Hinweise in einem einfachen Englisch beschreibt.
- Zeiger und Speicher Einführung in Zeiger – Stanford Computer Science Education Library
- Zeiger in der C-Programmierung Ein visuelles Modell für Anfänger in der C-Programmierung
- 0pointer.de Eine knappe Liste von Quellcodes mit minimaler Länge, die einen Nullzeiger in verschiedenen Programmiersprachen dereferenzieren
- „Das C-Buch“ – mit Zeigerbeispielen in ANSI C
- Gemeinsames Technisches Komitee ISO/IEC JTC 1, Unterkomitee SC 22, Arbeitsgruppe WG 14 (2007-09-08). Internationale Norm ISO/IEC 9899 (PDF) . Entwurf des Ausschusses .CS1-Wartung: mehrere Namen: Autorenliste ( Link ) .