Dlaczego printf ("%f", 0); daje nieokreślone zachowanie?

Oświadczenie

printf("%f\n",0.0f);

Drukuje 0.

Jednak twierdzenie

printf("%f\n",0);

Wyświetla losowe wartości.

Zdaję sobie sprawę, że przejawiam jakieś nieokreślone zachowanie, ale nie mogę zrozumieć dlaczego.

Wartość zmiennoprzecinkowa, w której wszystkie bity są równe 0, jest nadal poprawną {[3] } z wartością 0.
float i int są tego samego rozmiaru na mojej maszynie(jeśli to w ogóle istotne).

Dlaczego używa się liczby całkowitej zamiast zmiennoprzecinkowej dosłowne w printf powodują takie zachowanie?

P. S. to samo zachowanie można zobaczyć, jeśli używam

int i = 0;
printf("%f\n", i);
Author: Antti Haapala, 2016-07-26

10 answers

Format "%f" wymaga argumentu typu double. Podajesz argument typu int. Dlatego zachowanie jest nieokreślone.

Standard nie gwarantuje, że wszystkie bity-zero są poprawną reprezentacją 0.0 (choć często tak jest), lub dowolnej wartości double, lub że int i double są tego samego rozmiaru (pamiętaj, że to double, a nie float), lub nawet jeśli są tego samego rozmiaru, to są przekazywane jako argumenty do funkcji zmiennej w ten sam sposób.

To może zdarza się, że" działa " na Twoim systemie. To najgorszy możliwy objaw nieokreślonego zachowania, ponieważ utrudnia zdiagnozowanie błędu.

N1570 7.21.6.1 pkt 9:

... Jeśli jakikolwiek argument nie jest prawidłowym typem dla odpowiedniego Specyfikacja konwersji, zachowanie jest niezdefiniowane.

Argumenty typu float są promowane do double, dlatego printf("%f\n",0.0f) Działa. Argumenty typów całkowitych węższe niż int są promowane do int lub do unsigned int. Niniejsze zasady promocji (określone w N1570 6.5.2.2 pkt 6) nie pomagają w przypadku printf("%f\n", 0).

 119
Author: Keith Thompson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 23:55:03

Po pierwsze, jak wspomniano w kilku innych odpowiedziach, ale moim zdaniem nie jest to wystarczająco jasne: Działa, aby zapewnić liczbę całkowitą w większości kontekstów, w których funkcja biblioteczna przyjmuje argument double lub float. Kompilator automatycznie wstawia konwersję. Na przykład, sqrt(0) jest dobrze zdefiniowany i będzie zachowywał się dokładnie tak, jak sqrt((double)0), i to samo dotyczy każdego innego wyrażenia typu integer używanego tam.

printf to co innego. Jest inny, ponieważ pobiera zmienną liczbę argumentów. Jego prototypem funkcji jest

extern int printf(const char *fmt, ...);

Dlatego kiedy piszesz

printf(message, 0);

Kompilator nie ma żadnych informacji o tym, jakiego typu printf oczekuje , że drugi argument będzie. Ma tylko typ wyrażenia argumentu, który jest int, aby przejść przez. Dlatego, w przeciwieństwie do większości funkcji bibliotecznych, to Ty, programista, musisz upewnić się, że lista argumentów odpowiada oczekiwaniom ciągu formatującego.

(Modern Kompilatory mogą przyjrzeć się ciągowi formatowemu i powiedzieć ci, że masz niedopasowanie typu, ale nie zaczną wstawiać konwersji, aby osiągnąć to, co miałeś na myśli, ponieważ lepiej, aby Twój kod zepsuł się teraz, kiedy zauważysz, niż lata później, gdy został przebudowany z mniej pomocnym kompilatorem.)

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, oba reprezentowane jako 32 bity, z których wszystkie są zerowe, dlaczego to nie działa w każdym razie, przez przypadek? Standard C mówi po prostu "to nie jest wymagane do pracy, jesteś zdany na siebie", ale pozwól mi wyjaśnić dwa najczęstsze powody, dla których to nie działa; to prawdopodobnie pomoże Ci zrozumieć dlaczego to nie jest wymagane.

Po pierwsze, ze względów historycznych, kiedy przekazujesz {[3] } przez zmienną listę argumentów, otrzymuje awans do double, co w większości nowoczesnych systemów jest 64 bity szerokie. Więc printf("%f", 0) przekazuje tylko 32 bity zerowe do callee 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) wygląda w rejestrze f1 na to zero, ale w ogóle go tam nie ma.

 58
Author: zwol,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 19:20:42

Zwykle gdy wywołujesz funkcję oczekującą double, ale podajesz int, kompilator automatycznie przekształci się w double dla Ciebie. Tak się nie dzieje z printf, ponieważ typy argumentów nie są określone w prototypie funkcji - kompilator nie wie, że należy zastosować konwersję.

 13
Author: Mark Ransom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 18:28:08

Dlaczego użycie liczby całkowitej zamiast liczby zmiennoprzecinkowej powoduje takie zachowanie?

Ponieważ printf() nie ma wpisanych parametrów poza const char* formatstring jako pierwszy. Używa elipsy w stylu c (...) dla całej reszty.

To po prostu decyduje, jak interpretować przekazywane tam wartości zgodnie z typami formatowania podanymi w łańcuchu formatowania.

Zachowywałbyś się tak samo, jak przy próbie

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB
 13
Author: πάντα ῥεῖ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 19:09:04

Użycie źle dopasowanego specyfika printf() "%f"i typu (int) 0 prowadzi do niezdefiniowanego zachowania.

Jeśli specyfikacja konwersji jest nieprawidłowa, zachowanie jest niezdefiniowane. C11dr §7.21.6.1 9

Kandydaci do UB.

  1. To jest UB per spec, a kompilacja jest ornery-powiedział nuf.

  2. double i {[4] } są różnej wielkości.

  3. double i int mogą przekazywać swoje wartości za pomocą różnych stosów (ogólne vs. FPU stack.)

  4. A double 0.0 może nie być zdefiniowany przez cały zero bitowy wzorzec. (rzadko)

 12
Author: chux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 20:27:12

Jest to jedna z tych wspaniałych możliwości, aby dowiedzieć się 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.

Więc, printf produkuje niezdefiniowanego zachowania, ponieważ przekazujesz mu niezgodny typ argumentu.

 10
Author: wyrm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-27 15:46:38

Nie wiem, co jest mylące.

Twój format oczekuje double; zamiast tego podajesz int.

To, czy te dwa typy mają tę samą szerokość bitów, jest całkowicie nieistotne, z wyjątkiem tego, że może to pomóc uniknąć wyjątków od naruszenia twardej pamięci z uszkodzonego kodu, takiego jak ten.

 9
Author: Lightness Races in Orbit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 18:25:58

"%f\n" gwarantuje przewidywalny wynik tylko wtedy, gdy drugi parametr printf() mA typ double. Następnie dodatkowe argumenty funkcji wariacyjnych są przedmiotem domyślnej promocji argumentów. Argumenty liczb całkowitych należą do promocji liczby całkowitej, która nigdy nie skutkuje wartościami zmiennoprzecinkowymi. I float parametry są promowane do double.

Na koniec: standard pozwala na to, aby drugi argument był or float lub double i nic więcej.

 4
Author: Sergio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 19:53:56

Dlaczego jest formalnie UB zostało omówione w kilku odpowiedziach.

Powód, dla którego masz konkretnie to zachowanie, jest zależny od platformy, ale prawdopodobnie jest następujący:

  • printf oczekuje jego argumentów zgodnie ze standardową propagacją vararg. Oznacza to, że {[1] } będzie double i wszystko mniejsze od {[3] } będzie int.
  • przekazujesz int gdzie funkcja oczekuje double. Twój int jest prawdopodobnie 32-bitowy, Twój double 64-bitowy. Że oznacza to, że cztery bajty stosu zaczynające się w miejscu, w którym ma się znajdować argument to 0, ale następujące cztery bajty mają dowolną zawartość. To jest to, co jest używane do konstruowania wartości, która jest wyświetlana.
 4
Author: glglgl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-27 07:14:33

Główna przyczyna tego problemu "nieokreślonej wartości" znajduje się w obsadzie wskaźnika na int wartość przekazana do sekcji printf parametry zmiennej do wskaźnika na typach double, które wykonuje makro va_arg.

Powoduje to odwołanie się do obszaru pamięci, który nie został całkowicie zainicjowany z wartością przekazaną jako parametr do printf, ponieważ double Rozmiar obszaru bufora pamięci jest większy niż int Rozmiar.

Dlatego, gdy ten wskaźnik jest dereferenced, jest zwracany nieokreślona wartość, lub lepiej "wartość", która zawiera w części wartość przekazaną jako parametr do printf, A dla pozostałej części może pochodzić z innego obszaru bufora stosu lub nawet obszaru kodu (wywołanie wyjątku błędu pamięci), rzeczywiste przepełnienie bufora.


może brać pod uwagę te specyficzne fragmenty semplifikowanych 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..
.... 


the real implementacja w vprintf (biorąc pod uwagę gnu impl.) o podwójnych parametrach wartości zarządzanie jest:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



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



linki

  1. implementacja projektu gnu glibc "printf"(vprintf))
  2. przykład kodu semplifikacji printf
  3. przykład kodu semplifikacji va_arg
 0
Author: Ciro Corvino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:24:19