paulo1205
(usa Ubuntu)
Enviado em 11/03/2013 - 03:55h
Usar
double só muda a precisão, mas não o fato de que o valor não consegue ser devidamente representado. Se você aumentar o número de algarismos significativos na saída do
printf(), isso vai ficar bem evidente.
Nos nossos computadores digitais, os números são representados usando codificação binária -- isto é: a base de numeração 2, o que significa que só há dois algarismos (0 e 1), e que cada casa à esquerda vale duas vezes o valor da casa à direita.
Números inteiros podem ser escritos usando uma soma ds potências não-negativas de 2. Por exemplo, o número 153 pode ser decomposto da seguinte forma: (1*2^7)+(0*2^6)+(0*2^5)+(1*2^4)+(1*2^3)+(0*2^2)+(0*2^1)+(1*2^0) ou, resumidamente, em notação binária: 10011001(2). Aliás, pensando por analogia, o mesmo vale para nossa represnetação decimal: 153 é uma notação mais curta para (1*10^2)+(5*10^1)+(3*10^0). (Na minha época, valor absoluto de um algarismo e o valor relativo que ele assume, dependendo da posição em que é escrito no numeral, era matéria de 4ª série, e sua generalização, usando bases de numeração diferentes de 10, matéria de 7ª ou 8ª).
O C não possui uma notação para base binária, mas possui duas que facilitam o agrupamento de bits: octal (base 8), que usa o prefixo
0 e algarismos de 0 a 7, agrupando os bits de três em três, e hexadecimal (base 16), que usa o prefixo
0x, e algarismos de 0 a 9 mais A a F (para os valores de dez a quinze). agrupando os bits de quatro em quatro.
Os tipos nativos de inteiros têm ainda outra característica em C: eles são representados usando uma quantidade fixa de bits, o que implica que existe um valor máximo representável nativamente. Para representar valores maiores do que esses, é necessároi fazer alguma composição usando vários inteiros nativos, e fia a cargo do usuário cuidar dessa composição e da forma de representá-la e interpretá-la (ainda que ele se valha de alguma biblioteca preexistente).
Algo semelhante vale para a representação de valores fracionados. A diferença em relação à representação de inteiros é que as potências de 2 usadas como parcelas na representação do número podem ter expoentes negativos. Assim sendo, 5,75, por exemplo, poderia ser pensado como (1*2^2)+(0*2^1)+(1*2^0)+(1*2^(-1))+(1*2^(-2)). Numa notação binária fracionada, poderia ser escrito como 101,11(2).
O problema para nós, que estamos acostumados a escrever números fracionados em notação decimal, é que o que parece muito simples em decimal pode virar uma dízima periódica em base 2. Por exemplo, 0,2(10), equivalente a 2/10 ou 1/5 , vira a dízima periódica 0,0011001100110011001100110011...(2), que requereria infinitos bits para poder ser exatamente representada. Como ninguém gostaria de gastar toda a memória do computador com bits de dízimas -- até porque aliás, mesmo gastando toda a memória, o número continuaria sem uma representação
exata --, o que se faz é limitar a precisão e, na hora de exibir o número para o usuário, usar algum esquema de arredondamento do número.
Também isso remete a coisas que já vimos nos tempos de colégio. Uma fração como 2/3 produz a dízima periódica 0,66666666... quando escrita em notação decimal (matéria de 5ª ou 6ª séries, se não me engano). Se quisermos limitar a quantidade de
algarismos significativos (início do segundo grau/ensino médio) a, por exemplo, 5 dígitos, teremos de arredondar para 0,66666 ou 0,66667, nenhum dos dois uma representação exata do núemro (lembrando que zero a esquerda não conta como algarismo significativo).
A forma mais comum de representar números fracionados em computadores digitais é a representação em ponto flutuante (em Inglês,
floating point, de onde deriva o nome do tipo
float do C). Ela é análoga, usando base 2, de outra coisa que se vê no primeiro ano do segundo grau: a
notação científica, que permite escrever de modo uniforme números muito grandes ou muito pequenos, na forma
M·10^n, onde
M é um número decimal tal que
1<=M<10 e com uma quantidade determinada de algarismos significativos, e
n é um expoente inteiro, usado de modo a fazer com que o valor de
M, compreendido entre 1 e 10, volte à faixa original. Usando notação científica, o número 1234567 poderia ser escrito como 1,234567·10^7, e 0,000321 ficaria como 3,21·10^(-4). Em C, Pascal, BASIC, Perl, Python e outras linguagens de programação de uso comum no Ocidente, o termo "·10^" é representado somente com a letra "E" ou "e", de modo que os dois exemplos anteriores são expressos, respectivamente, como
1,234567E6 (ou 1,234567E+6[/i]) e
3,21E-4.
A representação binária em ponto flutuante escreve os valores na forma
[+-]M·2^n. A parte
M, chamada de
mantissa, guarda um valor maior ou igual a 1 e menor do que 2 (ou, depedendo da implementação, maior ou igual a 1/2 e menor do que 1). Tanto
M quanto
n são representados usando uma quantidade fixa de bits, e é isso que limita a quantidade de algarismos siginificativos da mantissa e os maiores e menores números representáveis.
Apesar de frequentemente se dizer que
float e
double servem para variáveis que vão armazenar valores reais, na verdade valores reais em geral não podem ser armazenados de forem maiores do que um limite máximo ou menores do que um limite mínimo, e somente de modo aproximado se tais valores forem irracionais ou se forem racionais mas com um denominador que não seja uma potência de 2.
Nos nossos PCs, celulares e tablets, a representação interna de números em ponto flutuante segue o padrão IEEE-754, de 1985. Recomendo ler a respeito (na Wikipedia em Inglês, por exemplo, há uma introdução razoável). Houve uma revisão do padrão em 2008, que acrescentou, entre outras coisas, números de ponto flutuantes representados em decimal. Poucos processadores, por enquanto, têm suporte nativo a essa nova versão do padrão.