Permalänk
Medlem

Hjälp med ett C-program!

Hej,

Jag har stött på ett problem i ett relativt enkelt C-program och undrar om någon förstår varför det blir så?
Använder boken "Vägen till C", Jan Skanshom (http://skansholm.com/vagen_c/index.html). I uppgift 4, kapitel 2 ska man skriva ett program hur många dagar det tar att tjäna ihop 1 miljon kr, om man får lön 1 öre första dagen, 2 ören andra dagen, 4 ören tredje dagen, 8 ören fjärde dagen osv.

Så här ser koden ut enligt lösningar till övningsuppgifter (se länk ovan):

/* 2_4.c */ #include <stdio.h> #define SLUTSUMMA 1000000.0 int main() { int dag = 0; float sum = 0, lon = 0.01; while (sum < SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.0f kr.\n", dag, SLUTSUMMA); }

När man kör koden får man svaret 27 dagar! Det må vara rätt.

MEN!
Jag testade att ändra lite i koden för att kolla att while-loopen stämmer.
Se nedan:

#include <stdio.h> #define SLUTSUMMA 0.03 /*Hur många dagar tar det att tjäna 3 ören?? */ int main() { int dag = 0; float sum = 0, lon = 0.01; while (sum < SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, SLUTSUMMA); }

Vi kan själv räkna ut att om han får 1 öre första dagen och 2 ören andra dagen så har han totalt 3 ören efter 2 dagar.

Problemet när man kör denna koden är att man får som resultat att han har 3 ören efter 3 dagar.
Varför är det så? Det verkar som att while-satsen utförs en extra gång trots att villkoret sum < SLUTSUMMA inte gäller.

Kan någon snälla förklara var felet är? Har det med att man inte får använda makron som jämförelse i while-satsen?

Tacksam för svar.

Edit: Snyggade till inlägget så att koden syns inramad för tydlighetens skull.
För lösning till problemet, se inlägg #2 samt #10 av Yoshman!

Permalänk
Datavetare

Du har precis lärt dig orsaken varför program som jobbar med pengar aldrig använder flyttal

Talet 0,03 kan inte representeras exakt med den metod din dator använder, således blir jämförelsen som görs i while-loopen inte helt väldefinierad i fallet när något borde vara exakt 0,03.

Ett sätt att lösa problemet är att bara räkna med hela ören för att sedan enbart konvertera till flyttal vid t.ex. presentation.

#include <stdio.h> #define SLUTSUMMA 3 /*Hur många dagar tar det att tjäna 3 ören?? */ static float ore_to_kr(unsigned ore) { return ore / 100.0; } int main() { unsigned dag = 0; unsigned sum = 0, lon = 1; while (sum < SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, ore_to_kr(SLUTSUMMA)); }

Detta är vad ditt program håller i loopens andra varv med flyttal

(gdb) p sum $2 = 0.0299999993 (gdb) p dag $3 = 2

Här ser vi alltså att "sum" inte riktigt är 0,03 utan det är något som är marginellt mindre vilket betyder att "sum < SLUTSUMMA" blir falskt när "dag == 2".

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

@Yoshman:

Tack för utförligt svar, uppskattas!

OK, så det är alltså typen float som ställer till det.
Kan man inte på något sätt säga till att den ska avrunda till 2 decimaler, så att talet 0.03 blir väldefinierat?

Permalänk
Medlem

Du får använda en minor currency. Ta för vana att alltid räkna pengar i den minsta fraktion som kunden kan behöva och använda heltal. I det här fallet kör allting som en int i ören och sköt avrundningar själv.

Visa signatur

8700K 5Ghz | 32GB 3200Mhz | 2080Ti 11GB | Phanteks Enthoo | Asus PG27AQ

Permalänk
Datavetare
Skrivet av jason83:

@Yoshman:

Tack för utförligt svar, uppskattas!

OK, så det är alltså typen float som ställer till det.
Kan man inte på något sätt säga till att den ska avrunda till 2 decimaler, så att talet 0.03 blir väldefinierat?

Problemet är att 0,03 inte kan skrivas exakt som en summa av tal på formen B(1) * 1 / 2 + B(2) * 1 / 4 + ... B(N) * 1 / 2^N. Här är då B(N) endera noll eller ett (om biten i den positionen i datorns flyttalsregister är tänd eller släckt). Det är så din dator i grunden representerar flyttal (har kapat en del detaljer).

Lite samma sak som att 1 / 3 är exakt men det går inte att beskriva det exakt med ett decimaltal, man får som närmast 0,33333...

Sällan ett problem i isolation, men om du gör exakta jämförelser eller än mer om du utför väldigt många beräkningar så kan det lilla avrundningsfel som inträffar vid konverteringen i vissa fall växa sig rätt stor efter ett gäng beräkningssteg om det vill sig illa.

Så att räkna ören med heltal är verkligen exakt, det motsvarar 1 / 3 representationen och sedan matematik med bråk medan flyttal är lite förenklat som att du räknar med decimaltal (inte en hel perfekt jämförelse, men nära nog hoppas jag).

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

@Yoshman: Jag är med på det du skriver.

Testade att ändra lite i koden så att man jämför med en variabel istället för en konstant (makro).
Då funkar det, jag får ut 2 dagar! Tror jag förstår varför, men kan du förklara det?

/* 2_4.c */ #include <stdio.h> /*#define SLUTSUMMA 0.03*/ int main() { int dag = 0; float sum = 0, lon = 0.01, slutsum = 0.03; while (sum < slutsum) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han maste jobba %d dagar for %.2f kr.\n", dag, slutsum); return 0; }

Permalänk
Medlem

@jason83:

Ett gyllene tips just nu är att förklara det du tror. DÅ kan vi/någon peka Precis på vad du missat eller behöver kika vidare på.

Visa signatur

[1700X] [B350 mITX] [32GB DDR4@3000MHz CL15] [Vega 56] [Nano S] [D15S] [Win 10 Pro]

Permalänk
Medlem

@HenKenny:
Bra tanke!

Det jag tror är att man jämför ett "oexakt" värde med samma "oexakta". På så sätt bllir det väldefinierat i while-loopen?
Eller är jag ute och cyklar....?

Permalänk
Medlem

Testa och ändra koden tilfälldigt för att presentera lönen vid varje dag som går. (printat till användaren)

Dag 1, Lönen är ..
Dag 2, Lönen är ..
Dag 3, Lönen är

Visa signatur

[1700X] [B350 mITX] [32GB DDR4@3000MHz CL15] [Vega 56] [Nano S] [D15S] [Win 10 Pro]

Permalänk
Datavetare

@jason83: nu börjar du gräva dig ner i ett väldigt djupt kaninhål

Två saker som är kritiska att förstå här:

1.
När man får en oexakt representation av något är den i hälften av fallen större och i hälften av fallen mindre än det exakta värdet.

Jämför med 1 / 3 som alltid är större än 0,3333... oavsett hur många treor jag smackar på. För 2 / 3 gäller det omvända, blir ju avrundat alltid en sjua i sista positionen 0,6666666667.

2. jämförelser mot en flyttalskonstant kommer inte vara en jämförelse mellan två tal med typen "float", utan det kommer vara en jämförelse mellan "double". När man byter typ finns risk att avrundningsfelet slår åt andra hållet, vilket är precis vad som händer här.

Även detta get "rätt" svar

#include <stdio.h> #define SLUTSUMMA 0.03 /*Hur många dagar tar det att tjäna 3 ören?? */ int main() { int dag = 0; double sum = 0, lon = 0.01; while (sum < SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, SLUTSUMMA); }

Även detta blir "rätt" och är precis det fall du nu har i praktiken

#include <stdio.h> #define SLUTSUMMA 0.03 /*Hur många dagar tar det att tjäna 3 ören?? */ int main() { int dag = 0; float sum = 0, lon = 0.01; while (sum < (float)SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, SLUTSUMMA); }

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

@HenKenny:

Vet inte om jag är med på hur det skulle hjälpa mig, men jag gjorde om koden enligt din beskrivning:

/* 2_4.c */ #include <stdio.h> int main() { int dag = 0; float sum = 0, lon = 0.01, slutsum = 0.03; while (sum < slutsum) { sum = sum + lon; lon = 2*lon; dag++; printf("Dag %d, lonen ar %.2f kr\n", dag, sum); } printf("Han maste jobba %d dagar for %.2f kr.\n", dag, slutsum); return 0; }

Och vid kompilering och körning får jag då ut:

C:\Prg>gcc -Wall -ansi -pedantic 2_4_1.c -o 2_4_1.exe C:\Prg>2_4_1.exe Dag 1, lonen ar 0.01 kr Dag 2, lonen ar 0.03 kr Han maste jobba 2 dagar for 0.03 kr.

Permalänk
Datavetare
Skrivet av jason83:

@HenKenny:

Vet inte om jag är med på hur det skulle hjälpa mig, men jag gjorde om koden enligt din beskrivning:

/* 2_4.c */ #include <stdio.h> int main() { int dag = 0; float sum = 0, lon = 0.01, slutsum = 0.03; while (sum < slutsum) { sum = sum + lon; lon = 2*lon; dag++; printf("Dag %d, lonen ar %.2f kr\n", dag, sum); } printf("Han maste jobba %d dagar for %.2f kr.\n", dag, slutsum); return 0; }

Och vid kompilering och körning får jag då ut:

C:\Prg>gcc -Wall -ansi -pedantic 2_4_1.c -o 2_4_1.exe C:\Prg>2_4_1.exe Dag 1, lonen ar 0.01 kr Dag 2, lonen ar 0.03 kr Han maste jobba 2 dagar for 0.03 kr.

Om du går tillbaka till det ursprungliga programmet, fast lägger in din nya utskrift + tvingar den att använda tio decimaler. De flesta implementationer klipper utskriften vid sex decimaler och nu kanske du tvingar ned det till två med "%.2f" (ser ju inte koden för nya utskriften).

Då får du detta

[kjonsson@ryzen tmp]$ ./foo Dag 1 är lönen 0.0099999998 kr Dag 2 är lönen 0.0299999993 kr Dag 3 är lönen 0.0700000003 kr Han måste jobba 3 dagar för 0.03 kr.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

@jason83: nu börjar du gräva dig ner i ett väldigt djupt kaninhål

Två saker som är kritiska att förstå här:

1.
När man får en oexakt representation av något är den i hälften av fallen större och i hälften av fallen mindre än det exakta värdet.

Jämför med 1 / 3 som alltid är större än 0,3333... oavsett hur många treor jag smackar på. För 2 / 3 gäller det omvända, blir ju avrundat alltid en sjua i sista positionen 0,6666666667.

2. jämförelser mot en flyttalskonstant kommer inte vara en jämförelse mellan två tal med typen "float", utan det kommer vara en jämförelse mellan "double". När man byter typ finns risk att avrundningsfelet slår åt andra hållet, vilket är precis vad som händer här.

Även detta get "rätt" svar

#include <stdio.h> #define SLUTSUMMA 0.03 /*Hur många dagar tar det att tjäna 3 ören?? */ int main() { int dag = 0; double sum = 0, lon = 0.01; while (sum < SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, SLUTSUMMA); }

Även detta blir "rätt" och är precis det fall du nu har i praktiken

#include <stdio.h> #define SLUTSUMMA 0.03 /*Hur många dagar tar det att tjäna 3 ören?? */ int main() { int dag = 0; float sum = 0, lon = 0.01; while (sum < (float)SLUTSUMMA) { sum = sum + lon; lon = 2*lon; dag++; } printf("Han måste jobba %d dagar för %.2f kr.\n", dag, SLUTSUMMA); }

Jag måste säga att jag är imponerad av din kunskap!
Jag ska sluta gräva djupare nu, och nöjer mig med de utförliga svar jag har fått!
Tack!

Permalänk
Medlem

Kontentan är alltså att Janne kan stoppa sin bok där solen inte skiner och att @jason83 istället borde läsa Kernighan och Ritchies The C Programming Language (som finns som PDF på olika håll), i synnerhet så han redan kompilerar sin kod med -ansi -pedantic

Permalänk
Medlem
Skrivet av suzieq:

Kontentan är alltså att Janne kan stoppa sin bok där solen inte skiner och att @jason83 istället borde läsa Kernighan och Ritchies The C Programming Language (som finns som PDF på olika håll), i synnerhet så han redan kompilerar sin kod med -ansi -pedantic

Tack för tipset!

Permalänk
Medlem

En annan sak att tänka på är väl att du har 0kr dag 0,så sätt dag till 1, annars blir det en fel

Skickades från m.sweclockers.com

Permalänk
Hedersmedlem

I flyttalens* förlovade land kan man också få svar på den eviga frågan: om du får välja, skulle du vilja ha

  1. 10 miljoner betalningar av 10 öre

  2. 100 miljoner betalningar av 1 öre, eller

  3. 200 miljoner betalningar av 1 öre?

Vi skriver snabbt ett program för att hjälpa oss:

#include <stdio.h> #include <stdlib.h> #define MILLION 1000000 void sum_floats(int times, float amount) { int i; float sum; for (i = 0, sum = 0; i < times; ++i) sum += amount; printf("%9d repeated additions of %.2f sums to %10.2f.\n", times, amount, sum); } int main(void) { sum_floats(10 * MILLION, .1); sum_floats(100 * MILLION, .01); sum_floats(200 * MILLION, .01); return EXIT_SUCCESS; }

10000000 repeated additions of 0.10 sums to 1087937.00. 100000000 repeated additions of 0.01 sums to 262144.00. 200000000 repeated additions of 0.01 sums to 262144.00.

Alternativ 1, så klart! Vi fick ju till och med lite ränta.

Extrauppgifter:

  1. Varför har summeringen av ettöringar stannat på just den summan? (Känner vi igen talet?)

  2. Var borde en liknande summering av enkronor stanna? Räkna först, testa sedan.

  3. Var borde summeringen av 10-öringar stanna? Räkna först, testa sedan.


*: Här "single precision".

Visa signatur

Nu med kortare användarnamn, men fortfarande bedövande långa inlägg.