sintaxis que lenguaje hace función formato dev c++ c printf implicit-conversion undefined-behavior

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.

  1. Es UB por especificación y la compilación es aburrida - dijo nuf.

  2. double e int son de diferentes tamaños.

  3. double e int pueden pasar sus valores usando diferentes pilas (pila general vs. pila FPU ).

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

  1. implementación glibc del proyecto gnu de "printf" (vprintf))
  2. ejemplo de código de amplificación de printf
  3. ejemplo de código de amplificación de va_arg

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 un float será un double y cualquier cosa más pequeña que un int será un int .
  • Estás pasando un int donde la función espera un double . Su int es probablemente de 32 bits, su double 64 bits. Eso significa que los cuatro bytes de la pila que comienzan en el lugar donde se supone que se ubica el argumento son 0 , 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.