La declaración
printf("%f\n",0.0f);
imprime 0.
Sin embargo, la declaración
printf("%f\n",0);
imprime valores aleatorios.
Me doy cuenta de que estoy exhibiendo algún tipo de comportamiento indefinido, pero no puedo entender por qué específicamente.
Un valor de punto flotante en el que todos los bits son 0 sigue siendo válido float
con un valor de 0
float
y int
son del mismo tamaño en mi máquina (si eso es incluso relevante).
¿Por qué el uso de un literal entero en lugar de un literal de punto flotante printf
causa este comportamiento?
PD: se puede ver el mismo comportamiento si uso
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
fuente
fuente
printf
está esperando undouble
, y le está dando unint
.float
yint
puede ser del mismo tamaño en su máquina, pero en0.0f
realidad se convierte en adouble
cuando se inserta en una lista de argumentos variadic (yprintf
espera eso). En resumen, no está cumpliendo con su parte del trato enprintf
función de los especificadores que utiliza y los argumentos que proporciona.(uint64_t)0
lugar de0
y ver si todavía obtiene un comportamiento aleatorio (asumiendodouble
yuint64_t
tiene el mismo tamaño y alineación). Es probable que la salida siga siendo aleatoria en algunas plataformas (por ejemplo, x86_64) debido a que se pasan diferentes tipos en diferentes registros.Respuestas:
El
"%f"
formato requiere un argumento de tipodouble
. Le está dando un argumento de tipoint
. Por eso el comportamiento no está definido.La norma no garantiza que todos los bits de cero es una representación válida de
0.0
(aunque a menudo es), o de cualquierdouble
valor, o queint
ydouble
son del mismo tamaño (recuerda que esdouble
, nofloat
), o, incluso si son de la misma size, que se pasan como argumentos a una función variable de la misma manera.Puede suceder que "funcione" en su sistema. Ese es el peor síntoma posible de comportamiento indefinido, porque dificulta el diagnóstico del error.
N1570 7.21.6.1 párrafo 9:
float
Se promueven argumentos de tipodouble
, razón por la cualprintf("%f\n",0.0f)
funciona. Argumentos de tipos enteros más estrechos que los queint
se promuevenint
ao aunsigned int
. Estas reglas de promoción (especificadas por N1570 6.5.2.2 párrafo 6) no ayudan en el caso deprintf("%f\n", 0)
.Tenga en cuenta que si pasa una constante
0
a una función no variada que espera undouble
argumento, el comportamiento está bien definido, asumiendo que el prototipo de la función es visible. Por ejemplo,sqrt(0)
(after#include <math.h>
) convierte implícitamente el argumento0
deint
adouble
, porque el compilador puede ver en la declaraciónsqrt
que espera undouble
argumento. No tiene tal información paraprintf
. Las funciones variables comoprintf
son especiales y requieren más cuidado al escribirles las llamadas.fuente
double
, nofloat
por lo que espera supuesto el ancho de la OP no puede (probablemente no lo hace). En segundo lugar, la suposición de que el cero entero y el cero en coma flotante tienen el mismo patrón de bits tampoco se cumple. Buen trabajofloat
se asciende adouble
sin explicar por qué, pero ese no era el punto principal.printf
, aunque gcc, por ejemplo, tiene algunos para que pueda diagnosticar errores ( si la cadena de formato es literal). El compilador puede ver la declaración deprintf
from<stdio.h>
, que le dice que el primer parámetro es aconst char*
y el resto se indica con, ...
. No,%f
es paradouble
(yfloat
se asciende adouble
) y%lf
es paralong double
. El estándar C no dice nada sobre una pila. Especifica el comportamiento deprintf
solo cuando se llama correctamente.float
pasado aprintf
se promueve adouble
; no hay nada mágico en eso, es solo una regla de lenguaje para llamar a funciones variadas.printf
él mismo sabe, a través de la cadena de formato, lo que la persona que llama afirmó pasarle; si esa afirmación es incorrecta, el comportamiento no está definido.l
modificador de longitud "no tiene efecto en un siguientea
,A
,e
,E
,f
,F
,g
, oG
especificador de conversión", el modificador de longitud para unalong double
conversión esL
. (@ robertbristow-johnson también podría estar interesado)En primer lugar, como tocado en varias otras respuestas, pero no, en mi opinión, explicado con suficiente claridad: Se hace el trabajo para proporcionar un número entero en la mayoría de los contextos en los que una función de biblioteca toma una
double
ofloat
argumento. El compilador insertará automáticamente una conversión. Por ejemplo,sqrt(0)
está bien definido y se comportará exactamente comosqrt((double)0)
, y lo mismo ocurre con cualquier otra expresión de tipo entero que se use allí.printf
es diferente. Es diferente porque requiere un número variable de argumentos. Su función prototipo esextern int printf(const char *fmt, ...);
Por tanto, cuando escribes
printf(message, 0);
el compilador no tiene ninguna información sobre qué tipo
printf
espera que sea ese segundo argumento. Solo tiene el tipo de expresión de argumento, que esint
, para pasar. Por lo tanto, a diferencia de la mayoría de las funciones de la biblioteca, depende de usted, el programador, asegurarse de que la lista de argumentos coincida con las expectativas de la cadena de formato.(Los compiladores modernos pueden buscar en una cadena de formato y decirle que tiene una falta de coincidencia de tipos, pero no van a comenzar a insertar conversiones para lograr lo que quería decir, porque mejor su código debería romperse ahora, cuando lo notará , que años después cuando se reconstruyó con un compilador menos útil).
Ahora, la otra mitad de la pregunta era: dado que (int) 0 y (float) 0.0 están, en la mayoría de los sistemas modernos, ambos representados como 32 bits, todos los cuales son cero, ¿por qué no funciona de todos modos, por accidente? El estándar C simplemente dice "esto no es necesario para funcionar, estás solo", pero déjame explicarte las dos razones más comunes por las que no funcionaría; eso probablemente le ayudará a comprender por qué no es necesario.
Primero, por razones históricas, cuando se pasa a
float
través de una lista de argumentos variables, se asciende a ladouble
que, en la mayoría de los sistemas modernos, tiene 64 bits de ancho. Por lo tanto,printf("%f", 0)
pasa solo 32 bits cero a un destinatario que espera 64 de ellos.La segunda razón, igualmente significativa, es que los argumentos de la función de punto flotante pueden pasarse en un lugar diferente al de los argumentos enteros. Por ejemplo, la mayoría de las CPU tienen archivos de registro separados para enteros y valores de punto flotante, por lo que podría ser una regla que los argumentos del 0 al 4 vayan en los registros r0 a r4 si son enteros, pero de f0 a f4 si son de punto flotante. Entonces
printf("%f", 0)
busca en el registro f1 ese cero, pero no está allí en absoluto.fuente
()
.AL
. (Sí, esto significa que la implementación deva_arg
es mucho más complicada de lo que solía ser.)ret n
instrucción del 8086, donden
era un número entero codificado, que por lo tanto no era aplicable para funciones variadas. Sin embargo, no sé si algún compilador de C realmente lo aprovechó (los compiladores que no son de C ciertamente lo hicieron).Por lo general, cuando llama a una función que espera un
double
, pero proporciona unint
, el compilador se convertirá automáticamente en adouble
por usted. Eso no sucede conprintf
, porque los tipos de argumentos no se especifican en el prototipo de la función; el compilador no sabe que se debe aplicar una conversión.fuente
printf()
en particular, está diseñado para que sus argumentos puedan ser de cualquier tipo. Debe saber qué tipo espera cada elemento en la cadena de formato y debe proporcionarlo correctamente.Porque
printf()
no tiene parámetros escritos ademásconst char* formatstring
del primero. Utiliza una elipsis de estilo c (...
) para el resto.Simplemente decide cómo interpretar los valores pasados allí de acuerdo con los tipos de formato dados en la cadena de formato.
Tendrías el mismo tipo de comportamiento indefinido que cuando intentas
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB
fuente
printf
podrían funcionar de esa manera (excepto que los elementos pasados son valores, no direcciones). El estándar C no especifica cómo funcionanprintf
y otras funciones variadas, solo especifica su comportamiento. En particular, no se mencionan los marcos de pila.printf
tiene un parámetro escrito, la cadena de formato, que es de tipoconst char*
. Por cierto, la pregunta está etiquetada como C y C ++, y C es realmente más relevante; Probablemente no lo hubiera usadoreinterpret_cast
como ejemplo.El uso de un
printf()
especificador"%f"
y un tipo(int) 0
que no coinciden conduce a un comportamiento indefinido.Causas candidatas de UB.
Es UB por especificación y la compilación es insoportable '', dijo Nuf.
double
yint
son de diferentes tamaños.double
yint
pueden pasar sus valores usando diferentes pilas (general frente a pila FPU ).Es
double 0.0
posible que A no esté definido por un patrón de bits completamente cero. (raro)fuente
Esta es una de esas grandes oportunidades para aprender de las advertencias de su compilador.
$ 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); ^
o
$ 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.
Entonces,
printf
está produciendo un comportamiento indefinido porque le está pasando un tipo de argumento incompatible.fuente
No estoy seguro de qué es confuso.
Su cadena de formato espera un
double
; en su lugar, proporciona unint
.Si los dos tipos tienen el mismo ancho de bits es completamente irrelevante, excepto que puede ayudarlo a evitar las excepciones de violación de la memoria por un código roto como este.
fuente
int
sería aceptable aquí.int
calificaría como un patrón flotante válido? El complemento a dos y las diversas codificaciones de punto flotante no tienen casi nada en común.0
a un argumento escritodouble
hará lo correcto. No es obvio para un principiante que el compilador no haga la misma conversión para losprintf
espacios de argumentos abordados por%[efg]
."%f\n"
garantiza un resultado predecible solo cuando el segundoprintf()
parámetro tiene el tipo dedouble
. A continuación, los argumentos adicionales de las funciones variadas están sujetos a la promoción de argumentos predeterminados. Los argumentos enteros se incluyen en la promoción de enteros, lo que nunca da como resultado valores con tipo de punto flotante. Y losfloat
parámetros se promueven adouble
.Para colmo: estándar permite que el segundo argumento sea o
float
odouble
y nada más.fuente
Por qué es formalmente UB ahora se ha discutido en varias respuestas.
La razón por la que obtiene específicamente este comportamiento depende de la plataforma, pero probablemente sea la siguiente:
printf
espera sus argumentos de acuerdo con la propagación estándar de vararg. Eso significa que unfloat
será undouble
y cualquier cosa más pequeña que unint
será unint
.int
donde la función espera undouble
. Suint
es probablemente de 32 bits, eldouble
64 bits. Eso significa que los cuatro bytes de la pila que comienzan en el lugar donde se supone que debe estar el argumento están0
, pero los siguientes cuatro bytes tienen contenido arbitrario. Eso es lo que se utiliza para construir el valor que se muestra.fuente
La causa principal de este problema de "valor indeterminado" se encuentra en la conversión del puntero en el
int
valor pasado a laprintf
sección de parámetros de variable a un puntero en losdouble
tipos queva_arg
realiza la macro.Esto provoca una referencia a un área de memoria que no se inicializó completamente con el valor pasado como parámetro a printf, porque el
double
tamaño del área de la memoria intermedia de memoria es mayor que elint
tamaño.Por lo tanto, cuando este puntero es desreferenciado, se devuelve un valor indeterminado, o mejor un "valor" que contiene en parte el valor pasado como parámetro
printf
, y la parte restante podría provenir de otra área de búfer de pila o incluso un área de código ( levantando una excepción de falla de memoria), un desbordamiento real del búfer .Puede considerar estas porciones específicas de implementaciones de código semplificado de "printf" y "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);
fuente