Informatica 11 Academiejaar:2011-1.2 Opleiding: Bachelor in de industriële Wetenschappen Studieomvang: 6 $tudiepunten Trajectschijf: 2 Totale studietijd:170,00 Uren Dit opleidingsonderdeel wordt gequoteerd op 20 (tot op een geheel getal).· Tweede examenkans:wel mogelijk. Deliberatie-info:onder bepaalde voorwaarden kunnen tekorten op dit opleidingsonderdeel gedelibereerd worden. Niveau: Uitdiepend Coördinator:Naessens Helga Andere docenten:Stoop Rudy Talen:Nederlands Tijdsorganisatie:Semester 1
Omschrijving Studiematerialen Syllabus. Ter aanvulling zijn boeken over de behandelde onderwerpen ter beschikking .in de bibliotheek. De lesgevers bieden slides, voorbeeldprogramma's en oefeningen aan via het leerplatform van de Hogeschool.
Omschrijving OndeiWijsorganisatie Tijdens de theoretische contacturen, wordt mede aari de hand van voorbeelden stap voor stap de theorie uitgelegd. Tijdens de oefeningensessies werkt de student zelfstandig aan een PC.
Omschrijving OndeiWijsorganisatie Hoorcollege (24,00 uren) Werkcollege (30,00 uren) Zelfstudie (116,00 uren)
Omschrijving Begincompetenties De eindcompetenties verworven in Informatica I.
Omschrijving Eindcompetenties Kerncompetentie 1: In staat zijn om theoretische en praktische inzichten uit de informatica correct te hanteren binnen ingenieurstechnische oefeningen en vraagstukken. (SCA 1) Onder meer: In staat zijn om zelfstandig een computerprogramma op te stellen, te testen en uit te voeren. In staat zijn om blijvend kritisch, creatief en wetenschappelijk te denken en te redeneren. (ACt)
Algemene competentie 1: In staat zijn om wetenschappelijk- disciplinaire inzichten betreffende de informatica zelfstandig en in teamverband toe te passen op wetenschappelijke enlof ingenieurstechnische problemen. (AIC1) Onder.meer: In staat zijn om een probleem te analyseren en te structureren en dit te vertalen naar een computerprogramma.
Algemene competentie 2: In staat.zijn om relevante bestaande en nieuwe technologieën enlof theorieën te assimileren, te implementeren en te gebruiken binnen het domein van de informatica. (AIC2)
Omschrijving Doelstellingen De opleidingsonderdelen Informatica I & 11 vormen de industrieel ingenieur tot een professioneel programmeur die de algemene principes van moderne programmeertalen kan toepassen. Abstracte datatypering is hem niet vreemd. De aangeleerde grondbeginselen kan hij later makkelijk toepassen in andere talen, waaronder diegene die gebruikt worden in allerlei softwareof toepassingspakketten . . Bovendien ontwikkelt hij door het leren programmeren zijn analytisch vermogen, leert hij problemen zelf oplossen en hiervoor correcte oplossingen formuleren. Dit opleidingsonderdeel vormt de basis voor alle vakken in de opleiding Informatica waar nioet geprogrammeerd worden.
Omschrijving Inhoud De cursus C++ (gedoceerd in het opleidingsonderdeel Informatica I) wordt verder uitgediept. De volgènde onderwerpen komen aan bod: samengestelde gegevenstypes (structs, meerdimensionale tabellen, STL-containers), functies en procedures, pointers en hun toepassingen, invoer/uitvoer, excepties, beginselen van objectgericht programmeren.
Omschrijving Begeleiding Uitleg op afspraak en extra oefeningen op aanvraag.
Omschrijving Evaluatie Theorie: schriftelijk examen (52%) Labo: permanente evaluatie, gequoteerde oefeningen en testen (48%) De beoordeling en het tot stand komen van de eindquotatie van opleidingsonderdelen gebeurt via het wiskundige gemiddelde volgens de toegekende coëfficiënten. Indien nochtans op één van de onderscheiden vakken (delen van opleidingsonderdelen) 7 of minder op 20 wordt behaald, kan worden afgeweken van deze rekenkundige berekening van de eindquotatie ·van het opleidingsonderdeel en kunnen de punten bij consensul:; worden toegekend. Uitzonderlijk wordt voor dit opleidingsonderdeel ook voor het oefeningenlabo een tweede zittijd ingericht. Een eventuele deelname aan een tweede zittijd voor het luik 'labo' wordt aanzien als een remediëring van een tekort. De quotatie in tweede zittijd van het luik 'labo' veniangt niet het volledige cijfer van de eerste zittijd, maar slechts van een beperkt percentage.
Trefwoorden Computerwetenschappen (P170), Informatica (P175), Computertechnologie (T120)
Studiekosten € 10 (kosten syllabus en kopies van de labo~opdrachten).
Inhoudsopgave
1
Basisopdrachten
8
1.1 Inleiding . ..
8
1.2 Variabelen en constanten
8
1.2.1
Variabelen declareren
8
1.2.2
Initialisatie bij declaratie
9
1.2.3
Constanten . . ... . .
10
1.3 Basisopdrachten of statements
11
1.3.1
De toekenning of assignment
11
1.3.2
De uitvoeropdracht .
11
1.3.3
De invoeropdracht
12
1.3.4
Een procedureoproep.
12
1.4 Uitdrukkingen of expressions
12
1.4.1
Numerieke Operatoren .
13
1.4.2
Een functieoproep
13
1.4.3
Voorbeelden . . . .
14
1.4.4
Speciale-toekenningsoperatoren
15
1.5 De while-lus .
16
1.6 De for-lus ..
18
1.7 De if-structuur
19 1
@HelgaoNaessens@hogent obe .
1.8
Een volledig programmà
1.~
Tabellen 0 0 0
22
0
24
1.10 Het type bool
26
1.11 De conditionele uitdrukking
27
1.12 Gebruik van bibliotheken 0
28
1.1201 Declaraties van functies
29
1.1202 Declaraties van procedures
30
1.1203 Tabelparameters
31
1.12.4 Namespaces 0 0 0
31
2 De types char en string 201
202
34
Het type char 0 0 0 0 0
34
201.1
Karakters in schrijfopdrachten
36
201.2
Inlezen van een karakter 0 0
37
201.3
Bewerkingen met karakters
38
Het type string 0 0 0 0 0 0 0 0 0 . 0
41
202.1
Declareren, initialiseren, inlezen en uitschrijven van strings
41
20202
Bewerkingen met strings
43
0
3 Samengestelde gegevenstypes
45
3.1
Tweèdimensionale tabellen
302
Structs . 0 0 0 . .
48
3.201
Algemeen
.48
302.2
Tabellen van structs
52
3.203
Structs met tabellen als velden
52
45
0
2
@Helga.Naessens@hogent. be
4 ·Containers
54
De tabel
54
4.1.1
Voordelen van de tabel .
54
4.1.2
Problemen met de tabel
54
4.2 Basisprincipe van een container
56
4.1
4.3
De groeitabel· .
..
..
57
4.3.1
Principe van de groeitabel .
57
4.3.2
Vector
58
4.3.3
String
61
De gelinkte lijst .
61
4.4.1
Principe van de gelinkte lijst
61
4.4.2
List
63
4.5
De iterator
63
4.6
De stapel en de wachtrij
66
4.6.1
Principe van de stapel
66
4.6.2
Stack . . . . . . . . .
67
4.6.3
Principe van de wachtrij
68
4.6.4
Queue . . . . . . . . . .
4.6.5
Principe van de prioriteitswachtrij
69
4.6.6
Achterliggende implementatie
69
De verzameling . . . . . . . . . . . .
70
4.7.1
Principe van de verzameling.
70
4.7.2
Set ..
70
De afbeelding
72
4.8.1
72
4.4
4. 7
4.8
..
Principe van de afbeelding. 3
68
©Helga.Naessens@hogent. be
4.8.2
Map . . . . ·. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5 Functies en procedures 5.1
5.2
5.3
75
Functies . . . . . . . .
. .. . . . . . .. . 75 . . . ... 80
5.1.1
Logische functies
5.1.2
Structs als functieresultaat. en als functieparameter .
82
5.1.3
Tabellen als functieparameters
83
Procedures
. ... . . . . . . . . . . . .
85
5.2.1
Procedures zonder parameters
86
5~2.2
Procedures met enkel invoerparameters
89
5.2.3
Procedures met uitvoerparameters . . .
89
5.2.4
Procedures .met invoer-uitvoerparameters
92
5.2.5
Tabellen als procedureparameters .
93
Recursie . . . . . . . . .
96
5.3.1
Wat is recursie? .
96
5.3.2
Voorbeelden .. . .
96
5.3.3
Algemene structuur v.an een recursieve functie.
98
5.3.4
De voor- en nadelen van recursie . . . . . . . . .
98
6 Bestanden
99
99
6.1
Algemeen
6.2
Testen op het einde van een bestand
. 102
106
7 Excepties 7.1
Inleiding .
. 106
7.2
Exception handling .
. 107
4
@Helga.Naessens@hogent. be
8
Objectgeorienteerd programmeren 8.1
8.2
8.3
8.4
112
Basisbegrippen
. 112
8.1.1
Objecten
. 112
8.1.2
Kenmerken van objecten .
. 113
8.1.3
Klassen
8.1.4
lnka,pseling en verbergen van informatie
. 114
8.1.5
Berichten en methodes .
. 114
. .
Zelf klassen maken
...
.
..
. 114
.
·.
. 115
8.2.1
Een eerste voorbeeld
. 115
8.2.2
Private en public: gegevens verbergen
. 117
8.2.3
Lidfuncties definiëren buiten de klassespecificatie
. 118
8.2.4
Afzonderlijke compilatie
. 121
8.2.5
Constructoren
. 124
Gebruik val). objecten
. 129
8.3.1
Een object als waarde-parameter van een lidfunctie .
. 129
8.3.2
Een object als functieresultaat . . . .
. 132
8.3.3
Een object aanpassen in een lidfunctie
. 132
Klassen: conversie en constructie .
_.
. 133
8.4.1
Initialisatie bij constructie .
. 135
8.4.2
De copy-constructor . . .
. 136
8.4.3
Conversie bij constructie .
. 137
8.4.4
Conversie na initialisatie .
. 138
8.4.5
Conversie met andere constructoren
. 139
140
9 Pointers
5
@Helga.Naessens@hogent. be
9.1
Inleiding . . . . . . .
. 140
9.2
Pointers en tabellen
. 143
9.3
Pointers
al~
parameters
. 144
9.4
Pointers naar const . .
. 145
9.5
Rekenen met pointers
. 146
9.6
Pointers naar structs .
. 150
9.7
Pointers als resultaten van deelprogramma's .
. 152
9.8
De null"-pointer .
. 154
9;9
De operator ne:w
. 155
9.10 De operator delete
. 156
10 Toepassingen van pointers
158
10.1 C-strings .. . . . . . . . .
. 158
10.1.1 Bewerkingen met strings .
. 161
10.1.2 Tabellen van strings en pointers naar strings
. 164
10.1.3 argc en argv .
. 166
10.2 Gelinkte lijsten . . .
. 167
10.2.1 Opbouwen van lijsten
. 169
10.2.2 Overlopen van een lijst .
. 171
10.2.3 Elementen toevoegen aan een lijst
. 172
10.2.4 Een element uit een lijst verwijderen
. ·173
10.2.5 Definitie van nieuwe types .
. 176
10.2.6 Pointers en objecten . . . .
. 178
11 Aanvullingen bij invoer en uitvoer
181
11.1 Verzorgde uitvoer . . . . . . . . . .
. 181
6
@Helga.Naessens@hogent. be
11.2 Invoer via het toetsenbord . . . 11.3 Inlezen van
~én
.184
enkel karakter .
185
11.4 Nog meer voorbeelden van invoer
187
7
Hoofdstuk 1 Basisopdrachten De bedoeling van dit hoofdstuk is om eerst even het weinige dat je vorig jaar van C++ hebt gezien, . kort te herhalen. Daarna worden een aantal eenvoudige nieuwe concepten geïntroduceerd.
1.1
Inleiding
In een programmeertaal zoals C++ bestaat een programma gewoon uit tekst, die dan wel aan bepaalde conventies moet voldoen. Deze tekst wordt door de computer gelezen, . vertaald en uitgevoerd. Een programma is opgebouwd uit speciale tekens of combinaties van tekens (bv. == , « of *) en speCiale sleutelwoorden (bv. if, for of return) die elk een specifieke betekenis hebben (en voor niets anders mogen gebruikt worden). . C++ gebruikt een vrij formaat. Dit betekent dat alleen de volgorde van de tekens en woorden in een programma van belang is, en niet de schikking. In principe kan hetzelfde programma dus uit één enkele (lange) lijn bestaan, of uit zeer vele lijnen met op elke ·lijn één enkel woord of symbool. . In ·de praktijk voegen we echter steeds extra spaties en lege lijnen aan een programma toe om de visuele structuur van het programma te verduidelijken. Op die manier begrijpt niet alleen de computer het programma, maar is het ook veel duidelijker. voor iedere menselijke lezer. Probeer in jouw programma's een gelijkaardige schikking te volgen.
1.2 1.2.1
Variabelen en constanten Variabelen declareren
Variabelen kunnen in een programma of in een deelprogramma slechts gebruikt worden, indien ze voordien gedeclareerd werden. Deze declaratie gebeurt vaak in het he:. gin van het (deel)programma vooraleer de variabelen effectief in programmaopdrachten 8
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
worden gebruikt. De declaratie van een variabele mag echter ook ergens middèn in het (deel) programma gebeuren, vlak voordat de variabele voor het eerst wordt gebruikt. De declaratie van een variabele betekent dat je voor elke variabele haar naam en haar type moet aangeven. Het type bepaalt het soort bewerkingen dat je dan later Op die variabele kunt toepassen. Types in C++ hebben een Engelse afkorting. In C++ bestaan heel wat types om gehele getallen aan te duiden: de types short, int, long, eventueel voorafgegaan door het sleutèlwoord unsigned. Ook voor re~le getallen bestaan er minstens twee soorten, namelijk float en double. We hebben er echter voor gekozen om in deze ·cursus enkel int te gebruiken voor gehele getallen en double voor reële getallen (zoals trouwens aanbevolen wordt door B. Stroustrup, de uitvinder van C++ ). Een declaratie ziet er als volgt uit: eerst komt de naam van het type, dan één of meerdere namen van variabelen, en op het einde komt een kommapunt. In het progranimafragment hieronder declareren we bijvoorbeeld vier gehele en vier reële variitbèlen: int a~ b, c; double hoogte, lengte, breedte, maand13; int aantal; De naam van een variabele moet steeds aan de volgende eisen voldoen: • hij begint met een letter • hij be:vat uitsluitend hoofd- en kleine letters, cijfers en het onderstreepteken ' _ ' • hij bevat geen blanco's Let ook op dat C++ ~en onderscheid maakt tussen hoofdletters en kleine letters. De variabele a en de variabele A zijn dus niet dezelfde. Schrijf ook niet Int of INT in plaats van int. Traditioneel beginnen in C++ variabelen met een kleine letter en worden niet uitsluitend hoofdletters gebruikt, die worden immers voor speciale doeleinden gereserveerd.
1.2.2
Initialisatie bij declaratie
Vaak begint een programma met het toewijzen van een beginwaarde aan een aantal variabelen, zoals in het onderstaande programmafragment: double getal, grootste; int aantal; grootste = 0.0; aantal = 0; cin >> getal; 9
@Helga.Naessens@hogent. be
Hoofdstuk 1. Basisopdrachten
Je kan dit in C++ echter ook op een kortere manier noteren. Je kan een declaratie en een toewijzing combineren door onmiddellijk na de gedeclareerde naam een gelijkheidsteken en een waarde te plaatsen, zodat het voorgaande programmafragment dus ingekort kan worden tot: . · double getal, grootste int aantal = 0; cin » getal;
0 . 0;
Merk op dat de variabele getal niet geïnitialiseerd wordt, doch enkel maar gedeclareerd. Het is hier immers niet nodig dat getal reeds vóór de leesopdracht een specifieke waarde krijgt. Let echter op: denk niet dat een variabele die je niet initialiseert, automatisch de waarde nul krijgt!
1.2.3
Constanten
Constanten zijn waarden die niet kunnen .yeranderen, zoals bijvoorbeeld de gehele constanten 45 en -89, en de reële constante 45.0. Constanten kunnen in het programma echter ook een naam krijgen, net als een variabele. Het voordeel is, dat je bijvoorbeeld niet overal in het programma de waarde 20 ziet staan, maar wel de naam van de constante, . bijvoorbeeld MAXIMUM_ QUOTERING, die iets vertelt over de betekenis van deze waarde. Constanten worden als volgt gedeclareerd: const int MAXIMUM_QUOTERING
= 20;
Deze opdracht stelt eigenlijk de declaratie voor van eeii gehele waarde die door het predikaat cbnst niet kan veranderd worden.
Samenvatting declaraties
type naaml, naam2 ... ; =? niet-geï~itialiseerde
variàbelen
of
type naamt =?
= waardel, naam2 = waarde2 ... ;
geïnitialiseerde vadabelen
of
const type naam = waarde ... ; =?
constante, verplicht geïnitialiseerd
10
@Helga.Naessens@hogent. be
Hoofdstuk 1. Basisopdrachten
1.3
Basisopdrachten of statements
. Het gedeelte van het programma waarin we beschrijven welke acties de computer moet uitvoeren·, bestaat uit een aantal geneste structuren die elk bestaan uit één of meerdere basisopdrachten of statements. Een basisopdracht is hierbij een toekenning, een uitvoeropdracht, een invoeropdracht of een procedureoproep.
1.3.1
De toekenning of assignment
De bedoeling van deze opdracht is een waarde te geven aan een variabele. De algemene vorm ervan is:
variabele = uitdrukking; Links van een toekenning moet steeds een variabele staan, wat ook wel eens een lvalue genoemd wordt. Het rechterlid wordt dan een rvalue genoemd. Bij een toekenning ·wordt eerst de waarde van de uitdrukking die rechts staat, berekend en dan toegekend aan de variabele die links staat. De voorgaande waarde van de variabele wordt hierbij overschreven . . Er moet ook steeds aandacht besteed· worden aan . het feit dat het rechterlid van hetzelfde type moet zijn als het linkerlid, of toch op zijn minst automatisch getransformeerd moet kunnen worden naar het type van de linkse variabele.
1.3.2
De uitvoeropdracht
Deze opdracht wordt gebruikt om gegevens op het scherm t e zetten. De algemene vorm ervan 1s:
I cout « . outl «
out2 «
out3 .. . ;
Hierbij kan outi telkens een constante, een variabele, een uitdrukking of een manipulator zijn. Zo is endl een veelgebruikte manipulator die ervoor zorgt dat er naar de volgende lijn gegaan wordt. In het onderstaande programmafragment zijn enkele uitvoeropdrachten weergegeven: cout << resultaat << endl; cout << "To C++ or not to C++, that is the question" << endl; cout << "Het resultaat is " << som << endl; Merk op dat er in de derde opdracht een extra spatie staat vóór de sluitende aanhalingstekens, opdat de waarde van som in de uitvoer mooi van ·het woordje is zou gescheiden zijn.
11
@Helga.Naessens@hogent. be
Hoofdstuk 1. Basisopdrachten .
1.3.3
De invoeropdracht
Deze opdracht wordt gebruikt om de gebruiker de mogelijkheid te bieden om ûM waarden in te typen. De algemene vorm ervan is: cin »
variabelel »
variabèle2 »
variabele3 ...
Beschouw bijvoorbeeld het onderstaande programmafragment: int getal! , geta12; cin » getal! » geta12; Wanneer de computer deze leesopdracht ontmoet, krijg je de gelegenheid om twee gehele getallen in te tikken. Je kan deze getallen ingeven op één lijn (waarbij de getallen gescheiden zijn door één of meerdere spaties) of je kan elk getal op een andere lijn plaatsen (en dus op de Enter-toets drukken na het eerste getal). De »-operator slaat immers alle 'white spaces' (I.e. spaties, tabs en enters) over. Meer informatie hierover vind je in Hoofdstuk 11. ·
1.3.4
Een procedureoproep
Een opdracht kan ook gewoon een reeds bestaande procedure oproepen. Een voorbeeld van een dergelijke opdracht is weergegeven in het programmafragment Code 1.1 dat een willekeurig geheel getal uit het interval [20, 30] genereert en uitschrijft: srand(time(O) ); int getal = 20 + rand()%11; cout << getal; Code 1.1: Een procedureoproep Het éénrnalig oproepen van de procedure srand uit de cstdlib-library (in de eerste regel van dit programmafragment) .zorgt voor de initialisatie van de randomgenerator. De generatie van een willekeurig positief geheel getal gebeurt m.b.v. de functie rand (eveneens uit de cstdlib-library), waarna d.m.v. de gepaste bewerkingen dit getal omgezet wordt naar een getal uit het interval (20, 30].
1.4
Uitdrukkingen of expressions
Een uitdrukking is in zijn eenvoudigste vorm een constante, kan ook een variabèle of een functieoproep zijn, of bestaat uit een combinatie van constanten:, variabelen, functieoproepen en operatoren.
12
Hoofdstuk 1. Basisopdrachten
1.4~ 1
@Helga.Nàessens@hogent. be
Numerieke Operatoren
Een overzicht van de numerieke operatoren vind je in Tabel 1.1.·
uitdrukking
I resultaat
a+ b
Som van a en b
a -.b
Verschil vari a en b
-a
Tegengestelde van a
*b
Produkt van a en b
a
a I b
Geheel quotiënt bij deling van a door b (a en b geheel)
a
%b
Rest bij deling van a door b (a en b geheel)
a I b
Reëel quotiënt bij deling van a door 1;> (a en/of b reëel) Tabel1.1
Merk op dat C++ geen afzonderlijk symbool gebruikt om een gehele deling aan te geven. Een deling wordt beschouwd als een gehele deling zodra beide argumenten geheel zijn, anders is het een reële deling. '3 I 4' betekent dus niet 'drie kwart' maar is gewoon een ingewikkelde manier om nul voor te stellen. Als je wel degelijk 'drie kwart' bedoelt, schrijf dan '3,0 I 4.0' of '0. 75'. Merk ook op dat C++ geen operator voorziet om de machtsverheffing te kunnen uitvoeren!
1.4.2
Een functieoproep
Als onderdeel ván een uitdrukking kan ook een functie opgeroepen worden. In C++ zijn er niet echt Ingebouwde functies, zoals dat in sommige andere programmeertalen wel het geval is. Dit betekent echter niet dat er geen functies beschikbaar zouden zijn. Je vindt ze terug in bibliotheken, waarvan je expliciet de naam moet vermelden in je programmatekst. '
'
'
In het programmafragment Code 1.1 kwamen reeds twee functieoproepen voor. We maakten toen immers gebruik van de functie rand (uit de cstdlib-library) om een willekeurig geheel getal te genereren. Ook in de opdracht
srand(time(O)); komt een functieoproep voor: time ( 0) is een functieoproep die op zich opnieuw een onderdeel is van een procedureoproep.
13
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
De cmath-library bevat. heel veel wiskundige functies. De belangrijkste hiervan zijn weergegeven in Tabel 1.2.
naam
J
betekenis
cos(x)
cosinus van x (x in radialen)
sin(x)
sinus van x (x in radialen)
tan(x)
tangens van x (x in radialen)
acos(x)
boogcosinus van x in radialen
asin(x)
boogsinus van x in radialen
atan(x)
boogtangens van x in radialen
exp(x)
e tot de macht x
log(x)
neperiaanse logaritme van x (lnx)
loglO(x)
logaritme van x met basis 10
sqrt(x)
vierkantswortel van x
abs(i)
absolute waarde van het geheel getal i
fabs(x)
absolute waarde van het reëel getal·x Tabell.2
Al deze functies kan je oproepen met een geheel of een reëel argument. Ongeacht het type van het argument zal het resultaat van een dergelijke functieoproep ook steeds als reëel beschouwd worden. Dus, hoewel de vierkantswortel van 4 wiskundig gezien een geheel getal is, is het resultaat van de functieoproep sqrt ( 4) toch van het type double,
Et is echter één belangrijke uitzondering: abs (i) ·neemt enkel een geheel getal als argument en levert als resultaat een geheel getal op. De cmath-library bevat overigens niet alleen functies: ze bevat ook de. .constante M-'-P:t, . . zodat je in je .programma 1r' niet zelf hoeft te declareren en te initialiseren.
1.4.3
Voorbeelden
Enkele voorbeelden van uitdrukkingen, waarbij a en b gehele en x en y reële variabelen zijn, zijn: sqrt(cos(x) * cos(y) + sin(x) fabs(x * y) - abs(a) - abs(b) b * (a I b) + a %b ~ a
*
sin(y))
Het berekend resultaat kan dan gebruikt worden als onderdeel van een opdracht; Enkele voorbeelden van opdrachten die uitdrukkingen gebruiken, zijn:
14
Hoofdstuk L Basisopdrachten
@Helga.Naessens@hogent. be
cout .<< "De som van de getallen is " << (g1 + g2) « endl; vierkdiscr sqrt(b * b- 4 *a* c); cout << "Klant nr " << klantnr << " moet nog " << (debet- credit) << " Euro betalen !" << endl;
=
1.4.4
Speciale toekenningsoperatoren
Opdrachten zoals 'tel 3 op bij deze variabele' of 'vermenigvuldig deze variabele met 5' komen in de praktijk veel voor. Deze opdrachten kunnen in C++ korter genoteerd worden, zoals we in Tabel1.3 hebben opgesomd. (Hierin mag je var vervangen door een willekeurige variabele en uitdr door een willekeurige uitdrukking.)
Opdracht
Afkorting
I
Betekenis
var = var + uitdr;
var += uitdr;
Tel uitdr op bij var
var = vár - uitdr;
var .,.-= uitdr;
'Irek uitdr af van var
var = var * uitdr;
var *= uitdr;
Vermenigvuldig var met uitdr
var = var I uitdr;
var /= uitdr;
Deel var door uitdr
var = var % uitdr;
var %= uitdr;
Vervang var door de rest bij deling door uitdr Tabel1.3
.E en paar voorbeelden van opdrachten die gebruik maken van deze speciale toekenningsoperatoren zijn hieronder weergegeven:
som += g % 10; g /= 10; In sommige gevallen kan. het nog korter, namelijk wanneer we één willen optellen of aftrekken van een gehele variabele. Deze opdrachten zijn weergegeven in Tabel 1.4.
Opdracht
var= var+ 1·
' var= var- 1· '
I
Afkortingen
I
Betekenis
var++;
var+= 1;
Vermeerder var met 1
var--;
var-= 1;
Verminder var met 1
Tabel1.4 Bovendien mag var++ of var-- niet alleen als een opdracht worden geschreven, maar kan dit ook in een uitdrukking worden vermeld, zoals bijvoorbeeld in de volgende opdracht:
a = i++ + 3; De waarde van de uitdrukking 'i++' is de waarde van i, maar onmiddellijk daarna wordt i wel met één verhoogd~ Als i bijvoorbeeld de waarde 10 heeft, dan plaatst de bovenstaande opdracht het getal13 in a en krijgt i de waarde 11.
15
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
Voor de volledigheid dient vermeld te worden dat je in een uitdrukking de operatoren ++ en -- ook vóór een variabele kan plaatsen, in plaats van erachter. In dat geval wordt de variabele eerst met één verhoogd of verlaagd vooraleer zijn waarde gebruikt wordt. Stel opnieuw dat i de waarde 10 heeft, dan plaatst de opdracht
· a
= ++i
+ 3;
ditmaal het getal14 in a en krijgt i opnieuw de waarde 11.
1.5
De while-lus
Een while-lus wordt gebruikt om één of meerdere opdrachten een niet expliciet gespecificeerd aantal keer te herhalen. De structuur van een dergelijke lus ziet er als volgt uit: while (voorwaarde ) { opdrachtl; opdracht2; opdracht3; }
Deze opdracht herhaalt de drie opdrachten opdrQ,chtl, opdracht2 en opdracht3 zolang aan de voorwaarde voorwaarde voldaan wordt. Dit geldt natuurlijk ook als er meer of minder dan drie opdrachten binnen in de lus staan. Het herhaalde deel staat tussen de accolades en is lichtjes geïndenteerd. Dit wil zeggen dat er een aantal extra spaties (meesta13) voor elke lijn staan, zodat de linkermarge naar rechts is opgeschoven. Indenteren (of inspringen) is niet verplicht- de computer houdt er geen rekening mee - maar is wel ten zeerste aan te raden . De structuur van het programma wordt hierdoor veel leesbaarder. De accolades mag je weglaten als het slechts om één enkele opdracht gaat, maar soms is het duidelijker en consistenter öm ze te laten staan. Let ook op de ronde haakjes rond de voorwaarde. Die moeten er altijd staan, zelfs al bestaat de voorwaarde slechts uit één woord. De voorwaarde in een while-lus is een logische uitdrukking. Tabel 1.5 resumeert de notatie van de verschillende operatoren die je in dit geval kan gebruiken. Merk op dat een dubbel gelijkheidsteken gebruikt wordt om de gelijkheid te testen. Verwar dit niet met het enkele gelijkheidsteken dat gebruikt wordt in een toewijzing. Het gebruik van = in plaats van == is één van de meest voorkomende fouten bij beginnende C++programmeurs. Dubbel zo goed uitkijken, is de boodsçhap.
16
Hoofdstuk 1. Basisopdrachten
©
[email protected]
1· Logische
operator
Betekenis
a == b
a is gelijk aan b
a != b
.a is verschillend van b
a < b
a is kleiner dan b
a <= b
a is niet groter dan b
a > b
a is groter dan b
a >= b
a is niet kleinet dan b Tabel1.5
Beschouw als voorbeeld van het gebruik van een while-lus het onderstaande programmafragment dat op zoek gaat naar het kleinste getal n waarvoor 1 + 2 + ... + n strikt groter is dan 1000:
som
= 0;
n == 0;
while (som <= 1000) { n++; som += n; }
cout << n << endl; Voorwaarden kunnen gecombineerd worden met ·de logische operatoren 'niet', 'en' en 'of' , zoals· weergegeven in Tabel 1.6. Samengestelde voorwaarde
Betekenis
niet voorwaarde
· ! voorwaarde
voorwaardel && voorwaarde2
voorwaardel envoorwaarde2
voorwaardel 11 voorwaarde2
voorwaardel of voorwaarde2
Tabell.6
Indien in één voorwaarde meer dan één van deze logische operatoren gebruikt wordt, dan heeft ! prioriteit boven&&, en && boven 11, tenzij ronde haakjes zijn gebruikt. De logische operatoren && en I I hebben bovendien een speciale eigenschap. De rechterkant van zo'n operator wordt namelijk alleen maar geëvalueerd als dit nog nodig is, of anders gezegd, als het resultaat niet kan bepaald worden uit de waarde van de linkerkant: • De logische && wordt als volgt geëvalueerd: Eerst wordt de waarde van de linkerkant bepaald. Is dit resultaat onwaar dan wordt de rechterkant niet meer geëvalueerd en is het eindresultaat ook onwaar. Is het linkerlid echter waar, dan wordt het
17
Hoofdstuk 1. Basisopdrachten
©Helga.Naessens@hogent. be
rechterlid geëvalueerd en is de waarde van de volledige uitdrukking waar öf onwaar al naargelang het rechterlid waar is of niet. • De logisch~ I I werkt op een gelijkaardige manier. Eerst wordt de waarde van de linkerkant bepaald. Is die waar dan wordt de rechterkant niet meer geëvalueerd en is het eindresultaat ook waar. Anders is de waarde ':an de volledige uitdrukking waar of onwaar al naargelang het rechterlid waar is of niet. Daarom noemt men een berekening met deze operatoren lui ('lazy evaluation'). Bij logische operatoren is dus de volgorde van de. deeluitdrukkingen van wezenlijk belang. Het onopzettelijk omwisselen van linker- en rechterkant kan verstrekkende gevolgen hebben. Dat het rechterlid niet noodzakelijk geëvalueerd wordt, kan een voordeel zijn, zoals geïllustreerd wordt in het volgende programmafragment: int a, b; cin >> a >> b; while (b != o.&& (a == o I I b%a != 0)) { cout << "a is geen deler van b" << endl; cin >> a >> b; }
In dit fragment worden twee gehele getallen a en b eventueel herhaaldelijk ingelezen, totdat a een deler is van b. Elk getal is een deler van nul, terwijl nul alleen een deler is van zichzelf. .Zonder de luie evaluatie zou een dergelijke beknopte schrijfwijze niet juist zijn, omdat dan b. altijd door a gedeeld wordt, zelfs als a nul is.
1.6
De for-lus
Ook de for-lus wordt gebruikt om één of meerdere opdrachten een aantal keer te herhalen. De structuur van een dergelijke lus ziet er als volgt uit: f or (
initialisatie teller
voorwaarde ;
aanpassing teller ) {
opdrachtl; opdracht2; }
We gebruiken een for-lus enkel als op voorhand gekend is hoeveel keer een reeks opdrachten moet herhaald worden. Natuurlijk kan dit ook met een while-lus gebeuren, maar een forlus is specifiek hiervoor ontworpen, en laat veel duidelijker het aantal keer zien dat de lus herhaald wordt. Bij een for-lus gelden dezelfde opmerkingen over indentatie en het eventueel weglaten van accolades als bij while.
18
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
Als toepassing hierop beschouwen we het volgende programmafragment dat de som berekent van de eerste 100 natuurlijke getallen: som = 0; for (int teller = 1 ; teller <= 100 .som +=teller; cout << som << 'endl;
teller++ )
Bemerk dat de variabele teller hier binnen de ronde haakjes gedeclareerd wordt. Dat betekent dat deze teller enkel binnen de for-lus bestaat, en daarbuiten niet meer gebruikt kan worden. Men zegt dat de teller een lokale variabele van de for-lus is. Dezelfde variabele kan dan eventueel ook opnieuw gedeclareerd worden in een andere (maar natuurlijk geen inwendige) for-lus. Het hergebruiken van dezelfde teller is niet echt verplicht, maar wel een zeer goede gewoonte, omdat de teller gebruikt bij de herhaling,· vaak geen betekenis meer heeft buiten deze herhaling. (
1. 7
De if-structuur
Een selectie wordt in
C++ beschreven met een
if-structuur:
if ( voorwaarde ) {
opdracht I a; ópdrachtl b; opdracht I c; }
else {
opdracht2a; opdracht2b; }
Net zoals bij de while-opdracht, moet de voorwaarde van een if-structuur steeds tussen ronde haakjes staan. Het aantal opdrachten hoeft natuurlijk niet drie en twee te zijn. Het if-gedeelte komt in een if-structuur steeds voor, het elsè-gedeelte daarentegen mag eventueel weggelaten worden, zoals in het volgende kadertje: ïf ( voorwaarde ) {
opdrachtla; opdracht I b; opdracht I c; }
19
Hoofdstuk 1. Basisopdrachten
©Helga.Naessens@hogent. be
Als voorbeeld schrijven we in Code 1.2 het grootste van drie ingelezen getallen op het scherm.
cin » getal1; cin » getal2; · if (getal1 > getal2) grootste getal1; el se grootste getal2; cin » getal3; i f (getal3 > grootste} . groótste = getal3; cout << grootste << endl; Code 1.2: Bêpaalt het grootste van drie getallen Merk op dat we opnieuw accolades hebben weggelaten waar slechts ~én opdracht tussen stond. Bij een if-opdracht moet je hier echter mee opletten. Er zijn namelijk gevallen waarbij het ondoordacht verwijderen van accolades de betekenis van het programma ver~ndert. Bekijk bijvoorbeeld de twee if-opdrachten in Code 1.3 en Code 1.4.
> 0) { if (b > 0) co ut << "1" << endl; el se co ut << "2" << endl;
i f (a
}
Code 1.3: Voorbeeld if-opdracht i f (a > 0) { i f (b > 0)
cout << "1" << endl; }
el se cout << "2" << endl; Code 1.4: Voorbeeld if-opdracht In beide gevallen staat er slechts .één opdracht tussen de accolades. (Deze opdrachten bestaan wel uit meerdere regels, maar een volledige if-else-structuur wordt nog steeds als één enkele opdracht behandeld.) Dus normaal gezien zou je in beide gevallen de accolades mogen weglaten. Wanneer je dit echter zou doen, dan krijgen beide opdrachten letterlijk dezelfde vorm op een aantal spaties na, maar die zijn in C++ niet relevant - terwijl beide opdrachten nochtans duidelijk een verschillende betekenis hadden. We hebben hier dus te maken
20
Hoofdstuk 1. ·· Basisopdrachten
@Helga.Naessens@hogent. be
met een uitzondering op de algemene regel: alleen in het eerste geval kan je de accolades weglaten zonder de betekenis van het programma te wijzigen. Anders gezegd: wanneer je de accolades uit het tweede geval weglaat, zal de compiler denken. dat je het eerste geval bedoelt. Decomputer overloopt het programma van boven naar onder en houdt een lijst bij van alle if's waarvoor hij nog geen else gevonden heeft. Wanneer hij een nieuwe else ontmoet, loopt hij die lijst af van onder naar boven en probeert hij elke if met deze else te associëren totdat hij er één gevonden heeft die past. Als er meerdere if's met een else kunnen gekoppeld worden, krijgt dus de onderste if de voorrang. Ook de volgende situatie verdient onze aandacht: if ( voorwaardel ) { opdracht1 a; opdracht1 b; }
else {
if ( voorwaarde2 ) { opdracht2a; opdracht2b; }
else {
opdracht3a; opdracht3b; } }
Hier mag de allerlaatste sluitende accolade, en de ermee verbonden open accolade zonder problemen worden weggelaten. In dit geval echter zal men meestal ook de schikking van het programma veranderen en noteren zoals in het kadertje op de volgende pagina. Daar doet men dus alsof dit ganse fragment één enkele structuur vormt, in plaats van een combinatie van twee vernestelde if-structuren. Dit maakt een programma leesbaarder, zeker als er meerdere else if delen zijn.
21
@Helga.Naessens@hogent. be
Hoofdstuk 1. Basisopdrachten
if (
voorwaardel ) {
opdracht i a; opdracht i b; · }
else i f ( voorwaarde2 ) { opdracht2a; opdracht2b; }
else { · opdracht3a; opdracht3b; }
1.8
Een volledig programma
Nu we besproken hebben hoe de verschillende onderdelen van een programma er uitzien, kunnen .we eindelijk aangeven hoe je ze nu allemaal combineert tot een volwaardig programma, m.a.w., hoe de tekst er uitziet die je uiteindelÜk door de compiler laat verwerken. Bovenaan komen de volgende lijnen code: #include
using namespace std; Deze lijnen geven aan dat je wenst gebruik te maken van invoer- of uitvoeropdrachten. Als je in je programma mathematische functies (zoals cos en abs) gebruikt, plaatst je bovenaan ook nog de volgende lijn: #include Indien je de randomgenerator wenst te gebruiken, schrijf je eveneens de opdrachten #include #include .· Daarna komt het eigenlijk programma, dat gewoonlijk de hoofding int mainO heeft, gevolgd door accolades die de uit te voeren opdrachten bevatten. Als laatste opdracht in het hoofdprogramma schrijven we de volgende return-opdracht:
22
Hoofdstuk 1. Basisopdrachten
@Helga.Naessen{3@hogent. be
re.turn 0; Later komen we op deze opdacht nog wel eens terug. Ter illustratie geven we in Code 1.5 een programma dat eerst een geheel getal n inleest, gelegen in het gesloten interval [5, 100], en dat nadien het grootste getal bepaalt van n willekeurige positieve gehele getallen (die gegenereerd werden met de randomgenerator).
#include #include #include using namespace std; int mainO { //inlezen van n int n· ' cout <<"Geef n in (uit het interval [5,100]): "; cin >> n; while (n < 5 I I n > 100) { cout << "Foutief getal n! ! ! " << endl « "Geef n in (uit het interval [5,100]): "; cin >> n; }
srand(time(O));
//initialisatie randomgenerator
//n. g~tallen genereren en grootste bepalen cout << "Den gegenereerde getallen zijn: " << endl; int getal; int grootste = rand() ; cout << grootste << " " '· for (int i = 1 ; i < n ; i++) { getal = rand() ; cout << getal << " "·, if (getal > grootste) grootste = getal; }
//grootste getal uitschrijven cout << endi << "Het grootste getal is " << grootste; return 0; }
Code 1.5: Voorbeeld volledig programma Het symbool '/I' en alles wat verder op dezelfde lijn staat, wordt door de compiler volledig genegeerd. Dit onderdeel van een programma noemen we commentaar.
23
Hoofdstuk 1. Basisopdrachten
1.9
@Helga.Nà.essens@hogent. be
Tabellen
C++ kent ook tabellen, maar er is één belangrijke restrictie: het indexbereik van een tabel begint steeds bij nul. In een tabel van vijf gehele getallen worden de elementen dus .genummerd van nul tot vier. Een tabel wordt gedeclareerd door na de naam van de variabele tussen vierkante haakjes het aantal elementen aan te geven (dit is dus één meer dan de index ·van het laatste element). Het volgende programmafragment toont de declaratie van twee gehele variabelen i en j, een tabel tab van 10 gehele getallen (genummerd van nultot negen) en twee tabellen coord1 en coord2 van elk 2000 -reële getallen: double coord2[2000]; int i, tab[10], j; double coord1[2000]; Bemerk dat declaraties van gewone variabelen en van tabelvariabelen door elkaar mogen gebeure:p.. In dit geval is het onderstaande programmafragment echter duidelijker: int i, j; int tab[10]; double coord1[2000], coord2[2000]; Initialisatie bij declaratie kan je ook gebruiken bij tabelvariabelen. In dit geval schrijf je na het gelijkheidsteken de opeenvolgende waarden van de elementen van de tabel, tussen accolades en gescheiden door komma's: int dagen_per_maand[12]
= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Elementen van een tabel duid je aan met behulp van vierkante onderstaande codefragment: int tab [10]; for (int i = 0 ; i < 10 cin » tab [i] ;
haa~jes,
zoals in het
i++)
Ter illustratie geven we in Code 1.6 een programma dat een aantal gehele getallen inleest, gelegen in het gesloten interval [-10, 10], en dat nadien het getal op het scherm schrijft dat het meest werd ingelezen. De invoer stopt bij een getal dat niet tot het interval behoort. Als er meerdere getallen voor uitvoer in aanmerking komen, nemen we voor de eenvoud het kleinste. Dit programma maakt gebruikt van een tabel freqtab die dienst doet als frequentietabel om te tellen hoeveel keer de getallen uit het gegeven gesloten interval reeds ingelezen werden.
24
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
#include using namespace std; int main() { int freqtab[21];
//frequentietabel
//initialisatie van de frequentietabel for (int i = 0 ; i < 21 ; i = i+ 1) . freqtab [i] = 0; //inlezen van de getallen int g; cout << 11 Inlezen van getallen uit [-10,10] 11 << endl; cout << 11 Geef een getal : 11 ; cin » g; while (~10 <= g && g <= 10) { freqtab[g + 10]++; cout << 11 Geef een. getal: 11 ; cin » g; }
//zoeken van de (index van de) grootste frequentie int imax =0; for (int i = 1 ; i < 21 ; i = i+1) if (freqtab [i] > freqtab[imax]) imax = i; //uitvoer van het meest voorkomend getal ! = 0) 11 .cout << Het getal 11 << (imax - 10) << 11 werd het meest ingelezen. 11 << endl; else. cout << 11 Geen g~ldi ge getallen ingelezen. 11 << endl; return 0; i f (freqtab [imax]
}
Code 1.6: ·Programma ·dat gebruik maakt van een frequentietabel
25
Hoofdstuk 1. Basisopdrachten
1.10
@Helga.Naessens@hogent. be
;Het type hooi
Een logische uitdrukking zoalS 'a > 3' is 'waar' of 'onwaar'. Om dit aan te duiden gebruikt men in C++ de speciale waarden true en false van een zgn. 'logisch' typebool. Er kunnen ook variabelen van dit type gedeclareerd worden. Die bevatten dan logische waarden, zodat we bijvoorbeeld kunnen schrijven: bool b
=a >
3;
De waarde van een dergelijke variabele kan overal gebruikt worden waar een logische uitdrukking voorkomt, zoals bij deif-opdracht of de while-lus: if (b) { }
Het bovenstaande betekent dan hetzelfde als: i f (a > 3) .{ }
Logische variabelen worden ook vaak gebruikt om aan te duidendat een bepaalde grootheid slechts twee verschillende waarden kan aannemen. Stel bijvoorbeeld dat we een reeks gehele getallen moeten inlezen, die allen in het gesloten interval [1, 10] gelegen zijn. Het inlezen stopt bij het eerste ongeldige getal. Nadien willen we weten welke getallel.l uit dit bereik niet in de reeks voorkwamen. Daartoe volstaat het om bij elk geldig getal aan te .duiden dat het voorkwam. Een aanwezigheidstabel van 10 logische waarden is daarvoor zeer geschikt: een getal komt voor in de reeks of niet. Kiezen we bijvoorbeeld de waarde true om 'aanwezig' voor te stellen en false voor 'ontbrekend', dan krijgen we het programma Code 1.7. · Beginnende programmeurs hebben nogal vaak de verkeerde gewoonte om logische variabelen te testen door ze te vergelijken met trtie of false. Schrijf dus op de derde laatste regel van het programma Code 1.7 niet: if (aanwezig [i]
==
false)
//NIET zo!
. Tussen de haakjes van een i f of een while moet een logische uitdrukking staan, zoals 'a > 3', die true is of false. Een logische variabele is echter op zichzelf reeds een (eenvoudige) logische uitdrukking, en moet nergens meer mee vergeleken worden. Men schrijft . . toch ook niet: if ((a > 3) -- truè)
//natuurlijk NIET zo!
26
Hoofdstuk 1. Basisopdrachten
int main() { bool aanwezig[10];
@Helga.Naessens@hogent. be
//aanwezigheidstabel
//initialisatie van de tabel for (inti= 0 i< 10; i++) aanwezig [i] = false; //inlezen int g; cout << 11 Geef getal: 11 ; cin » g; while (1 <= g && g <= 10) { aanwezig[g - 1] = true; cout << 11 Geef getal: 11 ; cin » g; }
//resultaat .cout << 11 De volgende getallen komen niet voor: 11 << endl; for (int i = 0 ; i < 10 ; i++) if (!aanwezig [i] ) cout << (i + 1) << endl; return 0; }
Code 1.7: Maakt gebruik van een aanwezigheidstabel
1.11
De conditionele uitdrukking
In C++ bestaat er een uitdrukking van de volgende vorm:
voorwaarde ? uitdrukkingl : uitdrukking2 De waarde van een dergelijke conditionele uitdrukking is de waarde van uitdrukkingl als voorwaarde waar is en van uitdrukking2 als voorwaarde niet waar is. Naar betekenis lijkt een conditionele uitdrukking goed op een if-else-opdracht, maar ze wordt op een andere manier gebruikt. Het is geen opdracht maar een uitdrukking en als dusdanig slechts een onderdeel van een opdracht. In de omkaderde programmaregel moet er dus nog verteld worden wat er met de waarde van die uitdrukking moet gebeuren (zoals testen, of toewijzen aan). Merk op dat een conditionele uitdrukking steeds een 'als'- en een· 'als niet'-kant. bevat. Je kan het ':'-gedeelte niet weglaten zoals het else-gedeelte van een if~opdracht.
27
@Helga.Naessens@hogent. be
Hoofdstuk 1. Basisopdrachten
Ter illustratie geven we een aantal voorbeelden van conditionele uitdrukkingen die in een opdracht gebruikt worden. Het eerste voorbeeld stopt de absolute waarde van b in a: a = (b
> 0? b : -b);
De equivalente if-else-opdracht is: if (b > 0) a = b;
el se a
= -b;
Het tweede voorbeeld telt 17 of 13 op bij c, al naargelang de waarde van d: ' . c += (d == 6? 13 : 17);
Hierbij is de equivalente if-else-opdracht: i f (d == 6) e += 13;
el se c += 17;
In het volgende voorbeeld wordt er gekozen tussen de enkelvouds- of de meervoudsvorm afhankelijk van de waarde van x:
cout << x << (x != 1 ? " oplossingen" : " oplossing") << endl; Je mag de ene conditionele uitdrukking ook binnen de andere gebruiken, zoals in het onderstaande programmafragment dat het teken van een getal bepaalt:
double getal; cin » getal; cout <<."teken: " << (getal > 0 ? 1
1.12
getal < 0 ? -1
·O) << endl;
Gebruik van bibliotheken
Zoals we reeds in een voorgaande paragraaf vermeld hebben, bevat de taal C++ zelf geen ingebouwde procedures of functies, maar vinden we er wel terug in (code)-bibliotheken zöals de cmath-library. Eventueel kan er ook een eigen bibliotheek (met zelf gedefinieerde functies) gemaakt worden. I
Alvorens een bibliotheek te kunnen gebruiken, moet ze eerst geïncludeerd worden d.m.v. een #include. Hierbij wordt er een onderscheid gemaakt tussen standaardbibliotheken
28
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
(meegeleverd met de compiler) en zelfgemaakte bibliotheken. Namen van standaardbibliotheken worden tussen < en > genoteerd, zoals in: #include · Voor zelfgemaakte· bibliotheken worden dubbele aanhalingstekens gebruikt: #include "datums.h" Hierbij is datums .h bijvoorbeeld een zelfgemaakte bibliotheek met allerlei functies die betrekkjng hebben op datums. Vroeger eindigde elke bibliotheeknaam op ".h" (eerste letter van "header", zie verder), maar tegenwoordig schrijft men voor standaardbibliotheken de ".h" niet meer. Naast functies en procedures kunnen bibliotheken ook constanten en klassen definiëren. Zo definieert de cmath-bibliotheek oa. de constante M_PI = 3.14159 ...
1.12.1
Declaraties van functies
Als men een functie wenst te gebruiken, hoeft men over het algemeen de interne werking hiervan niet in detail te kennen. De gebruiker van de functie moet enkel weten wat de functie doet, wat voor argumenten er verwacht worden, en wat· er teruggegeven wordt als resultaat. Wie bijvoorbeeld de functie sin uit de cmath-bibliotheek wil gebruiken, is meestal niet geïnteresseerd om te weten hoe die sinus precies berekend wordt (waarschijnlijk m.b.v. één of andere reeksontwikkeling). Het enige wat de gebruiker moet weten,_ is dat die functie een reëel getal als parameter (=argument) verwacht en dat het resultaat opnieuw een reëel getal is. Ais je het bestand cmath.h bekijkt, dan tref je daar (onder andere) de volgende regel aan: double sin(double x); Dit is de declaratie van de functie sin. Tussen de haakjes staat het type e:p. de naam van de parameter, vóór de functienaam staat het type van het resultaat (kortweg returntype genoemd). De naam van de parameter (x in ons geval) heeft hier eigenlijk geen belang1 . Wie de declaratie leest, weet in principe genoeg om de functie te kunnen gebruiken. De volgende opdrachten maken bijvoorbeeld correct gebruik van de voorgaande functie sin: double y; double.a = 3.14; y = sin(a); 1
Soms wordt ze ook gewoonweg weggelaten.
29
Hoofdstuk 1. Basisopdrachten
y y
@Helga.Naessens@hogent. be
= sin(2 . 1); sin(a +
2~3);
In principe kun je de headerbestanden lezen, maar vaak zitten de declaraties verstopt in een boel ingewikkelde code. Dit is zeker het geval voor standaardbibliotheken. Er is echter over standaardbibliotheken zeer veel documentatie te vinden, zowel in boeken als op het internet. In de meeste informatiebronnen wordt voor elke besproken functie de declaratie gegeven, zodat het belangrijk is dat je die begrijpt. Een functie kan ook meerdere parameters bevatten, zoals in de volgende declaratie: int ggd(int a, int b);
11 bepaalt de grootste gemene deler /I van de gehele getallen a en b
Deze functie kan bijvoorbeeld als volgt opgeroepen worden: cout « ggd(28, 12); Vaak zijn de parameters van een functie niet allemaal van hetzelfde type. Bij .het oproepen van e_en dergelijke functie is het dim vooral van belang dat men de parametersin de juiste volgorde en met het juiste type opgeeft. We zullen hier later nog op terugkomen. Verder in deze cursus zullen we ook nog .bespreken hoe je zelf functies kan schrijven. Hiervoor moet er behalve een declaratie Ook een definitie (i.e. de interne werking van de functie) gemaakt worden. ·
1.12.2
Declaraties van procedures
Een procedure is niets anders dan een functie die niets terUggeeft. Dit wordt aangeduid door als returntype het sleutelwoord void (mid. vertaling: leegte, niets) te gebruiken. Een voorbeeld van een declaratie van een procedure is: void toon(double d, int n);
//drukt het reeel getal d af op het. //scherm met n cijfers na de komma
Het oproepen van deze procedure gebeurt bijvoorbeeld als volgt: toon(4.12345, 3); Omdat een procedure niets teruggeeft, is het bijvoorbeeld verkeerd iets als cout << toon(4.12345, 3); te schrijven.
30
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@hogent. be
Tot slot wensenwe nog op te merken dat vele informatiebronnen geen gebruik maken van de term "procedure", maar hiervoor ook de benaming "functie" gebruiken.
1.12.3
Tabelparameters
Functies en procedures kunnen ook tabellen als parameters verwachten. Dit wordt in de declaratie op één van de volgende manieren aangegeven: double som(double .tab[], int n);
//berekent de ·som van de //elementen van de tabel //berekent de som van de //elementen van de tabel
double som(double *tab, int n);
eerste n tab eerste n tab
In de tweede versie betekent het sterretj~ dat tab hier als een pointer geïnterpreteerd wordt (zie Hoofdstuk 9). Qua gebruik is er geen verschil tussen de beide vormèn. Voor de beide versies kan de functie som immers als volgt opgeroepen worden: double rij[5] = {1.0, 2.0, 3.0, 4.0, 5.0}; cout « som(rij, 3); //6.0 komt op het scherm Merk op dat men geen vierkante haakjes moet schrijven bij het oproepen van een dergelijke functie. Ook hier geldt dat de naam van de parameter in de declaratie (tab in dit geval) geen enkel belang heeft voor de oproeper (en vandaar soms ·ook weggelaten wordt in de ~eclaratie van de functie). Op een analoge wijze kan de procedure void schrijf(int tab [] , int n); //schrijft de eersten elementen //van de tabel tab op het scherm als volgt opgeroepen worden: int rij[3] = {1, 2, 3}; schrijf(rij, 3);
1.12.4
Namespaces
Indien men meerdere bibliothekenincludeert, dan bestaat de kans dat een bepaalde functie in meer dan één bibliotheek voorkomt. Zolang deze furieties een verschillend aantal · parameters hebben of zolang (bij eenzelfde·aantal parameters) de argumenttypes van deze functies verschillend zijn, is er geen probleem. Men spreekt dan immers van overloading, waarbij de compiler op basis Vé}ll het aantal en het type van de parameters zal uitmaken 31
Hoofdstuk 1.
B~isopdrachten
@Helga.Naessens@hogent. be
welke fu11ctie dient opgeroepen te worden. Indien er echter twee functies bestaan waarvan de naam, het returntype, het aantal en de types van de parameters dezelfde zijn, is er een probleem. De compiler zal dan immers onmogelijk kunnen beslissen welke van deze twee functies er moet geactiveerd worden. Veronderstel bijvoorbeeld dat de functie int som(int a, int b); zowel in "abc.h" als in "def.h" gedefinieerd wordt. Als nu in een programma beide bibliotheken geïncludeerd worden en in het programma de volgende procedure-oproep geschreven staat: cout << som(123, 4567); dan kan de compiler niet weten welke versie van de functie som bedoeld wordt. Om dit probleem op te lossen, heeft men namespaces uitgevonden. Een namespace is een groepering van declaraties van verwante gegevens, functies/procedures, etc .. Men geeft deze groep een (unieke) naam. Deze naam wordt alsprefix (voorvoegsel) aan de functieof procedurenaam gevoegd, gevolgd door twee dubbele punten. Bibliotheek "abc.h" zou bijvoorbeeld zijn functies kunnen onderbrengen in een namespace met de naam "abcspace". Wie de functie som wil oproepen, nioet dart de volledige naam abcspace:: som gebruiken. Indien "def.h" zijn functies onderbrengt in namespace "def", dan is de volledige naam van zijn versie van de functie def : :som. Door dit (unieke) voorvoegsel is het conflict van daarnet opgelost. Nu rijst bij sommige lezers misschien de vraag wat er precies moet gebeuren indien men zeker weet dat er geen conflicten kunnen optreden (bv. omdat er slechts één bibliotheek geïncludeerd wordt)? Moet dan overal die prefix toegevoegd worden? (Dit is immers veel werk, en draagt niet bij tot de leesbaarheid van het programma.) Het antwoord is "neen" . Door middel van de opdracht using namespace XXX; kan men er immers voor zorgen dat de prefix XXX: : mag weggelaten worden. Bijvoorbeeld: #include "abc.h" using namespace abcspace; · int main() { cout « som(123, 4567); cout « abcspace::som(123, 4567); }
32
//ok // ook .ok
Hoofdstuk 1. Basisopdrachten
@Helga.Naessens@bogent. be
Alle functies en procedures vim de standaardbibliotheken (string, cmath, iostream) zijn ondergebracht in de namespace std. In de meeste programma's zul je dan ook bovenaan using namespace std;
aantreffen. Op die manier hoef je nergens de volledige namen te schrijven, zoals std: :cout en std: : abs.
33
Hoofdstuk 2 De types cbar en string Na wat je tot nog toe geleerd hebt, zou je stilaan de indruk krijgen dat programma's enkel worden g()bruikt bij het oplossen van mathematische problemen. Dit komt omdat we tot nog toe slechts gebruik gemaakt hebben van numerieke gegevenstypes zoals int en double én dat daarom onze voorbeelden zich tot n~merieke problemen hebben beperkt. C++ ondersteunt echter ook nog een aantal gegevenstypes die niets met wiskunde te maken hebben, zoals de types char en string, die we hier in dit hoofdstuk zullen bespreken.
2.1
Het type char
Variabelen van het type char worden gebruikt voor het opslaan van karakters. Karakters Zijn de onderdelen waar woorden en zinnen van gemaakt zijn: kleine letters, hoofdletters en leestekens maar ook cijfers en spaties. Behalve deze die we zojuist hebben opgesomd, herkennen de meeste computers ook nog andere tekens, maar deze verschillen van computer tot computer en van land tot land. Dergelijke speciale tekens (zoals letters met accenten, Griekse letters of mathematische symbolen) zullen we in deze cursus niet gebruiken. Een variàbele van het type char kan slechts één karakter tegelijkertijd bevatten. Als we een volledig woord willen opslaan, moeten we een tabel van karakters (of het type string: zie verder) gebruiken. De volgende figuur stelt twee variabelen teken en woord voor. De variabele teken is van het type char en bevat één enkel teken terwijl woord een tabel is van 9 karakters.
Bovenstaande variabelen kan je als volgt declareren en initialiseren: char
teken
=
'%'; 34
Hoofdstuk 2. De types char en string
char
©Helga.Naessens@hogent. be
woord[9] = {'U', 'T', 'P', '- ', 'k', 'a', 'b', 'e', '1'};
Je merkt dat karakters in een programma steeds tussen aanhalingstekens worden geplaatst. Gebruik steeds enkelvoudige aanhalingstekens. Dubbele aanhalingstekens worden gebruikt voor strings. Het is belangrijk dat je de aanhalingstekens niet vergeet, omdat anders het programma een volledig andere betekenis kan krijgen. Een procentteken dat niet tussen aanhalingstekens staat wordt bijvoorbeeld geïnterpreteerd als de restoperator, terwijl er mét de aanhalingstekens gewoon een .karakter staat dat er toevallig uitziet als een procent. . . Eén gewone letter die tussen aanhalingstekens staat is een karakter van het type char, · terwijl een letter zonder aanhalingstekens beschouwd wordt als de naam van een variabele . . Bekijk bijvoorbeeld de volgende declaraties en toewijzingen: char
a, b = 'z'.;
ä ::::: 'b';
a = b; De eerste toewijzing aan a stopt de kleine letter b in de variabele a. De tweede toewijzing copieert de inhoud van de variabele b naar de variabele a, m.a.w., de kleine letter z. Verwar ook niet de tien getallen van één cijfer en de tien verschillende cijfertekens. De toewijzing 'i = 9' wordt gebruikt als i vali het type int is, terwijl de toewijzing i = '9' alleen zinheeft voör een variabele i van het type char. Je kan karakters nietbij elkaar optellen, ook al zien ze er uit als getallen. Voor rekenkundige bewerkingen gebruik je int of double en niet char. Er zijn drie karakters die op een speciale manier genoteerd worden, ·omdat de compiler anders in de war zou geraken: het aanhalingsteken, het dubbel aanhalingsteken en de zogenaamde backslash (het omgekeerd delingsteken: \). In deze drie gevallen plaats je vóór het karakter een 'backslash' en plaats je het geheel dan tussen aanhalingstekens. Deze backslash-conventie wordt trouwens ook gebruikt bij lettE)rlijke tekst in een schrijfopdracht. We geven een aantal voorbeelden: char quote='\'', dquote = '\"',backslash. =;\\'; cout <<"Hij zei:\"\'s Avonds om drie uur !\"." << endl; cout « "Twee schuine · strepen: \\\\." << endl; Ook allijkt het misschien niet zo, de drie variabelen quote, dquote en backslash bevatten elk slechts één karakter. De ·laatste schrijfopdracht drukt slechts twee schuine strepen (backslashes) af:
35
Hoofdstuk 2. De types char en string
2.1.1
@Helga.Naessens@hogent. be
Karakters in schrijfopdrachten
Om karakters uit te schrijven, gebruiken we een schrijfopdracht die dezelfde vorm heeft als bij getallen. Dit wordt geïllustreerd in programmafragment Code 2.1 , dat de letter a vijf keer onder elkaar afdrukt.
ehar eh = ' a' ; for (int i = 0 i < 5 eout << eh << endl;
i++)
Code 2.1: Een karakter uitschrijven
Door in de schrijfopdracht gebruikt te maken van het woordje endl, wordt er na de inhoud van de variabele eh (wat de kleine letter a is) telkens een speciaai karakter 'afgedrukt' dat we (in het Engels) een linefeed of newline noemen; Wanneer we dit karakter afdrukken, dan verschijnt er niet echt een teken op hetscherm (of op papier), maar neemt de computer op die plaats een nieuwe lijn, zodat alles wat daarna wordt uitgeschreven automatisch op de volgende lijn verschijnt. Dit karakter is ook van het type ehar en wordt als '\n' genoteerd. In plaats van '<< endl' kunnen we overal '<< '\n'' schrijven om hetzelfde effect te bekomen 1 . Net zoals bij de drie andere 'backslash'karakters, kunnen we dezelfde notatie ook gebruiken bij tekst tussen dubbele aanhalingstekens. Dit betekent bijvoorbeeld dat · de volgende drie lijtien hetzelfde effect hebben: .
eout << "Een roos is een roos is een roos" << endl; eout << "Een roos is een roos is een roos" << '\n'; eout << "Een roos is een roos is een roos\n"; Een dergelijke linefeed mag gerust méér dan één keer in dezelfde tekst voorkomen, zoals in de volgende reeks opdrachten:
eout << "Een lijn\n"; eout << "en\nnog twee lijnen.\n" ; Op het scherm vers<;hijnt dan het volgende:
Een lijn en nog twee lijnen. Een ander speciaal karakter waarover C++ beschikt en waar we nog regelmatig gebruik van zullen maken in deze cursus, is het karakter '\ t ' . · Dit karakter stelt de tab voor. Ook 1
Dit doet je natuurlijk denken dat endl een variabele is van het type char die met een linefeed werd geïnitialif!eerd. Dit is niet het geval. endl kan alleen in een schrijfopdracht gebruikt worden en bijvoorbeeld niet aan een andere variabele worden toegewezen.
36
Hoofdstuk 2. De types char en string
@Helga..Na.essens@hogent. be
dit karakter kunnen wé gebruiken in een schrijfopdracht, met als gevolg dat er dan naar de volgende tabplaats overgegaan wordt. Samen met de spatie (' ') en de newline (' \n') vormt de tab (' \ t ') de groep van de white spaces, een term die we nog vaak zullen tegenkomen in deze cursus.
2.1.2
Inlezen van een karakter
Om een karakter in te lezen, kunnèn we gebruik m11ken van de »-operator, zoals i~ het programmafragment uit Code 2.2: ehar eh; eout << "Geef een karakter in : "; ein >> eh; eout << "Het karakter. was"<< eh << endl; Code 2.2: Een karakter inlezen
In dit programmafragment wordt eerst een variabele eh van het type ehar gedeclareerd. Vervolgens wordt ·aah de gebruiker gevraagd om een karakter in te tikken. De opdracht 'ein » eh;' betekent dat het karakter dat de gebruiker intikt in de variabele eh terecht- · komt. We dienen hier echter wel op te merken dat de variabele eh slechts één karakter kan ·bevatten. Indien de gebruiker dus meerdere karakters intikt (alvorens op enter te drukken), dan zal enkel het eerste karakter (indien het geen white space is) in eh belanden. Verder dienen we ook op te merken dat de operator » alle white spaces overslaat. Het voorgaande programmafragment zal dus enkel het eerste karakter inlezen dat geen white space is en dit daarna ook uitschrijven. Meer informatie over deze eigenschap van de operator » vind je in. paragraaf 11.2 (met als titel: ~nvoer via het toetsenbord). In sommige gevallen kan het echter belangrijk zijn dat de white spaces niet genegeerd worden. Bijvoorbeeld: stel dat je een programma wenst te schrijven dat een tekst exact kopieert zoals die ingegeven werd, inclusief de white spaces. Door gebruik te maken van (enkel) de »;..operator zouden we :riiet tot een dergelijk programma kunnen komen. Hiervoor hebben we een nieuw soort leesopdracht nodig, nl. de functie cin.get. De uitdrukking 'cin. get ()' leest één erikel karakter in en geeft dit karakter als waarde terug. Dit wordt geïllustreerd in het volgende programmafragment: ehar eout eh = eout
eh; << "Geef een karakter in: "; ein.getO; << "Het karakter was " << eh << endl;
De programmafragment zal aan de gebruiker vragen om een karakter in te tikken en het 37
Hoofdstuk 2. De types char en string
@HeJga.Naessens@hogent. be
eerste karakter dat de gebruiket ingetikt heeft, opslaan in de variabele eh. In tegenstelling tot in het programmafragment uit Code 2.2, kan eh hier wel een white s:pace bevatten. Als laatste voorbeeld op het gebruik van ein.get, beschouwen we het programma'uit Code 2.3. Dit programma vraagt aan de gebruiker om één lijn in te tikken en schrijft daarna een exacte kopie van de ingetikte lijn terug op het scherm. Om het einde van de lijn te kunnen herkennen, kunnen we hier geen gebruik maken van de operator>>, maar zoeken we onze toevlucht tot de functie e;in. get. Merk tevens. op dat we er van uitgaan dat een lijn uit niet meer dan 80 karakters bestaat (andE;lrs is de tabellijn te klein). int main() { ehar lijn[80]; int pos = 0; ehar eh; eout << "Typ 1 lijn in:" << endl; eh = cin.get(); · while (eh != '\n') { lijn[pos++] = eh; eh = ein.getO; }
eout << "U typte deze lijn in: " << endl; for (int i = 0 ; i < pos ; i++) eout « lijn [i] ; return 0; . }
Code 2.3: Een lijn inlezen Bijkomende informatie over de functie ein ..get (en de bijhorende procedure ein. putbaek) vind je terug in paragraaf 11.3 (met als titel: Inlezen van één enkel karakter). Voor extra voorbeelden waarin gebruik gemaakt wordt van ein.get verwijzen we naar paragraaf 11.4 (met als titel: Nog meer voorbeelden van invoer).
2.1.3
Bewerkingen met karakters
We hebben reeds gezien dat karakters kunnen ingelezen en uitgeschreven worden. Ook andere bewerkingen die je vroeger hebt toegepast op getallen, kan je nu toepassen op karakters. Zo kan je bijvoorbeeld nagaan of twee karakters al dan niet .aan elkaar gelijk zijn. In het programma uit Code 2.3 maakten we hiervan reeds gebruik, namelijk om op zoek te gaan naar de linefeed moesten we de variabele eh vergelijken met het karakter '\n'. Ook in het programmafragment Code 2.4 vetgelijken we karakters met elkaar om na te gaan of 38
@Helga.Naessens@hogent. be
Hoofdstuk 2. De types char en string
de gebruiker bepaalde gegevens wilt bewaren. ehar antwoord; eout << "Wilt u deze gegevens bewaren? (J/N) " << endl; ein >> antwoord; i f (antwoord == 'j ' I I antwoord == 'J') { //gegevens bewaren }
Code 2.4: Invoer van karakters
Drijf de analogie tussen karakters en getallen echter niet te ver: je kan karakters niet bij elkaar optellen of met elkaar vermenigvuldigen, zelfs al zien ze er toevallig uit als cijfers. De verschillende karakters in C++ zijn geordend volgens een soort uitgebreid alfabet. Dit alfabet verschilt van computer tot computer, maar de volgende regels zijn geldig: • De kleine letters 'a' t.e.m. 'z' staan in de verwachte volgorde en bovendien bevinden er zich tussen de kleine letters geen andere karakters. • De hoofdletters 'A' t.e.m. 'Z' staan eveneens in de verwachte volgorde, zonder andere tussenliggende karakters. • De cijfertekens '0' t.e.m. '9' staan in numerieke volgorde, eveneens zonder andere tussenliggende karakters. Over andere karakters en andere verbanden kan er niets met zekerheid gezegd w9rden. Een vraagteken kan· vóór of na de cijfers komen, bij de ene soort computers worden de hoofdletters gevolgd door de kleine letters, bij een andere computer. is het net andersom, enz. Deze ordening wordt gebruikt wanneer we karakters met elkaar vergelijken (met behulp van < of>= bijvoorbeeld), zoals in het programmafragment uit Code 2.5. ehar eh; if ((eh eout else if eout el se eout
>= 'a' && eh <= 'z') I I (eh >= 'A' && eh <= 'Z' )) << eh << " is een letter" << endl; (eh >= '0' && eh <= '9') << eh << " is een eijferteken" << endl; << eh << " is een leesteken" << endl; Code 2.5: Bepaalt het type karakter
39
Hoofdstuk 2. De types char en string
@Helga.Naessens@hogent. be
Het heeft geen zin om twee karakters bij elkaar op te tellen, maar je mag. wel een geheel getal bij een karakter optellen of ervan aftrekken. Wanneer ik 1 bij een karakter optel, krijg ik gewoon het volgende karakter in het alfabet, wanneer ik 1 aftrek, krijg ik het vorige karakter. Een positief getal optellen bij een karakter, verschuift dit karakter evenveel plaatsen vooruit in het alfabet, een negatief getal erbij optellen keert terug in het alfabet. Dus 'a' +3 is niets anders dan 'd' en 'Z '-6 is een andere schrijfwijze voor 'T'. Let op echter wel op: '5' +4 is inderdaad '9', maar '5' +5 heeft niets meer met 10 te maken en is één of ander karakter dat van machine tot machine verschilt. Het optellen van getallen bij karakters gebeurt nog het meest in while-lussen van de volgende vorm: ehar eh; eh = 'A'; while (eh .<= 'Z') { eout << eh; eh++; }
eout << endl; Dit fragment schrijft het volgende op het scherm:
ABCDEFGHIJKLMNOPQRSTUVWXYZ Alhoewel het geen zin heeft om twee karakters bij elkaar op te tellen of met elkaar te vermenigvuldigen, is het wel toegelaten om twee karakters van elkaar af te trekken. Het resultaat is dan een getal dat aangeeft hoeveel posities beide karakters in· het gebruikte alfabet van elimar verwijderd zijn. Deze bewerking heeft de volgende belangrijke toepassing: ze kan gebruikt worden om een cijferteken om te zetten naar het corresponderende getal (trek er gewoon het teken '0' van af!). Tot slot van deze paragraaf wensen we hier nog te vermelden dat de ctype-bibliotheekheel wat interessante functies bevat die met karakters te maken hebben. Zo bevat ze o.a. de functîes isalpha (om na te gaan of een karakter een letter is), isdigit (om na te gaan of een karakter een cijferteken is), islower (om na te gaan of een karakter een kleine letter is) en isupper (om na te gaan of een karakter een hoofdletter is). Het programmafragment uit Code 2.6 doet dus net hetzelfde als het programmafragment uit Code 2.5, maar deze keer wordt er gebruik gemaakt van functies uit de ctype-bibliotheek. Ook d~ procedures tolower en toupper uit de ctype-bibliotheekkunnensoms handig van pas komen. Waarvoor en hoe je deze procedures gebruikt, moet je zelf maar eens nagaan.
40
@Helga.Naessens@hogent. be
Hoofdstuk 2. De types char en string
ehar eh; if (isalpha(eh)) eout << eh << 11 is een letter 11 << endl; else if (isdigit(eh)) eout << eh << 11 is een eijferteken 11 << endl; el s e eout << eh << 11 is een leesteken 11 « endl; Code 2.6: Bepaalt het type karakter
2.2
Het type string
De term string is een engeis woord dat zou kunnen vertaald worden als karaktersliert en duidt op een aaneenschakeling van een áantal karakters. Strings kunnen alle karakters bevatten, zoals letters, cijfers, leestekens, . witruimte... Letterlijke strings worden tussen dubbele aanhalingstekens genoteerd, zoals 11 C++ is gemakkelijk! 11 en 1111 (dit is de lege string). In C++ wordt er een onderscheid gemaakt tussen C-strings en standaard strings. Een: C-string wordt gekenmerkt door een tabel van karakters die afgesloten wordt met een speciaal karakter, nl. het nulkarakter. Vele C/C++ bibliotheken, vooral oudere, maken gebruik van deze C-strings. Ze zijn echter vrij omslachtig in het gebruik en we stellen de verdere bespreking vàn dergelijke strings dan ook uit tot in een later hoofdstuk. De standaard string is recenter, en is veel gemakkelijker te gebruiken. Als we het vanaf nu hebben over een "string", dan bedoelen we hiermee een "standaard string". Strings kunnen opgeslagen worden in variabelen van het type string.
2.2.1
Declareren, initialiseren, inlezen en uitschrijven van strings
In Code 2.7 is een eerste voorbeeldprogramma dat gebruik maakt van strings weergegeven. Het programma vraagt ·aan de gebruiker om zijn naam in te geven, waarna de naam opnieuw uitgeschreven wordt. Wanneer het programma uitgevoerd wordt, bekomèn we bijvoorbeeld de volgende uitvoer: G.eef uw naam in : Peter Hallo Peter
41
Hoofdstuk 2. De types cbar en string
©Helga.Naessens@hogent. be
#include #include <string> using namespace std; int main() { string naam; cout << ''Geef uw naam in: "; cin >> naam; cout << "Hallo " << naam << endl; return 0; }
Code 2.7: Eerste voorbeeld met strings Zoals je kan zien in de code van het programma gebeurt de declaratie, het inlezen en het uitschrijven van strings op een analoge manier als bij de andere datatypes die we reeds gezien hebben. Het gebruik van de »-operator om strings in te lezen, brengt echter nog een -klein probleem met zich mee, zoals geïllustreerd wordt door de volgende uitvoer: Geef uw naam in: Hallo Peter
Peter Pan
Zoals je kan zien, kan er met de >>-operator slechts êén woord ingelezen worden, tot aan de white space. Zoals gebruikelijk voor deze operator worden ook de beginnende white spaces overgeslagen. In ons programmavoorbeeld zal na het inlezen van de naam de variabele naam dus enkel het woord Peter bevatten. Indien je echter wenst dat zowel de voornaam als de achternaam ingelezen worden, kan je beter direct een volledige lijn inlezen met behulp van de procedure getline. Het gebruik van deze procedure wordt geïllustreerd in Code 2.8. int mainO { string naam; cout << "Geef uw naam in: •1 ; getline(cin, naam); cout << "Hallo " << naam << endl; return 0; }
Code 2.8: Het gebruik van getline
De functie getline bezit twee parameters. De eerste parameter duidt aan van welke stream (irivoerkanaal) de lijn moet gelezen worden (hier: cin) . De tweede parameter bepaalt in welke variabele de ingelezen lijn bewaard ·dient te worden (hier: de variabele naam). In tegenstelling tot de »-operator slaat de functie getline geen enkele white space over. Een mogelijke uitvoer voor het programma in Code 2.8 zou dus kunnen zijn:
42
Hoofdstuk 2. De types char en string
@Helga.Naessens@hogent. be
Geef uw naam in: Peter Pan Hallo Peter Pan Samengevat bekomen we de volgende tabel: Inlezen van strings: » versus getline cin >> naam; =? slaat eerst alle white spaces over,
leest dan één woord in (tot aan een white space) of getl ine ( c in, naam) ; :::;. leest één lijn in, slaat geen white spaces over Strings kunnen niet alleen ingelezen en/ of uitgeschreven worden. Men kan er ook voor opteren om hen vanuit het programma een waarde toe te kennen d.m.v. de toekenningsoperator =, zoals in het volgende programmafragment: string s = "Hallo"; string t; t = "Alles goed?"; string u; u = t;
2.2.2
Bewerkingen met strings
Strings kunnen {net zoals getalien en karakters) met elkaar vergeleken worden door gebruik te maken van relationele operatoren(==, !=, <, >, <=, >=). Bijvoorbeeld: string s; cout << "Geef een aantal woorden in. Type stop om te stoppen. " « endl; cin ». s; while (s != "stop") { cin >> s; }
Een string wordt beschouwd ~ls kleiner dan een andere string indien hij eerder in een woordenboek zou staan. Men noemt dit soms "alfabetische volgorde", maar de juiste term is "lexicografische volgorde" . De volgende vergelijkingen zijn bijvoorbeeld allemaal waar (true):
43 "
Hoofdstuk 2. De types char en string
"aap" < "noot" "noot" >= "mies" "kom" < "koinma"
"" < "abc"
@HeJga.Naessens@hogent. be
//kortste string is de kleinste //lege string is korter dan elke andere
Om twee strings te concateneren (achter mekaar te plakken) gebruik je de operator +: string s= "Com" ; string u; u = s + "put er"; u += '!';
I /geeft "Computer;' //geeft "Computer!"
Merk op dat uit de laatste opdracht blijkt dat je aan een string ook een karakter kan toevoegen. Let echter wel op: twee letterlijke strings concateneri:m met + gaat niet! De volgende opdracht is dus niet toegestaan: u= "Hallo " + "Peter.";
//FOUT!!!!!
Om de individuele karakters van een string te bekomen, wordt gebruik gemaakt van dezelfde notatie als bij tabellen: u= "Hallo Peter"; cout << u[O] << endl; u[1] = 'e'; cout << u;
//op het scherm komt: H //tweede karakter wordt vervangen //op het scherm komt: Hello Peter
Van een string kan je ook gemakkelijk zijn lengte (d.w.z. het aantal karakters) bepalen door gebruik te maken van de (lid)functie size 0: · string u= "Hallo Peter"; cout << u.size(); //op het scherm komt : 11 Let op de object-georiënteerde notatie bij het oproepen van deze lidfunctie! Een string is eigenlijk een object van de klasse string. Het puntJe betekent dat men "iets vraagt aan object u''. De lege haakjes moeten er staan: we roepen een (lid)functie op zonder paraméters. De klasse string voorziet nog veel meer lidfuncties en lidprocedures, bijvoorbeeld om te zoeken in strings naar karakters of deelstrings, om delen van strings te verwijderen of te wijzigen, enz. Een volledige lijst kan men vinden op .Qet net. In de oefeningen wordt hier ongetwijfeld dieper op ingegaan.
44
Hoofdstuk 3 Samengestelde gegevenstypes We hebben in de vorige hoofdstukken reeds verschillende gegevenstypes ontmoet: basisgegevenstypes zo.als int, double, bool en char, en tabellen als samengesteld gegevenstype. In dit hoofdstuk· breiden we tabellen uit naar tweedimensionale tabellen en introduceren we nog een nieuw samengesteld gegevenstype: structs.
3.1
Tweedimensionale tabellen
C++ laat ons toe om tabellen te gebruiken waarvan de elementen niet gewoon getallen of karakters zijn, maar op hun beurt tabellen. De volgende declaratie introduceert bijvoorbeeld een tabel tab waarvan de drie elementen zelf tabellen zijn die op hun beurt elk twee gehele getalen bevatten:. int tab [3] [2] ;
De volgende figuur geeft een idee van hoe zo'n tabel eruit ziet:
talt{2}
In deze tabel duiden we het element met waarde 1 bijvoorbeeld aan als tab [0] [0] en het element met waarde 6 als tab [2] [1]. Inderdaad, het laatste element van tab heet tab [2] en is zelf een (kleine) tabel van twee gehele getallen. In ons geval bevat ze de getallen 5 en 6. Het tweede element van die kleine tabel heet dus tab [2] [1] en heeft de waarde 6.
45
Hoofdstuk 3. Samengestelde gegevenstypes
©Helga.Naessens@hogimt. be
Net zoals alle variabelen kunnen deze tabellen ook geïnitialiseerd worden. De tabel uit de figuur bekom je bijvoorbeeld met de volgende declaratie: int tab[3] [2]
= {{1, 2}, {3, 4},
{5, 6}};
Het is gebruikelijk om dergelijke tabellen van tabellen voor te stellen als rechthoekige roosters, onderv:erdee~d in hokjes die de verschillende elementen bevatten:
t:tblO] t.a.bfl]
tabi.2J
·2
1---+---ooo~
3
4
5
6
t------ii----1
---"-·-~
" tab.f.2.Jr.l] In die zin spreekt men dan van een tweedimensionale tabel. De elementen van de tabel zijn dan de rijen van het rooster en het element tab[i] [j] bevindt zich op de i-de rij en de j-de kolom (als we 4e rijen en. kolommen teilminste vanaf nul nummeren) . . Om dit beeld iets beter tot uiting te laten komen, noteert men de initialisatie van een dergelijke tabel trouwens liever als volgt: int
tab[3][2]
= {{1,
2},
{3, 4}, {5, 6}};
We geven nu een voorbeeld van een toepassing die gebruik maakt van een tweedimensionale1 tabel: . In een klas van 20 studenten heeft elke student examen afgelegd van 7 verschillende vakken (gequoteerd op 20). Niet elk vak is even belangrijk, en daarom krijgt elk vak een bepaald gewicht (een geheel getal). Gevraagd wordt een programma te schrijven dat voor elk vak de behaalde punten inleest, en dat daarna voor elke student het .eindresultaat (op 20) op het scherm schrijft, en voor elk vak het gemiddelde resultaat van de klas. De oplossing is weergegeven inCode 3.1. Merk op dat we in dit programma gebruik gemaakt hebben van 2 constanten (AANTVK en AANTST) om de tabellen te declareren. Dat maakt het programma immers leesbaarder, en bovendien is het hierdoor eenvoudiger om het programma aan te passen .met nieuwe waarden voor deze constanten. Zoals we reeds vroeger vermeld hebben, is het de gewoonte om namen van constanten in hoofdletters te schrijven. 1
C++ stopt niet bij twee dimensies. Op een voor de hand liggende manier kan men ook tabellen van tabellen van tabellen declareren en tabellen van nog hogere dimensie.
46
Hoofdstuk 3. Samengestelde gegevenstypes
@Helga.Naessens@hogent. be
#include using namespace std; int mainO { const Înt AANTVK = 7; const int AANTST = 20; int gewichten[AANTVK] = {100, 225, 150, 100, 175, 225, 200}; double punten[AANTST] [AANTVK]; for (int v = 0 ; v < AANTVK ; v++) { cout << 11 Geef de punten voor vak nummer 11 << (v + 1) << endl; for (int s = 0; s < AANTST; s++){ cout << " van student nummer 11 << (s + 1) << cin >> punten[s][v];
11
•
11
} }
//berekenen van het totale gewicht int totgew = 0; for (int v = 0 ; v < AANTVK ; v++) totgew += gewichten[v]; //berekenen van het resultaat per student · double totaal; cout << 11 De eindresultaten zijn: 11 << endl; for (int s = 0 ; s < AANTST ; s++) { totaal·= 0; for (int v = 9 ; v < AANTVK ; v++) totaal += punten[s] [v] * gewichten[v]; cout << 11 voor student nummer 11 << (s + 1) << 11 : « (totaal I totgew) « 11 /20 11 << endl; }
//berekenen van het kiasgemiddelde per vak cout << ''Per vak zijn de gemiddelden: 11 << endl; for (int v = 0 ; v < AANTVK ; v++) { totaal = 0; for (int s = 0 ; s < AANTST s++) totaal+= punten[s] [v]; cout << 11 voor vak nummer 11 << (v + . 1) << 11 • « (totaal I AANTST) << 11 /20 11 «endl; }
return 0; }
Code 3.1: Voorbeeld tweedimensionale tabellen
47
11
11
;
Hoofdstuk 3. Samengestelde gegevenstypes
3.2 3.2.1
@Helga.Naessens@hogent. be
Structs Algemeen
Soms is het nuttig om bepaalde gegevens in een programma te groeperen en als één geheel te behandelen. Beschouw bevaarbeeld een programma waarin we een aantal bewerkingen op lijnstukken in het vlak willen uitvoeren. Een programmafragment om de coördinaten van een lijnstuk in te lezen, zou er dan bijvoorbeeld als volgt kunnen uitzien: double x1, y1, x2, y2; cout << "Geef de coordinaten van het lijnstuk in: " << endl; cout << ·"Eerste eindpunt: "; cin » x1 » yl; cout << "Tweede eindpunt: "; cin » x2 » y2; Vier verschillende variabelen om slechts één ·lijnstuk voor te stellen, is echter niet erg handig 2 • Een mogelijke alternatieve oplossing ZO\.l bijvoorbeeld de vier coördinaten v~ een lijnstuk kunnen verenigen in een tabel. Gebruik je hier een tabel, dan moet je echter goed onthouden welke index met welke coördinaat overeenkomt: . is het tweede element de x-coördinaat van het tweede eindpunt of dey-coördinaat van het eerste? Hier komt het nieuwe begrip struct ons te hulp 3 : .in C++ kan de programmeur een nieuw gegevenstype definiëren met een zelfgekozen haam (bijvoorbeeld Lijnstuk) om een object aan te duiden dat uit verschillende componenten bestaat, elke component eventueel van een verschillend type. Een dergelijke struct die oestaat .uit vier coördinaten (elk aangeduid met een reëel getal) wordt dan bijvoorbeeld als volgt gedefinieerd: struct Lijnstuk { double x1, y1; double x2, y2; };
Elke struct-definitie begint met· het sleutelwoord struct gevolgd door de naam van het nieuwe type en daarna tussen accolades de beschrijvingen van de onderdelen van dat type. Na de sluitende accolade staat een puntkomma, vergeet die niet! Definities van structs staan vooraan in het programma, vlak na de #include-lijnen: De componenten van een struct noemen we velden~ Elk veld heeft een nàam (x1, y1, x2 en y2 in dit geval) en eeri. type dat binnen de struct-definitie wordt aangegeven op dezelfde manier waarop variabelen worden gedeclareerd. Let op! De velden van een struct zijn geen variabelen, en de naain van de struct trouwens ook niet. 2
Zeker niet als we later met procedures en functies zullen werken en we het aantal parameters van procedures en functies zoveel mogelijk zullen wensen te beperken. 3 Er bestaat geen goede vertaling voor dit begrip. De term 'struct' is trouwens niet eens een Engels woord. Meestal wordt ook de Engelse term record gebruikt.
48
@Helga.Naessens@hogent. be
Hoofdstuk 3. Samengestelde gegevenstypes
Als je een struct hebt gedefinieerd, kan je de naam van die strud gebruiken op alle plaatsen waar C++ een t:}rpenaam verwacht, zoals vooraan in een variabeledeclaratie, of (zoals we later nog wel zullen zien) in een paranieterdeclaratie of als resultaattype van een functie. We schrijven dus .
Lijnstuk
1;
om een variabele 1 te declareren die tegelijkertijd de vier coördinaten van een lijnstuk kan bevatten. Een veld van een dergelijke variabele duiden we aan door eerst de naam van de variabele te schrijven, gevolgd door een punt, en daarna de naam van het veld. De variabele 1 kunnen we dus bijvoorbeeld initialiseren met behulp van de volgende instructies: l.x1 l.y1 l.x2 l.y2
5.8; 3.65; 13; 1.23;
Maar, net zoals bij initialisaties van tabellen, kunnen we de variabele 1 öok als volgt initialiseren bij declaratie:
Lijnstuk
= {5 ; 8,
1
3.65, 13, L23};
Variabelen van hetzelfde struct-type kunnen gewoon aan elkaar toegewezen worden. Het volgend stukje code is dus toegestaan:
Lijnstuk Lijnstuk
= {5.8, = 1;
1
h
3.65, 13, L23};
Variabelen van hetzelfde struct-type kunnen echter niet zomaar rechtstreeks met elkaar vergeleken worden. Als je dus bijvoorbeeld twee lijnstukken met elkaar wenst te vergelijken, moet je één voor één de velden van de lijnstukken vergelijken. Schrijf dus nooit:
Lijnstuk if (1
==
1, h;
I /FOUT ! ! ! ! ! !
h)
maar wel:
Lijnstuk i f ( (l.x1 (l.x2
1, h; h.x1) h.x2)
&& &&
(l.y1 -- h . y1) && (1.y2 -- h.y2))
49
Hoofdstuk 3. Samengestelde gegevenstypes
@Helga.Naessens@hogent. be
Ook het inlezen en het uitschrijven van variabelen van een bepaald struct-type kunnen niet. rechtstreeks gebeuren, maar moet men veld per veld doen. Een volledig programma dat de vier coördinaten van een lijnstuk inleest en de lengte van het lijnstuk berekent en uitschrijft, is w~rgegevenïn Code3.2. #include #include using namespace std; struct Lijnstuk { double x1, y1, x2, y2; };
int mainO { Lijnstuk 1; double lengte; cout << "Geef de coordinaten van het lijnstuk in : " « endl; cout << "Eerste eindpunt : " ; cin >> l ~ x1 >> l.y1; cout <<"Tweede eindpUnt: "; cin >> l.x2 >> l.y2; lengte = sqrt ( (1. x1 '-. 1. x2) * (1. x1 - 1. x2) + (l.y1 - l.y2) * (l.y1 - l.y2)); cout << "De lengte van het lijnstuk is " << lengte .<< endl; return 0; }
Code 3.2: Leest een lijnstuk in en berekent de lengte van het lijnstuk
Een struct kan vaak op verschillende manieren gedefinieerd worden. Ook de volgende definitie van een struct lijnstuk heeft meer zin: struct Punt { double x, y ; };
struct Lijnstuk { Punt eindpunt!, eindpunt2; };
We gebruiken hier de ene struct in de definitie van de andere. Is 1 een variabele van het type lijnstuk dan wordt de x-coördinaat van het eerste eindpunt van dit lijnstuk genoteerd als '1. eindpunt 1. x'. Het lijnstuk waarvan de uiteinden de coördinaten (5.8 , 3.65) en (13 , 1.23) hebben, kunnen we dus als volgt declareren en initialiseren: 50
Hoofdstuk 3. Samengestelde gegevenstypes
Lijnstuk 1; l. eindpunt i. x 1. eindpunt 1 . y l.eindpunt2.x l.eindpunt2.y
@Helga.Naèssens@hogent. be
= 5.8; 3.65;
= 13; 1.23;
Ook hier kunnen we het lijnstuk initialiseren bij declaratie: Lijnstuk
1 = {{5.8, 3.65}, {13, 1.23}};
Door gebruik te maken van deze tweede definitie van de struct lijnstuk, kan het programma Code 3.2 dat de vier coördinaten van een lijnstuk inleest en de lengte van het lijnstuk berekent en uitschrijft, nu ook geschreven worden als Code 3.3. #include #include using namespace std; struct Punt { double x, y; };
struct Lijnstuk { Punt eindpunt!, eindpunt2; };
int main() { Lijnstuk 1; double lengte; cout << "Geef de coordinaten van het lijnstuk in: " << endl; cout << "Eerste eindpunt: "; cin >> l.eindpuntl.x >> l.eindpuntl.y; cout << "Tweede eindpunt: · "; cin >> l.eindpunt2.x >> l.eindpunt2.y; lengte = sqrt((l . eindpuntl. x - l.eindpunt2.x) * . (l.eindpuntl.x - l.eindpUiit2.x) + (l.eindpuntl.y - l.eindpunt2.y) * (l . eindpuntl.y - l.eindpunt2.y)); cout << "De lengte van het lijnstuk is " << lengte << endl; return 0; }
Code 3.3: Leest een lijnstuk in en berekent de lengte van het lijnstuk
Tenslotte kunnen we nog opmerken dat het grote voordeel van structs t.o.v. tabellen in .het feit ligt dat de velden van een struct niet noodzakelijk van eenzelfde type hoeven te
51
Hoofdstuk 3. Samengestelde gegevenstypes
@Helga.Naessens@hogent. be
zijn. Tabellen hebben namelijk het grote nadeel dat alleen gegevens van hetzelfde type in een tabel kumieri geplaatst worden. Een cirkel in het vlak kan je bijvoorbeeld voorstellen door middel van een middelpunt en een straal. Indien je hier zou gebruik maken van een tabE;)l van drie elementen (twee elementen voor het middelpunt van de .cirkel en één voor de straal), dan zou je goed moeten onthouden welke elementen het middelpunt vormen en welke de straa.l. Het gebruik van bijvoorbeeld de volgende struct is veel duidelijker:
struct Punt { double x, y; };
struct Cirkel { Punt middelpt; double straal; };
3.2.2
Tabellen van structs
Structs zijn volwaardige gegevenstypes en dus bestaan er ook tabellen van structs. Beschouw als voorbeeld het volgende programmafragment waarin de coördinaten van 50 punten ingelezen worden en opgeslagen worden in een tabel:
struct Punt { double x, y; };
int main() { Plint tab [50] ; for (int ·i = 0 ; i < 50 ; i++) { cout << ncoordinaten punt 11 << (i+ 1) << n. cin » tab [i] . x » tab [i] .y;
11
;
}
}
3.2.3 . Structs met tabellen als velden In C++ moet voor elke tabel aangegeven wörden hoeveel elementen die tabel maximaal kan bevatten. Dit .betekent vaak dat we voor elke tabel ook nog de effectieve lengte van die .tabel moeten onthouden4 (d.w.z., het werkelijke aantal elementen dat op dat moment 4 Een andere manier Is de volgende: in de plaats van telkens de effectieve lengte te onthouden, gebruik je een speciaal element om de tabel af te sluiten. Dit principe wordt bijvoorbeeld gebruikt bij C-strings, waar het nulkarakter als afsluiter dient. Hier komen we later op terug. .
52
Hoofdstuk 3. Samengestelde gegevenstypes
@Helga.Naessens@hogent. be
in gebruik is) . Structs vormen een elegante manier om dit te doen. Je kan immers een struct definiëren die zowel de tabel als het aantal elementen bevat. We illustreren dit principe aan de hand van een voorbeeld. De volgende struct wordt gebruikt om het begrip 'veelhoek' voor te stellen. Elke veelhoek bestaat uit een aantal hoekpunten die op hun beurt worden voorgesteld als structs van het type punt. We spreken af dat een veelhoek maximaal 40 hoekpunten kan hebben en dat de hoekpunten in volgorde van tegenwijzerszin opgeslagen worden in de tabel van hoekpunten. struct Veelhoek { int aantalptn; Punt hoekptn[40]; };
Code 3.4 is een voorbeeld van een programmafragment dat gebruik makend van deze struct de omtrek van een veelhoek bepaalt. In Hoofdstuk 5 zullen we nog wel zien hoe we een elegantere en meer overzichtelijke oplossing kunnen bekomen, Veelhoek v; //inlezen van de veelhoek //berekenen van de omtrek double hulp!, hulp2, afstand; double omtrek;· hulp!= v.hoekptn[v.aantalptn - 1].x - v.hoekptn[O].x; hulp2 = v.hoekptn[v.aantalptn - 1].y - v.hoekptn[O].y; afstand= sqrt(hulp1 *hulp!+ hulp2 * hulp2); omtrek = afstand; for (int i = 1 ; i < v.aç.ntalptn ; i++) { v.hoekptn[i] .x - v.hoekptn[i-1] .x; hulp! v.hoekptn[i] .y - v.hoekptn[i-1] .y; hulp2 afstand sqrt(hulp1 *hulp!+ hulp2 * hulp2); omtrek += afstand; }
cout << "De omtrek van de veelhoek is
11
<< omtrek << endl;
Code 3.4: Berekent de omtrek van een veelhoek
53
'
' '
'
'
~H~~·J d_· ~ ,- -·--·-.-
~ -~-- I
.)i
1
~- ·- ·
I
;
!
, ·-
I
I
-.-
=~ ~~! I
l~r#e <.. ~'JF-nc~~ ··T>
·.O~cf PciRf
t
;
I
I
I
!
. ,.
.
l
.
.
z;~ f ~,,~·~ 1 ~1-}L )Mo ·~~ uj/o~ '-)
--J
l
I
p
•
I
l
'
I
I
I
I
•(i~ {t:rJ 'J PI --F~ <~IJ.e) <:.( ' I'
1
-p~n_
_ I
I
.
l
1
'
'
'
~J ~-l~(~:"-1[ ûJ'"V\'\- ( (
~l.-t..< )
I
0\i-\it -
1
. ) "
LN"'·' t; ( .-t 2.-1 j
'
ï Z•
~
'
.
o vf-:~-L <JAbJ ivp-
;
;
I
•
.
:
'
( -~-e., t,r{ uil t?fe~ ~~~~~'
I !,4l, -~ '
c::.)
J
1
c 2.) ' :b I
q )
I
._.4\~}
~e-f_~ ~ _fiár-:.ê~--·X \') · oo<.d_ rt-,~ :~)
rr r
w
t..".J\'f\1 (
f
I
JJIJ fi~
1
•
,
.
'j' i)
<-n_;ti f 'A');·
l
'
-~
.
+
t
C++ : DE STANDARD TEMPLATE LIBRARY (STL)
, ~-··"'çJ \(\:·!"'
Deze beknopte tekst bevat een inleiding tot templates en de STL, een overzicht van de standaard STL-containers (met enkele relevante operaties), en een minieme selectie van STLalgoritmen.
1
Templates
Vaak moet men gegevensstructuren, functies of procedures definiëren, die enkel verschillen in het type van de gegevens waarmee ze werken. In plaats van zoveel exemplaren aan te maken, is het eenvoudiger om slechts één sjabloon (een generische vorm) te definiëren, en het aan de compiler over te laten om daarvan zoveel concrete versies te maken als nodig. Programmeren met types als parameters noemt men dan ook generisch programmeren.
1.1
Class templates
Voorlopig beperken we ons hier tot een struct. Deze erfenis van C werd echter door C++ uitgebreid, zodat het eigenlijk een klasse geworden is. (Zie later.) Als voorbeeld van een dergelijke template nemen we de gegevensstructuur pair, die ook door de STL gebruikt wordt. (Gedefinieerd in de header .) Dat is eigenlijk een gewone struct met twee datavelden, first en second genaamd. Het type van die velden kan variëren. Daarom definieert men die struct als een template: template struct pair{ 11 Publieke dataleden T1 first; T2 second; 11 Constructoren };
Achter het gereserveerde woord template staan een of meerdere formele template parameters, tussen< >. Zoals alle formele parameters hebben die ook een type en een naam. Dat 'type' is hier eigenlijk een metatype, omdat de parameter zelf een type is. (In plaats van typename mag men ook class schrijven. Dat wil echter niet zeggen dat template parameters klassen moeten zijn.) Wanneer men een pair declareert, moet het vergezeld gaan van de actuele parameters, ook tussen < >. Omdat een struct een klasse geworden is, definieert een pair ook constructoren, die beide velden initialiseren. We beperken ons hier tot enkele voorbeelden van hoe ze gebruikt worden:
1
#include using std::pair; pair<string,int> p1; //Default constructor: ledige string en nul p1.first = "Peter Peeters"; p1.second = 60; pair<string,int> p2("Jan Janssen",37); // Gonstructor pair<string,double> ptab[100]; //Default constructor voor alle pairs
Function templates
1.2
Ook voor functies en procedures kunnen we een sjabloon definiëren. Als voorbeeld nemen we insertion sart op een C-tabel van een of ander rangschikbaar type. Om stijgend te kunnen rangschikken onderstellen we dat gegevens van dat type kunnen vergeleken worden met de operator <. (Indien nodig kunnen we die zelf definiëren, zie later.) template void insertion_sort(T
*
tab, int n)
{
for(int i=1 ; i=O && h
tab[j+1]=h; //Beter swap(tab[j+1] ,h); } }
int main() {
int t1[100], t2[400]; double t3[200]; string t4[150]; insertion_sort(t1,100); insertion_sort(t3,200); insertion_sort(t4,150); insertion_sort(t2,400); }
Aan de hand van de actuele parameters waarmee de procedure opgeroepen wordt, weet de compiler welke code hij moet genereren. Hier worden dus drie exemplaren aangemaakt. (Ook swap is een function template. Deze procedure verwisselt twee gegevens, en dat gebeurt efficiënt bij grote gegevens zoals strings.) 2
2
Overzicht van de STL: Containers en Algoritmen
De Standard Template Library vormt een belangrijk onderdeel van de standaardbibliotheek van C++. Ze bevat zowel containers als algoritmen. Containers zijn sjablonen (templates) voor allerlei veel gebruikte gegevensstructuren. De template parameters zijn (onder meer) de types van de gegevens die ze bevatten. Algoritmen zijn sjablonen (function templates) voor veel voorkomende bewerkingen op deze containers. Om dezelfde bewerkingen op containers met een verschillende structuur te kunnen uitvoeren, wordt intensief gebruik gemaakt van iteratoren.
2.1
Iteratoren
Een iterator is een soort pointer waarmee de gegevens van elke container op dezelfde manier kunnen overlopen worden, in een volgorde bepaald door hun interne structuur. Elke container definieert een iteratortype iterator, een iteratorwaarde begin() die naar het eerste element verwijst, en een iteratorwaarde end 0 die voorbij het laatste element wijst. (Bij een ledige container zijn beide waarden dus identiek.) Voor de operaties op iteratoren gebruikt men de notaties van analoge operaties op pointers: *it is het element waarnaar de iterator it verwijst, en ++i t (of i t++) verplaatst de iterator naar het volgende element. Als dat element eenstructof object is, waarvan men een onderdeel wil selecteren, kan men de notatie it-> .•. gebruiken. Om wijziging van containerelementen te verhinderen, bestaat er ook een iterator naar const ( const_i ter a tor). Alle elementen van een container overlopen gebeurt dan als volgt: vector v(100); vector::iterator it while(it ! = v.end()){ 11 Doe iets met *it ++it;
v.begin(); //De iterator van het containertype vector
}
Bemerk dat er vergeleken wordt met ! = en niet met <. Zoals bij pointers heeft deze ordening enkel zin als ze in dezelfde tabel verwijzen, maar containers gebruiken niet noodzakelijk een tabel.
2.2 2.2.1
Containers Sequences
Sequenties zijn eendimensionale gegevensstructuren: elk element, behalve het eerste en het laatste, heeft een linkerbuur en een rechterbuur. Wanneer men deze containers met een iterator overloopt, gebeurt dat in die lineaire volgorde.
3
Vector Gedefinieerd in de header . Een vector is een gemakkelijker te gebruiken en veiliger alternatief voor een C-tabel. Zo weet hij hoeveel elementen hij bevat, en kan hij zijn grootte indien nodig uitbreiden. (De size is het huidig aantal gedefinieerde elementen, de capacity het maximaal aantal elementen.) Omdat men hem kan indexeren, zijn iteratoren meestal niet nodig. • vector v; // Ledige vector (size nul) vector v(n); // Size n, alle elementen default waarde vector v(n,e); // Size n, alle elementen waarde e • vector v(w); //Declaratie en initialisatie met andere vector w vector v=w; //Declaratie en initialisatie met andere vector w • v[i] // Indexering, zonder controle op i v.at(i) // Indexering, met controle op i (werpt eventueel out-of-range error) &
v.push_back(e); //Achteraan toevoegen (capaciteit kan ook stijgen) v.pop_back(); //Achteraan verwijderen (geeft niets terug) v.back() //Het laatste element v.front() // Het eerste element
• v=w; // Toewijzing met andere vector • v == w // Vergelijken (of !=) • v.size() // Aantal gedefinieerde elementen v.capacity() // Aantal gereserveerde elementen: size <= capacity v.empty() // Test of ledig (size nul?) • v.clear(); //Maak ledig (size wordt nul) v.resize(n); // Size wordt n (capacity kan stijgen), nieuwe elementen default waarde v.resize(n,e); // Size wordt n (capacity kan stijgen), nieuwe elementen waarde e v.reserve(n); //Reserveer n elementen (size wijzigt niet, geen initialisatie) Voor de volledigheid werden zowel de copy constructor als de toewijzing in deze lijst opgenomen. Die zijn voorzien voor alle containers, en maken een volledige kopie ('deep copy'). List Gedefinieerd in de header <list>. Een list heeft nagenoeg dezelfde operaties als een vector, maar gebruikt natuurlijk geen onderliggende tabel, zodat indexeren ( [] en at) en een gereserveerd aantal elementen (capac i ty en reserve) niet van toepassing zijn. Het voornaamste voordeel van een lijst ten opzichte van een vector is dat men efficiënt kan toevoegen en verwijderen op een willekeurige plaats. De belangrijkste bijkomende operaties zijn: • l.push_front(e); //Vooraan toevoegen l.pop_front(); //Vooraan verwijderen (geeft niets terug) 4
• l.insert(it,e); //Invoegen van e net voor it, geeft iterator naar nieuw element l.erase(it); //Verwijdert het element bij it, geeft iterator naar volgend element Deze laatste twee operaties bestaan ook wel voor een vector, maar zijn uiteraard inefficiënt, zodat hun nut beperkt is.
Deque Gedefinieerd in de header <deque>. Een deque ('double-ended queue') heeft dezelfde operaties als een vector, met uitzondering van capaci ty () en reserve(). Indexeren is dus toegelaten. Het belangrijkste verschil met een vector is dat men ook vooraan (efficiënt) kan toevoegen en verwijderen, zoals bij een list (push_front () en pop_front ()) .
2.2.2
Sequence adapters
Deze containers leggen een toegangsvolgorde tot de elementen op, doo: het aantal toegankelijke elementen te beperken. Ze worden gewoonlijk geïmplementeerd met een van de vorige containers. (Ze heten dan ook 'adapters' omdat ze de interface van sequences aanpassen, beperken.) Ze definiëren slechts een gering aantal operaties. Indexeren is onmogelijk en ze hebben ook geen iteratoren, want dan zouden alle elementen toegankelijk worden.
Stack Gedefinieerd in de header <stack>. Een stack is een stapel die enkel toegang geeft tot het laatst toegevoegde element. Verwijderen gebeurt in omgekeerde volgorde van toevoegen. (Het is een LIFO-structuur: Last In, First Out.) • stack s; // Ledige stapel • s.push(e); //Toevoegen van e s.pop(); //Verwijderen van het laatst toegevoegde element (geeft niets terug) s.top(); //Het laatst toegevoegde element • s.empty() // Test of ledig s.size() //Aantal elementen
Queue Gedefinieerd in de header . Een queue is een wachtrij die enkel toegang geeft tot het eerst en het laatst toegevoegde element. Verwijderen gebeurt in dezelfde volgorde van toevoegen. (Het is een FIFO-structuur: First In, First Out.) • queue q; // Ledige wachtrij • q.push(e); //Toevoegen q.pop(); //Verwijderen q.front(); //Het eerst q.back(); //Het laatst
van e van het eerst toegevoegde element (geeft niets terug) toegevoegde element toegevoegde element
5
• q.empty() 11 Test of ledig q.size() 11 Aantal elementen
Priority queue Gedefinieerd in de header . Een priori ty _queue is een prioriteitswachtrij die enkel toegang geeft tot het element met de grootste waarde. (De waarde van een element wordt beschouwd als zijn prioriteit.) Verwijderen gebeurt dus volgens dalende prioriteiten (waarden). Hierbij onderstelt men dat elementen kunnen vergeleken worden met de operator<. Wil men steeds de kleinste in plaats van de grootste prioriteit, dan moet men als derde template parameter de klasse greater<elementtype> opgeven (gedefinieerd in de header ). Deze vereist de operator> op de elementen. Om technische redenen moet men ook een tweede template parameter opgegeven. Gewoonlijk is dat een vector voor de elementen: vector<elementtype>. • priority_queue Pi 11 Ledige p (grootste eerst) priority_queue,greater >Pi 11 Ledige p (kleinste eerst) • p.push(e)i 11 Toevoegen van element e p.pop()i 11 Verwijderen van het element met de grootste waarde (of kleinste) p. top() i I I Het element met de grootste waarde (of kleinste) • p.empty() 11 Test of ledig p.size() 11 Aantal elementen
2.2.3
Associative containers
Een associatieve container slaat gegevens op die bestaan uit een sleutel met bijbehorende informatie. (Hij associeert sleutels met hun informatie.) Via die sleutel kan men gegevens efficiënt opzoeken om de erbij horende informatie te raadplegen of te wijzigen. De gegevens worden dan ook gerangschikt op hun sleutels bijgehouden. (Maar niet in een tabel.) Wanneer men de gegevens van deze containers met een iterator overloopt dan gebeurt dat in stijgende sleutelvolgorde. Sleutels moeten daartoe de operatie < ondersteunen.
Map en muitirnap Gedefinieerd in de header <map>. Om een sleutel met zijn bijbehorende informatie te associëren, gebruiken deze containers een pair. Bij een map zijn sleutels uniek, bij een multimap niet noodzakelijk. • map<S,I> mi 11 Ledige map: sleutels van type S, informatie van type I multimap<S,I> mmi 11 Ledige multimap: sleutels van type S, informatie van type I • m.insert(p)i 11 Voegt pair<S,I> toe, geeft pair (it naar p, b true als nieuw) mm.insert(p)i 11 Voegt pair<S,I> toe, geeft iterator naar p (wordt altijd toegevoegd) m.erase(s)i 11 Verwijdert pair met sleutels, geeft aantal verwijderde elementen mm.erase(s)i 11 Verwijdert elk pair met sleutels, geeft aantal verwijderde elementen 6
• m.count(s) // Het aantal pairs met sleutel s (nul of n) mm.count(s) // Het aantal pairs met sleutel s (nul of meer) • m.find(s); //Geeft iterator naar pair met sleutels, of m.end() als niet gevonden mm.find(s); //Geeft iterator naar eerste pair met sleutels, of mm.end() • m == m2 // Vergelijken (of !=) mm == mm2 // Vergelijken (of !=) • m[s] //Als rvalue: geeft informatie bij s, of voegt s toe met default informatie m[s] //Als lvalue: wijzigt informatie bij s, of voegt s toe met opgegeven informatie • m.size() //Aantal elementen mm.size() //Aantal elementen m.empty() // Test of ledig mm.empty() //Test of ledig • m.clear(); //Maak ledig mm.clear(); //Maak ledig Set en multiset
Gedefinieerd in de header <set>. Dit zijn vereenvoudigde associatieve containers: hun sleutels hebben geen bijbehorende informatie. Ze implementeren verzamelingen (set), waarbij eventueel duplikaten toegelaten zijn (mul ti set). Ook hier worden de elementen gerangschikt bijgehouden, zodat ze de operator < moeten ondersteunen. Hun operaties zijn nagenoeg identiek aan die van map en multimap. • set<S> s; // Ledige set, elementen van type S multiset<S> ms; //Ledige multiset, elementen van type S • s.insert(e); //Voegt e toe, geeft pair (it naar e, b true als toegevoegd) ms.insert(e); //Voegt e toe, geeft iterator naar e (wordt altijd toegevoegd) s.erase(e); //Verwijdert e, geeft aantal verwijderde elementen ms.erase(e); //Verwijdert alle elementen e, geeft aantal verwijderde elementen • s.count(e) // Aantal elementene (nul of n) ms.count(e) //Aantal elementene (nul of meer) • s.find(e); //Geeft iterator naar e, of s.end() als niet gevonden ms.find(e); //Geeft iterator naar eerste e, of s.end() als niet gevonden • s == s2 //Vergelijken (of !=) ms == ms2 //Vergelijken (of !=) • s.size() // Aantal elementen ms.size() //Aantal elementen s.empty() // Test of ledig ms.empty() // Test of ledig • s.clear(); //Maak ledig ms.clear(); //Maak ledig
7
2.3
Algoritmen
Reader . STL-algoritmen zijn veel voorkomende operaties die op verschillende STL-containers kunnen uitgevoerd worden. Met als gevolg dat indien gewenst, een container kan vervangen worden door een andere, die het algoritme sneller maakt. Ze werken dan ook met iteratoren. De containerelementen waarop ze werken worden vaak aangeduid door een paar iteratoren dat een half open bereik definieert: vanaf het element bij de eerste iterator tot het element net vóór de tweede. Er zijn zestig STL-algoritmen. We geven hier enkel een heel beperkte selectie: • svap(a, b); // Vervisselt tvee elementen • sort(it1,it2); //Rangschikt elementen uit een half open bereik Enkel voor vector, deque, en G-tabel (met pointers in plaats van iteratoren) . Onderstelt dat de elementen de operatie < ondersteunen. • max(a, b) // Grootste min(a, b) // Kleinste max_element(it1, it2) min_element(it1, it2)
element van twee element van twee // Geeft iterator naar grootste element uit half open bereik //Geeft iterator naar kleinste element uit half open bereik
Onderstellen dat de elementen de operatie < ondersteunen. • lower_bound(it1,it2,e); //Geeft iterator naar eerste element e uit een half open berE Enkel voor een reeds gerangschikte vector, deque, en C-tabel (met pointers in plaats van iteratoren). Veel efficiënter dan sequentieel zoeken (implementeert binair zoeken). Onderstelt dat de elementen de operatie < ondersteunen. Als e niet aanwezig is, wordt een iterator teruggegeven naar het eerste element groter dan e, of i t2 als dat niet bestaat.
8
Hoofdstuk 4 Containers Eén container (of ook wel collectie genaamd) is een datastructuur die meerdere elementen kan bevatten, waarbij normaal gezien alle elementen van de container hetzelfde type moeteil hebben. De eenvoudigste container is de tabel (array). Er bestaan echter nog veel andere soorten containers, elk met hun specifieke eigenschal}pen. Sommige containers kunnen "dynamisch"gröeien (d.w.z. dat hun grootte toeneemt tijdens de uitvoering van het programma). Sommige houden de gegevens geordend bij. Bij sommige kun je snel een element toevoegen of wis~en of zoeken ...
4.1
De tabel
Vooraleer we enkele containertypes nader bekijken, gaan we wat dieper in op enkele goede en slechte eigenschappen tekortkomingen van de klassieke tabel.
4.1.1
Voordelen van de tabel
De . tabel is een eenvoudig type waarin je meerdere elementen van hetzelfde type kan opslaan. De declaratie is eenvoudig en gemakkelijk uitbreidbaar naar meerdere dimensies~ Elk element in de tabel is even gemakkelijk en even snel bereikbaar met tabel[i] of mat[i][j].
4.1.2
Problemen met de tabel
Stel dat je een onbepaald aantal positieve gehele getallen wilt inlezen en in een tabel opslaan; het inlezen st9pt indien er een strikt negatief getal ingegeven wordt. Een mogelijke implementatie ziet er als volgt uit:
54
Hoofdstuk 4. Containers
©Helga.Naessens@hogent. be
const int MAX = 1000; int tabel[MAX]; int x, n = 0; cin >> x; while (x >= 0 && n < MAX) { tabel[n] = x ; n++; cin >> x; }
Omdat het aantal in te lezen getallen op voorhand niet gekend is, heeft men een keuze gemaakt voor het maximaal aantal, namelijk 1000. Van die tabel worden slechts de eerste n elementen effectief gebruikt. Men noemt lOOO de capaciteit van de tabel en n de grootte van ·de tabel. Bij deze implementatie kunnen er echter niet meer dan 1000 positieve getallen ingelezen worden. Als n te groot wordt, ligt tabel [n] "buiten de tabel". Aangezien er echter geen index-checking gebeurt, moet de programmeur daar steeds zelf aan denken, vandaar de extra voorwaarde in de while-lus. Hoe groot moet de capaciteit van een tabel gekozen worden? Alnaargelang de context kan men dit soms op voorhand bepalen, maar vaak moet men gokken. Hoe groter de capaciteit, hoe kleiner de kans op "capacity overflow", maar hoe meer geheugen er verspild wordt. Trouwens, welke capaciû~it je ook kiest, er kunnen toch altijd nog meer getallen ingegeven worden ... Indien men de nodige capaciteit kan bekomen vóór het inlezen van .de elementen, dan kan men gebruik maken van dynamische allocatie (zie sectie 9.9 uit het hoofds.t uk over pointers). Stel bijvoorbeeld dat de gebruiker het aantal getallen n eerst ingeeft, dan kan men schrijven1 : · int n; cin >> n; int *tabel = new int[Ii.]; int x, i = 0; cin >> x; while (x >= 0 && i < n) { tabel[i] =x; i++; cin >> x;
//dynamische allocatie
}
delete[] tabel; // geheugen vrijgeven 1
Alhoewel sommige compilers de opdracht 'int tabel[n];' wel aanvaarden, is dit niet standaard en dus geen professionele en verstandige manier van doen. Vandaar dat wij deze opdracht dan ook niet toelaten.
55
@Helga.Naessens@hogent. be
Hoofdstuk 4. Containers
Een dergelijke dynamisch gealloceerde tabel wordt volledig opgevuld (er is dus geen geheugenverlies), zodat na het inlezen d~ grootte en de capaciteit van de tabel aan èlkaar gelijk zijn. Dit is al een stuk beter, maar spijtig genoeg is het vaak onmogelijk om op voorhand het aantal elementen te bepalen. Denk bijvoorbeeld aan het inlezen van een bestand. Bovendien mag men niet vergeten het opgevraagde. geheugenblok terug vrij te geven met delete [], op straffe van een "memo;ry leak". Frequente memory leaks kunnen het systeem vertragen en zelfs ·leiden tot een crash. Een laatste probleem met tabellen manifesteert zich in het feit dat er steeds extra variabelen of constanten nodig zijn om de grootte en/of de capaciteit van de tabel bij te houden. Dit is nog duidelijker indien men een tabel als parameter aan een functie doorgeeft: m dat geval is het m"eestal nodig om ook de grootte en/ of de capaeiteit mee te geven. We kunnen dus besluiten dat bij een gewone C++-tabel de volgende vier belangrijke problemen optreden: 1. je moet bij constructie de capaciteit van de tabel opgeven
2. deze capaciteit kan nadieri niet meer wijzigen (noch groeien, noch krimpen) 3. er is geen index-checking 4. een tabel kent haar eigen grootte niet Om al deze problemen aan te pakken kan men gebruik mak~n van andere datastructuren, die containers implementeren. We zullen een aantal interessante containers bespreken.
4.2
Basisprincipe van een container
Het basisprincipe van elke container is steeds hetzelfde: 1. Je hebt een extra· bibliotheek (STL library) nodig waarin de container beschreven wordt.
2. Alle elementen in een container zijn van hetzelfde type. Je kan dit type vastleggen bij declaratie met behulp van een template-parameter . Een aantal containers aanvaarden enkel "eenvoudige~'types (int, char, double, string), andere containers laten ook complexe types toe (struct, tabel, container,). 3. De container is eigep.lijk een klasse waarvan je objecten aanmaakt. 4. De container weet zelf hoeveel elementen hij heeft. Je kan dit meestal opvragen met behulp van een lidfunctie met een voor zich sprekende naam (.si ze()/. count () . 5. Je kan alle rator.
~lementen
van de container overlopen, meestal met behulp van een ite-
56
@Helga.Naessens@hogent. be
Hoofdstuk 4. Containers
6. Elke container heeft specifieke methodes (lidfuncties) en operatori:m om het gebruik te vereenvoudigen. 7. Elke container heeft voor- en nadelen, en is ontworpen met specifieke kenmerken. Om een specifieke container , bijvoorbeeld een vector, te kunnen gebruiken moet je de juiste bibliotheek .std: :vector inladen. Omdat. deze klasse in de namespace std gedefinieerd is, mag je dankzij een "using namespace std"het voorvoegsel "std: :" overal weglaten. Dit wordt dus: #include using namespace std; Container-klassen zijn steeds geparametrizeerde klassen, ook wel template-klassen genoemd. Bij declaratie wordt de template-parameter opgegeven tussen '<' en '> '. De templateparameter bepaalt het type van de elementen. Bijvoorbeeld: vector v; vect~r<stiing>
w;
We bespreken nu een aantal implementaties voor een groeitabel, een gelinkte lijst, een stapel, een wachtrij, een verzameling en een afbeelding.
4.3 4.3.1
De groeitabel Principe van de groeitabel
Een groeitabel is een tabelgebaseerde container die dynamisch (at runtime) kan groeien 2 . Het principe van .een groeitabel is eenvoudig. Bij de declaratie van een groeitabel wordt een tabel met een bepaalde opgegeven capaciteit voorzien (gealloceerd) en wordt het type van de elementen vastgelegd. Indien er een element toegevoegd wordt aan de tabel, dan zijn er twee mogelijke scenario's: 1. Er is nog plaats in de tabel, namelijk het aantal gebruikte elementen n is strikt kleiner dan de capaciteit van de tabel. In dit geval wordt het element gewoon in het eerstvolgende vrije vakje van de tabel geplaatst en de grootten wordt verhoogd. 2. Er is geen plaats meer in de tabel, want het aantal gebruikte elementen n is gelijk . aan de capaciteit van de tabel. In dit .geval wordt er een nieuwe tabel gemaakt met een grotere capaciteit, en alle elementen uit de eèrste tabel worden gekopieerd nàar de nieuwe tabel, tenslotte wordt de eerste tabel "geschrapt" en vervangen door de .nieuwe tabel, zodat men nu plaats heeft om het element toe te voegen aan de nieuwe tabel en men in scenario 1 belandt. 2
Let wel: de naam "groeitabel" is geen ingeburgerde benaming: In C++ gebruikt men de benaming vector, in andere programmnieertalen, zoals bijvoorbeeld Java, heet deze datastructuur vaak arraylist.
57
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
Het allaceren van een nieuwe tabel en het kopiëren van alle bestaande elementen is echter een "dure" operatie, d.w.z. dat dit enige tijd vraagt. In de praktijk kiest men ervoor om de capaciteit. bij elke re-allocatie te verdubbelen. Hierdoor worden naarmate de capaciteit toeneemt, de re-allocaties relatief zeldzamer, maar ze worden wel "duurder", zodat enkel de extra kost per element constant blijft. Het netto-effect is dat toevoegen aan een groeitabel gemiddeld toch vrij snel gebeurt. Gelukkig gebeurt dit alles achter de schermen, zodat de gebruiker van de groeitabel zich niets hoeft aan te trekken van capaciteiten en re-allocaties.
4.3.2
Vector ~
De standard C++ library voorziet een groeitabel onder de naam ve ~tor 3 , gedefinieerd in de bibliotheek . Beschouw als voorbeeld het volgende programma: 0
#include using namespace std; ( / -f!J(.e ~~- i-
1
vector v(5);
2 3 4
v[3] = 8;
5
6 7
· " ,, ~.x..,fè"?-' ; - ·/lciiC.-1"c'· l " v.push_back(12); for (int i = 0 ;. i < v.size() i++) cout « v [i] « endl; cout « v[4];
V • push_ back ( 10)
In lijn 0 wordt de vector-bibliotheek geïncludeerd. Deze bevat dè nodige definities om met vectoren te kunnen werken. In lijn 1 wordt een nieuwe variabele v van het type vector (men zegt "vector van int") gedefinieerd, waarbij ook de capaciteit van de vector (nl. 5) opgegeven wordt. Bovendien worden als een extra eigenschap van deze container alle inhouden van de vector op 0 geïnitialiseerd. Indien je de elementen van de vector liever initialiseert op een andere waarde, dan kan je ook deze waarde meegeven bij de definitie van de variabele, bijvoorbeeld vector v(5, -1); Indien bij de definitie van de variabele geen capaciteit opgegeven wordt (vector v; ), dan heb je een "lege" container. In lijnen 2 en 3 zie je dat de gewone tabelnotatie v [i] kan gebruikt worden voor inhouden die reeds gealloceerd zijn. Er is evenwel nog steeds geen index-checking: indien je bijvoor3 Verder heeft deze vector weinig te q1aken met de vectoren uit de wiskunde, behalve dat ze beide tabel-achtig zijn.
58
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
beeld v [ -1] opvraagt, krijg je (net als bij een tabel) een willekeurig getal terug (maar het kan ook gebeuren dat het operating system een fout geeft). . In lijnen 4 en 5 wordt gebruik gemaakt van de methode push_back om een getal (achteraan) toe te voegen aan de vector (en dus de container te laten groeien). · In lijn 6 worden alle elemen.ten overlopen. Met v. si ze 0 bekom je de grootte van de vector v (hier 7=5+2). Achter de schermen zit er een tabel waarvan de capaciteit waarschijnlijk groter is dan 7, maar daar hoeft de gebruiker zich niets van aan te trekken. Merk op dat een vector (in tegenstelling tot een gewone tabel) wel zijn eigen grootte kent, waarmee het laatste probleem (zie boven) opgelost is . . In lijn 7 wordt er telkens éen element op het scherm gezet. Hierbij wordt er opriieuw gebruik gemaakt ·van de vier: kante haakjes om het i-de element uit de vector te bekomen.· Ook bij vectoren wordt er vanaf nul geteld, dus 0 <= i < v. si ze 0. Zoals we reeds eerder vermeld hebben, kan men aan een vector naar hartelust elementen toevoegen4 . Indien nodig wordt er automatisch een re-allocatie gedaan om de capaciteit te verdubbelen. Indien men echter de uiteindelijke gewenste grootte van de vector op voorhand kent, dan kan men de performantie verbeteren door de begincapaciteit expliciet op te geven. Zo betekenen de volgende opdrachten vector<double> w; w.reserve(200); dat men voorziet dat er 200 elementen zullen toegevoegd worden aan de vector w: Let wel dat de vector in dit gevalleeg blijft, dus w. si ze() == 0. Door expliciet geheugen te reserveren vermijdt men de dure re-allocaties. Dit is dus niet hetzelfde als de declaratie waarbij de capaciteit wordt vastgelegd op 200 (vector<double> w(200) ;), want dan is de grootte (.si ze()) van de vector 200 en zijn alle inhouden geïnitialiseerd op 0. Nog enkele handige methodes die kunnen toegepast worden op een. vector zijn; • front () : Geeft het eerste element van de vector terug. cout « v. front 0 ; Bijvoorbeeld: • back () : Geeft het laatste element van de vector terug. cout « v. back 0 ; Bijvoorbeeld: • at (int i): Geeft het element op meegegeven index i terug. Hier is er wel indexchecking: indien je een index ~eegeeft die buiten de grootte van de vector valt, wordt er een fout gegenereerd. . . Bijvoorbeeld: cout « V at (2) ; 0
• empty(): gaat na ofde vector leeg is Bijvoorbeeld: i f ( v. empty ()) co ut « 4
11
leeg 11 ;
•
Men kan zelfs vooraan of in het midden van een vector elementen toêvoegen of verwijderen, maar dat
is een relatief "dure" operatie, omdat alle volgende elementen een plaatsje moeten opschuiven~
59
Hoofdstuk 4. Containers
@Helga.Naessen8@bogent. be
• clear (): Wist alle elementen uit de vector en zet de grootte van de vector op 0. Bijvoorbeeld: v. clear () ; • pop_back(): Verwijdert het laatste element uit de vector en vermindert tevens de grootte (si ze) van de vector met 1. Bijvoorbeeld: v. pop_back () ; • resize (int · n): Zet de grootte van de vector in op de meegegeven waarden. Indien er in de vector meer dan n elementen waren, worden de overtollige verwijderd .. Indien er minder waren, dan worden elementen bijgemaakt en allemaal geïnitialiseerd op 0. Let wel dat bestaande elementen niet wijzigen. Bijvoorbeeld: v. resize (5); . Verder wensen we ook nog op te merken dat de elementen van een vector geen elementaire types (zoals int, double, ... ) hoeven te zijri. Men kan bijvoorbeeld een v'ector<string> gebruiken om een lijst woorden op te slaan. Of met behulp van een vector > (merk op dat de spatie tussen > en > verplicht is) kan men een soort tweedimensionale tabel bekomen, waarvan de rijen mogelijks een verschillende grootte en zelfs capaciteit hebben. Het elementtype kan ook een struct zijn, zoals in het onderstaande programmafragment: struct Persoon { string naam, voornaam; int lengte; };
int mainO { vector personen; Persoon p; p.naam = . "Mozart"; . p.voornaam = "Wolfgang"; p.lengte = 160; personen.push_back(p); }
Ook het kopiëren van vectoren kan op een eenvoudige rnahier door gebruik te maken van de operator='; Bijvoorbeeld: vector v1(5, 2); vector v2 = v1;
//vector v1 bevat 5 keer 2 //v2 bevat een kopie van v1
Let wel: het kopiëren van een grote vector kan een "dure" operatie zijn, zowel qua tijd als qua geheugengebruik. Probeer dit dus indien mogelijk te vermijden. Ook wam:ieer men een vector als parameter doorgeeft aan een functie, .d an wordt er ook een kopie gemaakt, tenzij de parameter een referentie-parameter (invoer/uitvoer,..parameter) is:
60
Hoofdstuk 4. Containers
void f1(vector vee);
@[email protected]
11 NIET DOEN
void f2(const vector ·&vee);
I I OK
Bij de procedure f2 wordt eigenlijk enkel het adres van de vector doorgegeven. Indien de functie iets aan de vector moet wijzigen, moet men uiteraard ook een referentieparameter gebruiken, maar dan zonder const. Elke container heeft zijn eigen problematiek bij kopiëren, maar vermijd het onnodig kopiëren van grote objecten! Tot besluit van de bespreking van een vector wensen we nog op te merken dat het opmerkelijke aan dit container-type is dat indexering d.m.v. [] precies even snel is als indexering van een tabel! De vector is dermate handig en snel dat men zich kan afvragen waarom men nog een gewone tabel zou gebruiken. Eigenlijk is dit enkel nog nodig indien je werkt met bibliotheekfuncties die gewone tabellen als parameter verwachten. Het zelfde geldt overigens voor std:: string t.o.v. de kla8sieke C-string.
4.3.3
String
Deze standarci-string is eigenlijk een soort vector van karakters. Dat wil zeggen dat hij zijn eigen lengte kent en dynamisch kan groeien. Hiervoor kun je, naast . push_ back 0 ook de operator += gebruiken: string naam= "jan"; naam += "tje"; cout << naam.size(); Verder biedt de string-klasse nog een hoop extra functionaliteit aan, bijvoorbeeld voor het vergelijken(<,>,==, !=)van strings, zoeken en vervangen van substrings, enz.
4.4 4.4.1
De gelinkte lijst Principe van de gelinkte lijst
Een gelinkte lijst bestaat uit knopen, die op willekeurige plaatsen verspreid staan in het geheugen. Elke knoop bevat zowel data als wijzers (pointers) naar buurknopen. Bij een enkelgelinkte lijst bevat elke knoop een wijzer naar zijn opvolger. Bij . een dubbeigelinkte lijst bevat de knoop bovendien een wijzer naar zijn voorganger. Beschouw een eenvoudige enkelgelinkte lijst. De lijst zelf moet· enkel een wijzer naar de eerste knoop bevatten. Deze knoop noemt men de kop. De laatste knoop, de staart genaamd, heeft geen opvolger. Dit wordt vaak aangegeven door middel van de nullpointer.
61
[email protected]@hogent. be
Hoofdstuk 4.. Containers
- tMen kan de volledige lijst overlopen (en zo alle elementen een voor een bekomen) door telkens naar de opvolger te springen tot men bij de laatste knoop (of nullpointer) komt. Voor ·vele toepassingen is een dergelijke sequentiële toegang voldoende. De i-de knoop kan men echter niet zomaar direct bekomen, zonder .eerst alle voorgangers te overlopen. Indexeren of random access (d.m.v. vierkante haakjes) is dus bij gelinktè lijsten een dure operatie. Het grote voordeel van gelinkte lijsten is dat men heel snel een knoop kan toevoegen achter een bestaande knoop. Nieuwe knopen worden altijd eerst dynamisch gealloceerd. Daarria volstaat het de opvolglink van de nieuwe knoop naar de opvolger, van de bestaande knoop te laten wijzen, en de opvolglink van de bestaande knoop naar de nieuwe knoop. Vergelijk dit met invoegen in een tabel of vector, waarbij alle opvolgers moeten verschoven worden! Achteraan toevoegen is heel gemakkelijk, op voorwaarde ·dat men toegang heeft tot de staartknoop. Daarom houdt men in de gelinkte lijst behalve een wijzer naar de kop ook een wijzer naar de staart bij. Bij het wissen van een knoop moet men de opvolglink van de voorganger laten wijzen naar de opvolger en daarna het geheugen van de verwijderde knoop de-alloceren (teruggeven). Dit vereist echter dat men de voorganger van de te verwijderen knoop kent. Bij een eiikelgelinkte lijst kan dit niet rechtstreeks en moet men de elementen een voor een vanaf de kop overlopen om de voorganger te vinden .. : Hetzelfde probleem doet zich voor indien men een knoop· wil toevoegen vóór een bepaalde knoop. In een dubbeigelinkte lijst houdt elke knoop een link naar zijn opvolger en een link naar zijn voorganger bij. Dit zorgt voor iets meerboekhoudkundige operaties(linken verleggen) bij het toevoegen, maar het maakt wissen en toevoegen vóór een ·knoop heel eenvoudig. Bovendien kan men de lijst nu ook makkelijk achterwaarts overlopen. Soms laat men de laatste knoop terug naar de eerste wijzen. Op die manier krijgt men een Cirkulaire lijst. Zowel bij groeitabellen als bij gelinkte lijsten kan je heel snel achteraan een element toevoegen. De keuze tussen beide containertypes hangt af van andere factoren: als je frequent moet tussenvoegen is een gelinktè lijst aangewezen. Als dat niet het geval is ben je meestal beter af met een groeitabel, omdat je die random access toelaat. Een gelinkte lijst verbruikt meestal iets meer geheugen doordat elke knoop pointers moet bijhouden. Bovendien zorgt de verspreiding ih het geheugen voor performantieverlies omdat het cache-geheugen voortdurend herladen moet worden.
62
Hoofdstuk 4. Containers
4.4.2
@Helga.Naessens@bogent. be
List
Hoe maak je mi een gelinkte lijst in C++? Het kan "met de hand", door zelf knoopklassen (of structs) te definiëren met de gepaste data. Ook de gepa8te algoritmes voor toevoegen, zoeken, wissen e.d. moeten dan zelf geschreven worden. Dit is niet eenvoudig (hoewel heel leerrijk!) en wordt beschreven in paragraaf 10.2. Als alternatief kun je gebruik maken van de C++ standard library. Deze voorziet een klasse std:: list in <list>. Het gebruik is heel eenvoudig, zoals je kan zien in het onderstaande voorbeeld. int mainO { list lijst; lijst.push_back(2); lijst.push_back(3); lijst.push_front(1); }
Het resultaat is dus eèn lijst met achterenvolgens de getallen 1, 2 en 3. Zoals bij een vect~r kun je de grootte van een list opvragen met de lidfunctie si ze() , en de lijst volledig wissen met de lidfunctie clear (). Om de lijst te överlöpen of elementen tussen te voegen of te verwijderenl gebruikt men geen indexering (traag), maar iteratoren.
4.5
De iterator
Tabellen,_vectoren en gelinkte lijsten zijn voorbeelden· van sequentiële containers. Dat wil zeggen dat je de elementen in een bepaalde volgorde staan. Dit soort containers beschikken meestal over de mogelijkheid om de elementen een voor een te overlopen met behulp van een iterator (of "itereren", uit het Latijn: "iter" betekent "weg"). Een iterator kan ook gebruikt worden om één element aan te duiden, bijvoorbeeld als resultaat van een zoekactie. Een iterator is niets anders dan een wijzer naar één element van de container en heeft twee belangrijke operaties: • geef het element waar de iterator naar wijst • ga naar het volgende element (of naar het vorige) in de container In de C++ standard library heeft men ervoor gekozen om voor iteratoren de pointernotatie te gebruiken: indien i t een iterator is, dan is *i t het element waar de iterator naar wijst ("de waarde").
63
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
De iterator verplaatsen kan op twee manieren: ++i t of i t++ springt naar het volgende element --i t of i t-springt naar het vorige element Elk type container heeft een eigen implementatie voor de iterator. ·Wat er precies gebeurt indien je *i t of ++i t schrijft, hangt af van de container waar de iterator. bijhoort. Bij een vector betekent ++i t "schuif een geheugenplaats op", maar voor een gelinkte lijst betekent diezelfde opdracht "spring naar de opvolger van de knoop waar it naar wijst". Om de elementen van een container te overlop~n heeft men een iterator naar het eerste element nodig, plus een manier om aan te geven dat de volledige collectie doorlopen is. In de C++ standard library gebeurt dit aan de hand van twee speciale methodes, die elke container die een iterator toelaat, aanbiedt: . begin() . geeft eeri iterator die wijst naar het eerste element geeft een iterator die Wijst voorbij het laatste element, dus buiten de . end() container Men kan een deelreeks van een container aanduiden d.m.v. twee iteratoren die het begin en einde van de deelreeks markeren. Meestal hanteert men ook hier de conventie dat de tweede iterator voorbij het einde van de deelreeks wijst. Om de container te overlopen kan men schrijven: it = mijnlijst.begin(); while (it != mijnlijst.end()) { cout << *i t << endl; I I of doe i. ets anders met *i t ++it; }
Het mooie aan iteratoren is dat deze code werkt voor elk type (sequentiële) container, dus onafhankelijk of mijnlij st een vector is, of een list<double>; of ... Let op: je zou misschien geneigd zijn om in de while-voorwaarde een < te schrijven i.p.v. een ! =. Dit werkt echter niet voor alle containers, het lukt wel voor vectoren, maar niet voor lijsten. Af te raden dus. Overtuig jezelf er ook van dat het laatste element ook behandeld wordt, en niet verder!! En wat gebeurt er indien de collectie leeg is? Om bovenstaande code te compileren moet men de variabele i t ook declareren, m.a.w. men moet het type van de iteratör kennen. Zoals gezegd heeft elke soort container een eigen soort iterator, elk met een typisch gedrag. De juiste notatie is containertype: :i terator, voor het desbetreffende containertype. Bijvoorbeeld een iterator it voor de vector mijnlijst: vector<double> mijnlijst; · vector<double>::iterator it; Je kunt een iterator ook gebruiken om een element te wijzigen. Zo wordt bijvoorbeeld
64
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
in het volgende codefragment elke spatie in de lijst lst (liggend streepje). list lst; list::iterator i while (i != j) { i f (*i == ' ') *i = ' - ' ,.
verv~gen
lst . begin() , j = lst . end 0
door een underscore
;
++i; }
Om een element in een container te zoeken, kun je de elementen een voor een aflopen met behulp van een iterator en telkens controleren of het element de gezochte waarde heeft. Er bestaat echter ook een functie die dit doet: find(it1, it2, x) doorzoekt de deelreeks [i ü, it2 [ en geeft een iterator naar het eerste element met de waarde x. Indien de waarde x niet voorkomt in de gegeven deelreeks, wordt i t2 teruggegeven. Om een volledige container te doorzoeken gebruik je dus de iteratoren. begin() en . end() , zoals in het onderstaande codevoorbeeld: vector v; vector::iterator it = find(v . begin(), v . end(), x); i f (i t ! = v. end() ) cout << "gevonden" << endl; el se cout << "niet gevonden" << endl; Bekijk ook eens de volgende code die alle elementen tot en met het gezochte element uitschrijft : · vector::iterator itz = find(v.begin(), v.end(), a); vector< int>: :i terator i t = v·. begin() ; while (it ! = itz) { cout << *it << endl; ++it~
}
if (itz != v.end()) cout << *itz << endl; Merk op dat find() geen lidfunctie is van de container, maar een gewone (uitwendige) functie, waarbij het type van de iteratorE)n niet vastligt. De meeste containers voorzien ook methodes (lidfuncties) om met belwip van e{m iterator een element toe te voegen of te verwijderen: . insart (i t, x) : voeg x toe in de container vóór het element waar i t naar wijst . erase (i t): verwijder het element waar it riaar wijst uit de container Naast de gewone iterator bestaat er ook een const-iterator, met dezelfde functionaliteit,
65
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
behalve dat het niet mogelijk .is om het element waar de iterator naar wijst te wijzigen: list::const_iterator cit = ... cout « *cit « endl; // ok *ei t = 12; I I gaat niet! ! ! ! ! ! ! Indien de container als const-referentie aan een functie wordt doorgegeven, dan is men verplicht een const-iterator te gebruiken: void f(const list &lst) { list::iterator it = lst.begin(); .
//gaat niet
}
Tot slot van deze paragraaf wensen we nog op te merken dat men in andere programmeertalen andere notaties gebruikt voor iteratoren. In Java bijvoorbeeld gebruikt men de lidfuncties .getValue() en .nextO in plaats van'*' en'++'.
4.6
De stapel en de wachtrij
Dit zijn containers met beperkte mogelijkheden. Ze laten bijvoorbeeld geen iteratoren t oe. In de meeste implementaties kan men bij een stapel of wachtrij ook niet rechtstreeks aan het i-de element en kan men de elementen niet overlopen zonder ze te verwijderen. Het zijn dus geen sequentiële containers.
4.6.1
Principe van de stapel
Het principe van een stapel (Engels: stack) is het makkelijkst uit te leggen aàn de hand van een "echte" stapel, bijvoorbeeld een stapel borden. Men kan borden enkel bovenaan op de stapel borden toevoegen. D.eze operatie heet push. Men kan ook borden terug van de stapel afhalen, maar telkens enkel het bovenste horde (het top-element 5 ). Deze operatie heet pop. Dit kan natuurlijk enkel zolang de stapel niet leeg is. Daarom noemt men een stapel ook een LJFO-structuur (Last I~, First Out): het element dat als laatste op de stapel werd geplaatst, zal er als eerste terug afgehaald worden. Bij een herhaaldelijke pop worden de elementen dus in omgekeerde volgorde afgehaald. Behalve pop en push worden er meestal nog wat hulpoperaties voorzien, bijvoorbeeld om te kijken ofeen stapel leeg is, of om de grootte op te vragen. 5
Soms worden stapels ook wel horizontaal voorgesteld; in dat geval is top het voorste element.
66'
Hoofdstuk 4. Containers
4.6.2
@Helga.Naessens@hogent. be
Stack
In de C++ standard library vind je de klasse std: : stack in de bibliotheek <stack>. Hierin ·zijn tevens de volgende specifieke methodes voorzien om de elementen van de stapel te manipuleren: • push (x): voegt x toe bovenaan de ·stapel • pop(): verwijdert het laatst toegevoegde element van de stapel • top 0: geeft het laatst toegevoegde element terug, zonder het te verwijderen van de stapel. • empty (): gaat na of de staper leeg is • size (): geeft degröotte van de stápel terug Ter illustratie is hieronder een programma weergegeven om een reeks woorden (afgesloten door "stop") in omgekeerde volgorde uit te schrijven:
1
2
#include #include <stack> · using namespace std; int main() { stack<string> stapel; string woord; cin >> woord; while (woord != "stop") { stapei.push(woord); cin >> woord;
3
}
4 5 6
while (!stapel.empty()) { cout « stapel. top 0 « stapei.pop();
endl;
}
return 0; }
In tegell wordt de stack-bibliotheek geïncludeerd. In regel 2 wordt er een stapelvariabele gemaakt. In regel 3 wordt het laatste ingelezen woord op de stapel gezet. Na het inlezen komt men bij regel 4 terecht. Zolang de stapel niet leeg is wordt het bovenste element uitgeschreven (regel 5), en verwijderd (regel 6). Tot slot van deze paragraaf wensen we nog de volgende opmerkingen te maken:
67
Hoofdstuk 4. Contf!lners
@Helga.Naessens@hogent. be
• In de meeste boeken en in de mee8te stack-implementaties verwijdert de methode pop het bovenste element, en geeft het ook terug (return). In std: : stack geeft . pop() niets (void) terug, en moet men . top() aanroepen om hetbovenste element van de stapel te krijgen (of te wijzigen!). Hier zijn technische redenen voor. • In C++ is het niet mogelijk öm de elementen van een stapel te overlopen d.m.v. een iterator; je kunt enkel aan het top-element. Hetzelfde geldt voor een wachtrij en een prioriteitswachtrij (zie verder).
4.6.3
Principe van de wachtrij
Een wachtrij (Engels: queue) is vergelijkbaar met een stapel, maar met dat verschil dat pop het eerst toegevoegde element afhaalt. Dit noemt men FIFO (First In, First Out): het element dat als eerste toegevoegd werd aan de wachtrij, wordt er als eerste terug afgehaald. Denk hier aan een wachtrij bij een loket: wie eerst in de rij stond komt eerst aan de beurt. Meestal worden wachtrijen horizontaal voorgesteld.
4.6.4
Queue
In de standard C++ library vind je de klasse std: :queue aan in . Specifieke methodes die hierbij voorzien worden om de elementen van een wachtrij te manipuleren, zijn: • push (x): voegt x toe achteraan de wachtrij • pop(): verwijdert het voorste (eerst-toegevoegde) element vim de wachtrij • front(): geeft het voorste element terug zonder te verwijderen • back(): geeft het achterste (laatst-toegevoegde) element terug zondede verwijderen • empty 0: gaat na of de wachtrij leeg is • size (): geeft de grootte van de wachtrij terug Bijvoorbeeld: queue<double> r1J; rij. push(1. 5); rij.push(3.0); cout << rij.size() << ~ndl; cout << rij.front() << endl; cout << rij.size() << endl; rij .pop(); cout « rij . front() « endl; cout << rij.size() << endl;
11 2 11 1.5 11 2 11 3.0 11 1 68
Hoofdstuk 4. Containers
4.6.5
@Helga.Na:essens@hogent. be
Principe van de prioriteitswachtrij
De prioriteîtswachtrij (Engels: priority queue) is een uit breiding neefje van de wachtrij. Het beschikt naast de methodes van een wachtrij over de volgende extra functionaliteit: je kunt nog steeds elementen toevoegen met push, maar nu worden de elementen geordend opgeslagen. Het is dus noodzakelijk dat je elementen gebruikt die kunnen geordend worden, en die dus de operator < implementeren: de template-parameter kan bijgevolg bijvoorbeeld geen struct zijn! Een prioriteitsrij kan dus gebruikt worden om gegevens te sorteren. Een prioriteitswachtrij van strings slaat de strings op in lexicografische orq.e, zoals in een woordenboek. In de standard C++ library tref je hiervoor de klasse std: :priority_queue aan in . Beschouw als voorbeeld het volgende programma: . #include #include using namespace std; int mainO { priority_queue qu; qu.push('b'); qu.push('a'); qu.push('c'); cout << qu.sizeO <<" "; while (!qu.empty()) { · cout << qu. top() << " "; qu.pop.O; }
return 0; . }
waarvan de output er als volgt uitziet:
3
4.6.6
c
b
a.
Achterliggende implementatie
Een stapel wordt geïmplementeerd d.m.v. een container waarvoor men heel efficiënt een element (bv. achteraan) kan toevoegen en (terug bv. achteraan) kan verwijderen . . Dat kan bijvoorbeeld met een gelinkte lijst of met een groeitabeL Een wachtrij · wordt gemaakt door intern een container bij te houden die toelaat heel snel achteraanelementen toe te voegen en vooraan te verwijderen. In de praktijk is dat meestal een gelinkte lijst of een deque ( double-ended queue).
69
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
Men kan stapel en wachtrij eigenlijk ook beschouwen als een laagje rond een sequentiële container dat beperkté toegang regelt. Intern wordt een prioriteitswachtrij geïmplementeerd m.b.v. een heap, een speciale boomstructuur. Hoe dan ook, de gebruiker hoeft normaliter niet te weten wat er zich achter de schermen van de container bevindt.
4. 7 4. 7.1
De verzameling Principe van de verzameling
Een verzameling (Engels: set) is een container waarin elk element uniek is. Je kunt 1. elementen toevoegen, waarbij er niets gebeurt indièn dit element al aanwezig is 2. elementen opzoeken: geeft true of false 3. de elementen overlopen. Wat implementatie betreft zijn er twee families, met verschillende karakteristieken: 1. gebaseerd .op binaire zoekboom 2. gebaseerd op hash-tabel Een binaire zoekboom slaat intern de elementen in gesorteerde volgorde op (bv. van klein naar groot), en laat toe om vrij snel elementen toe te voegen of te vinden. Een hash-tabel is een speciale data-structuur waar je zeer snel elementen kunt aan toevoegen of uit verwijderen. De prijs voor deze verhoogde snelheid wordt betaald door het feit .dat de volgorde verloren gaat. Sequentieel overlopen gaat wel (met een iterator!}, maar in willekeurige volgorde. Een variant op de boomgebáseerde verzameling is de multi-verzameling. Ook hier worden de elementen gesorteerd opgeslagen, maar duplicaten worden ook bewaard. Dat wil zeggen dat een bepaald element meerdere keren kan voorkomen. Er bestaat geen dergelijk equivalent met hashing.
4. 7.2
Set
In de standard C++ librarY: is de klasse std: :set uit <set> een boomgebaseerde verzameling. Specifieke methodes die hierbij voorzien worden om de elementen van een verzameling te manipuleren, zijn: 70
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
• insert (x): voegt x toe, waarbij er niets gebeurt indien dit element al aanwezig is • erase (x): verwijdert x uit de verzameling, waarbij er niets gebeurt indien dit element niet aanwezig is • clear (): wist de volledige verzameling • count (x): telt het aantal keer dat x voorkomt (dit is uiteraard 0 of 1) • si ze(): het aantal elementen in de verzameling • empty () : gaat na of de verzameling leeg is • begin(): geeft een iterator terug naar het "eerste" element • end(): geeft een iterator terug naar het "laatste" element • find(x): geeft een iterator terug naar het gezochte element, of de . endO-iterator indien x niet voorkomt • erase (i t) : verwijdert het element waar de iterator naar wijst Beschouw als voorbeeld het programma uit Code 4.1.
1
2
3
#include #include <set> using namespace std; int mainO { set<string> v; string woord; cin >> woord; T.rhile (woord ! = "stop") { v.insert(woord); cin >> woord; }
string zoekwoord; .cout << "Wat moet ik zoeken? " ; cin >> zoekwoord; cout << zoekwoord << (v.count(zoekwoord) << " gevonden" <~ endl;
4
1 ?
set<string>::iterator it = v.find(zoekwoord); if (it != v.end()) cout << " gevonden" << endl; return 0;
5
}
Code 4.1: Voorbeeld met een set 71
1111
" niet")
Hoofdstuk 4. Containers.
@Helga.Naessens@hogent. be
In regel 1 wordt de nodige bibliotheek ingeladen. In regel 2 wordt een verzameling strings gedeclareerd. In regel 3 worden woorden toegevoegd (duplicaten worden maar 1 keer toegevoegd). In regel 4 schrijft men uit of een bepaald zoekwoord al dan niet voorkwam. In regel5 wordt het element gezocht met . find() die een iterator teruggeeft. Merk op dat . findO hier een lidfunctie is van set! Bij vector en list hebben we gebruik gemaakt van een externe furietie f ind (i t 1, i t2, x) met drie argumenten. Dit kan in principe ook voor een set, maar de lidfunctie is veel efficiënter en dus te verkiezen. Het overlopen van de elementen (in stijgende volgorde) kan d.m.v. iteratoren. Voorbeeld: string s = "abracadabra" ; set letters; for (int i = 0 ; i :< s.size() ; i++) letters.insert(s(i]); //voeg elke letter toe set: :iter a tor i t = letters. begin() ; while (it ! ,;. letters. end()) { cout « *it; ++it; }
Dit geeft als uitvoer "abcdr". Merk op dat de letters geordend zijn. Dit is een gevolg van de implementatie met een binaire zoekboom. Tot slot van deze paragraafwensen we nog op te merken.datin de standard C++ library de klasse std: :multiset uit <multiset> kan gebruikt worden voor ee·n multi-verzameling. Als je in regel 2 in Code 4.1 set vervangt door multiset, dan worden duplicaten wel bewaard. Het bepalen hoeveel keer een zoekwoord voorkwam, zou dan als volgt kunnen gebeuren: cout << zoekwoord << v.count(zoekwoord) << " keer gevonden" << endl;
4.8 4.8.1
De afbeelding Principe van de afbeelding
Een afbeelding (Engels: map) is een verzameling sleutel-waarde paren (key/value pairs). Bij het toevoegen geef je een sleutel en bijhorende waarde op. Aan de hand van de sleutel kun je dan een waarde opzoeken. Een map wordt ook wel eens woordenboek (Engels: dictionary) genoemd. Men spreekt ook wel van naam-waarde paren. Een map is dus eigenlijk niet veel meer dan een set van paren. Ze is op een binaire zoekboom gebaseerd, en de paren worden in sleutelvolgorde bewaard.
72
Hoofdstuk 4. Containers
4.8.2
@Helga.Naessens@hogent. be
Map
In de standard C++ library is in <map> de klasse std : :map voorzien om met afbeeldingen te kunnen werken. Specifieke methodes die hierbij voo:rzien worden om de elementen van een map te manipuleren, zijn:
• clear (): wist de volledige afbeelding • count (x): telt h~t aantal keer dat de sleutel x voorkomt (dit is 0 of 1) • size (): het aantal elementen in de afbeelding • empty () : gaat na of de afbeelding leeg is • begin() : geeft een iterator terug naar het "eerste" element • end(): geeft een iterator terug naar het "laatste" element • find(x): geeft een iterator terug naar de gezochte sleutel-, of de . endO-iterator indien de sleutel x niet voorkomt • erase (i t): verwijdert het element waar de iterator naar wijst • erase (x): verwijdert de sleutel x uit de afbeelding, waarbij er niets gebeurt indien dit element niet aanwezig is Het gebruik van map is vrij intuïtief omdat men de []-notatie van tabellen overgenomen heeft. Een voorbeeld: 1
#include <map> #using namespace std;
2 3 4 5
int main() { map<string, int> gewichten; gewichten["jan"] = 72; . gewichten["koen"] = 68; cout « gewichten["jan"] « éndl;
//72
In lijn 1 includeert men de nodige bibliotheek. In lijn 2 wordt een map< string, int> gemaakt. Merk op dat er bij de declaratie dus twee template-parameters zijn: een voor de sleutel (hier string) en een voor de waarde {hier int). In lijnen 3en 4 worden er sleutel-waarde paren toegevoegd. In regel5 wordt de waarde opgevraagd die overeenkomt met sleutel "jan". Er zit echter wel een addertje onder het gras: 6 cout « gewichten["piet"] « endl;
73
//oops
Hoofdstuk 4. Containers
@Helga.Naessens@hogent. be
geeft als resultaat 0 terug. Indien de waarde van een niet-voorkomende sleutel opgevraagd wordt, dan wordt die sleutel toegevoegd met a~s waarde nul! Je kunt itereren over de elementen van een afbeelding, maar bedenk wel dat elk element een paar is. Zo'n paar is een soort struct met twee leden: first is de sleutel en second is de waarde. Om alle elementen in een map uit te schrijven, schrijf je bijvoorbeeld: map<string, int> gewichten; ... //map opvullen map< string, int>: : i terat·or i t = gewi ehten. begin() ; while (it != gewichten.end()) { cout << (*it).first << 11 -> 11 << (*it) . second << endl; it++; }
Merk op dat een tabel (of vector) in zekere zin een map met gehele sleutels is. Bijvoorbeeld:· map
strin~> 11
m;
jan 11 ;
cout << m[10] << endl; Er is nochtans een belangrijk verschil: bij een (groei)tabel zijn alle elementen O... size- 1 aanwezig, maar bij een map niet. In bovenstaand voorbeeld bevat de map slechts 1 element! Dit kan een handige plaatsbesparing zijn indien je "ijle" (niet aaneensluitende) indices nodig hebt, maar de []-operator is wel minder efficiënt. Tenslotte wensen we nog op te merken dat er ook een multirnap bestaat waarbij je meerdere waarden aan een sleutel kunt koppelen. Je kunt ze makkelijk tellen met . count (x), maar het is niet zo makkelijk om de verschillende waarden van een sleutel op te vragen.
74
Hoofdstuk 5 Functies en procedures Programma'svan enige lengte worden nooit in één stuk geschreven. Net zoals een boek onderverdeeld wordt in hoofdstukken (en die weer in paragrafen), splitst men een programma op in afzonderlijke delen die elk één welbepaalde taak hebben. Eén van die delen noemt men het hoofdprogramma, en het kan gebruik maken van alle andere delen. De naam van het hoofdprogramma in C++ is steeds main. De andere delen noemt men functies en procedures 1 . Hoe we 'ingebouwde' functies en procedures moeten oproepen, hebben we reeds besproken in het eerste hoofdstuk. In dit hoofdstuk zullen we jullie aantonen hoe eigen functies en procedures kunnen geschreven worden.
5.1
Functies
Stel eens dat we in de loop van een programma de volgende uitdrukking moeten berekenen:
Onderstel bovendien dat we niet kunnen beschikken over functies voor machtsverheffing en logaritme, zodat we zelf de derdemachtswortels zullen moeten berekenen. (Wie die functies schreef, stond immers voor een vergelijkbaar probleem.) De volgende oefening op de while-structuur biedt een mogelijke oplossing: Om de derde machtswortel van een reëel getal a te berekenen, maken we gebruik van de methode van Newton: beschouw de rij xo = 1, x1, ... met als eigenschap
. . Xn+l
=
2x~ +a
3x2 n
1 In C++ spreekt men eigenlijk enkel over functies. Een procedure is niets anders dan een void-functie, of dus een functie zonder resultaat.
75
Hoofdstuk 5. Functies en procedures
@Helga.Naessens@hogent. be
Deze rij convergeert naar ~- Om het resultaat tot op 3 cijfers na de komma nauwkeurig te bepalen, moeten er op~nvolgende waarden van Xn berekend worden totdat lxn+l - Xnl < 0.001. Hiermee zou het programma er kunnen uitzien zoals in Code 5.1.
int mainO { double x, y, a; double term1, term2, resultaat; //derdemachtswortel uit x- a double u, v; //twee opeenvolgende termen uit de reeks u = 1; v = (2 +x- a) /·3; · while (fabs(v - u) >= 0.001) { v·, u v = (2 *u* u* u+ x~ a) I (3 *u* u) ; }
term1 = v; //derde machtswortel uit _Y - a u= 1;
v=
(2 +J Y - a)
I 3;
while (fabs(v - u) >= 0.001) { u
v;
v
= (2
*u~
u* u+ y - a) I (3 *u* u);
}
term2 = v; //derdemachtswortel uit de som u= 1;
v = (2 + term1 + term2) I 3; while (fabs(v - u) >= 0.001) { U
v
Vj
= (2 *u* u* u+ term1 + term2) I (3 *u* u);
}
//het gewenste resul taat resultaat = v; return 0; }
Code 5.1: Berekent een ingewikkelde uitdrukking zonder functies
Zoals je merkt valt deze oplossing vrij'langdradig uit en kunnen in een dergelijke oplossing gemakkelijk fouten gemaakt worden. Hoeveel eenvoudiger zou het niet geweest zijn indien in de opgave vierkantswortels in de plaats van derdemachtwortels voorkwamen. Daarvoor 76
Hoofdstuk 5. Functies en procedures
@Helga:Naessens@bogent. be
bestaat immers een 'ingebouwde' functie sqrt, zodat het programma zich zou beperken tot Code 5.2. int main() { double x, y, a; double resultaat; resultaat
sqrt(sqrt(x - a) +. sqrt(y - a)) ;
return 0; }
Code 5.2: Berekent ei:m ingewikkelde uitdrukking m.b.v. sqrt
Dit is niet alleen veel korter, maar ook veel duidelijker: de notatie gelijkt op ·die van de wiskunde. Gelukkig laat C++ dus toe om zelf functies te schrijven. Dat zijn aparte stukjes programma die vóór het hoofdprogramma staan en er mee samenwerken. Het doel van een functie is, vertrekkend van bepaalde gegevens, het berekenen van één enkel resultaat. Een functie definiëren we door er een naam voor te kiezen, te specificeren waarop ze werkt en wat het type is van haar resultaat. Deze drie gegevens staan op de eerste lijn van qe functiedefinitie, in de zogenaamde hoofding. . Daarna staan tussen accolades de opdrachten die de functie uitvoert om haar resultaat te berekenen. De accolades zijn hier echter steeds verplicht, ook al bevatten ze maar één opdracht. De functie wordt steeds beëindigd door een return-opdracht, die de waarde van het resultaat definieert. Het type van het resl,ll.taat kan zowat alles zijn, behalve (om efficiëntieredenen) een tabel. . De return-opdracht
I return uitdrukking;
::::} Is steeds de laatste opdracht van een functie en geeft de waarde van
uitdrukking als resultaat terug ::::} Het ·type van uitdrukking moet overeen komen met het type van het resultaat van de .functie (dat in de hOofding van de functie vermeld staat) Een functie voor onze derdemachtswortel zou er bijvoorbeeld kunnen uitzien zoals in Code 5.3.
77
@Helga.Naessens@hogent. be
Hoofdstuk 5. Functies en procedures
double root3 (double a) { const double EPS = 0.001; double vx 1 , x = (2 + a) I 3; while (fabs(vx - x) >= EPS) { vx =x; x= (2 * vx * vx * vx +a) I (3
=
*
vx
*
vx);
}
return x; }
Code 5.3: Functie voor derdemachtswortel De naam die aan de functie gegeven werd is root3. Vóór de naam van de functie staat het type van het resultaat, namelijk double. Achter de naam staat tussen haakjes de (formele) parameter waarop de functie zal in~erken. Deze parameter bestaat uit een type en een symbolische naam (a in ons voorbeelçl) voor het (voorlopig onbekende) getal waaruit de derdemachtswortel zal getrokken worden. Bij het ~chrijven van de functie hebben we die symbolische naam nodig om te kunnen specificeren welke bewerkingen er met dat nog onbekend getal moeten gebeuren. Binnen de accolades lijkt alles zeer goed op een hoofdprogramma. De variabelen en constantEm die daar eventueel gedefinieerd worden, noemt men lokale variabelen en constanten. Let echter wel goed op: deze lokale variabelen hebben niets met die van het hoofdprogramma te maken! Ze mogen zelfs dezelfde naam dragen als de variabelen uit het hoofdprogramma. Als ze van waarde veranderen:, heeft dat geen enkele invloed op een eventuele naamgenoot in het hoofdprogramma (en omgekeerd). Daarom moeten we ons ook nooit afvragen of een lokale ~ariabelenaam reeds elders in een (groot) programma in gebruik is. De naam van de parameter (a in ons voorbeeld) is eveneens lokaal, en mag binnen de functie niet meer voor een andere variabele gebruikt worden. Zoals voor elke variabele moet de waarde van een lokale variabele geïnitialiseerd worden vooraleer men ze gebruikt. Bekijk nog maar eens de functie root3 uit Code 5.3: de lokale variabelen vx en x worden eerst geïnitialiseerd. Dit geldt echter niet voor de (formele) parameter: die heeft altijd reeds een beginwaarde bij . de aanvang van de functie. Als je nogmaals de functie root3 bekijkt, bemerk je hoe de parameter a onmiddellijk (nl. zonder eerst nog een waarde toe te kennen aan a) gebruikt wordt bij de initialisatie van de lokale variabele x. -Een zelf gedefinieerde functie wordt op dezelfde manier opgeroepen als een ·ingebouwde functie: we schrijven haar naam gevolgd door tussen haakjes het gegeven waarop ze moet werken. Dit laatste noemt rrien de werkelijke, de actuele parameter. De waarde daarv~ wordt gebruikt om de formele parameter bij de oproep te initialiseren. Het resultaat van de functie komt dan in dè plaats van de oproep, en moet dus gebruikt worden als onderdeel van een opdracht (anders gaat het resultaat verloren).
78
Hoofdstuk 5. Functies en procedures
@Helga.Naessens@hogent. be
Met de functie voor de derdemachtswortel ziet het hoofdprogramma in Code 5.4 er heel wat overzichtelijker uit, net als bij de vierkantswortels. int main() { double x, y, a; double resultaat; resultaat = root3(root3(x- a) + root3(y - a)); return 0; }
Code 5.4: Berekent een ingewikkelde uitdrukking m.b.v. de functie root3
De actuele parameters van de binnenste functies zijn resp. ·x - a en y - a. Het resultaat van bijvoorbeeld root3 (x - a) vervangt deze oproep in de uitdrukking w~rin hij voorkomt. Dé actuele parameter voor de buitenste functie is dus de som van de resultaten van de twee binnenste functie-oproepen. Deze actuele parameters zijn telkens de beginwaarden voor de formele parameter a in de functie. Bemerk ook dat het hóofdpn~gramma eigenlijk ook een functie is met als resultaat een geheel getal, dat aan het besturingssysteem teruggegeven wordt. Indien de waarde 0 geretourneerd wordt, betekent dit dat dé uitvoering van het programma zonder problemen verlopen is. Een resultaat verschillend van 0 (meestal 1) geeft aan dat er een fout opgetreden is, Wat het besturingssysteem daar~ee doet, heeft niets meer te maken niet
C++. Elke constante, type, of variabele die in een programma gebruikt wordt, moetreeds vroeger het programma gedeclareerd worden. Dit. geldt ook voor zelf gedefinieerde functies. Die worden vóór het hoofdprogramma geschreven2 , meestal na de include-opdrachten:. Functies kunnen gebruik maken van andere functies (zoals hier fabs); Opnieuw geldt dezelfde regel: de gebruikende functie komt na de gebruikte. (fabs wordt via de iricludeopdracht beschikbaar gemaakt.)
in
Een functie kan ook meer dan één parameter hebben. In de functiehoofding staat dan tussen de haakjes een parameterlijst, waarin de parameterdefinities gescheiden worden door komma's. Per type mag je echter slechts één parameter opgeven, gelijke type-namen moeten dus herhaald worden.
Algemeen: functie-defini tie type_ resultaat riaam_funtie ( type_par1 naam_ par1, type_par2 naam_par2, . . . ) { lokale declaraties en opdrachten } 2
Eventueel kan ook enkel de declaratie van de functie voor het hoofdprogramma vermeld worden (zie vroeger) en wordt de definitie van de functie elders geschreven(bijvoorbeeld na het hoofdprogramma) . .
79
Hoofdstuk 5. Functies en procedures
@Helga.Naessens@hogent. be
Als voorbeeld geven we in Code 5.5 de functieggddie de grootste gemene deler van twee gehele getallen a en b bepaalt met behulp van het algoritme van Euclides. De functie onderstelt dat zowel a als b niet-negatief zijn, en niet beide nul. Het is de verantwoordelijkheid van de gebruiker van de functie om daarvoor te zorgen. int ggd (int a, int b) { while (b != 0) { int rest = a % b; a = b; b = rest; }
return a; }
Code 5.5: Functie die de grootste gemene deler van twee getallen bepaalt De volgorde van de parameters in de parameterlijst is belangrijk. Eens ze gespecificeerd werd, moet men er zich bij elke oproep aan houden. De waarde van de pàrameters wordt hier door de functie gewijzigd. Functieparameters zijn inderdaad een soort lokale variabelen, met dat verschil dat ze steeds een beginwaarde hebben. Deze wijzigingen hebben .geen enkele invloed op de actuele parameters in het hoofdprogramma. De actuele parameters mogen ook constanten zijn, vermits hun waarde dient om de formele parameters te initialiseren. Zo kunnen we bijvoorbeeld schrijven: co.ut << ggd(124, 847) «
endl;
Als voorbeeld van het gebruik van een dergelijke functie in een hoofdprogramma, berekenen we in Code 5.6 de grootste gemene deler van een aantal getallen, die vooraf in een tabel ingelezen worden. Bemerk dat de actuele parameters hier tabelelementen zijn. Voor de functie is dat om het even, zolang het maar gehele getallen zijn, zoals gespecificeerd.
5.1.1
Logische functies
Functies worden ook vaak gebruikt om een voorwaarde te testen. Het resultaat van deze logische functies is dan van het type bool, en kan enkel true of false zijn. Het is een goede gewoonte om dergelijke functies een naam te geven die met ja ofneeii moet beantwoord worden. (Dit geldt trouwens voor alle functies: de naam weerspiegelt liefst het resultaat.)
ao
Hoofdstuk 5. Functies en procedures
· @Helga.Naessens@hogent. be
#include using namespace std; int ggd (int a; int b) { }
int main() { int n; int tab[50]; //inlezen van eerst n (2<=n<=50) en dan van n getallen in tab //bepalen van de grootste gemene deler in tab int g = ggd(tab [OJ, tab [1]); for (int i = 2 ; i < n ; i++) g = ggd(g, tab [i]); cout << 11 De ·ggd van alle getallen is 11 << g << endl; return 0; }
Code 5.6: Berekent de grootste gemene deler van getallen uit een tabel
Bijvoorbeeld, de functie vormen_ driehoek uit Code 5. 7 is een logische functie die test of drie willekeurige reële getallen (ze mogen ·zelfs negatief zijn) de lengten van de zijden van een driehoek kunnen vormen. bool vormen_driehoek (double a, double b, double c) { return a <= b + c && b <= c + a && c <= a + b; }
Code 5.7: Logische functie Een logische functie kan dan in een test gebruikt worden, zoals elke logische waarde: double x, y, z; cout << 11 De lengten 11 <<x<< 11 , 11 << y << 11 , 11 << z << << (vormen_driehoek (x, y, z) ? 1111 ngn) 11 11 << een driehoek vormen. << endl;
11
kunnen
11
Een tweede voorbeeld van een logische functie is weergegeven in Code 5.8. Deze functie gaat na of het woord dat meegegeven is als parameter .aan de functie een palindroom is. Een palindroom is een symmetrisch woord dat van links naar rechts gelezen hetzelfde is als van rechts naar links gelezeil .(zoals het woord "meetsysteem") .
81
Hoofdstuk 5. Functies en procedures
©Helga.Naessens@hogent. be
bool is_palindroom(string s) { int i = o; j = s.size() - 1; while (i < j && s [i] =.;. s [j]) { i++; j -- ; }
return i >= j; }
Code 5.8: Logische functie
5.1.2
Structs als functieresultaat en als functieparameter
Beschouw de volgende voorstelling van een complex getal als een gecombineerd reëel en imaginair deel: struct Complex { . double re, ~m; };
In Code 5.9 is de functie norm weergegeven om de norm van een dergelijk complex getal te berekenen. double norm (Complex c) { return sqrt(c.re * c.re + c.im
*
c.im); ·
}
Code 5.9: Functie berekent de norm van een complex getal Om het product van twee complexe getallen te berekenen, kunnen we gebruik maken van de functieproduit Code 5.10. · Complex prod (Complex a, Complex b) { Complex res; res.re = a.re * b.re - a.im * b.im; res.im = a.re * b.im + a.im * b.re; return res; }
Code 5.10: Functie berekent het product van twee complexe getallen Uit de voorgaande voorbeelden blijkt dus dat structs, net als de gewone scalaire gegevenstypes, als functiewaarde en als functieparameter kunnen dienen. Ook het voorbeeld Code 3.4 uit Hoofdstuk 3 waarin de omtrek van een veelhoek bepaald wordt, kunnen we beter aan de hand van enkele functies herschrijven. Hiervoor maken we gebruik van de volgende structs:
82
Hoofdstuk 5. Functies en procedures
@Helga.Na.essens@hogent. be
struct Punt { double x , y; } ;
struct Veelhoek { int aantalptn; Punt hoekptn[40] ; };
Vooreerst schrijven we een functie afstand die de Euclidische afstand tussen twee punten in het vlak bepaalt: double afstànd (Punt pi, Punt p2) { return sqrt(sqr(p1. x - p2 . x) + sqr(p1.y - p2.y)); }
Voor de duidelijkheid maakt deze functie gebruik van een functie sqr om het kwadraat van een reëel getal te berekenen: double sqr (double x) { ' return x * x ; }
In Code 5.11 isde functie omtrek weergegeven die de omtrek van een veelhoek bepaalt, wat duidelijk een heel stuk overzichtelijker is dan het programmafragment uit Code 3.4. double omtrèk (Veelhoek v) { double som= afstand(v.hoekptn[v . aantalptn - 1], v.hoekptn[O]) ; for (int · i = 1 ; i < v.aantalptn ; i++) { som+= afstand(v.hoekptn[i - 1], v.hoekptn[i]); return som; }
Code 5.11: Functie bepaalt de omtrek van een veelhoek
5.1.3
Tabellen als functieparameters
Ook een tabel kan doorgegeven worden als parameter van een functie. Als voorbeeld nemen we de functie index uit Code 5.. 12 die de plaats bepaalt van een opgegeven geheel getal g in de èerste n posities van een tabel tab van gehele getallen. Als het getal niet gevonden wordt, is het resultaat de niet-bestaande index -1. Als het getal meermaals voorkomt, dan is het resultaat de kleinste index.
83
Hoofdstuk 5. Functies en procedures
@Helga.Naessens@hogent . be
int index (int g, const int tab[], int n) { int i = 0; while (:1 < n && g != tab[i]) i++; return i < n ? i : -1; ·}
Code 5.12: Functie bepaalt de index. van een getal in een tabel Bemerk dat hier de formele tabelparameter geen tabelgrootte opgeeft (en dus bruikbaar is voor meerdere groottes). De actuele tabelparameter mag dus elke grootte hebben. (De const staat er opdat de functie niets aan de tabel zou kunnen veranderen. We komen hier later nog op terug.) De functie moet echter wel weten hoe ·groot de actuele tabel is, vandaar de derde parameter. De functie index kan bijvoorbeeld als volgt opgeroepen worden: int pos, t[1000]; for (int i = 0 ; i < 1000 ciii » t[i]; pos= index(O, t, 1000);
i++)
Dit programt:nafragment gaat in de tabel t op zoek naar het getal 0. Merk op dat je als tweede parameter enkel de tabelnaam meegeeft. Vierkantje haakjes worden hier niet geschreven! De functie index kan bijvoorbeeld ook gebruikt worden om de doorsnede van twee verzamelingen van gehele getallen te bepalen, zoals weergegeven in Code 5.13. Stel dat beide verzamelingen resp. n1 en n2 getallen bevatten, en dat we ze opgeslagen hebben in de tabellen v1 en v2. De doorsnede komt in de tabel v3, en zal n3 getallen bevatten. Merk op dat hier de exacte waarde van de index niet gebruikt wordt, enkel het teken interesseert ons. De volgende functie berekent de waarde van een reële veelterm met graad n
in een gegeven punt x. De reële coëfficiënten Ci zitten in volgorde opgeslagen in een tabel v, de constante term: bij index nul. (Sommige coëfficiënten mogen nul zijn, Cn echter niet.) De evaluatie gebeurt met de efficiënte methode van Homer. Deze begint met de coëfficiënt van de hoogste graad, vermenigvuldigt telkens het vorig resultaat met x en telt er de volgende coëfficiënt bij op:
v(x) = (· · · (((en)x + Cn-,I)x + Cn-2)x + · · · + c1)x +co Dat vereist precies n optellingen en evenveel vermenigvuldigingen (het minimaal aantal). 84
Hoofdstuk 5, Functies en procedures
@Helga.Naessens@bogenf be
int main() { int n1, n2; int ~1[100], v2[100]; //inlezen van n1, v1 en van n2, v2 int n3 = 0; int v3[100]; i++) for (int i = 0; i < n1 if (index(v1[i], v2, n2) >= 0) v3[n3++] = v1[i]; }
Code 5.13: Bepaalt de doorsnede van twee verzamelingen De functie eval_veelt is weergegeven in Code 5.14. double eval_veelt (double x, · const double v[], int n) { double res =v[n]; for (int i = n - 1 ; i >= 0 ; i-:..) res = res *x+ v[i]; return res; }
Code 5.14: Berekent de waarde van een veelterm in een gegeven punt
In het laatste voorbeeld kijken we of twee tabellen dezelfde elementen hebben in dezelfde volgorde. De eerste tabel bevat m elementen, in de tweede tabel zijn n elementen ingevuld. De bedoeling is dat we enkel die elementen bekijken waarin de tabellen overlappen. Het volstaat dus om <;ie eerste min elementen te bekijken: waarbij min het minimum is vanmen n. De logische functie gelijk is weergegeven in Code 5. 15. (Waarom gebruiken we hier EPS en fabs?) .
5.2
Procedures
Het doel van een functie is het berekenen of bepalen van één enkele waarde en die als resultaat op de plaats van oproep terug te geven. Indien een deelprogramma echter meerdere resultaten heeft, of een tabelresultaat, of zelfs geen resultaat, dan moet men een procedure gebruiken.
85
Hoofdstuk 5. Functies en procedures
@[email protected]
bool gelijk (const double tabm[]~ int m, const double tabn[], int n) { co:nst double EPS = 0.000001; int i = 0, min = m > n ? n : m; while (i < min && fabs(tabm[i] - tabn[i]) < EPS) i++; return i == min; }
Code 5.15: Gaat na of het overlappend gedeelte van twee tabellen gelijk is Een procedure is een soort uitgebreide functie. Wanheer er slechts één scalair resultaat is, kunnen we zowel een procedure als een functie gebruiken. Meestal verkiest men in dat geval een functie, omdat die rechtstreeks in een uitdrukking kan opgenomen worden. Bij . een procedure is dat onmogelijk (omdat procedures meerdere resultaten kunnen hebben). Een proceduredefinitie ziet er quasi hetzelfde uit als een functiedefinitie. Nu begint de hoofding echter met het gereserveerd woord void 3 in plaats van met een resultaattype en komt er geen return-opdracht voor. In. C++ is een procedure gewoon een functie zonder typ~.
5.2.1
Procedures zonder parameters
Dergelijke procedures lijken op het eerste zicht misschien niet echt nuttig, maar toch kunnen ze in bepaalde gevallen erg handig zijn. Beschouw bijvoorbeeld het eenvoudige programma Code 5.16 waarin aan de gebruiker een keuzemenu getoond wordt, waarna afhankelijk van de keuze van de gebruiker bepaalde berekeningen uitgevoerd worden. Zoals je kan zien, wordt een dergelijk programma op deze manier nogal snel onoverzichtelijk en lang. Een veel betere oplossing is weergegeven in Code 5.17, waar we gebruik gemaakt hebben van een procedure om het keuzemenu weer te geven. In de procedure tooii_keuzemenu worden alle schrijfopdrachten die het keuzemenu vormen, gebundeld. Merk op dat er nergens in deze procedure een return-'opdracht te bespeuren valt. · In het hoofdprogramma wordt ·de procedure opgeroepen door haar naam te schrijven, in tegenstelling tot de oproep van een functie, gevolgd door lege haakjes. De oproep is een zelfstandige opdracht afgesloten door een kommapunt, geen (deel van) een uitdrukking.
nu,
3
Nederlandse vertaling: ledig, ongeldig, nietig.
86
Hoofdstuk 5. Functies en procedures
int main() int k; cout << co ut << cout << co ut << cout << co ut << co ut << cout << co ut <<
@Helga.Naessens@hogent. be
{
"=='========== Keuzemenu ============" << endl; "===================================" << endl;
endl; "1: som berekenen van 2 getallen" << endl; "2: produkt berekenen van 2 getallen" << endl '; endl; "0: stoppen" << endl; endl « endl; "Uw keuze: "; ~in » k; while (k != 0) { i f (k
==
1) {
//som berekenen van 2 getallen }
else if (k == 2) { //produkt berekenen van 2 getallen }
else { · cout << "Foutieve keuze: kies 1,2 of 0" << endl; }
cout. << "============ Keuzemenu ============" << endl; cout << "===================================" << endl; cout << endl; cout << "1: som berekenen van 2 getallen" << endl; cout << "2: produkt berekenen van 2 getallen" << endl; cout << endl; cout << "0: stoppen" << endl; cout << endl << endl; cout << ."Uw keuze: " ; cin » k; }
return 0; }
Code 5.16: Programma met keuzemenu zonder procedure
87
·©Helga.Naessens@hogent. be
Hoofdstuk 5. Functies en procedures
void toon_keuzemenu() { co ut << "============ Keuzemenu ============" << endl; co ut << "===================================" << endl; co ut << endl; co ut << "1: som berekenen van 2 getallen" << endl; co ut << "2: produkt berekenen van 2 getallen" << endl; cout <<· endl; co ut << "0: stoppen" << endl; co ut << endl « endl; cout << "Uw keuze: "; }
int mainO { int k; toon_keuzemenu(); cin » k; while (k != 0.) { if (k == 1) {
//som berekenen van 2 getallen }
else if (k == 2) { //produkt berekenen van 2 getallen }
else { . cout << "Foutieve keuze: kies 1,2 of 0" << endl; }
toon_keuzemenu(); cin » k; }
return 0; }
Code 5.17: Programma met keuzemenu met procedure toon....keuzemenu
88
Hoofdstuk 5. Functies en procedures
5.2.2
@Helga.Naessens@hogent. be
Procedures met enkel invoerparameters
Dergelijke procedures zijn procedures die enkel invoergegevens verwachten en geen expliciete gegevens als resultaat hebben. Men kan ze bijvoorbeeld gebruiken om informatie in een bepaalde vorm naar het scherm te schrijven. Stel dat we de getallen tussen de grenzen a en b op het scherm willen schrijven (waarbij er mag verondersteld worden dat a::::; b). Om te vermijden dat de lijnen te lang worden, geven we het aantal getallen n dat elke lijn moet bevatten (behalve eventueel de laatste). De procedure schrijf_uit is weergegeven in Code 5.18. void schrijf_uit (int a, int b, int n) { int aantal b - a+ 1; int g = a; //volledige lijnen uitschrijven for (int 1 = 0 ; 1 < aantal I n 1++) { for (int k = 0 ; k < n ; k++) cout << setw(6) << g++; cout << endl; }
//laatste lijn uitschrijvèn i f (g
<= b) { (g <= b)
w~ile
cout « setw(6) « g++; cout << endl; } }
Code 5.18: Schrijft de getallen tussen a en b netjes uit
In het hoofdprogramma wordt deze procedure opgeroepen door haar naam te schrijven, gevolgd door tussen haakjes de gepaste a<:;tuele parameters, zoals bijvoorbeeld: schrijf_uit(25, 100, 7); Dit schrijft de getallen van 25 tot 100 op het scherm, 7 getallen per lijn.
5.2.3
Procedures met uitvoerparameters
Omdàt procedures meer dan één resultaat kunnen hebben, bezorgen ze die resultaten aan de oproeper via parameters, aangezien het aantal parameters niet beperkt is. Bij alle functies en procedures die we tot nu toe besproken hebben, werd er enkel gebruik gemaakt van invoerparameters. Dergelijke parameters zijn .steeds geïnitialiseerd bij de
89
@Helga.Naessens@hogent. be
Hoofdstuk 5. -Functies en procedures
aanvang van de fun-ctie/procedure. Wijzigingen aan invoerparameters hebben ook nooit een invloed op de actuele parameters uit het hoofdprogramma. Er bestaat echter nog een tweede soort parameter: de uitvoerparameter. Uitvoerparameters zijn parameters die geen beginwaarde hebben (en dus willekeurige informatie bevatten), maar door de functie/procedure ingevuld worden. De waarde die de uitvoerparameters dus hebben als de procedure eindigt, is het resultaat van de oproeper. De richting van een parameter (invoer of uitvoer) wordt vanuit het standpunt van de functie/procedure bepaald. In de parameterlijst mogen de invoerparameters en de uitvoerparameters door elkaar staan. Om de beide soorten parameters vàn elkaar te kunnen onderscheiden, noteert me:n. een ampersand (&) vóór de naam van de scalaire (formele) uitvoerparameter (bij tabellen wordt dus géén ampersand gebruikt, maar hier komen we later nog op terug). Als eerste voorbeeld bepalen we de grootste gehele waarde m waarvoor nm < g, waarbij n en g twee gegeven gehele getallen zijn en we veronderstellen dat 1 < n < g. Vermits er slechts één scalair resultaat is, kan dit zowel als functie als onder de gedaante van een procedure geschreven worden. Op die manier zien we duidelijker het verschil. De functie is weergegeven in Code 5.19, de procedure iri Code 5.20. int e:?Cponent (int g, int n) { -int macht = n, m = 1; while (macht < g) { macht *= n; m++; }
m--; return m; }
Code 5.19: Functie bepaalt grootstem zodat nm < g void bepaal_exponent (int int macht = n, tm 1; while (macht < g) { macht *= n; tm++;
g~
int n, int -&m) {
}
m := tm - 1; }
Code 5.20: Procedure bepaalt grootstem zodat nm
In plaats van · de return-opdracht wordt er op het einde van de · procedure voor gezorgd dat de uitvoerparameter m de juiste waarde heeft. De lokale variabele tm is eigenlijk overbodig-we kunnen even goed meteen de parameter gebruiken. De procedure kan dus 90
Hoofdstuk 5. Functies en procedures
. @Helga.Naessens@hogimt. be
herschreven worden als in Code 5.21. void bepaal_exponent (int g, int n, int int macht = Ii; m = 1; while (macht < g) { matht *= n; m++;
&m) {
}
m--; }
Code 5.21: Procedure bepaalt grootstem zodat nm
Een hoofdprogramma waarin zowel de functie als de procedure opgeroepen worden, is weergegeven in Code 5~22. Het gebruik yan een procedure vereist hier twee stappen om hetzelfde effect te verkrijgen als met de functie. Dat is natuurlijk de prijs die betaald wordt voor de grotere mogelijkheden van een procedure. Bij dit voorbeeld prefereert men meestal de functie, zoals ·reeds gezegd, int main() { int g, n, m; //gen n initialiseren //als functie cout << exponent(g, n) << endl; //als procedure bepaal_exponent(g, n, m) ;. cout << m << endl; }
Code 5.22: Verschil tussen de oproep van een functie en een procedure Merk op dat er in een uitvoerparameter geschreven wordt (na afloop van de procedure is die immers ingevuld), zodat een actuele uitvoerparameter steeds een variabele (lvalue) moet zijn. Dit is niet zo bij een actuele invoerparameter: een dergelijke parameter mag eventueel ook een constante waarde bevatten. Stel nu dat we niet 'alleen de grootste gehele waarde m nodig hebben waarvoor nm < g, met n en g twee gegeven gehele getallen, maar dat we eveneens geïnteresseerd zijn in de waarde nm. Met behulp van de functie exponent uit Code 5.19 zouden we de waarde m kunnen bepalen, terwijl we een tweede functie macht zouden kunnen schrijven om vervolgens de waarde van nm te bepalen .. Het spreekt natuurlijk voor zich dat het veel efficiënter is om de beide waarden tegelijkertijd te bepalen. Dit wordt dan een procedure met twee uitvoerpaiameters, zoals weergegeven in Code 5.23. 91
Hoofdstuk 5. Funçties en procedures ·
@Helga.Naessens@hogent. be
void bepaal_exp_en_macht (int g, int n, int &m, int &macht) { macht = n; m
= 1;
while (macht < g) { macht *= n; · m++; }
m--; macht /= n; }
Code 5.23: Procedure bepaalt grootstem en nm zodat nm < g Ook structs kunnen niet alleen gebruikt worden als invoerparameters, maar ook als uitvoerparameters of als invoer-uitvoerparameters (zie volgende paragraaf). De functie prod uit Code 5.10 die het product vap twee complexe getallen berekent en bijgevolg een struct als functieresultaat bezit, kan dus eigenlijk ook als een procedure geschreven worden. Deze procedure is weergegeven in Code 5.24. void bepaal_prod (Complex a, Complex b, Complex &c) { c.re = a.re * b.re - a. im * b.im; c.im = a.re * b.im + a.im * b.re; }
Code 5.24: Procedure berekent het product van twee complexe getallen
5.2.4
Procedures met invoer-uitvoerparameters
Een parameter die reeds bij het begin van een procedure geïnitialiseerd is, en bovendien gebruikt wordt om op het einde een resultaat terug te geven, is een invoer-uitvoerparameter. C++ maakt echter geen ondersclieid tussen een dergelijke parameter en een zuivere uitvoerparameter. Dat wil zeggen dat er een & moet. staan vóór een (scalaire) parameter, en .eigenlijk niets vóór een tabelparameter (zie verder) . . Toch is het nuttig om conceptueel steeds een onderscheid te maken tussen de verschillende soorten parameters. Dit helpt om een juist gebruik van de parameter te verzekeren (de compiler laat ons immers in de steek), en bovendien bestaan er programmeertalen die wél het verschil duidelijk maken en door de compiler laten controleren. Als voorbeeld op een procedure die gebruik maakt van invoer-uitvoerparameters, geven we in Code 5.25 een procedure verwissel die de inhoud van twee reële getallen verwisselt. De parameters x en y moeten reeds ingevuld zijn en worden geWijzigd .. Het zijn dus invoer-uitvoerparameters.
92
@Helga.Naessens@hogent. be
Hoofdstuk 5. Functies en procedures
void verwissel (double double h = x; x =:' y;
~x,
double &y) {
y = h; }
Code 5.25: Procedure verwisselt de inhöud van de parameters
5.2.5
Tabellen als procedureparameters
In een voorgaande paragraaf hebben we reeds besproken hoe tabellen kunnen gebruikt worden als functieparameters. Op een analoge manier kan een tabel ook dienst doen als invoerparameter voor een procedure. Stel bijvoorbeeld dat we van een gegeven tabel tab die n reële getallen bevat de grootste en de kleinste waarde wensen te bepalen. Dit kan aan de hand van de procedure bepaal_maxmin die w~ergegeven is in Code 5.26. void bepaal_maxmin (const qouble tab [] , int n, double &max, double &min) { max
=
t [0];
min = t[O]; for (int i = 1 ; i < n ïf
Ct [i] > ~ax
=
i++)
max)
t [iJ;
else if (t[i] < min) min = t [i]; }
Code 5;26: Procedure berekent de grootste en kleinste waarde uit een tabel · Bij de voorgaande functies en procedures waren alle tabellen die doorgegeven werden op voorhand reeds ingevuld en werden ze bovendien ook niet gewijzigd. De inhoud van een tabel kan echter ook door dat deelprogramma worden ingevuld of veranderd, zodat een tabelparameter in C++ steeds zowel een invoer- als een uitvoerparameter is, zonder dat ervóór de tabelnaam een & staat. De procedure bepaal_product bijvoorbeeld berekent het produkt van twee veeltermen v1 en v2 waarvan de coëfficiënten in twee tabellen van reële getallen opgeslagen zijn (op dezelfde manier als bij de methode van Homer). De veeltermen v1 en v2 bezitten respectievelijk de graden grv1 en grv2. De procedure plaatst de graad van het resulterende produkt in .grv3 en de coëfficiënten in de tabel v3. De procedure is weergegeven in Code 5.27.
93
Hoofdstuk 5. Functies en procedures
void
@HeJga.Naessens@hogent. be
prod~veelt
(const double v1 [] , int grv1, const double v2 [] , int grv2, double v3[], int &grv3) { grv3 = grv1 + grv2; for (int i = .o ; i <= grv3 i++) v3[i] = 0.0; for (int i = 0 ; i <= grv1 , i++) for (int j = 0 ; j <= grv2 ; j++) v3[i + j] += v1[i] * v2 [j] ;
}
Code 5.27: Berekent het produkt van twee veeltermen :Het gebruik van het sleutelwoord const zorgt ervoor dat er een beter onderscheid is tussen tabelparameters die dienst doen als invoerparameter of als (invoer-)uitv.o erparameter. Als je dit sleutelwoord immers plaatst bij een tabelparameter, geef je aan dat hethier over een invoerparameter gaat en dat de functie/procedure niets kan veranderen aan de elementen uit de tabel. Indien . het sleutelwoord const niet vermeld wordt bij de tabel parameter, kunnen de elementen uit de tabel wel gewijzigd worden vanuit de procedure, en kan de tabelparameter dus dienst doen als (invoer-)uitvoerparameter. Bij de procedure bepaal_product zijn de tabellen v1 en v2 invoerparameters, terwijl de tabel v3 een uitvoerparameter is. Door het sleutelwoord const te schrijven bij. de tabelparameters v1 en v2, zal de compiler ook steeds controleren of er nooit een uitdrukking van de vorm v1 [ ... ] of v2 [ ... ] aan de linkerkant van een toewijzing komt t~ staan, of als argument gebruikt wordt in een deelprogramma dat daar een uitvoerparameter verwacht 4 • In de volgende procedure keerom, die weergegeven is in Code 5.28, worden de eerste n getallen van een tabel t omgekeerd. Hierbij wordt de tabel dus gebruikt als invoeruitvoerparameter: de inhoud van de tabelis reedsbij het begin van de procedure geïnitialiseerd is en wordt gewijzigd in de loop van de procedure. void keerom (int t[], int n) { for (int i = 0 ; i < n I 2 ; i++) { int h = t[i]; t[i] = t[n- i - 1]; t[n - i - 1] = h; } }
Code 5.28: Keert de eerste n elementen van een tabel t om Deze procedure kan eleganter door twee indices te gebruiken die naar elkaar toe bewegen, 4 We kunnen dit lijstje nog uitbreiden: opdrachten -zoals 'cin > > vl( ... ]' rljn bijvoorbeeld ook niet toegelaten. Kortom, de compiler probeert er zo goed mogelijk voor te zorgen dat er aan de tabellen vl en v2, niets gewijzigd wordt. De compiler is echter niet alwetend, dus zijn er altijd wel mogelijkheden om deze regel te omzeilen. Dit doe je dan op .eigen verantwoordelijkheid.
94
Hoofdstuk 5. Functies en procedures
@Helga.Naessens@hogent. be
zoals weergegeven in Code 5.29. void keerom (int t[], int n) { int i = 0, j = n - 1; while (i < j) { int h = t[i]; t [i] t[j]; t [j] = h;
i++; j--; } }
Code 5.29: Keert de eerste n elementen van een tabel t om
Als laatste voorbeeld geven we in Code 5~30 de beste methode om dè eerste n getallen van een tabel in stijgende (niet dalende) volgorde te zetten, voor kleinen. Voor groteren bestaan er veel snellere (maar meer ingewikkelde) methoden. void insertion_sort (double tab[], int n) { for (int i = 1 ; i < n i++) { double h = tab[i]; int j = i - 1; while (j >= 0 && h < tab[j]) { tab[j + 1] = tab[j]; j--; }
tab [j + 1] = h; } }
Code 5.30: De sorteermethode insertion sart De methode gaat ervan uit dat de eerste i getallen in de tabel reeds gerangschikt zijn (de r.est van de getallen staat nog in de oorspronkelijke volgorde). Dan wordt het getal bij index ·i op de juiste plaats tussen de vorige geschoven, door het telkens te vergelijken met opeenvolgende getallen die io nodig naar achter opgeschoven worden5 . Als dat gebeurd is, zijn nu de eerste i + 1 getallen gerangschikt, en kunnen we de hele operatie herhalen tot de volledige tabel in volgorde staat. Hoe beginnen we? De initiële veronderstelling is steeds vervuld voor het eerste getal in de tabel. Het tussenvoegen begint dus met het tweede getal. Een dubbele voorwaarde in de binnenste h(;)rhaling is vereist om niet buiten de tabel terecht te komen als een tussen te voegen getai kleiner is dan alle vorige. Ga eens na waarom er na deze while-lus geen if.:.test meer nodig is (ondanks die dubbele voorwaarde). 5
Vandaar de Engelse naam voor de methode, insertion sort.
95
Hoofdstuk 5. Functies en procedures
5.3
@Helga.Naessens@hogent. be
Recursie
5.3.1
Wat is recursie?
Eerder in deze cursus hebben we gezien hoe een funCtie een andere functie kan oproepen. We hebben echter niet behandeld wat er gebeurt wanneer een functie zichzelf oproept6 . Op het eerste gezicht lijkt dit ook vreemd. Hoe kun je nu dichter bij de oplossing van een probleem komen door binnen een functie dezelfde functie aan te roepen? Het lijkt alsof je op deze manier geen stap dichter bij de oplossing komt. Toch zul je zien dat je op deze manier op een elegante wijze aan een antwoord kan komen. Het verschijnsel dat een functie/procedure zichzelf oproept en op deze manier een oplossing verkrijgt, heet recursie.
5.3.2
Voorbeelden
5.3.2.1
Recursieve functie
Bekijken we even de volgende (wellicht minder gekende) wiskundige definitie om de facul-' teit van een positief geheel getal n te bepalen: n!
=1
als n =· 1
n! = (n - 1)! *n
als n
>1
Hieruit blijkt dat voorn> 1 de berekening van n!aan de hand van (n -1)! gebeurt. De faculteit wordt dus in termen van zichzelf gedefiniéerd, wat op recursie duidt. De voor de hand liggende implementatie van dit recursief algoritme ziet èr als volgt uit:
int faculteit (int n) { if (n == 1) return 1; el se return faculteit(n - 1)
*
n;
}
Als tweede voor beeld van een recursieve functie beschouwen .we de volgende .;recursieve definitie om de grootste gemene deler te bepalen van twee positieve gehele getallen a en b:.
6
a =/= 0
ggd(a, 0) =a
met
ggd(a, b) = ggd(b, a% b) .
met b > 0
Dit is ook de reden waarom een lokale variabele niet de functienaam mag hebben.
96
@Helga.Naessens@hogent. be
Hoofdstuk 5. Functies en procedures
De implementatie van dit algoritme is weergegeven in Code 5.31 ~ int ggdrecursief (int a, int b) { if {b == Ö) return a; el se return ggdrecursief (b, a% b); }
Code 5.31: Recursieve functie ggdrecursief
5.3.2.2
Recursieve procedure
Onderstel dat we een programma moeten schrijven waarin de gebruiker een onbepaald aantal. gehele getallen kan ingeven. Het inlezen stopt indien de gebruiker het getal 0 heeft ingegeven. Vervolgens moet het programma alle ingelezen getallen in omgekeerde volgorde afdrukken. Dit programma kan vrij eenvoudig geschreven worden door gebruik te ·m&ken van een container, zoals een vector of een stapel. We zullen nu· laten zien dat het programma ook kan geschreven worden zonder een container te gebruîken. De oplossing hiervoor is weergegeven in Code 5.32. In deze oplossing werd een recursieve procedure keerom geschreven, met als effect: de overblijvend~ getallen van de reeks worden ingelezen en in omgekeerde volgorde uitgeschreven. void keeromO { int getal; cin » getal; i f (getal == -O) cout << "De omgekeerde reeks is: " << endl; else { keerom(); cout << getal << endl; } }
int mainO { cout <<"Geef een reeks gehele getallen in (eiride « endl; 'keerom(); }
Code 5.32: Een recursieve procedure
97
= 0): "
Hoofdstuk 5. Functies en procedures
5.3.3
@Helga.Naessens@hogent. be
Algemene structuur van een recursieve functie
Zoals je bij de voorgaande voorbeelden gezien hebt, is de basisstructuur van een recursief algoritme niets anders dan een if-structuur. Een goed recursief algoritme heeft een stopvoorwaarde en controleert eerst of het basisresultaat gegeven moet worden. Als er geen basisresultaat gegeven moet worden, voert het een bewerking uit met de oplossing van een versimpelde versie van hetzelfde probleem. Een recursieve oplossing van een probleem bevat dus steeds: • Een of meer basisgevallen: dit zijn eenvoudige gevallen waarvoor een oplossing geformuleerd wordt en waarin geen recursieve oproep voorkomt. • Een of meer recursieve gevallen: dit zijn gevallen waarvan de oplossing wordt herleid tot een eenvoudigere versie van hetzelfde probleem en waarin een of meer recursieve· oproepen voorkomen.
5.3.4
De voor- en nadelen van recursie
De grote voordelen van recursieve algoritmen zijn de relatief korte lengte en de eenvoud van het algoritme. Voor bepaalde problemen is de lengte van een recursief algoritme veel geringer dan wanneer je het op een andere manier zou schrijven. Vergelijkmaar eens de lengte van de functie ggdrecursief uit Code 5.31 met de functie ggd uit Code 5.5. Er zijn echter ook enkele nadelen aan recursieve algoritmen verbonden. Ten eerste wordt er meestal meer beslag gelegd op het geheugen van de computer. Doordat in elke functie/procedure weer dezelfde functie/procedure wordt aangeroepen, kan, bij een groot probleem, het aantal aanroepen fors toenemen en elke aanroep kost geheugen. Het terugkeren in de recursie kan ook pas beginnen als de bodelll is bereikt, dat betekent dat de resultaten van alle voorgaande stappen bewaard moeten blijven. Dit zorgt voor meer overhead bij toename van het aantal recursieve stappen. Bij algoritmen met een lusconstructie .daarentegen worden helemaal geen nieuwe aanroepen gedaan en wordt er dus meestal minder aanspraak op het geheugen gedaan. Ten tweeçle is het, ondanks de eenvoud van het recursief algoritme, vaak moeilijk om in te zien wat er precies gebeurt. De kans op een fout antwoord neemt hierdoor toe. Bijna alle recursieve algoritmen kunnen omgeschreven worden in een lusstructuur. Het algoritme is dan vaak overzichtelijker (het is meteen duidelijk wat er gebeurt) en er kun. nen minder snel problemen met het (beperkte) beschikbare geheugen van de computer ontstaàn. Daarom wordt in veel gevallen toch de voorkeur gegeven aan een lusstructuur. Er bestaan echter ook problemen waarvoor dit verhaal niet opgaat.
98
Hoofdstuk 6 Bestanden Elk computersysteem laat toe om groepen gegevens op te slàan in min of meer permanente vorm, bijvoorbeeld op harde schijf, op CD-ROM, op memory stick, of op een andere computer via een computernetwerk. Gegevens worden door het besturingssysteem opgeslagen in de vorm van bestanden. • In dit hoofdstuk bespreken we een aantal mogelijkheden die C++ biedt om met bestanden te werken. In de plaats van de resultaten van berekeningen op het scherm te tonen, kan je immers een programmaschrijven dat deze resultaten rechtstrreks in een bestand opslaat. In de plaats van lijsten met getallen in te tikken, kan je ook gewoon een bestand gebruiken dat al deze getallen bevat. De uitvoer van het ene programma kan, via een bestand, gebruikt worden als invoer van een ander programma. ·
6.1
Algemeen
Soms maakt men onderscheid tussen twee soorten bestanden: tekstbestanden en binaire bestanden. Wat C++ betreft, bestaat dit onderscheid niet. Er wordt echter wel een onderscheid gemaakt tussen verschillende manieren. waarop je een bestand behandelt: een bestand kan binair worden bewerkt, of als tekst. Beide methodes worden doorgaans niet tegelijkertijd gebruikt op hetzelfde bestand. In deze cursus zullen we enkel die opdrachten gebruiken waarin een bestand als tekst wordt behandeld. Om gegevens uit een bestand op te halen, gebruiken we leesopdrachten die goed lijken op de gewone opdrachten die dienen om gegevens te lezen van het toetsenbord. Om gegevens in een bestand op te slaan, gebruiken we schrijfopdrachten op nagenoeg dezelfde manier als bij het schrijven op het scherm. Deze vorm vanbestandsverwerking is sequentieel: een bestand wordt steeds van voor naar achter ingelezen of uitgeschreven. Het twintigste getal uit een bestand met vierhonderd getallen kan je niet ophalen als je niet eerst de negentien getallen inleest die vóór dat getal in het bestand komen. Als je reeds twintig getallen op een bestand hebt geplaatst, kan je
99
Hoofdstuk 6. Bestanden
©Helga.Naessens@bogent. be
niet zomaar terugkeren om het eerste getal te verbeteren. Voor elk bestand dat je in een programma wenst te gebruiken, moet je een variabele declareren. Het type van die variabele hangt af van waar je het bestand voor wil gebruiken: een bestand waarvan je wil lezen, heeft een geassocieerde variabele van het type ifstream (input file stream), een bestand waarop je zal schrijven heeft als typ·e ofstream (output file stream). Dit betekent dat je op voorhand een keuze moet maken of je het bestand zult inlezen of beschrijven. De naam van die variabele mag je vrij kiezen en hoeft niet onmiddellijk verband te houden met de naam die door het bedrijfssysteem aan het' bestand wordt toegekend. De meeste gangbare bestandsnamen, zoals bijvoorbeeld 'gegevens .dat' zijn trouwens toch geen geldige n~men van variabelen in C++. Onderstaand fragment declareert twee bestanden: één bestand dat als invoer en één dat we zullen gebruiken als uitvoer:
w~
zullen gebruiken
#include using namespace std; ifstream invoer; ofstream uitvoer;
Let op de #include-opdracht die nodig is om met bestanden te kunnen werken. De naam stream is de Engelse term die in C++ wordt gebruikt om dingen aan te duiden die in principe sequentieel worden behandeld. Bestanden zijn voorbeelden van streams, maar ook cin en cout zijn streams. Het zal je dus niet verbazen als blijkt dat de operatoren « en » op dezelfde manier met bestanden worden gebruikt als in de gewone lees- en schrijfopdrachten. Om het reëel getal x in te lezen van het bestand met variabele invoer schrijven we gewoon: invoer >> x; en om ·de tekst . 'Hallo iedereen! ' op het bestand uitvoer te plaatsen, schrijven we uitvoer<<
11
Hallo iedereen! 11 << endl;
Merk op dat een bestand ook uit verschillende lijnen kan bestaan, en dat endl dus ook hier zin heeft. Vooraleer we de eerste lees- of schrijfopdracht op èen bestand kunnen uitvoeren, moeten we dit bestand openen (cin en cout worden automatisch geopend in het begin van elk programma). Hiervoor gebruiken we een opdracht die er als volgt uit ziet:
100
Hoofdstuk 6. Bestanden
@Helga.Naessens@hogent. be
invoer.open("gegevens.dat"); uitvoer.open("resul:t.dat"); Tussen de haakjes staat de naam van dit bestand {dit is de naam die door het bedrijfssysteem wordt gebruikt) tussen dubbele aanhalingstekens. Het openen van een bestand legt dus het verband tussen de bestandsvariabele en de naam van dat bestand. Afhankelijk van het bedrijfssysteem, kan zo'n naam ook aangeven waar de computer dit bestand·precies moet vinden. Een bestand 'data. doe' dat zich in subdirectory 'doe' op je floppy bevindt, open je bijvoorbeeld op de volgende manier: invoer.open("A:\\doc\\data.doc"); Merk op er hier vier schuine strepen staan in plaats van twee. Waarom dit zo is, hebben we in Hoofdstuk 2 reeds uitgelegd. De procedure open die hierboven gebruikt werd om de bestanden te openen, verwacht als parameter een C-string. Een letterlijke string tussen dubbele aanhalingstekens is eigenlijk een C-string en kan dus, zoals we hierboven gezien hebben, meegegeven worden als parameter. Variabelen van het type string zijn echter geen C-strings, maar wel standaardstrings (uit de nieuwere C++ en niet uit de oude C). Dergelijke variabelen kunnen dus niet zomaar meegegeven worden als parameter.· Gelukkig voorziet C++ wel demogelijkheid .om van een variabele van het type string de corresponderende C-string op te vragen via de functie . c_strO. In het onderstaande programmafragment bijvoorbeeld, wordt aan de gebruiker de naam van het te openen bestand gevraagd en wordt vervolgens met behulp van de functie . c_str () op de ingelezen standaardstring het bestand geopend: string b~tandsnaam; cout << "Geef de naam van het invoerbestand in: " ; cin >> bestandsnaam; ifstream inv; inv.open(bestandsnaam.c_str()); Een bestand kan ook geopend worden op het moment dat het gedeclareerd wordt. Je noteert dit dan als volgt: ifstream invoer("gegevens.dat"); ofstream uitvoer("result.dat"); ifstream inv(bestandsnaam. c_strO); Tijdens het openen worden er door het bedrijfssysteem ook nog een aantal extra taken uitgevoerd: een bestand dat we openen voor uitvoer wordt eerst gewist, en van een invoerbestand wordt gecontroleerd of het wel bestaat. Met behulp van de volgende test kunnen we bijvoorbeeld nagaan of het bestand gegevens. dat (dat gelinkt is. aan de bestandsvariabele invoer) al dan niet correct kon geopend worden:
101
Hoofdstuk 6. Bestanden
@Helga:Naessens@hogent. be
i f (!invoer .is_open())
cout << "Het bestand kan niet geopend worden" << endl; else { //Lees de gegevens uit het bestand }
Wanneer je met een bestand klaar bent, moet je het sluiten. Dit gebeurt als volgt: invoer. close() ; uitvoer.close(); Het is vrij eenvoudig om zelf voorbeeldprogramma's te construeren die gebruik maken van bestanden: neem gelijk welk programma dat je vroeger hebt geschreven en laat de invoer nu van een bestand komen in plaats van van het toetsenbord, of laat de uitvoer naar een bestand schrijven in plaats van op het scherm. Je hoeft alleen maar overal de streams cin eii cout door een gepaste bestandsvariabele te vervangen, in het begin van je programma die nieuwe variabelen te declareren en de bestanden te openen, en op het einde de bestanden opnieuw te sluiten.
6.2
Testen op het einde van een bestand
Het programma Code 6.1leest 10 reële getallen in van een bestand en schrijft hun som op het scherm (het bestand heet gegevens. dat). Een dergelijk programma kan je heel gemakkelijk testen. Je gebruikt dezelfde editor die je ook gebruikt om programma's in te tikken, je tikt daarmee 10 getallen in (één of meerdere per lijn) en je bewaart dit onder de naam gegevens. dat. Daarna voer je het programma uit .. Veronderstel nu echter dat je op voorhand niet weet hoeveel getallen het in te lezen bestand bevat. Als we een onbekend aantal getallen inlezen van het toetsenbord, moeten we iets voorzien om aan te geven dat deze reeks ten einde is. Meestal spreken we dan af dat de reeks eindigt met een speciale getalwaarde, bijvoorbeeld nul. Zonder een dergelijke afspraak blijft het programma eeuwig om nieuwe invoer vragen. Een invoerbestand daarentegen gedraagt zich anders: een bestand kan op het moment zelf niet meer aangevuld worden op vraag van het programma. Wanneer we in het programma meer leesinstructies proberen uit te voeren dan er getallen in het invoerbestand staan, krijgen we problemen.
102
@Helga. Naesse~@bogent. be
Hoofdstuk 6. Bestanden
#include using namespace std; int main() { double .som = 0.0, getal; ifstream invoer; invoer.open( 11 gegevens.dat 11 ) ; if (!invoer.is_open()) cout << 11 Het bestand kan niet geopend worden 11 << endl; else { for (int i = 0 ; i < 10 ; i++) { invoer >> getal; som += getal; }
cout << 11 Som = 11 << som << endl; invoer. close() ; }
return 0; }
Code6.1: Berekent de som van 10 getallen uit een bestand Gelukkig bestaat er een uitdrukking die ons toelaat om na te gaan of we het einde van een bestand bereikt hebben: de logische uitdrukking 'invoer. eof ()'is true als de vorige leesopdracht vanuit het bestand invoer tot bij het einde van het bestand is gekomen. Hierbij is eof een afkorting van 'end of file', wat. 'einde van het bestand' betekent. Let opnieuw op de object-georiënteerde notatie: invoer is een object van de klasse ifstream en voor dit object roepen we de functie eof 0 op om na te gaan of we het einde van het bestand bereikt hebben. Het inlezen van een bestand kan echter nog om andere redenen stopgezet worden. Er kunnen zich immers foutieve gegevens in het bestand bevinden. Veronderstel eens dat het programma gehele getallen verwacht , maar dat er ergens in ·het bestand bijvoorbeeld de tekst "a3"staat. De manier om dit na te gaan is de waarde te testen van de logische uitdrukking 'invoer. fail () '. Deze uitdrukking is true als de vorige leesopdracht om één of andere reden mislukt ~s. De uitdrukking 'invoer. failO' is ook true indien we in het bestand nog iets wensen te lezen, terwijl we het einde van het bestand reeds bereikt hadden. Dit betekent dat we gegevens uit een bestand zullen moeten inlezen tot 'invoer.failO' true is, Als dan bovendien 'invoer. eof ()' true is, dan weten we dat het volledige bestand met succes ingelezen werd. Zoniet bevatte het bestand foutieve gegevens.
103
Hoofdstuk 6. Bestanden
@HeJga.Naessens@hogent. be
Samengevat:
Een· bestand volledig lezen ifstream naam;
naam. open (bestandsnaam) ; if (!naam. is_open())
FOUT: bestand kan niet geopend worden else {
naam >> while (!naam. fail ()) {
naam >> }
if (naam.eof())
OK: bestand kon volledig gelezen worden el se
FOUT: bestand bevat foutieve gegevens naam. close(); }
We passen het programma Code 6.. 1 nu zodanig aan dat het de som van alle getallen in het bestand bepaalt, zonder dat we op voorhand moeten weten hoeveel getallen het bestand (minstens) bevat. Het resultaat is terug te vinden in Code 6.2. Tenslotte wensen we nog op te merken dat ook voor (invoer)bestanden het gebruik van de functie get toegelaten is om een karakter (al danniet een white space) in te lezen. Een voorbeeld hiervan is weergegeven in programma Code 11.7 op het einde van de cursus. Ook de procedure getline die we gebruiken om een volledige lijn in te lezen, is toepasbaar op een bestand. Beschouw als voorbeeld de procedure kopieer _bestand uit Code 6.3 dat van een tekstbestand een letterlijke kopie maakt. Alle tekst die in het bestand staat, moet dus gekopieerd worden, inClusief white spaces. De parameter naamBron (van het type string) bevat de naam van het te kopiëren bestand, terwijl de parameter naamDoel (eveneens van het type string) de naam bevat van het bestand naar waar gekopieerd moet worden.
104
Hoofdstuk 6. Bestanden
@Helga.Naessens@hogent. be
#include using namespace std; int mainO { double som = 0.0, getal; ifstream invoer; invoer.open("gegevens.dat"); if (!invoer.is_open()) cout << "Het bestand kan niet geopend worden" << endl; else { invoer >> getal; while (!invoer.fail()) { som += getal; invoer >> getal; }
if (invoer . eof()) cout << "De som is " << som << endl; el se cout << "Het bestand bevat foutieve ·gegevens" << endl; invoer. close() ; }
return 0; }
Code 6.2: Berekent de
so~ . van
alle getallen uit een bestand
void kopieer_bestand(const string &naamBron, const string &naamDoel) { ifstream inv(naàmB~on.c_str()); if (!inv.is_open()) cout << "Fout: het bestand kan niet geopend worden!"; else { ofstream uitv(naamDoel.c_str()); string s; getline(inv,s); while (!inv.failO) { uitv << s << endl; getline(inv,s); }
cout <<"Het bestand is gekopieerd ." ; inv. close() ; uitv.close(); } }
Code 6.3: Maakt een letterlijke kopie van een bestand
105
Hoofdstuk 7 Excepties 7.1
Inleiding
Ook in correcte programma's kan er ih uitzonderlijke. omstandigheden iets misgaan: een bestand kan niet geopend worden, het netwerk valt uit, ... Met als gevolg dat het progi:arrima foutieve resultaten aflevert (die niet noodzakelijk meteen opvallen) of zonder meer beëindigd wordt. In een professionele omgeving is dit onaanvaardbaar: dergelijke uitzonderingen moeten op een gepaste manier behandeld worden. Als de programmeur de plaatsèn kent waar uitzonderingen eventueel kunnen optreden, kan hij daar code voorzien om ze te detecteren en te behandelen. Vroeger greep men gemakkelijk naar. het laatste redmiddel: exit ( 0) ; (deze opdracht beëindigt het programma). Tegenwoordig . bestaat hier een eleganter en krachtiger mechanisme voor: exception handling. Stel je, om het wat concreter te maken, een functie vkw voor die de vierkantswortel van zijn argument berekent: double vkw (double x) { double resultaat; ... 11 bereken resultaat met numerieke methode return resultaat; }
Maar wat als x negatief is? De vierkantswortel is dan een imaginair getal, en die kun je niet ·in een double gieten. Een mogelijke oplossing bestaat erin om een èpeciale waarde terug te geven die aangeeft dat er een fout opgetreden is, bijvoorbeeld -1:
106
Hoofdstuk 7. Excepties
@Helga.Naessens@hogent. be
double vkw (double x) { double resultaat; if (x < 0) resultaat = - 1; el se ... /l bereken resultaat met numerieke methode return resultaat; }
De oproeper van de functie kan dan aan het resultaat zien of de functie-oproep geslaagd is. Er zijn echter een paar nadelen aan deze aanpak: 1. Als de gebruiker vergeet het resultaat te controleren, dan loopt het programma verder. met de speciale waarde (hier -1), mogelijksmet desastreuze gevolgen. 2. Het is niet altijd mogelijk een speeiale returnwaarde te vinden. Een functie tan (x) bijvoorbeeld die de tangens van x berekent komt in de problemen voor waarden van x waarvoor cos (x) == 0. De waarde -1 teruggeven is geen goede optie, want - 1 is de tangens van -45 graden. Sterker nog, elke mogelijke double is de tangens van een hoek, zodat er geen speciale waarde overblijft. Merk op dat in dit eenvoudig voorbeeld de gebruiker natuurlijk gemakkelijk kan controleren of x positief is alvorens de functie vkw op te roepen. In het algemeen is dit echter niet altijd mogelijk. Een alternatieve (ouderwetse) oplossing bestaat erin om een bepaalde globale variabele te gebruiken om fouten aan te duiden. In oudere bibliotheken ziet men bijvoorbeeld vaak dat de variabele errno (error-number) op een niet-nul waarde. gezet wordt om een fout aan te duidei:L . Dit lost het tweede probleem op, maar niet het eerste, want de verantwoordelijkheid om errno te controleren ligt nog steeds bij de gebruiker van de functie. Bovendien zijn globale variabele.n absoluut af te raden.
7.2 . Exception handling In moderne programmeertalen wordt dit probleem opgelost d.m.v. exception handling. Een functie die in de problemen komt, werpt een exceptie op (Eng: throws an exception). Die exceptie kan opgevangen worden (Eng: catch) door de oproeper van de functie. Indien. dit niet gebeurt dan wordt het programma automatisch beëindigd. In C-t-+ gebeurt het opwerpen van een exceptie met een throw-opdracht. In vele programmeertalen kan men enkel speciale exceptie-objecten opwerpen en is de opgeworpen exceptie een object dat informatie bevat over de oorzaak van de uitzondering. In C++ kan menom het even wat opwerpen: een getal (foutcode), een string (foutboodschap), een object, .... De functievkwkan bijvoorbeeld als volgt een exceptie als string opwerpen:
107
Hoofdstuk 7. . Excepties
@Helga.Naessens@hogent. be
double vkw · (double x) { if (x < 0) throw "exceptie in vkw(): argument is negatief " ; double resultaat; ... 11 bereken resultaat met numerieke methode return resultaat; }
Indien je in het hoofdprogramma geen speciale maatregelen neemt om deze exceptie op te vangen, dan wordt bij het optreden van de exceptie het programma automatisch afgebroken, zonder foutmelding! Bijvoorbeeld: int mainO { double x; cin >> x; cout << vkw(x) << endl; cout << "einde" << endl; }
Indien x negatief is, wordt "einde" niet uitgeschreven (en komt er geen foutboodschap op het scherm). · Om de exceptie op te vangen, moet men de functie-oproep in een try-catch blok zetten: int mainO { double x; cin >> x; try { cout << vkw(x) << endl; }
catch ( ... ) { cout << "Oops. Er is een exceptie opgetreden" << endl; }
cout << "einde" << endl; }
Het try-blok is een soort beschermde zone: alle excepties die geworpen worden binnen dit blok (mogelijks door opgeroepen functies) worden opgevangen door de catch-clausule. Het resultaat is hier dat de vierkantswortel uitgeschreven wordt indien x positief is, terwijl er een foutboodschap "Oops. Er is een exceptie opgetreden" getoond wordt indien x negatief is.
108
Hoofdstuk 7. Excepties
©Helga.Naessens@hogent. be
In het try-blok kunnen ook meerdere opdrachten (statements) gezet worden. Dit wordt gei1lustreerd in Code 7.1. int mainO { try { double x; .cin >> x; cout << "vierkantswortel : " << endl; 1 double y = vkw(x); 2 cout << y << endl; 3 cout << "controle: " << y * y << " is " << x << " ? " << endl ; }
catch ( . .. ) { cout << "Dops. Er i s een exceptie opgetreden" << endl;
4
}
cout << "einde" << endl;
5 }
Code 7.1: Meerdere opdrachten in het try-blok Belangrijk opmerking: zodra een opdracht in het try-blok een exceptie opwerpt, wordt er naar het catch-blok gesprongen. Indien x negatief is, wordt er in lijn 1 een exceptie geworpen. In dat geval worden lijn 2 en 3 niet uitgevoerd, maar wel lijn 4. Ongeacht de waarde van x wordt uiteindelijk lijn 5 uitgevoerd, omdat die buiten het try-catch blok staat. Er valt ook op te merken dat het onmogelijk is om terug te springen naar de plaats van het euvel, bijvoorbeeld om verder te gaan met lijn 2. In die zin kun je throw vergelijken met uit het raam springen ... Hopen dat iemand je opvangt! catch( ... ) betekent "vang alle excepties op". Men kan er ook voor kiezen om slechts bepaalde soorten excepties op te vangei:i. In het bovenstaande voorbeeld wordt er een letterlijke string "exceptie in vkw().~." geworpen. Letterlijke strings zijn in C++ van het type const char* (of ook const char [] ) , niet chàr* of string. Om een dergelijke exceptie op te vang(m schrijf je catch(const char *s) { cout << "foutboodschap: " << s << endl; }
De opgeworpen foutboodschap wordt hier opgevangen in s en kan dan ook op het scherm getoond worden. Merk op dat dit niet noodzakelijk is- men kan ook andere acties on~ dernemen in het catch-blok. Soms wordt er zelfs niets gedaan, doch deze praktijk is af te raden. Indien je zeker weet dat de geworpen excepties letterlijke strings zijn, is catch(const char *s) te verkiezen boven catch(. .. ) omdat de string s informatie bevat over de oorzaak van de fout. Dit is vooral interessant als er meerdere excepties kunnen ontstaan in het 109
Hoofdstuk 7. Excepties
@Helga.Naessens@hogent. be
try-blok, die een verschillend type opwerpen. Merk op dat catch(const char *s) alle (letterlijke) string-excepties opvangt, maar niet andere soorten. Als er in het try-blok bijvoorbeeld throw 12 staat, .dàn wordt deze intexceptie niet ·opgevangen. In dit geval wordt het programma alsnog afgebroken, zonder dat "einde" uitgeschreven wordt: Je kunt eventueel ria de catch(const char* s) eeil. catch( ... ) zetten die alle overige excepties opvangt. Let op: Als een try-blok vergezeld gaat van meerdere catch-blokken, worden die in volgorde overlopen om de gepaste foutafhandeling voor een opgeworpen exceptie te vinden. Het spreekt vanzelf dat een eventuele catch( ... )-opdracht dan laatst moet staan, zoniet :worden de volgende catch-blokken hooit gebruikt. De .wa,re kracht van exception handling wordt pas duidelijk wanneer je geneste functies bekijkt (i.e. een functie roept een andere functie op). Een voorbeeld: ·void behandel (double x) { cout << "de vierkantswortel van " << x << " is " << vkw(x) << endl; }
int main() { double getal; cin >> getal; behandel(getal); }
Als het ingegeven getál negatief is, dan werpt de functie vkw, die indirect door behandel opgeroepen wordt, een exceptie op. Aangezien deze nerge'ns opgevangen wordt, breekt het programma af. Om dit tegen te gaan moet men de exceptie opvangen. Het fijne is nu dát je kunt kiezen of je dat in behandel of in het hoofdprogramma doet. Bijvoorbeeld: void behandel (double x) { try{ cout << "de vierkantswortel van 11 << x << " is " << vkw(x) << endl; }
catch (const char *s) { cout << "probleem: " << s<< endl; } }
In dit geval wordt de exceptie die door vkw geworpen werd, opgevangen door behandel.
110
Hoofdstuk 7. Excepties
'
@Helga.Naesseris@hogent. be
Maar je kunt de try-catch ook in main zetten: int mainO { try { double getal; cin » getal; behandel(getal); }
catch (const char *s) { cout << 11 probleel'n : 11 << s << endl; } }
De exceptie die doOr vkw geworpen werd, wordt door behandel gewoon doorgegeven. In dit geval wordt hij in main opgevangen, zodat het netto-resultaat hetzelfde is. Dankzij exception handling is het mogelijk om de oorzaak van een exceptie los te koppelen van de remedie. De functievkwprobeert zelf geen "slimme" oplossing voor het probleem te bedenken, maar gooit gewoon een exceptie, zonder te weten waar die exceptie opgevangen zal worden en wat er dan mee gebeurt. 1\La.w. vkwneeint zelf geen verantwoordelijkheid voor de remediëring van de exceptie. De verantwoordelijkheid wordt genomen door (een ván) de oproepende functie(s) d.m.v. een try-catch blok. In het voorbeeld kan behandel kiezen of hij zelf de verantwoordelijkheid op zich neemt of niet. De regel is dat je een exceptie moet opvangen als je die kunt behandelen. Uiteindelijk kom je altijd in main terecht, en het is een goede gewoonte .om daar alle excepties op te vangen en te rapporteren, als een ultiem vangnet. Het valt op te merken dat exception hándling niet bestond in C, de voorl9per va~ C++. Dit verklaart het feit dat slechts heel weinig functies en klassemethodes uit standaardbibliotheken excepties opwerpen. Ook low-level excepties zoals deling door nul en int-overflow resulteren niet in een exceptie! Deling door nul kun je niet opvangen - op voorhand controleren is dus de boodschap.
111
Hoofdstuk 8 Objectgeorienteerd programmeren In dit hoofdstuk wordt afgestapt van het proeedmaal ontwikkelen van programma's en gaat de aandacht naar het aanleren van principes en concepten van objectgericht programmeren. Hierbij bekijken we een programma niet meer als een lijst met instructies die de computer vertelt wat hij moet doen, en ook niet als een stel kleine programma's die reageren op een specifieke gebeurtenis die door gebruikersinvoer in werking wordt gesteld. In de objectgeoriënteerde benadering van een prograinma wordt dit bekeken als een verzameling objecten die op een vooraf gedefinieerde manier s~menwerken om taken te voltooien.
8.1 8.1.1
Basisbegrippen Objecten
Wat is een object? Een object vertegenwoordigt een al dan niet b~staande entiteit. Alles wat we kunnen bedenken, zien, aanraken of meten is op de een of and~re manier een object. Ons besef van objecten in de werkelijkheid hangt samen met ons begrip van het concept hoeveelheid. Objecten zijn echte dingen met duidelijke contouren. Elk object is uniek. Objecten kunnen we van elkaar onderscheiden omdat ze elk hun eigen kenmerken hebben. Het benoemen van objecten is hetzelfde als dingen zoeken. Objecten komen vaak overeen met zelfstandige naamwoorden. Voorheelden zijn: personeelsafdeling, computermagazine, lokaal 119. Omwille van hun eigenschappen zijn ze duidelijk te onderscheiden van ·elkaar. Net als twee mensen zijn ook twee objecten nooit dezelfde.
112
Hoofdstuk 8. Objectgeorienteerd programmeren
8.1.2
©Helga.Naessens@hogent. be
Kenmerken van objecten
De essentiële karakteristieken van een object zijn: z'n identiteit, z'n toestand en z'n gedrag. De identiteit is de unieke code waarmee we het ene object van het andere kunnen onderscheiden. In softwaretermen kunnen we dit vergelijken met de naam van een variabele. De toestand van een object vertegenwoordigt een verzameling gegevens die .informatie over dat object bevat. Dit komt overeen met de geheugeninhoud van dat object. Zo zal de toestand van een tweedimensionaal punt met x-coördinaat=! en y-coördinaat=2 voorgesteld worden door het koppel numerieke waarden (1,_2). Van een auto kunnen we bijvoorbeeld snelheid en positie bijhouden. Het gedrag van een object komt overeen met de diensten diè het object levert aan zijn klanten. Het gedrag is vergelijkbaar met de manier waarop objecten reageren op gebeurtenissen van buitenaf en communiceren met andere objecten. Zo kan bij een auto bijvoorbeeld het draaien aan het stuur zijn positie veranderen, het duwen op het rempedaal zal zijn snelheid aanpassen. Gedrag is ook dat onze auto bijvoorbeeld zijn snelheid meedeelt op een eenvoudige vraag. Het gedrag is sterk afhankelijk van de context waarin het object zicht bevindt. Het gedrag van een punt zal in een CAD-applicatie sterk verschillen ten opzichte van een punt dat gebruikt wordt in een toepassing waar zijn geometrische eigenschàppen minder belangrijk zijn. In het eerste geval kan het gedrag van een punt bestaan uit volgende handelingen: • de afstand tussen zichzelf en een ander punt berekenen • het punt roteren • zijn oorspronkelijke cordinaten weergeven • het punt spiegelen Het gedrag van een object komtin feite overeen met een serie boodschappen die naar het object worden gezonden. Zo'n boodschap kan opgevat worden als een verzoek van een klant, aan wie het object zijn diensten aanbiedt. Zo kan een object dat een lijnstuk vertegenwoordigt, gebruik niaken van de diensten die punten kunnen aanbieden: om zijn lengte te kennen, kan de afstand tussen de twee eindpunten worden berekend. Verandering in de toestand van een object heeft geen invloed op zijn identiteit; deze is dus onafhankelijk van zijn geheugenwaarden. Er kunnen dus best twee verschillende punten bestaan die toevallig dezelfde coördinaten hebben.
113
Hoofdstuk 8. Objectgeorienteerd programmeren
8.1.3
@Helga.Naessens@hogent. be
Klassen
Objecten met een zelfde gedrag kunnen gegroèpeerd worden. Alle tweedimensionale punten reageren op dezelfde manier op gebeurtenissen en berichten van l:>uiten. In plaats van te proberen alle mogelijke puriten tegelijkertijd te bevatten (wat natuurlijk helemaal niet mogelijk is), wordt er een concept gecreëerd waarin alle objecten (punten) kunnen opgenomen worden. Dit concept wordt een klasse genoemd. Een klasse is een abstract begrip. Een object daarentegen is concreet: het is een welbepaald voorbeeld van de klasse, een soort van incarnatie. In het Engels spreekt men. van een instanee van de klasse. Het object is de weergave van de abstracte klasse. Een klasse kan .bovendien ingezet worden in drie situaties: • Ze kan een type aanduiden van zowel een object van die klasse als van een referentie naar een dergelijk object. • Ze kan dienst doen als fabriek van objecten: door middel van · speciale methodes (constructoren, zie verder) kunnen er meerdere objecten worden aangemaakt van dezelfde klasse. • Ze kan dienst doen als aanbieder van diensten: in deze situatie gedraagt de klasse zich zelf als een object waaraan bepaalde services kunnen gevraagd worden.
8.1.4
Inkapseling en verbergen van informatie
Objecten hebben dus een identiteit, een toestand en een gedrag. De toes~and is meestal statisch, het gedrag dynamisch. De toestand van een object is niet direct toegankelijk voor een klant. Dit wordt inkapseling ( encapsulation) genoemd: de eigenlijke gegevens betreffende het object blijven verborgen voor de buitenwereld. Deze gegevens zijn wel bereikbaar op een onrechtstreekse manier via de diensten die het object aanbiedt. De diensten zelf hebben dan natuurlijk wel rechtstreekse toegang tot de objecttoestand. Deze inkapseling heeft als belangrijkste voordeel dat een object kan bekeken worden als een zwarte doos (black box). Hoe dit object zijn gegevens intern structureert, is onzichtbaar voor de klanten. Dit heeft als voordeel dat de implementatie van het object kan veranderd worden, zonder dat de klantobjecten daar ènige hinder van ondervinden.
8.1.5
Berichten en methodes
Het gedrag van een object komt overeen met de verzameling berichten die ernaartoe worden gezonden. Objecten wisselen immers gegevens uit met mekaar via berichten.
114
Hoofdstuk 8. Objectgeorienteerd programmeren
[email protected]@hogent. be
De methodes zijn stukjes programmacode in een of andere objectgeoriënteerde taal die deze berichten uitvoeren. Bij ontvangst van een bericht bepaalt de methode hoe aan het verzoek van de klant moet worden voldaan. De methodes hebben rechtstreeks toegang tot de toestand van het object. Ze kunnen ingedeeld worden in vier categorieën al naar gelang van de manier waarop ze de toestand van het object beïnvloeden. In C++ worden ze ook wel.lidfuncties genoemd. De vier categorieën zijn: 1. Constructoren: Dit is een methode die nieuwe objecten van de klasse creëert. In objectgerichte talen zet een constructor een deel onbewerkt geheugen om naar een gestructureerd object. Nieuwe objecten kunnen op verschillende manieren worden aangemaakt; een klasse kan dus meerdere constructoren bezitten. 2. Destructoren: Dit is een methode die een object uit het geheugen verwijdert. Bij sommige objectgeoriënteerde talen (Smalltalk en Java) moet deze methode niet toegepast worden, omdat het geheugen automatisch wordt opgekuist indien objecten niet meer worden gebruikt; dit gebeurt door een afvalverwerker (garbage collector). In andere talen zoals C++ ontbreekt deze faciliteit en moet de programmeur zelf bepalen wat er moet gebeuren indien een object wordt verwijderd. 3. Selectoren of inspectoren: Dit soort methodes verandert niets aan de objecten waarop ze worden toegepast. Ze voeren uitsluitend read-only manipulaties uit op het object en dienen meestal om een deel van de toestand van het object naar buiten zichtbaar te.maken. 4. Modifiers . of mutatoren: Zoals de naam doet vermoeden is een mutator een methode die de toestand vai1 het object verandert. Vermits het de klassebouwer is die deze methode schrijft, heeft hij de manier waarop het object wordt aangepast volledig in de hand; er kan bijvoorbeeld getest worden of bepaalde waarden aanvaardbaar zijn of niet. Een methode die onze auto doet afremmen kan hier als voorbeeld aangehaald worden.
8.2
Zelf klassen maken
8.2.1
Een eerste voorbeeld
In Code 8.1 is aan de hand van eenstructeen eerste klasse beschreven. Deze klasse houdt van studenten de naam, de klas en het aantal examentekorten als informatie bij. De variabelen st i en st2 zijn objecten van de klasse Student. De overeenkomst met de declaratie van variabelen is duidelijk. Het enige verschil tussen de declaraties 'int i;' en 'Student st 1; ' is dat van deze laatste het type zelf gespecificeerd is.
115
Hoofdstuk 8. Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
#include using namespace std; struct Student { string naam, klas; int aantal_tekorten; void print() { cout << naam << " zit in " << klas << " en heeft " << aantal_tekorten << " tekorten"; }
}; // Student int main() { Student st1, st2; cout << "Geef de gegevens van student1 in:" << endl; cin >> stl . naam >> stl.klas >> stl.aantal_tekorten; cout << "Geef de gegevens van student2 in:" << endl; cin >> st2.naam >> st2.klas >> st2.aantal_tekorten; st 1. print 0 ; st2. print () ; return 0; }
Code 8.1: Eenvoudig voorbeeld van een klasse en twee objecten Naastvariabelen (naam, klas en aantal_tekorten) bevat de klasse Student ook de procedure print. In het algemeen komen in de beschrijving van een klasse irnmers zowel variabelen als proceduresen/of functies voor, waarbij men dan eerder over lidfuncties spreekt ('vermits er in C++ eigenlijk geen onderscheid wordt gemaakt tussen procedures en functies). Deze laatste zijn evenwel niet echt nodig: elke eenvoudige struct zonder lidfuncties is in C++ in feite een klasse. De activering van deze lidfuncties gebeurt zoals bij de componenten van een struct. Voor de naam van de functie verm.eldt men de naam van het object waarvan de functie .een lidfunctie is: st 1. print() ; Deze laatste aanroep moet uitgelegd worden als pas de procedure print () toe op het object st1.
116
Hoofdstuk 8. Objectgeorienteerd programmeren
8.2.2
@Helga.Naessens@hogent. be
Private eri public: gegevens verbergen
Het verbergen van gegevens of data hiding is het camoufleren van bepaalde gegevens die opgeslagen zijn in de variabelen van een object. Op welke wijze dit kan in de declaratie van de klasse Student, is weergegeven in Code 8.2. #include using namespace std; struct· Student { private: string naam, klas; int aantal_tekorten; public: void print() ·{ cout << naam << " zit in " << klas << " en heeft " << aantal_tekorten << " tekorten"; };
Code 8.2: Gegevens verbergen in de klasse Student In deze definitie van de klasse Student geeft de specificatie private aan, dat de leden naam, klas en aantal_ tekorten niet van buitenaf bereikbaar zijn. Dit betekent dat in de programmadelen die gebruik maken van deze datastructuur, deze leden bijvoorbeeld niet kunnen ingevuld worden. De compiler zal dus een fout genereren indien we het hoofdprogramma onveranderd zouden laten met de nieuwe declaratie· van de klasse Student van hierboven. Deze private struct-leden zijn nog alleen bruikbaar door de lidfuncties van de klasse. Het onzichtbaar maken van bepaalde klassecomponenten heeft zekere voordelen. Je moet dit natuurlijk in een ruimer kader bekijken: een klasse wordt meestal opgezet door de ene programmeur, en het zijn meestal een of meerdere andere programmamakers die deze klasse zullen gebruiken in hun programma's. Een eerste voordeel is dat je er als programmeur zeker van bent dat het uitsluitend je zelf geschreven lidfuncties zijn die de datacomponenten hanteren. Invullen van ontoelaatbare waarden bijvoorbeeld houd je zo zelf in de hand. Indien hovendien het opzet van de klasse om een of andere reden moet worden aangepast, dient de code die van de klasse gebruik maakt, niet te worden herschreven . . Tot slot móet eengebruiker van de klasse zich alleen maar bekommeren om hoe de publieke lidfuncties moeten gebruikt worden. De specificatie public in bove~staande klasse moet voorkomen dat de lidfuncties die hierna vermeld zijn, ook niet meer toegankelijk zouden zijn; op geen enkele manier zouden objecten van die klasse anders nog kunnen gehanteerd worden.
117
Hoofdstuk 8. Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
Uit het bovenstaande blijkt dat we aan de dataleden van een object van de klasse Student dus niet zo maar een waarde kunnen geven, zoals. we in het vorige hoofdprogramma gedaan hebben. De enige manier om dit probleem op te lossen, is hier een aparte lidfunctie voor voorzien. Die kan er als volgt uitzien: void vul_op (string nm, string kl, int atk) { naam= nm; klas = kl; aantal_tekorten atk ; }
Het. voll~dige programma is weergegeven in Code 8.3. De aandachtige lezer heeft wellicht gemerkt dat er een kleine aanpassing in de declaratie van de klasse Student is gebeurd: het woordje struct is vervangen door het woordje class. In C++ iseen struct eigenlijk steeds een klasse. De benaming struct is bij de overgang van C naar C++ gehandhaafd. Waarom we dan niet meteen het begrip class hebben geïntroduceerd, heeft te maken met het verschil in toegankelijkheid van beide; Indien er niets over staat gespecificeerd, zijn alle leden van een struct public. Bij een class is dit net andersom: alle leden zijn standaard private. Dit betekent dat we in vorig programma zowel struct als class hadden kunnen noteren, omdat we expliciet de private en publieke leden hebben aangeduid. Voortaan noteren we steeds class voor een klassespecificatie. Er moet dan minstens één lid public zijn, meestal een lidfunctie die toegang geeft tot de private leden. Het woord private kan worden weggelaten, omdat standaard alle leden privaat zijn. Of de publieke leden eerst of laatst worden vermeld, hangt af van de gewoontes van de programmeur.
8.2.3
Lidfuncties definiëren buiten de klassespecificatie
Tot nu toe hebben we alle lidfuncties van de klasse Student binnen de specificatie van de klasse zelf volledig gedefinieerd. Normaal doet men dit uitsluitend voor zeer kleine functies. Dit heeft te maken met het feit dat alle functies en procedures die binnen een klassespecificatie staan uitgeschreven, inline worden gecompileerd. voert de computer nogal wat ~dministratieve Bij een normale functie- of procedureaanroep . . taken uit om na afloop van het deelprogramma op een correcte manier verder te kunnen werken aan het aanroepend programmadeeL Stel dat in programmadeel A de procedure B wordt aangeroepen: hierbij moeten wellicht enkele actuele parameters worden doorgegeven. Dit wordt verwezenlijkt door deze waarden (of hun adressen) op de stack te plaatsen. De stack is een stuk in het geheugen dat gebruikt wordt als tijdelijke opslagplaats die beheerd wordt als een stapel: de gegevens die er het eerst worden opgezet, worden er het laatst weer afgehaald. Omdat na afloop van procedure B, de computer nog moet weten met welke instructiê in programmadeel A hij bezig was en
118
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
#include using namespace std; class Student { private: string naam, klas; int aantal_tekorten; public: void print 0 { cout << naam << " zit in " << klas << " en heeft " << aantal_tekorten << "tekorten"; }
void vul_op (string run, strfng kl, int atk) { naam = run; klas = kl; aantal_tekorten atk; }
}; 11 Student int mainO { Student st1, st2; cout << "Geef de gegevens van student! in : " << endl; 11 de volgende opdracht lukt niet meer: I I · cin » st1.naam » st1.klas >> st1. aantal_ tekOrten; string run, kl; int atk; cin >> run >> kl >> atk; stL vul_op.(run, kl; atk); cout << "Geef de gegevens van student2 in:" << endl; cin.>> run>> kl >> atk; st2.vul_op(run, kl, atk); st1. print() ; st2.print(); return 0; }
Code 8.3: Klasse Student met extra lidfunctie vuLop
119
Hoofdstuk 8. Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
hoe de toestand van de gebruikte registers in de CPU was, wordt ook het terugkeeradres en de processortoestand op. de stack bewaard. Ook de aangeroepen procdure B maakt gebruik van deze stack: zij haalt er de waarden van de parameters af, en plaatst eventueel na afloop de teruggegeven waarde (indieri het een functie betreft) er weer op. Als het programmadeel zijn taak weer opneemt, moet de stack weer worden schoongemaakt. Al deze administratieve handelingen nemen uiteraard nogal wat tijd en geheugenruimte in beslag. Bij zeer kleine functies kost dit wellicht meer moeite dan wat de functie zelf moet presteren. Om dit te verhelpen bestaat het concept inline-functie. Bij een dergelijke functie voorziet de compiler niet in een echte functieaantoep zoals hierboven beschreven. In de plaats hiervan zal hij telkens de opdrachten zelfgenereren die nodig zijn om de functie haar werk te laten uitvoeren. Dit betekent dus wel, dat voor elke functieaanroep telkens alle opdrachten voor deze functie expliciet in ·de eigenlijke machinecode zijn geïntegreerd. Het is dus evident dat we dit moeten vermijden indien het een grote functie betreft; Een goede vuistregel is dat procedures en functies die slechts één of twee statements bevatten, wel als inline kunnen worden gecompileerd. Indien we dus de lidfuncties niet als inline willen gecompileerd zien, mogen we ze niet definiëren binnen in de klassedeclaratie. Vermits evenwel toch moet worden aangegeven dat het om leden van de klasse gaat, behouden we in de declaratie uitsluitend de hoofding van de-desbetreffende lidfunctie (prototype). Dit zorgt in het geval van de klasse Student voor volgende declaratie: class Student { private: string naam, klas; int aantal_tekorten; public: void print() ; void vul_op (string nm, string kl, int atk); } ;
Het volgend programmafragment volgt in het programmabestand op de declaratie van de klasse en komt vóór het hoofdprogramma. Stel dat we bijvoorbeeld de procedure print zouden definiëren zoals hieronder staat afgedrukt, dan zou de compiler vooreE;Jrst niet weten dat het gaat om de lidfunctie van de klasse Student, en bovendien zouden de datacomponenten naam)'klas en aantal_ tekorten onbekend zijn: void print() { //verkeerde declaratie !!! cout << naam << " zit in " << klas << " en heeft " << aantal_tekorten << " tekorten"; }
Dit probleem kan verholpen worden door gebruik te maken van de scope-operator
120
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
Hij staat tussen de naam van de lidfunctie en de naam van de klasse waar ze deel van uitmaakt: void Student :: print() Op deze manier geef je aan dat de procedure print deel uitmaakt van de klasse Student en niet een los gedefinieerde procedure is. Meteen zal de compiler ook geen problemen meer maken met de private dataleden, waar procedures die geen deel uitmaken van de klasse niet mee mogen werken. Indien we de beide lidfuncties print en vul_ op dus definiëren buiten de klassespecificatîe, ziet het volledige programma uit Code 8.3 er dus uit zoals in Code 8.4.
8.2.4
Afzonderlijke compilatie
Stel nu dat we de klasse Student willen gebruiken in meerdere hoofdprogramma's en/of toepassingen. We kunnen hiertoe in elk hoofdprogramma de tekst van de modules tekstueel invoegen ofwel via de editor, ofwel via een include-file. Het nadeel hiervan is dat de modules elke keer opnieuw moeten gecompileerd worden samen met het hoofdprogramma. Dit is tijdrovend, zeker als er meerdere en grote modules worden gebruikt. Beter en efficiënter is separate of afzonderlijke compilatie. Dit betekent dat alle modules die in het programma worden gebruikt, apart worden gecompileerd. De verschillende objectfiles worden· nadien aan elkaar gelinkt tot één uitvoerbare run-file. In Borland-C++ moet hiertoe een projectbestand worden gemaakt zodat men weet welke modules moeten worden gelinkt. Er kunnen nog andere redenen aangehaald worden om deze techniek toe te passen: bij-· voorbeeld bij het werken met meerdere personen aan hetzelfde project. Ook indien een softwàrebibliotheek wordt aangekocht zijn alleen de objectfiles beschikbaar, zeker niet de sourcefiles. Separate compilatie vereist de volgende werkwijze:
1. Reader-file Vooreerst moet een header-file worden gemaakt. Deze draagt de basisnaam van de module-soureefile (bv. student.cpp) met als extensie '.h'. In ons voorbeeld zou hij dus student.h heten. Een header-file bevat in principe alle declaraties die door de oproepende modules en toepassingen zichtbaar moeten zijn: types,· functies, procedures, (constanten, variabelen). Alleen de functie-hoofdingen zijn van belang. De body van de functies moeten niet zichtbaar zijn voor de buitenwereld. Concreet betekent dat voor ons
121
©Helga.Naessens@bogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
using namespace std; #includ~
class Student { private: string naam, klas; int a~tal_tekorten; public : void print 0; void vul_op (string run, string kl, int atk); }; // Student void Student : : print() { cout << naam << 11 zit in 11 << klas << 11 en heeft 11 << aantal_tekorten <<
11
tekorten 11
;
}
void Student : : vul_op (string run, string kl, int atk) { naam= run; klas kl; aantal_tekorten atk;
=
}
int mainO { Student st1, st2; cout << 11 Geef de gegevens van student1 in: 11 << endl; string run, kl; int atk; cin >> run >> kl >> atk; st1.vul_op(run, kl, atk); . cout << 11 Geef de gegevens van student2 in: 11 << endl; cin » run » kl >> atk; st2.vul~op(run, kl, atk); st1.print0; st2.print(); return 0; }
Code 8.4: Klasse Student met definitie van de lidfuncties buiten de klaSsespecificatie
122
Hoofdstuk 8. . Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
voorbeeld, dat alleen de declaratie van de klasse Student in de header-file moet worden opgenomen. 2. Module bronbestand Dit bestand bevat de body's van alle functies die we în de toepassing willen gebruiken. Het hoofdprogramma dat gediend heeft om de modules te testen, verdwijnt hieruit. De naam is student.cpp. Om alle vergissingen te vermijden bij het declareren van functies, is het een goede gewoonte om ook de header-file in dit bestand in te voegen (zie hierna) . 3. Bronbestand van de toepassing Dit bestand bevat het hoofdprogramma van onze toepassing waar alle functies en procedures van de module student worden gebruikt. De header-file moet hier uiteraard zeker worden ingevoegd: #include
11
Student .h 11
Dit is noodzakelijk opdat de compiler zou weten op welke manier de functies moeten worden opgeroepen. We geven hem als naam demostud.cpp. We dienen hier echter wel nog,de volgende opmerking te maken: na #include kan de naam van de include-file zowel tussen < > als tussen 11 11 worden vermeld. Het verschil heeft te maken met de plaats waar de compiler de header-files gaat zoeken. Include-files .die vermeld staan tussen < >zijn standaard geleverde header-files en worden gezocht in de· standaard include-directory. Filenamen vermeld tussen 11 11 zijn door de gebruiker aangemaakt en worden gezocht in de werkdirectory. Tot slot van deze paragraaf wensen we julli~ nog te wijzen op het gebruik van compilerdirectieven. In grote projecten die bestaan uit verschillende modules, is het immers niet uitgesloten dat een header-file meerdere kèren zou mo~ten worden ingevoegd; het vernestelen van include-opdrachten is immers mogelijk. Om dit te verhelpen bestaan er compiler-directieven. Het zijn alle lijnen die beginnen met een #. De compiler verwacht hier geen C++-opdracht maar een commando. Onze header-file zou dan volgende opbouw hebben:· #ifndef STUDENT_H #define STUDENT_H 11 hier bevinden zich de declaraties van de klasse Student #endif De directieve #ifndef (if not defined) wordt als waar geëvalueerd als de naam die erop volgt nog niet vroeger is gedefinieerd. In deze situatie worden alle lijnen tot aan #endif ingelast. Is de naam wel reeds gedefinieerd, dan wordt alles tot aan #endif overgeslagen. De directieve #define definieert de naam die er op volgt. Deze naam kan je uiteraard zelf kiezen. Op deze manier wordt vermeden dat de effectieve inhoud van student.h een tweede keer wordt ingevoegd. ·
123
Hoofdstuk 8. Objectgeorienteerd programmeren
8.2.5
@Helga.Naessens@hogent. be
Constructoren
Een constructor is een speciale lidfunctie die • dezelfde naam heeft als de klasse waarvan hij een lid is; • geen type heeft, ook niet void; • automatisch wordt uitgevoerd van zodra. er een object van die klasse wordt aangemaakt; • zorgt voor de initialisatie van het aangemaakte object. De bedoeling van een constructor is meestal het initialiseren van de private dataleden van de objecten van de klasse. Vermits het vaak om een kort stukje code gaat, kan dit. dan ook soms wel inline gebeuren. Bekijken we de klasse Datum uit Code 8.5. class Datum { private: int dag, ' maand, jaar; public: Datum() {//default eenstructor zonder parameters dag = 1; maand ~ 1; jaar = 1980; }
void print() ; };
void Datum :: - print() { cout << setfill('O') << setw(2) << dag << '/' << setw(2) << maand << '/' « setw(4) « jaar << endl; }
Code 8.5: Klasse Datum met een default coilstructor zonder parameters De lidfunctie Datum() is een cohstructor die er voor zorgt dat elk object van de klasse Datum automatisch de waarde krijgt van 1/1/1980. Dit betekent dat na het creëren van d1 en d2 deze beide geïnitialiseerd worden op 1 januari 1980.
124
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
Het volgende hoofdprogramma:
int main () { Datum dl, d2; dl.print(); d2.print0; return 0; }
levert ons bijgevolg de volgende uitvoer op:
01/01/1980 01/01/1980
8.2.5.1
De constructor bij verstek·
Indien een klassespecificatie geen constructor vermeldt, dan voorziet de compiler zelf een default constructor of constructor bij verstek. Deze doet dan niets anders dan het object creëren en ruimte vrijmaken in het geheugen om de inhoud ervan te kunnen opslaan; deze plaàts wordt dan evenwel niet met een speciale waarde ingevuld. Voorzien we zelf, zoals in vorig voorbeeld, in een constructor zonder parameters, dan spreekt men ook van een default constructor. Het is dan deze lidfunctie dié zal worden geactiveerd, op het moment dat er een object wordt gecreëerd, dat gedeclareerd is zonder speciale of extra vermelding. Uit wat volgt, zal blijken dat een klasse ook meerdere constructoren kan hebben.
8.2.5.2
Een constructor met parameters
Vermits een constructor een lidfunCtie is, kan die uiteraard ook parameters bezitten. Dit laat toe dat een gedeciareerd object meteen kan geïnitialiseerd worden op een waarde die de programmeur zelf verkiest. Stel dat we in de klasse uit Code 8.5 vorig voorbeeld de default constructqr zonder parameters vervangen door de volgende constructor:
Datum (int d, int m, int j) { dag d; maand= m; jaar = j; }
Dit heeft voor gevolg dat we bij de declaratie van datumobjecten steeds drie actuele parameters moeten voorzien, zoals bij het volgende hoofdprogramma:
125
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
int main () { Datum d3(6, i, i998); d3.print0; return 0; }
De declaratie 'Datum d3(6, i, i998) ; ' creëert het datumobject d3 en vult het meteen op met d~ waarde 6/1/1998, zodat dit programma ons de volgende uitvoer oplevert: 06/0i/i998 De vervanging van de default constructor door de nieuwe constructor met parameters heeft echter wel tot gevolg dat de declaraties van di en d2 uit de vorige paragraaf (Datum di, d2;) niet meer correct zullen gecompileerd worden. Dit kan opgevangen worden op twee manieren: ofwel voorzien we in een tweede constructor (en doen we aan constructor-overloading), ofwel zorgen we dat de drie parameters defaultwaarden krijgen. Beide alternatieven wotden besproken in de t wee volgende paragrafen.
8.2.5.3
Constructor,.overloading
Het concept overtoading werd reeds in paragraaf 1.12.4 aangehaald. Er is echter sprake van constructor-overloading indien een klasse meerdere constructoren bezit. Bekijken we de klasse Datum uit Code 8.6. · Dit heeft als effect dat het volgende hoofdprogramma toepasbaar is: int main () { Datum di, d2(i, i, 200i);
/1 default constructor wordt geactiveerd 11 constructor met 3 parameters //wordt geactiveerd
di . print() ; d2.print0; return 0; }
126
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
class DatUiil { private: int dag, maand, jaar; public: Datum() { I l default eenstructor zonder parameters dag = 1; maand = 1; jaar = 1980; }
Datum (int d, int m, i nt j) { llconstructor met 3 parameters dag = d; maand = m; jaar j; }
void print() ; };
void Datum:: print() { cout << setfill('O') << setw(2) << dag << 'I' << setw(2) << maand << 'I' << setw(4) << jaar << endl; }
Code 8.6: Gonstructor overloading Welke van de twee voorziene constructoren wordt gebruikt, wordt bepaald door de compiler op basis van het aantal en het type van de voorziene parameters. We moeten er dus op letten dat deze keuze op een éénduidige manier kan gebeuren en er zich geen dubbelzinnigheid voordoet. Stel dat we volgende twee constructoren hebben voorzien:
Datum (int d) {dag = d;} en Datum (int m) {maand = m;} dan kan de compiler onmogelijk beslissen welke vari de twee constructoren moet worden geactiveerd bij de volgende declaratie:
Datum d(12); Dit leidt dus zeker tot een foutmelding bij het compileren.
127
©Helga. Naessens@ho~Jent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
8.2.5.4
Een eenstructor met default parameters
Zoals gewone functies en procedures kunnen ook lidfuncties (en dus coilstructoren) gedefinieerd worden zodanig dat hun parameters standaardwaarden (verstekwaarden) krijgen, indien er geen ·actuele parameters zijn ingevuld bij de functieaanroep. Dit gebeurt bijvoorbeeld als volgt: Datum (int d = 1, int m = 1, int j = 1980 ) { dag = d; maand = m; jaar = j; }
De waarden die vermeld staan achter de gelijkheidstekens worden gebruikt indien de corresponderende actuele parameter ontbreekt. Volgende declaraties zijn dus mogelijk: Datum dl, d2(12)'
d3(12, 12)' d4(12, 12, 1997);
11 1 januari 1980 11 12 januari 1980 11 12 december 1980 11 12 december 1997
Het weze duidelijk dat alleen de laatste actuele parameters kunnen worden weggelaten. Dit is de enige manier waarop de compiler kan weten welke standaard waarde moet worden ingevuld.
8.2.5.5
Constructoren: overloading en default parameters
Indien de twee vorige oplossingen samen worden gebruikt, bestaat er gevaar voor ambiguïteit. Bekijken we het volgende voorbeeld: Datum() { I I default constructor zonder parameters dag = 1; maand = 1; jaar = 1980; }
Datum (int d = 1, int m = 1, int j = 1980) { 11 constructor met default parameters dag = d; maand = m; jaar = j; }
Niettegenstaande de twee constructoren er öp het eerste gezicht totaal verschillend uitzien, ontstaat er toch een probleem bij de volgende declaratie: Datum dat; De compiler kan immers onmogelijk beslissen welke van de twee constructoren moet gebuikt worden om dat te initialiseren.
128
.
Hoofdstuk 8. Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
Ook hier zijn er twee oplossingen mogelijk: 1. ofwel behouden we maar één van beide constructoren (meestal wordt dan de default constructor zonder parameters weggelaten)
2. ofwel verminderen we het aantal default parameters in de tweede constructor: Datum (int d, int m ;" 1, int j = 1980) { dag = d; maand = m; jaar = j; }
Deze laatste eaustructor kan maar gebruikt worden 1 indien we minstens één argument opgeven bij de declaratie van een datum.
8.3
Gebruik van objecten
8.3.1
Een object als waarde-parameter van een lidfunctie
Het is evident dat een lidfunctie niet altijd uitsluitend gebruik zal maken van degegevens die in het object zelf zitten opgeborgen, maar dat er soms ook lidfuncties zullen zijn die data nodig hebben van andere objecten. In deze situatie zullen deze objecten via de parameterlijst aan de betreffende lidfunctie worden doorgegeven. · Veronderstel bijvo'orbeeld dat we een klasse PMDbak definiëren waarvan de dataleden er als volgt uitzien: class PMDbak { private: int nblik, // aantal blikjes nkart. I/ aantal drankkartons nfles; // aantal flessen double totgewicht; };
Indien we twee dergelijke objecten bij elkaar willen optellen, dan zal het gesommeerde totaal ook een dergelijke PMDbak zijn. Stel dat de lid~nctie die dit totaal berekent som genoemd wordt en dat het resultaat in het object totaal wordt opgeslagen, dan ziet de aanroep van de lidfunctie er als volgt uit: PMDbak bak1, bak2, totaal; totaal.som (bak1, bak2);
129
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
[)e bedoeling is uiteraard, dat de aantallen van beide argument-bakken opgeteld worden en dat de respectievelijke totalen de aantallen zijn die deel uitmaken van de totaal-bak. In programma Code 8.7 vind je hiervan een voorbeeld.
8.3.1.1
De lidfunctie som
Bekijken we vooreerst één van de toekemi.ingsopdrachten in de lidfunctie som: nblik
= bi.nblik
+ b2.nblik;
nblik is hier de component van het object waar de lidfunctie som wordt op toegepast.
Het ·is maar omdat som een lidfunctie is van de klasse PMDbak dat de private dataleden van de bakken bi en b2 toegankelijk zijn. Vermits de lidfunctie niet wordt toegepast voor de objecten b1 en b2, moet .hun naam wel worden vermeld. Tot slot dient nog vermeldteworden dat bi en b2 hier waardeparameters zijn: bij actievering van de lidfunctie worden hierin dus copieën gemaakt van de · actuele parameters bak1 en bak2 uit het hoofdprogramma.
8.3.1.2
De constructor van PMDbak
Bij het bekijken van deze constructor valt op dat hier de lidfunctie bereken wordt geactiveerd zonder dat er een object voor wordt vermeld. Het is evident dat deze activering betrekking heeft op het object waarvoor de constructor wordt gebruikt. Dit betekent dat bij de declaratie PMDbak bak2(1, 6)
vanuit de constructorde lidfunctie bereken wordt toegepast op het object l;lak2,
8.3.1.3
Waardeparameters versus const referentieparameters
Bij de opdracht totaal.som(bak1, bak2);
wordt de inhoud van de actuele parameters bak1 en bak2 gekopieerd naar de geheugenplaatsen gereserveerd voor de fomiele parameters b1 en b2. Het gaat immers om waardeparameters. Dit heeft het voordeel dat indien de programmeur al dan niet per ongeluk hieraan een wijziging zou aanbrengen, dit geen invloed heeft op de actuele parameters bak1 en bak2.
130
©Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd progrmnmeren
const int blikgew = 20, kartgew
.50, flesgew
25;
class PMDbak { private: int nblik, // aantal blikjes nkart, // aantal drankkartons nfles; // aantal flessen double totgewicht; public: PMDbak (int nb = 0, int nk = 0, int nf void bereken(); vöid print() ; void som (PMDbak b1, PMDbak b2);
0);
};
PMDl;>ak :: PMDbak (int nb, int nk, int nf) { nblik = nb; nkart = nk; nfles = nf; bereken(); }
void PMDbak ::bereken() { totgewicht = nblik * blikgew + nkart
*
kartgew
+ nfles *
flesgew;
}
void PMDbak : : print() { cout << "PMDbak bevat " << nblik << " blikj es, " << nkart << 11 drankkartons en " << nfles << " flessen." << endl << "Het totaal gewicht is "<< totgewicht << endl; }
void PMDbak :: som (PMDbak b1, PMDbak b2) { nblik = b1.nblik + b2.nblik; nkart = b1. nkart + b2. nkart; nfles. = b1.nfles + b2.nfles; bereken(); }
int main () { PMDbak bak1(5, 10, 6) , bak2 (1, 6) , totaal; bak! . print() ; bak2. print() ; totaal.som(bak1, bak2); totaal. print() ; return 0; }
Code 8.7: Een object als parameter van een lidfunctie
131
Hoofdstuk 8. Objectgeorienteerd programmeren
@Helga.Naessens@hogent. be
Dit kopieerproces kost echter tijd en ruimte, zeker als het om grote objecten gaat. Dit kan :vermeden worden door de formele parameters als referentieparameters te declareren. Om te vermijden dat de actuele parameters toch zouden kunnen worden veranderd, ge.;en we ze het predikaat const mee. De hoofding van de lidfunctie som ziet er dan, zowel bij de declaratie als bij de definitie, als volgt uit: void som (const PMDbak &p1, const PMDbak &p2) ;
8.3.2
Een object als functieresultaat
We kunnen de lidfunctie som ook op een andere manier definiëren, namelijk als een functie . dieals resultaat een object aflevert van het type PMDbak. Vermits deze functie nog steeds voor een welbepaald object moet geactiveerd worden, zou de aanroep voor de berekening van totaal er dan zo uitzien: totaal= bak1.som(bak2); Dit betekent: stuur een bericht naar object bak1 om het de som te laten berekenen van zijn eigen inhoud en die van object bak2 en laat dit een functiewaarde als resultaat opleveren. De inhoud van het objeèt bak1 wordt daarbij niet gewijzigd. De declaratie zou er dus .als volgt uitzien: PMDbak som (const PMDbak &b); Om een object te kunnen teruggeven, moet de functie die natuurlijk eerst zelf aanmaken. Hiertoe gebruiken we een hulpobject dat slechts tijdelijk bestaat gedurende de activering van de lidfunctie. De volledige definitie van som luidt dan: PMDbak PMDbak :: som (const PMDbak &b){ I I eerste PMDbak is type van functi_e 11 tweede is scopeaanduiding voor som PMDbak hulp(nblik + b.nblik, nkart +b.nkart, nfles + b.nfles); return hulp; }
8.3.3
Een object aanpassen in een lidfunctie
In één van de vorige paragrafen hebben we gezien dat een lidfunctie een referentieparameter kan hebben met het predikaat const. Indien we dit predikaat weglaten, dan is de functie wel in staat om het overeenkomstige argument te :veranderen. Stel dat men de inhoud van twee PMD-bakken in een derde stopt en de eerste twee wil leeg maken. We maken hiertoe een procedure ruim_op met twee referentieparameters
132
Hoofdstuk 8. Objectgeoriènteer.d programmeren
@Helga.Naessens@hogent. be
waarvan de inhoud gedropt wordt in het object waarvoor de proeedlire wordt geactiveerd. De aanroep is dan bijvoorbeeld: bak3.ruim~op(bak1,
bak2);
De aangepaste klasse PMDbak vind je terug in Code 8.8. In Code 8.9 is een hoofdprogramma weergegeven waarin: het gebruik van deze klasse geïllustreerd wordt.
8.3.3.1
Een private lidfunctie
Om de twee bron-objecten leeg te kunnen maken, werd in de nieuwe klasse PMDbak een lidfunctie maakleeg gedefinieerd die deze taak volbrengt. De declaratie van deze functie werd in het private deel van de klasse geplaatst, waardoor het alleen de klasse-eigen lidfuncties zijn die er gebruik van kunnen maken. Bijgevolg kunnen de gebruikers van de klasse ook niet zomaar een PMDbak leeg maken en de inhoud ervan in het niets laten verdwijnen. (De lidfunctie maakleeg oproepen vanuit het hoofdprogramma is dus niet toegestaan.)
8.3.3.2
Een const lidfunctie
Het predikaat const kan ook worden gegeven aan een lidfunctie zelf: hiermee willen we dan aangeven dat deze functie het object waarop ze wordt toegepast, niet zal veranderen. In de nieuwe klasse PMDbak werd zowel bij de declaratie als bij de definitie van de lidfunctie print het predikaat const toegevoegd, met als gevolg dat bij de aanroep bak!. print(); ge!3n enkele gegevenscomponent van bak! kan veranderd worden. We raden aan om indien mogelijk steeds dit predikaat. te gebruiken.
8~4
Klassen: conversie en constructie
Klassen zijn enigszins vergelijkbaar met types, objecten hebben veel weg van vatiabelen. Voor de standaard types als int, double en char bestaan allerlei voorzieningen: er kunnen onder meer omzettingen gebeuren van het ene naar het andere type. Deze conversies gebeuren vaak vanzelf zonder dat we er erg in hebben, bv. bij de opdracht double x
= 89;
133
Hoofdstuk 8. Objectgeorienteerd programmeren
const int blikgew
@Helga.Na.essens@hogent. bé
20, kartgew
50, flesgew
= 25;
class PMDbak { private: int nblik, 11 aantal blikjes nkart, I I aantal · drankkartons nfles; 11 aantal flessen double totgewicht; void maakleeg(); 11 een private lidfunctie public: PMDbak (int nb = 0, int nk = 0, int nf = 0); void bereken(); void print() const; 11 een const lidfunctie void ruim_op (PMDbak &pi ·, PMDbak &p2) ; };
void PMDbak :: maakleeg () { nblik = Ö; nkart = 0; nfles bereken();
0;
}
PMDbak :: PMDbak (int nb, int nk, int nf) { nblik = nb; nkart = nk; nfles = nf; bereken(); }
void PMDbak :: bereken() { totgewicht = nblik * blikgew + nkart
*
kartgew + nfles
*
flesgew;
}
void PMDbak ::print() const { cout << 11 PMDbak bevat 11 << nblik << 11 blikjes, 11 << · nkart << 11 drankkartons en 11 << nfles << '1 flessen. 11 << endl << 11 Het totaal gewicht is 11 << totgewicht << endl; }
void PMDbak : : ruim_op (PMDbak &b1, PMDbak &b2) { nblik b1 . nblik + b2.nblik; nkart b1. nkart + b2. nkart; nfles = b1.nfles + b2 . nfles; bereken() ; b1.maakleeg(); b2. maakleeg () ; }
Code 8.8: Een object als referentieparameter
134
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
int main () { PMDbak bak1(5, 10, 6), bak2(1,6 ), bak3; bak i. print() ; bak2. print() ; bak3. print 0 ; bak3. ruim_ op (bak1, bak2) ; bak1.print(); bak2.print(); bak3.print(); return 0; }
Code 8.9: PMDbakken,öpruimen In sommige gevallen moeten we de conversie zelf afdwingen door wat men noemt typecasting, bijvoorbeeld in de opdrachten: int ·i = 789, j = 456; double y = doublè(i) Ij; Conversies tussen klassen en standaard types in beide richtingen moeten we zelf definiëren. In deze paragraaf bekijken we de conversie van standaard types naar klasseobjecten, waarbij voor constructoren een zeer belangrijke rol is weggelegd.
Initialisatie bij constructie
8.4. 1
Reeds vroeger hebben we gezien dat constructoren kunnen zorgen voor de initialisatie van de objecten. We kunnen ze op een eenvoudige manier inzetten door verschillende constructoren te voorzien, waarbij er natuurlijk verschil moet zijn in de sootten en aantallen parameters. Als voorbeeld definiëren we in Code 8~10 een klasse Student, waarvoor we 4 verschillende constructoren voorzien: één zonder parameters (default), één met alleen een nummer, één met alleen een string en één met beide argumenten. In het hoofdprogramma kan deze klasse aanleiding geven tot de volgende objectdeclaraties en initialisaties: int main'O { Student st1, st2("Jan.ssens"), st3(3), st4("Peeters", 196); st1.druk(); st2.druk(); st3.druk0; st4.druk(); return 0; }
Je moet maar eens zelf nagaan wat er op het .scherm verschijnt.
135
@Helga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
class Student { private: string naam; int stnr; public: Student().{ naam = "Anoniem"; stnr
0;
}
Student (string nm) { naam = nm; stnr = 0; }
Student (int nr) { naam = "Anoniem"; stnr
nr;
}
Student (string nm, int nr){ naam = nm; stnr = nr ; }
void druk() { cout « setw(20) «
naam «
setw(6) << stnr« endl;
} };
Code 8.10: De klasse Student
8.4.2
De copy-constructor
Zonder speciale voorzieningen kan ook de volgende declaratie van student st5 gebeuren: Student st5(st4); Dit object wordt aangemaakt en geïnitialiseerd met de gegevens die zijn opgeslagen in object st4. Hiervoor wordt de copy-constructor gebruikt: na de geheugenreservatie worden de gegevens van st4 gecopieerd naar het nieuwe object. Deze constructor is altijd standaard aanwezig. TEmzij we in het programma zelf een copyconstructor voorzien, wordt er van deze default-constructor gebruik gemaakt. Het is evenwel slechts in een aantal speciale gevallen echt noodzakelijk, namelijk indien we dynamisch geheugen gebruiken, zelf zo'n constructor te definiëren. Toch willen we hier nu reeds aangeven hoe zo'n copy-cönstructor er uit ziet. Uit bovenstaande declaratie van st.5 kunnen we afleiden dat hij één parameter heeft van het- type
136
@Helga.Naessens@hogent.·be
Hoofdstuk 8. Objectgeorienteerd programmeren
Student. Je zou zijn hoofding dus kunnen zien als Student (Student st); maar dit is evenwel NIET correct. De rèden moet gezocht worden in het feit. dat de copy':"constructor ook nog in twee andere situaties wordt gebruikt. Indien een lidfunctie een object als resultaat heeft, zoals we vroeger gezien hebbèn bij som in de klasse PMDbak, wordt het functie-resultaat ook gecopieerd naar een nieuw object. Indien in een functiehoofding een klasseobject als formele waardeparameter vermeld staat, zal bij de functieaanroep de actuele parameter ook gecopieerd worden .naar de formele parameter met behulp van de copy-constructor. Het is dit laatste geval dat verklaart waarom de bovenstaande hoofding van de copyconstructor niet correct is. Je kan bij de' definitie van de copy-constructor geen gebruik maken van waardeparameters, omdat die slechts kunnen overgedragen met behulp van de lidfunctie die je aan het definiëren bent. Dit betekent dat de parameter een referentieparameter moet zijn. De juiste hoofding en standaard acties vind je hieronder: Student (Student &;stud) { naam = stud.naam; stnr
=
//copy constructor, default aanwezig stud.stnr;
}
8.4.3
Conversie bij constructie
De declaratie en initialisatie van st6 Student st6("Vanderheyden"); kan ook op volgende manier worden geschreven: string s = "Vanderhe.yden"; Student st6 ";, ·::;; Hoewel dielijkt op een toekenningsopdracht, is het dit zeker niet! Het is wel een initialisatie met behulp van de constructor Student (string nm); Hoewel het natuurlijk om het opvullen van objecten met beginwaarden gaat, zou je dit toch wel kunnen bekijken als het converteren van standaard types naar klasseobjecten. Het is evident dat dit alleen maar mogelijk is met constructoren die slechts één parameter hebben. Het kan natuurlijk ook als er meer zijn, waarbij er sommige parameters opgevuld worden met default waarden zoals bijvoorbeeld met
137
@He]ga.Naessens@hogent. be
Hoofdstuk 8. Objectgeorienteerd programmeren
Student (string nm, int stnr = 0); Met de vroeger vermelde constructoren kunnen we dus ook volgende initialisaties neerschrijven: Student st7 Student st8
7; st4;
Indien je wil zien welke constructor wanneer wordt geactiveerd, kan je er tijdelijk enkele uitvoeropdrachten in programmeren die de constructor kenmerken.
8.4.4
Conversie na initialisatie
Tot .nu toe hebben we constructoren gebruikt bij declaratie en de daarbij horende initialisatie van objecten. Dit is natuurlijk de ho~fdtaak van constructoren. Maar ook na het initialiseren kan er nog steeds van constructoren gebruik worden gemaakt. Bekijken we volgend hoofdprogrammafragment: Student st1, st2, st3; string s "Vondeling"; st1 ~; st2 3; st3 st1; De drie objecten worden door de default-constructor gemaakt en opgevuld met de waarden "Anoniem" en 0. Wat gebeurt er bij de drie toekenningsopdrachten? Vermits er bij de eerste twee geèn klasseobject staat aan de rechterkant maar wel aan de linkerkant, is de compiler op zoek gegaan naar constructoren van de klasse Student waarvoor hij de initiaUsaties met respectievelijk een string en een integer kan gebruiken. Vermits dergelijke constructoren bestaan, wordt bij het uitvoeren van het programma in deze twee gevallen een tijdelijk object gemaakt van deze klasse: deze worden op· de manier geïnitialiseerd zoals beschreven in vorige paragraaf. Nadat dit gebeurd is, wordt dit tijdelijk object (zonder naam) toegewezen aan het object aan de linkerkant van de opdracht met behulp van de standaard toekenningsoperator = die voor elke klasse aanwezig is. Na deze toekenning wordt het tijdelijke object vernietigd. Bij de derde opdracht gebeurt er helemaal geen conversie. Aan beide kanten van de operator staat een object van dezelfde klasse, waardoor hier gewoon een toekenning gebeurt d.m.v. de standaard operator=. Deze operator kopieert de inhoud van het object aan de rechterkant naar dat aan de linkerkant.
138
Hoofdstuk 8. Objectgeorienteerd programmeren
8.4.5
@Helga.Naes;ens@hogent. be
Conversie met andere constructoren
Naast het impliciet activeren van constructoren, zoals in alle vorige voorbeelden, bestaat ook de mogelijkheid om een welbepaalde constructor expliciet aan te roepen. Hier bestaat dan niet de beperking van één argument. Zo kunnen we bijvoorbeeld de vroeger gedeclareerde student st1 verder in het programma een totaal nieuwe waarde geven met de opdracht st1 =Student(); Hierbij wordt, zoals in vorige paragraaf beschreven, een tijdelijk object gemaakt waarvoor hier de default constructor wordt geactiveerd. Nadien gebeurt er een toekenning naar st1. Op een analoge manier kunnen de volgende opdrachten gebeuren: st2 st3
Student( 11 Bonte 11 , 123); Student( 11 Gates 11 ) ;
Bij de eerste toekenning wordt voor het tijdelijke object de constructor met twee parameters gebruikt, bij de tweede toekenning wordt het tijdelijke object gecreëerd m.b.v. de constructor met de string-parameter.
139
Hoofdstuk 9 Pointers Elke programmeertaal heeft zo zijn specifieke stijlkenmerken en bij C++ is dit ondermeer . het veelvuldig gebruik van pointers. Pointers worden in C++ heel veel gebruikt, maar toch is het relatief moeilijk om onmiddellijk duidelijke voorbeelden te geven waarin pointers op een relevante manier worden toegepast. We hopen dat de betekenis van het begrip gaandeweg duidelijk wordt naarmate dit hoofdstuk vordert.
9.1
Inleiding
De Engelse term pointer wordt in C++ gebruikt voor ee11 gegeven dat verwijst naar een ander gegeven 1 . · Een variabele pa die naar een gehele variabele a verwijst, heet een gehele pointervariabele, en analoog, een variabele pr die naar een reële variabele r verwijst noeme.n we een reële pointervariabele. Wanneer we met pointers werken, is het soms nuttig om een kleine schets te maken met' . daarop de verschillende variabelen en hun onderlinge relaties. Een gewone .variabele tekenen we dan als een hokje met daarin eventueel een waarde, terwijl een pointervariabele meestal gerepresenteerd wordt als een pijl die wijst naar de corresponderende variabele. Zo worden de pointers pa en pr als volgt voorgesteld:
1 . Een andere Engelse vertaling van het woord 'verwijzing' is 'reference', en in C++ is er ook een concept dat met de term reference variable aangeduid wordt, waar we in deze cursus trouwens niet verder op ingaari. Verwar dit niet met het begrip pointer zoals hier beschreven.
140
[email protected]@hogent. be
Hoofdstuk 9. Pointers
Het is belangrijk te weten dat C++ een duidelijk onderscheid maakt tussen deze twee soorten pointers. Je kan dezelfde pointervariabele dus niet eerst naar een geheel en dan . . later naar een reëel getallaten verwijzen. Zoals alle andere variabelen moeten ook pointervariabelen steeds worden gedeclareerd vooraleer ze worden gebruikt. Dit gebeurt door eerst het type aan te geven waarnaar die variabele verwijst en daarna de naam te schrijven, voorafgegaan door een sterretje. In het volgende voorbeeld zijn pr, q en r pointers naar reële getallen en verwijzen pa en xb naar gehele getallen: double *pr; int *pa, *xb; double *q, . *r, s ; Je mag dus gerust meerdere pointerdeclaraties tot één enkele. lijn combineren, op voorwaarde dat je het sterretje steeds herhaalt. De variabele s hierboven is dus een gewone reële variabele en geen pointer! Om met pointers te werken, gebruiken we de twee operatoren & en *· Is a een gehele variabele, dan is &a een gehele pointer( constante) die naar a verwijst. Is pr een pointer( waarde) naar een reëel getal, dan is *pr het reëel getal waar pr naar verwijst. Je kan dus schrijven double r = 3.5; double *pr; pr = &r; cout << (1.0 *pr) << endl;
+
en er komt 4.5 op het scherm. De derde lijn zorgt ervoor dat pr naar de variabele r komt te verwijzen en de vierde lijn drukt de som af van het getal 1 en de inhoud van *pr, m.a.w., de inhoud van het reëel getal waar pr naar verwijst, of nog anders gezegd: de inhoud van r. Verwar niet: bij de declaratie van een pointer gebruiken we een sterretje, maar als we een pointer naar een gegeven variabele willen laten verwijzen, gebruiken we&. De operator * mag dus alleen toegepast worden op pointers 2 en &alleen op variabelen. (of tabelelementen, zoals we later zullen zien). . Een sterretje voor een pointervariabele plaatsen, noemen we een pointer derefereren. (De *-operator heet de dereferentie-operator en de &-operator heet de adres-operator.) Een gederefereerde pointer mag zich ook aan de linkerkant van een toewijzing bevinden, zoals in het volgende voorbeeld:
2 Natuurlijk heeft de & hier niets te maken met (invoer-)uitvoerparameters of met de logische 'en', en houdt * hier geen verband met dé vermenigvuldiging.
141
Hoofdstuk 9. Pointers
©Helga.Naessens@hogent. be
int a, b = 200; int *p, *q; p = &a; *P = 123; q = &b; *q += 121; Dit is een ingewikkelde manier om aan de variabele a de waarde 123 toe te kennen en aan b de waarde 321. De opdracht *P = 123; betekent dat het getal 123 moet geplaatst worden in de variabele waar p naar verwijst, terwijl *q += 121; wil zeggen: 'tel 121" op bij de variabele waar q naar verwijst'. Net zoals bij initialisatie van gewone variabelen kan men de declaratie van een pointervariabele combineren met een toewijzing aan die pointer. Bovenstaand voorbeeld wordt dus korter genoteerd als int a, b = 200; int *P = &a, *q *P = 123; *q += 121;
&b;
De initialisatie van q in de tweede lijn illustreert een schrijfwijze die soms wel eens aanleiding geeft tot verwarring. Door de notatie *q = &b krijg je de indruk dat &b (een verwijzing naar b) in *q (de variabele waar q naar verwijst) gestopt wordt, terwijl die verwijzing in werkelijkheid in q gestopt wordt. Het sterretje vóór q maakt onderdeel uit van de declaratie en niet van de toewijzing. Niet alleen de gederefereerde pointer, maar ook de pointervariabele zélf kan aan de linker-:kant van een toewijzing staan. We dienen dus een. duidelijk onderscheid te maken tussen de toewijzing *P = *q en de toewijzing p = q. Beschouw bijvoorbeeld de variabelen p en q zoals hierboven gedeclareerd.
Het effect van de twee verschillende toewijzingen op deze situatie hebben we hierna geschetst.
142
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
Aan de linkerkant van de figuur is het effect te zien van de toeWijzing *P = *q. Hier verandert de inhoud van de variabele waar p naar verwijst, maar p zelf blijft ongewijzigd.· In het tweede geval wordt de inhoud van de variabele p echter zelf veranderd. p wijst nu niet meer naar a, maar krijgt ·dezèlfde inhoud als q, namelijk een verwijzing naar b. · Merk op dat toewijzingen zoals *P = q of p = *q in dit geval niet toegelaten zijn, want dan zijn de linker~ en de rechterkant van het gelijkheidsteken niet vah een gelijkaardig type. De ene kant is een pointer en de andere kant een gewoon getal.
9.2
Pointers en tabellen
In plaats van naar een variabele, . kan een pointer ook verwijzen naar een element in een tabel3 , waarbij we steeds zorgen dat het type van de pointer overeenkomt met het type van het tabelelement. Als voorbeeld geven we de volgende declaraties: int tab[4] = {3, -12, 5, 7}; int *P = &tab[O]; int *q = &tab[2]; In een figuur ziet dit er dan als volgt uit (we geven een tabel hier weer als een horizontale rij van hokjes):
In de praktijk maakt men nogal veel gebruik van pointers naar het eerste element van een tabel. Daarom bestaat er daarvoor in C++ ook een speciale notatie: is ·tab eeri tabel, dan mag je in plaats van .&tab [0] . ook gewoon tab schi:ijven. Vergeet echter niet dat tab dan een constante pointerwaarde voorstelt, en dus zeker geen variabele is. Het vorige voorbeeld kan je dus herschrijven als: int tab[4] = {3, -12, 5, 7}; int *P = tab; int *q = &tab[2]; 3 Er bestaan ook pointers die naar een volledige tabel verwijzen in plaats van naar één enkel element. We gaan dergelijke pointers hier niet gebruiken. Het onderwerp is zo al verwarrend genoeg.
143
Hoofdstuk 9. Point.ers
. @Helga.Naessens@bogimt. be
Deze gelijkschakeling van tabellen en pointers wordt ook in omgekeerde zin toegepast. Is p een pointer naar een geheel getal, dan mag je in plaats van *P ook p [0] schrijven om het getal aan te duiden waar p naar verwijst. De analogie gaat zelfs verder. Is p een pointer naar een element van een gehele tabel, .dan zijn uitdrukkingen zoals p [1], of nog meer algemeen p [i], ook toegelaten. Als p naar een bepaald element in een tabel verwijst, dan duidt p [1] op het element in dezelfde tabel, doch één plaats verder, terwijl in het algemeen p [i] het element aangeeft op i plaatsen voorbij p [0]. De index i mag hier trouwens ook negatief zijn: p [ -1] is dus het element in de tabel dat vóór p [0] komt. De volgende tekening stelt een tabel tab voor van 10 elementen en een pointer q naar het zesde element in die tabel (q = &tab [5]) . Je ziet dat je de verschillende elementen van tab nu ook via q kan bereiken.
qf1 !
·-· -
9.3
l
"
Pointers als parameters
In hoofdstuk 5 hebben we gezien dat tabellen parameters van functies en procedures kunnen zijn. Er werd toen ook gezegd dat een tabelparameter om efficiëntieredenen nooit gecopieerd wordt, maar dat er steeds gewerkt wordt met de originele tabel. De functie of procedure moet dus verteld worden waar dat origineel zich bevindt. Dat gebeurt door een pointer naar het eerste element van de tabel als parameter door te geven, ook al was dat niet uit de notatie af te leiden~ We schreven immers void proc(int tab [] ){ }
maar we hadden net zo goed kunnen schrijven void proc(int *tab){ }
omdat beide notaties. equivalent zijn. De tweede laat echter duidelijk zien dat de werkelijke parameter een pointer is. Bovendien· is deze pointer een invoerparameter; omdat pointers dezelfde regels volgen als normale types: een (invoer-) uitvoerparameter moet vooraf gegaan worden door &. (We komen daarop terug.) Ook al is de pointer zelf een invoerparameter, de tabel waar hiJ naar verwijst kan gewijzigd worden, en kan dus als
144
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
(indirecte) invoer-uitvoerparameter beschouwd worden. De procedure copy_doubles van Code 9.1 copieert de eerstenreële getallen van een tabel souree naar een tabel dest, die groot genoeg ondersteld wordt. void copy_doubles (double *dest, double *source, int n) { for (int i 0 ; i < n ; i++) dest[i] source[i]; }
Code 9.1: Copieert de eerste n getallen van de tabel souree naar de tabel dest
Deze procedure gebruik je dan bijvoorbeeld op de volgende manier: double a[40]; ·double b [80] ; copy_doubles(b, a, 40); copy_doubles(&b[40], a, 40); copy_doubles(a, &a[!], 39); Wanneer je een procedure met pointerparameters oproept, hoeven de argumenten niet noodzakelijk naar verschillende tabellen te verwijzen. In de laatste opdracht gebruiken we bijvoorbeeld twee verwijzingen naar elementen van a. Het effect van deze opdracht is dat de elementen van a allemaal één plaats naar links worden opgeschoven: Het eerste element . verdwijnt en het laatste element bevindt zich dan op de laatste én op de voorlaatste plaats.
9.4
Pointers naar const
De inhoud van een tabel die via een pointerparameter aan e~n deelprogramma wordt doorgegeven, kan steeds door dat deelprogramma veranderd worden. Wanneerje echter het sleutelwoord const vóór de declaratie van een pointer plaatst, dan kan je die pointer niet meer gebruiken om datgene waar hij naar verwijst te veranderen. Een dergelijke pointer heet 'een pointer naar const' 4 • Als je dit toepast op een pointerparameter, geef je aan dat de tabel een soort invoerparameter is. Let wel: de pointer zelf was zonder de const reeds een invoerparameter, maar nu wordt (met const) ook de tabel dat. (Ook bij de. eerste notatie voor een tabelparameter werd reeds const gebruikt als de tabel niet mocht gewijzigd worden. Dat sloeg ook op de pointerparameter, alleen was die niet zichtbaar.) De definitie van copy...:doubles uit Code 9.1 kan dan verbeterd worden zoals in Code 9.2. 4 Je zou misschien eerder de term 'const-pointE!r' verwachten, maar die benaming wordt gebruikt voor een ander begrip.
145
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
void copy _doubles (double *dest, const double * souree, int n) { for (int i 0 ; i < n ; i++) dest[i] = source[i]; }
Code 9.2: Copieett de eerste n getallen van de tabel souree naar de tabel dest Met een dergelijke const-declaratie zal de compiler steeds. controleren of er nooit een dereferentie van souree (d.w.z. een uitdrukking van de vorm * souree of souree [ ... ] ) aan de linkerkant van een toewijzing komt te staan, of als argument gebruikt wordt in een deelprogramma dat daar een invoer-uitvoerparameter verwacht 5 . Ook kan je de pointer souree zelf nooit toewijzen aan een andere .pointer die de const-vermelding niet in zijn declaratie heeft, want dan zou je immers via die nieuwe pointer * souree kunnen veranderen. Dit betekent dus ook dat je een pointer naar const niet als argument kan gebruiken van een deelprogramma dat daar een pointerparameter heeft die zelf niet met const werd gedeclareerd. Indien het dus werkelijk nodig is om souree aan een andere pointer toe te wijzen, dan moet ook die andere pointer met const gedeclareerd zijn. Het omgekeerde is wel toegelaten: met bovenstaande declaraties zou je eventueel souree = dest mogen schrijven. In het volgende fragment is de eerste reeks toewijzingen dus toegelaten maar de tweede reeks niet:. int *p, n; const int *cp; const int *ca; ·*p = *cp; cp *Ca = n; *cp
9.5
p; cp = ca; //dit is toegelaten *ca; p = ca; //dit is NIET toegelaten
Rekenen met pointers
We weten reeds dat de uitdrukkingen &p [0] en p in C++ door elkaar gebruikt mogen worden. Dit soort notatie wordt zelfs nog verder veralgemeend: je mag de uitdrukking &p [i] steeds vervangen door de uitdrukking p+i. Met andere woorden, je kan gehele getallen optellen bij pointers en er ook van aftrekken. Tellen we het getal i op bij een reële pointer p, dan bekomen we een pointer die precies i reële getallen verder wijst. Een paar voorbeelden terug hebben we van de procedure copy _doubles een toepassing gegeven. Met de nieuwe verkorte notatie, ziet dit fragment er nu als volgt uit: 5 We kunnen dit lijstje nog uitbreiden: opdrachten zoals 'cin >> *source' zijn bijvoorbeeld ook niet toegelaten. Kortom, de compiler probeert er zo goed mogelijk voor te zorgen dat er aan de tabel waar souree naar verwijst, niets gewijzigd wordt. De compiler is echter niet alwetend, dus zijn er altijd wel mogelijkheden om deze regel te omzeilen. Dit doe je dan op eigen verantwoordelijkheid.
146
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
double a[40]; double b[80]; copy_doubles(b, a., 40); copy_doubles(b + 40, a, 40) ; copy_doubles(a, a+ 1, 39); Let op! Je kan enkel gehele getallen optellen bij pointers: Reële getallen zijn niet toegelaten en je kan ook nooit twee pointers· bij elkaar optellen. Twee pointers die naar elementen van dezelfde tabel verwijzen kan je wel van elkaar aftrekken. Het. resultaat is dan het aantal elementen (met teken) dat zich tussen beide verwijzingen bevindt. ln de volgende figuur komen bijvoorbeeld drie pointers p, q en r voor die wijzen naar reële getallen in een bepaalde tabel.
Met deze gegevens is p - q bijvoorbeeld gelijk aan 2, r :- q is 5 en p - ris -3. Niet alleen de operatoren + en - zijn toegelaten, maar ook de overeenkomstige toewijzingsoperatoren +=, ~= , ++ en --. Indien we pointers zelf opschuiven, spreken we van schuivende pointers. Ook kunnen we met ==en ! = nagaan of twee pointers gelijk zijn of verschillend, m.a.w., of ze naar dezelfde plaats wijzen of niet. Vergis je niet, in bovenstaande figuur wijzen zowel p als r naar het getal 99.9, en toch is p == r onwaar. *Pen *r zijn daarentegen wel gelijk. Je kan in dezelfde tabel ooktesten of een bepaalde pointer'kleiner' is of 'groter' dan een andere pointer, m.a.w., of het verschil tussen de twee pointers negatief is of positief, of nog anders gezegd, of de ene pointer wijst naar een plaats vóór of na de andere pointer. In de bovenstaande figuur is q kleiner dan p en p kleiner dan r. Bekijk de functie uit Code 9.3. Deze functie berekeb.t de som van alle gehele getallen in een tabel die tussen p [0] en q [0] liggen.
147
@Helga.Naessens@hogent. be
Hoofdstuk 9. Pointers
int som (const int *p, const int *q) { int res = 0; while (p < q) { res += *p; p++; }
return res; }
Code 9.3: Berekent de som van de getallen in een tabel tussen p[O] en q[O] Dit gebeurt door de pointer p stap voor stap één plaats naar rechts te schuiven (p is dus een schuivende pointer) en telkens het getal waar hij naar verwijst bij een variabele res op te tellen. Dit hebben we geschetst in onderstaande figuur , .
r """.'--
P I
q [
·+"I.__.__---...~~.-.-_..~--_-'--_______.__---~----.J
3-+---------------1
Merl.<. op: • .Dat peen pointer naar const is, betekent niet dat we p niet mogen wijzigen, maar alleen dat we geen enkel element van de tabel waar p naar wijst, van waarde mogen veranderen. · • Op het einde van de functie s om verwijst p naar hetzelfde element als q. Aangezien p als pointervariabele eigenlijk een invoerparameter is, en geen invoer-uitvoerparameter, heeft dit geen effect op de rest van het programma. • Alleen de grijze hokjes worden opgeteld, d.w.z. , het element waar p oorspronkelijk naar verwijst telt mee, dat waar p uiteindelijk naar verwijst, niet. . • Roep je de functie op met een tweede parameter die vóór de eerste wijst, dan is het resultaat nul. Je kan de binnenkant van de while-lus uit Code 9.3 nog korter noteren zoals in Code 9.4. Merk op dat *p++ geïnterpreteerd wordt als * (p++), d.w.z., gebruik de waarde van het geheel getal waar p naar verwijst en schuif achteraf p één plaats verder op. De uitdrukking (*p)++ daarentegen betekent dat er één moet worden opgeteld bij het. geheel getal waar p naar verwijst, zonder dat p zelf verandert. In dat geval mag je de haakjes niet weglaten.
148
@Helga.Naessens@hogent. be
Hoofdstuk 9. Pointers
int som (const int *P• const int *q) { int res = 0; while (p < q) res += *p++; return res; }
Code 9.4: Berekent de som van de getallen in een tabel t.ussen p[O] en q[O] De volgende functie uit Code 9.5 bepaalt het product van een reeks elementen in een tabel. De lengte van de reeks is niet gekend, maar ze wordt afgesloten met een nul. int prod (const int *p) { int res = 1, i = 0; while (p[i] != 0) { res *= p[i]; i++; }
return res; }
Code 9.5: Berekent het produkt van de getallen in een tabel
Dit kan echter ook zonder de index i, door de pointer p zelf op te schuiven, zoals weergegeven in Code 9.6. We maken hier dus gebruik van een schuivende pointer. int prqd (const int *p) { int res = 1; while (*p != 0) res *= *p++; return res; }
Code 9.6: Berekent het produkt van de getallen in een tabel
Ook de definitie van copy _doubles kunnen we herschrijven met schuivende pointers, zoals in Code 9.7. Zoals we later nog zullen zien, zijn er heel wat toepassingen .waarbij de lengte van een reeks of van een tabel niet op voorhand gegeven is, maar indirect bepaald wordt door de reeks af te sluiten met een speciaal element. In. dat geval is een teller meestal niet nodig en zal men de voorkeur geven aan schuivende pointers.
149
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
void copy_doubles (double *dest, const double *source, int n) { for (int i 0 ; i < n i++) *dest++ = *source++; }
Code 9.7: Copieert de eerste n getallen van de tabel souree naar de tabel dest De volgende procedure uit Code 9.8 neemt twee tabellen sl en s2 van positieve gehele getallen, elk afgesloten met een negatief getal, en maakt daarvan één lange tabel d die eerst de elementen van s1 bevat, daarna de elementen van s2 en tenslotte een negatief getal om op zijn beurt de reeks afte sluiten. (Onderstel dat de tabel waar d naar wijst groot genoeg is.) Het gebruik van schuivende pointers maakt deze definitie heel beknopt. void concat (int *d, const int *sl, const int *s2) { while (*sl >= 0) *d++ = *Sl++; while (*s2 >= 0) *d++ = *s2++; *d *s2;
=
}
Code 9.8: Voegt twee tabellen samen
Merk op dat bij schuivende pointers de notatiè *s de voorkeur geniet boven s (0] . Dit is ·opnieuw een kwestie van traditie.
9.6
Pointers naar structs
Aangezien structs volwaardige gegevenstypes zijn, bestaan er niet alleen tabellen van structs (cfr. paragraaf 3.2.2), maar ook pointers naar structs. Ook om een tabel van structs door te geven van en naar een functie of procedure, maken we dus gebruik van een pointer die wijst naar het eerste element van de tabel. We beschouwen opnieuw de volgende voorstelling van een complex getal: struct Complex { double re , im; · };
De functie tabelprad uit Code 9.9 berekent het product van n complexe getallen in een tabel tab, waarbij we g(:lbruik maken vari de functie prod uit Code 5.10.
150
Hoofdstuk 9. Pointers
©[email protected]
Complex tabelprad (const Complex *tab, int n) { Complex res = tab[O]; for (int i = 1 ; i < n ; i++) res = prod(res, tab[{]); return res; }
Code 9.9: Berekent het product van n complexe getallen in een tabel tab Pointers naar structs wordt echter niet alleen gebruikt om tabellen van structs door te geven. Ze worden ook vaak gebruikt om één enkele struct door te geven als parameter, om zo te vermijden dat de volledige struct gecopieerd moeten worden. Bijgevolg zal .men niet zomaar een struct doorgeven als parameter, maar wel een pointer naar de struct. · De functie omtrek uit paragraaf 5.1.2 die de omtrek van een veelhoek bepaalt, kunnen we dus ook herschrijven door gebruik te maken van een pointer, zoals weergegeven in Code 9.10. double omtrek (const Veelhoek *v) { int gr = (*v).aantalptn; double som= afstand((*v).hoekptn[gr - 1], (*v).hoekptn[O]); for (int i = 1 ; i < gr ; i++) som+= afstand((*v).hoekptn[i - 1], (*v).hoekptn[i]); return som; }
Code 9.10: Functie bepaalt de
o~trek
van een veelhoek
Merkop dat de haakjes in (*v). aantalpunten nodig zijn. Een uitdrukking zoalS *a. b wordt in C++ nàmelljk geïnterpreteerd als *(a. b), m.a.w., het ding waar de pointer a . b naar verwijst, wat alleen nuttig is als het veld b van a een pointer is. , C++ biedt echter een andere afkorting. In plaats van (*a). b mag je a->b schrijven, en deze tweede notatie geniet doorgaans de voorkeur. In Code 9.11 wo;dt gebruik gemaakt van deze notatie. double omtrek (const Veelhoek *v) { int gr = v->aantalptn; double som= afstand(v->hoekptn[gr - 1], v->hoekptn[O]); for (int i = 1 ; i < gr ; i++) som+= afstand(v->hoekptn.[i - 1], v->hoekptn[i]); return som; }
Code 9.11: Functie bepaalt de omtrek van een veelhoek
151
Hoofdstuk 9. Pointers
9.7
@Helga.Naessens@hogent. be
Pointers als resultaten van deelprogramma's
In een tabel van gehele getallen, afgesloten met een nul, wensen we de plaats te vinden van het grootste getal. (Dit heeft enkel zin als de tabel tenminste één getal bevat, vóór de nul.) Code 9.12 toont de gebruikelijke oplossing. int index_max (const int *tab) { int res = 0, i = 1; while (tab[i] . != 0) { if (tab[i] > tab[res]) res
i;
i++; }
return :res; }
Code 9.12: Bepaalt de index van het grootste getal in een tabel
Kunnen we deze functie nu ook herschrijven zonder een gehele teller maar met schuivende pointers? Wanneer we werkelijk de index van het grootste getal willen teruggeven, dan: heeft dit weinig zin, want dan hebben we toch een teller nodig om die index te bepalen. In C++ mag je echter oök functies schrijven die een pointer als waarde hebben, en dus kunnen we hier in plaats van een index, een pointer naar het grootste getal teruggeven. Dit gebeurt zoáls in Code 9.13. int * point~r_max (int *tab) { int *res = tab++; while (*tab ! = 0)· { if (*tab > *res) res = tab; tab++; }
return res; }
Code 9.13: Functie met als resultaat een pointer naar het grootste getal in een tabel
De hoofding van deze functie begint met int * orn aan te duiden dat de functiewaarde een gehele pointer is. Vanzelfsprekend moet dan ook de uitdrukking in de return-opdracht een pointer van dit type zijn (res in dit geval).
152
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
De functie pointer_max kan in een hoofdprogramma bijvoorbeeld als volgt gebruikt worden om het grootste getal af te drukken en te vervangen door -1. int a[70]; int *p; p = pointer_max(a); cout << (*p) << endl; *P = -1; Merk op dat in de definitie van pointer..:.max de parameter tab niet als pointer naar const werd gedeclareerd, terwijl dat in index_max wel het geval was. We kunnen de compiler de parameter tab alleen maar als pointer naar const laten aanvaarden als we ook van res een pointer naar const maken en dus ook de functiewaarde. In dat geval ziet de functie er uit zoals in Code 9.14. const int * pointer_max (const int *tab) { const int *res = tab++; while (*tab != 0) { if (*tab > *res) res = tab; tab++; }
return ·r es; }
Code 9.14: Functie met een pointer naar const als resultaat
Jammer genoeg moet nu ook p in het hoofdprogramma een pointer naar const worden, want anders weigert de compiler de toewijzing van pointer_max(a) aan p, en dan kunnen we in de laatste regel onmogelijk nog iets aan *P toewijzen. Het gebruik van pointers naar const is dus iets subtieler dan men. op het eerste gezicht zou verwachten. Een pointer kan niet alleen als resultaat van een functie gebruikt worden, maar ook als resultaat van een procedure, m.a.w., als (invoer-)uitvoerparameter. Hierbij gelden de gebruikelijke notatieregels: . plaats een &-teken vóór de naam van de pointer in de parameterlijst. De procedure diff uit Code 9.15 vergelijkt twee tabellen met elkaar en geeft pointers terug naar de eerste elementen waarin beide tabellen van elkaar verschillen. Merk op dat deze procedure niet veilig is. als de twee tabellen aan elkaar gelijk zijn! We hebben opnieuw geen pointers naar .const gebruikt, omdat dit meer ruimte biedt aan latere .toepassingen van deze procedure.
153
@Helga.Naessens@bogent. be
Hoofdstuk 9. Pointers
void diff (double *&p, double *&q) { while (*p == *q) { p++; q++; } }
Code 9.15: Pointers als (invoer-)uitvoerparameter Bemerk ook dat bij een oproep diff (a, b) van deze procedure, de aCtuele parameters a en b als pointervariabelen ('lvàlues') moeten gedeclareerd zijn, en dus geen tabelnamen mogen zijn. Tabelnamen kunnen weliswaar als pointers naar hun eerste element beschouwd worden, maar dat zijn constante pointerwaarden, geen variabelen.
9.8
De nuli-pointer
We wensen een functie te schrijven die een bepaald getal (verschillend van nul) opzoekt in een tabel met gehele getallen, afgesloten met nul. In ·de ·plaats van de positie van dit getàl terug te geven, moet de functie een pointer naar dit getal als waarde hebben. Wat doen we als het gezochte getal zich niet in de tabel bevindt? Als we met posities werken in plaats van met pointers, spreken we in dit geval meestal af dat we een negatieve index teruggeven, zodat het hoofdprogramma aan de waarde van functie kan zien dat het getal niet werd gevonden. Aangezien we nu echter geen positie, maar een pointer. als functiewaarde nodig hebben, zitten we met een probleem. Orri dit probleem op te lossen, voorziet C++ een speciale pointerwaarde, die we de nullpointer heten (met twee 'l'-en). Deze speciàle pointer kan je interpreteren àls een 'verwijzing naar nergens'. Hij mag nooit gederefereerd worden. Is p een pointervariabele, dan schrijf je p = 0 om er de nullpointer aan toe te wijzen, en dan gebruik je de uitdrukkingen p -- 0 en p ! = 0 om te kijken of p de nuli-pointeris of niet6 . . . . De functie die we zochten is weergegeven in Code 9.16, met detypische beknoptheid eigen aan C++. Het programmafragment uit Code 9.17 gebruikt deze functie om het getal 3 op te zoeken en door nul te vervangen (als het zich in de tabel bevindt). Bemerk dat 0 meerdere betekenissen kan hebben in C++! 6 Deze notaties wekken de indruk dat pointers eigenlijk gehele getallen zijn. Laat je echter niet misleiden, enkel het getal 0 kan aan een pointervariabele worden toegewezen, en zoals reeds. eerder gezegd, pointers kan je niet bij elkaar optellen of met elkaar vermenigvuldigen !
154
Hoofdstuk 9. Pointers
@Helga.Naessens@hogent. be
int * getalptr (int getal, int *tab) { while (*tab ! = 0 &&. *tab != getal) tab++; return *tab != 0 ? tab : 0; }
Code 9.16: Geeft een.pointer terug naar een bepaald getal in een tabel int a [80]; int *p; p = getalptr(3, a); i f (p != 0) *P = 0;
Code 9.17: Programmafragment maakt gebruikt van de functie getalptr
9.9
De operator new
Wanneer je tot nog toe in een programma eeh tabel gebruikte, was je steeds verplicht. het aantal elementen van die tabel op voorhand vast te leggen, desnoods door die tabel alvast 'groot genoeg' te kiezen. lil vele gevallen is dit een verspilling van computergeheugen, en soms bestaat het risico dat 'groot genoeg' toch nog niet gr.oot genoeg is. De operator new biedt hier in sommige gevallen een alternatief. Het resultaat van deze operator is een pointerwaarde die verwijst naar een nieuwe (anonieme) tabel die een opgegeven aantal elementen bevat. Bekijk de volgende voorbeelden: char *str = new char[e1J; double *t; int n,· *tab; t = new double[20]; cout << "Geef het aantal getallen"; cin >> n; tab= new int[n]; In de eerste lijn wordt de variabele str gedeclareerd en geïnitialiseerd met een verwijzing naar een nieuwe tabel van 81 karakters. Dit kan je een beètje vergelijken met de initialisatie van een stringvariabele met letterlijke tekst: die letterlijke tekst vertegenwoordigt ook een nieuwe tabel van karakters. Bij de operator new wordt · de· nieuwe tabel echter niet op voorhand met bepaalde karakters opgevuld. De operator new hoeft niet noodzakelijk in een initialisatie voor te komen, maar kan bijvoorbeeld ook in een toewijzing gebruikt worden .. In ons voorbeeld wordt aan t bijvoorbeeld een pointer naar een nieuwe tabel van 20 reëie getallen toegekend. Merk nogmaals
155
@Helga.Naessens@hogent. be
Hoofdstuk 9. Pointers
op dat de inhoud van die tabel niet nader bepaald is, en dus zeker niet uit enkel maar nullen bestaat. De laatste drie lijnen van het voorbeeld lezen een getal n in van het toetsenbord, creëren een naamloze tabel die precies dit aantal gehele getallen kan bevatten en plaatsen tenslotte een pointer naar die tabel in de variabele tab. Om een tabel aan te maken wordt het sleutelwoord new dus steeds gevolgd door de naam van een type met daarna tussen vierkante haakjes de grootte van de tabel (een geheel getal). Dit getal kan een constante zijn of een uitdrukking die nog door de computer moet worden uitgewerkt. De ·operator new kan ook gebruikt worden om pointers te creëren naar afzonderlijke elementen in plaats van naar volledige tabellen. In dit geval worden de vierkante haakjes en het aantal gewoon weggelaten. · Zoals we later zullen zien wordt dit voornamelijk gebruikt voor pointers naar structs (en objecten). complex *c = new complex; lijnstuk *l = new lijnstuk; Let op! Hoewel dit niet zo duidelijkblijkt uit de notatie, creëert new complex niet zomaar een nieuw complex getal, maar wel een pointerwaarde naar een complex getal. We zouden dus even goed kunnen schrijven: complex *C = new complex[1]; lijnstuk *1 = new lijnstuk[1];
9.10
De operator delete
Lokale variabelen in een · functie of procedure nemen slechts geheugen in beslag tijdens haar .uitvoering. Het geheugen van variabelen die met new gecreëerd werden blijft echter gereserveerd tot het einde van het programma. Tenzij de programmeur het. expliciet weer vrijgeeft. Dergelijkgeheugen wordt dan ook best vrijgegeven wanneer het niet langer nodig is. Enhet moet zeker vrijgegeven worden vooraleer het onbereikbaar wordt. (Wanneer de enige pointer die ernaar verwijst, dreigt te verdwijnen.) . Geheugen vrijgeven gebeurt met de operator delete, de tegenhanger van de operator new. Elke new wordt dus best vergezeld van een overeenkomstige delete. Let wel: enkel ·geheugen dat met new gereserveerd werd kan met delete worden vrijgegeven! Het spreekt · voor zich dat variabelen waarvan het geheugen werd vrijgegeven nadien niet meer mogen gebruikt worden . . Het resultaat van de operator new is een pointerwaarde die wijst naar het gegeven of de tabel die gecreëerd werd. De delete-operator moet vergezeld gaan van diezelfde pointerwaarde:
156
Hoofdstuk 9. Pointers
complex *C
@Helga.Naessens@hogent. be
new complex;
delete c;
Om het geheugen van een tabel vrij te geven moet men de operator delete []
gebruiken (met lege haakjes- de compiler weet immers hoe groot de aangemaakte tabel is):
char *str
~
new char[1001];
delete [] str;
//tabelvorm van de operator delete
157
Hoofdstuk 10 Toepassingen van pointers 10.1
C-strings
· Naast de stàndaardstrings kan men in C++ ook de strings gebruikèn van zijn voorganger C, de C-strings. In tegenstelling tot de standaardstrings, waarvan de klasse .in C++ gedefinieerd werd, behoren de C-strings tot C++ zelf (om compatibiliteitsredenen). Als we in dit hoofdstuk over strings spreken, bedoelen we een C-string. Stel dat we een woord op het scherm schrijven met de volgende procedure. Dit · woord wordt voorgesteld als (een pointer naar) een tabel van karakters, en het aantal karakters "'ordt meegegeven als een extra parameter: void schrijf (const char *woord, int n) { for (int i = 0 ; i < n ; i++) cout << (*woord++); }
Dit is weliswaar een zeer bruikbare procedure, maar ze heeft .als nadeel dat we van elk woord op voor hand moeten tellen hoeveelletters het bevat. Een alternatief is het volgende: we spreken af dat we elk ·woord laten volgen door een dollarteken wanneer we het opslaan in een tabel. (Een dollarteken omda:t dit een weinig gebruikt. symbool is.) Wanneer we dan het woord afdrukken, drukken we het dollarteken natuurlijk niet af. . Met deze conventies kunnen we de procedure schrijf herdefiniëren zoals in Code 10.1. void schrijf · (const char *woord) { while (*woord != '$') cout << (*woord++); Code 10.1: Schrijft een. woord dat afgesloten is met een dollartèken. op het sch~rm
158
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
Het belangrijkste nadeel van deze werkwijze is het feit dat we woorden of teksten waarin het dollarteken zelf voorkomt, niet met deze procedure kunnen afdrukken. Om deze reden heeft men in C/C++ het concept string ingevoerd. Een string is een pointer naar een tabel van karakters die afgesloten worden met een speciaal karakter dat het nuli-teken (of korter, de null) genoemd wordt. De null neemt hier dus de plaats in van de dollar uit ons voorbeeld. De inhoud van een string kan men noteren door letterlijke tekst tussen dubbele aanhalingstekens te plaatsen, het nuli-teken wordt in dat geval door de compiler automatisch achteraan toegevoegd. Er worden twee soorten variabelen gebruikt voor het opslaan van strings: ofwel een t abelvariabele waarvan de elementen van het type char zijn (deze tabel moet dan wel groot genoeg zijn om alle karakters van de string te bevatten, inclusief de null!) ofwel een pointervariabele die naar een karakter verwijst. Beschouw bijvoorbeeld de volgende declaraties en initialisaties: char s[4] char *t char *u
= "Jan"; s; "Piet" ;
Het resu~taat hiervan kunnen we schematisch voorstellen als volgt (de null wordt hier als een kruis getekend):
~~ ~ · .I })
' i ,.
L.:_
Zowel s, t àls u worden door C++ als strings beschouwd. Merk op dat u wel . degelijk verwijst naar het eerste element van een tabel van 5 karakters, maar dat in tegenstelling tot de tabel waart naar verwijst , deze tabel geen naam heeft. Deze anonieme tabel wordt automatisch door C++ gecreëerd. · Het nuli-teken kan je ook rechtstreeks in je programma gebruiken, een beetje op dezelfde manier als de nuli-pointer uit paragraaf 9.8. Wil je aan een variabele c van het type char het nuli-teken toew!jzen, dan schrijf je gewoon 'c = 0'. Wil je c met null :vergelijken, dan kan je 'c == O' of 'c != 0' schrijven. Passen we dit toe op de procedure schrijf (maar nu voor echte strings en niet voor dollarstrings) dan geeft dit de procedure uit Code 10.2.
void schrijf (const char *woord) { while (*woord != 0) cout << (*woord++); }
Code 10.2: Schrijft een string op het scherm
159
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
Om dan het woordje Hallo op het scherm te schrijven, gebruiken we bijvoorbeeld ·de volgende oproep: schrijf("Hallo\n"); In dit geval is bij het oproepen van schrijf de variabele woord een pointer naar een (anonieme) tabel van 7 karakters: de letters H, a, 1, 1 en .o,een linefeed en een null-teken. Merk op dat een oproep van schrijf hetzelfde doet als een schrijfopdracht met letterlijke tekst als enige argument. Ook omgekeerd kunnen we een willekeurige string gebruiken in een schrijfopdracht, dus niet alleen letterlijke tekst, maar ook variabelen zoals s, t en u hierboven. De opdrachten t = "Korneel"; cout .<< s << ", " << u << "
Joris en" << t << endl;
schrijven dus op het scherm Jan, Piet, Joris en Korneel Strings kunnen ook ingelezen worden met behulp van een leesopdracht. Is s een pointer naar een tabel van karakters, dan leest de opdracht 'cin >> s' een woord 1 in van het toetsenbord en stopt dit in de tabel waar s naar verwijst, m.a.w., het woord wordt letter voor letter in de tabel gestopt en aU:tomatisch afgesloten met een null-teken. Een dergelijke leesopdracht biedt jammer genoeg veel gelegenheid tot het maken van fou:ten. Beschouw het volgende fragment: char s [8] = "Martien"; char t [4] = "Pol"; ehar *U = "Ed"; char *V; cin >> s; cin >> t; cin >> u; cin >> v; Het effect van bovenstaande opdrachten wanneer we achtereenvolgens Jan, Piet, Joris en Korneel intikken, is geschetst in de volgende figuur: 1
In hoofdstuk 11 wordt uitgelegd wat er in deze context precies met woord bedoeld wordt. Gebruik deze leesopdracht voorlopig enkel wanneer de invoerlijn geen spaties bevat.
160
Hoofdstuk 10. Toepa.Ssingen van pointers
©Helga.Naessens@i:wgent. be
J IZ
4 . . JI. :. L._.J--L-1_ . .,_r:JZL~J._.
n
t l: l i l eGJX ~f
[3
.
~-J
t>'
.::_I
o
r ]
i
~
X
De eerste leesopdracht vormt geen enkel probleem. De drie karakters waaruit de naam Jan bestaat worden samen met een afsluitende null in de tabel s geplaatst. De tabel biedt weliswaar plaats voor nog vier karakters meer (in de figuur zijn de restanten van de initialisatie vans nog duidelijk te zien) maar dit speelt geen enkele rol bij het gebr~ik van s als string. Schrijven we later s uit met eeri schrijfopdracht, dan komt alleen Jan op het scherm. De andere drie leesopdrachten zijn echter wel foutief. De tabel t heeft bijvoorbeeld één plaats te weinig om de naam Piet te bevatten, en de eind-null komt buiten de tabel terecht (meestal in een nietsvermoedende variabele). Hetzelfde gebeurt bij u, want de anonieme tabel waar eerst de naam Ed in zat, biedt slechts plaats voor drie tekens, en het gaat helemaal verkeerd bij laatste opdracht, omdat de pointer v nu niet eens geïnitialiseerd is! Wanheer je iets inleest in een variabele van het type char, int of double, dan hoeft die variabele helemaal niet geïnitialiseerd te zijn, en in de meeste gevallen is ze dat ook niet. In dit geval echter kan .de niet geïnitiall.seerde pointer werkelijk overal naar toe wijzen, met meestal algemene chaos tot gevolg. Zoals bove:qstaande voorbeelden aantonen, is het van het allergrootste belang dat een stringvariabele die gebruikt wordt in een leesopdracht een pointer is die naar een welgedefinieerde tabel verwijst die bovendien groot genoegis 2 !
10.1;1
Bewerkingen met strings
Kan je twee strings met elkaar vergelijken met behulp van de gewone operator '=='? Natuurlijk, want strings zijn pointers en pointers kan je met elkaar vergelijken. Dit is echter slechts een half antwoord, want met deze redenering zullen s en t in onderstaande figuur wel als gelijk beschouwd worden (want het zijn pointers naar hetzelfde t11belelement) terwijl u en s verschillend zijn. · 2
En daar wringt het schoentje: hoe groot is groot genoeg? Zelfs als je een tabel van 1000 karakters voorziet, is er niets (behalve luiheid) dat de gebruiker er van weerhoudt om er 2000 in te tikken. We komen hier later op terug.
161
Hoofdstuk 10. Toepassingen van pointers
t
u
@Helga.Naessens@hogent. be
G/ D 1=-::J
I C?<J
.:J
1.?
J
I 102!JXJ
,
n
(),
Als je u en s wel als gelijk willen herkennen (wat in de meeste toepassingen het g~val is), kan je de gewone gelijkheid niet gebruiken, maar zal je een eigen (logische) functie moeten schrijven. Deze functie zal de twee strings teken per teken moeten vergelijken en bovendien ook rekening moeten houden met hun lengte. Jan en Jane zijn niet dezelfde namen! Deze functie ·kan er bijvoorbeeld uitzien zoals in Code 10.3 ..
bool is_zelfde_string (const char *s, const char while (*s ·!= 0 && *S == *t) {
*t) {
s++; t++; }
return (*s == *t); }
Code 10.3: Gaat na of twee strings aan elkaar gelijk zijn
De functie bevat een lus met een dubbele conditie. De lus stopt ofwel als *s null wordt, met andere woorden als het einde van de eerste string bereikt werd, ofwel als de overeenkomstige letters i!l beide strings verschillend zijn, ofwel als beide condities zich tegelijkertijd voordoen. Daarna geeft de functie de waarde true terug als de laatst bekeken waarden in beide strings gelijk zijn, en anders false~ · Het is een goede oefening om eens na te gaan waarom deze functie inderdaad in alle gevallen haar werkdoet! Het is een kleine moeite om deze functie uit te breiden zodat ze niet alleen nagaat of twee strings al dan niet gelijk zijn, maar ook meteen aangeeft welke string alfabetisch vóór de andere komt 3 . Merk op dat, net zoals bij de gelijkheid, de operatoren <en> enkel pointers vergelijken en niet de strings zelf en in dit geval dus zo goed als waardeloos zijn. De funètie strcmp uit Code 10.4 geeft -1 terug als haar eerste argument eerder in het alfabet komt dan het tweede, 0 als ze gelijk .zijn en ·1 als de eerste string later komt dan de tweede.
3
Je hoeft alleen de definitie van ·d e functie strcmp grondig te bestuderen om te weten wat er precies met alfabetische volgorde van strings bedoeld wordt. Toch enkele voorbeelden: 'aap' komt voor 'mens', 'mens' komt voor 'menselijk' en 'appel' koq~.t voor 'peer'. Aangezien het begrip alfabetische volgorde bij strings afgeleid· is van de volgorde ïn het gebruikte computeralfabet, kunnen we niet in het algemeen zeggen of 'mens' dan wel vóór of na 'Mens' komt. Ze zijn in ieder geval niet aan elkaar gelijk.
162
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
int strcmp. (const char *S, const char *t) { while (*s != 0 && *s == *t) { s++; t++; }
if (*s == *t) return 0; · else if (*s == 0 I I (*t ! = 0 && *S < *t)) return -1; el se return 1; }
Code 10.4: Gaat na welke string alfabetisch vóór de andere string komt Merk op dat we in de .voorwaarde vart de else if-opdracht de null-tekens afzonderlijk behandelen. In sommige computeralfabetten is het null-teken weliswaar het eerste teken en dus kleiner dan elk ander teken, maar dat is niet altijd zo4 ! Nuttige en dus vaak gebruikte functies en procedures voor C-strings, zoals strcmp, zijn terug te vinden in de bibliotheek . Een andere van die stringfuncties is bijvoorbeeld strlen die de lengte van .een string bepaalt (d.i., het aantal karakters in de string, ei~d-null niet meegerekend). Deze functie zou je kunnen programmeren zoals in Code 10.5: . int strlen (const char *s) { int i = 0; while (*s != 0) { i++; s++; }
return i; }
Code 10.5:· Bepaalt de lengte van een string
Indien we deze functie strlen oproepen met als argument (een pointer naar) een tabel die als eerste karakter al meteen het nuli-teken bevat, dan is het resultaat 0. Een dergelijke string noemen we een ledige string. Een ledige string wordt voorgesteld als 11 11 • Verwar niet met de null-pointer. Een vaak gebruikte stringprocedure is strcpy, die weergegeven is in Code 10.6 en gebruikt wordt om een volledige string souree teken voor teken in een tabel dest te copiëren. Je moet er steeds voor zorgen dat dest een voldoende grote tabel is, of wijst naar een tabel 4 In het alfabet van de meeste PC's komt de null vóór alle gewone karakters, maar de geaccentueerde · tekens komen vóór de null.
163
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naesse~@hogent. be
van voldoende grootte. Let op! Het eerste argument is dest en niet souree zoals je misschien zou verwachten. void strcpy (char *dest , const char *source) { while (*source != 0) *de st++ *source++; //het afsluitend nullteken *dest = 0; }
Code 10.6: Copieert de string souree naar dest
De functie concat uit Code 10.7 maakt een nieuwe string aan door twee gegeven strings achter elka,ar te plaatsen. De nieuwe string wordt voorgesteld als een pointer. naar een nieuwe tabel, en teruggegeven· als functieresultaat. char * concat (const char *str1, const char *str2) { int 11 = strlen(str1); int 12 = strlen(str2); char *res. = new char[l1 + 12 + 1]; //Waarom+ 1 ? strcpy(res, str1); strcpy(res + 11, str2); return res; }
Code 10.7: Creëert een nieuwe string door twee strings te cohcateneren
Er bestaat een bibliotheekfunctie strcat die bijna 'hetzelfde doet. Het grote verschil is echter dat bij strcat de tweede string in dezelfde tabel als de eerste string wordt geplaatst en dat de geb~uiker er met andere woorden dus zelf moet voor zorgen dat er in die tabel plaats genoeg is. De functie concat daarentegen creëert een nieuwe tabel van karakters die precies groot genoeg is om het eindresultaat te bevatten.
10.1.2
Tabellen van strings en pointers naar strings
Strings zijn volwaardige gegevenstypes en kunnen dan ook in tabellen worden geplaatst. De enige moeilijkheid bij dergelijke tabellen is wellicht de manier waarop ze gedeclareerd worden. Beschouw als voorbeeld de procedure uit Code 10.8 die een gegeven datum afdrukt op het scherm. In deze procedure wordt eerst een tabel maandnaam van 13 strings gedeclareerd en onmiddellijk geïnitialiseerd. Merk op dat we 13 elementen gebruiken en geen 12 omdat tabellen in C++ steeds vanaf nul genummerd worde~ 5 . Het eerste element van die tabel wordt 5
We hadden natuurlijk kunnen volstaan met een tabel van 12 elementen en in de laatste lijn maand-l
164
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
void sch:djf_datum (int dag, int maand~ int jaar) { char ·*maandnaam[13] = {0, "januari"; "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober" , "november", "december"}; cout << dag << ' ' << maandnaam[maand] << ' ' << jaar << endl; }
Code 10.8: Drukt een datum op het scherm dus eigenlijk niet gebruikt en daarom gaven we het de nuli-pointer als waarde (en niet het getal 0, want dat is niet van het juiste type), we hadden evengoed een lege string kunnen gebruiken, maar dan hadden we iets meer computergeheugen verbruikt ..
u i o r i ~~~--~---L---1~·--~--~--~ r
J
u
m
b
(De nuli-pointer hebben we met een groot kruis voorgesteld, het nuli-teken als een klein kruis.) De declaratie van een tabel van strings combineert dus het sterretje van de stringdeclaratie en de vierkante haakjes van de tabeldeclaratie6 . Wanneer we een stringtabel willen gebruiken als parameter van een functie of procedure, dan hebben we een pointer naar een string nodig. Een dergelijke pointer wordt genOteerd met twee sterretjes, zoals in het volgende voorbeeld. De functie eerste_string uit Code 10.9 zoekt het alfabetisch eerste woord in een tabel van woorden. Het einde van de tabel is aangegeven door een null-pointer. Merk op dat deze functie gewoon de zoveelste variant is van de bekende minimum-functie, maar dit keer toegepast op strings. Let op, om twee strings alfabetisch met elkaar te vergelijken gebruik je strcmp en niet < ! als index kunnen gebruiken, maar dit had ons een mooie kans ontnomen om nog eens een nuli-pointer te gebruiken. 6 Een declaratie van de vorm 'char (*k)[13]' bestaat ook. Deze declareert een pointer k naar een volledige tabel van 13 karakters. Maar we hadden b.eloofd om over dergelijke pointers te zwijgen . . .
165
@Helga.Nae~sens@hogent. be
Hoofdstuk 10. Toepassingen van pointers
char * eerste_string (char **tab) { char *min = *tab++; while (*tab != 0) { if (strcmp(*tab, min) ~ 0) min tab++;
*tab;
}
return min; }
Code 10.9: Zoekt het alfabetisch eerste woord in een tabel van woorden
10.1.3
argc en argv
Tot nu toe hebben we de functie main leren kennen als de hoofdfunctie in C++ waarvan de hoofding de gedaante 'int mainO '.heeft. Indien jouw programmacode bijvoorbeeld opgeslagen is in het bestand met bestandsnaam 'Test.cpp', dan bekomen we na een succesvolle compilatie het uitvoerbaar programma 'Test.exe'. Dit programma kan dan uitgevoerd worden door Test te schrijven -achter de 'prompt' van de opdrachtregeL Net als vele andere procedures en functies, kan je de procedure main echter ook voorzien van formele parameters. Bij het aanroepen van het programma kan je dan aan de programmanaam zoveel strings toevoegen als je maar wil, bijvoorbeeld: Test Jan Piet Joris Corneel De procedure main ontvangt deze strings via twee formele parameters in de parameterlijst. Over het algemeen wordt uit historische en praktische overwegingen aan dèze parameters steeds dezelfde namen gegeven, nl. argc (argument count) ep argv (argument values). Schematisch ziet de procedure main waaraan parameters worden doorgegeven er als volgt uit: int main(int argc, char **argv) { }
De parameter argc is van het type int en bevat het aantal strings op de opdrachtregel, waarbij de programmanaam meegeteld wordt. In het bovenstaande voorbeeld bezit argc de waarde 5 aangezien de opdrachtregel 5 strings vermeldt, nl. de programmanaam "Test" en de 4 voornamen "Jan", "Piet", "Joris" en "Corneel". De parameter argV" is een stringtabel en bevat alle strings die vermeld staan op de opdrachtregel, inclusief de programmanaam. Deze stringtabel wordt bovendien afgesloten met een nullpointer. In het bovenstaande voorbeeld bevat de stringtabel 5 strings, nl. "Test", "Jan", "Piet", "Joris" en "Corne~l".
166
@Helga.Naessens@hogent. be
Hoofdstuk 10. Toepassingen van pointers
De programmanaam is altijd de eerste stfing die in de stringtabel argv voorkomt en is dus steeds opgenomen in argv [0] . En aangezien we een programma niet kunnen oproepen zonder programmanaam, is de waarde van argc daatgm altijd ten minste 1. Merk tevens op dat de afsluitende nullpointer hierbij nooit meegeteld wordt. Code 10.10 is een eenvoudig programmafragment waarbij gebruik gemaakt wordt van de parameters argc en argv. Dit programmafragment laat de gebruiker toe om bij de aanroep van het programma op de commandolijn een extra string toe te voegen (na de programmanaam). Deze string bevat de naam van het bestand dat moet ingelezen worden. Indien de gebruiker geen extra string toevoegt (in dit geval heeft argc de' waarde 1), wordt de naam van het in te lezen bestand tijdens de uitvoering vari het programma aan de gebruiker gevraagd. int main(int argc, char **argv) { ifstream inv; char s[81]; if (argc == 1) { cout << 11 Geef de naam in van het in te lezen bestand: cin >> s;
11
;
}
else { cout <<
Het bestand 11 << argv [1] << 11 Zal ingelezen worden. 11 << endl; strcpy(s, argvU]) 11
}
inv. open(s); }
Code 10.10: Maakt gebruik van argc en argv
10.2
Gelinkte lijsten
De operator new kan nuttig gebruikt worden in een aantal gevallen waarbij we de grootte van een tabel niet op voorhand kunnen vastleggen, máar lang niet altijd! Beschouw bijvoorbeeld de volgende opgave: lees een rij gehele getallen in die wordt afgesloten met een 0, en bepaal hoeveel van die getallen er groter zijn dan het gemiddelde van die getallen. · Het is duidelijk dat we dit niet kunnen doen zonder al die getallen afzonderlijk op te slaan, en als we hiervoor een tabel willen gebruiken (wat nogal voor de hand ligt) stelt zich de vraag hoe groot die tabel dan wel moet zijn. Kiezen we voor een grote tabel (van bijvoorbeeld 1000 elementen) dan wordt er nogal kwistig omgesprongen met geheugenruimte. Als we echter een kleine tabel kiezen bestaat er een kans dat de gebruiker meer dan het voorziene aantal getallen intikt .. Om dit probleem op te lossen gebruiken we een nieuw soort gegevensstructuur die er
167
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
helemaal anders uitziet dan een tabel; namelijk een zogenaamde gelinkte lijst 7 • De meest eenvoudige manier om met dit nieuwe gegevenstype kennis te maken is via een tekening; onderstaande figuur stelt een lijst voor met daarin de ·gehele getallen 1, 2, 3 en 4 (in die volgorde). f
j
.
t......"· - ·-'
- 1 ~.I. J
~...._:_ ; .l----_,: ~
tt
><1
Een gelinkte lijst bestaat uit een aantal knopen die naar elkaar verwijzen. Elke knoop bestaat uit twee delen: .een inhoud (een geheel getal in dit geval) en een verwijzing naar een andere knoop. De laatste knoop verwijst nergens naar, en de variabele l die de lijst bevat, verwijst· naar de eerste knoop. In C++-terminologie hebben we dus een variabele 1 die een pointer bevat naar een struct met daarin twee velden. Het eerste veld is een geheel getal en het tweede veld is een pointer die op zijn beurt naar een tweede struct verwijst die dezelfde structuur heeft. Wanneer we achtereenvolgens de verschillende verwijzingen doorlopen ontmoeten we alle elementen van de lijst~ Het pointerveld van de laatste knoop bevat altijd de null-pointer~ Dezelfde lijst kan trouwens op een andere manier worden getekend. Alleen de volgorde van de knopen is immers van belang:
We kunnen de variabele 1 als volgt declareren8 : . struct Lijstknoop { int getal; Lijstknoop *next; } ;
Lijstknoop *1; Met deze definities wordt het eerste getal uit de lijst 1 aangeduid met '(*1) .getal', of beter nog, met '1->getal'. Zoalsje in onderstaande figuur kunt nagaan, heet het vierde getal uit de lijst '1->next->next->next->getal'. 7
Opnieuw een Engelse term die geen goede Nederlandse vertaling heeft. De naam van de struct lijstknoop komt hier voor als onderdeel van zijn l':)igen definitie. Er zijn weinig situaties waarin C++ dit tolereert, maar dit is er één van. 8
168
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
gdal
l~!_j" ~..r. 2 ~~-
;f.....
10.2.1
lli!U
t .fl"a,f
~-
•
l .~ .J--o{ 3 l 3-
Opbouwen van lijsten
Het vergt zeker enige ervaring om gemakkelijk met gelinkte lijsten om te gaan. Daarom bestaat de rest van dit hoofdstuk voornamelijk uit voorbeelden._ In het begin loont het trouwens de moeite om de verschillende lijsten die we zullen ontmoeten op papier uit te tekenen. We beginnen met een eenvoudig voorbeeld: lees een reeks gehele getallen in, afgesloten met een nul, en bouw een gelinkte lijst op die deze getallen als elementen bevat. We doen dit zoals in Code 10.11. Lijstknoop *1, *h; int getal; 1
= 0;
cin » getal; while (getal ! = 0) { h = new Lijstknoop; h->getal = getal; h->next = 1; 1
= h;
cin >> getal; }
Code 10.11: Vooraan toevoegen
We maken hier gebruik van een aantal C++-opdrachten waarmee je nog niet goed vertrouwd bent. De initializatie '1 = 0' bijvoorbeeld, stopt niet het getal nul maar de nulipointer in 1, want 1 is een pointervariabele. De operator new in de while-lus creëert een pointer naar een nieuwe lijstknoop en stopt' die in een hulpvariabele h. De twee opdrachten daarachter vullen dan de velden van die lijstknoop op. Om dit programmafragment goed te begrijpen moet je het stap voor stap uitvoeren en een schets maken van alle pointers en lijstknopen die telkens veranderen. De volgende vier figuren tonen wat er gebeurt wanneer we achtereenvolgens de getallen 12, -5, 99 en 0 intikken. We schetsen de situatiè telkens vóór er een nieuw getal wordt ingelezen.
169
@Helga.Naessens@hogent. be
Hoofdstuk 10. Toepassingen van pointers
zX
--o-1.' -tl -2···· 1"'·><1;
/q~
_;\..:::::.~
!.
12IX !l --·~~ -.5 r l-1
Zoals je kunt zien aan de figuur, bevat de variabele 1 uiteindelijk eenlijst met daarin de getallen 99~ -5 en 12: de juiste getallen maar wel in omgekeerde volgorde. Je kan een lijst ook in de goeie volgorde opbouwen, maar dan wordt het programma meer ingewikkeld. De wijze waarop dit" gebeurt is weergegeven in Code 10.12.
Lijstknoop *1, *h; int getal; cin » getal; if (getal == 0) 1
= 0;
else { 1 new Lijstknoop; h
1;
= getal; cin » getal; while (getal ! = 0) { h->next = Iiew Lijstknoop; h = h->next; h->getal = getal; cin » getal; h-~getal
}
h->next
= 0;
}
Code 10.12: Achteraan toevoegen
Bij dit fragment hoort de volgende figuur:
, _
,....--··- --.---.
~-l
l
h -./~
·i> 1.2
Merk op dat de null-pointer in de achterste lijstknoop pas opgevuld wordt ná de while-lus! Deze twee voorbeelden illustreren een belangrijke eigenschap van gelinkte lijsten: de meest 170
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
eenvoudige manier om een lijst op te. bouwen is van achter naar voor! In beide voorbeelden zall de nuli-pointer bevatten wanneer er als eerste getal onmiddellijk 0 wordt ingegeven. Een lege lijst wordt dus steeds voorgesteld als een null.:pointer.
10.2.2
Overlopen vari een lijst
Eenmaal een gelinkte lijst geconstrueerd is, moeten we et ook nog iets mee doen. De procedure uit Code 10.13 drukt bijvoorbeeld alle elementen af uit een gegeven lijst. void druklijst (const Lijstknoop *1) { while (1 != 0) { cout « 1->getal « endl; 1 = 1->next; } }
Code 10.13: Drukt alle elementen uit een gegeven gelinkte lijst l af
De vergelijking '1 ! = O' controleert of 1 niet de nuli-pointer is. We raden je ten zeerste aan om deze procedure eens stap voor stap te overlopen en er de corresponderende tekening bij te maken. Om een bepaald element in een lijst terug te vinden, moet je alle lijstknopen die vóór dit element staan passeren. Het zoeken naar het i-de element van ee~ lijst (i ;:::: 1) zoals in Code 10.14 verloopt dus veel trager dan het bepalen van het i-de element uit een tabel. int element (const Lijstknoop *1, int i) { for (int t = 1 ; t < i ; t++) 1 = 1->next; return 1->getal; }
Code 10.14: Bepaalt het i-de element in een gegeven gelinkte lijst l
Merk op dat er zich grote problemen zullen voordoen wanneer de index i buiten de lijst valt! We geven nog een voorbeeld dat zowel het aflopen als het creëren van een lijst combineert. De functie uit Code 10.15 maakt een lijst die dezelfde elementen bevat als een gegeven lijst, maar dan in omgekeerde volgorde. Het is leerzaam om de definitie van deze functie eens te vergelijken met de procedure druklij st en met het eerste programmafragment uit paragraaf 10.2.1.
171
Hoofdstuk 10.- Toepassingen van pointers
@Helga.Naessens@hogent. be
Lijstknoop * omgekeerde (const Lijstknoop *1) { Lijstknoop *r, *h; r
= 0;
whi1e (1 != 0) { h = new Lijstknoop; h->geta1 = F->geta1; h->next = r;
= h; 1 = 1->next;
r }
return r; }
Code 10.15: Creëert een omgekeerde gelinkte lijst
10.2.3
Elementen toevoegen aan een lijst
Zij 1 een gegeven lijst en stel dat we aan die lijst het getal x willen toevoegen. Vooraan toevoegen aan de lijst is het eenvoudigst en is weergegeven in Code 10.16. h = new Lijstknoop; h->geta1 = x; h->next = 1; 1
= h; Code 10.16: Voegt vooraan een getal x toe aan de gelinkte lijst l
Om het getal achteraan toe te voegen, inoeten we eerst de achterste lijstknoop opzoeken en daar het next-veld naar een nieuwe lijstknoop laten verwijzen. De manier waarop dit gebeurt, is weergegeven in Code 10.17. h
= 1;
. while (h->next ! = 0) h = h->next; h->next = new Lijstknoop; h = h->next; h->getal = x; h->next = 0; Code 10.17: Voegt achteraan .een getal x toe aan de gelinkte lijst l
Merk op dat dit voorbeeld niet werkt wanneer de oorspronkelijke lijst 1leeg is. Soms moeten we het getal ook ergens tussenin toevoegen. Het volgende voorbeeld voegt een getal x toe aan een lijst 1 waarvan de elementen in stijgende volgorde zijn geordend. 172
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
Het is de bedoeling dat x op een zodanige plaats wordt tussengevoegd dat de nieuwe lijst nog altijd geordend is. We zoeken eerst de knoop waarachter het getal zal moeten worden ingevoegd: h
= 1;
while
(h~>next->getal
< x) h = h->next;
Het getal x moet dan ingevoegd worden tussen knoop *hen knoop * (h->nèxt):
=
m h->next; //mis een tweede hulplijst h->next = new Lijstknoop; h = h->next; h->getal = x; h'->next = m; Op onderstaande figuur zie je wat het resultaat is van die bewerkingen:
~.
..
b ----
·~ "
"
~I_J."L~J / '
/
.
. I
( -'
!
~~L2J
V ûtll'
Er zijn een aantal speciale gevallen waar we geen rekening mee hebben gehouden: wanneer x bijvoorbeeld groter is dan alle getallen in de lijst, dan loopt er iets mis met de while-lus die dient oin h te bepalen. Dit kan opgelost worden door deze lus ook te doen stoppen wanneer h->next null wordt. Controleer zelf dat ook in dit geval de invoeging van x nog op dezelfde manier kan gebeuren. Wanneer de oorspronkelijke lijst 1 leeg is, helpt zelfs deze correctie niet. We moeten dit geval apart behandelen. Uiteindelijk krijgen we het programmafragment uit Code 10.18.
10.2.4
Een element uit een lijst verwijderen
· Tot slot tonen we nog hoe een gegeven getal x uit een lijst 1 kan verwijderd worden. Wanneer het getal x zich helemaal vooraan in de lijst bevindt, dan is het eenvoudig om de corresponderende lijstknoop te verwijderen: we schrijven gewoon '1 = 1->nèxt; '. Het is echter niet zo gemakkelijk om een lijstknoop te verwijderen die zich middenin of achteraan een lijst bevindt. Een knoop verwijderen betekent immers de pointer aanpassen in het next-veld van de knoop die vóór de gegeven knoop in de lijst komt. We zullen dus een pointer nodig hebben naar de knoop vóór de gegeven knoop, in de plaats van naar die knoop zelf.
173
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
if (1 != 0 && x > 1->geta1) {
= 1;
h
while (h->next ! = 0 && h->next->geta1 < x) h = h->next; m = h->next; h->next = new Lijstknoop; h = h->next; h->geta1 x; h->next = m; }
e1se { h = new Lijstknoop; h-:>geta1 x; h->next == 1;
= h;
1 }
Code 10.18: Voegt een getal x tussenin toe aan de· gelinkte lijst l Je kan hiervoor de volgende while-lus gebruiken: h
= 1;
while (h->next != 0 && h->next->geta1 != x) h = h->next; Op het einde van deze lus wijst h->next ofwel naar een knoop die het getal x bevat, ofwel is h->next de null-pointer. Let op, we bedoelen niet de knoop die het veld h->next bevat, maar wel het ding waar de inhoud van dat veld naar verwijst! h->next is ee~ nuli-pointer wanneer x het eerste element van de lijst is, of wanneer x zich niet in de lijst bevindt. We zullen deze twee gevallen dus afzonderlijk moeten behandelen. Bovendien werkt de for-lus niet wanneer de oorspronkelijke lijst 1leeg is! Eenmaal je h hebt gevonden, kan je de knoop verwijderen met de eenvoudige toewijzing 'h->next = h->next->next; '. In de volgende figuur zie je hiervan het resultaat: .
.
· :·
h ____/
.....----.---,
! ,- t- -c.J
~--· ·,---,
/~
.....----.,----,
I~
.~:
'~--------------J~I
--· i .. >;
Dezelfde toewijzing werkt ook in het geval dat h wijst naar de voorlaatste knoop van de lijst, ook al is de figuur dan niet van toepassing. Wanneer we rekening gehouden met alle speciale gevallen en er bovendien voor zorgen dat
174
Hoofdstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
we 1 ongemoeid laten waimeer de gegeven x zich niet in die lijst bevindt, dan krijgen we uiteindelijk Code 10.19 als resultaat.
if (1 != 0) { if (1->geta1 x) 1 1->next; e1se { h
=
1;
.
while (h->next != 0 && h->next->geta1 != x) h = h->next; i f (h->next · ! = 0) h->next = h->next->next; } }
Code 10.19: Verwijdert een getal x uit de gelinkte lijst l
Nadat we in bovenstaand voorbeeld de lijstknoop hebben verwijderd, is er geen enkele variabele meer die naar deze lijstknoop verwijst. De geheugenplaats die door deze knoop wordt ingenomen mag dus gerust weer door de computer worden gebruikt om bijvoorbeeld later een andere lijstknoop in op te slaan. Eri als dat niet gebeurt bij intens gebruik van lijsten, kan het geheugen vol komen te zitten van onbereikbare knopen. We hebben dus opnieuw de operator de1ete nodig, zoals in het programmafragment van Code 10.20.
if
(1 != 0) { if (1->geta1
== x) { = 1; 1 = 1->next;
m
de1ete m; }
e1se {
h = l; whïle (h->next != 0 && h->next->geta1 != x) h = h->next; if (h->next != 0) { m = h->next; h->next = m->next; de1ete m; } } }
Code 10.20: Verwijdert een getal x uit de gelinkte lijst l en vernietigt de knoop
175
Hoofçlstuk 10. Toepassingen van pointers
@Helga.Naessens@hogent. be
We hebben hier een extra lijstvariabele m nodig, precies omdat we een struct die reeds ge'deleted' is, niet meer mogen gebruiken. Schrijf dus niet:
delete 1; 1
= 1->next; // FOUT
delete h->next; h->next
10.2.5
h->next->ne~t;
//FOUT
Definitie van nieuwe types
Er bestaan verschillende mogelijkheden om in C++ nieuwe types te introduceren waarvan de naam op dezelfde manier kan gebruikt worden als de klassieke types int, double, bool en ehar. We hebben vroeger reeds gezien dat een structdefinitie een dergelijk nieuw type definieert. Een andere manier om dit te doe.n is via een rechtstreekse typedefinitie van de völgende vorm: typedef ehar karakter; typedef double tabel3[3]; typedef ehar *estring ; Hier worden drie nieuwe types gedefinieerd: het type karakter, dat gewoon een synoniem is voor ehar, het type tabel3 dat een tabel van 3 reële getallen voorstelt en het type estring dat een afkorting wordt voor 'pointer naar ehar' (C-string dus). . Dergelijke typedefinities bevinden zich bovenaan het programma, waar ook de structdefinities zich bevinden. Een typedefinitie begint steeds met het sleutelwoord typedef en ziet er verder uit als een declaratie van een variabele. Laat je echter niet door deze gelijkenis in vorm misleiden! De nieuwe namen karakter, tabel3 en estring zijn een soort afkortingen voor langere type-aanduidingen, en geen variabelen. Met eeri type dat je in een typedefinitie hebt geïntroduceerd, kan je dan wel later in je programma nie~we variabelen declareren, bijvoorbeeld als volgt: karakter eh; estring s, *q; tabel3 v, w[5]; Dit heeft precies dezelfde betekenis (maar is wellicht iets duidelijker) als ehar eh; ehar *s, **t; double v[3], w[5] [3]; (Let speciaal op de declaratie van w!)
176
@Helga.Naessens@hogent. be
Hoofdstuk.lO. Toepassingen van pointers
Ook de definitie van een gelinkte lijst wordt in de praktijk vaak uitgedrukt met behulp van een typedefinit!e, zodat deze er meestal als volgt uitziet: struct Lijstknoop { int getal; Lijstknoop *next; };
typedef Lijstknoop *Lijst; Als er dan in cie.loopvan het programma gebruik willen maken van een gelinkte lijst 1, dan mogen we kortweg schrijven 'Lijst 1', Ook bijvoorbeeld de functie omgekeerde uit Code 10.15 kan nu herschreven worden naar de functie uit Code 10.21. Lij st omgekeerde (Lij st 1) { Lijst r = 0, h; while (1 != 0) { h = new Lijstknoop; h->getal = 1->getal; h->next = r; r = h;
1
= 1->next;
}
return r;
} Code 10.21: Creëert een omgekeerde gelinkte lijst
Dikwijls gebruikt men ook de volgende definitie van een gelinkte lijst 9 : struct Lijstknoop; typedef Lijstknoop *Lijst; struct Lijstknoop { int getal; Lijst next; };
Bij deze definitie lijkt het alsof een lijst uit twee delen bestaat: een geheel getal en een andere lijst. In de praktijk blijkt deze vreemde interpretatie zelfs nog niet eens zo gek. Ze laat toe om enigszins abstractie te maken van de pointers die overal opduiken. 9
0pnieuw wordt Lijstknoop hier gebruikt vóór zijn definitie, maar ook in dit geval knijpt C++ een oogje dicht, op voorwaarde dat we alvast laten weten dat Lijstknoop een struct zal zijn.
177
Hoofdstuk 10. Toepassingen van pointers
10.2.6
©Helga.Naessens@hogent. be
Pointers en objecten
Net zoals structs kunnen (naamloze) objecten ook via de operator new gecreëerd worden. Daarbij wordt hun constructor opgeroepen (eventueel de default constructor): #include <string> using std: :string; int main() { ·string *pi new string; // default eenstructor (ledige string) string *p2 ·= new string("informatica"); // andere eenstructor }
En zoals bij structs kan de notatie -> gebruikt worden om een lidfunctie via de teruggegeven pointer op te roepen: · cout << p2->size () << endl; Objecten kunnen tabellen als dataleden bevatten. Wanneer men de tabelgrootte opgeeft in hun klassedefinitie, heeft elk object een tabel met dezelfde grootte, wat meestal ongewenst is. (Een ruim bemeten tàbel kan in vele gevallen te groot zijn, maar soms toch nog te klein uitvallen.) In die gevallen zal men als datalid een pointer voorzien, die naar een tabel van de gepaste grootte verwijst. Het is dan de taak van de constructor om die pointer een waarde te geven, en dus een tabel te creëren via new. Laten we zelf eens een eenvoudige klasse maken die strings kan voorstellen. Als dataleden voorzien we een geheel getal voor de stringlengte, en een pointer naar een tabel met de karakters. Er wordt dus geen afsluitend nulkarakter gebruikt: class String { public: String(const char* s = ""); //doet ook diènst als default private: int lengte; char* karptr; };
De constructor kan er uitzien zoals in Code 10.22.
178
@[email protected]
Hoofdstrik 10. Toepassingen van pointers
#include String: :String(const char* s) { lengte= strlen(s); karptr = · new char[lengte]; for(int i = 0 ; i < lengte i++) karptr[i] = s[i] ; }
Code 10.22: Constructoren default constructor Een dergelijk object als lokale variabele in een functie of procedure, verdwijnt wanneer deze uitgevoerd is. Daarmee •verdwijnt de pointer die naar de karaktertabel wijst, maar de tabel zelf blijft bestaan, en wordt onbereikbaar. Ze moet dus eerst vrijgegeven worden met delete. Wanneer een object verdwijnt, roept de compiler steeds een destructor op. Tot nog toe was dat een default destructor, die niets speciaals deed. Maar nu moeten we zelf een destructor voorzien. · Die draagt de naam · van de klassè, voorafgegaan .door ~en tilde •-•, en heeft geen parameters. (In tegenstelling tot een constructor kan er dus maar één destructor zijn.) Voor onze klasse ziet die er uit zoals in Code 10.23. Bemerk dat enkel geheugen dat met new .werd aangemaakt moet vrijgegeven worden. class String { public: String(const char* s = ""); // constructoren -string(); //destructor private: int lengte; char* karptr; };
String::-string() { delete[] karptr; }
Code 10.23: Destrm;tor
Een pointer als dataiid kan voor nog meer problemen zqrgen. Immers, wanneer we een dergelijk object copiëren. naar' een ander object, wordèn alle dataleden gecopieerd, en dus ook de pointer. Dat betekent dat er nadien twee objecten zijn die naar dezelfde karaktertabel verwijzen! Als we dus iets wijzigen aan de karakters van een van die objecten, wijzigen meteen ook die van het andere object, wat zelden de bedoeling is. Erger nog, wanneer de destructor van het ene object opgeroepen wordt, heeft het tweede object geen tabel meer! Bij het copiëren van objecten moeteil dus niet enkel de dataleden gecopieerd worden, maar ook de gegevens naar waar ze eventueel verwijzen ( deep copy).
179
Hoofdstuk 10. Toepassingen
van pointers
@Helga.Naessens@hogent. be
Dat betekent dat zowel de copy-constructor als de toewijzingsoperatie, die steeds default aanwezig zijn, niet meer naar behoren werken. Zelf een copy-constructor schrijven is vrij eenvoudig, zie Code 10.2.4. Zoals reeds vroeger gezegd, moet de parameter nu zeker een referentieparameter zijn, want een invoerparameter wordt gecopieerd, en net dat werkt niet correct. De functie van een operàtor zoals = wijzigen is wat ingewikkelder, en valt buiten het opzet van deze inleidende cursus.
class String { public: String(const char* s = 1111 ) ; / / constructoren String(const String & s); 11 copy constructor -string(); //destructor private: int lengte; char* karptr; };
String::String(const String & s) : lengte(s.lengte) { karptr = new char[lengte]; //er was noggeen tabel 11 waar karptr naar verwees for(int i = 0 ; i < lengte i++) karptr[i] = s.karptr[i]; }
Code 10.24: Copy constructor
180
Hoofdstuk 11 Aanvullingen bij invoer en uitvoer In dit hoofdstuk behandelen we invoer en uitvoer in C++ in meer detail. We maken kennis met een aantal nieuwe operatoren en bespreken ook eèn aantal eigenschappen van de operatoren << en >>.
11.1
Verzorgde uitvoer
Het volgende programma probeert de kwadraten en derde machten van de getallen tussen 20 en 39 netjes in twee kolommen af te drukken.
#include using namespace std; int mainO { int j; cout << "+--+--------+--+---------+\n" ; for (int i = 20 ; i < 30 ; i++) { cout « ' I ' « i « ' I ' « (i * i) « j
=i
' ' «
(i
*
i
*
i);
j) << ' ' «
(j
*
j
*
j);
+ 10;
cout << 'I'<< j <<'I' << (j cout << "1\n";
*
}
cout << "+--+--------+--+---------+\n"; return 0; }
Dat dit niet helemaal geslaagd is, kan je merken aan de volgende uitvoer:
181
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
+--+--------+~-+---------+
120 1400 8000 130 19.00 21000 I 121 1441 92611311~61 297911 1221484 10648 13211024 32768 1 1291841 2438913911521 59319 1 +--+--~-----+--+---~-----+
Er loopt duidelijk iets fout vanaf de derde rij in de tabel, waardoor de kolommen niet meer mooi onder elkaar staan; Dit komt doordat, vanaf het getal 22, de derde machten 5 cijfers hebben in plaats van 4, en dat ook de kwadraten een cijfertje meer krijgen vanaf het getal 32. Er zijn verschillende manieren om dit probleem op te lossen. · Zo kan je een extra ifopdracht inlassen die .kijkt of een kwadraat minder dan 4 cijfers heeft en in dat geval een extra spatie afdrukt (en een analoge i f voor de derde machten):
cout << 'I' << i<< 'I'; i f (i * i < 1000) cout
«
' ';
cout << (i*i) << ' '; if (i* i* i< 10000) cout « cout « (i* i* i);
' ';
Je kan ook een procedure schrijven, bijvoorbeeld met naam schrijf_getal en twee gehele parameters getal en breedte, die een gegeven getal uitschrijft en voldoende spaties afdrukt zodat ze samen met het getal een gegeven breedte op het scherm in beslag neemt. (We zeggen ook dat het getal wordt uitgeschreven in een veld van een gegeven breedte.) Je roept deze procedure dan bijvoorbeeld zo op:
cout <<'I'<< i<< ' I '; schrijf_getal(i *i, 4); schrijf_getal(i *i* i, 6);
Je hoeft echter al die moeite niet zelf te doen, aangezien de meeste C++-omgevingen een dergelijke procedure reeds aanbieden - zij het met een enigzins andere notatie. Het bovenstaande programma kan je namelijk noteren zoals in Code 11.1. De Uitdrukking setw(6) noemen we een (invoer-uitvoer)manipulator. Wanneer je de manipulator setw( breedte) tussenvoegt in de argumentenlijst van een schrijfopdracht, zal het volgende element rechts gealigneerd worden uitgeschreven in een veld .van de gegeven breedte. Als dit· nodig is worden extra spaties vooraan bijgevoegd.
182
@[email protected]
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
#include · #include using namespace std; int mai:Ó.() { int j; cout << "+--+----------+--+----------+\n"; for (int i = 20 ; i < 30 ; i++) { co ut << ' I ' << i « ' I ' << setw(4) << (i * i); co ut << setw(6) << (i * i * i); j = i + 10; co ut << ' I ' « j << ' I ' << setw(4) << (j * j); co ut << setw(6) << (j * j * j) << "1\n" ; }
cout << "+--+----------+--+----------+\n"; return 0; }
Code 11.1: Drukt gegevens netjes in twee kolommen af. Let op! Invoer-uitvoermanipulatoren kan je enkel gebruiken als je bovenaan het programma de opdracht '#include ' toevoegt. In Code 11.2 geven we nog een voorbeeld. #include #include using namespace std; int mainO { char *s.tr = "Hallo"; f or (int i = 0 ; i < 5 ; i++) cout « setw(6) « (i * i * i * i) « setw(6) « (str + i) << setw(9) << (1.o·; (i+ 1 . 0)) << setw(i) <
Code 11.2: Illustre_ert een aantal eigenschappen van setw
In dit geval is de uitvoer: 10 allo 0.51 llo 0 . 333333 2 0.25 3 lo 0.2 4 0
0 Hallo
1 16 81 256
183
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
Uit dit voorbeeld kan je een aantal eigenschappen van set'w opmaken: - setw kan gebruikt worden bij het afdrukken van allerhande gegevens: zowel gehele getallen, reële getallen, karakters als strings. - Het argument van setw hoeft niet constant te zijn, ook variabelen zijn toegelaten. - Wanneer de veldbreedte niet groot genoeg is, wordt het element toch volledig afgedrukt, zoals bij de laatste nul op de eerste uitvoerlijn. De opgegeven breedte is hier slechts 0, maar toch wordt er één karakter afgedrukt. Er bestaan nog verschillende andere manipulatoren, maar we vermelden hier alleen nog setprecision(p) dat de 'precisie' aangeeft waarmee een reëel getal dient worden afgedrukt. Dus, als we schrijven for (int i = 1 ; i < 6 ; i++) cout << setw(9) << setprecision(i) << (1.0 I 1.3) << endl krijgen we het volgende op het scherni 1 :
0.8 0.77 0.769 0.7692 0.76923 Merk op dat manipulatoren zoals set'w en setprecisi6n geen blijvend effect hebben. Ze moeten telkens opnieuw herhaald worden voor elk element dat wordt afgedrukt.
11.2
Invoer via het toetsenbord
Om alle details van de diverse leesopdrachten te begrijpen, is het nuttig om het invoerproces via het toetsenbord van iets dichterbij te bekijken. Hierbij moeten we één belangrijke stelregel goed onthouden: Invoer via het toetsenbord gebeurt steeds lijn per lijn. Met andere woorden, de gebruiker kan altijd een volled{ge lijn intikken, ook al heeft het programma op dat moment slechts één enkel karakter nodig. Een lijn bestaat uit nul of meer karakters en wordt beeindigd met de 'Return'-toets. Tijdens het intikken van een 1
Probeer dit eerst uit op je computer vooraleer je de manipulator setprecision gebruikt. Bepaalde versies van de Horland C++-compiler werken niet helemaal zoals hier beschreven.
184
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
lijn kunnen verbeteringen aangebracht worden en het programma loopt pas verder op het moinent dat de 'Return'-toets wordt ingedrukt 2 • Wanneer de computer een eerste leesopdracht (bijvoorbeeld 'cin >> getal') in een programma ontmoet, krijgt je de gelegenheid om een getalin te tikken. Pas nadat je de 'Return'-toets indrukt, plaatst de computer het ingetikte getal in de opgegeven variabele en loopt het programma verder. Het is best mogelij~ dat je behalve dit ene getal, nog andere gegevens op die lijn had ingetikt. Deze elementen worden dan automatisch verwerkt bij de volgende leesopdracht, zonder dat de controle eerst weer aan het toetsenbord wordt gegeven. Omgekeerd, als de gebruiker bij de eerste leesopdracht onmiddellijk op 'Return' drukt (m.a.w. als de eerste ingetikte lijn leeg is), dan zal automatisch een nieuwe lijn worden gevraagd vooraleer het programma verder gaat. Bekijk bijvoorbeeld het volgende programmafragment: int a, b; cout << "Geef het getal a: "; cin >> a; cout << "a * a = " << (a * a) << endl; cout <<"Geef het getal b: "; cin
» -b;
cout << "b
*b = "
<< (b
* b)
<< endl;
Wanneer je nu per ongeluk een getal te veel intikt wanneer naar het getal a wordt gevraagd, . .. krijg je het volgende effect: Geef het getal a: 12 34 a * a = 144 Geef het getal b: b * b
= 1156
Bij detweede leesopdracht ('cin » b') krijg je niet meer de gelegenheid om iets in te tikken aangezien de vorige invoerlijn nog niet is 'o:pgebruikt'.
11.3
Inlezen van één enkel karakter
Het ongewenste effect uit bovenstaande paragr~:~,af kan je tegengaan door na de eerste leesopdracht de computer op één of andere manier te vragen de rest van de invoerlijn te negeren. Hiervoor heb je een nieuw soort leesopdracht nodig, want we kunnen dit niet verwezenlijken met de leesopdrachten die we tot Iiog toe kennen. Geen enkele opdracht 2
In sommige C++-omgevingen bestaat er een speciale procedure die precies één toets inleest. Wanneer je met deze procedure bijvoorbeeld een reëel getal wil inlezen, moet je dat getal cijfer voor cijfer inlezen en opbouwen, en volgt er bovendien nog heel wat extra programmeerwerk om de gebruiker toe te laten om zijn invoer te verbeteren. TrouwenS meestal verschijnt er met deze speciale procedure helemaal niets ·op het scherm, en moet de programmeur ook daarvoor zelf.zorgen.
185
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
van de vorm 'ein » een lijn.
@Helga.Naessens@hogent. be
... maakt namelijk onderscheid tussen een spatie of het einde van
De uitdrukking 'ein. get ()' leest één enkel karakter in en heeft dit karakter als waarde3 . Deze uitdrukking wordt meestal gebruikt aan de rechterkant van een toewijzing; maar dit is niet strikt noodzakelijk. Wanneer je met get afzonderlijke karakters inleest, worden spaties :niet overgeslagen en wordt bovendien een linefeed ingelezen telkens we aan het einde van de invoerlijn komen. Je zou deze linefeed kunnen beschouwen als een representatie van de 'Return'~toets die je moet indrukken om het einde van de invoerlijn, aan te geven. De procedure lees_string uit Code 11.3 gebruikt deze nieuwe opdracht bijvoorbeeld om het gedrag van de opdracht 'ein » str' te imiteren. Zoals je reeds weet, leest deze opdracht een string in van het toetsenbord en stopt die in de variabele str. Alle spaties en lege lijnen4 waarmee de invoer begint, worden echter gewoon overgeslagen, en er wordt gestopt met lezen zodra er een spatie wordt ingetikt of het einde van de lijn wordt bereikt.
void lees_string (ehar *str) { ehar eh; eh = ein.getO; while (eh == ' ' I I eh '\n ' ) eh = ein.getO; *str++ = eh; eh = cin.get(); while (eh != ' ' && eh ! = ' \n ' ) { *str++ = eh; eh = ein.getO; }
*str
= 0;
}
Code 11.3: Imiteert het gedrag van de opdracht cin
>> str
Eerst lezen we alle spaties en lege lijnen in, die we dan verder negeren. Daarna worden afzonderlijke letters ingelezen en één voor één in str opgeslagen, totdat opnieuw een spatie of het einde van een lijn wordt ontmoet. Merk op dat de eerste while-lus van lees_string op het einde reeds het eerste karakter van de string in de variabele eh inleest. Zij die dit onnatuurlijk vinden, kunnen in de plaats daarvan de opdracht 'ein.putbaek(eh)' gebruiken. Deze opdracht maakt de vorige get 3
Je zou hier wellicht eerder een notatie zoals 'get(cin)' verwachten. De operator'.' die hier in de plaats gebruikt wordt, houdt verband met het zogenaamd 'object gericht' programmeren in C++. 4 Er zijn ook nog andere karaiders die worden overgeslagen, zoals bijvoorbeeld de 'Tab'-toets. Al deze karakters worden samen aangeduid met de Engelse term whitespace.
186
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer .
@[email protected]
'ongedaan'. Ander~ gezegd: een get onmiddellijk na een putback(ch) leest opnieuw het karakter eh in. Let wel: putback gebruiken zonder een voqrafgaande get is zinloos! We kunnen dus schrijven:
· eh = cin.get(); while (eh == ' ' I I eh eh= cin.get (); _ein.putback(ch); eh = cin.getO; while ...
'\n')
}
Keren we nu terug naar het probleem uit het begin van deze paragraaf: hoe negeerje alle karakters tot op het einde van een invoerlijn? Dit kan met de volgende ppdrachten: ehar eh; eh = cin.getO; while (çh != '\n') eh = ein.get(); of nog korter: while (cin.getO != '\n'); Bemerk dat deze làatste while-lus geen 'binnenkant' heeft. Om dit aan te duiden is de kommapunt verplicht. Schijnbaar wordt er niets herhaald. De vergelijking test echter telkens het (naamloze) resultaat van herhaalde oproepen van de functie cin. get ().
11.4
Nog meer voorbeelden_ van invoer
Het ·programm~ uit Code 11.4 leest een aantal lijnen in. Elke lijn bevat het gewicht van een persoon, voorafgegaan met de letter 'M' of 'V' om het geslacht aan te duiden. Om de invoer af te sluiten tikken we de letter 'S' in. Het programma drukt het totale gewicht af van alle mannen en van alle vrouwen in de lijst. . Door de manier waarop '»'werkt, hoefje de invoer trouwens niet exact lijn per lijri in te geven. Ook onderstaande invoer zal bijvoorbeeld aanvaard worden:
M75.5 V 54.4V 45.8M 90.0 M
87.2S
187
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
#include using namespace std; int main() { 0 . 0, somv 0.0, gew; double somm char eh; cin » eh; while (eh 'M ' I I eh == 'V') { cin >> gew; (eh 'M' ? somm: somv) += gew; cin >> eh; }
cout << "Mannen = " << somm << " << somv << endl; return 0;
vrouwen
"
}
Code 11.4: Drukt het totale gewicht af van alle mannen en van alle vrouwen .Merk opdat '»' altijd spaties en linefeeds aan het begin van de invoer negeert, en dat bij het inlezen van getallen steeds 'zoveel mogelijk' karakters worden gelezen. Het .programma uit Code 11.5 drukt de som: af van alle getallen die op één en dezelfde invoerlijn voorkomen. We gebruiken get om het einde van de invoerlijn te detecteren. We hebben ook een putback nodig voor het geval dat de get een karakter heeft ingelezen dat reeds tot het volgende getal behoort. In het programma uit Code 11.6 worden de getallen onder elkaar ingegeven, ditmaal afgesloten met een lege lijn. Merk op dat dit programma slechts in één lijn van het vorige verschilt. Ook bij bestanden kan je gebruik maken van get en putback. Dit wordt geïlh,tstreerd in de functie product_bestand uit Code 11.7 die het product berekent van de reële getallen uit een bestand. Het bestand mag ook breuken van de vorm teller/ noemer bevatten. De functie heeft één parameter, nl. een string, die de naam van het invoerbestand bevat.
188
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
#include using namespace std; int mainO { double som = 0.0, getal; char eh= cin.get(); while (eh != '\n') { cin.putback(ch); cin » getal; som += getal; eh = cin.getO; }
cotit << "Som = " << som << endl; return 0; }
Code 11.5: Berekent de som van alle getallen op één invoerlijn
#include using namespace std; int mainO { double som = 0.0, getal; char eh= cin.get(); while (eh != '\n') { cin.putback(ch); cin » getal; som += getal; while (cin.getO != '\n'); eh = cin.getO; }
cout << "Som = " << som << endl; return 0; }
Code 11.6: Berekent de som van getallen, afgesloten van een lege lijn
189
Hoofdstuk 11. Aanvullingen bij invoer en uitvoer
@Helga.Naessens@hogent. be
double produçt_bestand(char *s) { ifstream inv; inv.open(s); double prod = 1. 0, getal, noemer; char eh; . inv » getal; while (!inv.fail()) { inv >> eh; if (!inv.fail()) { if (eh== '/') { inv >> noemer; getal / = noemer; }
el se inv.putback(ch); }
prod *= getal; inv » · getal; }
inv. close 0 ; return prod; }
Code 11.7: Berekent het product van reële getallen (eventueel breuken)
190