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.
- A diferença entre
floatedouble, medida comstd::setprecision(20). - Por que
0.1 + 0.2 == 0.3dá falso. - Notação científica:
6.022e23,1e-3, e por que ocoutàs vezes mostra1.23457e+06. infenan: o que a divisão por zero faz com umdouble.- 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
| Tipo | Bytes | Dígitos confiáveis | Como escrever o valor |
|---|---|---|---|
float | 4 | 6 a 7 | 1.5f (com f no fim) |
double | 8 | 15 a 16 | 1.5 (o padrão) |
long double | 16 no Linux da Ana | 18 | 1.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
ffaz o1.0serfloat. Como ele tem ponto, a divisão tem vírgula (lembre que1 / 3, inteiro com inteiro, daria 0; aula Inteiros). - std::setprecision(20)
- Dali em diante, o
coutmostra 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.)
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
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
| Escrita | Lê-se | Vale |
|---|---|---|
6.022e23 | 6,022 × 1023 | 602 200 000 000 000 000 000 000 |
1e-3 | 1 × 10-3 | 0,001 |
1.23457e+06 | 1,23457 × 106 | 1 234 570 |
1.23e-05 | 1,23 × 10-5 | 0,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
| Conta | Resultado | Significa |
|---|---|---|
1.0 / 0.0 | inf | infinity, infinito positivo |
-1.0 / 0.0 | -inf | Infinito negativo |
0.0 / 0.0 | nan (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 + 1 | inf | Infinito mais um continua infinito |
- O
-no-nan: no processador do PC da Ana, onanproduzido por essas contas vem com o "bit de sinal" ligado, e ocoutmostra o sinal. Não tem significado útil; em outros sistemas (inclusive no editor do guia) pode aparecer sónan. nan == nané falso: umnannão é igual a nada, nem a ele mesmo. Por isso, para perguntar "isto é nan?", usestd::isnan(x); para infinito,std::isinf(x). As duas vêm de<cmath>(aula Matemática).- O
nancontamina: qualquer conta comnandánan. Uma média comnanno meio virananno 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.001já 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";
}
< 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
== 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).
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.
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).
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
| Escrita | O 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-3 | Notação científica |
std::setprecision(n) | Mostrar n algarismos (<iomanip>) |
std::fixed / std::scientific | Notação fixa / científica na saída |
std::abs(a - b) < 1e-9 | Comparar com tolerância (<cmath>) |
std::isnan(x), std::isinf(x) | Detectar nan e infinito (<cmath>) |
std::numeric_limits<double>::digits10 | Dígitos decimais garantidos (15) |
Exercícios
1. Sufixo do float
Complete para o valor ser float:
2. Notação científica
Escreva 0,005 em notação científica do C++ (5 × 10-3):
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, sufixof) 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. Ocoutmostra 6 algarismos e às vezes usae+06;std::fixedmuda isso. - Divisão por zero com
doublenão mata o programa: dáinf,-infounan.nannão é igual a nada; usestd::isnan. - Dinheiro "de verdade": centavos num inteiro.