Tipos de punto flotante: float, double y long double

Cuando trabajas con números que tienen decimales, no puedes usar tipos de datos enteros como int o long. Necesitas tipos de punto flotante. A diferencia de un entero, que almacena un valor exacto, el punto flotante almacena una aproximación basada en una notación científica binaria (signo, mantisa y exponente).

Para entender esto, piensa en cómo escribimos ciencia: es más fácil escribir $1.5 \times 10^3$ que $1500$, y es más fácil $1.5 \times 10^{-3}$ que $0.0015$. El punto flotante hace lo mismo en la memoria del ordenador para representar números muy grandes o muy pequeños de forma eficiente.

Existen tres niveles principales de precisión en C. El float es de precisión simple y es el más ligero; se usa mucho en gráficos 3D o procesamiento de señales donde la velocidad y el ahorro de memoria son más importantes que tener veinte decimales exactos. El double es de precisión doble y es el estándar por defecto en casi todo el software moderno; ofrece un equilibrio óptimo entre precisión y rendimiento. Finalmente, el long double es de precisión extendida; su tamaño y precisión dependen de tu procesador y compilador (puede ser de 80 o 128 bits), y se reserva para cálculos científicos donde el error de redondeo debe ser mínimo.

¿Por qué existen estos niveles y no uno solo? Porque la memoria y el tiempo de CPU son recursos finitos. Usar double para algo que solo requiere float desperdicia memoria y puede ralentizar el procesador al no poder usar instrucciones especializadas (como SIMD, que procesan varios datos a la vez).

Debes elegir el tipo según la escala de tus datos: usa float para grandes conjuntos de datos masivos (como texturas en un videojuego) y double para la lógica general de tu programa. Si intentas usar un float para cálculos astronómicos o financieros muy precisos, el error de redondeo se acumulará rápidamente y tus resultados serán basura. Lo más peligroso que puedes hacer es comparar dos números flotantes usando el operador de igualdad ==; debido a que los números se aproximan, dos valores que matemáticamente son iguales pueden no serlo bit a bit en la memoria.

#include <stdio.h>
#include <float.h>

int main(void) {
    /* 
     * Los sufijos son vitales: 
     * Sin sufijo, el compilador asume que el literal es 'double'.
     * 'f' o 'F' indica 'float'.
     * 'L' indica 'long double'.
     */
    float f = 0.1234567890123456789f;
    double d = 0.1234567890123456789;
    long double ld = 0.1234567890123456789L;

    // Mostramos la realidad de lo que el ordenador realmente guarda
    printf("--- Precisión real ---\n");
    printf("Float:    %.20f\n", f);
    printf("Double:   %.20f\n", d);
    printf("Long:     %.20f\n\n", ld);

    // El 'Epsilon' es la distancia mínima entre 1.0 y el siguiente número representable.
    // Nos dice qué tan "fino" es el escalonamiento de cada tipo.
    printf("--- Epsilon (precisión mínima) ---\n");
    printf("FLT_EPSILON: %e\n", FLT_EPSILON);
    printf("DBL_EPSILON: %e\n\n", DBL_EPSILON);

    // Demostración del error de redondeo en sumas
    printf("--- La trampa de la igualdad ---\n");
    float a = 0.1f;
    float b = 0.2f;
    float suma = a + b;
    float objetivo = 0.3f;

    if (suma == objetivo) {
        printf("La suma de 0.1 + 0.2 es exactamente 0.3\n");
    } else {
        printf("Error: 0.1 + 0.2 no es igual a 0.3 en float\n");
        printf("El valor real de la suma es: %.20f\n", suma);
    }

    return 0;
}

Análisis del código

En el ejemplo, cuando declaramos f, d y ld, el compilador interpreta los literales de forma distinta gracias a los sufijos. Fíjate en la salida de printf: el float empieza a mostrar valores erróneos (ruido digital) mucho antes que el double. Esto ocurre porque el float solo tiene unos 7 dígitos de precisión garantizada (FLT_DIG), mientras que el double ofrece unos 15-17.

Al imprimir con %.20f, estamos forzando a la función printf a mostrar 20 decimales. Esto revela la naturaleza aproximada de la representación. Lo que ves después del séptimo decimal en f no es el número real, es el “residuo” de cómo se aproximó el número decimal a la base binaria.

La parte más crítica es la comparación suma == objetivo. Matemáticamente, $0.1 + 0.2 = 0.3$, pero para un float, la suma de esas dos aproximaciones binarias da un resultado ligeramente distinto al de la aproximación de $0.3$ por sí sola. El error ocurre porque el redondeo de cada operación no siempre compensa el otro.

El uso de FLT_EPSILON y DBL_EPSILON (definidos en <float.h>) es fundamental para entender la escala de error. Si necesitas comparar si dos números son “iguales”, la práctica profesional no es usar ==, sino comprobar si la diferencia entre ellos es menor que un margen de error aceptable (un valor basado en el epsilon).

El error frecuente

El error más común es intentar realizar comparaciones de igualdad exacta con tipos de punto flotante:

// ERROR: Comparación lógica incorrecta
if (valor_calculado == 0.3) { 
    // Este bloque raramente se ejecutará como esperas
}

Si valor_calculado es el resultado de una operación matemática, es casi seguro que tendrá un error de redondeo infinitesimal. Aunque este error no causará un fallo de segmentación ni será detectado por herramientas como Valgrind (que busca errores de memoria) o AddressSanitizer, producirá un error de lógica que puede ser un dolor de cabeza en sistemas de control o simulaciones. Para evitarlo, compara siempre contra un margen de tolerancia: if (fabs(valor_calculado - 0.3) < 0.00001).

14

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio