10

Teniendo en cuenta que en C/C++ los literales de punto flotante sin un sufijo son por defecto de tipo double, entonces al asignar un literal de este tipo a un float se realiza un conversión implícita de double a float.

float n = 3.14;

if(n == 3.14f)
   puts("Igual");

Esto no imprime el mensaje en cambio si agregamos el sufijo f a 3.14 en la declaración de n para evitar la conversión si se imprime. La pregunta es, se produce perdida de precisión al llevarse a cabo la conversión?

cheroky
  • 551
  • 1
  • 3
  • 13

3 Answers3

7

Pregunta

se produce perdida de precisión al llevarse a cabo la conversión?

Si, se produce una pérdida de precisión.

Estrechamiento (Narrowing).

Los tipos de datos en coma flotante son:

  • float: Precisión simple. Habitualmente son un tipo de 32 bits de ancho que sigue el IEEE-754 de 32 bits.
  • double - Precisión doble. Habitualmente son un tipo de 64 bits de ancho que sigue el IEEE-754 de 64 bits.
  • long double - Precisión extendida. Habitualmente son un tipo de 80 bits en arquitecturas de 32 y 64 bits. No necesariamente sigue el IEEE-754.

Cada vez que pasas de un tipo de mayor precisión a uno de menos se produce un estrechamiento; cada vez que se produce un estrechamiento se pueden perder datos ¿cuándo sucede esto?

Conversión de tipos.

Según el estándar de C en la sección §6.3.1.5 Tipos en coma flotante reales (traducción y resaltado míos):

6.3.1.5 Tipos en coma flotante reales

  1. Cuando un float es promocionado a double o long double, o cuando un double es promocionado a long double, su valor no cambia.
  2. Cuando un double es degradado a float, un long double es degradado a double o float, o un valor representado en una mayor precisión y rango que la requerida por su tipo semántico (ver 6.3.1.8) es explícitamente convertido a ese tipo semántico, si el valor que se está convirtiendo se puede representar exáctamente en el nuevo tipo, no se cambiará. Si el valor que se está convirtiendo está en el rango de valores que pueden ser representados pero no puede ser representado con exactitud, el resultado será el número más próximo al valor tanto redondeando hacia arriba o hacia abajo, el redondeo es dependiente de implementación. Si el valor que se está convirtiendo está fuera del rango de valores que puede ser representado, el comportamiento es indefinido.

En tu caso, asignar el valor 3.14 a un float corresponde a un estrechamiento, pero como el valor 3.14 es representable por float con exactitud el valor no cambiará.

¿Qué está fallando?

Si el valor no ha cambiado ¿Por qué falla tu código?:

float n = 3.14;

if(n == 3.14f)
   puts("Igual"); // no se imprime!

Porque he mentido, el valor 3.14 NO es representable por float con exactitud. Existen números en coma flotante que no son exáctamente representables en binario, esto se debe a las propiedades de cada base1.

El valor 3,14 en binario no es exáctamente representable y con precisión doble su valor es aproximadamente 3,14000000000000012434497875802 pero al almacenarlo en un float has perdido algo de precisión, ¿cuánta exáctamente? dependerá de tu sistema...

Así que estarás comparando un número parecido al truncamiento del valor double 3,14000000000000012434497875802 contra un número parecido a 3.14f que en muchos casos no será el mismo número. Por ejemplo el literal 3,14 en float es aproximadamente 3,1400001049041748046875 entonces tu comparación sería, más o menos:

if(3.1400000000000001243449 == 3.1400001049041748046875) // El double ha sido truncado

Que evidentemente no cumple con la igualdad.

¡Es terrible! ¿qué puedo hacer?

Tal y como comenta eferion, debes evitar comparar números en coma flotante mediante la igualdad, debido a los errores de redondeo debes compararlos por la casi igualdad, una función así te podría ser de ayuda:

#include <stdbool.h>
#include <stdint.h> 

bool casi_iguales(float izquierda, float derecha)
{
     return fabs(izquierda – derecha) <= FLT_EPSILON;
}

bool casi_iguales(double izquierda, double derecha)
{
     return fabs(izquierda – derecha) <= DBL_EPSILON;
}

Los valores FLT_EPSILON y DBL_EPSILON son la diferencia entre 1 y el siguiente valor representable por float y double respectivamente; en otras palabras, son aproximadamente el menor valor representable por cada uno de los tipos, así que si la diferencia entre izquierda y derecha es menor o igual a este valor es que ambos valores son casi iguales.


1Por ejemplo, 1/3 en base 10 es un número periódico puro de valor 0,3333333... mientras que en base 12 es exáctamente 0.4. En decimal el valor 1/10 es exáctamente 0,1 pero en binario es un número periódico mixto de valor 0,00011001100110011...

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
  • 1
    Puede ayudar a complementar a cómo se representan los números en binario: [¿Por qué mis programas no pueden hacer cálculos aritméticos correctamente?](http://es.stackoverflow.com/q/197/127) – Mariano Oct 25 '16 at 08:25
  • @PaperBirdMaster Entonces es importante agregar el sufijo apropiado a cada literal, porque veo que no es cosa que se hace muy a menudo. – cheroky Oct 25 '16 at 13:24
  • @cheroky efectivamente. El sufijo adecuado *en mi opinión* es vital, no sólo con números en coma flotante (`.0`, `.0f`) si no también con números integrales (`0`, `0u`, `0l`, `0ul`, `0ll`, `0ull`) y cadenas (`"hola"`, `L"hola"`, `u8"hola"`, ...). Lamentablemente hay tipos que carecen de sufijo (`short`, `long double`, `char`) y debemos usarlos con conocimiento :) – PaperBirdMaster Oct 25 '16 at 13:28
2

Los números en coma flotante nunca jamás se han de comparar usando el operador de comparación.

El motivo es que dichos números poseen una precisión determinada, siendo el resto de los dígitos prácticamente aleatorio.

La forma correcta es realizar la comparación asumiendo un cierto margen de error:

float v1,v2;
//...
if (fabs(v1-v2)<1e-4)
  // Los números son iguales
else
  // Los números son distintos

Puedes jugar con la precisión para adaptarla a tus necesidades. El ejemplo es simplemente ilustrativo.

Un saludo

eferion
  • 49,291
  • 5
  • 30
  • 72
1

He aquí un ejemplo muy ilustrativo de lo que sucede. Y de verdad que merece la pena explicarlo.

Tú tienes dos áreas de memoria sin inicializar así:

Área A:

+--------------------------------------+
| qwerytuiopasdfhgjklzxcvbnm1234567890 |
+--------------------------------------+

Área B:

+--------------------------------------+
| qwerytuiopasdfhgjklzxcvbnm1234567890 |
+--------------------------------------+

Luego pones un float en A:

+----------------+
| 3.14           | // Nota como float es más pequeño.
+----------------+

Y un double en B:

+--------------------------------------+
| 3.14                                 |
+--------------------------------------+

Entonces al comparar A con B, obtienes la comparación de:

+--------------------------------------+
| 3.14                                 |
+--------------------------------------+

Con

+--------------------------------------+
| 3.14           gjklzxcvbnm1234567890 |
+--------------------------------------+

Siendo estos diferentes. Espero que te haya resultado didáctico.

Juan Manuel
  • 401
  • 2
  • 6