Programmeren in C++: snelcursus

Deze beknopte inleiding tot C++ is geschreven voor studenten die in hun vooropleiding reeds ervaring opdeden met java.

Waarom deze inleiding?

Met het aanbieden van deze schakel naar programmeren in C++ willen we volgende zaken op een rijtje krijgen:
  1. een zekere vlotheid in je basishandelingen
    Bepaalde programmaonderdelen, zoals aanbieden van een keuzemenu, zoeken in een tabel, bewerkingen op cijfers van gehele getallen,... komen zeer dikwijls voor. Even dikwijls zien we echter teveel ballast verschijnen in de geproduceerde code: teveel hulpvariabelen, overbodige testen, storende bewerkingen tussendoor. We geven je enkele stijltips, en sommen op welke basishandelingen je vlot uit je mouw moet kunnen schudden. (Dit staat uiteraard los van de gebruikte programmeertaal, maar we hebben hier de gelegenheid enkele van onze aandachtspunten aan jullie voor te stellen.)
  2. een goed begrip van het hot topic in C(++): pointers
    Dit komt jullie dit semester nog van pas in andere vakken (netwerkprogrammatie).
  3. een denk- in plaats van tik-reflex
    Vooral bij het gebruik van pointers, gelinkte lijsten, boomstructuren, pointers naar pointers,... is het van belang dat je de zaken al bij de eerste poging juist op papier zet. (Jawel, PAPIER, uitvinding van voor onze jaartelling, maar sedertdien o zo geduldig.) Begin je gehaast aan een kladpoging op je scherm, en sla je de bal of de pointer mis, dan kan elke kleine aanpassing (poging tot verbetering) je verder af drijven van het juiste pad. Dit straatje zonder eind kan zeer frustrerend werken, en "tabula rasa" maken is dan dikwijls het enige aangewezen hulpmiddel.
  4. een goede feeling voor recursief programmeren
    (zowel in interface als functie-opbouw), met een zicht op de voor- en nadelen ten opzichte van niet-recursief programmeren
  5. een zicht op jullie kijk op de zaak
    Aarzel dus niet om op- of aanmerkingen door te geven. IEDEREEN moet mee zijn met deze leerstof, anders is er geen beginnen aan in het tweede semester (labo bij de cursus Algoritmen en Gegevensstructuren).






Hoofdstuk 1: Basishandelingen

Wat je vlot moet kunnen

Doordat veel basiscursussen programmeren zich tegenwoordig onmiddellijk in de objectgerichte aanpak storten (in plaats van het verouderde procedurele werk), verschoof de aandacht misschien ongewild van het vlot uitvoeren van sommige handelingen / procedurele taken / functies, naar het opzetten van een goede structuur (interface) van allerhande objecten. Dit neemt niet weg dat procedures (lees: handelingen) zeer belangrijk blijven, en dikwijls het verschil uitmaken tussen een efficiënt en inefficiënt (snel versus traag) programma. Laten we daarom eens kijken naar enkele eenvoudige maar veel voorkomende handelingen. We geven je de properste code om deze zaken te coderen (niet overladen en toch leesbaar), en raden je aan om je aan deze stijl te houden. (Tip: spiekbriefje van maken voor de eerste 100 keer. Daarna zit het in je vingers.)

  1. zoeken in een ongesorteerde tabel (dus lineair)
    int i = 0;
    while (i < lengte   &&   gezocht != gegevens[i]){
        i++;
    }
    if (i == lengte)  // niet aanwezig
    else              // aanwezig 
    
  2. zoeken in een gesorteerde tabel : lineair
    int i = 0;
    while (i < lengte   &&   gezocht < gegevens[i]){
        i++;
    }
    if (i < lengte && gezocht == gegevens[i])  // aanwezig
    else                                       // niet aanwezig
    
  3. zoeken in een gesorteerde tabel : binair (geen duplicaten ondersteld)
    int l=0, r=lengte_tab;
    int m;
    while(r>l){
        m=l+(r-l)/2;
        if     (tab[m]<gezocht) l=m+1;
        else if(tab[m]>gezocht) r=m;
        else r=l;
    }
    if(tab[m]==gezocht) // gevonden
    else                // niet gevonden
    
    
  4. aanbieden van keuzemenu
    keuze = keuze_uit_menu();
    while (keuze != stopwaarde){
        if (keuze == eerstewaarde)       // eerste geval
        else if (keuze == tweedewaarde)  // tweede geval
        ...
        else if (keuze == laatstewaarde) // laatste geval
        else                             // foutmelding
    
        keuze = keuze_uit_menu(); 
    }
    
    Merk op: indien de gebruiker een ongeldige keuze maakt, krijgt hij enkel een foutmelding, automatisch gevolgd door het opnieuw aangeboden keuzemenu.

  5. cijferrekenen

    Het afzonderen van de cijfers uit een gegeven geheel getal kan zeer eenvoudig, als we enkel de bewerkingen "/" (gehele deling) en "%" (gehele rest) gebruiken. Andere bewerkingen (-,*,+) komen er niet aan te pas! Als voorbeeld tellen we het aantal cijfers in een (strikt positief) getal, en berekenen we tegelijkertijd de som van die cijfers.

    int aantal = 0;
    int som = 0;
    while (getal != 0){
        aantal++;
        som += getal % 10;
        getal /= 10;
    }
    
    
    
    
  6. regel van Horner

    De regel van Horner wordt bij de meeste (ex-)scholieren direct gelinkt met het zoeken van nulpunten. Of toch minstens met "dat ding met die lijnen". Nochtans is de regel van Horner breder inzetbaar. Ga maar na: willen we weten of t nulpunt is van de derdegraads veelterm ax3+bx2+cx+d, dan rekenen we met het schema van Horner uit:

       |  a         b         c          d
    t  |            at        (at+b)t    ((at+b)t+c)t
    ___|_______________________________________________
       |  a         at+b      (at+b)t+c  ((at+b)t+c)t+d 
    
    
    We hopen dan rechtsonder in het schema een nul tegen te komen - in dat geval is t inderdaad nulpunt van ax3+bx2+cx+d. Maar wat heb je eigenlijk uitgerekend in dat rechteronderhoekje? Werk je de haakjes uit, dan staat er - uiteraard - at3+bt2+ct+d. En inderdaad, als dat gelijk is aan nul, is t een nulpunt van de gegeven veelterm.

    Wat is dan het nut van de regel van Horner?

    1. Zouden we ax3+bx2+cx+d in (C++) programma code omzetten, dan zou je misschien geneigd zijn het volgende te schrijven: a*x*x*x + b*x*x + c*x + d. Je gebruikte zes vermenigvuldigingen en drie optellingen. (Suggereer je a*pow(x,3)+...? Lees volgende paragraaf!)
    2. Volg je de regel van Horner stap voor stap (zie schema!) dan gebruik je achtereenvolgens: * + * + * +. Dus slechts drie vermenigvuldigingen (aantal optellingen blijft gelijk). Voor veeltermen van hogere graad gaat dat verschil nog groter zijn.
    Dit is dus de kracht van de regel van Horner: je doet niet meer bewerkingen dan nodig.

    In telegramstijl (makkelijk om te onthouden) komt er:

       eerste tussenresultaat   =  0   (of coëfficiënt van hoogstegraadsterm)
       volgend tussenresultaat  =  ( vorig tussenresultaat * x ) + volgende coëfficiënt
    

    Slaan we de coëfficiënten van de veelterm op in een tabel (hoogstegraadscoëfficiiënt eerst), dan komt er:

    double resultaat = 0;
    for (int i=0; i<graad_van_veelterm+1; i++){
        resultaat = resultaat * x + coeff[i];
    }
    
  7. waarom is x*x*x NIET gelijk aan pow(x,3)?

    Omdat je voor x*x*x amper 2 bewerkingen nodig hebt. En voor pow(x,3) heb je een functieaanroep (onder andere doorgeven van parameters), en een reeksontwikkeling. Dit is een benaderende berekening (stopt als 'verbeterde' uitkomst niet meer verschilt van vorige benadering). Dus veel te veel bewerkingen met bovendien kans op afrondingsfouten. De functie pow(grondtal,exponent) is bedoeld voor niet-gehele exponenten. Als de exponent een negatief geheel getal is, dan is pow(x,t)=xt=1 / x-t; ook hier doe je geen beroep op de functie pow(). Bovendien gebeurt het in berekeningen dikwijls dat je xt wil uitwerken, maar xt-1 ondertussen al kent. Dan gebruik je uiteraard de regel van Horner: xt = x*xt-1 in plaats van x*x*...*x.

Tot hier de nodige basisreflexen. Sorteren van een tabel met een eenvoudige maar niet te inefficiënte methode (bvb. insertion sort of selection sort) behoort ook tot de basis. Zoek dit eens op!


Programmeren met stijl in C++

Misschien ken je nog andere structuren dan diegene die we hierboven aanhaalden? Maak je soms gebruik van do...while, een for-lus met dubbele stopvoorwaarden; break? We hebben goede redenen om jullie slechts volgende structuren te leren: Bieden we je meerdere wegen aan om eenzelfde probleem op te lossen (bvb. for met break in plaats van een correcte while-lus), dan heb je ook meer kans om verloren te lopen. En vooral: je geeft je collega-programmeur die je code diagonaal doorleest het verkeerde signaal (de break zal een kantlijn te ver genest zijn om goed op te vallen). Meer uitleg vind je in de bundel Programmeren met stijl. Deze tekst heeft alles weg van een mooi stuk antiek - zo eentje waar ze op 'n veiling voor vechten. Hoewel oorspronkelijk geschreven zonder de achtergrond van het objectgerichte programmeren (dat wordt in onze academische-bacheloropleiding pas geïntroduceerd als er een stevige basis ligt), hebben de aanbevelingen niet aan waarde ingeboet. Integendeel. Omdat de beschreven tips verzameld werden na lange les- en werkervaring, staan ze garant voor tijds- en moeitebesparing. Veel leesgenot.

Hoofdstuk 2: pointers en references

Gezien de cursus C++ van het 2e jaar academische bachelor het onderwerp pointers van nul af behandelt, verwijzen we naar daar voor de basiskennis. Hieronder volgen enkele speciale aandachtspunten voor javaprogrammeurs. Ook hier geldt: de relevante gegevens tot een spiekbriefje omvormen.

Verschil tussen lokaal en dynamisch creëren van variabelen

Gezien in java alles dynamisch gecreëerd (én opgekuist) wordt, heb je hier in java geen onderscheid in. In C++ echter wel.

Een lokale variabele is weer verdwenen (= geheugenplaats weer vrijgegeven) zodra het stukje code waarin hij gedeclareerd werd, afgerond is. Plastisch uitgedrukt: als je de lokale variabele declareert "aan de vierde kantlijn", dan is hij weer verdwenen zodra de code een kantlijn meer naar links inspringt (dus aan "de derde kantlijn").

Een dynamisch gecreëerde variabele wordt slechts vrijgegeven aan het geheugen, als je er expliciet om vraagt met het codewoord 'delete'. Doe je dit niet, dan blijft de variabele in het geheugen plaats innemen.

In schemavorm:

enkelvoudige variabele tabelvariabele pointervariabele
lokaal
int x;
int tab[20];
int * xp;
dynamisch
int * xp;
xp = new int;
delete xp;
int * tab;
tab = new int[20];
delete [] tab;
int ** xpp;
xpp = new int *;
delete xpp;


Hetzelfde geldt voor variabelen (objecten) van een zelfgedefinieerde klasse:

enkelvoudige variabele tabelvariabele pointervariabele
lokaal
ding z;
ding tab[20];
ding * zp;
dynamisch
ding * zp;
zp = new ding;
delete zp;
ding * tab;
tab=new ding[20];
delete [] tab;
ding ** zpp;
zpp = new ding *;
delete zpp;

Merk op: opkuisen MOET. VOOR je een 'new' intikt, MOET je de 'delete' al hebben staan. Hiertegen zondigen leidt gegarandeerd tot memory leaks.

Doorgeven van parameters naar (lid)functies

Bij het opstellen van de parameterlijst van een functie, moet je maar op één ding letten: wil ik dat de inhoud van de ingevoerde variabele NIET wijzigt (const); of wil ik dat wijzigingen die de functie aanbrengt WEL zichtbaar zijn in de oproepende code (uitvoer)? Afhankelijk hiervan gebruik je onderstaande syntax (opgedeeld volgens type parameter).

enkelvoudige variabele tabelvariabele pointervariabele
const
int x;
const int * tab;
const int tab [];
int * p;
uitvoer
int & x;
int * tab;
int tab [];
int * & p;

Voor variabelen (objecten) van een zelfgemaakte klasse, geldt hetzelfde. Alleen geven we hier nooit een copie door, gezien een object zeer groot kan zijn.

enkelvoudige variabele tabelvariabele pointervariabele
const
const ding & z;
const ding * tab;
const ding tab [];
ding * zp;
uitvoer
ding & z;
ding * tab;
ding tab [];
ding * & zp;

Bij oproep van een functie met bovenstaande parameters, moet je niet beginnen twijfelen over de variabele die je meegeeft aan de functieoproep. Zorg ervoor dat de variabele hetzelfde type heeft als de parameter. Dus toegepast op de functies van bovenstaande tabel (voor basistype uitgeschreven; voor zowel constante als uitvoerparameter):
enkelvoudige variabele tabelvariabele pointervariabele
int t;
int * tp = new int;
functie(t);
functie(*tp);
int t[20];
int * tp = new int[20];
functie(t);
functie(tp);
      
int * xp;
functie(xp);

Oefeningen op gebruik van pointers

  1. Beschouw volgende variabelen en hun type :
    int i, *ip;
    double d, *dp;
    
    Welke van volgende beweringen zijn (syntactisch) correct? Indien ze correct zijn, verklaar wat ze uitdrukken. Indien je niet zeker bent, probeer dan de code uit in een programma.

    i=7;ip=&7;&i=ip;
    &i=*7;ip=&i;*ip=i;
    *ip++;(*ip)++;*ip *= i;
    *ip = *&i;ip++;i = ip - &i;
    dp = &i;dp = ip;&dp = &&d;

  2. Gegeven: zes korte programma's, waarvan hooguit enkele juist zijn. Geef aan welke niet, en waarom. Als je dit foutloos doet, heb je de basiswerking van pointers door. Gebruik enkel pen en papier!
    Hint: deel je blad op in twee kolommen, waarbij links de (gewone en pointer-)variabelen komen die in het hoofdprogramma gebruikt worden, en waarbij rechts de variabelen uit de functie/procedure getekend worden. Geef aan wat er gebeurt bij oproep van de module, en bij afsluiten van de module (i.e. welke (waarden van de) variabelen worden doorgegeven van hoofdprogramma naar module, en omgekeerd?) De lokale variabelen teken je in blauw, de dynamische variabelen in rood.
    Wat ontbreekt in programma 1, 5 en 6?

    VERSIE 1
    double* kwadraat (double a){
        double *q= new double;
        *q = a*a;
        return q;
    }
    int main(){
        double* p;
        p=kwadraat(3.1);
        cout<<*p;
    }
    VERSIE 4
    void kwadrateer 
         (double*& q, double a){
        *q = a*a;
    }
    int main(){
        double* p;
        kwadrateer(p,3.1);
        cout<<*p;
    }
    VERSIE 2
    double* kwadraat (double a){
        double q;
        q = a*a;
        return &q;
    }
    int main(){
        double *p;
        p=kwadraat(3.1);
        cout<<*p;
    }
    VERSIE 5
    void kwadrateer 
         (double* q, double a){
        q=new double;
        *q = a*a;
    }
    int main(){
        double* p;
        kwadrateer(p,3.1);
        cout<<*p;
    }
    VERSIE 3
    void kwadrateer 
         (double* q, double a){
        *q = a*a;
    }
    int main(){
        double* p;
        kwadrateer(p,3.1);
        cout<<*p;
    }
    VERSIE 6
    void kwadrateer 
         (double*& q, double a){
        q=new double;
        *q = a*a;
    }
    int main(){
        double* p;
        kwadrateer(p,3.1);
        cout<<*p;
    }

  3. Schrijf een functie c_str(str) die een string str omzet in een C-string. In de programmeertaal C wordt een string voorgesteld als een pointer naar een tabel van karakters afgesloten met 0. De functie c_str geeft bijgevolg een pointer terug!

    Schrijf een bijhorend hoofdprogramma om deze functie uit te testen.

  4. Gegeven volgende code.
    #include <iostream>
    #include <fstream>
    #include <string>
    using namespace std;
    
    const int MAX = 25;
    
    void lees_bestand(string bestandsnaam, string items[],int &aantal)
    {
         aantal = 0;
         ifstream invoer(bestandsnaam.c_str());
         if (invoer.is_open())
         {
              string lijn;
              getline(invoer, lijn);
              while (!invoer.fail() && aantal<MAX)
              {
                   if (lijn.size() > 0)
                   {
                        items[aantal] = lijn;
                        aantal++;
                   }
                   getline(invoer, lijn);
              }
              if (! invoer.eof())
                  cout<<bestandsnaam<<" bevat fouten of is niet volledig ingelezen "<<endl;
         }
         invoer.close();
    }
    
    void schrijf(const string items[],int n){
        for(int i=0;i<n;i++)
            cout<<items[i]<<endl;
    }
    
    int main()
    {
        string namen[MAX];
        int aantal;
        lees_bestand("namen.txt", namen, aantal);
        schrijf(namen,aantal);
        return 0;
    }
    

    Verwijder hierin de constante MAX, en pas de code aan zodat de tabel dynamisch wordt aangemaakt in de module en nooit groter is dan noodzakelijk.
    Vervang in de hoofding van elke module string items[] door string *items, en doe de nodige aanpassingen zodat het programma terug werkt.

Gevolgen voor constructor en destructor in C++

De constructor is de member functie die gebruikt wordt om een object een begintoestand te geven. Deze zal bij het creëren van een object zorgen voor een voorinstelling van de waardes van interne variabelen. Let wel: de (private) dataleden worden automatisch ("achter de schermen") gedeclareerd, de initialisatie ervan moet je echter zelf voorzien in de code van de constructor (eventueel via initialiser list).

De destructor is de lidfunctie die een object netjes opruimt als de levensduur van het object afgelopen is. Deze wordt nooit expliciet aangeroepen, maar moet wel geïmplementeerd worden. Een destructor zal altijd, automatisch, zonder dat je code moet toevoegen, alle lidvelden terug vrijgeven aan het geheugen. Maar als één van die lidvelden een pointer is, zal ENKEL de pointer vernietigd worden, en NIET de variabele/het object waarnaar die pointer verwijst. Dat wil dus zeggen dat dat object voor altijd in het geheugen blijft zitten (er is nl. geen wegwijzer meer naartoe). Gevolg: memory leak.

Hoe kan je nagaan of je een memory leak hebt? Daarvoor schrijf je (in de testfase van je programma) in de destructor uit wAt je precies vernietigt.
(Let op: "delete k;" met k een nullpointer is mogelijk,
maar "cout<<"ik vernietig nu knoop met sleutel "<<k->sl<<endl;" met k een nullpointer werkt niet. (Maak eventueel onderscheid met if/else bij het uitschrijven.)
Tel bij het runnen van je programma na: heb je 15 knopen aangemaakt, dan moet je 15 keer de "delete"-melding krijgen op je scherm (al dan niet op het einde van je programma).

Hoe kan je een memory leak vermijden? Door consequent "delete" te gebruiken waar nodig (in de destructor dus).






















Hoofdstuk 3: gelinkte lijsten

Gelinkte lijst: niet-recursieve basisversie

Hieronder de interface van een lijst. Vul de code aan zodat de gegeven lidfuncties doen wat ze aangeven, m.a.w.: implementeer de klasse. Meer over interfaces en afspraken tussen programmeurs krijg je later nog. Nu enkel dit: wijzigen aan de interface MAG NIET tenzij na goedkeuring van de leveranciers. Extra private of protected lidfuncties aanbrengen kan wel; gezien die niet tot de interface behoren.
typedef int T;   // type van een sleutel; te vervangen indien nodig
class Lijstknoop;
class Lijst{
    public:
        Lijst();       // constructor: maak een nieuwe lijst aan
        ~Lijst();      // destructor: geef alles wat je met new aanmaakte,
                       // hier weer vrij aan het geheugen.
     
        int geefaantal() const;   
        void voegtoe (const T &);
        void verwijder (const T &);
        
        friend ostream& operator<< (ostream& os, const Lijst& l){
            l.schrijf(os);
            return os;   
        }
        
    protected:
        Lijstknoop * eerste;        
        void schrijf(ostream & os) const;
};

class Lijstknoop{
    friend class Lijst;
    private:
        T sl;
        Lijstknoop* volgende;
        Lijstknoop();
        ~Lijstknoop();
        Lijstknoop(const T&);  
};         
Zoek eerst uit welke functies je eerst implementeert, voor je de eerste keer test. Schrijf niet meteen teveel code: zorg voor een tussenstap, die toch een controleerbaar geheel vormt. (Controleerbaar wil zeggen dat je al minstens kan uitschrijven wat er intern door het programma werd opgeslagen.)







Gelinkte lijst: recursieve basisversie

We hebben zonet een eerste mogelijke implementatie van de klasse Lijst gegeven. Nu is een alternatieve implementatie van diezelfde interface aan de beurt. Ga maar na: alle publieke lidfuncties zijn gelijk qua naamgeving en signatuur (uitvoertype, aantal en type van parameters,...). De private of protected dataleden zijn echter verschillend. Gezien een deellijst van een lijst immers ook een lijst is, is recursief programmeren voor deze structuur geen gek idee. Maar om recursief te programmeren, moet ook de interne opbouw van de lijst die recursieve structuur weerspiegelen. Met andere woorden: elke deellijst moet eruit zien als de 'startlijst'.

typedef int T;   // type van een sleutel; te vervangen indien nodig

class Lijstknoop;
class Lijst{
    public:
        Lijst();       // constructor: maak een nieuwe lijst aan
        ~Lijst();      // destructor: geef alles wat je met new aanmaakte,
                       // hier weer vrij aan het geheugen.
        
        int geefaantal()const;
        void voegtoe (const T &);
        void verwijder (const T &);
        
        friend ostream& operator<< (ostream& os, const Lijst& l){
            l.schrijf(os);
            return os;   
        }
        
    protected:
        Lijstknoop * k;    // naamwijziging...
        void schrijf(ostream & os) const;
};

class Lijstknoop{
    friend class Lijst;
    private:
        T sl;
        Lijst volgende;    // enige punt waarop interface 
                           // recursief/niet-recursief verschilt
        Lijstknoop();
        ~Lijstknoop();
        Lijstknoop(const T&);  
};  

Gelinkte lijst: niet-recursieve uitgebreide versie

Breid de interface van de lijst uit met de lidfuncties verwijdereerste() en zoek(const T &). Deze laatste lidfunctie geeft een poitner terug als uitvoer. Gebruik deze functie ook in de verwijderfunctie die reeds geschreven was. (BASISREFLEX van de programmeur: doe nooit dubbel werk! Dupliceer nooit code!)

Gelinkte lijst: recursieve uitgebreide versie

Doe nu hetzelfde voor de recursieve versie.

Dubbelgelinkte lijst

Breid je gelinkte lijst (recursieve versie) uit met een extra link tussen twee opeenvolgende knopen. Pas alle lidfuncties aan, zodat deze link altijd goed staat. Test uitvoerig!

Let op:

  1. De code van de gelinkte lijsten die je hier maakt moet foutloos zijn! Deze klasse vormt immers de basis voor de labo's algoritmen - en elke fout in je code zal je dus blijven achtervolgen. Je moet de code die je schrijft dus TESTEN. Dat geldt voor alle code die je schrijft. Je moet er altijd over nadenken hoe je dat doet. Probeer zo veel mogelijk omstandigheden te bedenken. In het geval van een lijst: probeer dingen toe te voegen. Probeer dingen te verwijderen die er zijn, die herhaalde malen voorkomen. Probeer dingen te verwijderen uit een lege lijst. Vul een lijst op, haal hem leeg, vul hem terug op, enzovoorts. Alleen zo vind je de fouten, en alleen als je de fouten vindt wordt je code bruikbaar.
  2. Houd voor jezelf de tijd bij die je met pen en papier spendeert aan de vraag, versus de tijd die je doorbrengt aan je klavier. We schatten een normale verhouding op 2:1 (met 2 wel degelijk in het voordeel van pen en papier).
  3. Hoeveel schetsjes van lijsten en/of knopen heb je gemaakt bij het ontwerpen van je code?

Sneeuwbaleffect bij destructor

Gegeven de (afgeslankte) Lijst- en Lijstknoopklasse:

class Lijst{
    protected:	
        Lijstknoop* k;
};

class Lijstknoop{
    protected:
        int sl;
        Lijst volgend; 		
};

Stel dat je ergens in je code, voor een bepaalde Lijstknoop* k, "delete k" schrijft. Wat gebeurt er dan? Het object waar k naar wijst (een Lijstknoop dus) wordt weer vrijgegeven aan het geheugen. Dus wordt de destructor opgeroepen voor die Lijstknoop met naam *k. Voor het datalid sl van *k betekent dit niets speciaals (geheugenplaats voor een int wordt gewoon vrijgegeven). Het datalid volgend van *k is echter een Lijst, dus wordt de destructor van de klasse Lijst opgeroepen. Dit impliceert: delete l (met l=k->volgend.k), dus wordt ook de volgende knoop in het lijstje vrijgegeven aan het geheugen. Vandaar: het sneeuwbaleffect bij de destructor. Opgelet, dit heeft soms ongewenste effecten. Kijk vooral na of het verwijderen van een knoop uit een lijst netjes gebeurt (delete niet vergeten, maar ook niet teveel verwijderen.)