double
in plaats van float
.
© Harry Broeders.
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.
In het boek "De taal C - van PSD tot C-programma" van Daryl McAllister wordt
voor het opslaan van een reëel getal een variabele van het type
float
gebruikt. Lang geleden (toen het boek werd geschreven)
was dit inderdaad gebruikelijk. Ik adviseer je echter om in plaats van het
type float
het type double
te gebruiken.
Variabelen van het type float
en variabelen van het type
double
zijn allebei bedoeld om reële getallen in op te
slaan. Het verschil zit in het aantal bits dat voor de opslag wordt gebruikt.
Een variabele van het type float
wordt opgeslagen in 32 bits
(4 bytes). Een variabele van het type double
wordt opgeslagen
in 64 bits (8 bytes). Een double
heeft dus dubbel zoveel
bits als een float
. Dit verklaart meteen de naam van dit type.
Doordat een double
uit meer bits bestaat kunnen er grotere getallen
in worden opgeslagen. Nog veel belangrijker is echter dat een double meer
significante cijfers heeft en dus veel nauwkeuriger is. Een
float
heeft minimaal 6 en maximaal 9 significante cijfers en
een double
heeft minimaal 15 en maximaal 17 significante cijfers.
In het onderstaande programma wordt de waarde 100.0 / 3.0 berekent als
float
en als double
en afgedrukt met lekker veel
cijfers achter de decimale punt.
int main(void) {
float f = 100.0 / 3.0;
double d = 100.0 / 3.0;
printf("f = %.20f\n", f);
printf("d = %.20f\n", d);
getchar();
return 0;
}
Dit programma geeft als het gecompileerd wordt met Microsoft Visual C++ 2010 Express Edition de volgende uitvoer:
f = 33.33333206176757800000 d = 33.33333333333333600000
Zoals je weet kunnen float
variabelen worden afgedrukt met
printf
door de code %f
te gebruiken. Variabelen
van het type double
kun je ook afdrukken met %f
.
Je ziet dat een double
veel nauwkeuriger is dan een
float
.
Let op! Om variabelen van het type double
in te lezen
met scanf
moet je het format %lf
gebruiken
in plaats van %f
.
Getallen van het type float
en double
worden in
de computer in het binaire talstelsel opgeslagen. Alle breuken die niet als
a / 2b geschreven kunnen worden (waarbij a en b gehele getallen
zijn) moeten dan afgerond worden. In het tientallig stelsel moeten alle breuken
die niet als a / 10c geschreven kunnen worden (waarbij a en c
gehele getallen zijn) afgerond worden. Omdat 10c gelijk is aan
2c x 5c moeten alle getallen die in het decimale stelsel
afgerond moeten worden in het binaire stelsel ook afgerond worden. Maar sommige
getallen die in het decimale stelsel niet moeten worden afgerond moeten in
het binaire stelsel wel afgerond worden. Bijvoorbeeld 102/10 = 10.2.
#include <stdio.h>
int main(void) {
float f = 102.0 / 10.0;
double d = 102.0 / 10.0;
printf("f = %.20f\n", f);
printf("d = %.20f\n", d);
getchar();
return 0;
}
Dit programma geeft als het gecompileerd wordt met Visual Studio de volgende uitvoer:
f = 10.19999980926513700000 d = 10.19999999999999900000
Je ziet dat het getal 10.2 ook moet worden afgerond.
In het boek wordt gewaarschuwd dat het vergelijken van floating point getallen
met ==
en !=
gevaarlijk is omdat het resultaat
door afrondfouten anders kan zijn dan je zou verwachten. Het onderstaande
programma geeft bijvoorbeeld de uitvoer:
Dat verwacht je niet!
Het maakt daarbij overigens niet uit of je float
of
double
gebruikt.
#include <stdio.h>
int main(void) {
float f = 1.0 / 3.0;
if (f + f + f != 1.0)
printf("Dat verwacht je niet!\n");
getchar();
return 0;
}
In het boek wordt alleen gesproken over problemen bij het vergelijken met
==
en !=
. Ook bij alle andere vergelijkingsoperatoren
kunnen vergelijkbare problemen optreden.
Als voorbeeld is hieronder een programma dat een conversie tabel aanmaakt voor het omzetten van temperaturen van °F naar °C gegeven:
#include <stdio.h>
int main(void) {
float fahrenheit;
float celcius;
printf(" F C\n");
for (fahrenheit = 31.0; fahrenheit <= 32.0; fahrenheit = fahrenheit + 0.1) {
celcius=(fahrenheit - 32.0) / 1.8;
printf("%6.2f %6.2f\n", fahrenheit, celcius);
}
getchar();
return 0;
}
De uitvoer van dit programma is als volgt:
F C 31.00 -0.56 31.10 -0.50 31.20 -0.44 31.30 -0.39 31.40 -0.33 31.50 -0.28 31.60 -0.22 31.70 -0.17 31.80 -0.11 31.90 -0.06
Er wordt dus geen regel afgedrukt voor
fahrenheit
is 32 en dat zou
je wel verwachten omdat de voorwaarde
fahrenheit<=32.0
gebruikt wordt!
Het is overigens toeval dat in dit geval de laatste regel uit de tabel niet
wordt afgedrukt. Als we de start en stopwaarde allebei met 1.0 verhogen wordt
de laatste regel wel afgedrukt.
#include <stdio.h>
int main(void) {
float fahrenheit;
float celcius;
printf(" F C\n");
for (fahrenheit = 32.0; fahrenheit <= 33.0; fahrenheit = fahrenheit + 0.1) {
celcius=(fahrenheit - 32.0) / 1.8;
printf("%6.2f %6.2f\n", fahrenheit, celcius);
}
getchar();
return 0;
}
De uitvoer van dit programma is als volgt:
F C 32.00 0.00 32.10 0.06 32.20 0.11 32.30 0.17 32.40 0.22 32.50 0.28 32.60 0.33 32.70 0.39 32.80 0.44 32.90 0.50 33.00 0.56
Ook het gebruik van <
in plaats van <=
helpt
ons niets. De uitvoer van beide bovenstaande programma's is namelijk exact
gelijk als we de <=
vervangen door een
<
. De uitvoer van het eerste programma is dan correct (de
regel voor fahrenheit
is 32 wordt dan zoals je verwacht niet
afgedrukt) maar de uitvoer van het tweede programma is dan onverwachts (de
regel voor fahrenheit
is 33 wordt dan toch wel afgedrukt).
Conclusie: Vergelijken met een grenswaarde is onvoorspelbaar bij het gebruik van floating point getallen!
De enige methode die een voorspelbaar resultaat oplevert is om te vergelijken
met een waarde tussen twee stappen in en niet met een grenswaarde. Dus gebruik
in het eerste programma de for
lus:
for (fahrenheit = 31.0; fahrenheit < 32.05; fahrenheit = fahrenheit + 0.1) {
en in het tweede programma de for
lus.
for (fahrenheit = 32.0; fahrenheit < 33.05; fahrenheit = fahrenheit + 0.1) {
Beide programma's zullen nu altijd de tabel helemaal zoals je verwacht printen.
De geschiedenis
van de microprocessor kun je bekijken op:
http://www.computerhistory.org/exhibits/microprocessors/.
De eerste 32 bits Intel processor was de 80386 (1985). Deze processor
kon (in de hardware) alleen met integer getallen werken. Als je met floating
point getallen wilde rekenen dan moesten deze floating point berekeningen
(in de software) worden opgesplitst in integer berekeningen die in de hardware
werden uitgevoerd. Het rekenen met floating point getallen was met de 80386
dus erg traag ook al omdat deze microprocessor werkte op een klokfrequentie
van 16 MHz. Om sneller te kunnen rekenen met floating point getallen was
een speciale chip (de 80387) verkrijgbaar die naast de 80386 op het moederbord
kon worden geplaatst en die wel in staat was om de floating point berekeningen
in hardware uit te voeren. Omdat de meeste PC's in die tijd niet beschikten
over z'n extra rekenchip was het gebruik van
float
in plaats
van double
in die tijd aan te raden omdat de 80386
float
bewerkingen veel sneller kon uitvoeren (in software) dan
double
bewerkingen. Gebruikers die wel een 80387 in hun systeem
hadden konden echter, ook toen al, beter gebruik maken van
double
dan van float
omdat de 80387 (bijna) net
zo snel met double
's kon werken als met float
's.
In 1989 verscheen de 80486 (25 MHz) van Intel die een ingebouwde floating
point unit (FPU) had waarmee floating point bewerkingen in hardware konden
worden uitgevoerd. Vanaf die tijd kun je dus voor floating point berekeningen
beter variabelen van het type double
gebruiken omdat deze
berekeningen (bijna) net zo snel zijn, maar wel 2 maal zo nauwkeurig zijn,
als berekeningen met variabelen van het type float
. Voor de
opslag van grote hoeveelheden floating point getallen waarbij niet zoveel
significante cijfers nodig zijn wordt het type float
nog wel
gebruikt omdat 1 double
net zoveel ruimte inneemt als 2
float
's. Later (in 1991) heeft Intel overigens nog een low cost
variant van de 80486 op de markt gebracht (de zogenaamde 80486SX, de originele
80486 werd toen omgedoopt tot de 80486 DX). Deze 80486SX bleek een 80486DX
met een kapotte FPU te zijn ;-)
Citaat van: http://www.x86.org/articles/computalk/help.htm (link werkt niet meer...)
The 80486 offered little in the way of architectural enhancements over its 80386 predecessor. The most significant enhancement of the 486 family was the integration of the 80387-math coprocessor into the 80486-core logic. Now, all software that requires the math coprocessor could run on the 80486 without any expensive hardware upgrades. Like the 80386 SX, Intel decided to introduce the 80486 SX as a cost-reduced 80486 DX. Unfortunately, Intel chose to ensure that these processors were neither pin-compatible, nor 100% software compatible with each other. Unlike the 80386 SX, the 80486 SX enjoyed the full data bus and address bus of its DX counterpart. Instead, Intel removed the math coprocessor, thereby rendering the 80486 SX somewhat software incompatible with its DX counterpart. To further complicate matters, Intel introduced the 80487 SX -- the "math coprocessor" to the 80486 SX. Intel convinced vendors to include a new socket on the motherboard that could accommodate the 80486 SX and 80487 SX as an expensive hardware upgrade option. Unbeknownst to the consumer, the 80486 SX was an 80486 DX with a non-functional math unit (though later versions of the chip actually removed the math unit). The 80487 SX was a full 80486 DX with a couple of pins relocated on the package -- to prevent consumers from using the cheaper 80486 DX as an upgrade option. In this regard, Intel created a marketing deception. Intel marketed the 80487 SX as a math coprocessor to the 80486 SX. In reality, the 80487 SX electronically disabled the 80486 SX when installed, thereby relegating this chip to the status of an expensive space heater. Sadly, the consumer never knew or even suspected Intel of playing such manipulative games.
Vanaf de Intel Pentium (de opvolger van de 80486) heeft elke Intel processor een (of meer) floating point unit(s) aan boord. De FPU van de Pentium werd zelf beroemd vanwege de zogenaamde Pentium bug. Zie: http://en.wikipedia.org/wiki/Pentium_FDIV_bug