El tipo double
no es matemáticamente equivalente a un número real. No puede serlo, porque el conjunto de los reales es infinito (más que infinito, podría decirse, pues sólo entre 0 y 1 ya hay infinitos reales), mientras que el tipo double
tiene sólo 64 bits y por tanto sólo puede representar 2^64 números diferentes en el mejor de los casos (en realidad menos porque ciertas combinaciones se reservan para representar conceptos especiales, como "infinito" o "NaN").
Esto conlleva que hay números que no pueden representarse con un double
. De hecho, hay un número infinito de ellos.
Los que sí pueden representarse de forma exacta son los que son potencia de dos (tales como 2^-1, 2^-2, etc.) o sumas de potencias de dos. Así, el 0.5, el 0.25 o el 0.75 serían representables de forma exacta en un double
. Pero números de aspecto tan inocente como 0.1 ó 0.2 no son potencia de dos, y por tanto no son representables en este formato. Lo que guarda el computador es el representable más próximo (dentro de un cierto número de decimales).
En definitiva, los números con los que has probado no son representables de forma exacta. El 4.44 por ejemplo, si lo "miras bien" verás que en realidad es almacenado como:
4.44000000000000039079850466805510222911834716796875
que es la suma de potencias de dos más próxima a 4.44 que puede encontrarse, dada la precisión del formato. Para obtener este dato he hecho printf("%60.f", 4.44)
para verlo con 60 decimales (aunque la cantidad de decimales a mostrar varía con el número, si el número es grande, se guardan menos decimales y si es menor que 1 se pueden guardar más, pues lo que es fijo es por así decir la cantidad de dígitos, dentro de los cuales se coloca después la coma, por eso lo de "coma flotante").
Esa es la explicación de que no funcione tu algoritmo con el 4.44
Más sorpresas
Pero es que ¡tampoco el 0.3, el 6.66 o el 6.66664 son suma de potencias de dos, por lo que tampoco son representables! El 6.66 de hecho se almacena como:
6.660000000000000142108547152020037174224853515625
entonces la verdadera pregunta sería ¿Por qué funcionó tu algoritmo con ciertos números, cuando lo esperable sería que no hubiera funcionado con prácticamente ninguno? Es difícil dar por azar con un número que sea suma de potencias de dos, pero rápidamente puedes ver cuándo no lo es, si el último decimal no es 5 (que sea 5 tampoco es garantía, por ejemplo 0.05 no es potencia de 2).
Para descubrir cómo es que el 6.66 te funcionó, puedes modificar esEntero(x)
para que imprima su argumento, en cada iteración:
bool esEntero(double x){esEntero(3.23) = false
int y = x;
printf("x=%.60f\n", x);
return !(y-x);
//Si y-x es 0, significa que el numero no ha cambiado al truncarse, por tanto es entero
}
Entonces, al ejecutar el programa para x=6.66
vemos que se detiene tras tres iteraciones, y que los valores de x
son:
x=6.660000000000000142108547152020037174224853515625000000000000
x=66.599999999999994315658113919198513031005859375000000000000000
x=666.000000000000000000000000000000000000000000000000000000000000
¡Ha sido casualidad! Ya que no todos los números son representables, el procesador matemático se ve obligado a, tras cada operación, redondear el resultado a la suma de potencias de dos más próxima. Esto es, en cada operación hay un error de redondeo. La primera multiplicación por 10, por ejemplo, en lugar de dar 66.6000000000000014210854715202003717422485351562500000000000
, que sería lo correto (pero no es representable) ha dado 66.599999999999994315658113919198513031005859375
, que es incorrecto pero representable. Al multiplicar a su vez por 10 este valor, debería haber dado 665.99999999999994315658113919198513031005859375, pero de nuevo no es representable y se ha cambiado por
666.0`, que es el representable más próximo, y que casualmente tiene todo 0 en la parte fracción y por eso tu algoritmo se detiene.
Esta afortunada casualidad no tiene por qué producirse con otros números, en los que el redondeo a un número cuya parte-fracción sea todo 0 puede ocurrir en cualquier otro momento posterior (o no ocurrir nunca, salvo porque se llegue a un número tan grande, a base de multiplicar por 10, que ya no haya sitio en el formato para la parte fracción).
Solución
Como te han comentado, la solución típica a estos problemas cuando se trabaja con float
o double
consiste en nunca comparar directamente una variable con otra, sino conformarse con decir que su diferencia sea inferior a un cierto epsilon que es la máxima precisión con la que nos limitaremos a trabajar.
Así, tu función debería ser cambiada a esta forma:
#define EPSILON 1e-10 // aprox 10 decimales
bool esEntero(double x){//Devuelve true si x es entero, false si no lo es. esEntero(3.23) = false
int y = x;
return !(x-y > EPSILON);
//Si y-x es muy pequeño, significa que el numero no ha cambiado al truncarse, por tanto es entero
}
Otro enfoque podría ser convertir el double
en una cadena (usando sprintf()
y después contar cuántos dígitos hay tras el punto, hasta encontrarnos con una secuencia de ceros de una longitud "suficiente" (habría que definir esto también, de forma más o menos arbitraria, pues ¿vas a usarlo con números como 4.440000002
?). Considero que este enfoque podría ser mejor, al evitar los sucesivos errores de redondeo en cada multiplicación por 10.
La moraleja es: un double
no es un real. Atento a las sorpresas.