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 float
mit dem Wert 0 noch gültig
float
und hat int
auf meinem Computer die gleiche Größe (falls dies überhaupt relevant ist).
Warum verursacht die Verwendung eines Integer-Literals anstelle eines Gleitkomma-Literals printf
dieses Verhalten?
PS das gleiche Verhalten kann gesehen werden, wenn ich benutze
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
quelle
quelle
printf
erwartet eindouble
, und du gibst es einint
.float
undint
kann auf Ihrem Computer dieselbe Größe haben, wird jedoch0.0f
tatsächlich in eine konvertiert,double
wenn sie in eine Liste mit variablen Argumenten verschoben wird (undprintf
erwartet dies). Kurz gesagt, Sie erfüllen Ihr Schnäppchen nicht mitprintf
den von Ihnen verwendeten Spezifizierern und den von Ihnen angegebenen Argumenten.(uint64_t)0
statt0
und sehen , ob Sie noch zufälliges Verhalten bekommen (vorausgesetzt ,double
unduint64_t
haben 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.Antworten:
Das
"%f"
Format erfordert ein Argument vom Typdouble
. Sie geben ihm ein typisches Argumentint
. 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 einesdouble
Wertes, oder dassint
unddouble
die gleiche Größe haben (denken Sie daran , es istdouble
nichtfloat
), 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:
Argumente des Typs
float
werden befördertdouble
, weshalbprintf("%f\n",0.0f)
funktioniert. Argumente von ganzzahligen Typen, die enger sind alsint
zuint
oder nach heraufgestuftunsigned int
. Diese Werberegeln (festgelegt in N1570 6.5.2.2 Absatz 6) helfen im Fall von nichtprintf("%f\n", 0)
.Beachten Sie
0
, dassdouble
das 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 konvertiertsqrt(0)
(after#include <math.h>
) das Argument implizit0
vonint
nachdouble
-, da der Compiler anhand der Deklaration erkennen kann,sqrt
dass er eindouble
Argument erwartet . Es hat keine solchen Informationen fürprintf
. Variadische Funktionen wieprintf
sind etwas Besonderes und erfordern mehr Sorgfalt beim Schreiben von Anrufen.quelle
double
nichtfloat
so 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 Arbeitfloat
befördert wird,double
ohne zu erklären, warum, aber das war nicht der Hauptpunkt.printf
, obwohl gcc zum Beispiel einige hat, damit es Fehler diagnostizieren kann ( wenn die Formatzeichenfolge ein Literal ist). Der Compiler kann die Deklaration vonprintf
from sehen<stdio.h>
, die besagt, dass der erste Parameter a istconst char*
und der Rest durch angezeigt wird, ...
. Nein,%f
ist fürdouble
(undfloat
wird befördertdouble
) und%lf
ist fürlong double
. Der C-Standard sagt nichts über einen Stapel aus. Es gibt das Verhaltenprintf
nur an, wenn es korrekt aufgerufen wird.float
übergeben anprintf
wird befördert zudouble
; Daran ist nichts Magisches, es ist nur eine Sprachregel zum Aufrufen verschiedener Funktionen.printf
selbst weiß über das Format - String , was der Anrufer behauptete , zu übergeben; Wenn diese Behauptung falsch ist, ist das Verhalten undefiniert.l
Längenmodifizierer „hat keine Auswirkung auf folgendea
,A
,e
,E
,f
,F
,g
, oderG
Konvertierungsspezifizierer“, die Längenmodifizierer für einelong double
Umwandlung istL
. (@ Robertbristow-Johnson könnte auch interessiert sein)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.
double
float
sqrt(0)
sqrt((double)0)
printf
ist anders. Es ist anders, weil es eine variable Anzahl von Argumenten benötigt. Sein Funktionsprototyp istDeshalb, wenn Sie schreiben
Der Compiler hat keine Informationen darüber, welcher Typ dieses zweite Argument
printf
erwartet . Es hat nur den Typ des Argumentausdrucks, der verwendet werden sollint
. 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
float
durch eine variable Argumentliste wird sie gefördert zudouble
, 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 .quelle
()
.AL
. (Ja, dies bedeutet, dass die Implementierung vonva_arg
viel komplizierter ist als früher.)ret n
Anweisung des 8086 gedacht , bei dern
es 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).Wenn Sie eine Funktion aufrufen, die a erwartet
double
, aber eine bereitstelltint
, konvertiert der Compiler normalerweise automatisch in adouble
für Sie. Dies ist nicht der Fallprintf
, da die Arten der Argumente im Funktionsprototyp nicht angegeben sind. Der Compiler weiß nicht, dass eine Konvertierung angewendet werden soll.quelle
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.Weil
printf()
außer dem ersten Parameter keine Parameter eingegeben wurdenconst 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
quelle
printf
funktionieren möglicherweise auf diese Weise (außer dass die übergebenen Elemente Werte und keine Adressen sind). Der C-Standard legt nicht fest, wieprintf
und andere verschiedene Funktionen funktionieren, sondern nur deren Verhalten. Insbesondere werden Stapelrahmen nicht erwähnt.printf
hat eine typisierte Parameter, die Formatzeichenfolge, die vom Typ istconst char*
. Übrigens ist die Frage sowohl mit C als auch mit C ++ gekennzeichnet, und C ist wirklich relevanter. Ich hätte es wahrscheinlich nichtreinterpret_cast
als Beispiel genommen.Die Verwendung eines falsch übereinstimmenden
printf()
Bezeichners"%f"
und Typs(int) 0
führt zu undefiniertem Verhalten.Kandidatenursachen für UB.
Es ist UB pro Spezifikation und die Kompilierung ist ornery - 'nuf sagte.
double
undint
sind unterschiedlich groß.double
undint
können ihre Werte unter Verwendung verschiedener Stapel (allgemeiner vs. FPU- Stapel) übergeben.A wird
double 0.0
möglicherweise nicht durch ein Null-Bit-Muster definiert. (Selten)quelle
Dies ist eine dieser großartigen Möglichkeiten, um aus Ihren Compiler-Warnungen zu lernen.
oder
Erzeugt
printf
also undefiniertes Verhalten, weil Sie es als inkompatiblen Argumenttyp übergeben.quelle
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.
quelle
int
hier akzeptabel wäre.int
qualifizieren würde )" Warum sollte sich ein als gültiges Float-Muster qualifizieren? Das Zweierkomplement und verschiedene Gleitkomma-Codierungen haben fast nichts gemeinsam.0
für ein eingegebenes Argumentdouble
das Richtige bewirkt. Für einen Anfänger ist es nicht offensichtlich, dass der Compiler nicht dieselbe Konvertierung fürprintf
Argument-Slots durchführt, die von angesprochen werden%[efg]
."%f\n"
garantiert ein vorhersehbares Ergebnis nur, wenn der zweiteprintf()
Parameter den Typ hatdouble
. 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. Undfloat
Parameter werden auf befördertdouble
.Um das Ganze abzurunden: Standard erlaubt, dass das zweite Argument oder
float
oderdouble
und nichts anderes ist.quelle
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:
printf
erwartet seine Argumente gemäß der Standard-Vararg-Propagierung. Das heißt, einfloat
Wille ist eindouble
und alles, was kleiner als einint
Wille istint
.int
wo die Funktion a erwartetdouble
. Ihrint
ist wahrscheinlich 32 Bit, Ihrdouble
64 Bit. Das bedeutet, dass die vier Stapelbytes, die an der Stelle beginnen, an der sich das Argument befinden soll, sich befinden0
, die folgenden vier Bytes jedoch einen beliebigen Inhalt haben. Damit wird der angezeigte Wert erstellt.quelle
Die Hauptursache für dieses Problem mit "unbestimmten Werten" liegt in der Umwandlung des Zeigers auf den
int
Wert, der an denprintf
Abschnitt mit den variablen Parametern an einen Zeiger beidouble
Typen ü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
double
Speicherpufferbereich größer als dieint
Größ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
printf
und 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_arg
quelle