De forma resumida: os números são guardados em binário na memória. Para os valores não-inteiros, eles são guardados usando potências negativas de 2 (2^-1 = 0,5; ou 2^-2 = 0,25; ou 2^-3 = 0,125, e assim em diante para cada bit).
Por causa disso, não há representação exata para todos os números, e há uma leve perda de precisão. Se você sempre usar potências negativas de 2, por exemplo, essa perda de precisão não deve ocorrer.
Como lidar com isso: Provavelmente, nesse momento seja mais fácil nem se preocupar muito com isso. Quanto alguns exemplos em que se precisa ter cuidado, temos abaixo:
- Para imprimir o valor na tela, formatar ele para o número de casas decimais desejadas (no meu projeto, eu precisei propositalmente aumentar o número de casas decimais a serem impressos para reproduzir o problema. Por algum motivo, o padrão era um número de casas decimais menor que no seu projeto — o que demonstra que especificar o número de casas decimais pode ser uma boa ideia de qualquer modo).
- Para testar igualdade, se incluir uma margem de erro (por exemplo, subtrair o valor A de B, e verificar se o resultado está entre a margem de erro, como -0,001 e 0,001).
- Essa não é uma lista extensiva: pode ser que hajam outros casos que cuidados com esses números sejam importantes. Um exemplo que já ouvi falar é de sistemas que lidam com dinheiro, e que, ao invés de usar Double/Float, usam inteiro mas representando o número de centavos (ou seja, se o valor é de R$ 10,95, internamente o valor é 1095, e apenas na hora de apresentar ao cliente que o valor é formatado).
Leitura Extra: