Skip to content

Latest commit

 

History

History
679 lines (525 loc) · 34.6 KB

de_programmeertaal_c_deel_5.md

File metadata and controls

679 lines (525 loc) · 34.6 KB
                D E   P R O G R A M M E E R T A A L   C 
      
                              D E E L   5 
                                           
      
      
      N.B.: Dit is het vijfde deel van de serie. Bekendheid met de 
      eerste vier delen wordt verondersteld!
      
      
             C O M P L E X E   D A T A T Y P E N   I N   C 
      
      
                           I N L E I D I N G 
      
      De  kracht van C zit hem in het feit dat er zowel 'dichtbij' 
      de  machine  geprogrammeerd  kan  worden  (waardoor  er veel 
      minder vaak  naar assembler  hoeft te  worden gegrepen)  als 
      heel  abstract, iets  wat PASCAL  programmeurs vaak  als hun 
      specifieke terrein beschouwen.
      
      In  feite   heeft  PASCAL   maar  twee  dingen  voor  op  C: 
      enumeraties   en  sets.   Enumeraties  zijn  gemakkelijk  te 
      vervangen door  constanten, sets  geven wat  meer problemen, 
      maar  in C zijn wel uitgebreide bitmanipulaties mogelijk die 
      dit gemis  kunnen opvangen  - en  een stuk  efficienter. (In 
      ANSI   C  heeft   men  de   taal  overigens  uitgebreid  met 
      enumeraties, maar ANSI C is niet voor de MSX te krijgen.)
      
      Bij voorbaat  wil ik  de gebruikers  van Small C waarschuwen 
      dat  het meeste  van wat in deze aflevering staat niet geldt 
      voor deze compiler: buiten het enkelvoudige array kent Small 
      C   geen   samengestelde   datatypen,   geen   casts,   geen 
      initializers en is het geheugenbeheer - mogelijk afhankelijk 
      van de verschillende versies - beperkt.
      
      
                 S A M E N G E S T E L D E   T Y P E N 
      
      Of we  abstract kunnen programmeren hangt er vooral vanaf of 
      we  onze gegevens astract kunnen maken. Blijven we steken op 
      het niveau  van bits,  bytes of words (machinetaal, maar ook 
      BASIC)   dan  maken   we  het   onszelf  heel   moeilijk  om 
      begrijpelijke en  dus foutloze  programma's te schrijven als 
      we wat meer gecompliceerde algorithmen willen gebruiken. 
      
      
        A R R A Y 'S ,   P O I N T E R S   E N   D E   "C A S T "
      
      Array's   en  pointers  zijn  natuurlijk  in  deel  drie  al 
      behandeld, toch  komen hier  nog een paar dingen aan de orde 
      die niet eerder aan bod zijn gekomen.
      
      In  de eerste  plaats moet  ook binnen  declaraties rekening 
      worden   gehouden   met  de   prioriteit  en   volgorde  van 
      "evaluatie"   van   de   "operatoren".   Ik   gebruik   hier 
      aanhalingstekens omdat er natuurlijk niet echt sprake is van 
      een rekenkundige uitdrukking, maar het lijkt er veel op.
      
      In declaraties  komen gelukkig alleen de '()', de '[]' en de 
      '*'  "operator" voor.  De '()'  en de '[]' hebben een hogere 
      prioriteit  dan  de  '*'.  (De  '()'is  de  'functie-return' 
      operator;  deze  valt  echter  buiten  het  bestek van  deze 
      aflevering.) Gewone  haakjes kunnen gebruikt worden als deze 
      prioriteit  ons niet bevalt. De "evaluatie" verloopt voor de 
      '()' en  '[]' van  links naar rechts, die van de '*' precies 
      omgekeerd.   Gelukkig  hoeven   we  met   dat  laatste   bij 
      declaraties  geen  rekening te  houden, omdat  het de  enige 
      "operator"  is  met die  prioriteit die  in declaraties  kan 
      worden gebruikt.
      
      Zo is na
      
                char *vb1[10];
      
      'vb1' een array van 10 pointers naar char, en na
      
                char (*vb2)[10];
      
      'vb2' een  pointer naar  een array  van 10  chars, iets heel 
      anders. In het laatste geval hadden we ook kunnen schrijven:
      
                char (*vb2)[];
      
      Er wordt namelijk geen array gedeclareerd (waarvoor geheugen 
      gereserveerd  zou moeten  worden), maar een pointer naar een 
      array. Binnen  C hoef  je in  zo'n geval  de grootte van het 
      array niet op te geven, wat logisch is: in C wordt toch niet 
      gecontroleerd of een array index binnen de grenzen valt!
      
      Het is in principe mogelijk de declaraties zo ingewikkeld te 
      maken  als je  zelf wilt, als je bovenstaande regels maar in 
      acht neemt. (In het bijzonder kun je dus een array maken met 
      twee of meer dimensies!) Het is echter bijzonder gemakkelijk 
      om hierin  fouten te  maken, of  het overzicht te verliezen, 
      vooral omdat lang niet alle compilers meldingen geven als we 
      dingen  (bijvoorbeeld pointers  en int's)  doorelkaar halen. 
      ASCII-C vormt hierop gelukkig een uitzondering.
      
      Maar stel  dat we  juist wel  bepaalde datatypen  doorelkaar 
      willen  gebruiken? Dit  komt vaker voor dan je op het eerste 
      gezicht denkt.  Zo verwachten  sommige standaardfuncties een 
      unsigned  als een van hun parameters, maar vaak is de waarde 
      alleen als  int beschikbaar. (Zie ook 3.1 voor een voorbeeld 
      hiervan.) Hoe gaan we dan te werk?
      
      Allereerst  moet  de  de waarde  van de  int natuurlijk  wel 
      positief  zijn, want unsigned's zijn altijd positief. Is dat 
      het geval  dan volstaat het voor de uitdrukking '(unsigned)' 
      te  zetten. In  meer algemene termen: zet een declaratie van 
      het gewenste  type waarin een variabelenaam ontbreekt tussen 
      haakjes  en voor  de uitdrukking,  en voila!  We hebben  van 
      datatype gewisseld. Deze operatie wordt een 'cast' genoemd.
      
      Een  'cast'  kan alleen  worden uitgevoerd  bij enkelvoudige 
      datatypes (unsigned, int, char en pointers), wat logisch is, 
      want alleen  deze mogen  gebruikt worden  bij een assignment 
      ('='  operator), of  als functieparameter en -return. Ook de 
      'cast' operator  heeft een  prioriteit, uiteraard, en die is 
      dezelfde is als die van '++', '--', enz.
      
      Overigens  wordt bij een assignment meestal een automatische 
      cast uitgevoerd naar het gewenste type, bijvoorbeeld:
      
                int g1;
                char g2;
      
                g1 = 65;
      
                g2 = g1;
      
      Hierna bevat  g2 een  'A', als  het systeem waarop we werken 
      tenminste   de  ASCII-code   gebruikt.  Waar   echter  nooit 
      automatische casts worden uitgevoerd zijn functie-parameters 
      en -returns, wat bij ASCII-C tot problemen kan leiden, omdat 
      char's aan de ene kant, en unsigned's, int's en pointers aan 
      de andere  kant op  verschillende manieren  worden door-  en 
      teruggegeven.  De  functie  parameter  checker  van  ASCII-C 
      (FPC.COM)  ontdekt dit  soort fouten  wel, maar  het is niet 
      verplicht deze te gebruiken!
      
      Bij HiSoft C moet een cast op een iets andere manier gegeven 
      worden: voor  de haakjes  moet nog  het woord  'cast' worden 
      gebruikt.   Het  al  eerder  gebruikte  voorbeeld  zou  dan: 
      'cast(unsigned)' worden.  Naar eigen zeggen is dit gedaan om 
      de  compiler simpeler  te houden, maar het komt niet overeen 
      met enige standaard.
      
      
      
                             S T R U C T S 
      
      Structs  gebruiken  we  als  we  verschillende  gegevens bij 
      elkaar willen opslaan. Als voorbeeld nemen we een punt in de 
      ruimte: deze  heeft een X, Y en Z coordinaat. Gesteld dat we 
      genoegen nemen met int's voor deze coordinaten dan hebben we 
      na
      
                struct point { int x,y,z; } pnt1;
      
      een  variabele 'pnt1'  gecreeerd die  uit drie int's bestaat 
      met  de  namen  'x',  'y'  en  'z'. Deze  struktuur (vandaar 
      'struct') is  nu bekend  onder de naam 'point', zodat we met 
      bijvoorbeeld
      
                struct point pnt2;
      
      de  variabele 'pnt2'  in het  leven roepen,  die op  precies 
      dezelfde  manier  is  samengesteld.  We zijn  overigens niet 
      verplicht een  struct een naam (we spreken in C ook wel over 
      'tag  name')  te  geven,  maar  het  is  vaak  heel  handig. 
      Omgekeerd  kun je  ook de naam van de variabele weglaten. Er 
      wordt dan uiteraard geen variabele gecreeerd, maar de struct 
      is  dan   wel  gedefinieerd.   Dit  gebeurt  nogal  eens  in 
      '#include'  files waarin  we dit  soort definities  voor een 
      aantal modules tegelijk willen vastleggen.
      
      Om toegang  te krijgen  tot het "inwendige" van een struct - 
      de  'members'  (=  leden)  -  gebruiken we  de '.'  operator 
      gevolgd  door de  naam van  het lid.  Als we de Z-coordinaat 
      willen bereiken van 'pnt1' is
      
                pnt1.z
      
      voldoende,  en  dit  samenstel  is als  gewone variabele  te 
      gebruiken.  Veel  algorithmen maken  gebruik van  lijsten of 
      boomstrukturen. Hiervoor is nodig dat er in de de struct een 
      of meer pointers zitten naar andere variabelen van hetzelfde 
      type. Als  voorbeeld kunnen we denken aan een lijst woorden, 
      of  namen. De  pointer in de struct wijst dan naar de struct 
      met  de   volgende  naam,  en  de  pointer  daarin  naar  de 
      daaropvolgende, enzovoort. (In de laatste struct maken we de 
      pointer NULL.)
      
                struct name { struct name *next;
                              char print_name[20]; } firstname;
      
      Dit geeft aan hoe we zoiets kunnen doen. Nu we het toch over 
      pointers hebben: er bestaat een verkorte schrijfwijze om een 
      member  van een struct te bereiken uitgaande van een pointer 
      naar een  struct. Hou  bovenstaande struct  in gedachten, en 
      bekijk het stukje C hieronder:
      
                struct name *pointer;
      
                pointer = &firstname;
      
                pointer = pointer->next;
      
      Het  gaat hier  natuurlijk om de '->'. Volledig identiek met 
      de laatste regel zou zijn:
      
                pointer = (*pointer).next;
      
      (De  haakjes  zijn nodig  omdat de  '.' operator  een hogere 
      prioriteit  heeft  dan de  '*' operator.)  Het behoeft  geen 
      betoog dat  de andere  schrijfwijze korter  is, en  zeker zo 
      duidelijk.
      
      Als  een  struct  eenmaal  'bekend'  is,  of  beter  gezegd: 
      gedefinieerd  is, kan  een struct precies zo gebruikt worden 
      als een  gewone variabele,  al zijn er een paar restricties. 
      Zo  kan een struct niet als parameter van een functie worden 
      gebruikt, maar  een pointer  naar een struct mag wel, en dat 
      voldoet in bijna alle gevallen even goed.
      
      Een vervelender  restrictie is  het feit dat de '=' operator 
      niet gebruikt mag worden op structs: we moeten de leden stuk 
      voor  stuk kopieren,  of gebruik  maken van  de 'movmem'- of 
      'memcpy'-functie.  (Deze  functies worden  verderop in  deze 
      tekst besproken.)
      
      Wat  wel weer  mag is  in de  definitie van  een struct  een 
      andere struct  gebruiken. Een  uitgebreid voorbeeld  hiervan 
      staat aan het eind van het volgende gedeelte.
      
      
      
                              U N I O N S 
      
      Unions  lijken een  beetje op  structs. De  manier waarop ze 
      gedefinieerd en  gebruikt worden is zelfs precies gelijk aan 
      structs,  als je het woord 'union' voor 'struct' invult. Het 
      grote  verschil  is dat  in een  struct alle  leden tegelijk 
      bruikbaar zijn, en in een union slechts een lid tegelijk. Of 
      om het anders te zeggen: in een union zijn de leden 'bovenop 
      elkaar' gelegd, en in een struct 'naast elkaar'.
      
      Een voorbeeldje maakt alles misschien duidelijker:
      
                union char_int { char letter;
                                 int getal; } uvar;
      
      We kunnen nu bijvoorbeeld doen:
      
                uvar.letter = 'A';
      
      Nu bevat uvar.letter het karakter 'A', en is uvar.getal niet 
      gedefinieerd. Na
      
                uvar.getal = 123;
      
      bevat  uvar.getal  de  waarde  123,  en  is  de  waarde  van 
      uvar.letter niet gedefinieerd.
      
      Hadden we  een struct gebruikt, in plaats van een union, dan 
      had uvar.letter nu nog steeds de waarde 'A', maar een struct 
      gebruikt  daarom  wel  meer  geheugen:  een struct  gebruikt 
      evenveel  geheugen als de som van het geheugengebruik van de 
      afzonderlijke  leden,  een  union  gebruikt  slechts  zoveel 
      geheugen als het grootste lid.
      
      Bovenstaand voorbeeld  zal wel  het gebruik, maar zeker niet 
      de zin van de union duidelijk maken. Als leden van een union 
      worden   dan  ook   meestal  zelf  ook  samengestelde  typen 
      (array's, structs,  unions) gebruikt,  al dan niet samen met 
      simpeler  typen.  Als  voorbeeld  een  door  mijzelf  in een 
      programma gedefinieerde struct:
      
      struct node { char nodetype;
                 union {
                   struct { struct node *left, *right; } ptr_node;
                   CONSTANT con_node;
                   VARIABLE var_node; } nodeinfo ; };
      
      CONSTANT  en  VARIABLE  zijn  met  een  'typedef'  (zie 2.4) 
      gedefinieerde  types. Binnenin  de struct  'node ' worden op 
      hun  beurt  een  struct  en  een  union gedefinieerd.  Om de 
      variabele  'left'   te  bereiken   is  na  de  naam  van  de 
      struct-variabele  nodig:  '.nodeinfo.ptrnode.left',  waarbij 
      'nodeinfo'  de naam  is van de union, het tweede lid van van 
      de struct  'node', 'ptrnode'  de naam wan het eerste lid van 
      de  union, en  zelf een  struct, en  'left' de  naam van het 
      eerste lid van de binnenste struct. Het is even wennen.
      
      Overigens, omdat  ik juist 'left' en 'right' heel vaak moest 
      gebruiken, heb ik de volgende twee #defines gemaakt:
      
                #define NLEFT nodeinfo.ptrnode.left
                #define NRIGHT nodeinfo.ptrnode.right
      
      Dat voorkomt kramp in de typevingers.
      
      
             N I E U W E   T Y P E N   D E F I N I E R E N 
      
      In  het geval  van structs  en unions  kunnen we een eenmaal 
      gedefinieerd type  telkens opnieuw gebruiken, als we er maar 
      voor  zorgen dat  we het  een 'tag name' hebben gegeven. Bij 
      arrays  of   ingewikkelde  pointer   types,  of  erger  nog: 
      combinaties ervan, zou het handig zijn (en fouten voorkomen) 
      als we het type slechts een keer moesten definieren. Dat kan 
      met een zogenaamde 'typedef'.
      
      Het  gebruik van 'typedef' is heel eenvoudig. Als je uitgaat 
      van de  declaratie van  de variabele  van het gewenste type, 
      volstaat  het om er het woordje 'typedef' voor te zetten, en 
      de naam  van de  variabele te  vervangen door de naam die je 
      het nieuwe type wilt geven.
      
      Als  je  bijvoorbeeld  een type  LONG wilt  maken (omdat  je 
      compiler - zucht - geen long's kent) kun je bijvoorbeeld als 
      volgt te werk gaan:
      
                typedef char LONG[4];
      
      Een  LONG is dan gedefinieerd als een array van 4 char's, en 
      kan nu in declaraties worden gebruikt, zoals:
      
                LONG lang_getal1, lang_getal2;
      
      Natuurlijk heft  dat geen van de beperkingen op die voor het 
      specifieke datatype gelden en dus zal bijvoorbeeld
      
                lang_getal1 = lang_getal2;
      
      tot  een foutmelding  leiden, zoiets  als "must  be lvalue", 
      omdat we hier in feite arraynamen, en dus pointerconstanten, 
      gebruiken! (Dat  heeft weer  wel als voordeel dat we bij ons 
      nieuwe  type deze  arraynaam zonder  meer als  parameter aan 
      functies kunnen doorgeven, zonder dat we specifiek het adres 
      hadden moeten doorgeven; alles heeft voor- en nadelen.)
      
        - Het volgende tekstdeel kunt u in het submenu vinden -
      
      
      
         - Het eerste tekstdeel kunt u in het submenu vinden -
      
                D E   P R O G R A M M E E R T A A L   C 
      
                              D E E L   5 
                                           
      
      
                 G E H E U G E N B E H E E R   I N   C 
      
      PASCAL programmeurs zullen zich bij al die pointers wel even 
      het hoofd hebben geschud. Immers, de enige manier waarop zij 
      met pointers  kunnen werken  is na  een NEW, die een nieuwe, 
      anonieme   variabele   cre�ert,   en   een   pointer  ernaar 
      teruggeeft.  Dat is (officieel) de enige manier waarop je in 
      PASCAL aan een pointer kunt komen.
      
      Deze  beperkingen  zijn  waarschijnlijk  opzettelijk,  omdat 
      pointers tevens  een belangrijke bron van fouten zijn. Zelfs 
      binnen   het  keurslijf  van  PASCAL  kunnen  pointers  naar 
      niet-meer-bestaande objecten overblijven na een MARK/RELEASE 
      of DISPOSE.
      
      Toch  is de  keuze die  men voor PASCAL gemaakt heeft zo gek 
      nog niet:  bij anonieme  variabelen moet  je immers  wel met 
      pointers  werken,  omdat  je  er  niet  bij  naam  naar  kan 
      verwijzen,  en bij  veel algorithmen  die gebruik  maken van 
      bomen  ('trees')  en  lijsten  ('linked  lists')  -  en  dus 
      pointers -  moet je  nieuwe objecten  kunnen cre�ren en oude 
      vernietigen.   Vandaar  dat   men  die   link  (woordspeling 
      opzettelijk!) tussen geheugenbeheer en pointergebruik legde.
      
      Inderdaad, geheugenbeheer.  Voor wie  het nog niet door had: 
      als  je  een  nieuwe  variablele  wilt  cre�ren  dan heb  je 
      (ongebruikt)  geheugen nodig,  en dat  geheugen moet  ergens 
      vandaan komen.  In C  zijn daar verschillende manieren voor, 
      de meest gebruikelijke zijn echter:
      
                calloc(aantal, grootte)
                malloc(grootte)
                alloc(grootte)
      
      De   functie  'calloc'  reserveert  geheugen  voor  'aantal' 
      objecten van  'grootte' bytes,  'malloc' en 'alloc' een blok 
      van  'grootte'  bytes.  Deze twee  laatste doen  dus precies 
      hetzelfde, tenminste op byte-georienteerde systemen zoals de 
      Z80. Alle parameters van alledrie de functies zijn unsigned, 
      en  alledrie geven  ze een char * terug naar het eerste byte 
      in het  blok geheugen,  of NULL  als er onvoldoende geheugen 
      beschikbaar is.
      
      In  tegenstelling tot  PASCAL kunnen  we dus niet direct een 
      bepaald  object  cre�ren, we  zijn er  zelf verantwoordelijk 
      voor  om  de  juiste  hoeveelheid  geheugen  te  reserveren. 
      Gelukkig is dat niet zo moeilijk. Willen we bijvoorbeeld een 
      nieuw object van het type 'struct name' (zie eerder) dan kan 
      dat met
      
                malloc(sizeof(struct name))
      
      en bij  wat strengere  compilers moeten  we bovendien  casts 
      gebruiken, en dan wordt het bovenstaande:
      
              (struct name *)malloc((unsigned)sizeof(struct name))
      
      Dat   ziet  er  lelijk  -  en  onbegrijpelijk  -  uit.  Zo'n 
      wanproduct kunnen we binnen een aparte functie plaatsen (wat 
      we kunnen  combineren met  een actie  wanneer er onvoldoende 
      geheugen beschikbaar is) zoals:
      
      struct name *newname()
       {
         char *temp;
      
         if ((temp = malloc((unsigned)sizeof(struct name))) ==
            NULL) { fputs("Onvoldoende geheugen!\n", stderr);
           exit(1); } /* einde programma! */
      
         return (struct name *)temp;
       }
      
      Als   onze  preprocessor   voldoende  krachtig  is,  en  ook 
      #define's met  parameters accepteert  (zoals ASCII-C) kunnen 
      we ook het volgende doen:
      
              #define NEW(x) (((x) *)malloc((unsigned)sizeof(x)))
      
      en  dan reserveren  we altijd de juiste hoeveelheid geheugen 
      en krijgen we altijd het juiste type pointer terug, wat voor 
      data-type we  ook voor x invullen. Nu kunnen we eenvoudig "a 
      la PASCAL"
      
                NEW(struct name)
      
      doen.  Et voila! Deze manier is heel efficient, vooral omdat 
      het ons  geen byte aan code extra kost: al die casts zijn er 
      tenslotte voor de interne administratie van de compiler.
      
      Omdat  'calloc(aantal, grootte')  precies hetzelfde doet als 
      'malloc(aantal  *  grootte)'  zal  'calloc'  hier  wel  geen 
      verdere  toelichting  behoeven.  Waar  in  de  rest van  het 
      verhaal  'malloc'  staat kan  dus ook  - mutatis  mutandis - 
      'calloc' of 'alloc' worden gelezen.
      
      Het geheugen dat we met een van bovenstaande functies hebben 
      gekregen kunnen  we ook  aan het  systeem teruggeven, waarna 
      het  weer voor beschikbaar wordt voor een volgende 'malloc', 
      'alloc' of 'calloc'. Hiervoor is de functie
      
                free(pointer)
      
      beschikbaar.  De  parameter 'pointer'  is een  char *  en we 
      moeten hier  dezelfde pointer  gebruiken die we via 'malloc' 
      e.d.  hebben gekregen.  De functie  'free' geeft geen waarde 
      terug.
      
      Meer voor  de volledigheid  dan om praktische redenen wil ik 
      hier  nog vermelden  dat C nog een functie heeft om geheugen 
      te  reserveren,   namelijk  'sbrk'.  Niet  alle  C-compilers 
      ondersteunen deze functie, en bovendien kan met deze functie 
      gereserveerd  geheugen  niet  terug worden  gegeven aan  het 
      systeem. Wederom een Unix-relikwie.
      
      Ook  kent  C  nog  een  paar, weinig  gebruikte functies  om 
      blokken geheugen te verplaatsen of te vullen. Deze zijn:
      
                memcpy(dst, src, len)
                movmem(src, dst, len)
                memset(dst, byte, len)
                setmem(dst, len, byte)
      
      De  functies  'memcpy'  en  'movmem'  verplaatsen  een  blok 
      geheugen  dat  'len'  bytes  groot is  van 'src'  naar 'dst' 
      ('len'  is  unsigned,  'dst'  en  'src'  zijn char  *). Deze 
      functies zijn identiek, op de volgorde van de argumenten na. 
      Overigens mogen  de 'src' en 'dst' gebieden niet overlappen. 
      De  functies 'memset'  en 'setmem' vullen een geheugengebied 
      vanaf 'dst', dat 'len' bytes groot, is met de waarde 'byte', 
      een char.  Ook bij  deze twee functies is alleen de volgorde 
      van  de argumenten  verschillend. Aangezien  het ANSI comite 
      aan de  functies 'memcpy'  en 'memset'  de voorkeur geeft is 
      het misschien verstandig dit zelf ook te doen.
      
      
           S T R A T E G I E E N   V A N   G E H E U G E N - 
      
            B E H E E R :   E E N   V E R G E L I J K I N G 
      
      Vooral  assemblyprogrammeurs zullen  bij bovenstaand verhaal 
      de oren  hebben gespitst  en denken:  "maar zijn er dan geen 
      restricties in de volgorde waarin geheugen wordt aangevraagd 
      en  teruggegeven, en moet bij 'free' niet de grootte van het 
      blok geheugenblok worden vermeld?" Op beide vragen luidt het 
      antwoord:  nee!  Het  geheugenbeheersysteem  van  C  is  erg 
      flexibel wat  dat betreft,  maar voor  alles moet  een prijs 
      worden betaald.
      
      In  de eerste plaats moet het systeem zelf bijhouden hoeveel 
      geheugen er  in een blok zit. De meestgebruikte manier omdat 
      te  doen is  de grootte van een blok direct voor dat blok op 
      te slaan, wat op zich al wat geheugen kost.
      
      In  de  tweede plaats  moet een  blok een  bepaalde minimale 
      grootte hebben,  of soms zelfs een veelvoud van een bepaalde 
      waarde,  omdat er  zelfs in  een ongebruikt geheugenblok nog 
      bepaalde informatie moet worden opgeslagen. Deze twee feiten 
      maken het  onaantrekkelijk om  zeer veel  kleine blokken  te 
      gebruiken, omdat anders het verlies erg groot wordt. In zo'n 
      geval  kan het  aantrekkelijk zijn een of meer grote blokken 
      geheugen aan  te vragen,  en daar  zelf een  beheerstrategie 
      voor  uit te  denken. Vooral als we met stukjes geheugen van 
      allemaal  gelijk  formaat willen  werken kan  dit vaak  heel 
      eenvoudig.
      
      Als   laatste   probleem  wil   ik  noemen   het  (mogelijk) 
      versnipperen van  het geheugen  in allemaal  kleine blokken, 
      wat  tot gevolg  kan hebben dat er geen blokken geheugen van 
      een bepaalde  minimale grootte meer beschikbaar zijn, hoewel 
      de totale hoeveelheid vrij geheugen ruim voldoende kan zijn. 
      Een  en ander  hangt af  van in welke volgorde we de blokken 
      aanvragen  en  weer  vrijmaken,  en hoe  het formaat  van de 
      verschillende blokken  uiteenloopt. In  de praktijk valt het 
      echter allemaal wel mee.
      
      Als vergelijking  wil ik  hier noemen de manier waarop BASIC 
      met  strings  omgaat.  Strings  in  BASIC zijn  variabel van 
      lengte,  en voor  een string  kan dus geen vaste hoeveelheid 
      geheugen worden gereserveerd. Maar de geheugenallocatie voor 
      strings wordt veel simpeler aangepakt, en geheugen wordt ook 
      niet expliciet  weer vrijgegeven.  Als gevolg daarvan is het 
      geheugen  dan  ook  vrij  snel  vol, en  vervolgens gaat  de 
      interpreter  een zogenaamde  'garbage collection' uitvoeren, 
      die uiteindelijk  tot gevolg  heeft dat  alle bezette  delen 
      achterelkaar  worden geplaatst,  en er  een enkel  vrij blok 
      geheugen ontstaat.
      
      Dit klinkt op het eerste gezicht aantrekkelijk, maar er zijn 
      een  paar   problemen.  Het  belangrijkste  is  wel  dat  de 
      informatie  verschuift, en alle pointers naar die informatie 
      aangepast moeten worden. In BASIC gaat dat nog wel, omdat de 
      interpreter  het  beheer  over  die  pointers  (lees: string 
      descriptors) voert,  en precies  weet waar ze zich bevinden. 
      In  C is  dit niet mogelijk, omdat de pointers (en mogelijke 
      kopieen daarvan)  over het  gehele geheugen verspreid kunnen 
      zijn, vaak in de geheugenblokken zelf!
      
      Dat zo'n  garbage collection  ook nog  eens bijzonder  traag 
      gaat  is de BASIC programmeur wel bekend, hoewel dat laatste 
      voor een  niet onbelangrijk  deel aan de 'gierigheid' van de 
      BASIC-interpreter  ligt: als  ze twee bytes extra per string 
      hadden geinvesteerd hadden ze de garbage collection een orde 
      sneller kunnen  maken, en  hadden bovendien niet alle string 
      descriptors  in  hetzelfde  geheugendeel  hoeven  liggen. Ik 
      spreek  hier  uit  ervaring,  want  ik  heb  zelf  eens  een 
      stringuitbreiding  voor FORTH  geprogrammeerd, en ik heb het 
      systeem nooit  zichtbaar zien  vertragen tijdens een garbage 
      collection.
      
      Maar  het feit  dat de informatie in zo'n systeem van plaats 
      kan  veranderen  kan  in  veel gevallen  toch tot  problemen 
      leiden, en  vraagt de nodige discipline van de gebruiker. In 
      bovenstaand FORTH systeem worden strings dan ook in een keer 
      naar  de stack  gekopieerd voor ermee gewerkt mag worden, en 
      ook weer  in een  keer weggeschreven,  en dat  maakt een  en 
      ander   toch  weer  wat  minder  efficient.  (Al  past  deze 
      werkwijze wel bij uitstek in de FORTH-filosofie!)
      
      Binnen TURBO  PASCAL kan  op ongeveer dezelfde manier worden 
      gewerkt  als in C met NEW/DISPOSE of GETMEM/FREEMEM, al moet 
      bij FREEMEM  in tegenstelling  tot C  wel de grootte van het 
      geheugenblok  worden  opgegeven.  Binnen  TURBO  PASCAL  kan 
      echter  ook met  MARK/RELEASE worden  gewerkt (maar dan niet 
      met DISPOSE  of FREEMEM!)  om alle  blokken die  aangevraagd 
      zijn  na een  bepaald eerder blok weer vrij te geven. Een en 
      ander   functioneert   dan   ongeveer   als  een   stack.  C 
      programmeurs die  hetzelfde willen  kunnen doen  moeten daar 
      zelf een routine voor schrijven, die echter bijzonder simpel 
      is als we in een enkel, aaneengesloten blok geheugen werken.
      
      
      
                        I N I T I A L I Z E R S 
      
      Een  van de meestgebruikte statements in BASIC is 'DATA'. Er 
      zijn nauwelijks  programma's van  enige omvang die niet zijn 
      gezegend(?!)  met  een  grote hoeveelheid  van deze  dingen, 
      meestal   aan  het  eind.  Zelfs  als  we  die  DATA's  niet 
      meerekenen die gebruikt zijn om een stukje machinetaal in te 
      zetten -  omdat het programma anders niet vooruit te branden 
      is  - blijkt  dat we er nog een hoop overhouden met tabellen 
      en dergelijke.
      
      Handige programmeurs  maken vaak gebruik van tabellen, omdat 
      het  programma  er  vaak een  stuk simpeler  en kleiner  van 
      wordt.  Ook op  dit gebied laat C niemand in de steek, omdat 
      we al  onze variabelen  mogen intialiseren met een waarde of 
      waarden  bij de  declaratie ervan.  Dit kan  met een simpele 
      variabele als volgt:
      
                int var1 = 0;
                char var2 = 'N';
                unsigned var3 = 5 * 4;
                char *var4 = "En dit is ook een constante!";
      
      We mogen  variabelen alleen initialiseren met constanten, of 
      constante  uitdrukkingen. Samengestelde  typen mogen  echter 
      alleen  initialiseren  als  het  statische  variabelen  zijn 
      (buiten een  functie is  dat altijd  zo). Voor  automatische 
      variabelen (die komen alleen binnen een functie voor) gelden 
      dezelfde  beperkingen als die voor een assignment. (In feite 
      wordt het  ook naar een assignment vertaald, wat logisch is: 
      bij het aanroepen van de functie moet de initialisa-
      tie telkens opnieuw plaatsvinden!)
      
      In  het het  laatste voorbeeld van bovenstaand rijtje moeten 
      we ons realiseren dat we een pointer variabele initialiseren 
      zodat hij  naar die  string wijst; de string zelf zit op een 
      andere plaats in het geheugen. Doen we echter dit:
      
                char var4[] = "En nu vullen we een array!";
      
      Dan  wordt var4 een array, en gevuld met boventaande string. 
      Deze methode  is wat  efficienter want we sparen een pointer 
      variabele  uit. We hoeven ook niet op te geven hoe groot het 
      array  moet  worden, want  dat is  gewoon de  lengte van  de 
      string met  afsluitende nul:  dat zoekt de compiler zelf wel 
      uit.
      
      Een   string  kan   dus  twee  betekenissen  hebben  in  een 
      initializer. De  tweede manier  gebruikt de  string als  een 
      blok  gegevens, maar dit kan ook door een aantal constanten, 
      of constante  uitdrukkingen, gescheiden door komma's, tussen 
      accoladen  te zetten.  De volgende twee declaraties zijn dan 
      ook volledig identiek:
      
                char str[] = "String";
                char str[] = { 'S', 't', 'r', 'i', 'n', 'g', 0 };
      
      In het  algemeen is de eerste vorm wat duidelijker, maar die 
      kunnen  we natuurlijk  alleen gebruiken  als het  om strings 
      gaat.  In  alle andere  gevallen en  bij alle  samengestelde 
      typen  moeten   we  van  dit  soort  gegevensblokken  tussen 
      accoladen  gebruiken. In  het geval  we samengestelde  typen 
      binnen  samengestelde   typen  gebruiken   moeten  we  zelfs 
      gegevensblokken   binnen  gegegevensblokken   gebruiken.  We 
      moeten zo'n  blok dus  eigenlijk zien  als een samengestelde 
      constante. Hier een aantal voorbeelden:
      
                int rij[] = { 1, 2, 3 };
                int matrix[][] = { { 1, 2, 3 }, { 4, 5, 6 },
                                   { 7, 8, 9 } };
                struct point pnt3 = { 0, 1, -1 };
                struct point vlak[] = { { 1, 0, 0 }, { 0, 1, 0 },
                                        { 0, 0, 1} };
                char *teksten[] = { "tekst1", "tekst2", "tekst3",
                                    "tekst4" };
      
      
      In  het laatste  geval hebben we een array van char pointers 
      gemaakt,  en  elk van  die pointers  wijst naar  een van  de 
      teksten. Verder  nog vragen?  Geen vragen!  En dus wordt dit 
      het einde van deze aflevering!
      
                                                     Robert Amesz
      
      
      P.S.:  Het zal  misschien een  aantal mensen opgevallen zijn 
      dat een voorbeeldprogramma deze aflevering ontbreekt. Sorry. 
      Dat  komt  pas  bij  aflevering  6,  wanneer het  over zulke 
      diverse onderwerpen  zal gaan als modulair programmeren, het 
      maken  van  functiebibliotheken,  debuggen,  combineren  met 
      machinetaal, enz. enz.