Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
Cursus Algoritmiek - - - najaar 2005 Practicumopdracht 4 : werken met ‘kale’ gegevensbestanden 1.
Achtergrond
In de 2e en de 3e practicumopdracht heb je al kennis gemaakt met het via het toetsenbord laten invoeren van gegevenswaarden, om die gegevenswaarden vervolgens door je programma te laten verwerken. Als het om grotere aantallen gegevens gaat, die je eventueel vaker wilt gebruiken, is het noch handig, noch efficiënt om die gegevens telkens opnieuw in te tikken. Het ligt dan voor de hand ze ergens [voor een volgend gebruik] in een bestand op te slaan (net zoals je dat met tekst(verwerkings)documenten en uiteraard je eigen programma-code doet). In deze opdracht bestuderen we het maken en gebruiken van bestanden met gegevens.
2.
Leerdoelen
Na afloop van deze opdracht ben je in staat om: • in je programma’s op een beginnersniveau gebruik te maken van gegevensinvoer via niet alleen het toetsenbord, maar ook vanuit gegevensbestanden op schijf; • aan te geven wat er fout kan gaan bij het openen van, het lezen uit en het schrijven naar bestanden en aangeven hoe je via de bestandsvariabele kunt testen op het wel/niet succesvol verlopen zijn van de laatste bestandsoperatie; • de bestandsvariabele in je programma inderdaad correct te gebruiken; ook voor het uitvoeren van testen op o.a. het correct verlopen van ‘open’- en ‘lees’-operaties. • de in je programma gebruikte gegevens op een zodanige wijze naar een gegevensbestand op schijf weg te schrijven, dat die gegevens daarna door het programma weer vanuit dat gegevensbestand ingelezen en verwerkt kunnen worden.
3.
Instructie
Bestudeer allereerst de op het hoorcollege besproken onderdeel uit het Algoritmiek-dictaat en/of paragraaf 12.1 uit het boek ‘Big C++’, de sheets zoals die gebruikt zijn op het college van deze week (bekijk beslist ook de programma-voorbeelden) en uiteraard je op het hoorcollege gemaakte aanvullende eigen aantekeningen. Voordat je zelf aan de slag moet gaan met het werken met gegevensbestanden, geven we hier eerst een paar voorbeelden van het maken van zulke bestanden en het gebruiken ervan. Zeer vaak zul je bij het (‘eenvoudig’) werken met gegevensbestanden een [fragment van een] programma-structuur gebruiken, waarin je eerst een gegevensbestand opent, vervolgens er òf gegevens uit inleest, òf net gegevens naar toe programma-onderdeel voor schrijft en tot slot het bestand weer werken met een bestand ‘afkoppelt’ (‘sluit’). Indien je de ‘bestandsvariabele’ ? ? ? doorgeeft als parameter, dan moet dat steeds als ‘reference’-parameter open het lees of schrijf gegevens sluit het gebeuren. We geven hiernaast een gegevensbestand uit/naar bestand gegevensbestand schets van een top down-schema (nog zonder dataflow) voor zo’n ? programma-fragment. lees of schrijf één gegevensvaak in herhaling: Zo’n speciale bestandsvariabele zullen regel uit/naar bestand we (om te kunnen werken met gegevensbestanden op schijf) moeten koppelen aan de naam waaronder dat bestand op schijf is gezet. Zo’n bestandsvariabele kun je òf in twéé stappen òf in één stap voorbereiden voor gebruik. Je kunt hem òf eerst declareren + daarna initialiseren (‘openen’), zoals in: ifstream invoerbestand ; invoerbestand.open ( "getallen.dat" ) ;
1
// declaratie (bestandsobject)variabele // aparte initialisatie ‘object’-variabele
Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
òf hem in een klap declareren en initialiseren, zoals in: ifstream invoerbestand ( "getallen.dat" ) ;
// declaratie + object-initialisatie
We geven meestal de voorkeur aan de tweede mogelijkheid, waarbij je desgewenst tussen de haakjes de naam van een variabele mag plaatsen, waar die bestandsnaam in is opgeslagen. Maak voor jezelf steeds een goed onderscheid tussen een bestandsvariabele (verwijzend naar een ‘RAM-geheugenplaats’) en een bestandsnaam (van een bestand op schijf)! Belangrijk is ook dat je (bij het ‘lezen’ uit bestanden) goed doordrongen bent van de noodzaak van het gebruik van een test van de toestand van de bestandsvariabele om te bepalen of je bij het einde van de inhoud van het gegevensbestand bent aangekomen; wat er zich voor complicaties kunnen voordoen en hoe je dan soms met behulp van opnieuw een test op de toestand van de bestandsvariabele toch een correct werkend programma kunt maken.
Voorbeeld a) Het maken van een gegevensbestand met getallen We willen een gegevensbestand maken met daarin de cijfers 1 t/m 8; telkens op afzonderlijke regels, dus nà elk cijfer moet een ‘einde-regel-teken’ toegevoegd worden. Voor het maken (‘schrijven’) van zo’n bestand heb je een ‘ofstream’-bestandsvariabele nodig. Ga nu zelf na, dat je via onderstaande C++-routine inderdaad zo’n bestand kunt aanmaken: void maakGetallenBestand ( ) { ofstream uitvoerbestand ( "getallen.txt" ) ; if ( !uitvoerbestand ) // controle op correct openen cout << "\nBestand kon niet gemaakt worden!\n" ; else { for ( int teller=1 ; teller <=8 ; teller++ ) uitvoerbestand << teller << endl ; uitvoerbestand.close () ; } }
Rechts is getoond, hoe het gegenereerde gegevensbestand eruit ziet als je het onder MS Windows via ‘Kladblok’ opent. N.B. Indien je deze getallen niet telkens op afzonderlijke regels, maar net achter elkaar (op één regel) had willen plaatsen, dan moet je er voor zorgen dat tussen de afzonderlijke getallen een of meer spaties worden geplaatst. Als je dat niet doet, dan komt er in het bestand ‘12345678’ te staan en zal het later onmogelijk zijn de afzonderlijke getallen weer terug in te lezen. Je kunt dat tussenvoegen van zo’n spatie implementeren via een code-fragment als: for ( int teller=1 ; teller <=8 ; teller++ ) uitvoerbestand << teller << “ “ ;
waarbij telkens een of meer spaties achter de actuele waarde van ‘teller’ worden geplaatst, zodat de inhoud van het bestand wordt: ‘1 2 3 4 5 6 7 8 ’. Let er wel op, dat nu pal nà de ‘8’ een of meer spaties staan, dus vóór het ‘einde-bestandsteken’!
Voorbeeld b) Het verwerken van een ‘kaal’ tekstbestand Gegeven het ‘kale’ (d.w.z. zonder tekstopmaak qua lettertype, vetgedrukt e.d.) tekstbestandje “woorden.txt”, met de hiernaast weergegeven inhoud. Ga na wat je als resultaat verwacht als we het volgende programmafragment op dat gegevensbestand los laten; let er daarbij nogmaals specifiek op dat een parameter voor een bestandsvariabele altijd een call-by-reference-parameter moet zijn!
2
Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
bool openStringBestand ( ifstream& bestandsvariabele )
// hier keuze voor ‘bool’ // bestandsvariabele moet altijd call-by-reference parameter zijn!
{ char bestandsnaam [12] ; cout << "\nGeef naam te gebruiken stringbestand: " ; cin >> bestandsnaam ; // hier: "woorden.txt" bestandsvariabele.open ( bestandsnaam ) ; if ( bestandsvariabele ) // indien openen correct verlopen return true ; else { cout << "Bestand niet gevonden; het programma sluit nu af.\n" ; exit(1) ; // oei; noodstop } } void sluitInleesBestandAf ( ifstream& bestand ) { bestand.close ( ) ; } void stringVoorbeeld ( ) // nog te verbeteren versie! // deze procedure telt alle tekens [géén spaties, tabs en einde-regel-tekens] // van een op te geven bestand en toont deze tekens bovendien op het scherm { int aantal_tekens = 0; char teken ; ifstream inleesbestand ; if ( openStringBestand ( inleesbestand )) // indien correct geopend { while ( inleesbestand ) // zolang teken inlezen correct verloopt { inleesbestand >> teken ; cout << teken ; // toon teken op beeldscherm aantal_tekens++ ; // verhoog aantal tekens met 1 } sluitInleesBestandAf ( inleesbestand ) ; } cout << "\nAantal ingelezen tekens: " << aantal_tekens << endl ; }
Opvallend is de uitvoer die dit programmadeel oplevert: Opdezeregelstaanwoordenenerstaanookwoordenopdevolgenderegels.. Aantal ingelezen tekens: 62
Uit die getoonde uitvoer kunnen we het volgende opmaken: • bij het openen van een bestand kun je via de bestandsvariabele testen of dat openen lukte; • bij het lezen van een volgend teken wordt ‘over spaties heen gesprongen’; • idem ‘over einde-regel-tekens’; (N.B. dat gebeurt trouwens ook bij ‘tabs’) • het inlezen van het volgende teken gaat op het einde ‘fout’ bij de punt; achter de punt is blijkbaar nog een ‘einde-regel-teken’ toegevoegd (zie in het plaatje de cursor die op de onderste regel staat te wachten). Daardoor zie je dat erachteraan de zin twéé punten ‘.’ staan afgedrukt (en die verdubbelde punt is ook meegeteld met het ‘aantal_tekens’). •
N.B. bij gebruik van ‘primitievere’ invoeroperaties zoals ‘.get(..)’ doet zich dit probleem niet voor...
De bij het vierde punt vermelde ‘fout’ treedt blijkbaar tegen het einde op omdat na het lezen van de punt ‘.’ de test op de bestandsvariabele inleesbestand nog steeds ‘true’ oplevert (omdat die laatste inleespoging, die die punt ‘.’ inlas, correct verliep). Bij een volgende poging om een teken te lezen lukt dat echter niet meer! Dan slaagt het systeem bij het inlezen van een volgend [nìet-layout-teken] daar niet in; het stuit daarbij op een ‘einde-bestand-teken’ en zo’n inleespoging ‘lukt dan niet’.Vanwege de nu gebruikte code komt er geen nieuwe inhoud in de variabele ‘teken’ en blijft de oude inhoud (die ‘.’) gehandhaafd en opnieuw getoond en een tweede keer meegeteld.
3
Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
We kunnen nu opnieuw gebruik maken van de in C++ bestaande test op de toestand van de bestandsvariabele, die [ook] ‘false’ oplevert als er [zoals in dit beschreven geval] iets fout gaat bij het inlezen van een waarde. De verbeterde C++-code is daarom: void stringVoorbeeld ( ) // verbeterde versie // deze procedure telt alle tekens [géén spaties, tabs en einde-regel-tekens] // van een op te geven bestand en toont deze tekens bovendien op het scherm { int aantal_tekens = 0; char teken ; ifstream inleesbestand ; if ( openStringBestand ( inleesbestand )) // indien correct geopend { while ( inleesbestand ) // zolang inlezen correct verloopt { inleesbestand >> teken ; /*=>*/ if ( inleesbestand ) // levert 'false' bij einde bestandsteken { cout << teken ; aantal_tekens++ ; } } sluitInleesBestandAf ( inleesbestand ) ; } cout << "\nAantal ingelezen tekens: " << aantal_tekens << endl; }
Deze verbeterde versie levert als (correct) resultaat op: Opdezeregelstaanwoordenenerstaanookwoordenopdevolgenderegels. Aantal ingelezen tekens: 61
Voorbeeld c) Het (weer) inlezen van het gegevensbestand met getallen We gaan het hiervoor gemaakte gegevensbestand gebruiken om de daarin opgeslagen gegevenswaarden ‘terug’ te lezen en in een programma te gebruiken. Ga nu zelf na, dat je via onderstaande C++-routine inderdaad de gegevens uit zo’n bestand weer kunt ‘inlezen’, waarbij de vraag is (zie ook de getoonde figuur bij dit bestand) of alles goed gaat: void leesEnVerwerkEenGetal ( ifstream& inleesbestand, int& aantal, int& som ) { int getal ; inleesbestand >> getal ; // wordt later nog verbeterd! cout << getal << " " ; aantal++ ; som = som + getal ; } void getallenVoorbeeld ( ) { int som=0, aantal=0; ifstream inleesbestand ("getallen.txt" );
// N.B. mag ook in 2 stappen…
while ( inleesbestand ) // zolang [openen en] inlezen correct gebeurt { leesEnVerwerkEenGetal ( inleesbestand, aantal, som ) ; } sluitInleesBestandAf ( inleesbestand ) ; // voor code hiervan: zie eerder cout << "\nAantal getallen: " << aantal << "; hun somwaarde is: " << som << endl ; }
Het resultaat ziet er als volgt uit: 1 2 3 4 5 6 7 8 -858993460 Aantal getallen: 9; hun somwaarde is: -858993424
4
Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
En weer blijkt er iets te zijn fout gegaan na het inlezen van het échte laatste getal (die 8); de volgende leespoging van een getal mislukt, omdat er [na een ‘einde-regel-teken’] een einde-bestand-teken aangetroffen werd en géén volgend getal meer. De remedie voor het voorkómen van dit foutieve gedrag is ook hier weer het eerst uitvoeren van een test op de toestand van de bestandsvariabele: void leesEnVerwerkEenGetal ( ifstream& inleesbestand, int& aantal, int& som ) { // verbeterde versie int getal ; inleesbestand >> getal ; /*=>*/ if ( inleesbestand ) // alleen als inleespoging gelukt is; { // d.w.z. indien nog niet op einde-bestandsteken cout << getal << " " ; aantal++ ; som = som + getal ; } }
Met nu als correct resultaat op het scherm: 12345678 Aantal getallen: 8; hun somwaarde is: 36
En dan nu: een beschrijving van de echte practicumopdracht De door jezèlf uit te werken practicumopdracht gaat over de volgende situatie, waarbij je aan de slag gaat met een ‘kaal’ (zonder opmaakcodes) gegevensbestand, waarvan we de inhoud hiernaast in een plaatje met een ‘Kladblok’-window tonen. Je ziet als inhoud van elke regel achtereenvolgens: • een voornaam van een persoon (maximaal 6 tekens lang) • vervolgens [na een aantal spaties] een eerste aantallen-waarde, waarmee aangegeven wordt, hoeveel cijfers (b.v. practicumcijfers voor de Algoritmiek-cursus) voor die persoon geregistreerd staan • en tenslotte staan er op die regel inderdaad nog zoveel ‘cijfers’. Als je van tevoren een ‘char voornaam [7] ; ’ declareert, dan kun je zo’n bestandsregel inlezen, door eerst te proberen vanuit het bestand zo’n naam in te lezen (je kunt immers uit de figuur opmaken, dat na de laatste gegevensregel voor ‘Xander’ blijkbaar nog een <einde-regelteken> staat) en (als dat inlezen van een naam gelukt is) dat ‘aantal cijfers’ in te lezen en pas daarna in een herhaling dat ‘aantal’ afzonderlijke cijfers in te lezen. Je vindt dit bestand als ‘hulpbestand’ op de cursus-webpagina in “Gegevensbestanden.zip”.
1) De bedoeling van het eerste deel van deze practicumopdracht is nu, dat je een programma ontwerpt en realiseert, waarmee je de gegevens uit dat bestand op een correcte manier inleest en voor elke persoon de gemiddelde waarde van de opgegeven cijfers laat bepalen en zien. Het verkregen resultaat moet er als volgt uitzien: Naam Els John Karin Truus Xander
Aantal 4 2 0 5 3
Gemiddeld 7.75 7 // bij ‘0’ cijfers valt geen gemiddelde te bepalen! 6.2 7
Aantal aangetroffen gegevensregels: 5
Hint: door in de uitvoer naar het beeldscherm (of naar een bestand) de ‘escape sequence’ “\t” op te nemen, zal een volgend uitvoerdeel een ‘tab’-positie verderop terecht komen (bij een latere ‘leesopdracht’ van gegevens uit zo’n bestand, zal net als ‘over spaties en <einde-regeltekens>’ ook ‘over
’s heen’ gelezen worden).
2) In het tweede deel van deze practicumopdracht moet je de hierboven bepaalde gemiddelden (vooraf gegaan door voornaam + gevonden aantal; verder zònder titel/tabelkopjes) naar een uitvoerbestand ‘Gemiddelden.txt’ wegschrijven om ze daarin (‘tijdelijk’) te bewaren. N.B. Als je dit gedaan hebt, bekijk dan eerst eens goed (via b.v. ‘Kladblok’) hoe de inhoud van dat aangemaakte ‘Gemiddelden.txt’ -bestand eruit ziet en verander zo nodig de code waarmee je het in het vervolg zult aanmaken.
5
Cursus ‘Algoritmiek’ [Inleiding programmeren voor Informatiekundigen];
Practicumopdracht 4
3) Tenslotte gebruik je in het derde deel van deze opdracht dat ‘Gemiddelden.txt’ -bestand weer om statistische overall waarden voor ‘totaal aantal cijfers’ en ‘overall gemiddelde’ te bepalen. Uiteraard zul je daarvoor een ‘gewogen’ gemiddelde moeten bepalen door bijv. de bijdrage van Els aan het totaal op 4 * 7.75 = 31.0 te laten berekenen. De uitvoer naar (alleen het beeldscherm) kan er daarbij als volgt uitzien: Totaal aantal cijfers: 14 Totaal som der cijfers: 97 Overall gemiddelde: 6.92857
4.
Producten
Als producten moet je steeds top down-schema’s+dataflow en daarop gebaseerde C++-code hebben, die ervoor zorgt dat je programma voldoet aan de hieronder gegeven specificaties en waarbij je uitwerking voldoet aan de kwaliteitscriteria zoals die gesteld worden (zie bij punt 5. Zelfreflectie). Opdrachtonderdelen: a) Maak allereerst (werk efficiënt; het mag met potlood en papier!) een top down-schema voor het eerste onderdeel, waarbij de gegevens uit het ‘uitslagen.txt’-bestand ingelezen moeten worden en waarna regel-voor-regel de naam, het aantal cijfers en [indien mogelijk] de gemiddelde cijferwaarde op het scherm worden gezet. Geef in het schema de dataflow aan, die nodig is voor deze uitwerking. b) Realiseer je ontwerp in een C++-programma en test het uit. c) Pas je top down-schema aan voor de uitbreiding zoals beschreven in ‘onderdeel 2’ waardoor de hiervoor verkregen resultaten niet alleen op het scherm worden gezet, maar ook naar een bestand (’gemiddelden.txt’)worden weggeschreven. Geef in je schema de verschillende onderdelen aan en de communicatie tussen de programma-onderdelen (parameters met zinvolle namen e.d.). d) Realiseer ook je uitgebreide ontwerp in een C++-programma en test het uit. Vergeet beslist niet om met behulp van bijv. het programma ‘Kladblok’ te controleren of het gemaakte ’gemiddelden.txt’-bestand op een correcte manier is opgebouwd, zodat de inhoud ervan bij onderdeel 3 weer gemakkelijk kan worden ingelezen! e) Breid je top down-schema zodanig uit, dat [zoals beschreven in ‘onderdeel 3’] gebruik gemaakt wordt van het eerder gegenereerde ’gemiddelden.txt’-bestand om de gegevens daaruit weer ‘terug te lezen’ en om daarmee o.a. het gevraagde ‘overall gemiddelde’ te bepalen, zoals eerder geschetst. f) Realiseer ook nu weer je uitgebreide ontwerp in een C++-programma en test het uit.
5.
Zelfreflectie
Via de Algoritmiek-webpagina (of via www.cs.kun.nl/~sjakie/pi-info/algemeen/criteria.html) kom je uit op een aantal criteria die we gebruiken voor de kwaliteit van je ingeleverde werk. Die criteria liggen o.a. op het gebied van naamgeving, layout, structuur, programmeerstijl, aanpak, realisering, en een juist gebruik van taalprimitieven. Ga voor jezelf na [voor zover dat nu al van toepassing is] in hoeverre je uitwerkingen voldoen aan deze criteria; je hoeft je conclusie hierover niet in te leveren!
=> Inleveren van je producten:
vóór maandag 24 oktober, 13:45 uur, en
wel door van elk onderdeel je (uiteindelijke!) top down-schema+dataflow en de uiteindelijke inhoud van je eigen [totale] .cpp-bestand [ruim] op tijd op te sturen naar [email protected] , allen als ‘losse’ attached files. Plaats in de email-subjectregel de aanduiding ‘Algoritmiek opdracht 4’. Plaats duidelijk de namen en studentnummers van zowel jezelf als je practicumpartner in alle meegestuurde bestanden, maar ook jullie studierichting en nogmaals het nummer van deze practicumopdracht. Doe dit plaatsen van namen + studentnrs + ‘Algoritmiek opdracht 4’ óók in het ‘message-deel’ van je email. Plaats weer bij het inleveren van deze (/elke) opdracht in de body van je ‘aanbiedingsemail’ bovendien de volgende informatie (als je in een groepje werkt, dan graag per persoon deze gegevens!): Naam persoon 1: . . . . . . Naam 2: . . . . . . . Tijd aan voorbereidend college ( 0 - 2 uren): …… u …… u Tijd aan bijbehorend werkcollege ( 0 - 1 uur): …… u …… u Tijd aan voorbereiding opdracht: …… u …… u Tijd (aan uitwerking) tijdens [begeleid] practicum: …… u …… u Tijd (aan uitwerking) buiten practicumtijden: …… u …… u
6