c++ - que - sintaxis de printf y scanf
¿Por qué printf("% f", 0); dar un comportamiento indefinido? (10)
¿Por qué el uso de un literal entero en lugar de un literal flotante causa este comportamiento?
Porque
printf()
no tiene parámetros tipados además de la
const char* formatstring
como la primera.
Utiliza puntos suspensivos de estilo c (
...
) para todo lo demás.
Simplemente decide cómo interpretar los valores pasados allí de acuerdo con los tipos de formato dados en la cadena de formato.
Tendría el mismo tipo de comportamiento indefinido que cuando intenta
int i = 0;
const double* pf = (const double*)(&i);
printf("%f/n",*pf); // dereferencing the pointer is UB
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 coma flotante en el que todos los bits son 0 sigue siendo un valor
float
válido con un valor de 0.
float
e
int
son del mismo tamaño en mi máquina (si eso es relevante).
¿Por qué el uso de un literal entero en lugar de un literal de coma flotante en
printf
causa este comportamiento?
PD: el mismo comportamiento se puede ver si uso
int i = 0;
printf("%f/n", i);
El formato
"%f"
requiere un argumento de tipo
double
.
Le estás dando un argumento de tipo
int
.
Es por eso que el comportamiento es indefinido.
El estándar no garantiza que all-bits-zero sea una representación válida de
0.0
(aunque a menudo lo sea), o de cualquier valor
double
, o que
int
y
double
sean del mismo tamaño (recuerde que es
double
, no
float
), o, incluso si son del mismo tamaño, que se pasan como argumentos a una función variadic de la misma manera.
Puede suceder que "funcione" en su sistema. Ese es el peor síntoma posible de un comportamiento indefinido, porque dificulta el diagnóstico del error.
N1570 7.21.6.1 párrafo 9:
... Si algún argumento no es el tipo correcto para la especificación de conversión correspondiente, el comportamiento no está definido.
Los argumentos de tipo
float
se promueven a
double
, por lo que
printf("%f/n",0.0f)
funciona.
Los argumentos de tipos enteros más estrechos que
int
se promueven a
int
o a
unsigned int
.
Estas reglas de promoción (especificadas por N1570 6.5.2.2 párrafo 6) no ayudan en el caso de
printf("%f/n", 0)
.
El uso de un especificador
printf()
coincidente
"%f"
y tipo
(int) 0
conduce a un comportamiento indefinido.
Si una especificación de conversión no es válida, el comportamiento es indefinido. C11dr §7.21.6.1 9
Causas candidatas de la UB.
-
Es UB por especificación y la compilación es aburrida - dijo nuf.
-
double
eint
son de diferentes tamaños. -
double
eint
pueden pasar sus valores usando diferentes pilas (pila general vs. pila FPU ). -
Un
double 0.0
podría no estar definido por un patrón de todos los bits cero. (raro)
En primer lugar, como se menciona en varias otras respuestas pero, en mi opinión, no se explica con suficiente claridad: funciona para proporcionar un número entero en la
mayoría de los
contextos donde una función de biblioteca toma un argumento
double
o
float
.
El compilador insertará automáticamente una conversión.
Por ejemplo,
sqrt(0)
está bien definido y se comportará exactamente como
sqrt((double)0)
, y lo mismo es cierto para cualquier otra expresión de tipo entero utilizada allí.
printf
es diferente.
Es diferente porque toma un número variable de argumentos.
Su prototipo de función es
extern int printf(const char *fmt, ...);
Por lo 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 es
int
, para seguir.
Por lo tanto, a diferencia de la mayoría de las funciones de la biblioteca, usted, el programador, debe 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 discrepancia de tipo, pero no comenzarán a insertar conversiones para lograr lo que quería decir, porque mejor su código debería romperse ahora, cuando notará , que años más tarde cuando se reconstruyó con un compilador menos útil).
Ahora, la otra mitad de la pregunta era: dado que (int) 0 y (float) 0.0 son, 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 "no es necesario que funcione, estás solo", pero déjame explicarte las dos razones más comunes por las que no funcionaría; eso probablemente te ayudará a entender por qué no es obligatorio.
Primero, por razones históricas, cuando pasa un
float
través de una lista de argumentos variables, se
promueve
al
double
, que, en la mayoría de los sistemas modernos, tiene
64
bits de ancho.
Entonces
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 que 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 0 a 4 vayan en los registros r0 a r4 si son enteros, pero f0 a f4 si son de punto flotante.
Entonces
printf("%f", 0)
busca en el registro f1 para ese cero, pero no está allí en absoluto.
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.
La causa principal de este problema de "valor indeterminado" se encuentra en el reparto del puntero en el valor
int
pasado a la sección de parámetros variables
printf
a un puntero en tipos
double
que lleva a cabo la macro
va_arg
.
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 área del búfer de memoria de
double
tamaño es mayor que el tamaño
int
.
Por lo tanto, cuando este puntero se desreferencia, se devuelve un valor indeterminado, o mejor, un "valor" que contiene en parte el valor pasado como parámetro a
printf
, y para la parte restante podría provenir de otra área de almacenamiento intermedio de pila o incluso un área de código (provocando una excepción de falla de memoria),
un desbordamiento de búfer real
.
Puede considerar estas porciones específicas de implementaciones de código ejemplificado 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..
....
La implementación real en vprintf (considerando gnu impl.) de la gestión de casos de código de parámetros de doble valor es:
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);
referencias
No estoy seguro de lo que es confuso.
Su cadena de formato espera un
double
;
proporcionas en cambio un
int
.
Si los dos tipos tienen el mismo ancho de bits es completamente irrelevante, excepto que puede ayudarlo a evitar obtener excepciones de violación de memoria de código roto como este.
Normalmente, cuando llama a una función que espera un
double
, pero proporciona un
int
, el compilador se convertirá automáticamente en un
double
para usted.
Eso no sucede con
printf
, porque los tipos de argumentos no están especificados en el prototipo de la función; el compilador no sabe que se debe aplicar una conversión.
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 de vararg estándar. Eso significa que unfloat
será undouble
y cualquier cosa más pequeña que unint
será unint
. -
Estás pasando un
int
donde la función espera undouble
. Suint
es probablemente de 32 bits, sudouble
64 bits. Eso significa que los cuatro bytes de la pila que comienzan en el lugar donde se supone que se ubica el argumento son0
, pero los siguientes cuatro bytes tienen contenido arbitrario. Eso es lo que se usa para construir el valor que se muestra.
"%f/n"
garantiza un resultado predecible solo cuando el segundo parámetro
printf()
tiene un tipo de
double
.
A continuación, un argumento adicional de funciones variadas está sujeto a la promoción de argumentos por defecto.
Los argumentos enteros se incluyen en la promoción de enteros, que nunca da como resultado valores tipados de punto flotante.
Y los parámetros
float
se promueven al
double
.
Para colmo: estándar permite que el segundo argumento sea o
float
o
double
y nada más.