Números de ponto flutuante são essenciais para representar valores reais com casas decimais em C. Diferente dos inteiros, eles seguem o padrão IEEE 754 e possuem limitações de precisão que podem causar erros sutis. Nesta aula, exploraremos os tipos float e double, como escrever constantes em notação científica, e as armadilhas ao comparar números de ponto flutuante.

Entender essas nuances é fundamental para escrever programas confiáveis, especialmente em áreas como simulações numéricas, processamento de sinais e finanças.

float vs double

Em C, os principais tipos de ponto flutuante são float (precisão simples) e double (precisão dupla). O tipo float ocupa 4 bytes (32 bits) e oferece cerca de 7 dígitos decimais de precisão. Já o double ocupa 8 bytes (64 bits) e oferece cerca de 15 dígitos decimais. Por padrão, constantes com ponto decimal (como 3.14) são do tipo double; para forçar float, use o sufixo f (ex.: 3.14f).

A escolha entre float e double depende da necessidade de precisão e do uso de memória. Em aplicações gráficas ou embarcadas com recursos limitados, float pode ser suficiente. Em cálculos científicos ou financeiros, double é preferível para evitar erros de arredondamento.

#include <stdio.h>

int main() {
    float f = 3.141592653589793f;
    double d = 3.141592653589793;
    printf("float: %.15f\n", f);
    printf("double: %.15f\n", d);
    return 0;
}

Precisão e armadilhas

A precisão limitada dos números de ponto flutuante pode levar a erros de arredondamento. Por exemplo, o valor 0.1 não pode ser representado exatamente em binário, resultando em pequenos erros. Esses erros podem se acumular em loops ou comparações.

Outra armadilha é a perda de precisão ao subtrair números muito próximos (cancelação catastrófica). Para evitar problemas, use double sempre que possível, evite subtrações de números quase iguais e nunca compare igualdade exata com ==.

#include <stdio.h>

int main() {
    float a = 0.1f;
    float b = 0.2f;
    float c = 0.3f;
    if (a + b == c) {
        printf("Igual\n");
    } else {
        printf("Diferente: %.20f\n", a + b - c);
    }
    return 0;
}

Notação científica

Constantes de ponto flutuante podem ser escritas em notação científica usando e ou E para o expoente. Por exemplo, 1.23e-4 equivale a 0.000123. O expoente deve ser inteiro, e a base é 10.

Essa notação é útil para representar números muito grandes ou muito pequenos de forma legível. Para float, adicione o sufixo f (ex.: 1.23e-4f).

#include <stdio.h>

int main() {
    double grande = 6.022e23;   // Número de Avogadro
    float pequeno = 1.6e-19f;   // Carga do elétron
    printf("Grande: %e\n", grande);
    printf("Pequeno: %e\n", pequeno);
    return 0;
}

Comparações

Comparar números de ponto flutuante com == é arriscado devido a erros de arredondamento. A prática recomendada é usar uma tolerância (epsilon) que define uma margem de erro aceitável. O epsilon típico para float é 1e-6 e para double 1e-9, mas pode variar conforme a aplicação.

A função fabs da biblioteca <math.h> retorna o valor absoluto de um double. Para float, use fabsf. Exemplo: if (fabs(a - b) < 1e-9).

#include <stdio.h>
#include <math.h>

int main() {
    double a = 0.1 * 3;
    double b = 0.3;
    double epsilon = 1e-9;
    if (fabs(a - b) < epsilon) {
        printf("a e b são iguais dentro da tolerância\n");
    } else {
        printf("a e b são diferentes\n");
    }
    return 0;
}

Boas práticas

  • Prefira double a float em cálculos que exigem precisão.
  • Use sufixos f para constantes float.
  • Evite comparar igualdade exata; use tolerância.
  • Cuidado com acumulação de erros em loops.
  • Utilize %e para exibir notação científica.

Referências

Exercícios

  1. Escreva um programa que declare uma variável float com valor 0.1 e uma double com valor 0.1, e imprima ambos com 20 casas decimais. Explique a diferença.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        float f = 0.1f;
        double d = 0.1;
        printf("float: %.20f\n", f);
        printf("double: %.20f\n", d);
        return 0;
    }
    O float mostra imprecisão a partir da 7ª casa decimal, enquanto o double mantém precisão por mais dígitos.
  2. Calcule o valor de 0.1 + 0.2 usando float e compare com 0.3 usando uma tolerância de 1e-6. Imprima se são iguais ou diferentes.

    ✓ Resposta:
    #include <stdio.h>
    #include <math.h>
    
    int main() {
        float a = 0.1f + 0.2f;
        float b = 0.3f;
        float eps = 1e-6f;
        if (fabsf(a - b) < eps)
            printf("Iguais\n");
        else
            printf("Diferentes\n");
        return 0;
    }
    Com tolerância, eles são considerados iguais.
  3. Escreva uma constante em notação científica para representar a velocidade da luz (3e8 m/s) como double e imprima-a em notação científica e decimal.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        double c = 3e8;
        printf("Notação científica: %e\n", c);
        printf("Decimal: %f\n", c);
        return 0;
    }
  4. Crie uma função float_equals que recebe dois float e um epsilon, e retorna 1 se forem iguais dentro da tolerância, 0 caso contrário.

    ✓ Resposta:
    #include <math.h>
    
    int float_equals(float a, float b, float eps) {
        return fabsf(a - b) < eps;
    }
  5. Explique por que o código a seguir pode imprimir "Diferente" e corrija-o usando uma comparação segura.
    #include <stdio.h>
    int main() {
        double a = 0.1 * 3;
        double b = 0.3;
        if (a == b) printf("Igual\n");
        else printf("Diferente\n");
        return 0;
    }

    ✓ Resposta:O erro de arredondamento faz com que a seja ligeiramente diferente de b. Correção: use tolerância.
    #include <stdio.h>
    #include <math.h>
    int main() {
        double a = 0.1 * 3;
        double b = 0.3;
        double eps = 1e-9;
        if (fabs(a - b) < eps) printf("Igual\n");
        else printf("Diferente\n");
        return 0;
    }