Was sind die Regeln für das Umsetzen von Zeigern in C?

70

K & R geht nicht darüber hinweg, aber sie benutzen es. Ich habe versucht zu sehen, wie es funktionieren würde, indem ich ein Beispielprogramm geschrieben habe, aber es lief nicht so gut:

Es wird kompiliert, aber meine print-Anweisung spuckt Müllvariablen aus (sie sind jedes Mal anders, wenn ich das Programm aufrufe). Irgendwelche Ideen?

Theo Chronisch
quelle
1
int hat eine größere Größe als char und liest daher über das Leerzeichen des Zeichens '5' hinaus. Versuchen Sie dasselbe mit einem kleineren Datentyp (int c, printf "% c")
SheetJS
1
Der Wert von *nwird ein sein int, der 4 Bytes betragen sollte. *nzeigt auf die lokale Variable cin main(). Dies bedeutet, dass Sie den Wert von 'c'und die darauf folgenden drei Bytes im Speicher ausschreiben. (Meine Vermutung ist der Wert von d.) Sie können dies überprüfen, indem Sie die Zahl in hexadezimaler Form ausschreiben - zwei der Ziffern sollten jedes Mal gleich sein.
Millimoose
1
'5'- Sie sollten denken, dass dies wie ein int aussieht, da es eine Zahl zu sein scheint, aber es ist nur ein Zeichen, das die Ziffer 5 darstellt.
mah
Ich habe den gleichen Test auf meinem Computer ausgeführt (gcc, x86_64) und keine Kompilierungsfehler erhalten. Das Programm wird jedes Mal einwandfrei ausgeführt (kein Müll). Aber ich habe nichts anderes gemacht als das OP. Seltsam.
Andy J
Jeder, der diese Antwort liest, sollte sich die Antwort von
polynomial_donut

Antworten:

140

Wenn Sie über Zeiger nachdenken, ist es hilfreich, Diagramme zu zeichnen . Ein Zeiger ist ein Pfeil, der auf eine Adresse im Speicher zeigt, wobei eine Beschriftung den Typ des Werts angibt. Die Adresse gibt an, wo gesucht werden soll, und der Typ gibt an, was zu nehmen ist. Durch Umsetzen des Zeigers wird die Beschriftung des Pfeils geändert, jedoch nicht die Stelle, an der der Pfeil zeigt.

din mainist ein Zeiger cvom Typ char. A charist ein Byte Speicher. Wenn dalso dereferenziert wird, erhalten Sie den Wert in diesem einen Byte Speicher. In der folgenden Abbildung repräsentiert jede Zelle ein Byte.

Wenn Sie dzu int*besetzen, sagen Sie, dass dies dwirklich auf einen intWert hinweist . Auf den meisten heutigen Systemen intbelegt ein 4 Bytes.

Wenn Sie dereferenzieren (int*)d, erhalten Sie einen Wert, der aus diesen vier Speicherbytes bestimmt wird. Der Wert, den Sie erhalten, hängt davon ab, was in diesen Zellen markiert ?ist und wie ein intim Speicher dargestellt wird.

Ein PC ist Little-Endian , was bedeutet, dass der Wert von a auf intdiese Weise berechnet wird (vorausgesetzt, er umfasst 4 Bytes) : * ((int*)d) == c + ?₁ * 2⁸ + ?₂ * 2¹⁶ + ?₃ * 2²⁴. Wenn Sie also hexadezimal ( printf("%x\n", *n)) drucken , werden die letzten beiden Ziffern immer angezeigt 35(das ist der Wert des Zeichens '5') , während der Wert Müll ist .

Einige andere Systeme sind Big-Endian-Systeme und ordnen die Bytes in die andere Richtung : * ((int*)d) == c * 2²⁴ + ?₁ * 2¹⁶ + ?₂ * 2⁸ + ?₃. Auf diesen Systemen beginnt der Wert immer mit 35hexadezimalem Druck. Einige Systeme haben eine Größe int, die sich von 4 Bytes unterscheidet. Einige wenige Systeme sind intunterschiedlich angeordnet, aber es ist äußerst unwahrscheinlich, dass Sie ihnen begegnen.

Abhängig von Ihrem Compiler und Betriebssystem stellen Sie möglicherweise fest, dass der Wert bei jedem Ausführen des Programms unterschiedlich ist oder dass er immer gleich ist, sich jedoch ändert, wenn Sie den Quellcode auch nur geringfügig ändern.

Auf einigen Systemen muss ein intWert in einer Adresse gespeichert werden, die ein Vielfaches von 4 (oder 2 oder 8) ist. Dies wird als Ausrichtungsanforderung bezeichnet . Je nachdem, ob die Adresse von crichtig ausgerichtet ist oder nicht, kann das Programm abstürzen.

Im Gegensatz zu Ihrem Programm geschieht Folgendes, wenn Sie einen intWert haben und einen Zeiger darauf nehmen.

Der Zeiger pzeigt auf einen intWert. Das Etikett auf dem Pfeil beschreibt korrekt, was sich in der Speicherzelle befindet, sodass beim Dereferenzieren keine Überraschungen auftreten.

Gilles 'SO - hör auf böse zu sein'
quelle
4
Gute Beschreibung. Ich möchte darauf hinweisen / diskutieren, dass es auf den meisten Computern wahr sein kann, dass int ein 32-Bit-Wert ist, aber für andere Embedded-Ingenieure ist int normalerweise 16-Bit und zeigt, wie nützlich und wahrscheinlich wichtig es ist um uint16_t, uint32_t, int32_t usw. usw. zu verwenden. Versuchen Sie nicht, ein kluger Arsch zu sein, bitte nehmen Sie keine Beleidigung. :)
DiBosco
1
"... die letzten beiden Ziffern sind immer 35 (das ist der Wert des Zeichens '5')." Warum?
Kenny Worden
Hallo Gilles, als ich den Code hier char *a = "abcd"; int *i = (int *)a; printf("%x\n", *i);ausprobiert habe, ist die Ausgabe 64636261, aber ich denke, es sollte 61626364 sein. Bedeutet dies, dass der Speicher in diesem int-Block von hinten nach vorne gelesen wird?
Sommersonne
@SummerSun Warum sollte es Ihrer Meinung nach 61626364 sein? Wenn Sie einen Little-Endian-Computer haben (alle PCs sind Little-Endian-Computer), ist dies 64636261. Dies hat nichts mit der Reihenfolge zu tun, in der der Speicher gelesen wird. Ein intwird wahrscheinlich sowieso in einer einzigen Anweisung gelesen. Hier geht es darum, wie ein Block von 4 Bytes als intWert interpretiert wird.
Gilles 'SO - hör auf böse zu sein'
1
@ Malcolm Es ist undefiniertes Verhalten. Die Dereferenzierung des Ergebnisses der Umwandlung ist UB (es ist beispielsweise möglicherweise nicht richtig ausgerichtet), und selbst das bloße Konstruieren eines Zeigers ist normalerweise UB, wenn die Dereferenzierung UB wäre (ich denke, die einzigen Ausnahmen sind Funktionszeiger und Zeiger auf das Ende von eine Anordnung). Es gibt einen Fall, in dem das Verhalten definiert ist, wenn der Zeiger ursprünglich ein int*Zeiger war. Jeder Datenzeiger kann hin unsigned char*und zurück geworfen werden, und ich denke, er unsigned char *kann hin char *und zurück geworfen werden.
Gilles 'SO - hör auf böse zu sein'
40

Ein char(1 Byte) wird auf dem Stapel an der Adresse zugewiesen 0x12345678.

Sie erhalten die Adresse von cund speichern sie in d, also d = 0x12345678.

Sie zwingen den Compiler anzunehmen, dass 0x12345678er auf ein zeigt int, aber ein int ist nicht nur ein Byte ( sizeof(char) != sizeof(int)). Es können 4 oder 8 Bytes sein, je nach Architektur oder sogar anderen Werten.

Wenn Sie also den Wert des Zeigers drucken, wird die Ganzzahl berücksichtigt, indem das erste Byte (das war c) und andere aufeinanderfolgende Bytes, die sich auf dem Stapel befinden und nur Müll für Ihre Absicht sind, verwendet werden.

Jack
quelle
3
Andere aufeinanderfolgende Bytes sind kein Müll, sondern der Wert von d, dh 0x12345678in Ihrem Beispiel.
Kane
dist nicht groß genug, um zu halten0x12345678
Eine Person
1
@APerson Warum ist das so?
yyny
char c [] = "5"; char d = c; int * e = (int ) d; printf ("% p \ n", e);
Martian2049
1
Dies ist in der Tat UB: wiki.sei.cmu.edu/confluence/display/c/…
BearAqua
17

Das Casting von Zeigern ist in C normalerweise ungültig. Es gibt mehrere Gründe:

  1. Ausrichtung. Es ist möglich, dass der Zielzeigertyp aufgrund von Überlegungen zur Ausrichtung den Wert des Quellzeigertyps nicht darstellen kann. Wenn beispielsweise von int *Natur aus 4 Byte ausgerichtet wären char *, int *würde das Casting in die unteren Bits verlieren.

  2. Aliasing. Im Allgemeinen ist der Zugriff auf ein Objekt verboten, außer über einen Wert des richtigen Typs für das Objekt. Es gibt einige Ausnahmen, aber wenn Sie sie nicht sehr gut verstehen, möchten Sie es nicht tun. Beachten Sie, dass Aliasing nur dann ein Problem darstellt, wenn Sie den Zeiger tatsächlich dereferenzieren (wenden Sie die Operatoren *oder ->auf ihn an oder übergeben Sie ihn an eine Funktion, die ihn dereferenziert).

Die wichtigsten bemerkenswerten Fälle, in denen das Casting von Zeigern in Ordnung ist, sind:

  1. Wenn der Zielzeigertyp auf den Zeichentyp zeigt. Zeiger auf Zeichentypen können garantiert jeden Zeiger auf einen beliebigen Typ darstellen und ihn auf Wunsch erfolgreich auf den ursprünglichen Typ zurückführen. Der Zeiger auf void ( void *) ist genau das Gleiche wie ein Zeiger auf einen Zeichentyp, außer dass Sie ihn nicht dereferenzieren oder arithmetisch bearbeiten dürfen, und er konvertiert automatisch zu und von anderen Zeigertypen, ohne dass eine Umwandlung erforderlich ist, also Zeiger auf void sind für diesen Zweck normalerweise Zeigern auf Zeichentypen vorzuziehen.

  2. Wenn der Zielzeigertyp ein Zeiger auf den Strukturtyp ist, dessen Elemente genau mit den ursprünglichen Elementen des ursprünglich angegebenen Strukturtyps übereinstimmen. Dies ist nützlich für verschiedene objektorientierte Programmiertechniken in C.

Einige andere dunkle Fälle sind in Bezug auf die Sprachanforderungen technisch in Ordnung, aber problematisch und werden am besten vermieden.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
6
Können Sie mit diesen obskuren Fällen auf ein offizielles Dokument verlinken?
Eric
Ich habe an einigen Stellen Code gesehen, der ein Zeichen * nimmt und es in einen anderen Zeiger umwandelt, z. B. int. Zum Beispiel Streaming von RGB-Werten von einer Kamera oder Bytes aus dem Netzwerk. Bedeutet Ihre Referenz, dass dieser Code ungültig ist? Reicht die Ausrichtung der Daten aus, um den Code korrekt zu machen, oder ist es nur so, dass unsere gängigen Compiler diese Verwendung nicht ernst nehmen?
Evan Benn
1
@EvanBenn: Möglicherweise. Wenn der Puffer von erhalten wird mallocund Sie Daten byteweise über freadoder ähnlich darin speichern , solange die Offsets angemessen ausgerichtet sind (im Allgemeinen kann dies schwer zu bestimmen sein, aber es ist sicherlich wahr, wenn sie ein Vielfaches der Typgröße sind ) Es sollte konform sein, in den entsprechenden Zeigertyp zu konvertieren und auf die Daten als diesen Typ zuzugreifen. Wenn Sie jedoch mit einem Puffer arbeiten, dessen tatsächlicher Typ char[N]oder etwas anderes ist , ist er nicht gültig.
R .. GitHub STOP HELPING ICE
3

Ich vermute, Sie brauchen eine allgemeinere Antwort:

Es gibt keine Regeln für das Wirken von Zeigern in C! Mit der Sprache können Sie jeden Zeiger kommentarlos auf einen anderen Zeiger umwandeln.

Aber die Sache ist: Es gibt keine Datenkonvertierung oder was auch immer getan! Es liegt allein in Ihrer Verantwortung, dass das System die Daten nach der Umwandlung nicht falsch interpretiert - was im Allgemeinen der Fall ist und zu Laufzeitfehlern führt.

Wenn Sie also das Casting durchführen, liegt es ganz bei Ihnen, darauf zu achten, dass die Daten kompatibel sind, wenn Daten von einem gegossenen Zeiger verwendet werden!

C ist für die Leistung optimiert, daher fehlt die Laufzeitreflexivität von Zeigern / Referenzen. Aber das hat seinen Preis - Sie als Programmierer müssen sich besser darum kümmern, was Sie tun. Sie müssen selbst wissen, ob das, was Sie tun möchten, "legal" ist.

Ole Dittmann
quelle
10
Es gibt Regeln zum Umwandeln von Zeigern, von denen einige in Abschnitt 6.3.2.3 des C 2011-Standards enthalten sind. Unter anderem können Zeiger auf Objekte in andere Zeiger auf Objekte umgewandelt werden und werden, wenn sie zurückkonvertiert werden, gleich dem Original verglichen. Zeiger auf Funktionen können in andere Zeiger auf Funktionen umgewandelt werden und werden, wenn sie zurückkonvertiert werden, gleich verglichen. Das Konvertieren von Zeigern in Funktionen in Zeiger auf Objekte führt zu undefiniertem Verhalten. Zeiger auf Objekte können in Zeiger auf Zeichen konvertiert und für den Zugriff auf die Bytes eines Objekts verwendet werden.
Eric Postpischil
Das Konvertieren von Zeigern in Funktionen in Zeiger auf Objekte ist zulässig. "Ein Zeiger auf eine Funktion kann in einen Zeiger auf ein Objekt umgewandelt oder ungültig gemacht werden, so dass eine Funktion überprüft oder geändert werden kann (z. B. durch einen Debugger)", J.5.7
aqjune
2
@aqjune Sie zitieren eine beliebte Erweiterung von C, die per Definition nicht Standard C ist. Sie ist nur informativ.
Pipe
2

Sie haben einen Zeiger auf a char. Wie Ihr System weiß, befindet sich auf dieser Speicheradresse ein charWert für den sizeof(char)Speicherplatz. Wenn Sie es umwandeln int*, arbeiten Sie mit Daten von sizeof(int), sodass Sie Ihr Zeichen und etwas Speichermüll danach als Ganzzahl drucken.

gkovacs90
quelle