2016-09-03 8 views
18

Przechodziłem przez ten przykład, który ma funkcję wyprowadzającą wzór szesnastkowy, który reprezentuje dowolną liczbę zmiennoprzecinkową.Dlaczego rzutować na wskaźnik, a następnie na dereferencję?

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

Dlaczego warto podać adres wartości, rzutować na unsigned long wskaźnik, a następnie dereference? Czy ta praca nie jest równoznaczna z bezpośrednim rzutowaniem na niepodpisaną długą?

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

Próbowałem go i odpowiedź nie jest taka sama, tak zdezorientowana.

+9

To jest niezdefiniowane zachowanie. Jest to coś, co ludzie robili zanim C został ujednolicony w 1989 roku, a niektórzy nie nadążali z czasem –

Odpowiedz

27
(unsigned long)fValue 

ten przetwarza wartość float do wartości unsigned long według „zwykłe arytmetycznych konwersji”.

*(unsigned long *)&fValue 

Zamiarem jest, aby wziąć swój adres fValue jest przechowywany, udawać, że nie ma float ale unsigned long pod tym adresem, a następnie odczytać, że unsigned long. Celem jest zbadanie wzoru bitowego, który jest używany do przechowywania w pamięci float.

Jak pokazano, powoduje to niezdefiniowane zachowanie.

Powód: Nie można uzyskać dostępu do obiektu przez wskaźnik do typu, który nie jest "zgodny" z typem obiektu.Typy "kompatybilne" to na przykład (unsigned) char i każdy inny typ lub struktury, które mają tych samych początkowych członków (mówiąc o C tutaj). Patrz § 6.5/7 N1570 do szczegółowego (C11) Lista

Solution (Zauważ, że moje użycie „kompatybilny” różni - - bardziej szeroki niż w tekście odwołuje.): Żeliwne do unsigned char *, dostęp do poszczególnych bajtów obiektu i zmontować unsigned long z nich:

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; 
} 

Zauważ, że (jak zauważył @CodesInChaos) powyższe traktuje zmiennoprzecinkowa jak są przechowywane z jego najbardziej znaczącym pierwszy bajt („big endian”) . Jeśli twój system używa kolejnej kolejności bajtów dla wartości zmiennoprzecinkowych, musisz dostosować do tego (lub zmienić układ bajtów powyżej unsigned long, cokolwiek jest bardziej praktyczne dla ciebie).

+2

Czy 'reinterpret_cast (fValue)' be dozwolone/zdefiniowane w C++ (zakładając oczywiście, że rozmiary pasują do typu)? – celtschk

+5

Oryginalny kod działa tak długo, jak endianość zmiennych i liczb całkowitych jest taka sama (ignorując UB). Twój kod zakłada big-endianina. Chciałbym użyć 'memcpy' do' uint32_t' (i asercji dla pasujących rozmiarów). – CodesInChaos

+2

@celtschk Byłbym bardzo zaskoczony, jeśli użycie tego odniesienia nie byłoby równoznaczne z bezwzględnym naruszeniem aliasingu. - "Wyrażenie L typu T1 może zostać przekonwertowane na odniesienie do innego typu T2, wynikiem jest lwartość lub xvalue odnoszące się do tego samego obiektu co oryginalna l-wartość, ale z innym typem.Nie tworzony jest tymczasowy, żadna kopia nie jest wykonane, żadne konstruktory ani funkcje konwersji nie są wywoływane, wynikowe odniesienie jest dostępne tylko wtedy, gdy pozwala na to reguła aliasów typu "[(src)] (http://en.cppreference.com/w/cpp/language/reinterpret_cast) – CodesInChaos

3

Typowanie w języku C powoduje zarówno konwersję typu, jak i konwersję wartości. Zmieniona długa konwersja typu → powoduje obcięcie części ułamkowej liczby zmiennoprzecinkowej i ogranicza tę wartość do możliwego zakresu długości bez znaku. Konwersja z jednego rodzaju wskaźnika na inny nie wymaga zmiany wartości, więc użycie wskaźnika typu typecast jest sposobem zachowania tej samej reprezentacji podczas zmiany typu skojarzonego z tą reprezentacją.

W tym przypadku jest to sposób na wyprowadzenie binarnej reprezentacji wartości zmiennoprzecinkowej.

+1

"Konwersja z jednego rodzaju wskaźnika na inny nie wymaga zmiany wartości", może być lub nie być zmianą w reprezentacja wartości. Na nowoczesnych komputerach i tak dalej zazwyczaj nie ma. Ale to nie jest istotne dla problemu OP, tak myślę, że chciałeś powiedzieć coś w stylu, konwersja typu wskaźnika nie zmienia zawartości pamięci, na którą wskazuje wskaźnik. –

4

Wartości zmiennoprzecinkowe mają reprezentacje pamięci: na przykład bajty mogą reprezentować wartość zmiennoprzecinkową za pomocą IEEE 754.

Pierwszy wyraz *(unsigned long *)&fValue zinterpretuje te bajty, jakby to był reprezentacja od wartości unsigned long. W rzeczywistości w standardzie C powoduje niezdefiniowane zachowanie (zgodnie z tak zwaną "ścisłą regułą aliasingu"). W praktyce należy wziąć pod uwagę takie kwestie jak endianness.

Drugie wyrażenie (unsigned long)fValue jest zgodne z normą C. Posiada precyzyjne znaczenie:

C11 (n1570), § 6.3.1.4 Prawdziwe pływających i całkowitą

Po skończonej wartości rzeczywistej typu pływającego jest konwertowany do typu całkowitych innych niż _Bool The część ułamkowa jest odrzucana (tj. wartość jest obcięta w kierunku zera). Jeśli wartość części integralnej nie może być reprezentowana przez typ całkowity, zachowanie jest niezdefiniowane.

4

*(unsigned long *)&fValue nie jest odpowiednikiem bezpośredniego odlania do unsigned long.

Konwersja do (unsigned long)fValue konwertuje wartość fValue się z unsigned long, stosując normalne zasady przeliczania wartości float do wartości unsigned long. Reprezentacja tej wartości w unsigned long (na przykład w kategoriach bitów) może być zupełnie inna niż ta sama wartość przedstawiona w float.

Konwersja *(unsigned long *)&fValue formalnie ma niezdefiniowane zachowanie. Interpretuje pamięć zajmowaną przez fValue tak, jakby była to unsigned long. Praktycznie (to jest to, co często się zdarza, nawet jeśli zachowanie jest nieokreślone), często daje to wartość różną od wartości fValue.

1

Jak już zauważyli inni, rzutowanie wskaźnika na typ inny niż char na wskaźnik na inny typ inny niż char, a następnie dereferencja jest niezdefiniowanym zachowaniem.

To, że printf("%08lx\n", *(unsigned long *)&fValue) wywołuje niezdefiniowane zachowanie, niekoniecznie musi oznaczać, że uruchomienie programu, który próbuje wykonać taką parodię, spowoduje wymazanie dysku twardego lub spowoduje wyrzucenie demonów nosowych z nosa (dwóch cech niezdefiniowanego zachowania). Na komputerze, na którym oba typy mają te same wymagania dotyczące wyrównania, prawie na pewno zrobi to, czego się spodziewa, czyli wydrukuje reprezentację heksadecymalną wartości zmiennoprzecinkowej, o której mowa.

To nie powinno być zaskakujące. Norma C otwarcie zachęca do implementacji w celu rozszerzenia języka. Wiele z tych rozszerzeń znajduje się w obszarach, które są, ściśle rzecz biorąc, niezdefiniowanym zachowaniem. Na przykład funkcja POSIX dlsym zwraca wartość void*, ale ta funkcja jest zwykle używana do wyszukiwania adresu funkcji zamiast zmiennej globalnej. Oznacza to, że wskaźnik void zwrócony przez dlsym musi zostać przesłany do wskaźnika funkcji, a następnie odwołany do wywołania funkcji. Jest to oczywiście niezdefiniowane zachowanie, ale mimo to działa na dowolnej platformie zgodnej z POSIX. Nie zadziała to na architekturze Harvardu, której wskaźniki mają różne rozmiary niż wskaźniki do danych.

Podobnie, rzutowanie wskaźnika na float na wskaźnik na niepodpisaną liczbę całkowitą, a następnie dereferencja działa na prawie każdym komputerze z prawie każdym kompilatorem, w którym wymagania dotyczące rozmiaru i wyrównania tej niepodpisanej liczby całkowitej są takie same jak w przypadku a float.

To powiedziawszy, użycie unsigned long może spowodować problemy. Na moim komputerze unsigned long ma długość 64 bitów i wymaga 64-bitowego wyrównania. To nie jest kompatybilne z floatem. Byłoby lepiej użyć uint32_t - na moim komputerze, to znaczy.


Związek Hack jest jednym ze sposobów obejścia tego bałaganu:

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

Przypisanie do float_uint32_t.fval i dostępu od A `float_uint32_t.ival` kiedyś niezdefiniowane zachowanie. Tak już nie jest w przypadku C. Żaden znany mi kompilator nie wysadza nosowych demonów dla hackowania związków. To nie było UB w C++. To było nielegalne. Do czasu C++ 11, zgodny kompilator C++ musiał narzekać, że jest zgodny.


Każda nawet lepszy sposób wokół tego bałaganu jest użycie formatu na %a została częścią standardu C od 1999:

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

Jest to prosty, łatwy, przenośny, a tam nie ma szansa na niezdefiniowane zachowanie. To drukuje szesnastkową/binarną reprezentację wartości zmiennoprzecinkowej podwójnej precyzji, o której mowa. Ponieważ printf jest funkcją archaiczną, wszystkie argumenty float są konwertowane na double przed wywołaniem printf. Ta konwersja musi być dokładna zgodnie z wersją standardu C z 1999 roku. Można odebrać tę dokładną wartość, dzwoniąc pod numer scanf lub jej siostry.

+0

Dziękuję za dodanie tej odpowiedzi, pomaga to jeszcze bardziej wyjaśnić! Twoje zdrowie. – bobbay

Powiązane problemy