B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen 2 Dynamische variabelen (bereikbaar via ‘pointers’) 2.1 Inleiding In het algemeen willen wij zo min mogelijk te maken hebben met de hardware van een specifieke computer (o.a. omdat die zo snel verandert). Toch is het voor het begrip noodzakelijk op de hoogte te zijn van een aantal elementaire zaken. Tegenwoordig heeft ook een PC megabytes werkgeheugen. Vaak 1 MB wordt dit geheugen aangeduid met RAM. RAM staat voor Random Acces Memory. Dit betekent dat het geheugen willekeurig 640 kB toegankelijk is. Als de processor een bepaald geheugenadres op de o.a. MSDOS-deel adresbus zet, krijgt hij toegang tot een geheugenwoord. Dit (RAM) geheugenwoord kan de processor zowel lezen als schrijven. Dit Heap ‘vrije geheugen’ hangt af van de soort instructie die de computer moet uitvoeren. Alleen toegankelijk Gelukkig programmeren wij in een hogere programmeertaal en via ‘dynamische variabelen’ hoeven niet zoals een assembly programmeur van alle details op de (met ‘pointers’) “Heap” hoogte te zijn. Wanneer wij de variabele getal van het type REAL declareren dan zorgt de compiler ervoor dat er op een geschikte Stacksegment Stack adres in het werkgeheugen 6 bytes ruimte wordt gereserveerd voor Default: <=16 kB tbv lokale variabelen deze variabele. Nu kunnen wij een realwaarde toekennen aan getal Maximaal: 64 kB en value-parameter en bewerkingen uitvoeren met getal. Hierbij is getal een surrogaat Globale variabelen <= 64 kB voor de waarde. De variabele getal vervult dezelfde rol als x of y in Getypeerde Datasegment de middelbare school wiskunde. Een opdracht als write(getal) constanten schrijft de huidige waarde van getal naar het scherm. <= 64 kB Unit 1 Een erfenis uit het eerste begin van het MSDOS-tijdperk is de “image” beperktheid tot 64 kiloBytes van het zogenaamde ‘datasegment’, Unit 2 van waar de waarden van de in de B1-cursus behandelde ‘statische .EXE-file Unit . . variabelen’ in werden geplaatst. Zeeën van geheugenruimte bleven op zo’n manier onbenut. In de vroege MSDOS-tijd betekende die (hoofd) programma onbenutte ‘zee van ruimte’ het RAM-geheugen tot maximaal 640 256 Bytes kB. De sleutel tot die vergrootte benutting van ruimte, was het MSDOS-deel(tje) gebruik van zogenaamde dynamische variabelen in ‘de Heap’. Daarvoor was het gebruik van pointers (zie volgende paragraaf) Figure 1 Geheugengebruik onder MSDOS nodig. In de MSDOS-tijd kon door toepassing van wat truukjes (lees: hulprogramma’s) de benutting van het RAMgeheugen worden uitgebreid; bij gebruik van het Windows-besturingssysteem zijn die ‘truukjes’ niet nodig. 2.2 Pointers Bij programmeertalen als Pascal is het mogelijk om in dat relatief kleine datasegment (64 kB) verwijzingen (van telkens b.v. slechts 4 bytes grootte) op te nemen naar veel grotere datastructuren op de Heap. Zo’n verwijzing bestaat eigenlijk uit het adres van de geheugenplaats waar de ‘echte gegevens’ staan opgeslagen.1 Maar wat moet echter het type van zo’n verwijzing zijn? Pascal beschikt over een geschikt type hiervoor: het Pointer type. Het woord zegt het al, het is Pointer een aanwijzer. De waarde van pointer wijst naar Gegeven het adres van getal. Pascal is echter een strikt In ‘datasegment’ getypeerde taal en normaal neemt Pascal geen Op ‘Heap’ genoegen met een algemeen type Pointer. De waarde van een pointer is immers een Figure 2 Pointer in ' Datasegment' naar variabele in ‘Heap’ 1 We kunnen, als we dat willen, het geheugenadres waar een variabele (neem:) getal staat opvragen. Hiervoor beschikt Borland Pascal over de operator @. De opdracht: getaladreswijzer := @getal kent aan getaladreswijzer het geheugenadres van de variabele ‘getal’ toe, dit is een 4 bytes adreswaarde (vaak hexadecimaal aangegeven). 7 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen geheugenadres en op dit geheugenadres kan van alles staan. Om fouten door slordigheid met pointers te vermijden verwacht Pascal pointers van een bepaald type. Bij compilatie wordt ook voor wat betreft pointers een type check doorgevoerd. Zo is een pointer naar een INTEGER van een geheel ander type dan een pointer naar een STRING. De declaratie van een (getypeerde) pointer in Pascal gaat als volgt: VAR getal_ptr : ^INTEGER ; Lees dit als: ‘er is een variabele van het pointertype (4 bytes) die alleen naar een INTEGER (2 bytes) mag wijzen’. 2 De procedure New ( VAR P : Pointer ) maakt een nieuwe dynamische variabele en zet een pointer die naar de locatie van die variabele wijst. De opdracht: New ( getal_ptr ) ; reserveert 2 bytes (correcter: 8) geheugen voor de INTEGER en zorgt ervoor dat getal_ptr hier naar wijst. Dat de procedure New twee bytes geheugen moest reserveren kon afgeleid worden uit het pointertype. De opdracht: Dispose ( getal_ptr ) ; geeft het voor de variabele gereserveerde geheugen (2 bytes; of dus beter: 8 bytes!) weer vrij; de getal_ptr pointer zal ‘ergens’ naar blijven wijzen! Onbegrip ontstaat wanneer je niet beseft dat er steeds twee verschillende dingen gebeuren, nl. een pointer krijgt een adreswaarde toegewezen en er wordt geheugen gereserveerd of vrijgegeven.3 Om te kijken of je het begrijpt, bespreken we onderstaand programmafragment: VAR Tekstwijzer : ^STRING ; Regel : STRING; BEGIN Tekstwijzer := NIL ; Regel := ‘Hatsekidee, daar gaan we dan’ ; Tekstwijzer := @Regel ; Writeln ( ‘De variabele ‘’Regel’’ heeft als inhoud: ‘, Regel ) ; Writeln ( ‘‘’Tekstwijzer’’ verwijst naar de tekst: ‘, Tekstwijzer^ ) END. In dit programmavoorbeeld zullen beide Writeln-opdrachten dezelfde tekst (‘Hatsekidee, ...’) tonen. Regel is een gedeclareerde STRING -variabele waarvoor is de dataspace (‘het datasegment’) van het programma door de compiler 256 Bytes geheugen zijn gereserveerd. In eerste instantie hebben we op het moment van declaratie aan de variabele Tekstwijzer ‘alleen’ wat ruimte (4 bytes) gereserveerd. In de eerste toekenning ( := NIL) geven we aan, dat die Tekstwijzer beslist ‘nergens’ naar toe wijst. Vervolgens kennen we aan de variabele Regel de tekstwaarde ‘Hatsekidee, ... ’ toe. Na de laatste toekenning aan Tekstwijzer, wordt nu het geheugenadres van waar Regel in het geheugen zit, aan de pointer Tekstwijzer toegekend; deze pointer verwijst nu naar een geheugenplaats waar de bekende tekst staat. We hebben de procedure New niet gebruikt (voor het creëren van ruimte voor de Tekstwijzer) omdat we die lieten gaan wijzen naar de variabele Regel, die al bestond. Regel is een statische variabele, waarvoor in dit programma-fragment in de dataspace van het programma door de compiler al ruimte was gereserveerd. Pointers worden vooral gebruikt met dynamische variabelen. Dit zijn variabelen waarvoor niet door de compiler, maar tijdens de uitvoering van het programma geheugen wordt ingeruimd en vaak na een aantal bewerkingen ook weer wordt vrijgemaakt. Deze dynamische variabelen worden niet in de dataspace van het programma geplaatst maar op de Heap. De Heap is doorgaans vele malen groter dan de dataspace. We zullen later zien, dat ‘objecten’ typisch dit dynamische gedrag vertonen, ze worden gemaakt, bestaan een tijdje en gaan weer verloren (lees: ze worden ‘opgeruimd’). 2.2.1 Geheugenallocatie via ‘New’ De procedure New (…) maakt op de heap een nieuwe dynamische variabele, reserveert daarbij zoveel bytes geheugen als voor dat datatype nodig is, en zet een pointer die naar de locatie van die variabele wijst. 2.2.2 Geheugen vrijgeven via ‘Dispose’ Indien eerst geheugen ‘gealloceerd’ is met behulp van ‘ New (…)’, kan dat heap-geheugendeel later weer vrijgegeven worden via ‘Dispose (…)’. 2 Het reserveren van benodigde ruimte gebeurt steeds in blokjes van 8 bytes; ook voor een INTEGER worden dus 8 bytes op de Heap gereserveerd. 3 Een veel voorkomende fout is te vergeten om een pointer op Nil te initialiseren, waardoor later niet meer gecontroleerd kan worden of de aangetroffen adres/pointerwaarde naar een gealloceerd stuk geheugen op de Heap wijst. 8 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen 2.3 Kleine voorbeelden met dynamische variabelen Om het gebruik de procedure New te illustreren behandelen we eerst een aantal kleinere voorbeelden. Daarbij besteden we vooral aandacht aan: • het werken met pointers en dynamische variabelen, • het gebruik van typedeclaraties voor samengestelde datatypen, zoals records en arrays 2.3.1 Een dynamische ‘losse’ getalsvariabele en een dynamische STRING-variabele program dynamische_variabelen; Uses Crt; {onder Windows: WinCrt} TYPE GETALWIJZER = ^INTEGER ; STRINGWIJZER = ^STRING ; VAR GWijzer SWijzer TussenVrij : GETALWIJZER ; : STRINGWIJZER ; : LONGINT ; PROCEDURE BepaalVrijGeheugen ( message: STRING; VAR NuVrij: LONGINT ) ; BEGIN writeln('Door ', message, ' verminderd met: ', NuVrij - MemAvail, ‘ bytes’ ); NuVrij := MemAvail END; BEGIN TussenVrij := MemAvail ; writeln ('We starten met aantal bytes vrij geheugen: ', TussenVrij); GWijzer := NIL ; SWijzer := NIL ; BepaalVrijGeheugen ( '2x NIL-maken', TussenVrij ); New ( GWijzer ); BepaalVrijGeheugen ( 'New(GWijzer)', TussenVrij ); New ( SWijzer ); BepaalVrijGeheugen ( 'New(SWijzer)', TussenVrij ); GWijzer^ := 12345 ; BepaalVrijGeheugen ( 'GWijzer^:=12345', TussenVrij ); SWijzer^ := 'Hatsekidee'; BepaalVrijGeheugen ( 'SWijzer^:=''Hatsekidee''', TussenVrij ); Dispose ( GWijzer ); BepaalVrijGeheugen ( 'Dispose(GWijzer)', TussenVrij ); Dispose ( SWijzer ); BepaalVrijGeheugen ( 'Dispose(SWijzer)', TussenVrij ); writeln ('We eindigen met aantal bytes vrij geheugen: ', MemAvail ) END. Kun je goed het effect op het heap-geheugendeel nagaan van de operatie New, toekenningen van enerzijds ‘NIL’ en anderzijds een ‘echte’ waarde aan dynamische variabelen en tot slot van de operatie Dispose. De uitvoer van dit programma naar het beeldscherm is: a) Onder Turbo/Borland Pascal voor MSDOS: We starten met aantal bytes vrij geheugen: 603296 Door 2x NIL-maken verminderd met: 0 bytes Door New(GWijzer) verminderd met: 8 bytes Door New(SWijzer) verminderd met: 256 bytes Door GWijzer^:=12345 verminderd met: 0 bytes Door SWijzer^:='Hatsekidee' verminderd met: 0 bytes Door Dispose(GWijzer) verminderd met: -8 bytes Door Dispose(SWijzer) verminderd met: -256 bytes We eindigen met aantal bytes vrij geheugen: 603296 N.B. ook bij gebruik van REAL-waarden, net als bij INTEGERS, wordt door New(Gwijzer) dus 8 bytes geallocceerd! b) Onder Borland Pascal voor Windows krijgen we (helaas) een niet zo mooi overzicht … : We starten met aantal bytes vrij geheugen: 33067008 Door 2x NIL-maken verminderd met: 0 bytes Door New(GWijzer) verminderd met: -8176 bytes Door New(SWijzer) verminderd met: 256 bytes Door GWijzer^:=12345 verminderd met: 0 bytes Door SWijzer^:='Hatsekidee' verminderd met: 0 bytes Door Dispose(GWijzer) verminderd met: -4 bytes Door Dispose(SWijzer) verminderd met: 7924 bytes We eindigen met aantal bytes vrij geheugen: 33067008 (Een wat vreemd rijtje getallen, maar een aantal conclusies kun je er toch wel aan verbinden.) 9 B2: Algoritmiek, Datastructuren en Objectprogrammeren 2.3.2 Dynamische variabelen Een (statische) RIJ met pointers naar (dynamische) STRINGs We kunnen de uit de B1-cursus bekende Rij-datastructuur (ARRAY [...] OF ... ) ook gebruiken om niet ‘rechtstreeks’ statische waarden in op te slaan (zoals bij een ARRAY [...] OF STRING ; of een rij van een of ander RECORD-soort), maar om pointers naar een bepaald datatype in op te slaan. Voordelen daarvan zijn o.a.: - je hebt meer geheugen beschikbaar voor gegevenswaarden (in het volledige datasegment van 64 K kun je bijvoorbeeld maximaal 250 STRING’s kwijt ....) ; - bij verwisselingsoperaties (zoals bij sorteren) hoef je niet de waarden van b.v. gehele RECORD’s met elkaar te verwisselen, maar is het voldoende de pointers naar die waarden/RECORD’s te verwisselen (en van 2 adreswaarden van bijvoorbeeld elk 4 bytes gaat het verwisselen heel wat sneller dan dat van de inhoud van 2 STRING’s van elk 256 bytes.) Een voorbeeld: program rij_met_dynamische_variabelen; Uses Crt; {onder Windows: WinCrt} CONST Max_Aantal = 2000 ; TYPE STRINGWIJZER = ^STRING; DYNAMISCHE_STRING_RIJ = ARRAY [ 1 .. Max_Aantal ] OF STRINGWIJZER ; VAR SWijzer : STRINGWIJZER ; MijnRij : DYNAMISCHE_STRING_RIJ ; TussenVrij : LONGINT ; Teller : INTEGER ; PROCEDURE BepaalVrijGeheugen ( message: STRING; VAR NuVrij: LONGINT ) ; BEGIN writeln('Door ', message, ' verminderd met: ', NuVrij - MemAvail, ' bytes' ); NuVrij := MemAvail END; BEGIN TussenVrij := MemAvail ; writeln ('We starten met aantal bytes vrij geheugen: ', TussenVrij); FOR Teller := 1 TO Max_Aantal DO MijnRij [ Teller ] := NIL ; BepaalVrijGeheugen ( 'alles NIL-maken', TussenVrij ); FOR Teller := 1 TO Max_Aantal DO New ( MijnRij [ Teller ]) ; BepaalVrijGeheugen ( 'alle New(MijnRij[Teller])', TussenVrij ); FOR Teller := 1 TO Max_Aantal DO MijnRij [ Teller ]^ := 'Hatsekidee' ; BepaalVrijGeheugen ( 'SWijzer^:=''Hatsekidee''', TussenVrij ); FOR Teller := 1 TO Max_Aantal DO Dispose( MijnRij [ Teller ]) ; BepaalVrijGeheugen ( 'Dispose(MijnRij[Teller])', TussenVrij ); writeln ('We eindigen met aantal bytes vrij geheugen: ', MemAvail ) END. Weer kun je goed het effect op het Heap-geheugendeel nagaan van de operatie New, toekenningen van enerzijds ‘NIL’ en anderzijds een ‘echte’ waarde aan dynamische variabelen en tot slot van de operatie Dispose. De uitvoer van dit programma naar het beeldscherm is: a) Onder Turbo/Borland Pascal voor MSDOS: We starten met aantal bytes vrij geheugen: 595312 Door alles NIL-maken verminderd met: 0 bytes Door alle New(MijnRij[Teller]) verminderd met: 512000 bytes Door SWijzer^:='Hatsekidee' verminderd met: 0 bytes Door Dispose(MijnRij[Teller]) verminderd met: -512000 bytes We eindigen met aantal bytes vrij geheugen: 595312 Duidelijk is, dat die 2000 Strings een halve MegaByte aan heap-geheugen innemen; in het normale MSDOS ‘data-segment’ van 64 kB zouden ze nooit ‘gepast’ hebben. b) Onder Borland Pascal voor Windows krijgen we weer niet zo’n mooi overzicht … : We starten met aantal bytes vrij geheugen: 33067008 Door alles NIL-maken verminderd met: Door alle New(MijnRij[Teller]) verminderd met: -19700 Door SWijzer^:='Hatsekidee' verminderd met: Door Dispose(MijnRij[Teller]) verminderd met: 19700 We eindigen met aantal bytes vrij geheugen: 33067008 0 bytes bytes 0 bytes bytes (Wederom een vreemd rijtje getallen, maar je conclusies kun je er toch wel uit trekken.) 10 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen 2.4 ‘Pointer’-operaties Let op het totaal verschillende effect van de volgende ‘pogingen’ om ‘pointer’ -waarden te veranderen: Effect van: P1 := P2 2.4.1 Het effect van Pointer1 := Pointer2 Het effect is, dat pointer Pointer1 de waarde krijgt van het geheugenadres waar ook Pointer2 naar wijst. Met andere woorden: Pointer1 en Pointer2 wijzen na afloop van deze toekenning naar dezelfde geheugenlocatie! Een onplezierig bijeffect is, dat nergens meer bekend is waar de ‘oude waarde’ van P1 ergens zit en dat die geheugenlocatie niet meer via ‘Dispose(..) kan worden vrijgemaakt. Vóór verwisseling Ná verwisseling 3 P1 3 P1 A P2 4 A P2 4 B B Effect van: P1^ := P2^ 2.4.2 Het effect van Pointer1^ := Pointer2^ Vóór verwisseling Het effect van deze operatie is, dat de geheugenlocatie waar Pointer1^ naar wijst, dezelfde waarde zal krijgen als de geheugenlocatie waar Pointer2^ naar wijst. P1 4 P1 A P2 2.4.3 Ná verwisseling 3 Het verwisselen van pointer-waarden 4 B P2 B Ga voor jezelf in het volgende voorbeeld het effect na van de procedures Verwissel1 en Verwissel2: program verwisselen_waarden_dynamische_variabelen; TYPE COMBINATIE = RECORD Getal : INTEGER ; Teken : CHAR END ; COMBINATIEWIJZER = ^COMBINATIE ; PROCEDURE Toon ( Wijzer : COMBINATIEWIJZER ) ; BEGIN write ( Wijzer^.Getal, Wijzer^.Teken, ' ') END ; PROCEDURE Verwissel1 ( VAR Wijzer1, Wijzer2 : COMBINATIEWIJZER) ; VAR Hulp : COMBINATIEWIJZER ; BEGIN Hulp := Wijzer1 ; { pointerwaarden verwisselen } Wijzer1 := Wijzer2 ; Wijzer2 := Hulp END ; PROCEDURE Verwissel2 ( VAR Comb1, Comb2 : COMBINATIE ) ; VAR Hulp : COMBINATIE ; BEGIN Hulp := Comb1 ; { record-inhoud verwisselen } Comb1 := Comb2 ; Comb2 := Hulp END ; VAR Wijzer1, Wijzer2 : COMBINATIEWIJZER ; BEGIN Writeln ( 'MemAvail = ', MemAvail ); New ( Wijzer1 ) ; Wijzer1^.Getal := 3 ; Wijzer1^.Teken := 'A' ; New ( Wijzer2 ) ; WITH Wijzer2^ DO BEGIN Getal := 4 ; Teken := 'B' Toon ( Wijzer1 ) ; Toon ( Wijzer2 ) ; Verwissel1 ( Wijzer1, Wijzer2 ) ; Toon ( Wijzer1 ) ; Toon ( Wijzer2 ) ; Verwissel2 ( Wijzer1^, Wijzer2^ ) ; Toon ( Wijzer1 ) ; Toon ( Wijzer2 ) ; 11 END; 4 B B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen Dispose ( Wijzer1 ) ; Dispose ( Wijzer2 ) ; Writeln;Writeln ( 'MemAvail = ', MemAvail ) END. Het getoonde resultaat op het beeldscherm (in een MSDOS-box) is: MemAvail = 535040 3A 4B 4B 3A 3A 4B MemAvail = 535040 Het effect van de verschillende Verwisselx-procedures blijkt (zoals verwacht) hetzelfde te zijn. Een voordeel van het verwisselen van de adres-waarden in de pointers (dus met Verwissel1) is, dat dan slechts 3 waarden van elk 4 bytes verwisseld hoeven te worden, terwijl bij het verwisselen van de waarden op de aangewezen geheugenlocaties er 3x het totaal aantal bytes van het gehele te verwisselen record versleept moet worden (toevallig is dat in voorgaand voorbeeld weinig, maae als het b.v. over ‘persoonsrecords’ met b.v. 200 bytes grootte gaat, gaat het verwisselen van de pointerwaarden veel sneller dan het verwisselen van de recordinhouden zelf. Met behulp van zo’n Verwissel-procedure is het uiteraard mogelijk om een een sorteer-proces toe te passen op een rij van pointers naar (elk) een record. 2.5 Een uitgebreid voorbeeld: Studentenadministratie Als een uitgebreider voorbeeld van het werken met dynamische variabelen (lees: pointers) maken we een eenvoudig administratief programma voor het opslaan van studentengegevens bij een cursus. We besteden daarbij speciale aandacht aan: • het werken met pointers en dynamische variabelen, • het gebruik van typedeclaraties voor samengestelde datatypen, zoals records en arrays • de omgang met typed files, • het werken met Units. In het te ontwikkelen programma moeten van elke cursist de volgende gegevens worden opgenomen: • studentnummer • naam • studierichting • practicumcijfer • tentamencijfer • cursuscijfer Dit leidt tot de volgende typedeclaraties: TYPE NaamStr = STRING [ NaamLgt ]; StudieStr = STRING [ StudieLgt ]; PSTUDENT = ^RSTUDENT; {N.B. naar nog ongedefinieerd recordtype ‘mag’! } RSTUDENT = RECORD nummer : LongInt; naam : NaamStr; studierichting : StudieStr; prakticumcijfer, tentamencijfer : REAL ; cursuscijfer : INTEGER ; END; FStudent: FILE OF RSTUDENT ; 12 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen De gegevens worden als dynamische recordvariabelen op de Heap gezet. Als globale variabelen worden lijsten met pointers gebruikt. Er moeten drie lijsten komen, die naar de studentenrecords wijzen op resp. naamvolgorde, nummervolgorde en studierichting. De declaratie van het lijsttype luidt: D yn am isch e Heap space ALijst = ARRAY[1..MaxAantal] of PSTUDENT; en er zijn drie globale variabelen van dit type gedeclareerd: VAR Statisch e dataspace n aam lijst naamlijst, nummerlijst, studielijst : ALijst; Poin ter 1 Poin ter 2 Alle drie deze ALijst-variabelen kunnen geïnitialiseerd worden met behulp van de volgende procedure: procedure InitLijst ( VAR Lst : ALijst ); VAR i : INTEGER ; BEGIN FOR i := 1 TO MaxAantal DO Lst[i] := NIL; END {InitLijst}; Als je dat zou willen, zou je bijvoorbeeld de naamlijst – variabele kunnen initialiseren (alle pointers in die rij op NIL zetten) via de aanroep: InitLijst( naamlijst ). Jan Jansen 9500100 Cog 0.0 0.0 0 C laas Z ee 95 0002 0 T SI 0.0 0.0 0 Pointer 2 Figure 3 Statische en Dynamische Data nummerlijst Pointer 1 Pointer 1 Pietje Puk 9500010 Cog 0.0 0.0 0 Pointer 2 Pointer n Pointer n Claas Zee 9500020 TSI 0.0 0.0 0 Figure 4 Naam- en nummerlijsten naar studenten 13 Pietje Pu k 95 0001 0 C og 0.0 0.0 0 Poin ter n Let op het gebruik van de NIL-waarde (om aan te geven, dat de pointer in een bepaalde rijcel nog niet naar een gealloceerd record wijst). naamlijst Jan Jan sen 95 0010 0 C og 0.0 0.0 0 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen Programma’s opsplitsen in Units De voorgaande TYPE-declaraties zijn geplaatst in de Unit Student in het bestand Student.pas. Het programma heet Studenten_administratie en is te vinden in het hoofdbestand StudAdm.pas, de verdere programmadelen, komen in de Units Namen, Nummers en Studies, in de respectievelijke bestanden Namen.pas, Nummers.pas en Studies.pas Unit Student Student.pas Unit Namen Namen.pas We zullen het programma implementeren als een eenvoudig programma onder Windows, waarbij gebruik gemaakt wordt van de standaard unit Crt (of bij gebruik van Pascal for Windows: WinCrt), en waarbij minimale eisen gesteld worden aan de invoer vanaf het toetsenbord. De recordvelden mogen met een readln van het toetsenbord gelezen worden. Unit Nummers Nummers.pas Unit Studies Studies.pas Program Studenten_Administratie StudAdm.pas Figure 5 Units in Studenten Administratie Program Studenten_administratie; {In StudAdm.pas} Uses {Win}Crt, Student, Namen, Nummers, Studies; VAR naamlijst, nummerlijst, studielijst : ALijst; BEGIN { In de Unit Namen } MaakNaamLijst( naamlijst ); ToonNaamLijst( naamlijst ); { In de Unit Nummers } MaakNummerLijst( nummerlijst, naamlijst ); ToonNummerLijst( nummerlijst ); {In de Unit Studies } MaakStudieLijst( studielijst, naamlijst ); ToonStudieLijst( studielijst ); END. Programma 1 Studenten administratie In de Unit Namen komen de procedures MaakNaamlijst en ToonNaamLijst. De interface van de unit is: Unit Namen; INTERFACE Uses Student; PROCEDURE MaakNaamLijst( VAR opnaam : ALijst ); PROCEDURE ToonNaamLijst( opnaam : ALijst ); Het top down ontwerp en de implementatie van MaakNaamLijst moet de eerste keer een aantal studentrecords maken en opslaan in een bestand, en vervolgens, als het bestand bestaat, de studentenrecords lezen en dynamisch in het geheugen plaatsen: 14 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen PROCEDURE MaakNaamLijst( VAR opnaam : ALijst ); VAR bestand : FStudent; { FStudent: FILE OF RSTUDENT } BEGIN InitLijst ( opnaam ) ; Aantal := 0 ; IF OpenStudentBestand ( bestand ) THEN LeesStudentBestand ( bestand, opnaam ) ELSE BEGIN LeesNaamLijst ( opnaam ); BewaarStudentBestand ( bestand, opnaam ); END; END; De uitwerking van LeesStudentBestand is rechttoe rechtaan. Zolang het einde van het geopende bestand niet bereikt is, worden studentrecords, stud, ingelezen, de in het implementatie deel van de unit gedeclareerde variabele Aantal wordt verhoogd, een nieuw dynamisch object wordt gecreëerd met de tijdelijke pointer, phulp, en de inhoud van het studentrecord, r, toegekend aan phulp^. Steeds wordt de waarde van phulp op de juiste plek in de lijst geplaatst en tot slot wordt het bestand gesloten. PROCEDURE LeesStudentBestand ( VAR bestand : FStudent; VAR opnaam : ALijst ); VAR stud : RStudent; phulp : PStudent; BEGIN WHILE NOT EOF( bestand ) DO BEGIN Read ( bestand, stud ); {Lees een studentrecord } New ( phulp ); {maak ruimte voor een nieuw studentrecord} phulp^ := stud ; {ken er de inhoud van stud aan toe} VoegIn( opnaam, phulp ); {Voeg de pointer alfabetisch toe} END {While}; Close(bestand) ; {Sluit het bestand} END {LeesStudentBestand}; De procedure VoegIn plaatst de student in de lijst op de alfabetisch juiste plaats door de lijst van boven naar beneden af te lopen tot de eerste naam gevonden wordt die alfabetisch groter is dan de naam van de te plaatsen student of het einde van de lijst bereikt is. Nadat alle studenten op en na deze positie één plaats naar beneden geschoven zijn, kan de nieuwe student tussengevoegd worden: PROCEDURE VoegIn ( VAR opnaam : ALijst ; stud : PStudent ); VAR positie, j : INTEGER ; BEGIN {bepaal ‘positie’ voor invoegen } positie := 1; WHILE ( positie < MaxAantal) AND ( opnaam[positie] <> NIL ) AND ( opnaam[ positie ]^.naam < stud^.naam ) DO inc ( positie ) ; {maak ruimte door naar beneden te schuiven} FOR j := Aantal DOWNTO positie DO opnaam [ j+1 ] := opnaam [ j ] ; {voeg in} inc ( Aantal ) ; { Oei; toegrijpen op unit-globale variabele ...} opnaam [ positie ] := stud ; END {voegin}; Deze procedure is iets te simpel, want er volgt geen foutmelding als de lijst vol is. De procedure LeesNaamLijst bevat een herhaallus waarbij de gegevens van een studentrecord van het toetsenbord gelezen worden met de procedure LeesStudent. Als de naam niet leeg is wordt Aantal met één verhoogd en een nieuwe dynamische variabele gecreëerd en toegewezen aan de eerste vrije studentpointer in de naamlijst opnaam. 15 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen PROCEDURE LeesNaamLijst ( VAR opnaam : ALijst ); VAR stud : RStudent ; BEGIN REPEAT LeesStudent ( stud ); IF IsGeldig ( stud ) THEN BEGIN Inc ( Aantal ); New ( opnaam [ Aantal ] ); { vraag ruimte op de Heap } opnaam [ Aantal ]^ := stud { kopieer gegevens naar Heap } END; UNTIL ( NOT IsGeldig ( stud ) ) OR ( Aantal = MaxAantal ); END {LeesNaamLijst}; De boolse functie IsGeldig test de studentgegevens op geldigheid. We maken ons er op dit moment een beetje vanaf door te stellen dat studentengegevens geldig zijn als de naam uit minstens één teken bestaat en het studentnummer groter dan 0 is: FUNCTION IsGeldig( stud : Rstudent ) : BOOLEAN ; BEGIN IsGeldig:= ( Length ( stud.naam ) > 0 ) AND ( stud.nummer > 0 ); END {IsGeldig}; De procedure BewaarStudentBestand opent het bestand om te schrijven en schrijft vervolgens Aantal studentrecords naar het bestand f en sluit tot slot het bestand. PROCEDURE BewaarStudentBestand ( VAR bestand : FStudent; opnaam : ALijst ); VAR index : INTEGER ; BEGIN Rewrite ( bestand ); FOR index := 1 to Aantal DO BEGIN IF opnaam[index] <> Nil THEN write ( bestand, opnaam[index]^ ); END {for}; Close ( bestand ); END {BewaarStudentBestand}; Het ontwerp en de implementatie van de procedure ToonNaamLijst in de unit Namen moet de studenten aan de hand van de naamlijst in alfabetische volgorde tonen Deze procedure ToonNaamLijst wordt eenvoudig gehouden door de algemene ToonLijst aan te roepen. PROCEDURE ToonNaamLijst ( opnaam : ALijst ); BEGIN ToonLijst ( opnaam ); END; Het ontwerp en de implementatie van de procedure MaakNummerLijst in de unit Nummers, die de nummerlijst maakt en daarbij studenten aanwijst op nummervolgorde. Eerst wordt de nummerlijst Opnummer geïnitialiseerd en vervolgens wordt de naamlijst opnaam afgelopen en de pointers uit de naamlijst op nummervolgorde ingevoegd in de nummerlijst. Unit Nummers; INTERFACE Uses Student; PROCEDURE MaakNummerLijst ( VAR opnummer : ALijst; opnaam : ALijst ); PROCEDURE ToonNummerLijst ( opnummer : ALijst ); IMPLEMENTATION VAR Aantal : INTEGER ; { bereikbaar voor alle procedures binnen deze unit } 16 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen PROCEDURE MaakNummerLijst( VAR opnummer : Alijst ; opnaam : ALijst ); VAR positie : INTEGER ; BEGIN InitLijst ( opnummer ); Aantal := 0 ; positie := 1; WHILE ( opnaam [ positie ] <> NIL ) DO BEGIN VoegIn ( opnummer, opnaam [ positie ] ) ; Inc (positie ); END {while}; END; MaakNummerLijst maakt gebruik van een aangepaste voegin procedure die invoegt op nummervolgorde. PROCEDURE VoegIn ( VAR opnummer : ALijst; stud : PStudent ); VAR positie, j : INTEGER ; BEGIN {Alleen als er nog plaats is:} IF Aantal < MaxAantal THEN BEGIN {bepaal invoegpositie } positie := 1; WHILE (positie < Aantal ) AND ( opnummer [ positie ] <> NIL ) AND (opnummer[positie]^.nummer < stud^.nummer ) DO inc(positie); {maak ruimte door naar beneden verschuiven} FOR j := Aantal-1 DOWNTO positie DO opnummer [j+1] := opnummer [j]; {voeg in} Inc ( Aantal ); opnummer [ positie ] := stud ; END {if}; END {voegin}; Deze VoegIn procedure mag er eigenlijk van uit gaan dat de naamlijst niet te vol kan zijn, zolang de naam- en nummerlijst op hetzelfde type gebaseerd zijn. Als verzekering tegen toekomstige wijzigingen is er toch maar een begrenzende voorwaarde gesteld. De procedure ToonNummerLijst heeft niets nieuws te melden en implementeert met behulp van de procedure ToonLijst de procedure die de studenten toont op nummervolgorde aan de hand van de nummerlijst. PROCEDURE ToonNummerLijst( opnummer: ALijst ); BEGIN ToonLijst( opnummer ); END; Het ontwerp en de implementatie van de procedures MaakStudieLijst en ToonStudieLijst in de unit Studies, die de studenten groepeert naar studierichting verschilt niet van die van Opnummer, Unit Studies; INTERFACE Uses Student; PROCEDURE MaakStudieLijst ( VAR st : Alijst ; opnaam : ALijst ); PROCEDURE ToonStudieLijst ( st : ALijst ); slechts de VoegIn procedure is aangepast en sorteert op studierichting in alfabetische volgorde middels de opdracht:: WHILE ( st [ positie ] <> NIL ) AND ( st[positie]^.studierichting < stud^.studierichting ) DO Inc (positie); 17 B2: Algoritmiek, Datastructuren en Objectprogrammeren Dynamische variabelen 2.6 Practicumopdracht Je gebruikt in deze opdracht een rij (uiteindelijke rijdimensie: 5000, maar start met b.v. een rijlengte van 10 !) van pointers naar records, die elk als volgt zijn opgebouwd: GEGEVEN = RECORD getal teken : : INTEGER ; CHAR END Nà opstarten van het programma moeten de records van de rij random (50% wèl en 50% nìet) worden opgevuld met een willekeurig geheel getal èn een willekeurig teken uit het alfabet (hoofd en/of kleine letter). De (50%) pointers die nìet verwijzen naar zo’n opgevuld record, moeten een ‘ Nil’-waarde krijgen. Laat de inhoud van de bij de rij behorende records op het scherm zien; laat bij Nil-pointers het woord ‘NIL’ op het scherm verschijnen. Hierna moet de rij met pointers gesorteerd worden volgens de getalswaarde uit het geassocieerde record. Gebruik voor het benodigde sorteerproces een aanpassing van de selectiemethode (kijk eens in je B1-Reader of in een boek zoals Savitch of kijk naar het ‘sjabloon’ hieronder); verwissel bij het sorteerproces de pointerwaarden en nìet de recordinhouden en let op de Nil-pointers (die moeten ‘naar achteren’ in de rij)! Laat tot slot weer alle recordinhouden zien volgens de nu aangebrachte ordening van de rij, waarbij wederom Nil-pointers het woord ‘NIL’ op het scherm moeten tonen. Zorg ervoor, dat door controle van het in het begin èn op het einde beschikbare werkgeheugen, vóór het beëindigen van het programma, alle eerder dynamisch bezette geheugenruimte inderdaad weer is vrijgegegeven. Laat het evt. verschil in beschikbare begin- en eindwaarde op het beeldscherm verschijnen. Schematisch: 325 ‘C ’ 27 938 127 ‘z ’ ‘k ’ ‘m ’ rij vóór sorteren: N IL Gewenste uitvoer: 325 C NIL 325 ‘C ’ N IL N IL NIL 27 z 938 k NIL N IL 127 m 27 938 127 ‘z ’ ‘k ’ ‘m ’ NIL rij ná sorteren: Gewenste uitvoer: 27 z 127 m 325 C 938 k N IL N IL N IL NIL NIL NIL N IL NIL Sjabloon ‘selectie-sorteren’: PROCEDURE selection sort BEGIN eerst is het gesorteerde deel leeg { sorteer_vanaf := 0 } WHILE ongesorteerde deel nog niet leeg { sorteer_vanaf < aantal-1 } DO BEGIN zoek het kleinste element uit het nog ongesorteerde deel ; verwissel dit met het eerste element uit ongesorteerde deel END {WHILE} END 18