Nesta aula, vamos explorar os tipos de ponto flutuante em C, fundamentais para representar números reais. Entenderemos as diferenças entre float e double, os problemas de precisão que podem surgir, como usar notação científica e, principalmente, como comparar números de ponto flutuante de forma segura. Esses conceitos são essenciais para evitar bugs sutis em cálculos numéricos.

Os números de ponto flutuante seguem o padrão IEEE 754, que define a representação binária e as operações. Embora poderosos, eles têm limitações que todo programador deve conhecer para escrever código robusto.

float vs double

Em C, os principais tipos de ponto flutuante são float (32 bits) e double (64 bits). O float ocupa menos memória, mas oferece menor precisão (cerca de 7 dígitos decimais significativos). Já o double oferece aproximadamente 15 dígitos decimais significativos, sendo a escolha padrão para cálculos que exigem maior exatidão.

O tipo long double também existe, com precisão ainda maior (geralmente 80 ou 128 bits), mas seu tamanho exato depende da implementação. Na prática, double é suficiente para a maioria das aplicações científicas e de engenharia.

#include <stdio.h>

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

No exemplo acima, a saída mostra que float tem menos dígitos precisos. Ao declarar literais, use sufixo f para float (ex.: 3.14f) e nenhum sufixo para double.

Precisão e armadilhas

Números de ponto flutuante não podem representar todos os números reais com exatidão. Por exemplo, 0.1 em binário é uma dízima periódica, então seu valor armazenado é aproximado. Isso leva a erros de arredondamento que podem se acumular em operações sucessivas.

Outra armadilha comum é a perda de precisão ao somar números de magnitudes muito diferentes. Por exemplo, somar um número muito pequeno a um muito grande pode simplesmente ignorar o pequeno. Além disso, operações como subtração de números próximos podem causar cancelamento catastrófico.

#include <stdio.h>

int main() {
    double a = 1.0e20;
    double b = -1.0e20;
    double c = 1.0;
    double result = (a + b) + c;  // (1e20 - 1e20) + 1 = 0 + 1 = 1
    printf("Resultado: %f\n", result);
    
    // Outro exemplo: acumulando erro
    double sum = 0.0;
    for (int i = 0; i < 1000000; i++) {
        sum += 0.1;
    }
    printf("Soma esperada: 100000.0, obtida: %f\n", sum);
    return 0;
}

Para minimizar esses problemas, evite somar números de magnitudes muito diferentes, use algoritmos numericamente estáveis e considere bibliotecas de precisão arbitrária quando necessário.

Notação científica

Em C, podemos escrever literais de ponto flutuante em notação científica usando o caractere e ou E. Por exemplo, 1.5e-3 representa 1.5 × 10⁻³ = 0.0015. Isso é útil para representar números muito grandes ou muito pequenos de forma legível.

A notação científica também aparece na saída com o especificador %e ou %E. O formato %g escolhe automaticamente entre notação decimal e científica.

#include <stdio.h>

int main() {
    double avogadro = 6.022e23;   // 6.022 × 10^23
    double proton_mass = 1.672e-27; // 1.672 × 10^-27 kg
    printf("Número de Avogadro: %e\n", avogadro);
    printf("Massa do próton: %e\n", proton_mass);
    printf("Usando %%g: %g\n", 0.00012345);
    return 0;
}

Lembre-se de que o literal 6.022e23 é do tipo double. Para float, adicione o sufixo f, como em 6.022e23f.

Comparações

Comparar números de ponto flutuante com == é perigoso devido a erros de arredondamento. Dois valores que deveriam ser iguais podem diferir ligeiramente. A abordagem correta é verificar se a diferença absoluta é menor que uma tolerância (épsilon).

O valor da tolerância depende do contexto. Para comparações com zero, use um épsilon pequeno como 1e-9 para double. Para números grandes, uma tolerância relativa pode ser mais adequada.

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

int main() {
    double a = 0.1 * 10;
    double b = 1.0;
    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");
    }
    
    // Comparação relativa para números grandes
    double x = 1e20;
    double y = x + 1.0;
    double rel_epsilon = 1e-6;
    if (fabs(x - y) / fmax(fabs(x), fabs(y)) < rel_epsilon) {
        printf("x e y são aproximadamente iguais\n");
    }
    return 0;
}

Nunca use == para comparar floats/doubles a menos que você tenha absoluta certeza de que os valores são exatos (ex.: atribuições diretas de constantes). Prefira sempre uma comparação com tolerância.

Referências

Exercícios

  1. Escreva um programa que declare uma variável float com valor 1.0/3.0 e uma variável double com valor 1.0/3.0, e imprima ambas com 20 casas decimais. Explique a diferença observada.

    ✓ Resposta:
    #include <stdio.h>
    int main() {
        float f = 1.0f/3.0f;
        double d = 1.0/3.0;
        printf("float:  %.20f\n", f);
        printf("double: %.20f\n", d);
        return 0;
    }
    

    O float mostra apenas cerca de 7 dígitos significativos, enquanto o double mostra cerca de 15. A partir daí, os dígitos são lixo.

  2. Por que o código a seguir pode imprimir “Diferentes”? Corrija-o usando uma comparação com tolerância.

    double a = 0.1 + 0.2;
    double b = 0.3;
    if (a == b) printf("Iguais\n");
    else printf("Diferentes\n");
    

    ✓ Resposta:

    0.1+0.2 resulta em 0.30000000000000004, que é diferente de 0.3. Correção:

    #include <stdio.h>
    #include <math.h>
    int main() {
        double a = 0.1 + 0.2;
        double b = 0.3;
        double epsilon = 1e-9;
        if (fabs(a - b) < epsilon) printf("Iguais\n");
        else printf("Diferentes\n");
        return 0;
    }
    
  3. Escreva um programa que calcule a soma de 0.1 repetido 10 vezes e compare com 1.0 usando uma tolerância de 1e-9. Mostre o resultado da soma e se são considerados iguais.

    ✓ Resposta:
    #include <stdio.h>
    #include <math.h>
    int main() {
        double sum = 0.0;
        for (int i = 0; i < 10; i++) sum += 0.1;
        printf("Soma: %.20f\n", sum);
        double epsilon = 1e-9;
        if (fabs(sum - 1.0) < epsilon) printf("Iguais\n");
        else printf("Diferentes\n");
        return 0;
    }
    
  4. Explique o que é cancelamento catastrófico e dê um exemplo em C onde ele ocorre.

    ✓ Resposta:

    Cancelamento catastrófico ocorre quando subtraímos dois números aproximadamente iguais, perdendo dígitos significativos. Exemplo:

    #include <stdio.h>
    #include <math.h>
    int main() {
        double x = 1.0000000001;
        double y = 1.0000000000;
        double diff = x - y;
        printf("Diferença: %.20f\n", diff); // perde precisão
        return 0;
    }
    
  5. Usando notação científica, declare uma constante para a velocidade da luz (3×10⁸ m/s) e outra para a constante de Planck (6.626×10⁻³⁴ J·s). Imprima ambas no formato científico.

    ✓ Resposta:
    #include <stdio.h>
    int main() {
        double c = 3e8;
        double h = 6.626e-34;
        printf("Velocidade da luz: %e m/s\n", c);
        printf("Constante de Planck: %e J·s\n", h);
        return 0;
    }