© Harry Broeders.
In de propedeuse heb je datastructuren zoals array en struct leren gebruiken.
Deze datastructuren worden statisch genoemd omdat hun grootte tijdens het
compileren wordt bepaald en vast is. In de praktijk heb je al snel behoefte
aan zogenaamde dynamische datastructuren waarvan de grootte tijdens het uitvoeren
van het programma kan wijzigen. Later in dit kwartaal zul je leren dat in
de standaard C++ library de class list
is opgenomen die het
gebruik van dynamische (gelinkte) lijsten erg eenvoudig maakt. Om een goed
begrip te krijgen van de werking van dynamische datastructuren is het toch
leerzaam om zelf eens een eenvoudige gelinkte lijst te maken.
Een pointer is een variabele die het adres kan bevatten van een eerder gedeclareerde variabele. In de propedeuse heb je pointers als volgt leren gebruiken.
int i(3); // i=3
int *pi(&i); // pi wijst naar i
*pi=*pi+5; // i=8
Het is nu dus mogelijk de waarde van de variabele i
te benaderen
met de dereferentie van pi
: *pi
In H1 heb je geleerd dat het ook mogelijk is voor een pointer een stuk geheugen
te reserveren (met de new
operator) dat niet direct gekoppeld
is aan een eerder gedeclareerde variabele. De dereferentie van de pointer
kan nu gebruikt worden als een gewone variabele. Dit is dan wel de enige
manier om de waarde van deze "onbenoemde variabele" te benaderen. Voorbeeld
van het gebruik van een pointer naar met new
gereserveerd geheugen:
int* pi(new int); // pi wijst naar een onbenoemde variabele
*pi=3;
*pi=*pi+5;
Als het gereserveerde geheugen dat toegewezen is aan een bepaalde pointer
niet meer nodig is moet het weer vrijgegeven worden met de operator
delete
. Voorbeeld van het met delete
vrijgeven
van met new
gereserveerd geheugen:
delete pi;
Stel bijvoorbeeld dat de volgende declaratie van een class gegeven is:
class PlayTime {
public:
PlayTime(int m=0, int s=0);
void add(const PlayTime& t);
private:
void normalize();
int min;
int sec;
friend ostream& operator<<(ostream& out, const PlayTime& t);
};
// ...
PlayTime* pp;
pp=new PlayTime(3,20);
Na de declaratie
PlayTime* pp;
is de variabele pp
gecreëerd,
zie figuur 1a. Deze variabele bevat echter nog een niet gedefinieerde waarde
en er is nog geen geheugen gereserveerd voor een object.
Na de toekenning pp=new PlayTime(3,20);
is een stuk geheugen
gereserveerd dat groot genoeg is om 1 Playtime
object te bevatten
en dit object is geïnitialiseerd in de constructor. De variabele
pp
bevat nu het adres van dit gereserveerde stuk geheugen. Zie
figuur 1b.
In figuur 1c is dit abstracter weergegeven omdat de waarde van het adres
onbelangrijk is. De door pp
aangewezen tijdsduur kan door middel
van de notatie (*pp).add(*pp)
bij zichzelf worden opgeteld.
Dit kan ook als pp->add(*pp)
genoteerd worden.
Na de instructie: delete pp;
is het gereserveerde stuk geheugen
weer vrijgegeven voor hergebruik, de variabele pp
bestaat dan
nog wel maar de inhoud hiervan is onbepaald. Zie weer figuur 1a.
Een recursieve class is een class die naar zichzelf verwijst. Bijvoorbeeld:
class PlayListElement {
public
1: // Een PlayListElement bestaat uit:
PlayTime pt; // - een PlayTime
PlayListElement* next; // - een pointer naar het volgende PlayListElement
};
Door het gebruik van recursieve classes is het mogelijk recursieve datastructuren (bijvoorbeeld lijsten) te creëren van naar elkaar verwijzende objecten. Bijvoorbeeld:
PlayListElement e1,e2,e3;
PlayListElement* kop(&e1);
e1.next=&e2;
e2.next=&e3;
e3.next=0;
Grafisch is direct duidelijk hoe zoiets eruit ziet:
Als in het vorige voorbeeld gebruik gemaakt zou worden van dynamische geheugentoewijzing dan volgt:
PlayListElement* kop;
PlayListElement* staart;
PlayListElement* hulp;
kop= new PlayListElement; staart=kop; // eerste
hulp=new PlayListElement; staart->next=hulp; staart=hulp; // tweede
hulp=new PlayListElement; staart->next=hulp; staart=hulp; // derde
staart->next=0;
Precies hetzelfde wordt bereikt met het onderstaande algoritme dat iets korter is:
PlayListElement* kop(new PlayListElement);
PlayListElement* staart(kop);
staart->next=new PlayListElement;
staart=staart->next;
staart->next=new PlayListElement;
staart=staart->next;
staart->next=0;
Grafisch is dit als volgt weer te geven:
Merk op dat het enige verschil tussen figuur 2 en figuur 3 is dat in figuur
3 de variabelenamen e1
, e2
en e3
niet
voorkomen.
Voetnoot 1: Deze class bevat
public
datamembers en er is dus geen sprake van
"encapsulation" of datahiding. Dit was tegen de regels van goed object
georiënteerd programmeren! We zullen later zien dat deze class alleen
als private
class binnen een andere class wordt gebuikt waardoor
toch een goed object oriënted programma ontstaat.
Tot slot van deze inleiding over dynamische gelinkte lijsten volgt nog een
voorbeeld waarin een aantal manipulaties met lijsten is opgenomen. Dit programma
kun je hier kopiëren: lijst.cpp
.
#include <iostream>
#include <iomanip>
#include <fstream>
using namespace std;
// =============================================================================
class PlayTime {
public:
PlayTime(int m=0, int s=0);
void add(const PlayTime& t);
private:
void normalize();
int min;
int sec;
friend ostream& operator<<(ostream& out, const PlayTime& t);
};
istream& operator>>(istream& in, PlayTime& t);
PlayTime::PlayTime(int m, int s): min(m), sec(s) {
normalize();
}
void PlayTime::add(const PlayTime& t) {
min+=t.min;
sec+=t.sec;
normalize();
}
void PlayTime::normalize() {
long tsec(min*60+sec);
min=tsec/60;
sec=tsec%60;
}
ostream& operator<<(ostream& out, const PlayTime& t) {
return out<<setfill('0')<<setw(2)<<t.min<<":"<<setw(2)<<t.sec;
}
istream& operator>>(istream& in, PlayTime& t) {
int i;
if (in>>i) {
if (in.peek()==':') {
in.get();
int j;
if (in>>j)
t=PlayTime(i, j);
}
else
t=PlayTime(0, i);
}
return in;
}
// =============================================================================
// De class PlayList bevat:
// - een pointer naar een PlayListElement genaamd first dit is het eerste
// element in de gelinkte lijst.
// - een PlayListElement bevat:
// - een PlayTime object genaamd pt.
// - een pointer naar een PlayListElement genaamd next.
// (deze pointer is 0 als de lijst geen elementen meer heeft).
class PlayList {
public:
PlayList();
~PlayList();
void insert(const PlayTime& t);
PlayTime total() const;
private:
class PlayListElement {
public:
PlayTime pt;
PlayListElement* next;
};
PlayListElement* first;
friend ostream& operator<<(ostream& out, const PlayList& l);
};
istream& operator>>(istream& in, PlayList& l);
PlayList::PlayList(): first(0) {
}
PlayList::~PlayList() {
while (first!=0) {
PlayListElement* listElement(first);
first=first->next;
delete listElement;
}
}
PlayTime PlayList::total() const {
PlayTime t;
PlayListElement* pList(first);
while (pList) {
t.add(pList->pt);
pList=pList->next;
}
return t;
}
void PlayList::insert(const PlayTime& t) {
PlayListElement* newlistElement(new PlayListElement);
newlistElement->pt=t;
newlistElement->next=first;
first=newlistElement;
}
ostream& operator<<(ostream& out, const PlayList& l) {
PlayList::PlayListElement* pList(l.first);
while (pList!=0) {
out<<pList->pt<<endl;
pList=pList->next;
}
return out;
}
istream& operator>>(istream& in, PlayList& l) {
PlayTime pt;
while (in>>pt) {
l.insert(pt);
}
return in;
}
// =============================================================================
int main () {
ifstream fin("playlist.txt");
if (fin!=0) {
PlayList lijst;
fin>>lijst;
cout<<"De lijst:"<<endl<<lijst;
cout<<"Totale speelduur: "<<lijst.total()<<endl;
}
else
cout<<"De file playlist.txt kan niet worden geopend!"<<endl;
cout<<"Druk op de return-toets."<<endl;
cin.get();
return 0;
}
Weet je de antwoorden op de volgende vragen nog?
Q: | Noem twee voordelen van het gebruik van een ADT PlayTime
in plaats van een struct PlayTime . |
A: | Doordat de datamembers in een ADT private zijn en dus alleen bereikbaar via de memberfuncties van de ADT (we noemen dit datahiding) kun je de implementatie van een ADT wijzigen zonder dat de rest van het programma daar iets van merkt. Het gebruik van een ADT maakt het programma beter aanpasbaar en uitbreidbaar. Doordat in een ADT niet alleen de structuur van de data is vastgelegd maar ook de bewerkingen die op of met deze data kunnen worden uitgevoerd (de memberfuncties) is een ADT eenvoudiger herbruikbaar. |
- | |
Q: | De parameter t van de memberfunctie add is
van het type const PlayTime& . Kan hier ook het type
PlayTime gebruikt worden? Wat is dan het voordeel/nadeel? |
A: | Ja dat kan. Bij het gebruik van het type PlayTime wordt
dan wel een kopie van de parameter die bij aanroep meegegeven wordt gebruikt.
Het aanmaken en weer verwijderen van deze kopie kost tijd en heeft dus als
nadeel dat het trager is. Bij het gebruik van een
PlayTime& wordt geen kopie gemaakt van de parameter die
bij aanroep wordt meegegeven maar wordt een reference naar (andere naam voor)
deze parameter gebruikt. Omdat we deze reference alleen willen gebruiken
om de waarde van de bij aanroep meegegeven parameter te lezen gebruiken we
een const PlayTime & . |
- | |
Q: | Kun je een object van de class PlayTime als volgt
definiëren?
Zo nee, waarom niet? Zo ja, wat is dan de waarde? |
A: | Ja dat kan. De class PlayTime moet dan wel een constructor
hebben zonder argumenten of een constructor waarbij alle parameters een default
waarde hebben. PlayTime heeft de volgende constructor
PlayTime(int m=0, int s=0); . Als je deze constructor aanroept
zonder parameters mee te geven dan wordt m gelijk aan
0 en s ook gelijk aan 0 . In de
implementatie van deze constructor kun je zien dat de private variabele
min geïnitialiseerd wordt met m en dat de
private variabele sec geïnitialiseerd wordt met
s . De waarde van t is dus 00:00 . |
- | |
Q: | De operator<< voor het afdrukken van objecten van
de class PlayTime heeft als tweede parameter een const
PlayTime& . De operator>> voor het inlezen van
objecten van de class PlayTime heeft als tweede parameter een
PlayTime& . Verklaar dit verschil. |
A: | De als parameter van operator>> moet veranderd (ingelezen)
kunnen worden en kan dus geen const zijn. Bij het afdrukken
mag de operator<< het af te drukken object niet wijzigen
de parameter moet dus const zijn. |
- | |
Q: | De operator<< voor het afdrukken van objecten van
de class PlayTime is als friend van PlayTime
gedeclareerd. De operator>> voor het inlezen van objecten
van de class PlayTime is niet als friend gedeclareerd. Verklaar
dit verschil. |
A: | In de implementatie van de operator<< kun je zien
dat in deze code de private variabelen van de class PlayTime
gebruikt worden dit kan alleen als deze operator<< een
memberfunctie van PlayTime is (maar dat is niet zo) of als deze
operator<< een friend is van de class
PlayTime . In de implementatie van de
operator>> kun je zien dat deze operator de private variabelen
van PlayTime helemaal niet nodig heeft. |
- | |
Q: | De class PlayListElement heeft public member variabelen.
Dit mag toch niet als je het programma goed uitbreidbaar en aanpasbaar wil
maken? Zie ook het antwoord op de eerste vraag. |
A: | In dit geval mag het wel omdat de class PlayListElement
als private class binnen PlayList gedefinieerd is. De class
PlayListElement is dus alleen in memberfuncties (of fiends)
van de class PlayList te gebruiken. In dit geval zou je dus
net zo goed een struct kunnen gebruiken. |
- | |
Q: | In de vierde regel van main staat:
De
De eerste parameter is echter van het type |
A: | Er wordt gebruik gemaakt van polymorphism. De class ifstream
is afgeleid van de class istream . Er geldt een
ifstream is een istream . |
Deze opdracht bestaat uit de deelopdrachten 1a t/m 1g. Opgave 1a t/m 1f moet je laten nakijken door de practicumdocent. Opgave 1g is niet verplicht maar wel leerzaam.
In het voobeeldprogramma worden de speeltijden die zijn opgeslagen in de
file playlist.txt in de functie main
door middel van een zelf
gedefinieerde operator>>
ingelezen in een dynamisch gelinkte
lijst. Een datafile waarmee het programma getest kan worden is hier beschikbaar:
playlist.txt
. De gelinkte lijst wordt
in de
operator>>
als volgt gevuld:
istream& operator>>(istream& in, PlayList& l) {
PlayTime pt;
while (in>>pt) {
l.insert(pt);
}
return in;
}
De memberfunctie insert
is als volgt gedefinieerd:
void PlayList::insert(const PlayTime& t) {
PlayListElement* newlistElement(new PlayListElement);
newlistElement->pt=t;
newlistElement->next=first;
first=newlistElement;
}
Opdracht 1a.Loop stap voor stap het inlezen van de file door en teken daarbij de gelinkte lijst die ontstaat. Je kunt met C++ Builder het programma stap voor stap volgen door een breakpoint te zetten op de eerste regel (met de sneltoets F5) en daarna met de sneltoetsen F7 en F8 het programma regel voor regel uit te voeren:
Je kunt de objectstructuren mooi zichbaar maken door op een objectnaam (variabelenaam) te gaan staan met de muis en dan rechts te klikken en de popup menu optie Debug, Inspect... te kiezen (sneltoets Alt+F5). Als een datamember van het object een pointer is dan kun je het object waar die pointer naar wijst bekijken door deze datamember te selecteren en dan via de rechtermuis optie Inspect (sneltoets Ctrl+I) te kiezen. |
Vervolgens wordt de lijst vanuit de functie main
afgedrukt:
cout<<"De lijst:"<<endl<<lijst;
Deze zelf gedefinieerde operator<<
drukt de inhoud van
de gelinkte lijst op het beeldscherm af:
ostream& operator<<(ostream& out, const PlayList& l) {
PlayList::PlayListElement* pList(l.first);
while (pList!=0) {
out<<pList->pt<<endl;
pList=pList->next;
}
return out;
}
Vanuit deze operator<<
wordt een andere zelf gedefinieerde
operator<<
aangeroepen die er voor zorgt dat een
PlayTime
wordt afgedrukt. Het gebruik van twee operatoren (of
functies) met dezelfde naam (maar met verschillende parametertypes) wordt
operator overloading (of function overloading) genoemd.
Opdracht 1b.
Loop stap voor stap het uitvoeren van de |
Zorg ervoor dat je de werking van dit programma volledig begrijpt! Stel vragen aan de docent als er iets is wat je nog niet helemaal begrijpt.
Opdracht 1c.
Ontwerp nu zelf een memberfunctie van
De declaratie van de memberfunctie is dus:
|
Aan het eind van de if
wordt de destructor van
PlayList
automatisch aangeroepen. Deze destructor geeft het
met new
dynamisch gereserveerde geheugen, door middel van
delete
, weer vrij voor hergebruik. Deze destructor kan niet
als volgt gecodeerd worden:
PlayList::~PlayList() {
while (first!=0) {
delete first;
first=first->next;
}
}
Opdracht 1d.Bedenk waarom de bovenstaande destructor niet correct is. Als je de bovenstaande destructor probeert uit te voeren zul je merken dat deze destructor toch goed lijkt te werken. Bedenk waarom dat dit erg gevaarlijk is! |
Door de pointer naar het volgende PlayListElement
in een hulp
variabele op te slaan is de destructor in het voorbeeldprogramma wel correct
gecodeerd:
PlayList::~PlayList() {
while (first!=0) {
PlayListElement* listElement(first);
first=first->next;
delete listElement;
}
}
De destructor kan met behulp van een hulp functie dump
ook als
volgt gecodeerd worden:
void PlayList::dump(PlayListElement* p) {
if (p!=0) {
dump(p->next);
delete p;
}
}
PlayList::~PlayList() {
dump(first);
}
De functie dump
is een recursieve functie. Dat wil zeggen
dat deze functie zichzelf aanroept.
Opdracht 1e.Speel stap voor stap het uitvoeren van de destructor na. Maak daarbij gebruik van de in opgave 1a gemaakte tekening. In welke volgorde worden de elementen vrijgegeven? |
Opdracht 1f.Ontwerp en implementeer een memberfunctie die de inhoud van de gelinkte lijst van achter naar voor afdrukt met behulp van een recursieve hulp functie. |
Als je een element uit een gelinkte lijst moet verwijderen dan is dit best
wel lastig. Als je bijvoorbeeld het tweede element uit een gelinkte lijst
moet verwijderen dan moet je er eerst voor zorgen dat het eerste element
naar het derde element verwijst. Pas daarna kun je het tweede element met
behulp van delete
opruimen. Het verwijderen van het eerste element
heeft tot gevolg dat de pointer naar het begin van de lijst wijzigd.
Opdracht 1g(niet verplicht).
Ontwerp en implementeer een functie waarmee element
Deze memberfunctie moet element Test deze functie door in het hoofdprogramma toe te voegen:
De juiste uitvoer is: Na verwijderen: 10:09 |