Warum in einen Zeiger umwandeln und dann dereferenzieren?

Lesezeit: 8 Minuten

Ich ging dieses Beispiel durch, das eine Funktion hat, die ein Hex-Bitmuster ausgibt, um einen beliebigen Float darzustellen.

void ExamineFloat(float fValue)
{
    printf("%08lx\n", *(unsigned long *)&fValue);
}

Warum die Adresse von fValue nehmen, in einen vorzeichenlosen langen Zeiger umwandeln und dann dereferenzieren? Ist all diese Arbeit nicht gleichbedeutend mit einer direkten Umwandlung in unsigned long?

printf("%08lx\n", (unsigned long)fValue);

Ich habe es versucht und die Antwort ist nicht die gleiche, so verwirrt.

  • Dies ist ein undefiniertes Verhalten. Es ist etwas, was die Leute gemacht haben, bevor C 1989 standardisiert wurde, und einige sind nicht mit der Zeit gegangen

    – MM

    3. September 2016 um 23:11 Uhr

Benutzer-Avatar
Daniel Jour

(unsigned long)fValue

Das konvertiert die float Wert zu einem unsigned long Wert nach den “üblichen arithmetischen Umrechnungen”.

*(unsigned long *)&fValue

Die Absicht ist hier, die Adresse zu nehmen, an der fValue gespeichert ist, so tun, als gäbe es keine float aber ein unsigned long an dieser Adresse, und dann das zu lesen unsigned long. Der Zweck besteht darin, das Bitmuster zu untersuchen, das verwendet wird, um die zu speichern float in Erinnerung.

Wie gezeigt, führt dies jedoch zu undefiniertem Verhalten.

Grund: Sie dürfen auf ein Objekt nicht über einen Zeiger auf einen Typ zugreifen, der nicht mit dem Typ des Objekts “kompatibel” ist. “Kompatible” Typen sind zum Beispiel (unsigned) char und jeder andere Typ oder Strukturen, die dieselben Anfangselemente teilen (wo wir hier von C sprechen). Siehe §6.5/7 N1570 für die detaillierte (C11) Liste (Beachten Sie, dass meine Verwendung von “kompatibel” anders – breiter – ist als im referenzierten Text.)

Lösung: Cast to unsigned char *greife auf die einzelnen Bytes des Objekts zu und setze eine an unsigned long aus ihnen:

unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
  pattern |= *access;
  pattern <<= CHAR_BIT;
  ++access;
}

Beachten Sie, dass (wie @CodesInChaos betonte) der Gleitkommawert oben so behandelt wird, als würde er mit seinem höchstwertigen Byte zuerst gespeichert (“Big Endian”). Wenn Ihr System eine andere Byte-Reihenfolge für Gleitkommawerte verwendet, müssen Sie sich daran anpassen (oder die Bytes oben neu anordnen unsigned longwas für Sie praktischer ist).

  • Möchten reinterpret_cast<unsigned long&>(fValue) in C++ erlaubt/definiert werden (vorausgesetzt natürlich, die Typgrößen passen)?

    – Celtschk

    4. September 2016 um 5:28 Uhr


  • Der ursprüngliche Code funktioniert, solange die Endianness von Float und Integer gleich ist (wobei das UB ignoriert wird). Ihr Code geht von Big-Endian aus. Ich würde eine verwenden memcpy hinein uint32_t (und eine Behauptung für passende Größen).

    – CodesInChaos

    4. September 2016 um 8:41 Uhr

  • @celtschk Ich wäre sehr überrascht, wenn die tatsächliche Verwendung dieser Referenz nicht als strikte Aliasing-Verletzung gelten würde. — “Ein Lvalue-Ausdruck vom Typ T1 kann in einen Verweis auf einen anderen Typ T2 konvertiert werden. Das Ergebnis ist ein Lvalue oder Xvalue, das auf dasselbe Objekt verweist wie der ursprüngliche Lvalue, aber mit einem anderen Typ. Es wird kein temporäres Objekt erstellt, keine Kopie gemacht, werden keine Konstruktoren oder Konvertierungsfunktionen aufgerufen. Auf die resultierende Referenz kann nur dann sicher zugegriffen werden, wenn dies von den Typ-Aliasing-Regeln zugelassen wird.” (Quelle)

    – CodesInChaos

    4. September 2016 um 8:48 Uhr


Benutzer-Avatar
md5

Gleitkommawerte haben Speicherdarstellungen: Beispielsweise können die Bytes einen Gleitkommawert darstellen, indem IEEE754.

Der erste Ausdruck *(unsigned long *)&fValue interpretiert diese Bytes, als ob es die wäre Darstellung von einem unsigned long Wert. Tatsächlich führt dies im C-Standard zu einem undefinierten Verhalten (gemäß der sogenannten “strikten Aliasing-Regel”). In der Praxis gibt es Probleme wie Endianness, die berücksichtigt werden müssen.

Der zweite Ausdruck (unsigned long)fValue ist C-Standard-konform. Es hat eine genaue Bedeutung:

C11 (n1570), § 6.3.1.4 Real Floating und Integer

Wenn ein endlicher Wert eines reellen Floating-Typs in einen anderen Integer-Typ als konvertiert wird _Bool, wird der Bruchteil verworfen (dh der Wert wird in Richtung Null gekürzt). Wenn der Wert des ganzzahligen Teils nicht durch den ganzzahligen Typ dargestellt werden kann, ist das Verhalten undefiniert.

*(unsigned long *)&fValue ist nicht gleichbedeutend mit einer direkten Besetzung an unsigned long.

Die Umstellung auf (unsigned long)fValue konvertiert den Wert von fValue In ein unsigned longunter Verwendung der normalen Regeln für die Konvertierung von a float Wert zu einem unsigned long Wert. Die Darstellung dieses Werts in einer unsigned long (z. B. in Bezug auf die Bits) kann ganz anders sein, als derselbe Wert in a dargestellt wird float.

Die Umwandlung *(unsigned long *)&fValue hat formal undefiniertes Verhalten. Es interpretiert den Speicher belegt durch fValue als wäre es ein unsigned long. Praktisch (dh das passiert oft, obwohl das Verhalten undefiniert ist) ergibt dies oft einen ganz anderen Wert als fValue.

Die Typumwandlung in C führt sowohl eine Typkonvertierung als auch eine Wertkonvertierung durch. Die Gleitkomma → unsigned long Konvertierung schneidet den Bruchteil der Gleitkommazahl ab und beschränkt den Wert auf den möglichen Bereich eines unsigned long. Das Konvertieren von einem Zeigertyp in einen anderen hat keine erforderliche Wertänderung, daher ist die Verwendung der Zeigertypumwandlung eine Möglichkeit, den gleichen Wert beizubehalten in Erinnerung Repräsentation, während der dieser Repräsentation zugeordnete Typ geändert wird.

In diesem Fall ist dies eine Möglichkeit, die binäre Darstellung des Gleitkommawerts auszugeben.

Wie andere bereits angemerkt haben, ist das Umwandeln eines Zeigers auf einen Nicht-Zeichentyp in einen Zeiger auf einen anderen Nicht-Zeichentyp und das anschließende Dereferenzieren ein undefiniertes Verhalten.

Dass printf("%08lx\n", *(unsigned long *)&fValue) undefiniertes Verhalten hervorruft, bedeutet nicht unbedingt, dass das Ausführen eines Programms, das versucht, eine solche Travestie durchzuführen, zum Löschen der Festplatte führt oder Nasendämonen aus der Nase ausbrechen lässt (die beiden Kennzeichen von undefiniertem Verhalten). Auf einem Computer, in dem sizeof(unsigned long)==sizeof(float) und auf dem beide Typen die gleichen Ausrichtungsanforderungen haben, das printf wird mit ziemlicher Sicherheit das tun, was man von ihm erwartet, nämlich die Hex-Darstellung des betreffenden Gleitkommawerts ausgeben.

Dies sollte nicht überraschen. Der C-Standard lädt offen zu Implementierungen ein, um die Sprache zu erweitern. Viele dieser Erweiterungen befinden sich in Bereichen, die streng genommen undefiniertes Verhalten darstellen. Zum Beispiel die POSIX-Funktion dlsym gibt a zurück void*, aber diese Funktion wird normalerweise verwendet, um die Adresse einer Funktion und nicht einer globalen Variablen zu finden. Dies bedeutet den void-Zeiger, der von zurückgegeben wird dlsym muss in einen Funktionszeiger umgewandelt und dann dereferenziert werden, um die Funktion aufzurufen. Dies ist offensichtlich ein undefiniertes Verhalten, aber es funktioniert trotzdem auf jeder POSIX-kompatiblen Plattform. Dies funktioniert nicht auf einer Maschine mit Harvard-Architektur, auf der Zeiger auf Funktionen andere Größen haben als Zeiger auf Daten.

In ähnlicher Weise wird ein Zeiger auf a gecastet float auf einen Zeiger auf eine vorzeichenlose Ganzzahl und dann funktioniert die Dereferenzierung auf fast jedem Computer mit fast jedem Compiler, in dem die Größen- und Ausrichtungsanforderungen dieser vorzeichenlosen Ganzzahl die gleichen sind wie die von a float.

Das heißt, mit unsigned long könnte dich durchaus in Schwierigkeiten bringen. Auf meinem Computer, ein unsigned long ist 64 Bit lang und hat 64-Bit-Ausrichtungsanforderungen. Dies ist nicht mit einem Schwimmer kompatibel. Es wäre besser zu verwenden uint32_t — auf meinem Computer, das heißt.

Der Union-Hack ist eine Möglichkeit, dieses Chaos zu umgehen:

typedef struct {
    float fval;
    uint32_t ival;
} float_uint32_t;

Zuordnung zu a float_uint32_t.fval und der Zugriff von einem “float_uint32_t.ival` war früher ein undefiniertes Verhalten. Das ist in C nicht mehr der Fall. Kein Compiler, den ich kenne, bläst nasale Dämonen für den Union-Hack. Dies war nicht UB in C++. Es war illegal. Bis C++11 musste sich ein konformer C++-Compiler beschweren, um konform zu sein.

Ein noch besserer Weg, um dieses Chaos zu umgehen, ist die Verwendung von %a Format, das seit 1999 Teil des C-Standards ist:

printf ("%a\n", fValue);

Dies ist einfach, leicht, portabel, und es besteht keine Chance auf undefiniertes Verhalten. Dies gibt die hexadezimale/binäre Darstellung des betreffenden Gleitkommawerts mit doppelter Genauigkeit aus. Seit printf ist eine archaische Funktion, all float Argumente werden umgewandelt in double vor dem Anruf bei printf. Diese Umrechnung muss gemäß der Version 1999 des C-Standards exakt sein. Man kann diesen genauen Wert über einen Anruf bei abholen scanf oder seine Schwestern.

  • Vielen Dank, dass Sie diese Antwort hinzugefügt haben. Sie hilft, die Dinge noch weiter zu verdeutlichen! Prost.

    – bobbay

    7. September 2016 um 16:39 Uhr

  • Re “Das ist in C nicht mehr der Fall” für den Union-Hack, ich bin mir ziemlich sicher, dass Sie falsch liegen. C11 6.5/7 gibt die Umstände an, unter denen auf gespeicherte Werte mit einem lvalue zugegriffen werden kann, und seit float und uint32_t nicht kompatibel sind, ist es nicht erlaubt. Die Regeln, nur auf Unions mit demselben Feld zuzugreifen, das im letzten Geschäft verwendet wurde, gelten weiterhin. Es sei denn, Sie haben eine spätere Version des Standards, die dies ändert, aber da C17 nur aufgrund von ISO-Regeln erstellt wurde, die Ergänzungen begrenzen, bevor ein neuer Standard erforderlich ist, bezweifle ich, dass sie eine so grundlegende Änderung vorgenommen haben.

    – paxdiablo

    15. Juni 2020 um 13:45 Uhr

  • Vielen Dank, dass Sie diese Antwort hinzugefügt haben. Sie hilft, die Dinge noch weiter zu verdeutlichen! Prost.

    – bobbay

    7. September 2016 um 16:39 Uhr

  • Re “Das ist in C nicht mehr der Fall” für den Union-Hack, ich bin mir ziemlich sicher, dass Sie falsch liegen. C11 6.5/7 gibt die Umstände an, unter denen auf gespeicherte Werte mit einem lvalue zugegriffen werden kann, und seit float und uint32_t nicht kompatibel sind, ist es nicht erlaubt. Die Regeln, nur auf Unions mit demselben Feld zuzugreifen, das im letzten Geschäft verwendet wurde, gelten weiterhin. Es sei denn, Sie haben eine spätere Version des Standards, die dies ändert, aber da C17 nur aufgrund von ISO-Regeln erstellt wurde, die Ergänzungen begrenzen, bevor ein neuer Standard erforderlich ist, bezweifle ich, dass sie eine so grundlegende Änderung vorgenommen haben.

    – paxdiablo

    15. Juni 2020 um 13:45 Uhr

1018910cookie-checkWarum in einen Zeiger umwandeln und dann dereferenzieren?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy