Warum macht printf ("% f", 0); undefiniertes Verhalten geben?


87

Die Aussage

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

druckt 0.

Allerdings ist die Aussage

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

druckt zufällige Werte.

Mir ist klar, dass ich eine Art undefiniertes Verhalten zeige, aber ich kann nicht herausfinden, warum genau.

Ein Gleitkommawert, bei dem alle Bits 0 sind, ist floatmit dem Wert 0 noch gültig
floatund hat intauf meinem Computer die gleiche Größe (falls dies überhaupt relevant ist).

Warum verursacht die Verwendung eines Integer-Literals anstelle eines Gleitkomma-Literals printfdieses Verhalten?

PS das gleiche Verhalten kann gesehen werden, wenn ich benutze

int i = 0;
printf("%f\n", i);

37
printferwartet ein double, und du gibst es ein int. floatund intkann auf Ihrem Computer dieselbe Größe haben, wird jedoch 0.0ftatsächlich in eine konvertiert, doublewenn sie in eine Liste mit variablen Argumenten verschoben wird (und printferwartet dies). Kurz gesagt, Sie erfüllen Ihr Schnäppchen nicht mit printfden von Ihnen verwendeten Spezifizierern und den von Ihnen angegebenen Argumenten.
WhozCraig

22
Varargs-Funktionen konvertieren Funktionsargumente nicht automatisch in den Typ des entsprechenden Parameters, da dies nicht möglich ist. Die erforderlichen Informationen stehen dem Compiler im Gegensatz zu Nicht-Varargs-Funktionen mit einem Prototyp nicht zur Verfügung.
EOF

3
Oooh ... "Variadics". Ich habe gerade ein neues Wort gelernt ...
Mike Robinson


3
Das nächste , was zu versuchen , ist eine weitergeben (uint64_t)0statt 0und sehen , ob Sie noch zufälliges Verhalten bekommen (vorausgesetzt , doubleund uint64_thaben die gleiche Größe und Ausrichtung). Es besteht die Möglichkeit, dass die Ausgabe auf einigen Plattformen (z. B. x86_64) weiterhin zufällig ist, da unterschiedliche Typen in unterschiedlichen Registern übergeben werden.
Ian Abbott

Antworten:


121

Das "%f"Format erfordert ein Argument vom Typ double. Sie geben ihm ein typisches Argument int. Deshalb ist das Verhalten undefiniert.

Der Standard garantiert nicht , dass alle Bits von Null ist eine gültige Darstellung 0.0(obwohl es oft ist), oder eines doubleWertes, oder dass intund doubledie gleiche Größe haben (denken Sie daran , es ist doublenicht float), oder, selbst wenn sie die gleichen sind Größe, dass sie auf die gleiche Weise als Argumente an eine variable Funktion übergeben werden.

Es kann passieren, dass es auf Ihrem System "funktioniert". Dies ist das schlimmste Symptom für undefiniertes Verhalten, da es schwierig ist, den Fehler zu diagnostizieren.

N1570 7.21.6.1 Absatz 9:

... Wenn ein Argument nicht der richtige Typ für die entsprechende Konvertierungsspezifikation ist, ist das Verhalten undefiniert.

Argumente des Typs floatwerden befördert double, weshalb printf("%f\n",0.0f)funktioniert. Argumente von ganzzahligen Typen, die enger sind als intzu intoder nach heraufgestuft unsigned int. Diese Werberegeln (festgelegt in N1570 6.5.2.2 Absatz 6) helfen im Fall von nicht printf("%f\n", 0).

Beachten Sie 0, dass doubledas Verhalten gut definiert ist , wenn Sie eine Konstante an eine nicht variadische Funktion übergeben, die ein Argument erwartet , vorausgesetzt, der Prototyp der Funktion ist sichtbar. Beispielsweise konvertiert sqrt(0)(after #include <math.h>) das Argument implizit 0von intnach double-, da der Compiler anhand der Deklaration erkennen kann, sqrtdass er ein doubleArgument erwartet . Es hat keine solchen Informationen für printf. Variadische Funktionen wie printfsind etwas Besonderes und erfordern mehr Sorgfalt beim Schreiben von Anrufen.


13
Ein paar ausgezeichnete Kernpunkte hier. Erstens, dass es doublenicht floatso ist , dass die Breitenannahme des OP möglicherweise nicht (wahrscheinlich nicht) gilt. Zweitens gilt auch nicht die Annahme, dass Ganzzahl Null und Gleitkomma Null das gleiche Bitmuster haben. Gute Arbeit
Leichtigkeitsrennen im Orbit

2
@ LucasTrzesniewski: Ok, aber ich sehe nicht, wie meine Antwort die Frage aufwirft. Ich habe gesagt, dass floatbefördert wird, doubleohne zu erklären, warum, aber das war nicht der Hauptpunkt.
Keith Thompson

2
@ robertbristow-johnson: Compiler müssen keine speziellen Hooks haben printf, obwohl gcc zum Beispiel einige hat, damit es Fehler diagnostizieren kann ( wenn die Formatzeichenfolge ein Literal ist). Der Compiler kann die Deklaration von printffrom sehen <stdio.h>, die besagt, dass der erste Parameter a ist const char*und der Rest durch angezeigt wird , .... Nein, %fist für double(und floatwird befördert double) und %lfist für long double. Der C-Standard sagt nichts über einen Stapel aus. Es gibt das Verhalten printfnur an, wenn es korrekt aufgerufen wird.
Keith Thompson

2
@ robertbristow-johnson: In der alten Benommenheit führte "lint" oft einige der zusätzlichen Überprüfungen durch, die gcc jetzt durchführt. Ein floatübergeben an printfwird befördert zu double; Daran ist nichts Magisches, es ist nur eine Sprachregel zum Aufrufen verschiedener Funktionen. printfselbst weiß über das Format - String , was der Anrufer behauptete , zu übergeben; Wenn diese Behauptung falsch ist, ist das Verhalten undefiniert.
Keith Thompson

2
Kleine Korrektur: die lLängenmodifizierer „hat keine Auswirkung auf folgende a, A, e, E, f, F, g, oder GKonvertierungsspezifizierer“, die Längenmodifizierer für eine long doubleUmwandlung ist L. (@ Robertbristow-Johnson könnte auch interessiert sein)
Daniel Fischer

58

Zunächst einmal, wie in mehreren anderen Antworten erwähnt, aber meines Erachtens nicht klar genug formuliert: In den meisten Kontexten, in denen eine Bibliotheksfunktion ein oder ein Argument verwendet, funktioniert es, eine Ganzzahl bereitzustellen . Der Compiler fügt automatisch eine Konvertierung ein. Zum Beispiel ist es gut definiert und verhält sich genau so , und dasselbe gilt für alle anderen dort verwendeten Ausdrücke vom Typ Ganzzahl.doublefloatsqrt(0)sqrt((double)0)

printfist anders. Es ist anders, weil es eine variable Anzahl von Argumenten benötigt. Sein Funktionsprototyp ist

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

Deshalb, wenn Sie schreiben

printf(message, 0);

Der Compiler hat keine Informationen darüber, welcher Typ dieses zweite Argument printf erwartet . Es hat nur den Typ des Argumentausdrucks, der verwendet werden soll int. Im Gegensatz zu den meisten Bibliotheksfunktionen müssen Sie als Programmierer sicherstellen, dass die Argumentliste den Erwartungen der Formatzeichenfolge entspricht.

(Moderne Compiler können in eine Formatzeichenfolge schauen und Ihnen mitteilen, dass Sie eine Typinkongruenz haben, aber sie werden keine Konvertierungen einfügen, um das zu erreichen, was Sie gemeint haben, da Ihr Code jetzt besser kaputt gehen sollte, wenn Sie es bemerken , als Jahre später, als es mit einem weniger hilfreichen Compiler neu erstellt wurde.)

Die andere Hälfte der Frage lautete: Angesichts der Tatsache, dass (int) 0 und (float) 0.0 auf den meisten modernen Systemen beide als 32 Bit dargestellt werden, die alle Null sind, warum funktioniert es nicht aus Versehen? Der C-Standard sagt nur "das ist nicht erforderlich, um zu funktionieren, Sie sind allein", aber lassen Sie mich die zwei häufigsten Gründe darlegen, warum es nicht funktionieren würde; Das wird Ihnen wahrscheinlich helfen zu verstehen, warum es nicht erforderlich ist.

Erstens, aus historischen Gründen, wenn Sie einen Pass floatdurch eine variable Argumentliste wird sie gefördert zu double, die auf den meisten modernen Systemen ist 64 Bit breit. printf("%f", 0)Übergibt also nur 32 Null-Bits an einen Angerufenen, der 64 davon erwartet.

Der zweite, ebenso wichtige Grund ist, dass Gleitkommafunktionsargumente an einer anderen Stelle als ganzzahlige Argumente übergeben werden können. Beispielsweise haben die meisten CPUs separate Registerdateien für Ganzzahlen und Gleitkommawerte. Daher können die Argumente 0 bis 4 in den Registern r0 bis r4 enthalten sein, wenn sie Ganzzahlen sind, aber f0 bis f4, wenn sie Gleitkommawerte sind. So printf("%f", 0)sieht im Register f1 für die Null, aber es ist nicht , dass es überhaupt nicht .


1
Gibt es Architekturen, die Register für verschiedene Funktionen verwenden, auch unter solchen, die sie für normale Funktionen verwenden? Ich dachte, das war der Grund, warum verschiedene Funktionen ordnungsgemäß deklariert werden müssen, obwohl andere Funktionen [außer denen mit float / short / char-Argumenten] mit deklariert werden können ().
Random832

3
@ Random832 Heutzutage besteht der einzige Unterschied zwischen der Aufrufkonvention einer Variadik und einer normalen Funktion darin, dass einer Variadik möglicherweise einige zusätzliche Daten zugeführt werden, z. B. die Anzahl der tatsächlich angegebenen Argumente. Ansonsten läuft alles genau an der gleichen Stelle wie bei einer normalen Funktion. Siehe zum Beispiel Abschnitt 3.2 von x86-64.org/documentation/abi.pdf , in dem die einzige spezielle Behandlung für Variadics ein übergebener Hinweis ist AL. (Ja, dies bedeutet, dass die Implementierung von va_argviel komplizierter ist als früher.)
zwol

@ Random832: Ich dachte immer, der Grund dafür sei, dass auf einigen Architekturen Funktionen mit einer bekannten Anzahl und Art von Argumenten mithilfe spezieller Anweisungen effizienter implementiert werden könnten.
Celtschk

@celtschk Sie denken vielleicht an die "Registerfenster" auf SPARC und IA64, die den allgemeinen Fall von Funktionsaufrufen mit einer kleinen Anzahl von Argumenten beschleunigen sollten (leider tun sie in der Praxis genau das Gegenteil). Der Compiler muss verschiedene Funktionsaufrufe nicht speziell behandeln, da die Anzahl der Argumente an einem Aufrufstandort immer eine Konstante für die Kompilierungszeit ist, unabhängig davon, ob der Angerufene variadisch ist.
zwol

@zwol: Nein, ich habe an die ret nAnweisung des 8086 gedacht , bei der nes sich um eine fest codierte Ganzzahl handelt, die daher für verschiedene Funktionen nicht anwendbar ist. Ich weiß jedoch nicht, ob ein C-Compiler tatsächlich davon profitiert hat (Nicht-C-Compiler sicherlich).
Celtschk

13

Wenn Sie eine Funktion aufrufen, die a erwartet double, aber eine bereitstellt int, konvertiert der Compiler normalerweise automatisch in a doublefür Sie. Dies ist nicht der Fall printf, da die Arten der Argumente im Funktionsprototyp nicht angegeben sind. Der Compiler weiß nicht, dass eine Konvertierung angewendet werden soll.


4
Auch printf() insbesondere ist so ausgelegt, dass seine Argumente jeglicher Art sein könnten. Sie müssen wissen, welcher Typ von jedem Element in der Formatzeichenfolge erwartet wird, und Sie müssen ihn korrekt angeben.
Mike Robinson

@ MikeRobinson: Nun, jeder primitive C-Typ. Welches ist eine sehr, sehr kleine Teilmenge aller möglichen Typen.
MSalters

13

Warum verursacht die Verwendung eines Integer-Literals anstelle eines Float-Literals dieses Verhalten?

Weil printf()außer dem ersten Parameter keine Parameter eingegeben wurden const char* formatstring. ...Für den Rest wird eine Ellipse im C-Stil ( ) verwendet.

Es wird lediglich entschieden, wie die dort übergebenen Werte gemäß den in der Formatzeichenfolge angegebenen Formatierungstypen interpretiert werden sollen.

Sie würden das gleiche undefinierte Verhalten haben wie beim Versuch

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

3
Einige bestimmte Implementierungen von printffunktionieren möglicherweise auf diese Weise (außer dass die übergebenen Elemente Werte und keine Adressen sind). Der C-Standard legt nicht fest, wie printf und andere verschiedene Funktionen funktionieren, sondern nur deren Verhalten. Insbesondere werden Stapelrahmen nicht erwähnt.
Keith Thompson

Ein kleiner Kritikpunkt : printfhat eine typisierte Parameter, die Formatzeichenfolge, die vom Typ ist const char*. Übrigens ist die Frage sowohl mit C als auch mit C ++ gekennzeichnet, und C ist wirklich relevanter. Ich hätte es wahrscheinlich nicht reinterpret_castals Beispiel genommen.
Keith Thompson

Nur eine interessante Beobachtung: Gleiches undefiniertes Verhalten und höchstwahrscheinlich aufgrund eines identischen Mechanismus, jedoch mit einem kleinen Unterschied im Detail: Wenn Sie ein int wie in der Frage übergeben, geschieht das UB innerhalb von printf, wenn Sie versuchen, das int als double zu interpretieren - in Ihrem Beispiel , es passiert schon draußen bei der Dereferenzierung pf ...
Aconcagua

@Aconcagua Klarstellung hinzugefügt.
πάντα ῥεῖ

Dieses Codebeispiel ist UB für strikte Aliasing-Verletzung, ein völlig anderes Problem als das, worüber die Frage gestellt wird. Beispielsweise ignorieren Sie die Möglichkeit, dass Floats in verschiedenen Registern an Ganzzahlen übergeben werden, vollständig.
MM

12

Die Verwendung eines falsch übereinstimmenden printf()Bezeichners "%f"und Typs (int) 0führt zu undefiniertem Verhalten.

Wenn eine Konvertierungsspezifikation ungültig ist, ist das Verhalten undefiniert. C11dr §7.21.6.1 9

Kandidatenursachen für UB.

  1. Es ist UB pro Spezifikation und die Kompilierung ist ornery - 'nuf sagte.

  2. doubleund intsind unterschiedlich groß.

  3. doubleund intkönnen ihre Werte unter Verwendung verschiedener Stapel (allgemeiner vs. FPU- Stapel) übergeben.

  4. A wird double 0.0 möglicherweise nicht durch ein Null-Bit-Muster definiert. (Selten)


10

Dies ist eine dieser großartigen Möglichkeiten, um aus Ihren Compiler-Warnungen zu lernen.

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

oder

$ 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.

Erzeugt printfalso undefiniertes Verhalten, weil Sie es als inkompatiblen Argumenttyp übergeben.


9

Ich bin mir nicht sicher, was verwirrend ist.

Ihre Formatzeichenfolge erwartet a double; Sie bieten stattdessen eineint .

Ob die beiden Typen die gleiche Bitbreite haben, ist völlig irrelevant, außer dass Sie möglicherweise vermeiden können, Ausnahmen von der Verletzung des harten Speichers durch solchen fehlerhaften Code zu erhalten.


3
@Voo: Dieser Format-String-Modifikator heißt leider, aber ich verstehe immer noch nicht, warum Sie denken würden, dass ein inthier akzeptabel wäre.
Leichtigkeitsrennen im Orbit

1
@Voo: "(was sich auch als gültiges Float-Musterint qualifizieren würde )" Warum sollte sich ein als gültiges Float-Muster qualifizieren? Das Zweierkomplement und verschiedene Gleitkomma-Codierungen haben fast nichts gemeinsam.
Leichtigkeitsrennen im Orbit

2
Dies ist verwirrend, da für die meisten Bibliotheksfunktionen die Bereitstellung des Ganzzahlliteral 0für ein eingegebenes Argument doubledas Richtige bewirkt. Für einen Anfänger ist es nicht offensichtlich, dass der Compiler nicht dieselbe Konvertierung für printfArgument-Slots durchführt, die von angesprochen werden %[efg].
zwol

1
@Voo: Wenn Sie daran interessiert sind, wie schrecklich dies schief gehen kann, sollten Sie berücksichtigen, dass auf x86-64 SysV ABI Gleitkomma-Argumente in einem anderen Registersatz als ganzzahlige Argumente übergeben werden.
EOF

1
@LightnessRacesinOrbit Ich denke, es ist immer angebracht zu diskutieren, warum etwas UB ist, was normalerweise beinhaltet, darüber zu sprechen, welcher Implementierungsspielraum zulässig ist und was in häufigen Fällen tatsächlich passiert.
zwol

4

"%f\n"garantiert ein vorhersehbares Ergebnis nur, wenn der zweite printf()Parameter den Typ hat double. Als nächstes werden zusätzliche Argumente für verschiedene Funktionen der Standardargumentförderung unterzogen. Ganzzahlige Argumente fallen unter die Ganzzahl-Heraufstufung, was niemals zu Gleitkommawerten führt. Und floatParameter werden auf befördertdouble .

Um das Ganze abzurunden: Standard erlaubt, dass das zweite Argument oder floatoder doubleund nichts anderes ist.


4

Warum es formal UB ist, wurde jetzt in mehreren Antworten diskutiert.

Der Grund, warum Sie speziell dieses Verhalten erhalten, ist plattformabhängig, aber wahrscheinlich der folgende:

  • printferwartet seine Argumente gemäß der Standard-Vararg-Propagierung. Das heißt, ein floatWille ist ein doubleund alles, was kleiner als ein intWille istint .
  • Sie übergeben ein, intwo die Funktion a erwartet double. Ihr intist wahrscheinlich 32 Bit, Ihr double64 Bit. Das bedeutet, dass die vier Stapelbytes, die an der Stelle beginnen, an der sich das Argument befinden soll, sich befinden 0, die folgenden vier Bytes jedoch einen beliebigen Inhalt haben. Damit wird der angezeigte Wert erstellt.

0

Die Hauptursache für dieses Problem mit "unbestimmten Werten" liegt in der Umwandlung des Zeigers auf den intWert, der an den printfAbschnitt mit den variablen Parametern an einen Zeiger bei doubleTypen übergeben wird, dieva_arg Makro ausführt.

Dies führt zu einem Verweis auf einen Speicherbereich, der nicht vollständig mit dem als Parameter an printf übergebenen Wert initialisiert wurde, da der doubleSpeicherpufferbereich größer als die intGröße ist.

Wenn dieser Zeiger dereferenziert wird, wird daher ein unbestimmter Wert oder besser ein "Wert" zurückgegeben, der teilweise den als Parameter übergebenen Wert enthält printfund für den verbleibenden Teil aus einem anderen Stapelpufferbereich oder sogar einem Codebereich stammen könnte ( Auslösen einer Speicherfehlerausnahme), ein echter Pufferüberlauf .


Es kann diese spezifischen Teile semplifizierter Code-Implementierungen von "printf" und "va_arg" ... printf berücksichtigen

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..
.... 


Die eigentliche Implementierung des Code-Case-Managements mit doppelten Wertparametern in vprintf (unter Berücksichtigung von gnu impl.) ist:

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



Verweise

  1. gnu project glibc Implementierung von "printf" (vprintf))
  2. Beispiel für einen Vereinfachungscode von printf
  3. Beispiel für einen Semplifikationscode von va_arg
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.