Definição de ponto flutuante em C, C++ e C#

Última actualización: novembro 28, 2025
  • Em C/C++, literais com expoente e sufixos (f/F, l/L) definem float, double e long double; 0.1 não é exato em binário.
  • IEEE‑754 introduz ±0, ±∞ e NaN; printf arredonda na impressão, mas não corrige a representação interna.
  • Cuidado com erros: associatividade/distributiva podem falhar; prefira funções padrão do CRT e evite primitivos específicos.
  • Em C#, use double por padrão e decimal para finanças; em bancos, prefira DECIMAL/BCD para exatidão.

Representacao de ponto flutuante em C, C++ e C#

Se você programa em C e C++ ou C#, mais cedo ou mais tarde vai esbarrar nas sutilezas dos números de ponto flutuante: como eles são escritos, como a CPU os representa por dentro e, principalmente, quais surpresas podem surgir na hora de somar, multiplicar e imprimir valores aparentemente simples como 0.1. Parece trivial, mas há muita matemática, padrões (como o IEEE‑754) e detalhes de compiladores envolvidos nesse tema.

Neste guia completo, reunimos e reorganizamos cuidadosamente o conteúdo de várias fontes em português para esclarecer o que é uma constante de ponto flutuante em C/C++, como funcionam os tipos float, double e long double, que primitivos e funções o CRT da Microsoft expõe (e por que você deve preferir as funções padrão), os impactos práticos na aritmética (associatividade, distributiva, erros relativos), o papel de printf e a diferença entre impressão e valor real, além de orientações cruciais para C#, onde o tipo decimal salva o dia em cálculos financeiros. Ao final, você terá uma visão sólida e prática para evitar armadilhas e tomar decisões acertadas.

Constantes de ponto flutuante em C e C++: sintaxe, sufixos e exemplos

Uma constante de ponto flutuante é um literal que representa um número real com sinal, composto por parte inteira, parte fracionária e, opcionalmente, um expoente. Essas constantes são usadas para literais imutáveis que o compilador converte em float, double ou long double conforme a escrita.

A gramática dessas constantes pode ser descrita de forma resumida (parafraseando a especificação): um literal pode ter uma parte fracionária com ponto e, opcionalmente, um expoente e um sufixo de tipo; ou pode ser formado por dígitos seguidos de expoente e, novamente, um sufixo opcional. O expoente usa os caracteres e ou E, com sinal opcional (+ ou -) e uma sequência de dígitos.

Regras importantes de escrita: você pode pular os dígitos antes do ponto decimal (omitindo a parte inteira) ou depois do ponto (omitindo a parte fracionária), mas não ambos ao mesmo tempo; o ponto só pode ser omitido se você incluir um expoente. Não há permissão para espaços em branco dentro do literal; o número é positivo por padrão e um sinal de menos é tratado como operador unário de negação pelo compilador.

Exemplos ilustrativos de formas equivalentes de representar um mesmo valor reforçam essas regras. Para o número 15.75, por exemplo, você pode escrever 1575e-2, e para 0.0025, escrever -2.5e-3 ou 25E-4. Para 0.75, são possíveis variantes como .75, 0.75, .075e1 e 75e-2, respeitando as combinações válidas de parte fracionária e expoente.

O sufixo define o tipo do literal quando aplicável: sem sufixo, o tipo é double; com f ou F, vira float; e com l ou L, torna-se long double. Exemplos clássicos: 10.0 (double), 10.0F (float) e 10.0L (long double). No compilador Microsoft C, long double é representado internamente como double (embora sejam tipos distintos na linguagem), algo a ter em mente ao portar código ou depender de maior precisão.

IEEE‑754 na prática: bits, zeros com sinal, infinitos e NaNs

No padrão IEEE‑754, um float é composto por 1 bit de sinal, 8 bits de expoente com viés e 23 bits de fração (mantissa); o double amplia expoente e fração, e algumas plataformas oferecem long double com mais bits. Essa representação leva a peculiaridades importantes: existem +0 e -0 (dependendo do bit de sinal), ±infinito (expoente máximo e fração zero) e NaN (expoente máximo e fração não zero), que sinaliza valores “não numéricos”.

Valores subnormais (ou desnormalizados) aparecem quando o expoente é zero e a fração é diferente de zero, permitindo representar números muito pequenos com perda de precisão gradativa. Esses casos especiais afetam comparações, ordenação e propagação de erros, então é fundamental conhecê-los ao implementar algoritmos numéricos robustos.

Um exemplo clássico é o literal 0.1. Em base 2, 0.1 tem expansão binária infinita e periódica, logo precisa ser arredondado ao caber nos 23 bits de fração do float (ou nos 52 do double). Isso explica por que 0.1 não é representado exatamente, e por que somas aparentemente inocentes acumulam discrepâncias na casa das últimas casas decimais.

Relacionado:  Tipos de variáveis ​​e suas características (com exemplos)

Para visualizar a estrutura, veja um código em C que inspeciona os bits de um float, expondo sinal, expoente e fração:

/* f2u32.c */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

void showFloat(float x)
{
    int s, e, f;
    uint32_t u32 = *(uint32_t *)&x;

    /* Sinal */
    s = (u32 >> 31) & 1;
    /* Expoente (8 bits) */
    e = (u32 >> 23) & 0xff;
    /* Fração (23 bits) */
    f = u32 & 0x7fffff;

    printf("%f: 0x%08X {s:%d, e:0x%02X, f:0x%06X}\n",
           x, u32, s, e, f);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Usage: f2u32 <float>\n");
        return 1;
    }
    float x = (float)atof(argv);
    showFloat(x);
    return 0;
}

Ao executar com 0.1, você verá uma fração com padrão periódico nos bits, algo como 100110011001…; os bits finais são arredondados conforme as regras do IEEE‑754. Mesmo promovendo para double, o fenômeno persiste, e com long double (em arquiteturas que de fato o ampliam) a aproximação melhora, mas 0.1 segue impossível de representar exatamente.

Imprimir não é o mesmo que representar: por que printf “acerta” 0.1

Muita gente estranha quando printf exibe “0.100000” para 0.1. Não é mágica: é formatação com arredondamento. A rotina de impressão conhece o formato binário do número e converte para decimal com uma precisão padrão (6 casas em %f), o que tende a ocultar o erro de representação.

Experimente varrer o bit pattern ao redor de 0.1 e imprimir:

float x;
uint32_t *p = (uint32_t *)&x;

for (*p = 0x3DCCCC8A; *p <= 0x3DCCCD10; (*p)++)
    printf("%f: 0x%08X\n", x, *p);

Vários padrões distintos resultarão na mesma saída “0.100000” sob a precisão padrão de %f. Se você aumentar a precisão (por exemplo, %.15f), diferenças começam a aparecer, reforçando que o que vemos no console é uma versão arredondada do valor guardado.

Vale lembrar a sintaxe de formatação: %f. A largura define o total de caracteres e a precisão o número de casas decimais; com precisão maior, o erro latente aparece. Conclusão prática: confie nos bits e nos cálculos, e saiba que a impressão pode “embelezar” o número sem mudar sua realidade binária.

Erros relativos, associatividade e outras leis algébricas no mundo real

O erro relativo de uma operação em ponto flutuante é inerente ao arredondamento do último bit da fração. Para float, o erro típico é da ordem de 2^-23; para double, 2^-52. Esse erro se propaga ao realizar múltiplas operações, e adições/subtrações tendem a acumular mais erro que multiplicações/divisões, fenômeno conhecido e estudado em literatura clássica.

Algumas propriedades algébricas podem falhar numericamente. Embora a comutatividade costume se manter (a + b = b + a; a * b = b * a), a associatividade e a distributiva podem quebrar por conta do arredondamento em pontos diferentes da expressão, por exemplo, (a + b) + c pode diferir de a + (b + c). Isso é normal em ponto flutuante e requer cuidado na ordenação dos cálculos.

Casos especiais ajudam a visualizar: somar R$ 0,99 a R$ 4,01 e obter 5,01 no fim pode acontecer por acúmulo de arredondamentos, especialmente se o fluxo de operações e conversões intermediárias não for controlado. Projetos numéricos sérios recorrem a técnicas como somas compensadas (por exemplo, Kahan) e reformulações de expressões para reduzir perda de significância.

Para aprofundamento matemático, a obra The Art of Computer Programming, de Donald Knuth, discute aritmética de ponto flutuante (não focada no IEEE‑754, mas riquíssima em fundamentos), ajudando a entender por que certas identidades “quebram” sob arredondamento e como raciocinar com erros de truncamento e propagação.

Operadores aritméticos e de atribuição em C: inteiros vs ponto flutuante

Em C, os operadores aritméticos +, -, *, / e % (resto) operam sobre inteiros e ponto flutuante — com a ressalva de que % é apenas para inteiros. Existem operadores unários (como o menos unário e os incrementos/decrementos) e binários (soma, subtração, multiplicação, divisão, etc.). Unários agem sobre uma única variável e devolvem um valor; binários usam dois operandos e retornam o resultado sem modificar, em geral, os operandos originais.

Divisão inteira difere da divisão real: se ambos os operandos são inteiros, a divisão é truncada; se qualquer operando for ponto flutuante, a operação é real. Veja um exemplo:

int a = 17, b = 3;
int x = a / b;   /* 5 */
int y = a % b;   /* 2 */
float z = 17.0f;
float z1 = z / b;  /* 5.666666... */
float z2 = a / b;  /* 5.0 (divisão inteira, depois convertido para float) */

Os operadores de incremento e decremento (++, –) são unários e alteram o operando em 1. A forma prefixada incrementa/decrementa e devolve o valor já atualizado; a forma pós-fixada devolve o valor antigo e só depois atualiza. Isso influencia expressões como y = x++ (atribui o valor antigo a y) versus y = ++x (atribui o valor já incrementado).

Relacionado:  O que são galáxias irregulares?

O operador de atribuição (=) também retorna valor, permitindo encadeamentos do tipo x = y = z = 1.5. Contudo, cuidado ao usar atribuição dentro de condições e ao manipular variáveis em programação, como if (k = w): você não está comparando, e sim atribuindo e testando o valor atribuído, o que pode causar bugs de interpretação e lógica.

Primitivos de ponto flutuante do CRT (Microsoft): o que são e por que evitar

No ambiente Microsoft, existem funções primitivas internas para operar com ponto flutuante que são usadas para implementar versões padrão de algumas funções do CRT. Apesar de documentadas por completude, não são recomendadas para uso direto: há observações sobre precisão, exceções e conformidade com IEEE‑754 em algumas delas, e muitas existem apenas por compatibilidade com versões antigas. Para comportamento correto e portável, prefira as funções padrão do CRT e as macros recomendadas.

Estado global: por padrão, algumas dessas rotinas usam estado global com escopo de aplicativo. Caso precise alterar esse comportamento, consulte a documentação sobre o estado global no CRT. O cabeçalho envolvido é <math.h>, e há observações de compatibilidade específicas a consultar na plataforma.

Classificação de valores: _dclass, _ldclass, _fdclass

Essas rotinas implementam as versões C da macro fpclassify para double, long double e float. A assinatura é:

short __cdecl _dclass(double x);
short __cdecl _ldclass(long double x);
short __cdecl _fdclass(float x);

Elas retornam uma constante entre FP_NAN, FP_INFINITE, FP_NORMAL, FP_SUBNORMAL e FP_ZERO, assim como definido em <math.h>. Para maior portabilidade, dê preferência a fpclassify; funções como _fpclass e _fpclassf oferecem detalhes específicos da Microsoft.

Bit de sinal: _dsign, _ldsign, _fdsign

Esses primitivos equivalem à macro/função signbit do CRT e indicam se o bit de sinal está definido. Assinaturas:

int __cdecl _dsign(double x);
int __cdecl _ldsign(long double x);
int __cdecl _fdsign(float x);

O retorno é não zero se o bit de sinal estiver setado; caso contrário, retorna 0. Útil para distinguir +0 de -0, entre outros cenários.

Comparação parcial: _dpcomp, _ldpcomp, _fdpcomp

Comparações que refletem ordenação parcial (considerando NaN e casos especiais) são expostas por:

int __cdecl _dpcomp(double x, double y);
int __cdecl _ldpcomp(long double x, long double y);
int __cdecl _fdpcomp(float x, float y);

O resultado agrega flags como _FP_LT (x < y), _FP_EQ (x == y) e _FP_GT (x > y). Elas suportam as macros padrão como isgreater, isgreaterequal, isless, islessequal, islessgreater e isunordered.

Classificação (C++): _dtest, _ldtest, _fdtest

Numa interface estilo C++, temos:

short __cdecl _dtest(double* px);
short __cdecl _ldtest(long double* px);
short __cdecl _fdtest(float* px);

O ponteiro é avaliado e a classificação retorna uma das constantes FP_NAN, FP_INFINITE, FP_NORMAL, FP_SUBNORMAL e FP_ZERO. Novamente, fpclassify é a alternativa portátil.

Operar exponente e fração: _d_int/_ld_int/_fd_int, _dscale/_ldscale/_fdscale, _dunscale/_ldunscale/_fdunscale, _dexp/_ldexp/_fdexp

Conjunto de funções para manipular exponentes e significandos de forma controlada:

short __cdecl _d_int(double* px, short exp);
short __cdecl _ld_int(long double* px, short exp);
short __cdecl _fd_int(float* px, short exp);

short __cdecl _dscale(double* px, long exp);
short __cdecl _ldscale(long double* px, long exp);
short __cdecl _fdscale(float* px, long exp);

short __cdecl _dunscale(short* pexp, double* px);
short __cdecl _ldunscale(short* pexp, long double* px);
short __cdecl _fdunscale(short* pexp, float* px);

short __cdecl _dexp(double* px, double y, long exp);
short __cdecl _ldexp(long double* px, long double y, long exp);
short __cdecl _fdexp(float* px, float y, long exp);

_d_int/_ld_int/_fd_int removem a parte fracionária abaixo de um expoente dado; _dscale/_ldscale/_fdscale escalam o valor por 2^exp; _dunscale/_ldunscale/_fdunscale decompõem o número em significando ajustado (|m| ∈ =3.0, table=4.0, table=5.0 e n=2, isso representa 5.0x^2 + 4.0x + 3.0; para x=2.0, o resultado é 31.0. Não são rotinas usadas internamente no CRT moderno.

Logaritmos (base e e base 10, controlados por um sinalizador):

double __cdecl _dlog(double x, int base_flag);
long double __cdecl _ldlog(long double x, int base_flag);
float __cdecl _fdlog(float x, int base_flag);

Com base_flag=0, calcula ln(x); com valor não zero, log10(x). Prefira log/logf/logl e log10/log10f/log10l para código portável e confiável.

Seno/cosseno com deslocamento de quadrante (módulo 4):

double __cdecl _dsin(double x, unsigned int quadrant);
long double __cdecl _ldsin(long double x, unsigned int quadrant);
float __cdecl _fdsin(float x, unsigned int quadrant);

Quadrant=0,1,2,3 produz, respectivamente, sin, cos, -sin e -cos. Use sin/sinf/sinl e cos/cosf/cosl para portabilidade e precisão esperada.

Relacionado:  Números ímpares: como distingui-los, exemplos e exercícios

Leituras relacionadas do CRT

Além das citadas, verifique também: fpclassify, _fpclass, _fpclassf, isfinite, _finite, _finitef, isinf, isnan, _isnan, _isnanf, isnormal, frexp/frexpf/frexpl, ldexp/ldexpf/ldexpl, log/logf/logl/log10/log10f/log10l, sin/sinf/sinltodas em <math.h> com comportamento documentado e apoio das plataformas.

Ponto flutuante no C#: float, double e quando usar decimal

Em C#, os números reais são representados principalmente pelos tipos float e double (ponto flutuante binário) e pelo tipo decimal (base 10). O tipo padrão para literais de ponto flutuante é double — se você escrever 12.5 sem sufixo, o compilador trata como double; para float, use o sufixo F (12.5F). Essa escolha padrão favorece precisão em muitos cenários.

Precisão e intervalo aproximados: float possui cerca de 6 a 7 dígitos significativos e faixa ~±1,5e−45 a ±3,4e38; double alcança ~15 a 16 dígitos com faixa ~±5,0e−324 a ±1,7e308. Como 5/9 = 0,555… tem expansão infinita, float arredonda por volta do 7º dígito (0,5555556), enquanto double mantém mais casas (0,555555555555556), preservando mais informação.

Daí vem a recomendação prática: use double por padrão em cálculos científicos/gerais quando precisão é relevante, a menos que haja exigências de memória ou interoperabilidade que apontem para float. Nos cálculos financeiros, entretanto, a história muda.

Financeiro exige exatidão decimal. Exemplos reais mostram por quê: 4.99 × 17 pode resultar em 84.82999 com float (e arredondamentos inconvenientes), enquanto o esperado é 84.83. Outro clássico: preço inteiro 100 com desconto de 10% escrito de forma ingênua pode render 89 em vez de 90 por causa de conversões e truncamentos ao misturar float e int na expressão.

Para resolver, use decimal em C# quando trabalhar com moedas ou valores que exigem exatidão decimal. O tipo decimal é representado em base 10 internamente, o que permite representar muitos valores “com duas casas” exatamente e controlar arredondamentos conforme as regras de negócio. Expressões como (decimal)(preco * (1 – desconto)) evitam as discrepâncias típicas de float/double em operações monetárias.

Decimais, bancos de dados e BCD: por que não guardar dinheiro em float

Em bancos de dados, tipos como DECIMAL(10,2) são a escolha natural para valores monetários. O primeiro número (10) refere-se ao total de dígitos e o segundo (2) às casas decimais. Internamente, muitos SGBDs usam BCD (Binary Coded Decimal), onde cada nibble (4 bits) representa um dígito decimal (0-9), evitando as aproximações de base 2.

Na prática, BCD e decimais controlam o erro residual em multiplicações e divisões por regras próprias; somar e subtrair em BCD não introduz erro de base. Por isso, pegar um valor DECIMAL do banco e jogar em float/double para “calcular mais rápido” pode introduzir erros que o banco não teria — um anticlímax frequente em aplicações financeiras.

Historicamente, linguagens e padrões buscaram preencher essa lacuna. O C++ já discutiu tipos decimais em propostas do C++0x (TR1/N3126), enquanto C# oferece decimal nativamente. Em outras plataformas, tipos como Currency (em Delphi) funcionam como inteiros escalonados por um fator fixo (2 ou 4 casas), simplificando a exatidão decimal. O conceito é o mesmo: evitar frações binárias em problemas que são eminentemente decimais.

No fim, as regras do jogo são simples: para medições físicas, simulações e ML, use float/double conforme a necessidade; para dinheiro, impostos e preços, use decimal/BCD/inteiros escalonados. Isso impede “um centavo invisível” de virar um rombo ao longo de milhões de transações.

O ponto flutuante é poderoso, mas requer consciência: a sintaxe em C/C++ dá flexibilidade para escrever literais com ou sem parte inteira, fracionária e expoente; sufixos definem o tipo (float/double/long double). O formato IEEE‑754 habilita zeros com sinal, infinitos e NaNs, e, apesar de printf poder maquiar a saída, a representação binária carrega erros inevitáveis em números como 0.1. Em C, os operadores aritméticos exigem atenção a divisões inteiras e a efeitos de pré/pós‑incremento; no ecossistema Microsoft, há primitivos CRT documentados, mas a recomendação é ficar com as funções padrão para portabilidade e conformidade. Em C#, double é o padrão, mas decimal é obrigatório no financeiro, e no mundo dos bancos de dados DECIMAL/BCD é o caminho para a exatidão. Com esses critérios, você evita armadilhas e escolhe o tipo certo para cada problema.

Artículo relacionado:
Modelo de banco de dados relacional: elementos, como fazê-lo, exemplo