C++ float e double

Altura, peso, média de notas, temperatura, preço: quase tudo o que se mede tem vírgula. Em C++, números com vírgula moram nos tipos de ponto flutuante: float e double. Eles são rápidos e práticos, mas têm uma surpresa que todo programador precisa conhecer cedo: muitos números não são guardados exatamente. Nesta aula você vai ver, com saídas reais, por que 0.1 + 0.2 não é exatamente 0.3, o que são inf e nan e como comparar números com vírgula do jeito certo.

Você vai aprender
  • A diferença entre float e double, medida com std::setprecision(20).
  • Por que 0.1 + 0.2 == 0.3 dá falso.
  • Notação científica: 6.022e23, 1e-3, e por que o cout às vezes mostra 1.23457e+06.
  • inf e nan: o que a divisão por zero faz com um double.
  • Como comparar números com vírgula usando uma tolerância.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: a régua com riscos

Pegue uma régua escolar. Ela tem riscos a cada milímetro. Se você medir algo com 3,47 mm, vai ter que escolher: 3 mm ou 4 mm? Você anota o risco mais próximo. A régua não é "errada"; ela só não tem riscos para todos os números.

Um double é uma régua com uma quantidade enorme de riscos, mas finita. Muitos números caem exatamente num risco (0.5, 0.25, 3.0); muitos outros, como 0.1, caem entre dois riscos, e o computador guarda o mais próximo. A diferença é minúscula, mas existe, e às vezes aparece.

Por que 0.1 não tem risco? O computador guarda números em binário (base 2). Em binário, 1/10 é uma "dízima periódica", do mesmo jeito que 1/3 = 0,3333… em decimal: não dá para escrever com um número finito de casas. Já 0.5 (= 1/2) e 0.25 (= 1/4) são exatos em binário.

O nome "ponto flutuante" vem de como o número é guardado: alguns dígitos significativos e uma "posição da vírgula" que pode flutuar. É o mesmo truque da notação científica (seção 3).


1. float × double

TipoBytesDígitos confiáveisComo escrever o valor
float46 a 71.5f (com f no fim)
double815 a 161.5 (o padrão)
long double16 no Linux da Ana181.5L

Ver a diferença exige mostrar mais casas do que o cout mostra normalmente. Para isso existe o std::setprecision ("definir a precisão"), do cabeçalho <iomanip>:

#include <iomanip>
#include <iostream>

int main() {
    float f = 1.0f / 3;
    double d = 1.0 / 3;
    std::cout << "float:  " << f << "\n";
    std::cout << "double: " << d << "\n";
    std::cout << std::setprecision(20);
    std::cout << "float:  " << f << "\n";
    std::cout << "double: " << d << "\n";
    std::cout << "0.1 em float:  " << 0.1f << "\n";
    std::cout << "0.1 em double: " << 0.1 << "\n";
}
1.0f / 3
O f faz o 1.0 ser float. Como ele tem ponto, a divisão tem vírgula (lembre que 1 / 3, inteiro com inteiro, daria 0; aula Inteiros).
std::setprecision(20)
Dali em diante, o cout mostra até 20 algarismos significativos. Sem ele, mostra 6.
$ g++ -std=c++23 -Wall -Wextra precisao.cpp -o precisao
$ ./precisao
float:  0.333333
double: 0.333333                      « com 6 algarismos, os dois parecem iguais
float:  0.3333333432674407959         « 7 algarismos certos, depois "lixo"
double: 0.33333333333333331483        « 16 algarismos certos
0.1 em float:  0.10000000149011611938
0.1 em double: 0.10000000000000000555 « o 0.1 guardado não é exatamente 0.1

As duas últimas linhas são a régua em ação: o 0.1 que você escreveu foi guardado como o risco mais próximo. No double, o erro aparece só na 18ª casa; no float, já na 9ª.

O float perde até números inteiros

#include <iomanip>
#include <iostream>
#include <limits>

int main() {
    float grande = 16777217;
    double grandeD = 16777217;
    std::cout << std::fixed << std::setprecision(1);
    std::cout << "float:  " << grande << "\n";
    std::cout << "double: " << grandeD << "\n";
    std::cout << "dígitos confiáveis: float " << std::numeric_limits<float>::digits10
              << ", double " << std::numeric_limits<double>::digits10 << "\n";
}
$ g++ -std=c++23 -Wall -Wextra limite-float.cpp -o limite-float
$ ./limite-float
float:  16777216.0                    « guardou 16777217, saiu 16777216
double: 16777217.0
dígitos confiáveis: float 6, double 15

Com 8 algarismos, o número passou do que o float consegue guardar, e ele arredondou para o risco mais próximo, sem nenhum aviso do compilador. O std::numeric_limits<float>::digits10 confirma: o float garante só 6 dígitos decimais. (O std::fixed pede "notação fixa", sem e+07; seção 3.)

Qual usar: double

Use double sempre, a não ser que tenha um motivo forte. O float economiza memória e aparece em jogos, gráficos 3D e placas de vídeo, onde há milhões de números e 7 dígitos bastam. Num programa comum, a economia não compensa a precisão perdida. Os números com ponto que você escreve (1.65) já são double.


2. 0.1 + 0.2 == 0.3 é falso

Este é o exemplo mais famoso da computação, e acontece em praticamente toda linguagem:

#include <iomanip>
#include <iostream>

int main() {
    double a = 0.1;
    double b = 0.2;
    std::cout << "0.1 + 0.2 = " << a + b << "\n";
    std::cout << "É igual a 0.3? " << (a + b == 0.3) << "\n";
    std::cout << std::setprecision(17) << "0.1 + 0.2 = " << a + b << "\n";
}
$ g++ -std=c++23 -Wall -Wextra soma.cpp -o soma
$ ./soma
0.1 + 0.2 = 0.3                       « o cout arredonda para 6 algarismos
É igual a 0.3? 0                      « 0 = falso
0.1 + 0.2 = 0.30000000000000004       « a verdade, com 17 algarismos

O == pergunta "são iguais?" e responde 1 (verdadeiro) ou 0 (falso); a aula Booleanos explica por que aparece como número. Os parênteses em (a + b == 0.3) são necessários por causa da ordem dos operadores (aula Precedência).

O que aconteceu: o 0.1 guardado é um tiquinho maior que 0.1, o 0.2 também, e a soma dos dois "tiquinhos" caiu num risco diferente do risco onde o 0.3 é guardado. A diferença está na 17ª casa, mas o == exige igualdade perfeita.

O mesmo acontece somando 0.1 dez vezes (o for repete a soma 10 vezes; aula for):

$ cat dez.cpp
#include <iomanip>
#include <iostream>

int main() {
    double total = 0.0;
    for (int i = 0; i < 10; i++) {
        total = total + 0.1;
    }
    std::cout << "total = " << total << "\n";
    std::cout << "total == 1.0? " << (total == 1.0) << "\n";
    std::cout << std::setprecision(20) << "total = " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra dez.cpp -o dez
$ ./dez
total = 1
total == 1.0? 0
total = 0.99999999999999988898
E o dinheiro?

Por isso, sistemas de banco e de loja não guardam dinheiro em double: um centavo perdido aqui e ali, somado milhões de vezes, vira problema. A solução simples é guardar centavos num inteiro: R$ 12,50 vira long long precoCentavos = 1250;. Nos exercícios do Nível 1, double para preços está ótimo; só saiba que existe essa armadilha.


3. Notação científica

Para números muito grandes ou muito pequenos, a ciência escreve 6,022 × 10²³ em vez de 602 200 000 000 000 000 000 000. Em C++, o "× 10 elevado a" se escreve com a letra e (de "expoente"):

#include <iostream>

int main() {
    double avogadro = 6.022e23;
    double milimetro = 1e-3;
    double salario = 1234567.89;
    double pequeno = 0.0000123;
    std::cout << avogadro << "\n";
    std::cout << milimetro << "\n";
    std::cout << salario << "\n";
    std::cout << pequeno << "\n";
    std::cout << std::fixed << salario << "\n";
    std::cout << std::scientific << salario << "\n";
}
$ g++ -std=c++23 -Wall -Wextra cientifica.cpp -o cientifica
$ ./cientifica
6.022e+23
0.001
1.23457e+06                           « o salário virou notação científica, e perdeu os centavos
1.23e-05
1234567.890000                        « std::fixed: sem notação científica
1.234568e+06                          « std::scientific: sempre com e
EscritaLê-seVale
6.022e236,022 × 1023602 200 000 000 000 000 000 000
1e-31 × 10-30,001
1.23457e+061,23457 × 1061 234 570
1.23e-051,23 × 10-50,0000123

Por padrão, o cout mostra 6 algarismos significativos e troca para notação científica quando o número ficaria comprido demais. Por isso o salário de 1234567.89 apareceu como 1.23457e+06: o valor guardado continua certo, só a exibição foi arredondada. Para mostrar dinheiro com duas casas, use std::fixed com std::setprecision(2) (aula Formatar com iomanip) ou std::format (aula std::format e std::print).


4. inf e nan: divisão por zero com double

Na aula Inteiros, dividir um int por zero matou o programa. Com double, não: o padrão de ponto flutuante usado pelos computadores (o IEEE 754) tem valores especiais para esses casos.

#include <cmath>
#include <iostream>

int main() {
    double zero = 0.0;
    double positivo = 1.0 / zero;
    double negativo = -1.0 / zero;
    double nada = zero / zero;
    double raiz = std::sqrt(-1.0);
    std::cout << positivo << " " << negativo << " " << nada << " " << raiz << "\n";
    std::cout << "isinf: " << std::isinf(positivo) << "  isnan: " << std::isnan(nada) << "\n";
    std::cout << "nan == nan? " << (nada == nada) << "\n";
    std::cout << "inf + 1 = " << positivo + 1 << "\n";
}
$ g++ -std=c++23 -Wall -Wextra infnan.cpp -o infnan
$ ./infnan
inf -inf -nan -nan
isinf: 1  isnan: 1
nan == nan? 0
inf + 1 = inf
ContaResultadoSignifica
1.0 / 0.0infinfinity, infinito positivo
-1.0 / 0.0-infInfinito negativo
0.0 / 0.0nan (aqui, -nan)not a number, "não é um número": a conta não tem resposta
std::sqrt(-1.0)nan (aqui, -nan)Raiz quadrada de negativo não existe nos números reais
inf + 1infInfinito mais um continua infinito
  • O - no -nan: no processador do PC da Ana, o nan produzido por essas contas vem com o "bit de sinal" ligado, e o cout mostra o sinal. Não tem significado útil; em outros sistemas (inclusive no editor do guia) pode aparecer só nan.
  • nan == nan é falso: um nan não é igual a nada, nem a ele mesmo. Por isso, para perguntar "isto é nan?", use std::isnan(x); para infinito, std::isinf(x). As duas vêm de <cmath> (aula Matemática).
  • O nan contamina: qualquer conta com nan dá nan. Uma média com nan no meio vira nan no fim, longe de onde o problema nasceu.

O programa continuou rodando, mas com valores sem sentido. Um preço inf é tão errado quanto um programa que trava, só que mais difícil de perceber. Confira o divisor antes de dividir, com um if.


5. Comparar com tolerância

Se == não serve, como saber se dois números com vírgula são "iguais"? Pergunte se eles estão pertinho o suficiente: se a distância entre eles é menor que uma tolerância que você escolhe.

#include <cmath>
#include <iostream>

int main() {
    double soma = 0.1 + 0.2;
    double tolerancia = 1e-9;
    std::cout << "Com ==:         " << (soma == 0.3) << "\n";
    std::cout << "Com tolerância: " << (std::abs(soma - 0.3) < tolerancia) << "\n";
}
soma - 0.3
A diferença entre os dois números. Pode dar negativa, dependendo de qual é maior.
std::abs(...)
O valor absoluto ("sem sinal"): transforma -0,00001 em 0,00001. Vem de <cmath>.
< tolerancia
"É menor que 0,000000001?" Se sim, para o seu programa, os números são iguais.
1e-9
A tolerância: um bilionésimo. Escolha conforme o problema: para preços em reais, 0.001 já basta; para medidas científicas, talvez menos.
$ g++ -std=c++23 -Wall -Wextra tolerancia.cpp -o tolerancia
$ ./tolerancia
Com ==:         0
Com tolerância: 1

Missão: a balança da lojinha pesou 3 pacotes de 0.1 kg, e o programa diz que o total não é 0.3. Troque o == por uma comparação com tolerância (use std::abs e 1e-9) para sair Iguais? 1.

#include <cmath>
#include <iostream>

int main() {
    double total = 0.1 + 0.1 + 0.1;
    std::cout << "Iguais? " << (total == 0.3) << "\n";
}
#include <cmath>
#include <iostream>

int main() {
    double total = 0.1 + 0.1 + 0.1;
    std::cout << "Iguais? " << (std::abs(total - 0.3) < 1e-9) << "\n";
}
Comparar com < e > é tranquilo

O problema é só a igualdade exata (== e !=). Perguntas como "a nota é maior que 7?" (nota > 7.0) funcionam bem, a não ser que o valor esteja colado no limite (6.9999999 × 7.0). Comparações têm aula própria: Operadores de Comparação.


Quando dá errado

Uma comparação com == dá falso para números "iguais"

O que significa: os dois números diferem lá na 16ª ou 17ª casa, por causa dos arredondamentos da "régua".

Como consertar: compare com tolerância: std::abs(a - b) < 1e-9. Para ver a diferença, mostre com std::setprecision(17).

O resultado sai inf, -inf, nan ou -nan

O que significa: houve uma divisão por zero, uma raiz de negativo ou uma conta com um valor que já era nan.

Como consertar: procure de trás para frente a primeira conta que gerou o valor especial. Confira divisores antes de dividir; use std::isnan/std::isinf para detectar.

O número aparece como 1.23457e+06 ou perdeu casas

O que significa: o cout mostra 6 algarismos significativos por padrão. O valor guardado está certo; só a exibição foi arredondada.

Como consertar: std::cout << std::fixed << std::setprecision(2) para duas casas fixas (iomanip).

Uma conta com vírgula deu inteiro (ou 0)

O que significa: parte da conta era só de inteiros, como 5 / 9, que dá 0 antes de entrar no double.

Como consertar: escreva os números da conta com ponto: 5.0 / 9.


Referência rápida

EscritaO que faz
double x = 1.65;Número com vírgula, 15–16 dígitos (o padrão)
float x = 1.65f;Número com vírgula, 6–7 dígitos
6.022e23, 1e-3Notação científica
std::setprecision(n)Mostrar n algarismos (<iomanip>)
std::fixed / std::scientificNotação fixa / científica na saída
std::abs(a - b) < 1e-9Comparar com tolerância (<cmath>)
std::isnan(x), std::isinf(x)Detectar nan e infinito (<cmath>)
std::numeric_limits<double>::digits10Dígitos decimais garantidos (15)

Exercícios

1. Sufixo do float

Complete para o valor ser float:

float peso = 2.5;

2. Notação científica

Escreva 0,005 em notação científica do C++ (5 × 10-3):

double x = ;

3. O que std::cout << (0.1 + 0.2 == 0.3); mostra?

4. Quanto vale 1.0 / 0.0?

5. Qual é o jeito certo de perguntar se x é nan?

6. Quantos dígitos decimais um double garante?

Missão final: a febre

Missão: o termômetro americano marcou 98.6 °F. A fórmula para Celsius é (F − 32) × 5/9, que dá 37 °C. Mas o programa mostra Celsius: 0. Ache a conta que está sendo feita só com inteiros e conserte para sair Celsius: 37.

#include <iostream>

int main() {
    double fahrenheit = 98.6;
    double celsius = (fahrenheit - 32) * (5 / 9);
    std::cout << "Celsius: " << celsius << "\n";
}
#include <iostream>

int main() {
    double fahrenheit = 98.6;
    double celsius = (fahrenheit - 32) * (5.0 / 9);
    std::cout << "Celsius: " << celsius << "\n";
}

Curiosidade: desta vez a régua ajudou. No laboratório, mostrando esse resultado com std::setprecision(20), saiu exatamente 37: os arredondamentos das contas caíram bem no risco do 37. Nem sempre é assim, e é por isso que a comparação com tolerância existe.


Resumo

  • double (8 bytes, 15–16 dígitos) é o padrão para números com vírgula; float (4 bytes, 6–7 dígitos, sufixo f) só quando houver motivo.
  • Muitos números (como 0.1) não são guardados exatamente; std::setprecision(20) mostra a diferença.
  • 0.1 + 0.2 == 0.3 é falso. Compare com tolerância: std::abs(a - b) < 1e-9.
  • Notação científica: 6.022e23, 1e-3. O cout mostra 6 algarismos e às vezes usa e+06; std::fixed muda isso.
  • Divisão por zero com double não mata o programa: dá inf, -inf ou nan. nan não é igual a nada; use std::isnan.
  • Dinheiro "de verdade": centavos num inteiro.