significado - que es un puntero en c++
¿Por qué lanzar a un puntero y luego a la desreferencia? (5)
Estaba repasando este ejemplo que tiene una función que da salida a un patrón de bits hexadecimales para representar un flotante arbitrario.
void ExamineFloat(float fValue)
{
printf("%08lx/n", *(unsigned long *)&fValue);
}
¿Por qué tomar la dirección de fValue, convertir en puntero largo sin signo y luego desreferencia? ¿No es todo ese trabajo simplemente equivalente a un lanzamiento directo a unsigned de largo?
printf("%08lx/n", (unsigned long)fValue);
Lo intenté y la respuesta no es la misma, tan confusa.
Los valores de coma flotante tienen representaciones de memoria: por ejemplo, los bytes pueden representar un valor de coma flotante usando IEEE 754 .
La primera expresión *(unsigned long *)&fValue
interpretará estos bytes como si fuera la representación de un valor unsigned long
. De hecho, en el estándar C da como resultado un comportamiento indefinido (de acuerdo con la llamada "regla de alias estricto"). En la práctica, hay problemas tales como endianness que deben tenerse en cuenta.
La segunda expresión (unsigned long)fValue
C estándar. Tiene un significado preciso:
C11 (n1570), § 6.3.1.4 Flotante real y entero
Cuando un valor finito del tipo flotante real se convierte a un tipo entero distinto de
_Bool
, la parte fraccionaria se descarta (es decir, el valor se trunca hacia cero). Si el valor de la parte integral no puede representarse mediante el tipo entero, el comportamiento no está definido.
Typecasting en C realiza tanto una conversión de tipo como una conversión de valor. El punto flotante → conversión larga sin signo trunca la porción fraccionaria del número de coma flotante y restringe el valor al rango posible de un largo sin signo. La conversión de un tipo de puntero a otro no requiere ningún cambio en el valor, por lo que usar el puntero en el tipo de subtítulo es una forma de mantener la misma representación en memoria mientras se cambia el tipo asociado con esa representación.
En este caso, es una forma de poder generar la representación binaria del valor del punto flotante.
*(unsigned long *)&fValue
no es equivalente a un lanzamiento directo a un unsigned long
.
La conversión a (unsigned long)fValue
convierte el valor de fValue
en unsigned long
, utilizando las reglas normales para la conversión de un valor float
a un valor unsigned long
. La representación de ese valor en un unsigned long
(por ejemplo, en términos de los bits) puede ser bastante diferente de cómo se representa el mismo valor en un float
.
La conversión *(unsigned long *)&fValue
formalmente tiene un comportamiento indefinido. Interpreta la memoria ocupada por fValue
como si fuera un unsigned long
. Prácticamente (es decir, esto es lo que sucede a menudo, aunque el comportamiento no está definido), esto a menudo arrojará un valor bastante diferente de fValue
.
(unsigned long)fValue
Esto convierte el valor float
en un valor unsigned long
, de acuerdo con las "conversiones aritméticas habituales".
*(unsigned long *)&fValue
La intención aquí es tomar la dirección en la que se almacena fValue
, simular que no hay un float
sino un unsigned long
en esta dirección, y luego leer el unsigned long
. El objetivo es examinar el patrón de bits que se utiliza para almacenar el float
en la memoria.
Como se muestra, esto causa un comportamiento indefinido.
Motivo: no puede acceder a un objeto a través de un puntero a un tipo que no sea "compatible" con el tipo del objeto. Los tipos "compatibles" son por ejemplo char
( unsigned
) y cualquier otro tipo, o estructuras que comparten los mismos miembros iniciales (hablando de C aquí). Ver §6.5 / 7 N1570 para la lista detallada (C11) ( Tenga en cuenta que mi uso de "compatible" es diferente, más amplio, que en el texto al que se hace referencia ) .
Solución: Transmitir a unsigned char *
, acceder a los bytes individuales del objeto y armar un unsigned long
de ellos:
unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
pattern |= *access;
pattern <<= CHAR_BIT;
++access;
}
Tenga en cuenta que (como señaló @CodesInChaos), lo anterior trata el valor del punto flotante como almacenado con su byte más significativo primero ("big endian"). Si su sistema utiliza un orden de bytes diferente para los valores de coma flotante, deberá ajustarlo (o reordenar los bytes de arriba unsigned long
, lo que sea más práctico para usted).
Como otros ya han notado, el hecho de convertir un puntero a un tipo que no sea de tipo char en un puntero a un tipo distinto de char y luego eliminar la referencia es un comportamiento indefinido.
Ese printf("%08lx/n", *(unsigned long *)&fValue)
invoca un comportamiento indefinido no necesariamente significa que ejecutar un programa que intente realizar una parodia de este tipo resultará en el borrado del disco duro o hará que los demonios nasales salgan de la nariz (las dos características del comportamiento indefinido). En una computadora en la que sizeof(unsigned long)==sizeof(float)
y en la que ambos tipos tienen los mismos requisitos de alineación, esa printf
hará casi con seguridad lo que uno espera que haga, que es imprimir la representación hexadecimal de la flotación valor de punto en cuestión.
Esto no debería ser sorprendente. El estándar C invita abiertamente a las implementaciones para extender el lenguaje. Muchas de estas extensiones se encuentran en áreas que, estrictamente hablando, son un comportamiento indefinido. Por ejemplo, el dlsym de la función POSIX devuelve un void*
, pero esta función generalmente se usa para encontrar la dirección de una función en lugar de una variable global. Esto significa que el puntero void devuelto por dlsym
necesita ser lanzado a un puntero de función y luego desreferenciado para llamar a la función. Obviamente, este comportamiento no está definido, pero funciona en cualquier plataforma compatible con POSIX. Esto no funcionará en una máquina de arquitectura de Harvard en la que los punteros a las funciones tengan diferentes tamaños que los punteros a los datos.
Del mismo modo, convertir un puntero a un float
en un puntero a un entero sin signo y luego eliminar la referenciación funciona en casi cualquier computadora con casi cualquier compilador en el que los requisitos de tamaño y alineación de ese entero sin signo sean los mismos que los de un float
.
Dicho esto, usar unsigned long
podría llevarte a problemas. En mi computadora, una unsigned long
tiene 64 bits de longitud y tiene requisitos de alineación de 64 bits. Esto no es compatible con un flotador. Sería mejor usar uint32_t
- en mi computadora, eso es.
El truco sindical es una forma de evitar este desastre:
typedef struct {
float fval;
uint32_t ival;
} float_uint32_t;
Asignar a un float_uint32_t.fval
y acceder desde un `` float_uint32_t.ival` solía ser un comportamiento indefinido. Ese ya no es el caso en C. Ningún compilador que conozco golpea a los demonios nasales para el hack de la unión. Esto no fue UB en C ++. Fue ilegal. Hasta C ++ 11, un compilador C ++ compatible tenía que quejarse para cumplir.
Cualquier forma aún mejor en este lío es usar el formato %a
, que ha sido parte del estándar C desde 1999:
printf ("%a/n", fValue);
Esto es simple, fácil, portátil y no hay posibilidad de comportamiento indefinido. Esto imprime la representación hexadecimal / binaria del valor de coma flotante de doble precisión en cuestión. Como printf
es una función arcaica, todos los argumentos de float
se convierten en double
antes de la llamada a printf
. Esta conversión debe ser exacta según la versión 1999 del estándar C. Uno puede recoger ese valor exacto a través de una llamada a scanf
o sus hermanas.