Wyrok
printf("%f\n",0.0f);
drukuje 0.
Jednak oświadczenie
printf("%f\n",0);
wypisuje losowe wartości.
Zdaję sobie sprawę, że przejawiam jakieś niezdefiniowane zachowanie, ale nie potrafię dokładnie określić dlaczego.
Wartość zmiennoprzecinkowa, w której wszystkie bity są równe 0, jest nadal ważna i float
ma wartość 0.
float
i int
mają ten sam rozmiar na moim komputerze (jeśli to nawet ma znaczenie).
Dlaczego użycie literału liczby całkowitej zamiast literału zmiennoprzecinkowego printf
powoduje takie zachowanie?
PS to samo zachowanie można zobaczyć, jeśli używam
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
źródło
źródło
printf
oczekuje adouble
, a ty dajesz muint
.float
iint
może mieć ten sam rozmiar na twoim komputerze, ale w0.0f
rzeczywistości jest konwertowany na adouble
po umieszczeniu na wariadycznej liście argumentów (iprintf
oczekuje tego). Krótko mówiąc, nie wywiązujesz się ze swojej części umowyprintf
na podstawie specyfikacji, których używasz, i argumentów, które podajesz.(uint64_t)0
zamiast0
i zobaczyć, czy nadal się losowo (przy założeniu zachowaniadouble
iuint64_t
mają tę samą wielkość i wyrównanie). Istnieje prawdopodobieństwo, że dane wyjściowe będą nadal losowe na niektórych platformach (np. X86_64) ze względu na różne typy przesyłane w różnych rejestrach.Odpowiedzi:
"%f"
Format wymaga argumentu typudouble
. Dajesz mu argument typuint
. Dlatego zachowanie jest nieokreślone.Standard nie gwarantuje, że wszystkie bity-zero są poprawną reprezentacją
0.0
(chociaż często tak jest) lub jakiejkolwiekdouble
wartości,int
idouble
mają ten sam rozmiar (pamiętaj, żedouble
niefloat
), lub nawet jeśli są takie same size, że są przekazywane jako argumenty do funkcji wariadycznej w ten sam sposób.Może się zdarzyć, że „zadziała” w Twoim systemie. To najgorszy możliwy objaw niezdefiniowanego zachowania, ponieważ utrudnia zdiagnozowanie błędu.
N1570 7.21.6.1 akapit 9:
Argumenty typu
float
są promowanedouble
, dlategoprintf("%f\n",0.0f)
działa. Argumenty typów całkowitych węższe niżint
są promowane doint
lub dounsigned int
. Niniejsze zasady promocji (określone w N1570 6.5.2.2 paragraf 6) nie pomagają w przypadkuprintf("%f\n", 0)
.Zwróć uwagę, że jeśli przekazujesz stałą
0
do niezmiennej funkcji, która oczekujedouble
argumentu, zachowanie jest dobrze zdefiniowane, zakładając, że prototyp funkcji jest widoczny. Na przykładsqrt(0)
(po#include <math.h>
) niejawnie konwertuje argument0
zint
nadouble
- ponieważ kompilator może zobaczyć na podstawie deklaracjisqrt
, że oczekujedouble
argumentu. Nie ma takich informacji dlaprintf
. Funkcje zróżnicowane, takie jak,printf
są specjalne i wymagają większej uwagi podczas pisania do nich wywołań.źródło
double
niefloat
tak szerokość założenie PO może nie (prawdopodobnie nie robi) zawieszone. Po drugie, założenie, że zero całkowite i zero zmiennoprzecinkowe mają ten sam wzór bitowy, również nie jest aktualne. Dobra robotafloat
jest to promowanedouble
bez wyjaśnienia, dlaczego, ale nie o to chodziło.printf
, chociaż na przykład gcc je ma, więc może diagnozować błędy ( jeśli ciąg formatu jest literałem). Kompilator może zobaczyć deklaracjęprintf
from<stdio.h>
, która mówi mu, że pierwszy parametr to a,const char*
a pozostałe są wskazywane przez, ...
. Nie,%f
jest dladouble
(ifloat
jest promowanydouble
) i%lf
jest dlalong double
. Standard C nie mówi nic o stosie. Określa zachowanieprintf
tylko wtedy, gdy jest wywoływana poprawnie.float
Przekazywaneprintf
jest podnoszony dodouble
; nie ma w tym nic magicznego, to tylko reguła języka dotycząca wywoływania funkcji wariadycznych.printf
sama wie poprzez łańcuch formatu, co wywołujący twierdzi, że ma do niego przekazać; jeśli to twierdzenie jest nieprawidłowe, zachowanie jest nieokreślone.l
modyfikator długość „nie ma wpływu na kolejnyma
,A
,e
,E
,f
,F
,g
, lubG
specyfikacją konwersji”, modyfikator długości dolong double
konwersji jestL
. (@ robertbristow-johnson może być również zainteresowany)Po pierwsze, jak poruszył w kilku innych odpowiedzi, ale nie, moim zdaniem, wypisanym wystarczająco jasno: to robi pracę, aby zapewnić całkowitą w większości kontekstów gdzie funkcja biblioteki zajmuje
double
lubfloat
argument. Kompilator automatycznie wstawi konwersję. Na przykład,sqrt(0)
jest dobrze zdefiniowany i będzie się zachowywał dokładnie taksqrt((double)0)
, jak i to samo dotyczy każdego innego wyrażenia typu całkowitego tam użytego.printf
jest inny. Jest inaczej, ponieważ wymaga zmiennej liczby argumentów. Jego prototypem funkcji jestextern int printf(const char *fmt, ...);
Dlatego kiedy piszesz
printf(message, 0);
kompilator nie ma żadnych informacji o tym, jaki typ
printf
oczekuje tego drugiego argumentu. Ma tylko typ wyrażenia argumentu, którym jestint
. Dlatego, w przeciwieństwie do większości funkcji bibliotecznych, to ty, programista, musisz upewnić się, że lista argumentów jest zgodna z oczekiwaniami ciągu formatu.(Nowoczesne kompilatory mogą zajrzeć do ciągu formatu i powiedzieć, że masz niezgodność typów, ale nie zaczną wstawiać konwersji, aby osiągnąć to, co miałeś na myśli, ponieważ lepszy kod powinien się teraz zepsuć, kiedy zauważysz , niż lata później, gdy został przebudowany za pomocą mniej pomocnego kompilatora).
Teraz druga połowa pytania brzmiała: biorąc pod uwagę, że (int) 0 i (float) 0.0 są, w większości nowoczesnych systemów, reprezentowane jako 32 bity, z których wszystkie są zerowe, dlaczego i tak nie działa to przypadkowo? Standard C mówi tylko, że „to nie jest wymagane do pracy, jesteś sam”, ale pozwól mi przeliterować dwa najczęstsze powody, dla których to nie zadziała; to prawdopodobnie pomoże ci zrozumieć, dlaczego nie jest to wymagane.
Po pierwsze, z powodów historycznych, kiedy przechodzisz
float
przez zmienną listę argumentów, jest ona promowana dodouble
, która w większości nowoczesnych systemów ma 64 bity. Zatemprintf("%f", 0)
przekazuje tylko 32 bity zerowe do wywoływanego, oczekując 64 z nich.Drugim, równie istotnym powodem jest to, że argumenty funkcji zmiennoprzecinkowych mogą być przekazywane w innym miejscu niż argumenty liczb całkowitych. Na przykład większość procesorów ma oddzielne pliki rejestrów dla liczb całkowitych i wartości zmiennoprzecinkowych, więc może być regułą, że argumenty od 0 do 4 trafiają do rejestrów od r0 do r4, jeśli są liczbami całkowitymi, ale od f0 do f4, jeśli są zmiennoprzecinkowe. Więc
printf("%f", 0)
szuka w rejestrze f1 tego zera, ale w ogóle go tam nie ma.źródło
()
.AL
. (Tak, oznacza to, że implementacjava_arg
jest znacznie bardziej skomplikowana niż kiedyś.)ret n
instrukcji 8086, w którejn
była zakodowana na stałe liczba całkowita, która w związku z tym nie miała zastosowania do funkcji wariadycznych. Jednak nie wiem, czy jakikolwiek kompilator C faktycznie to wykorzystał (kompilatory inne niż C z pewnością to zrobiły).Zwykle, gdy wywołujesz funkcję, która oczekuje a
double
, ale podasz anint
, kompilator automatycznie przekonwertuje się na adouble
. Tak się nie dzieje w przypadkuprintf
, ponieważ typy argumentów nie są określone w prototypie funkcji - kompilator nie wie, że należy zastosować konwersję.źródło
printf()
szczególności jest zaprojektowany tak, aby jego argumenty mogły być dowolnego typu. Musisz wiedzieć, jakiego typu oczekuje każdy element w ciągu formatu i musisz podać go poprawnie.Ponieważ
printf()
nie ma wpisanych parametrów opróczconst char* formatstring
pierwszego. Do...
reszty używa się elipsy w stylu c ( ).Po prostu decyduje, jak interpretować wartości przekazywane tam zgodnie z typami formatowania podanymi w ciągu formatu.
Wystąpiłbyś tak samo niezdefiniowane zachowanie, jak podczas próby
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB
źródło
printf
mogą działać w ten sposób (z wyjątkiem tego, że przekazywane elementy są wartościami, a nie adresami). Standard C nie określa, jakprintf
i inne funkcje wariadyczne działają, a jedynie określa ich zachowanie. W szczególności nie ma wzmianki o ramkach stosu.printf
ma jeden wpisany parametr, ciąg formatu, który jest typemconst char*
. Przy okazji, pytanie jest oznaczone jako C i C ++, a C jest bardziej odpowiednie; Prawdopodobnie nie użyłbym tegoreinterpret_cast
jako przykładu.Użycie źle dopasowanego
printf()
specyfikatora"%f"
i typu(int) 0
prowadzi do niezdefiniowanego zachowania.Kandydujące przyczyny UB.
Jest to UB według specyfikacji, a kompilacja jest skomplikowana - powiedział nuf.
double
iint
mają różne rozmiary.double
iint
mogą przekazywać swoje wartości przy użyciu różnych stosów (stos ogólny kontra FPU ).A
double 0.0
może nie być zdefiniowane przez wzorzec wszystkich bitów zerowych. (rzadko spotykany)źródło
To jedna z tych wspaniałych okazji, aby wyciągnąć wnioski z ostrzeżeń kompilatora.
$ gcc -Wall -Wextra -pedantic fnord.c fnord.c: In function ‘main’: fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=] printf("%f\n",0); ^
lub
$ clang -Weverything -pedantic fnord.c fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat] printf("%f\n",0); ~~ ^ %d 1 warning generated.
Tak więc
printf
generuje niezdefiniowane zachowanie, ponieważ przekazujesz mu niezgodny typ argumentu.źródło
Nie wiem, co jest niejasne.
Twój ciąg formatu oczekuje
double
; zamiast tego podajesz plikint
.To, czy te dwa typy mają tę samą szerokość bitową, jest całkowicie nieistotne, z wyjątkiem tego, że może to pomóc w uniknięciu wyjątków twardych naruszeń pamięci z powodu takiego zepsutego kodu.
źródło
int
byłby tutaj akceptowalny.int
kwalifikowałby się jako prawidłowy wzorzec zmiennoprzecinkowy? Uzupełnienie do dwóch i różne kodowania zmiennoprzecinkowe nie mają prawie nic wspólnego.0
do wpisanego argumentudouble
zrobi właściwą rzecz. Dla początkującego nie jest oczywiste, że kompilator nie wykonuje tej samej konwersji dlaprintf
gniazd argumentów adresowanych przez%[efg]
."%f\n"
gwarantuje przewidywalny wynik tylko wtedy, gdy drugiprintf()
parametr ma typdouble
. Następnie dodatkowe argumenty funkcji wariadycznych są przedmiotem domyślnej promocji argumentów. Argumenty całkowite są objęte promocją liczb całkowitych, co nigdy nie skutkuje wartościami zmiennoprzecinkowymi. Ifloat
parametry są promowane nadouble
.Na domiar złego : standard pozwala na to, aby drugi argument był lub
float
lubdouble
i nic więcej.źródło
Dlaczego formalnie jest to UB, zostało omówione w kilku odpowiedziach.
Powód, dla którego otrzymujesz takie zachowanie, jest zależny od platformy, ale prawdopodobnie jest następujący:
printf
oczekuje swoich argumentów zgodnie ze standardową propagacją vararg. Oznacza to, żefloat
będzie todouble
i wszystko mniejsze niżint
będzieint
.int
gdzie funkcja oczekuje plikudouble
. Twójint
jest prawdopodobnie 32-bitowy, twójdouble
64-bitowy. Oznacza to, że cztery bajty stosu rozpoczynające się w miejscu, w którym powinien znajdować się argument0
, znajdują się , ale kolejne cztery bajty mają dowolną zawartość. To jest używane do konstruowania wartości, która jest wyświetlana.źródło
Główną przyczyną tego problemu z „nieokreśloną wartością” jest rzutowanie wskaźnika na
int
wartość przekazaną doprintf
sekcji parametrów zmiennych do wskaźnika odouble
typach, któreva_arg
makro.Powoduje to odwołanie do obszaru pamięci, który nie został całkowicie zainicjowany wartością przekazaną jako parametr do printf, ponieważ
double
rozmiar obszaru buforu pamięci jest większy niżint
rozmiar.Dlatego też, gdy ten wskaźnik jest wyłuskiwany, zwracana jest nieokreślona wartość lub lepiej „wartość”, która zawiera częściowo wartość przekazaną jako parametr do
printf
, a dla pozostałej części może pochodzić z innego obszaru bufora stosu lub nawet obszaru kodu ( podnoszenie wyjątku błędu pamięci), prawdziwy przepełnienie bufora .Może uwzględniać te konkretne fragmenty półproduktów implementacji kodu „printf” i „va_arg” ...
printf
va_list arg; .... case('%f') va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf.. ....
va_arg
char *p = (double *) &arg + sizeof arg; //printf parameters area pointer double i2 = *((double *)p); //casting to double because va_arg(arg, double) p += sizeof (double);
źródło