Inhoudsopgave

1 Inleiding 1 2 Opbouw van een programma 1 3 Lexicale structuur 3 4 Statements 3 5 Expressies 4 6 Statements II 6 7 Declaraties en typen 11 8 Constanten 13 9 Operatoren 14 10 Functies en parameters 20 11 De preprocessor 22 12 Standaardfuncties 24 13 Arrays en pointers 29 14 Strings 34 15 Declaraties en typen II 38 16 Structures 40 17 De preprocessor II 44 18 Unix-interactie 46

\begin{matrix}c \\ ◯ \end{matrix}Copyright 1992 Vakgroep Informatica, Rijksuniversiteit Utrecht

Deze tekst mag voor educatieve doeleinden gereproduceerd worden op de volgende voorwaarden:

• de tekst wordt niet veranderd of ingekort;

• in het bijzonder wordt deze mededeling ook gereproduceerd;

• de kopieën worden niet met winstoogmerk verkocht

De tekst is gezet met het L^{A}TEX systeem uit het lettertype Computer Modern. De hoofdtekst is 11 punts Roman.

1e druk januari 1992 2e druk februari 1992 3e druk januari 1993

1 Inleiding

Over de programmeertaal C zijn al vele boeken verschenen. Deze boeken zijn meestal ook een algemene inleiding in het programmeren. Wie al kan programmeren in een andere imperatieve programmeertaal, bijvoorbeeld Pascal, moet zich door grote hoeveelheden tekst heen worstelen om de relevante informatie te vinden. Speciaal voor Pascal-kenners die C willen leren is deze tekst geschreven. Veel concepten worden beschreven aan de hand van vergelijkbare constructies in Pascal, waarbij de verschillen worden benadrukt. De tekst vormt echter geen cursus programmeren: van de lezer wordt algemene kennis over bijvoorbeeld de mogelijkheden en het gebruik van variabelen, arrays, functies en parameters verwacht. C is een taal die veel wordt gebruikt onder het Unix operating system. De taal kan worden gebruikt voor high-level applicaties, maar er zijn ook mogelijkheden aanwezig voor systeem-programmering. Door de grote flexibiliteit herbergt C ook de nodige gevaren. Problemen die nieuwkomers vaak tegenkomen worden in deze tekst uitvoering besproken in speciale ‘waarschuwing’-kaders. Aanbevolen andere literatuur over C zijn de volgende boeken:

• Kernighan, B.W. & D.M Ritchie: The C programming language, 2nd edition. Prentice-Hall, 1988. (De officiële beschrijving van de taal door de ontwerpers. Tweede editie is aangepast aan de nieuwe ansistandaard).

• König, Andrew: C traps and pitfalls. Addison-Wesley, 1989. (Een boek dat, net als deze tekst, niet zozeer een programmeercursus is, maar veeleer aandacht besteed aan de aspecten waarop C afwijkt van andere imperatieve programmeertalen).

Reacties op deze tekst (fouten, omissies, onduidelijkheden) zijn welkom bij de auteur (liefst per electronic mail naar jeroen@cs.ruu.nl). Graag wil ik Maarten Pennings en Nico Verwer bedanken voor het kritisch doornemen van een voorlopige versie van deze tekst.

2 Opbouw van een programma

C-programma’s bestaan uit variabele-declaraties en functie-definities. Deze opbouw mogen door elkaar heen staan; het is dus niet nodig om alle declaraties aan het begin te zetten. Wel moeten variabelen gedeclareerd worden voordat ze gebruikt worden. Hier volgt een voorbeeld van een programma:

    int n;
    main()
    {    printf("hallo!");
    }
    char c;

In dit programma worden de variabelen n en c gedeclareerd, en er wordt een functie main gedefinieerd. (De declaratie van de variabelen is in dit geval niet zo nuttig, omdat ze niet worden gebruikt in het programma.) De functie met de naam main heeft in C een speciale betekenis: dit is het main hoofdprogramma. Op het moment dat het programma gestart wordt, wordt door het operating system als het ware de functie main aangeroepen. In elk C-programma moet daarom een functie main aanwezig zijn. De C-compiler heet cc. Files met de extensie .c bevatten gewoonlijk C- compiler programma’s. Als het voorbeeldprogramma in de file hallo.c staat, kun je het compileren door de opdracht cc hallo.c te geven. Als alles goed gaat wordt er een executeerbare file a.out gemaakt. Je kunt vervolgens a.out als opdracht geven om het programma uit te voeren. Een wat langer voorbeeldprogramma is het volgende. In dit voorbeeld zijn er geen globale variabele-declaraties; het complete programma bestaat dus uit de definitie van de functie main.

    main()
    {    int k;
         k = 0;
         while (k<10)
         {    printf( "%d\n", k );
              k = k+1;
         }
    }

Dit programma drukt de getallen van 0 tot en met 9 af. Er zijn een paar aspecten van C die je aan dit voorbeeld al kunt zien. Bijvoorbeeld: de accolades spelen de rol van begin en end. Net als in Pascal wordt dit gebruikt om accolades begin en einde van een functie aan te geven, maar onder andere ook om aan te geven welke statements bij de body van het while-statement horen. In de functie wordt een lokale variabele k gedeclareerd. In Pascal staan declaraties van lokale variabelen tussen de functie-heading en begin; in C staan ze pas na de openings-accolade. In het voorbeeldprogramma kun je verder zien dat assignment eenvoudigweg als ‘=’ wordt geschreven, en dus niet als ‘:=’ zoals in Pascal. Tenslotte is een verschil met Pascal dat er geen do hoeft te staan achter while; in plaats daarvan staat de conditie achter while tussen haakjes. De wat cryptische parameter van de functie printf wordt nader verklaard in hoofdstuk 12. De taal C werd oorspronkelijk gedefinieerd in 1972. In 1989 werd door het ansi-C Amerikaanse bureau voor standaarden (ansi) een standaard-versie van de taal vastgesteld. Deze versie wijkt op enkele plaatsen af van de originele taal, maar wordt tegenwoordig als standaard-C beschouwd. Veel compilers gaan ervan uit dat een programma in ansi-C geschreven is; aan de HP-compiler moet je dit meedelen met een optie: cc -Aa. In deze tekst zullen we uitgaan van ansi-C, maar af en toe ook de ‘klassieke’ variant noemen, omdat je in oude programma’s die constructies kunt tegenkomen.

3 Lexicale structuur

Net als in Pascal kan voor de naam van variabelen en functies een wille- identifier keurige identifier gekozen worden. Identifiers bestaan uit letters, cijfers en onderstrepings-tekens (‘ ’), maar mogen niet met een cijfer beginnen. Hoofdletters en kleine letters worden als verschillende letters beschouwd, dus aap, Aap en AAP zijn drie verschillende identifiers. Uitgesloten van gebruik als identifier zijn 27 gereserveerde woorden, ook weer vergelijkbaar met Pascal. In ansi-C zijn nog een vijftal identifiers gereserveerd. De volgende woorden zijn gereserveerde woorden: gereserveerde

woorden

      auto      double  int       struct  in ansi-C ook:
      break     else    long      switch    const
      case      extern  register  typedef   enum
      char      float   return    union     signed
      continue  for     short     unsigned  void
      default   goto    sizeof    while     volatile
      do        if      static

Natuurlijk kan in een C-programma ook commentaar worden opgenomen. Net als in Pascal mag dit op elke plaats in het programma gebeuren (alleen niet halverwege een getal, identifier enz.). Het begin van het commentaar wordt commentaar aangegeven met /* en het eind met */. Bijvoorbeeld:

    /* dit is commentaar */

Commentaar mag uit meerdere regels bestaan. Commentaar kan niet ‘ge- /*… */ nest’ worden: de symbolen /* binnen commentaar hebben geen betekenis; het commentaar is afgelopen bij de eerstvolgende */.

4 Statements

Er zijn een aantal verschillende vormen statements in C. De zes belangrijkste hiervan zijn:

• variabele = expressie ;

• if ( expressie ) statement

• if ( expressie ) statement else statement

• while ( expressie ) statement

• { declaraties statements }

• expressie ; De eerste vorm is het assignment-statement. Het enige verschil met Pascal is assignment dat er = in plaats van := wordt geschreven. De tweede en de derde vorm zijn een conditioneel statement. De expressie is meestal een Boolean expressie, bijvoorbeeld x>3. Deze expressie moet altijd tussen haakjes staan. Daardoor is een keyword then overbodig, en kan meteen een statement volgen.

De vierde vorm is een iteratief statement (‘loop’). Doordat de voorwaarde while-statement tussen haakjes staat, kan de body van de loop weer direct achter het sluithaakje staan. De body van de loop bestaat uit één statement. De vijfde vorm is een samengesteld statement (compound statement): door een compound stateaantal statements tussen accolades te plaatsen, kun je ze tot één statement ment samensmeden. Dat wordt bijvoorbeeld gebruikt als de body van een loop uit meerdere statements moet bestaan. De accolades zijn vergelijkbaar met begin en end in Pascal. In C zijn bovendien, voorafgaand aan de statements, nog declaraties toegestaan. Dit zijn lokale declaraties die alleen geldig zijn tussen lokale declaraties de accolades. Je kunt dus een hulpvariabele declareren die bijvoorbeeld alleen in de body van een loop gedefinieerd is. Als laatste vormt een expressie met een puntkomma erachter een statement. De betekenis hiervan is dat de waarde van de expressie wordt uitgerekend, en vervolgens weggegooid. Op het eerste gezicht lijkt het wat onzinnig om met het statement 3+4; de waarde 7 uit te rekenen en die meteen weer weg te gooien (dit is overigens wel legaal C). Maar als de expressie een functie-aanroep is, en de functie heeft neveneffecten, dan is dit wel degelijk zinvol. In C is er geen aparte constructie voor een procedure, zoals in Pascal. Er zijn procedure alleen maar functies. Waar je in Pascal een procedure zou gebruiken, schrijf je in C een functie waarvan het resultaat niet gebruikt wordt. De aanroep van de functie is een expressie, maar door daar een puntkomma achter te zetten wordt het een statement, vergelijkbaar met een procedure-aanroep in Pascal. De manier waarop puntkomma’s in een programma terecht komen is an- puntkomma ders dan in Pascal. Zoals uit bovenstaand overzicht blijkt, staan er puntkomma’s achter assignment-statements en achter de constructie die we gemakshalve maar ‘procedure-aanroep’ zullen noemen (en nog een paar statements die in hoofdstuk 6 besproken worden). Vergelijk dit met Pascal, waar de puntkomma’s tussen de individuele statements in een samengesteld statement staan.

5 Expressies

Om de mogelijke vormen van expressie te beschrijven gebruiken we twee nieuwe expressie begrippen:

• een primary is een eenvoudig soort expressie;

• een lvalue (spreek uit ‘el-value’) is een expressie die aan de linkerkant

van een assignment mag staan (zeg maar een variabele). Elke lvalue is ook een primary, en elke primary is op zijn beurt een expressie. Er zijn vier soorten lvalue: lvalue

• identifier

• primary [ expressie ]

• lvalue . identifier

• primary -> identifier De eerste vorm is een eenvoudige variabele (aangeduid door een identifier). De vorm daarvan werd al besproken in hoofdstuk 3. De tweede vorm is indicering van een array. Dit is ook een lvalue, omdat je aan de elementen van de array apart waarden kan toekennen. De derde en vierde vorm worden gebruikt voor record-selectie. Dat wordt verder besproken in hoofdstuk 16. De mogelijke soorten primary zijn de volgende: primary

• lvalue

• ( expressie )

• primary ( expressies )

• constant Elke lvalue is ook een primary. Daarnaast kun je een expressie tot primary maken door hem tussen haakjes te zetten. Dit wordt, net als in Pascal, gebruikt om groepering aan te brengen in een expressie. Het derde soort primary is een functie-aanroep. Deze bestaat uit een primary (meestal eenvoudigweg een functienaam) met daarachter de actuele parameters tussen haakjes. De parameters zijn nul of meer expressies, gescheiden door komma’s. Het laatste soort primary is een constante: een getal (bijvoorbeeld 25), een character (bijvoorbeeld 'A') of een string (bijvoorbeeld "hallo"). Constanten en strings worden besproken in hoofdstuk 8. De mogelijke vormen expressie zijn de volgende: expressie

• primary

• prefix-operator expressie

• expressie infix-operator expressie

• lvalue = expressie

• lvalue postfix-operator

• expressie ? expressie : expressie Elke primary is een expressie. Verder kun je van expressie grotere expressie maken door een prefix-operator voor een expressie te zetten of een infixoperator tussen twee expressies. Je kunt hierbij denken aan rekenkundige operatoren zoals +, relationele operatoren zoals <, en logische operatoren zoals ∧. De mogelijke operatoren en hun notatie worden besproken in hoofdstuk 9. In Pascal is een assignment (bijvoorbeeld x:=2) een statement. In C daarente-

assignment- expressie

gen is een assignment (bijvoorbeeld x=2) een expressie. Dat betekent dus dat een assignment een waarde heeft, zodat hij in een grotere expressie gebruikt kan worden. Als waarde van een assignment-expressie geldt de waarde van de variabele nadat de toekenning uitgevoerd is. De functie-aanroep f(3+(x=2)) zal f aanroepen met de waarde 5, nadat het assignment x=2 is uitgevoerd. Een assignment speelt in C dus bijna dezelfde rol als de andere operatoren. Het enige verschil is dat aan de linkerkant alleen een speciaal soort expressie mag staan, namelijk een lvalue. Verder heeft een expressie waarin een assignment voorkomt een neveneffect. Het is daarom niet verstandig om veel assignments neveneffect in een expressie te verwerken, omdat het programma daar erg onduidelijk van wordt. In hoofdstuk 4 over statements merkten we op dat elke expressie als statement gebruikt kan worden door er een puntkomma achter te zetten. Dat geldt expressie als

statement natuurlijk ook voor assignments: x=0; is een statement waarin x de waarde 0 krijgt. We hadden het assignment-statement dus helemaal niet apart hoeven te noemen in het overzicht van mogelijke statements. Naast prefix-operatoren bestaan er in C ook postfix-operatoren. Deze mogen postfix-operator alleen achter een lvalue staan. Een voorbeeld van een postfix-operator is ++. Dit is een afkorting voor een bepaald soort assignment: x++ heeft hetzelfde effect als x=x+1, dus x wordt één opgehoogd. Het verschil zit hem in de waarde van de expressie: in het geval van x++ is dit de waarde van x voordat hij werd opgehoogd, bij x=x+1 is dit de waarde na het ophogen. Hierop komen we terug in hoofdstuk 9. Bij gebruik als statement is er geen verschil tussen de twee vormen, omdat de resultaatwaarde toch niet wordt gebruikt. Als statement is x++; dus gewoon een kortere schrijfwijze voor x=x+1;. De laatste expressievorm gebruikt de enige ternaire operator van C. De symbolen ? en : vormen samen één operator, met drie argumenten. Deze operator wordt besproken in hoofdstuk 9.

6 Statements II

Naast de in hoofdstuk 4 genoemde vormen zijn er de volgende soorten statements:

• for ( expressie ; expressie ; expressie ) statement

• do statement while ( expressie );

• switch ( expressie ) statement

   • break ;
   • continue ;

• return expressie ;

   • ;

We zullen deze zeven vormen achtereenvolgens bespreken.

for-statement Het for-statement gebruik je, net als in Pascal, in loops waarin een teller nodig is. Bijvoorbeeld:

    for (x=0; x<10; x++) p(x);

roept de functie (‘procedure’) p aan voor alle getallen van 0 tot en met 9. De drie expressies die tussen de haakjes achter for staan, vormen achtereenvolgens de initialisatie, het criterium om door te gaan, en de manier om de teller op loop-initialisatie te hogen. Bovenstaand voorbeeld is equivalent aan de volgende statements:

    x=0;
    while (x<10)
    {    p(x);
         x++;
    }

Het for-statement is in C flexibeler dan in Pascal. Zo kun je bijvoorbeeld met stappen van twee tellen, of achteruit: stapgrootte

    for (x=0;  x<10; x=x+2) p(x);
    for (x=10; x>0;  x=x-1) p(x);

De tellende variabele hoeft geen integer te zijn. Het kan ook een real zijn, of zelfs een pointer:

    for (r=0.0; r<1.0;  r=r+0.1)   p(r);
    for (w=kop; w!=NIL; w=w->next) p(w->info);

Dit laatste is een elegante manier om een lineaire lijst te doorlopen. (In dit iteratie over lijst voorbeeld is ‘!=’ de ‘is ongelijk’-operator, en ‘->’ de C-notatie voor ‘^.’ in Pascal.) Meer over reals volgt in hoofdstuk 7, meer over pointers in hoofdstuk 13. Als de body van een for-statement maar uit één statement bestaat, zijn er geen accolades nodig (net zoals er in Pascal geen begin en end nodig is). Bestaat de body uit meerdere statements, dan moet je die met accolades groeperen tot één statement. De body mag natuurlijk weer een for-statement zijn, wat handig is voor geneste loops, zoals:

    for (x=0; x<640; x++)
        for (y=0; y<400; y++)
            p(x,y);

De expressies in een for-statement zijn optioneel. Laat je de eerste weg, dan defaults wordt de teller niet geïnitialiseerd, laat je de tweede weg dan gaat de loop oneindig lang door (tenzij hij op een andere manier wordt afgebroken), en laat je de derde weg dan wordt de teller niet opgehoogd. Je kunt dus eenvoudig

een oneindige loop maken: for(;;) printf("hallo");.

In Pascal is het voordeel van een for-loop boven een while-loop dat de forloop gegarandeerd stopt (daar is voor gezorgd door o.a. te verbieden om in de body van de loop zelf de teller te veranderen). Het zal duidelijk zijn dat dit terminatie in C niet meer geldt. In C mag dan ook alles wat je altijd al met loops hebt willen doen: de teller zelf veranderen, en de waarde van de teller inspecteren na afloop van de loop (de waarde is dan de eerste waarde die niet meer aan het doorgaan-criterium voldoet).

do-statement Het do-statement gebruik je waar je in Pascal een repeat- repeat-until statement zou gebruiken: voor een loop die minstens eenmaal doorlopen wordt, waarvan de conditie dus na het uitvoeren van de body getest wordt. Voorbeelden:

    do x=x*2; while(x<1000);
    do {p(x); x=x-1;} while (x>0);

Er kan tussen do en while maar één statement staan (in tegenstelling tot Pascal’s repeat/until). Moet de body uit meerdere statements bestaan, dan kun je die met accolades groeperen. switch-statement Het switch-statement komt overeen met het case-statement in Pascal. Het statement dat de body vormt van het switch-statement is in de praktijk altijd een compound statement. Een typisch voorbeeld is:

    switch(n)
    { case 1:  printf("een");
               break;
      case 2:  printf("twee");
               printf("nog eens twee");
               break;
      case 3:
      case 4:  printf("drie en vier");
               break;
      case 5:  printf("vijf");
      case 6:  printf("vijf en zes");
               break;
      default: printf("te veel");
    }

In de body van een switch-statement mogen sommige statements gemarkeerd worden door ‘case constante:’ of door ‘default:’. Hoewel het switch-statement voor hetzelfde doel gebruikt wordt als Pascal’s case-statement, is het belangrijk om de verschillen te kennen:

• In C schrijf je switch waar je in Pascal case gebruikt, en case waar je in Pascal of schrijft (even wennen…).

• Achter de case-labels mag meer dan één statement staan, zonder dat extra accolades nodig zijn (in Pascal zijn in dat geval extra begin/endparen nodig). Zie bijvoorbeeld case 2: in bovenstaand voorbeeld.

• De case-labels markeren het begin van een geval, maar niet het einde.

WAARSCHUWING!

De statements die bij een case horen moeten expliciet worden afgesloten met het speciale break-statement. Doe je dat niet, dan worden ook de volgende statements uitgevoerd (die bij de volgende case horen).

Je kunt dit verschijnsel overigens ook moedwillig gebruiken, zoals in bovenstaand voorbeeld gebeurt na case 5. Het is dan verstandig om in commentaar de opmerking ‘geen break!’ te zetten.

break-statement Hierboven werd al besproken dat in de body van een switch-statement break-statements nodig zijn. Het break-statement zorgt ervoor dat de uitvoering van de statements in een compound-statement wordt afgebroken. Een break-statement kan ook voorkomen in de body van een loop (for-, while- of do-statement). In dat geval wordt de loop afgebroken. In het vol- loop onderbreken gende voorbeeld worden getallen ingelezen en verwerkt, afgesloten door een negatief getal. Het negatieve getal wordt zelf niet verwerkt.

    for (;;)
    {    x = leesgetal();
         if (x<0) break;
         verwerkgetal(x);
    }

Om het programma overzichtelijk te houden is het aan te raden hier spaarzaam gebruik van te maken. Het gebruik van break is echter meestal duidelijker dan allerlei Boolean hulpvariabelen, zoals je in Pascal nogal eens ziet (while x<10

and not gevonden do. . . ).

continue-statement Net als het break-statement onderbreekt het continuestatement de body van een loop. De loop wordt in dit geval echter niet compleet afgebroken, maar er wordt verder gegaan met de volgende iteratie. Een voorbeeld van het gebruik is:

    for (;;)
    {    x = leesgetal();
         if (x>=100)
         {    printf("te groot getal");
              continue;
         }
         verwerkgetal(x);
    }

In dit voorbeeld worden alleen getallen kleiner dan 100 verwerkt. Het continuestatement is niet zo’n nuttige toevoeging aan de taal, omdat hetzelfde effect te bereiken is door de rest van de loop-body in het else-gedeelte van het if-statement te zetten.

return-statement Het return-statement wordt gebruikt om in een functie functieresultaat aan te geven wat de resultaatwaarde is. De kwadraat-functie kan bijvoorbeeld als volgt gedefinieerd worden (voor de opbouw van de functie-header zie hoofdstuk 10):

    int kwadraat(int x)
    {
        return x*x ;
    }

In een functie-body mogen meerdere return-statements voorkomen. Er is een verschil met Pascal: bij een return-statement wordt de functie ook meteen beëindigd, terwijl in Pascal bij assignment aan de functienaam de rest van functie wordt de functie-body ook uitgevoerd wordt. Vergelijk bijvoorbeeld de absolute- beëindigd waarde-functie in C en Pascal:

    /* C-versie */                { Pascal-versie }
    int abs(int x)                function abs(x: int): int;
    {                             begin
        if (x<0) return -x ;            if x<0 then abs := -x
        return x;                       else        abs := x
    }                             end;

In de C-versie is ‘else’ niet nodig, omdat voor negatieve x de functie-body toch al onderbroken is. Aan het eind van een functie moet altijd een returnstatement staan. Als je dat niet doet, dan geeft de functie ‘garbage’ (een onbekende waarde) terug. In functies waarvan de resultaatwaarde onbelangrijk is (anders gezegd: pro- procedure cedures) is geen return-statement nodig. Als je zo’n functie halverwege wilt onderbreken, kun je de expressie achter return weglaten.

lege statement Een losse puntkomma vormt ook een statement. Daardoor puntkomma keurt de compiler het goed als je overbodige puntkomma’s plaatst (bijvoorbeeld achter een sluit-accolade). Dat verandert meestal niets aan de betekenis van het programma. Het lege statement kan echter voor subtiele programmeerfouten zorgen, die niet worden gedetecteerd door de compiler:

WAARSCHUWING!

Zet geen extra puntkomma achter de voorwaarde van een if- of een while-statement. Daardoor zou de body namelijk het lege statement worden, wat meestal resulteert in een oneindige loop. Bijvoorbeeld

          x = 0;
          while (x<10); /* foute puntkomma */
          {  printf("hallo");
               x++;

} schrijft niet tien keer “hallo”! In plaats daarvan wordt, zolang x<10 is, het lege statement uitgevoerd, waardoor natuurlijk x<10 blijft. Aan het printen en ophogen van x komt de computer helemaal niet meer toe.

Ook achter de conditie van een if-statement kan een extra puntkomma desastreus zijn:

    if (x<0);  /* foute puntkomma */
       printf("negatief getal");

voert, indien x negatief is, het lege statement uit (!), en print daarna altijd de boodschap. In het geval van een for-loop kan het wel zinvol zijn dat de body uit een leeg statement bestaat. Bijvoorbeeld

    for (x=1; x<=1000; x=2*x);

berekent de eerste tweemacht die groter is dan 1000.

7 Declaraties en typen

Een variabele-declaratie in C bestaat in zijn eenvoudigste vorm uit een type ge- declaratie volgd door een identifier, afgesloten met een puntkomma. De Pascal-declaratie var x:char; wordt in C dus geschreven als char x;. Net als in Pascal kun je meerdere variabelen tegelijk declareren, bijvoorbeeld int a,b,c;. Declaraties kunnen op twee plaatsen staan:

• Aan het begin van een blok, dat omsloten wordt door accolades, dus lokale declaratie tussen de open-accolade en de statements in dit blok. Deze variabelen zijn lokaal in dit blok. Meestal is het blok de body van een functie, zodat de variabelen lokaal zijn voor die functie. Maar het is ook mogelijk om variabelen te declareren die lokaal zijn voor bijvoorbeeld het elsegedeelte van een if-statement.

• Buiten alle accolades, dus voor of eventueel tussen de functie-definities. globale declaratie Aldus gedeclareerde variabelen zijn globaal, en dus in alle functies te gebruiken.

Er zijn vier basistypen, en daarnaast zijn er mechanismen om van typen andere typen te maken (arrays e.d.). De basistypen zijn:

   • char
   • int
   • float
   • void

De eerste drie zijn bekend uit Pascal: char is het type waarvan de waarden de ascii-tekens zijn; int is het type van gehele getallen (positief en negatief); float is het type van de reële getallen (hoewel een wiskundige daar anders over denkt…). Als Pascal-kenner hoef je dus alleen te weten dat je int moet schrijven in plaats van integer, en float in plaats van real. In C is er geen type boolean. In plaats daarvan worden de integers gebruikt, boolean waarbij het getal 0 staat voor false en elk ander getal voor true. De waarde van de expressie 5>3+4 is dus bijvoorbeeld 0. Een oneindige loop kun je met integers-als-booleans zo schrijven: while(1) printf(öneindig");. Het vierde basistype, void, bestaat niet in Pascal. ‘Void’ betekent letterlijk void ‘leegte’. Als een functie geen waarde hoeft op te leveren (dus als je in Pascal een procedure zou gebruiken), dan is het resultaat-type van die functie void. Void wordt alleen gebruikt als type van het resultaat van een functie die niets hoeft op te leveren; het heeft geen zin om een void-variabele te declareren. (Wiskundig gesproken is void niet de lege verzameling, maar een verzameling met één element. Een functie met void resultaat kan dus niet anders doen dan deze ene waarde opleveren, wat er in de praktijk op neerkomt dat je de resultaatwaarde niet kunt gebruiken.) Het type void komt niet voor in de ‘klassiek’ C; het is een toevoeging die alleen geldt in ansi-C. (In klassiek C wordt int gebruikt als resultaat-type van functies waarvan het resultaat niet gebruikt wordt.) Vóór het eigenlijke type mag ook nog een type modifier staan. De volgende type modifier type modifiers zijn mogelijk:

   • short
   • long
   • unsigned

• signed (alleen in ansi-C) Hiermee kun je de manier waarop de waarden worden opgeslagen beïnvloeden, en daarmee het bereik van de mogelijke waarden. Wat het bereik is, hangt af van de implementatie. Door de HP-UX compiler wordt een int opgeslagen in 32 bits. Hiervan wordt representatie er één voor het teken gebruikt, en 31 voor de waarde. Dit geeft een bereik van −2^{31} tot 2^{31} − 1, dat wil zeggen ≈ ±2.1 miljard. Een veel gebruikte compiler op personal computers (Turbo C) slaat een int op in 16 bits, waarvan weer één voor het teken gebruikt wordt. Het bereik is dan −32768 tot 32767. Heb je onder Turbo-C grotere integers nodig, dan kun je ze declareren als long long int. Je krijgt dan alsnog een 32-bits integer met een bereik van ±2.1 miljard. Zijn de integers standaard al 32 bits (zoals onder HP-UX), dan kun je proberen nog grotere integers te krijgen met long int, maar in de meeste implementaties zijn die ook 32 bits. Wil je daarentegen geheugenruimte besparen, dan kun je een variabele declareren als short int. Er worden dan 16 bits (HP-UX) of zelfs maar 8 bits (Turbo C) gebruikt. In het laatste geval is het bereik slechts −128 tot 127. Als de getallen niet negatief kunnen zijn, dan kun je ze declareren als unsigned int. Niet alleen is het bereik dan groter (0 tot 65536 voor 16-bits getallen, 0 unsigned tot 4.2 miljard voor 32-bits getallen), maar bovendien werken de rekenkundige operatoren zoals het hoort. Je kunt verschillende type modifiers combineren, bijvoorbeeld een unsigned long int. In ansi-C is voor de volledigheid ook een modifier signed aanwezig, maar aangezien dit de default is heb je dat niet echt nodig. Ook van het type float bestaat een gewone en een lange versie (long float). Van de laatste is het bereik en de nauwkeurigheid groter. Hoewel het geheel er niet duidelijker van wordt, zijn er een aantal afkortingen toegestaan:

• Je mag long zonder meer schrijven in plaats van long int;

• Je mag unsigned zonder meer schrijven in plaats van unsigned int;

• Je mag double schrijven in plaats van long float. double Bij oppervlakkige beschouwing lijt het dus dat er zes standaardtypen zijn (void, char, int, long, float en double). In werkelijkheid ligt dit iets gecompliceerder. Wat precies het bereik van de verschillende typen is, hangt weer af van de implementatie:

compiler type  mantisse exponent nauwkeurig bereik
HP-UX float 24 bits 8 bits  6 digits  ≈ ±10^{±38}
HP-UX double 53 bits 11 bits  15 digits  ≈ ±10^{±308}
Turbo C float 24 bits 8 bits  6 digits  ≈ ±10^{±38}
Turbo C double 65 bits 15 bits  19 digits  ≈ ±10^{±4932}

Getallen worden automatisch ‘groter’ gemaakt als dat nodig is. Als bijvoorbeeld is gedeclareerd

    float x;
    int n;
    short int s;
    char c;

dan mag je n=s schrijven (van s wordt automatisch een int gemaakt). Schrijf

conversie ; int→float

je x=n dan wordt van n eerst een float gemaakt. Characters zijn converteerbaar naar integers (je hoeft dus niet, zoals in Pascal, de functie ord te gebruiken). Dit gebeurt bijvoorbeeld in het statement n=c.

conversie ; char→int

Getallen kunnen ook ‘kleiner’ gemaakt worden. Je moet je daarbij realiseren dat er nauwkeurigheid verloren gaat, als je bijvoorbeeld een double waarde in een float variabele stopt. Als je een long int naar int converteert, of een int naar char, dan worden de hoogste bits eenvoudig weggegooid. Als n de

conversie ; int→char

waarde 577 heeft, dan zal na het assignment c=n het character c de waarde 65 (letter 'A') hebben. Bij assignments waarbij je een float waarde in een int variabele stopt, wordt de float eerst afgerond naar de dichtsbijzijnde integer.

conversie ; float→int

8 Constanten

Er zijn drie soorten constanten die je kunt aangeven: getallen, characters en strings. Het gaat hier dus niet om constanten die aangeduid worden door een naam (zoals in Pascal mogelijk is met een const-declaratie). Dat is in C ook mogelijk, maar daarop wordt nader ingegaan in hoofdstuk 11.

Getallen Er zijn verschillende manieren om een getal aan te geven:

• Een rijtje van een of meer cijfers duidt een int aan (een eventueel min- decimaal teken hoort dus niet bij het getal; dit is een operator die erop wordt toegepast). Als het getal te groot is om als int opgeslagen te worden, wordt het vanzelf als long int beschouwd.

• WAARSCHUWING! Een getal dat met een nul begint, wordt als octaal getal beschouwd. Dus 017 is niet zeventien maar vijftien!

Octale getallen worden tegenwoordig zelden gebruikt; het is een over- octaal blijfsel uit het verleden.

• Een getal dat met 0x begint, wordt als hexadecimaal getal beschouwd. hexadecimaal In zo’n getal mogen ook de letters A tot F en a tot f voorkomen. Bijvoorbeeld 0x10 is zestien, 0xFF is tweehonderdvijfenvijftig.

• Een float-constante bestaat uit een deel voor de punt, de decimale punt, een deel achter de punt, een letter e of E, en een exponent. De delen voor en achter de punt zijn allebei rijtes cijfers. Eén van beide mag weggelaten worden. De decimale punt of de letter e met de exponent float mogen weggelaten worden (niet allebei). De exponent bestaat uit een rijtje cijfers, waar nog een + of een - voor mag staan. Een floatconstante wordt altijd als long float (double) beschouwd. Opgave: teken zelf een syntaxdiagram hiervoor.

• Achter een int-constante mag een L gezet worden om te benadrukken dat long het een long int is, of een U om te benadrukken dat het een unsigned

     int is.

Characters Characters kunnen aangeduid worden door ze tussen enkele aanhalingstekens (apostrophes) te zetten. Dit is dus net als in Pascal. Tussen de twee aanhalingstekens staat dus precies één letter, bijvoorbeeld 'a', '3' of '%'. Een paar speciale tekens kunnen met een backslash (\) worden aangegeven:

newline  ascii 10 '\n'
return  ascii 13 '\r'
backspace ascii 8  '\b'
formfeed ascii 12 '\f'
tab  ascii 9  '\t'
null  ascii 0  '\0'
backslash \        '\\'
apostrof '        '\''
overig  ascii ddd '\ddd'

waarbij \ddd bestaat uit een backslash gevolgd door 1, 2 of 3 octale cijfers, waarmee een willekeurig ascii-teken aan te duiden is. Een backslash voor andere letters wordt genegeerd.

Strings Strings (rijtjes characters) staan, om ze van een character te kunnen onderscheiden tussen dubbele aanhalingstekens, bijvoorbeeld "aap". Er is belangrijk verschil tussen 'a' en "a": het eerste is een character, het tweede is een string die uit één symbool bestaat. In strings kunnen ook weer backslashes gebruikt worden om speciale tekens mee aan te geven. Als het aanhalingsteken aanhalingsteken zelf in de string moet voorkomen, moet er ook een backslash voor gezet worden, bijvoorbeeld "hij zei \"hallo\"". Door de compiler wordt automatisch aan het eind van elke string een null-character (\0) gezet. In hoofdstuk 14 zullen we zien hoe deze einde-string markering handig gebruikt kan worden.

9 Operatoren

Er zijn in C unaire operatoren (operatoren met één argument), binaire operatoren (operatoren met twee argumenten), en er is zelfs een ternaire operator ariteit van opera-

toren (operator met drie argumenten). De unaire operatoren hebben de hoogste prioriteit (in hun argument mogen dus geen binaire operatoren voorkomen, tenzij er haakjes omheen staan), dan volgen de binaire operatoren, dan de ternaire operator, en tenslotte het assignment (in hoofdstuk 5 zagen we al dat assignment een speciaal soort operator is).

Binaire operatoren

Zijn er in Pascal maar drie prioriteiten voor binaire operatoren (multiplicatieve, additieve en relationele), in C zijn er maar liefst tien nivo’s. Dat is een prioriteit goed idee geweest, want in Pascal zijn er vaak extra haakjes nodig op onlogische plaatsen. In Pascal moet je bijvoorbeeld schrijven (x<3) and (y<5). Zou je de haakjes weglaten, dan krijg je een typerings-fout: door de hoge prioriteit van and zou dan 3 and y moeten worden uitgerekend. In C is de prioriteit van and lager dan van de relationele operatoren, zodat de haakjes overbodig zijn. De volgende binaire operatoren zijn beschikbaar, in volgorde van dalende prioriteit:

10. *  /  %  multiplicatieve operatoren
9. +  -  additieve operatoren
8. << >>  schuif-operatoren
7. <  <= > >= relationele operatoren
6. != ==  (on-)gelijkheid
5. &  bitsgewijs ‘and’
4. ^bitsgewijs ‘xor’
3. |  bitsgewijs ‘or’
2. &&  logisch ‘and’
1. ||  logisch ‘or’
0. = e.a.  assignment

We zullen nu de betekenis van de binaire operatoren bekijken.

multiplicatieve operatoren: * is vermenigvuldigen. Op integers doet / hetzelfde als div in Pascal, maar als één van beide (of allebei de) operanden div en mod van type float is, worden de getallen exact gedeeld. In dit geval worden vóór de berekening beide argumenten geconverteerd naar double. De operator % doet hetzelfde als mod in Pascal, en werkt alleen op integers.

additieve operatoren: + is optellen, - is aftrekken.

schuif- operatoren zijn onbekend in Pascal. Het eerste argument wordt als bitpatroon beschouwd, en zoveel plaatsen naar links (<<) of naar rechts bitpatroon (>>) geschoven als het tweede argument aangeeft. Aan de rechterkant wordt aangevuld met nullen; aan de linkerkant wordt aangevuld met het sign-bit. Het getal 9 bijvoorbeeld is in binaire notatie 1001_{2}. Dus sign-extensie 9<<2 is 100100_{2} oftewel 36. Met deze operatoren kun je gemakkelijk vermenigvuldigen met, of delen door machten van twee: n<<k is gelijk aan n × 2^{k}.

relationele operatoren werken volgens de bekende definitie. Het resultaat is niet ‘true’ of ‘false’ zoals in Pascal, maar 0 (voor ‘false’) of een ander getal (voor ‘true’). In samenwerking met if en while werkt dat zoals 0 is false je zou verwachten. Je moet alleen uitkijken dat je niet expressies als 1<=x<=10 schrijft. In Pascal geeft dat een typerings-fout. In C wordt de expressie door de compiler geaccepteerd, echter met een ongebruikelijke semantiek. Stel bijvoorbeeld dat x nul is: dan is 1<=x false, oftewel 0; vervolgens wordt 0<=10 uitgerekend, met als resultaat true – niet een uitkomst die voor de hand ligt!

gelijkheids operatoren: omdat = al gebruikt wordt voor assignment, moet = versus == het vergelijken van waarden geschreven worden als ==. Het is zeer gevaarlijk om je daarin te vergissen; dit is misschien wel het grootste nadeel van C.

WAARSCHUWING!

In C betekent = assignment, en is == de operator die op gelijkheid test. Schrijf niet een enkele = voor de gelijkheidstest: dit geeft fouten die erg moeilijk te vinden zijn. Bekijk bijvoorbeeld een programma dat iets speciaals doet als x gelijk is aan nul:

          if (x==0) printf("speciaal geval");

Zou je per ongeluk schrijven:

          if (x=0) printf("speciaal geval");

en x heeft de waarde 5, dan gebeurt het volgende: als neveneffect van het uitrekenen van de conditie wordt x gelijk gemaakt aan 0, en deze waarde wordt vervolgens als Boolean beschouwd (0, oftewel false). Er wordt dus niets geprint, en het programma gaat stilletjes verder. Maar wel is ondertussen x gelijk aan nul gemaakt, een situatie die je met dit statement waarschijnlijk juist wilde vermijden. Resultaat: core dumps en slapeloze nachten.

De ongelijkheid-operator wordt in C geschreven als !=. Deze operator is ongelijkheid vergelijkbaar met <> in Pascal.

bitsgewijze operatoren: beide argumenten worden door deze operatoren (net zoals het eerste argument door de schuif-operatoren) als bit-patroon beschouwd. Overeenkomstige bits worden volgens de bekende regels gecombineerd. Bijvoorbeeld 13&27 wordt als volgt uitgerekend: 13 is 01101_{2}, 27 is 11011_{2}; resultaat van de bitsgewijze and-operatie is 01001_{2} oftewel

9. Ander voorbeeld: 5|9 combineert 0101_{2} en 1001_{2} met de or-operatie, het resultaat hiervan (1101_{2}) is 13.

Bitsgewijze operatoren kun je handig gebruiken voor het knutselen aan representatie, bijvoorbeeld het omwisselen van het lage en hoge byte van een word:

    low   = x & 0x0F;
    high  = x & 0xF0;
    x     = low<<8 | (high>>8) & 0x0F;

Ook zijn bitsgewijze operatoren goed te gebruiken om een heleboel Booleans in één getal te coderen, bijvoorbeeld in een tekstverwerker:

   if (x&1) maak("cursief");
   if (x&2) maak("vet");
   if (x&4) maak("onderstreept");
   if (x&8) maak("outlined");

logische operatoren: dit zijn de operatoren and en or uit Pascal. Deze ope- and en or ratoren kunnen in principe op integers werken, maar in de praktijk gebruik je ze alleen op ‘Booleans’ (resultaten van relationele en gelijkheidsoperatoren). De operatoren kijken alleen of de argumenten gelijk aan nul zijn of niet. Eerst wordt het linker-argument bekeken. Als hiermee het eindantwoord al bepaald kan worden, dan wordt het andere argument niet uitgerekend. Als a een array is met indices 0..9, kun je dus toch schrijven:

   if ( i<10   && a[i]==c   )...
   if ( x==nil || x->info>n )...

zonder de array-bound te schenden, respectievelijk de nil-pointer te volgen. Dit op grond van de regels false ∧ x = false en true ∨ x = true.

assignment- operatoren: zoals opgemerkt in hoofdstuk 5 is het assignment (=) een speciaal soort operator. Speciaal, omdat aan de linkerkant niet een willekeurige expressie mag staan, maar alleen een lvalue. Naast het gewone assignment is er een hele reeks assignment-operatoren: +=, -=, *=, /=, %=, >>=, <<=, &=, ^= en |=. De expressie x+=5 is een afkorting voor x=x+5. Behalve dat dit korter is om op te schrijven, spaart dit rekentijd als het bepalen van x ingewikkeld is (bijvoorbeeld omdat er een functie-aanroep voor nodig is). In x+=5 wordt x namelijk slechts éénmaal geëvalueerd. Voor alle binaire operatoren (behalve de logische operatoren) is een dergelijke schrijfwijze mogelijk. De assignment-operatoren zijn rechts-associatief. De expressie x=y=1 wordt daarom geïnterpreteerd als x=(y=1), en het gevolg is dat zowel x als y gelijk aan één worden.

Bij het uitvoeren van rekenkundige operatoren (multiplicatieve en additieve operatoren) worden de argumenten zonodig eerst van hetzelfde type gemaakt, volgens de regels beschreven in hoofdstuk 8. Dit gebeurt als volgt: type-conversie 1. Eerst worden argumenten van type char en short int geconverteerd

naar int, en van type float naar type double.

2. Als één van de argumenten double is, wordt het andere daar ook naar geconverteerd, en dat is het type van het resultaat.

3. Anders, als één van de argumenten long int is, wordt het andere daar ook naar geconverteerd, en dat is het type van het resultaat.

4. Anders, als één van de argumenten unsigned int is, wordt het andere daar ook naar geconverteerd, en dat is het type van het resultaat.

5. Anders zijn beide argumenten van type int, en dat is het type van het resultaat.

Floating-point operaties worden dus altijd op double waarden uitgevoerd, en daarna zonodig weer afgerond tot float.

Unaire operatoren

Unaire operatoren werken op één argument. Sommige worden voor dat argument geschreven (prefix-operatoren), sommige erachter (postfix-operatoren). Sommige kunnen op een willekeurige expressie werken, andere alleen op een lvalue (variabele, of andere expressie die aan de linkerkant van een assignment mag staan). De volgende expressies met unaire operatoren zijn mogelijk:

* expressie  indirectie
& lvalue  adres
- expressie  tegenovergestelde
! expressie  logische ‘not’
~ expressie  bitsgewijze ‘not’
++ lvalue  pre-increment
-- lvalue  pre-decrement
lvalue ++  post-increment
lvalue --  post-decrement
sizeof expressie aantal benodigde bytes
sizeof type  idem

indirectie Deze operator wordt gebruikt om pointers te volgen. Het argu- dereference ment moet een pointer zijn (anders meldt de compiler een typeringsfout); het resultaat is een waarde van het type waar de pointer naar wijst. In C wordt dus de notatie *p gebruikt, waar je in Pascal p^zou schrijven.

adres Deze operator is in zekere zin het omgekeerde van *. Als x een variabele is, dan is &x een pointer naar deze variabele. Het is in C dus mogelijk pointer naar variom pointers te maken naar zelf-gedeclareerde variabelen. In Pascal kan abele dat niet; pointers wijzen in die taal altijd naar geheugen dat door middel van een aanroep van new is aangemaakt. Het argument van & moet een lvalue zijn; het is immers niet mogelijk om een pointer naar bijvoorbeeld 3+7 te krijgen.

not Van de ‘not’ zijn weer twee verschillende versies. Het meest gebruikt wordt !, de ‘logische not’. Deze operator kijkt alleen maar of het argument 0 is of iets anders. Je kunt hem gebruiken in combinatie met && en ||. De bitsgewijze not, die geschreven wordt met het tilde-symbool (~) ! versus ~ beschouwt zijn argument in de binaire notatie. Voor het resultaat van ~9 wordt elk bit in de binaire representatie van 9 (1001_{2}) omgeklapt. Ook alle nullen aan het begin worden omgeklapt, zodat het resultaat ...11110110_{2} is. Volgens de gebruikelijke representatie van negatieve getallen (two’s complement) is dit de waarde ‘min tien’. De bitsgewijze not wordt vooral gebruikt om losse bits in een bit-vector uit te zetten. Het vierde bit van een getal (dat overeenkomt met de acht-tallen) kun je op de volgende manier aan- en uitzetten:

    x = x |  8;   /* aanzetten vierde bit */
    x = x & ~8;   /* uitzetten vierde bit */

increment/decrement De notatie ++x is een afkorting voor x+=1, wat op zijn beurt weer een afkorting is voor x=x+1. Neveneffect van deze expressie is dat x wordt opgehoogd; de waarde van de expressie is de nieuwe waarde van x. Daarom heet deze expressie pre-increment: de variabele pre-increment wordt eerst opgehoogd, en daarna wordt de waarde bepaald.

Bij de notatie x++ wordt eerst de waarde bepaald, en daarna de variabele opgehoogd. Deze expressie wordt post-increment genoemd. De expressie post-increment waar x++ deel van uitmaakt kan dus voor de laatste keer verder rekenen met de oude waarde van x, maar de volgende keer zul je een grotere x aantreffen. Het volgende voorbeeld geeft het verschil aan tussen pre- en post-increment:

    /* x==5 */  a = ++x;  /* a==6, x==6 */
    /* x==5 */  a = x++;  /* a==5, x==6 */

Bij gebruik als statement (als het je dus alleen om het neveneffect te doen is), is er geen verschil tussen pre-increment en post-increment; de waarde van de expressie wordt dan immers weggegooid.

Voor de decrement-operatoren -- geldt iets dergelijks, alleen wordt dan natuurlijk x=x-1 uitgerekend.

sizeof De operator sizeof berekent het aantal bytes dat nodig is om zijn argument op te slaan. Het argument wordt niet berekend; in feite wordt de waarde van de expressie al tijdens het compileren bepaald. Daarom mag sizeof ook op typen worden toegepast, bijvoorbeeld sizeof int. Het gebruik van sizeof wordt verder besproken in hoofdstuk 16.

Ternaire operator

Er is in C één operator met drie argumenten; een zogenaamde ternaire operator. Deze operator wordt mixfix geschreven, dat wil zeggen een symbool (?) tussen het eerste en het tweede argument, en een symbool (:) tussen het tweede en het derde argument. De betekenis van de expressie c?x:y is: ‘x als c true (≠ 0) is, en anders y’. Het is een ‘if-then-else-expressie’, oftewel een conditionele expressie. In plaats conditionele van if (c) a=x; else a=y; kun je dus ook schrijven a = c?x:y;. In de expressie praktijk wordt deze operator vooral gebruikt bij het meegeven van parameters aan functies, bijvoorbeeld:

    schrijf( n,  (n==1 ? "persoon" : "personen") );

De prioriteit van de ternaire operator ligt tussen die van de logische operatoren en de assignment operatoren.

10 Functies en parameters

Een functie met parameters wordt in ansi-C als volgt gedefinieerd:

    int square(int x)
    { return x*x ;
    }

In ‘klassiek’ C werden de typen van de parameters gedeclareerd ‘achter de haakjes:

    int square(x) int x;
    { return x*x ;
    }

Dat was minder handig, omdat je zo x tweemaal moet noemen. Als de compiler ansi-C verstaat, is die schrijfwijze dus te prefereren. Moet het programma (ook) door een klassieke compiler vertaald worden, dan is helaas de klassieke schrijfwijze noodzakelijk. Declaraties van een functie zonder parameters gaat als volgt: functie zonder pa-

rameters

    /* ANSI-C */              /* klassiek C */
    int altijd1(void)         int altijd1()
    {    return 1 ;           {    return 1 ;
    }                         }

In ansi-C wordt door het keyword void (letterlijk ‘leeg’) aangegeven dat er geen parameters zijn; klassiek staat er gewoon niets tussen de haakjes. Aanroep van functies gaat, net als in Pascal, door de naam van de functie te noemen met tussen haakjes de actuele parameters. Bijvoorbeeld: square(3+4).

Bij een functie zonder parameters staat er (zowel klassiek als ansi) niets tussen de haakjes. De haakjes zelf zijn, anders dan in Pascal, wel altijd nodig, dus bijvoorbeeld 2+altijd1().

WAARSCHUWING!

Bij aanroep van een functie zonder parameters moet er een leeg paar haakjes achter de functienaam geschreven worden. Vooral als je een functie-aanroep als ‘procedure-aanroep’ gebruikt (dus met een puntkomma erachter) is het gevaarlijk om dit te vergeten. Schrijf je namelijk ‘p;’ in plaats van ‘p();’ dan wordt de waarde van het adres van de procedure uitgerekend, en vervolgens meteen weer weggegooid (net zoals in het statement 5;). De procedure wordt dus niet aangeroepen. Je moet daarom ‘p();’ schrijven. De procedure wordt dan aangeroepen, en het resultaat van de procedure wordt weggegooid (wat ook de bedoeling is).

In het volgende voorbeeld wordt een functie met twee parameters gedeclareerd, een int en een char. De functie schrijft zoveel kopieën van het teken als het getal aangeeft. Daartoe gebruikt hij een for-loop met een lokale teller t. Het functie-resultaat is het aantal geschreven tekens.

    /* ANSI-C */                     /* klassiek C */
    int f(int n, char c)             int f(n, c) int n; char c;
    {    int t;                      {    int t;
         for (t=0; t<n; t++)              for (t=0; t<n; t++)
             putchar(c);                      putchar(c);
         return t;                        return t;
    }                                }

De parameters worden dus vóór de open-accolade gedeclareerd (tussen de parameters versus haakjes in ansi-C, erachter in klassiek C), de lokale variabelen worden ná lokale variabelen de open-accolade gedeclareerd. In C zijn er geen procedures; hiervoor worden functies zonder resultaat ge- functies zonder bruikt. Je declareert ze als volgt: resultaat

    /* ANSI C */                     /* klassiek C */
    void p(char a, char b, char c)   p(a,b,c) char a,b,c;
    {    putchar(a);                 {    putchar(a);
         putchar(b);                      putchar(b);
         putchar(c);                      putchar(c);
    }                                }

Er staat dus simpelweg geen return in de functie (of een return zonder expressie erachter). In klassiek C zet je geen enkel type voor de naam van de procedure (de compiler neemt dan aan dat je int bedoeld, maar daar heb je verder geen last van). In ansi-C kun je expliciet zeggen dat de functie een void resultaat void, oftewel niets oplevert. Je mag functies gebruiken voordat ze gedeclareerd zijn. Er wordt dan aangenomen dat ze een int opleveren. Als later blijkt dat dat bijvoorbeeld double is, dan krijg je alsnog een foutmelding. Je moet in zo’n geval een declaratie doen die je in Pascal een forward declaratie zou noemen. In C wordt dat een ‘forward’ declaraprototype-declaratie genoemd. Die ziet er bijvoorbeeld als volgt uit: tie

    double square(double x);

Als prototype kun je dus gewoon de (ansi)-functieheader gebruiken. De puntkomma geeft aan dat er geen functie-body volgt. De naam van de parameter(s) mag je weglaten:

    double square(double);

Eén van de belangrijkste tekortkomingen van klassiek C was dat de typen van de parameters niet konden worden ge-prototypeerd. In klassiek C ziet de declaratie er dus als volgt uit:

    double square();

In C is het mogelijk om meerdere files apart te compileren. Functies die in een andere file worden gedefinieerd, kun je toch aanroepen. De typen van de parameters en het functieresultaat kun je met een prototype-declaratie declareren. In klassiek C worden de parameters van de functies die in een andere file gedefinieerd zijn echter niet gecontroleerd! Het is duidelijk dat dit tot onaangename fouten kan leiden.

11 De preprocessor

Declaraties zoals die in hoofdstuk 7 besproken zijn, zijn altijd declaraties van variabelen. Er is geen equivalent van de const-declaratie uit Pascal. Toch is het mogelijk om belangrijke constanten een naam te geven. Hiervoor is een mechanisme beschikbaar dat, behalve het naamgeven van constanten, nog veel meer mogelijkheden heeft. Regels van het programma die beginnen met een ‘hekje’ (#) vallen buiten de gewone syntax van de taal. Ze mogen dus overal staan, hoewel ze meestal aan het begin van het programma staan. Deze regels worden verwerkt door een speciale preprocessor, die de programmatekst doorneemt voordat de compiler preprocessor ermee aan de slag gaat. Na het hekje volgt een commando voor de preprocessor. Twee belangrijke preprocessor-commando’s zijn #define en #include. De preprocessor heeft een eigen tabel met namen. Met een #define-commando #define kan je hier iets aan toevoegen. Bijvoorbeeld:

    #define PI 3.1415926535

Na dit #define-commando worden alle verdere voorkomens van de identifier PI vervangen door de tekst 3.1415926535. De eigenlijke compiler krijgt het woord PI dus helemaal niet te zien; de preprocessor heeft dit al vervangen. Op deze manier kun je belangrijke constanten, bijvoorbeeld de afmetingen van je

constante- definitie

arrays op één plek vastleggen^{1}. Je kunt #define ook voor andere dingen gebruiken dan constante-definities. Het maakt de preprocessor niets uit wat de vervang-tekst is. Je zou bijvoorbeeld kunnen definiëren:

    #define begin   {
    #define end     }
    #define then
    #define integer int
    #define boolean int
    #define and     &&

Daarna mag je dan, à la Pascal, begin en end schrijven in plaats van { en }. nieuwe keywords De vervang-tekst voor then is niets, dus de preprocessor gooit alle voorkomens definiëren van then uit het programma. Je kunt dan, als verstokte Pascal-programmeur, then blijven schrijven achter if (maar ook op elke andere plek in het programma…). De vervang-tekst wordt ook eerst door de preprocessor verwerkt. Het is dus mogelijk om hierin weer andere constanten te gebruiken (mits die eerder zijn gedefinieerd), bijvoorbeeld:

    #define KWARTPI (PI/4)

Het is verstandig om hier haakjes omheen te zetten, zodat in elke context het juiste resultaat wordt berekend (ook in 1/KWARTPI). Er wordt immers textuele substitutie uitgevoerd. Het is gebruikelijk om de namen van constanten met hoofdletters te schrijven, om ze te onderscheiden van variabelen. Verplicht is dit niet. Het is goed gebruik om alle constante-definities en prototype-declaraties in een aparte file te zetten. Je kunt die file in een programma gebruiken door middel van het preprocessor-commando #include. De preprocessor zal de #include tekst van de betreffende file tussenvoegen; de eigenlijke compiler merkt er niets van dat het hier om een andere file gaat. De naam van de file moet tussen aanhalingstekens staan als het om een eigen file gaat, en tussen punthaken als het om een systeem-file gaat (de laatste worden in een andere directory gezocht). Bijvoorbeeld:

    #include "grenzen.h"
    #include <stdio.h>

Het is de conventie om files waarin uitsluitend constante-definities en typedeclaraties staan te laten eindigen op ‘.h’.

1Waarom zou je PI op deze manier definiëren? Antwoord: als de waarde van PI nog eens verandert, hoef je dat maar op één plaats in het programma te veranderen :–)

Voor een groot aantal taken zijn er bibliotheken met functies beschikbaar. De functiebibliotheken typen van deze functies worden gedeclareerd in .h-files, waarin ook voor de betreffende taak nuttige constanten zijn gedeclareerd. Veel gebruikte bibliotheken zijn bijvoorbeeld:

stdio.h standaard-functies voor input/output
string.h string-manipulatie
ctype.h functies om chars te classificeren
math.h  wiskunde (sinus, exponential e.d.)
time.h  functies om de tijd op te vragen
X11.h  grafische functies

Niet alle bibliotheken zijn op alle computers beschikbaar. Zo is bijvoorbeeld X11.h natuurlijk alleen maar aanwezig op computers die het X-window systeem ondersteunen. Andere bibliotheken, zoals stdio.h en math.h zijn altijd aanwezig.

12 Standaardfuncties

In dit hoofdstuk bespreken we een aantal functies waarmee input en output gepleegd kan worden. De typen van deze functies en enkele bijbehorende constanten staan in stdio.h. In het programma zal dus de regel

    #include <stdio.h>

moeten staan. Op het eerste gezicht lijkt input/output in C ingewikkelder dan in Pascal, maar uiteindelijk is het veel flexibeler. Er zijn verschillende nivo’s waarop je de input/output kunt aanspreken. Meestal gebruik je in een programma maar één soort.

• low-level: character-georiënteerd;

• high-level: datatype georiënteerd. De input/output functies werken op de standaard-input en standaard-output. Dit zijn meestal toetsenbord en scherm, maar onder Unix kun je met I/Oredirection hiervoor ook files gebruiken. Alle I/O-functies kunnen ook direct files aanspreken; zie hiervoor hoofdstuk 18.

low-level I/O

Dit is de simpelste vorm van communicatie. Er is een functie om een character te lezen en een functie om een character te schrijven. In het geval van een character-I/O regel-overgang is het ontvangen character het newline-character ('\n'). Je mag alle 256 mogelijke characters lezen en schrijven. Als het einde van de file bereikt is, levert de lees-functie een speciale waarde op. Deze speciale waarde is geen character – anders zou je het verschil niet kunnen zien tussen het einde van de file, en een voorkomen van dit character in de file. Daarom is het resultaat-type van de lees-functie niet char maar int. Uit symmetrieoverwegingen is ook de parameter van de schrijf-functie een int. Als je een waarde hebt gelezen, en je hebt vastgesteld dat dit niet de end-of-file markering is, dan kun je de waarde desgewenst alsnog in een char stoppen: int is immers converteerbaar naar char, waarbij alleen de laagste acht bits worden gebruikt. De prototypen (in ansi-C) van de functies luiden als volgt:

    int getchar(void);
    int putchar(int);

Ik kan wel verklappen dat als end-of-file waarde het getal −1 wordt gebruikt, maar eigenlijk hoef je dat niet te weten. In de include-file stdio.h wordt namelijk een constante EOF gedefinieerd, die je voor vergelijkingen kunt ge- EOF bruiken. De functie putchar levert de zojuist geschreven waarde ook weer als resultaat op, of EOF als het schrijven mislukt is (bijvoorbeeld omdat de disk vol is). Let op: er is niet zoiets als een end-of-file character; EOF is, integendeel, een int die door getchar wordt opgeleverd als er geen characters meer in de file zitten. Deze twee functies kun je gebruiken in een programma dat de standaard-invoer kopieert naar de standaard-uitvoer:

    #include <stdio.h>
    void main(void)
    {   int c;
        c = getchar();
        while (c != EOF)
        {    putchar(c);
             c = getchar();
        }
    }

De structuur van dit programma is ‘Pascal-achtig’: in het programma moet op twee plaatsen getchar aangeroepen worden. Met behulp van het breakstatement is dit overbodig:

    void main(void)
    {   int c;
        for(;;)
        {    c = getchar();
             if (c == EOF) break;
             putchar(c);
        }
    }

Maar doordat je in C een assignment ook in een expressie kunt verwerken, kan het nog eenvoudiger:

    void main(void)
    {   int c;
        while ( (c=getchar()) != EOF )
            putchar(c);
    }

Je moet zelf een afweging maken of je de compactere code nog wel duidelijk genoeg vindt, of dat je toch liever een wat langer programma gebruikt.

High-level output

Voor eenvoudige tekst-filters zijn de low-level operaties zeer geschikt. Het schrijven van getallen en strings zou hiermee een beetje moeizaam worden. Met de functie printf (‘print-formatted’) is dat eenvoudiger. Deze functie is ruwweg te vergelijken met writeln in Pascal. Net als writeln heeft printf printf een variabel aantal parameters^{2}. De eerste parameter van printf is altijd een string, de zogenaamde control string. Het gebruik van de control-string blijkt control string het beste uit een voorbeeld:

    for (n=32; n<127; n++)
        printf("ascii-code %d is %c \n", n, n );

Dit statement schrijft een ascii-tabelletje, met als uitvoer:

    ascii-code 32 is
    ascii-code 33 is !
    ascii-code 34 is "
    ascii-code 35 is #
    ....
    ascii-code 126 is ~

De control string wordt letterlijk geschreven, alleen de letters %d en %c worden speciaal behandeld. Op deze plek worden namelijk de waarden van de overige parameters van printf ingevuld. De letter achter het procent-teken bepaalt op welke manier dat gebeurt: een d staat voor ‘decimaal getal’, en een c staat voor ‘character’. In het voorbeeld worden zowel de %d als de %c vervangen door de waarde van n, omdat deze waarde tweemaal aan printf wordt meegegeven. Mogelijke procent-codes in de control string zijn o.a. de volgende:

2Anders dan in Pascal is het in ansi-C mogelijk om zelf functies met een variabel aantal parameters te schrijven. Het valt buiten het bestek van deze tekst om hierop in te gaan. Op een Unix-systeem kun je meer hierover lezen met ‘man varargs’.

%d decimaal getal (signed)
%u decimaal getal (unsigned)
%x hexadecimaal getal (met gebruik van a–f)
%X hexadecimaal getal (met gebruik van A–F)
%o octaal getal
%c character
%s string
%f floating-point getal in ‘0.000123’ notatie
%e floating-point getal in ‘1.23e-4’ notatie
%g de kortste van %e en %f
%% procent-teken zelf (gebruikt geen parameter)

WAARSCHUWING! Er wordt door de compiler niet gecontroleerd of de parameters van printf wel overeenkomen met de waarden die door de controlstring worden gespecificeerd. Niets weerhoudt je ervan om een pointer als integer, een character als floating-point getal, of een integer als string te printen. De eerste twee geven rare uitkomsten, de derde kan een core-dump tot gevolg hebben.

Tussen het procent-teken en de letter kunnen desgewenst nog een paar symbolen staan, die de conversie beïnvloeden:

• één of meer flags;

• een getal, dat de fieldwidth wordt genoemd;

• een punt gevolgd door een getal dat de precision wordt genoemd;

• een letter l of h. Deze vier dingen zijn allemaal optioneel; ze kunnen bovendien met elkaar gecombineerd worden. De fieldwidth geeft aan hoeveel ruimte minimaal gebruikt moet worden. Als fieldwidth de benodigde ruimte minder is, dan wordt aan de linkerkant aangevuld met spaties. Voorbeelden:

printf( "[%2d]" , 456   );        [456]
printf( "[%3d]" , 456   );        [456]
printf( "[%4d]" , 456   );        [ 456]
printf( "[%5s]" , "aap" );        [  aap]

Als flag zijn een aantal symbolen mogelijk, die de volgende invloed hebben op flags de conversie:

-  spaties worden aan de rechterkant toegevoegd
+  het teken (+ of -) wordt altijd afgedrukt
spatie als het teken + is, wordt een spatie afgedrukt
#  een ‘alternatieve vorm’ wordt gebruikt (zie manual)
0  links wordt in plaats van met spaties aangevuld met nullen

Voorbeelden:

    printf( "[%-5s]"  , "aap" );       [aap  ]
    printf( "[%-+6d]" , 456   );       [+456  ]
    printf( "[%-+6d]" , -456  );       [-456  ]
    printf( "[%- 6d]" , 456   );       [ 456  ]
    printf( "[%06d]"  , 456   );       [000456]
    printf( "[%04X]"  , 456   );       [01C8]

De betekenis van de precision hangt af van het soort conversie: precision

d, o, x minimum aantal cijfers (default 1)
e, f  aantal cijfers achter de decimale punt (default 6)
g  aantal significante cijfers
s  maximum aantal characters (rest wordt weggelaten)

Voorbeelden:

    printf( "[%6.4d]"  , 45    );        [  0045]
    printf( "[%.4g]"   , 1.0/7 );        [0.1429]
    printf( "[%.2s]"   , "aap" );        [aa]
    printf( "[%-5.2s]" , "aap" );        [aa   ]

De letter l of h tenslotte, geeft aan dat de betreffende parameter long respectievelijk short is.

High-level input

De input-tegenhanger van printf is scanf. Ook deze functie krijgt als eerste scanf parameter een control string. De volgende aanroep leest een getal in van de standaard-input:

    scanf( "%d", &n );

De tweede parameter (n) geeft aan in welke variabele het resultaat gezet moet worden. Dit is wat je in Pascal zou noemen een ‘var-parameter’: de waarde van de parameter wordt door de functie veranderd. In C moeten dit soort parameters vooraf worden gegaan door een &-teken. Meer hierover volgt in hoofdstuk 13. In de control-string kunnen drie soorten tekens staan:

• whitespace (spaties, tab-tekens (\t), newlines (\n)). Eén van deze tekens geeft aan dat op deze plaats spaties, tabs en newlines genegeerd moeten worden. Met de aanroep scanf("%d %d", &a, &b) kunnen bijvoorbeeld twee integers ingelezen worden die gescheiden worden door één of meer spaties, tabs of newlines. Dit komt overeen met de aanroep read(a,b) in Pascal.

• een conversie-specificatie, bestaande uit een % en een letter (analoog aan printf). Dit geeft aan dat op deze plaats input van het betreffende type verwacht wordt.

• andere characters, bijvoorbeeld een komma. Deze symbolen moeten op de invoer letterlijk worden aangetroffen. De aanroep scanf("%f,%f",&x,&x) leest twee floats in, gescheiden door een komma.

Spaties, tabs en newlines worden normaliter gebruikt om invoer te scheiden. Ook strings worden door spaties gescheiden: scanf("%s",s) slaat letters op in de string s totdat de eerstvolgende spatie, tab of newline aangetroffen wordt. Enige uitzondering hierop is "%c" die het eerstvolgende teken leest, ook als dit een spatie is.

13 Arrays en pointers

Net als Pascal kent C getypeerde arrays en pointers. Arrays werken grotendeels hetzelfde als in Pascal; de mogelijkheden van pointers zijn echter uitgebreider dan in Pascal. Declaratie van arrays en pointers demonstreren we aan de hand van het volgende voorbeeld:

    int n, a[5], *p;

Na deze declaratie is n een gewone integer, a een array met vijf integers, en declaratie van arp een pointer naar een integer. In een declaratie kun je dus bij elke variabele rays en pointers aangeven of het, in plaats van een simpele integer, misschien een array-van-of een pointer-naar-integers is. Als je meerdere pointers wilt declareren, moet er voor elke variabele een sterretje staan, bijvoorbeeld int *p, *q;. Arrays beginnen in C altijd te tellen bij 0. In de declaratie wordt alleen arrays beginnen de lengte van de array opgegeven en niet (zoals in Pascal) de onder- en de bij 0 bovengrens. Het laatste element van de hierboven gedeclareerde array a is dus a[4] – er zijn immers vijf elementen: a[0], a[1], a[2], a[3] en a[4]. Hoewel dit op het eerste gezicht onhandig lijkt, werkt dit goed als je arrays op de volgende manier doorloopt:

    for (k=0; k<len; k++)
        printf("het %de element is %d\n", k, a[k] );

De tellende variabele loopt dus van en met nul tot doch zonder de lengte. Als je een array bijvoorbeeld als buffer gebruikt, dan is het in C het handigst om de index van het eerste vrije element bij te houden (en niet die van het laatste gebruikte element). Je kan deze index eenvoudig initialiseren met 0, en de buffer is vol als de index gelijk is aan de lengte van de array. In hoofdstuk 5 werden al twee soorten expressies genoemd die met pointers te maken hebben. Het zijn:

* expressie volg de gegeven pointer
& lvalue  maak een pointer naar deze variabele

Als n gedeclareerd is als int, dan is &n een pointer naar de geheugenplaats adres van variabele waar de n wordt bewaard (oftewel het ‘adres’ van n). Je kan deze pointer bijvoorbeeld toekennen aan een pointer-variabele, bijvoorbeeld aan p zoals hierboven gedeclareerd: p=&x;. Je zou ook p naar het derde element van de array a kunnen laten wijzen: p=&a[2];. Net als in Pascal kun je behalve de pointer ook de waarde waarnaar de pointer wijst veranderen. Als je de waarde waar p naar wijst gelijk aan 7 wilt maken, schrijf je *p=7;. Dit komt pointer volgen dus overeen met het Pascal-assignment p^:=7. In het geval dat p nog steeds naar a[2] wijst, is de waarde van a[2] meeveranderd. De unaire operatoren & en * zijn elkaars inverse. Er geldt *(&v)==v voor elke verband tussen * lvalue v, en &(*p)==p voor elke pointer p. en & In Pascal kun je pointers alleen naar geheugen laten wijzen dat je met new hebt aangemaakt. In C is het daarentegen ook mogelijk om pointers naar variabelen te laten wijzen. Zoals altijd kan daar ook misbruik van gemaakt worden. Als je bijvoorbeeld een globale pointer naar een lokale variabele laat wijzen, en daarna de functie beëindigt, heb je een pointer in handen naar een deel van de stack dat niet meer in gebruik is, en later misschien wel voor iets anders gebruikt wordt. Dat kan rare effecten geven; reken er in het volgende globale pointer

naar lokale variabele is gevaarlijk programma maar niet op dat de waarde ‘1’ geprint wordt:

    int *p;
    f()
    {   int a;
        a = 1;
        p = &a;  /* gevaarlijk! */
    }
    g()
    {   int b;
        b = 2;
    }
    main()
    {   f();
        g();
        printf("%d", *p );
    }

Dit soort geintjes wordt je in C geacht niet uit te halen (in Pascal is het, om deze reden, domweg onmogelijk gemaakt om pointers naar stack-variabelen te maken). Het is geen toeval dat de declaratie van een pointer (‘int *p;’) en een expres- overeenkomst de-

claratie en expressie sie die deze pointer volgt (‘*p’) zo op elkaar lijken. In feite kun je de declaratie ook als volgt interpreteren: ‘*p is een int’ – dus p moet wel een pointer naar een int zijn! Dit geldt ook voor arrays. De declaratie int a[5] betekent: ‘a[...] is een int’ – dus a is een array met integers. Behalve naar eerder gedeclareerde variabelen kun je pointers ook naar geheel nieuw geheugen laten wijzen (net als in Pascal). Hiervoor is de functie malloc beschikbaar. Waar je in Pascal new(p) zou schrijven, schrijf je in C: malloc

    #include <malloc.h>
    ....
    p = malloc( sizeof *p );

De functie malloc krijgt een int parameter, die aangeeft hoeveel bytes geheugen er nodig zijn. Om op een machine-onafhankelijke manier dit aantal te bepalen, is er een operator sizeof aanwezig. Deze operator berekent de beno- sizeof digde geheugenruimte die nodig is om zijn argument op te slaan. Een andere truc is uitgehaald met het resultaat-type van malloc. De functie moet immers pointers naar int kunnen opleveren, maar ook pointers naar bijvoorbeeld char. De oplossing hiervan (in ansi-C): malloc levert een void-pointer op, void-pointer en dat soort pointers zijn converteerbaar naar elk ander pointer-type. Fraai is het niet, maar daar staat tegenover dat er niet een ‘ingebouwde’ functie nodig is, zoals Pascal’s new. In Pascal kun je geheugen dat met new is aangemaakt weer vrijgeven met dispose. In C is dat ook nodig; de functie waarmee dit gebeurt heet free. free Je kunt met malloc ook grotere blokken geheugen aanvragen, bijvoorbeeld door

    p = malloc( 5 * sizeof(*p) );

Na dit statement wijst p naar de eerste van een rij van vijf integers. Anders gezegd: p wijst naar het startpunt van een array! Om van dit geheugen nuttig gebruik te kunnen maken, is het in C toegestaan om een pointer te indiceren. pointer als array Je kunt dus schrijven: p[0], p[1], p[2] enzovoort. De expressie p[0] is equivalent met *p. De compiler verbiedt het niet om te schrijven p[100], of zelfs p[-1], waarmee je geheugen buiten het gealloceerde gebied kunt bereiken. Ook hier geldt: niet doen!

WAARSCHUWING!

In C wordt bij array-indicering niet gecontroleerd of de index binnen de grenzen van de array, respectievelijk het gealloceerde geheugen, ligt. Schenden van de array-grenzen kan, vooral aan de linkerkant van een assignment, erg rare effecten geven.

Er zijn vele mogelijke toepassingen van pointers. Drie belangrijke ervan zijn:

• Dynamisch geheugen

• Call by reference

• Arrays als parameter We zullen deze drie toepassingen hieronder bespreken.

Dynamisch geheugen Eerder werd al genoemd dat je pointers, net als arrays, kan indiceren. De lengte van arrays moet tijdens het compileren vastliggen. Als je de lengte van een array pas runtime wilt beslissen, dan kun je in plaats van de array een pointer gebruiken. Zodra de gewenste array met onbelengte bekend is, laat je de pointer wijzen naar een stuk geheugen dat je kende lengte met malloc creëert.

Voor de rest van het programma maakt het niet uit of een array gedeclareerd is door int a[5]; of door int *p; met ergens in het programma p=malloc(5*sizeof(*p));. Je kunt in beide gevallen met indicering de elementen bereiken. Het komt vaak voor dat je in een eerste versie van een programma een array declareert, maar in een later stadium bedenkt dat je de lengte ervan variabel wilt maken. Je hoeft dan alleen de declaratie aan te passen en de initialisatie (met malloc) toe te voegen.

Voor de programmeur mag het indiceren van een pointer er hetzelfde uitzien als het indiceren van een array; voor de compiler is er wel degelijk een verschil. De gegenereerde code van a[3] en p[3] zal dan ook verschillend zijn.

Call by reference Bij functie-aanroep in C geldt altijd call by value; er wordt een lokale kopie van de waarde van de parameter gemaakt (net als in Pascal). Er is in C geen direct equivalent van var-parameters in var-parameter Pascal. Dat is ook niet nodig, want met pointers kun je hetzelfde effect bereiken. Het volgende programma is een voorbeeld hiervan:

    void wissel(int *x, int *y)
    {   int h;
        h  = *x;
        *x = *y;
        *y = h;
    }
    void main(void)
    {   int a,b,c;
        wissel( &b, &c );
    }

De functie wissel verwisselt twee gegeven waarden. Niet de waarden die je wilt verwisselen worden meegegeven, maar pointers naar deze waarden. Daardoor is het mogelijk dat de procedure een blijvend effect heeft, en niet alleen zijn lokale kopieën omwisselt. Omdat de functie pointers als parameter heeft, moeten ook pointer-waarden worden meegegeven. Pointers naar de te verwisselen variabelen kun je verkrijgen door de adres-van-operator (&) daarop toe te passen.

Array-parameters Het is in C niet mogelijk om arrays als parameter mee te geven aan een functie. Wel kun je een pointer naar het begin van de array meegeven. In de functie kun je deze pointer weer gewoon als array behandelen; je kan pointers immers ook indiceren. Er is echter array als paramegeen lokale kopie van de array gemaakt, alleen een lokale kopie van het ter startadres van de array. De volgende functie bepaalt de som van n elementen van de array a. (Het is een eerste versie; later volgt een optimalisering hiervan.)

         int arraysom(int n, int *a) /* eerste versie */
         {   int k, s;
             s = 0;
             for (k=0; k<n; k++)
                 s += a[k];
             return s;
         }
         void main(void)
         {   int b[10], x;
             ...
             x = arraysom(10, &(b[0]) );
         }

Je hoeft niet noodzakelijk het beginpunt van de array mee te geven.

Je kunt bijvoorbeeld de tweede helft van de array b sommeren met de deel van array als aanroep arraysom(5, &(b[5]));. Het is de verantwoordelijkheid van parameter de programmeur om ervoor te zorgen dat alleen bestaande elementen van de array worden opgeteld; de computer controleert dit niet.

Als p een pointer is naar een element van een array, dan kun je hem naar het volgende element laten wijzen. Dat gebeurt met het statement p=&(p[1]);. Omdat dit veel voorkomt, is er een eenvoudigere notatie voor: p=p+1;, wat op zijn beurt weer afgekort kan worden met p++;. Het is dus mogelijk om te rekenen met poin- ‘rekenen’ met pointers (althans met de operatoren + en -). ters De operatoren + en - zijn in C dus ook te gebruiken als één van de argumenten een pointer is. (De operatoren zijn overloaded: de uit te voeren aktie is afhankelijk van het type van de argumenten. Dat was trouwens al zo, want optellen van float waarden is iets anders dan optellen van int waarden.) Bij het optellen van een getal bij een pointer wordt rekening gehouden met de afmeting van het object waar de pointer naar wijst. Als p bijvoorbeeld wijst naar de integer die is opgeslagen op byte nummer 3412 in het geheugen, en de afmeting (sizeof) van een integer is 4, dan wijst p na het statement p+=2; naar byte nummer 3420. Het is zelfs zo, dat in de officiële definitie van C array-indicering in termen van pointer-rekenkunde gedefinieerd wordt:

• een array-variabele wordt behandeld als een constante pointer naar het beginpunt;

• de expressie a[k] is equivalent aan *(a+k). verband pointerrekenkunde en array-indicering De uitdrukking &(a[0]) is daarom equivalent aan &(*(a+0)), en omdat & en

* elkaars inverse zijn, is dit weer te vereenvoudigen tot a. Je mag dus de naam van een array schrijven op de plaats waar een pointer nodig is, mits de pointer niet veranderd wordt. Deze notaties kunnen we goed gebruiken in de functie arraysom, waar we eerder een eerste versie van schreven:

    int arraysom(int n, int *a) /* tweede versie */
    {    int k, s;
         for (s=k=0; k<n; k++)
             s += *a++;
         return s;
    }
    void main(void)
    {    int b[10], x;
         ...
         x = arraysom(10, b);
    }

Ten opzichte van de eerste versie zijn er drie dingen veranderd:

• De initialisatie van s en k gebeurt in één expressie.

• In de body van de loop wordt s verhoogd met de waarde van het arrayelement waar a naar wijst; daarna gaat a alvast naar het volgende element wijzen. De expressie *a++ is als volgt opgebouwd: a is een pointer; *a++ a++ heeft als neveneffect dat a naar het volgende element gaat wijzen, maar als waarde de waarde van a voor het ophogen; *a++ is de waarde waar de nog-net-niet-opgehoogde pointer naar toe wijst. Het gebruik van een pointer die op deze manier alle elementen langsloopt, is efficiënter dan herhaald indiceren, zoals in de eerste versie.

• In de aanroep van de functie wordt de parameter b geïnterpreteerd als

een pointer naar het beginpunt van de array. In sommige gevallen is het gewenst een pointer een waarde te geven die ‘nergens’ naar wijst. Dit komt vooral voor bij het gebruik van recursieve datastructuren zoals een lineaire lijst of een binaire boom (zie hoofdstuk 16). In NULL-pointer Pascal wordt hiervoor de speciale expressie nil gebruikt. In C wordt er in de file stdio.h een constante NULL gedefinieerd die voor dit doel gebruikt kan worden.

14 Strings

Een string is een pointer naar een rij characters. Het einde van de string wordt aangegeven door het null-character (het character met ascii-code 0, dus niet het character '0'). De waarde van een string-constante, bijvoorbeeld "hallo", is dan ook een pointer naar de eerste letter. De compiler voegt aan constante strings automatisch een null-character toe. Je kan een pointer-variabele naar zo’n string laten wijzen, bijvoorbeeld:

    char *s;
    s = "hallo";

Door dit assignment worden niet de letters van de string gekopieerd, maar het startadres. Functies die werken op strings, kunnen gebruik maken van het feit dat strings eindigen op het null-character. Een functie die de lengte van een string bepaalt is bijvoorbeeld de volgende: strlen

    int strlen(char *s) /* eerste versie */
    {   int n;
        n = 0;
        while (*s != 0)
        {   n++;
            s++;
        }
        return n;
    }

Net als in de functie arraysom gebruiken we hier weer de pointer die als parameter binnenkomt (s) om de hele string langs te lopen. Een teller (n) houdt het aantal gevonden letters bij. We kunen dit programma weer wat vereenvoudigen. Net als in de functie arraysom kan het ophogen van de pointer s alvast gebeuren tijdens inspectie van *s in de while-conditie. Bovendien is het overbodig om in een conditie !=0 te schrijven, want een waarde die ongelijk is aan nul wordt toch al als true beschouwd. De functie kan dus als volgt geschreven worden:

    int strlen(char *s)   /* tweede versie */
    {   int n;
        n = 0;
        while (*s++)
            n++;
        return n;
    }

Nog korter kan het met een for-statement, al kun je je afvragen of het programma er veel duidelijker van wordt:

    int strlen(char *s)   /* derde versie */
    {   int n;
        for (n=0; *s++; n++);
        return n;
    }

Het komt voor dat je niet alleen de pointer naar het begin van een string wilt kopiëren, maar dat je echt een kopie van de volledige string wilt hebben. Daarvoor is de volgende functie strcpy (string-copy) te gebruiken. Deze strcpy functie heeft twee parameters: een pointer naar de plaats waar de kopie moet komen (dest), en een pointer naar het origineel (source). De functie levert de pointer naar de kopie ook op als functieresultaat.

    char *strcpy( char *dest, char *source)
    {   char *res;
        res = dest;
        while (*source)
            *dest++ = *source++;
        *dest = 0;
        return res;
    }

Deze functie wordt in het volgende programma gebruikt:

    void main(void)
    {   char a[15], b[10], *p;
        strcpy( a, "informatie" );
        p = &(a[9]);
        strcpy( p, "ca" );
        strcpy( b, a ); /* fout! */
    }

De string "informatie" wordt gekopieerd naar de array a. In het tweede statement wordt de pointer p naar de tiende letter van a gezet, dus naar de letter 'e' (dit had ook gekund met p=a+9). Het stuk tekst vanaf deze plaats wordt vervolgens vervangen door de string "ca". De array a bevat daarna dus de tekst "informatica". Het laatste statement kun je maar beter niet in het programma zetten. De string "informatica" heeft immers elf letters, en daar komt ook nog eens het null-character bij. De array b heeft echter slechts ruimte voor tien characters.

WAARSCHUWING!

Bij het aanroepen van strcpy moet je ervoor zorgen dat de eerste parameter naar een voldoende groot stuk geheugen wijst. Houd daarbij ook rekening met het null-character: een string neemt één positie meer in beslag dan zijn strlen.

Ook van de boven gegeven functie strcpy is een (nog) kortere versie mogelijk:

    char *strcpy(char *dest, char *source)
    {   char *res;
        res = dest;
        while (*dest++ = *source++);
        return res;
    }

Doordat in deze functie van post-increment gebruik wordt gemaakt, wordt ook het null-charater nog meegekopieerd. Het assignment is immers al geschied, op het moment dat de while doorkrijgt dat de string is afgelopen. Een andere veel gebruikte functie op strings is strcat. Deze functie plakt strcat een kopie van de tweede parameter achter de eerste parameter. Daar moet natuurlijk wel weer genoeg ruimte voor zijn. De functie levert als resultaat een pointer naar het begin van de resultaatstring.

char *strcat(char *s, char *t)
/* Tekst van t wordt toegevoegd aan s.
 * Resultaat wordt ook opgeleverd.
 * De string t blijft onveranderd.
 */
{   char *res;
    res = s;
    while (*s) s++;
    while (*t) *s++ = *t++;
    *s = 0;
    return res ;
}

Een vierde nuttige functie op strings is de functie strcmp. Deze functie bepaalt strcmp welke van twee strings alfabetisch het eerste komt (volgens de ascii-ordening). Als s eerder komt dan t dan levert de functie een getal kleiner dan nul, als de strings gelijk zijn levert hij nul, en anders een getal groter dan nul.

int strcmp(char *s, char *t)
{    while (*s==*t)
     {    if (*s==0) return(0);
          s++;
          t++;
     }
     return( *s - *t );
}

In plaats van *s==0 kun je (cryptischer) !*s schrijven. Een laatste voorbeeld van functies op strings is de functie atoi (‘ascii to atoi integer’). Deze functie verwacht een string cijfers, en levert de overeenkomstige integer waarde hiervan op. Bijvoorbeeld atoi("123") levert 123.

int isdigit(char c)
{
     return ( '0'<=c && c<='9' );
}
int atoi(char *s)
{    int n;
     n = 0;
     while (isdigit(*s))
         n = 10*n + *s++ - '0';
     return n ;
}

De in dit hoofdstuk genoemde functies hoef je niet zelf in ieder programma te schrijven; ze zijn aanwezig in de functiebibliotheek string.h. De functie isdigit en aanverwante functies zijn te vinden in ctype.h. In hoofdstuk 12 is de functie printf genoemd. Deze functie kan waarden in allerlei representaties op het scherm zetten. Een variant hierop is de functie sprintf (‘string-print-formatted’). Deze functie doet hetzelfde als printf, sprintf maar zet het resultaat in een string in plaats van op het scherm. Een pointer naar de plaats waar het resultaat moet komen te staan wordt als eerste parameter aan sprintf meegegeven; daarna volgen dezelfde parameters als die van printf (dus eerst een control string, en dan een variabel aantal waarden). Je moet er, net als bij strcpy, zelf voor zorgen dat er voldoende ruimte is voor het resultaat. Een voorbeeld is:

    char a[100];
    ...
    sprintf( a, "decimaal %3d is hex 0x%x", n, n );

Tot slot volgen hier nog twee opmerkingen over string-constanten. Een stringconstante (zoals "hallo") is in zoverre constant, dat hij op een vaste plaats is opgeslagen. De tekst van de string is eventueel wel te veranderen, bijvoorbeeld stringconstante met: veranderen?

    char *s;
    s = "hallo";
    s[1] = 'e';

Hoewel dit in de meeste gevallen goed gaat, is het toch niet gebruikelijk om dit te doen om de volgende redenen:

• Sommige compilers plaatsen de tekst van constante strings in beschermd, ‘read-only’ geheugen.

• Sommige compilers slaan twee dezelfde string-constanten maar één keer op. Als je de tekst van één van de twee verandert, is de andere meeveranderd.

Verder is het belangrijk om onderscheid te maken tussen een NULL pointer en een lege string. Als geldt s==NULL, dan is *s een illegale operatie. Is echter lege string s=="" (de lege string) dan is *s wel mogelijk; er geldt dan *s==0.

15 Declaraties en typen II

De declaraties die tot nu toe behandeld zijn, hebben de volgende vorm:

type-modifier type declarator-list ;

Hierbij is de type-modifier (long, unsigned enz.) optioneel. De declaratorlist bestaat uit nul of meer declaratoren, gescheiden door komma’s. Voor declarator declarator zijn er vijf mogelijkheden (waarvan we de laatste twee nog niet eerder zijn tegengekomen):

• identifier

• * declarator

• declarator [ constante ]

• declarator ()

• ( declarator ) De eerste vorm declareert een variabele van een basistype, de tweede vorm een pointer-variabele en de derde vorm een array. De vierde vorm wordt gebruikt om functies te declareren. Hiermee is het mogelijk om functies als parameter aan andere functies mee te geven. De gebruikelijke logica gaat hier weer op: in de declaratie int f(); is f() een int – dus is f een functie die een int oplevert. De laatste declaratie-vorm maakt het mogelijk in declaraties groeperingshaakjes te zetten. Bijvoorbeeld int *(a[5]) declareert een array met vijf pointers, terwijl int (*b)[5] een pointer declareert naar een array met vijf integers. De eerste mogelijkheid is de default, en mag dus ook geschreven worden als int *a[5]. Achter elke declarator kan ook meteen een initialisatie gegeven worden. In initialisatie plaats van

    int n;
    n = 0;

mag je dus schrijven int n=0;. Ook arrays kun je initialiseren; je moet daartoe de elementen opsommen en tussen accolades zetten. Bijvoorbeeld:

    int maandlen[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
    char *dagnaam[7] = {"maandag", "dinsdag", "woensdag",
                         "donderdag", "vrijdag",
                         "zaterdag", "zondag"    };

In het laatste voorbeeld is dagnaam een array met zeven pointers, die alvast gaan wijzen naar zeven verschillende teksten. Lokale arrays mogen niet geïnitialiseerd worden; globale initialisaties moeten constanten, of expressies met een constante waarde zijn. Als laatste onderdeel van een declaratie mag er vóór de type-modifier nog een storage class storage class staan. Er zijn vier mogelijke storage classes:

• static

• auto

• register

• extern Default voor globale variabelen is static; default voor lokale variabelen is auto. De betekenis hiervan is als volgt. Variabelen die static gedeclareerd zijn, blijven gedurende het hele programma actief. Variabelen met storage- static class auto worden op de stack gezet; bij recursieve procedures krijgt dus elke recursieve aanroep zijn eigen lokale variabelen. Met register kun je de compiler verzoeken deze variabele extra efficiënt op te slaan (sommige compilers doen dat al uit zichzelf). Met een extern declaratie tenslotte kun je het type extern aangeven van variabelen die in een andere file, die apart gecompileerd wordt, gedeclareerd worden. In de praktijk hoef je de storage-class zelden op te geven. Er is, afgezien van extern declaraties, eigenlijk maar één geval waar je er nuttig gebruik van kunt maken. De volgende functie is een voorbeeld hiervan:

    void f(void)
    {   static int t=0;
        t++;
        printf("ik ben %d keer aangeroepen", t );
    }

De variabele t heeft de gecombineerde voordelen van lokale en globale variabelen:

• De variabele blijft gedurende het hele programma bestaan. Hij wordt dus niet opgeruimd aan het eind van de functie-aanroep, wat het geval zou zijn als z’n storage-class niet static was.

• De variabele kan alleen binnen de functie worden gebruikt; de normale blokstructuur-regels blijven gelden.

16 Structures

Net als in Pascal zijn er in C records mogelijk, dat wil zeggen een aantal waarden die als één geheel beschouwd worden. Records worden in C structures genoemd. Het type van zo’n structure kun je aangeven met het woord struct gevolgd door een zelfgekozen naam. De eerste keer dat je een structure struct opschrijft, moet je ook de onderdelen (velden) opsommen. Bijvoorbeeld:

    struct persoon
    {   char naam[20];
        int  leeftijd;
    } jan;
    struct persoon wim, jet, *p;

Deze declaraties maken ruimte voor drie structuren, waarin steeds 20 letters en een getal opgeslagen kan worden, en voor een pointer die naar dergelijke structuren kan wijzen. Je ziet vaak dat met de eerste declaratie nul variabelen worden gedeclareerd (dat mag!), zodat de eerste declaratie lijkt op een soort bij eerste declara-

tie velden opsommen type-declaratie:

    struct persoon
    {   char naam[20];
        int  leeftijd;
    } ;
    struct persoon wim, jet, *p;
    struct persoon klas[30];

Als je daarentegen maar één exemplaar van een bepaalde structure wilt hebben, dan mag je de naam van de structure weglaten: naamloze struc-

ture

    struct
    {   char letter;
        int  waarde
    } tabel[26];

Na bovenstaande declaraties zijn bijvoorbeeld de volgende statements mogelijk:

    strcpy( wim.naam, "willem" );
    jet.leeftijd = 10;
    p = &jet;
    (*p).leeftijd ++;
    p = klas+4;
    strcpy( (*p).naam, "vijfje" );
    p++;

Net als in Pascal kun je de elementen van een structure gebruiken door achter de expressie die de hele structure aangeeft een punt en de veldnaam te zetten. s.veld Het eerste statement kopieert dus de naam "willem" naar de naam-component van de variabele wim, het tweede statement verandert van de variabele jet de leeftijd component. Met het derde statement gaat p wijzen naar jet. Met

pointer naar structure

het vierde statement wordt de leeftijd-component van de structure waar p naar wijst opgehoogd (en wordt daarmee 11). Met het volgende statement gaat p wijzen naar de vijfde structure uit een array van 30 structures. Op deze plaats wordt een naam ingevuld. Het laatste statement laat p naar de volgende structure uit de klas wijzen. Let op het verschil tussen het ophogen van p (met het statement p++;) en het ophogen van een veld uit de structure waar p naar wijst (met (*p).leeftijd++;). Dit laatste komt overeen met het Pascal statement p^.leeftijd:=p^.leeftijd+1. De haakjes rond *p zijn nodig, omdat de punt een hogere prioriteit heeft dan het sterretje. Omdat dit soort expressies zo vaak voorkomt, is er een speciale p->veld notatie die deze haakjes overbodig maakt:

             p->leeftijd is hetzelfde als (*p).leeftijd

De symbolen -> moeten hierin een pijltje voorstellen. In de praktijk gebruik je beide notaties:

• wim.leeftijd als je een veld van een concreet structure wilt hebben;

• p -> leeftijd als je een veld wilt hebben van een structure waar je een

pointer naar hebt. Door slim gebruik te maken van de post-increment operator van C kun je bepaalde expressies zeer compact opschrijven. Bijvoorbeeld

    p++ -> leeftijd ++

verhoogt de leeftijd-component van de structure waar p naar wijst, en laat p daarna naar het volgende element van de array van structures wijzen. Als een wat ingewikkelder voorbeeld van het gebruik van structures volgt nu een voorbeeld van de declaratie van een binaire zoekboom. binaire zoekboom

    struct knoop
    {    char woord[20];
         struct knoop *links, *rechts;
    };
    struct knoop *root = NULL;

De wortel van de boom wordt hierbij geïnitialiseerd op NULL, de speciale pointer die ‘nergens’ naar wijst (vergelijk nil in Pascal). We kunnen een recursieve NULL functie insert schrijven die een element aan de boom toevoegt. De eerste, nog niet helemaal goede poging, luidt als volgt:

    insert(char *naam, struct knoop *t)
    /* nog niet helemaal correcte versie */
    {   struct knoop *nieuw;
        if (t == NULL)
        {   nieuw = malloc( sizeof *t );
            strcpy( nieuw->woord, naam );
            nieuw->links = nieuw->rechts = NULL;
            t = nieuw;
        }
        else
            insert(naam, ( strcmp(naam, t->woord)<0
                         ? t->links
                         : t->rechts
                         )
                   );
    }
    void main(void)
    {   insert( "aap",  root );
        insert( "noot", root );
    }

Als de boom waarin je wilt invoegen leeg is, maak je een nieuwe knoop, waarin de naam en twee NULL-pointers worden gezet. Anders wordt de functie recursief aangeroepen met de linker of de rechter deelboom, afhankelijk van het feit of de in te voegen naam kleiner of groter is dan het in de boom aangetroffen woord. De keuze wordt gemaakt met behulp van een conditionele expressie. Helaas werkt deze versie nog niet naar behoren, want de pointer t wordt by value doorgegeven, waardoor het assignment t=nieuw geen blijvend effect heeft. Je zou van deze parameter in Pascal een var-parameter willen maken. Dat kan in C ook, door een pointer naar de waarde mee te geven. Dat de waarde in dit geval zelf op zijn beurt ook een pointer is geeft niets; we zetten gewoon insert in een overal *t in plaats van t. Bij aanroep zetten we een & voor de parameter. zoekboom

    insert(char *naam, struct knoop **t)
    /* verbeterde versie */
    {   struct knoop *nieuw;
        if (*t == NULL)
        {   nieuw = malloc( sizeof **t );
            strcpy( nieuw->woord, naam );
            nieuw->links = nieuw->rechts = NULL;
            *t = nieuw;
        }
        else
            insert(naam, ( strcmp(naam, *t->woord)<0
                         ? &(*t->links)
                         : &(*t->rechts)
                         )
                   );
    }
    void main(void)
    {   insert( "aap",  &root );
        insert( "noot", &root );
    }

Hiermee is de beschrijving van de binaire zoekboom voltooid. Eerder in dit hoofdtuk hebben we een declaratie van nul variabelen gebruikt als een soort type-declaratie. In C is ook een ‘echte’ typedeclaratie mogelijk. Een typedeclaratie heeft dezelfde opbouw als een variabele-declaratie. Het enige verschil is, dat in een type-declaratie op de plaats van de storage class het woord typedef staat. Zo kunnen we bijvoorbeeld schrijven: typedef

    typedef char *STRING;

Hierna kunnen we STRING x,y schrijven in plaats van char *x, *y. Na zo’n type-declaratie lijkt het dus echt dat er een nieuw type aan de taal is toegevoegd. Interessanter wordt zo’n typedeclaratie in combinatie met structures:

    typedef
        struct knoop
        {   char woord[20];
            struct knoop *links, *rechts;
        }
        KNOOP, *BOOM;

Na deze declaratie is KNOOP hetzelfde type als struct knoop, en BOOM is het type van een pointer naar zo’n knoop. Het zoekboom-programma wordt daarmee iets overzichtelijker:

    BOOM root;
    insert(STRING naam, BOOM *t)

De tweede parameter van insert kun je nu lezen als ‘pointer naar een BOOM’, en dat is gemakkelijker te begrijpen dan ‘pointer naar pointer naar struct

knoop’.

Naast struct is er in C nog een mechanisme om nieuwe typen te construeren: union. Net als een struct bestaat een union uit een aantal velden. Het union verschil tussen een struct en een union kun je op verschillende manieren omschrijven:

• In een struct kun je alle velden naast elkaar gebruiken, in een union kun je er maar één tegelijk gebruiken.

• Een struct is het cartesisch product van zijn velden, een union is de onderscheiden onderscheiden vereniging (disjoint union) ervan. vereniging

• De afmeting (sizeof) van een struct is de som van de afmetingen van zijn velden, van een union is het het maximum ervan.

• De velden van een struct worden naast elkaar opgeslagen, die van een

union over elkaar heen. De declaratie van een union verloopt precies zo als die van een struct, alleen het woord struct wordt vervangen door union. Toepassingen van het union-mechanisme zijn dezelfde waar je in Pascal een variant record zou gebruiken. Bijvoorbeeld een symbol-table, waar bij elke ‘variant record’ naam een int òf een float wordt opgeslagen, kan als volgt worden gedeclareerd:

    struct
    {   char *naam;
        int  soort;
        union
        {    int   intwaarde;
             float floatwaarde;
        }
    } symboltable[100];

17 De preprocessor II

In hoofdstuk 11 werd het gebruik van #define besproken. Dit preprocessorcommando werd gebruikt om constanten te definiëren, en om namen te geven aan bepaalde (combinaties van) symbolen:

    #define PI       3.1415926535
    #define begin    {
    #define boolean  int

Namen die met #define worden gedefinieerd kunnen ook van parameters wor- #define met paden voorzien. De volgende definities zijn bijvoorbeeld mogelijk: rameters

    #define KWAD(x)      ((x)*(x))
    #define LENGTE(x,y)  sqrt(KWAD(x)+KWAD(y))

Zo’n definitie met parameters wordt een macro genoemd. Het openingshaakje van de parameterlijst moet bij macro-definities direct volgen op de macronaam; anders zou KWAD een afkorting zijn voor (x) ((x)*(x)) (dat moet macro immers ook kunnen…). Er zijn grote verschillen tussen een macro en een functie. Bij gebruik van een tekstuele substimacro vindt textuele substitutie plaats: schrijf je in een programma KWAD(3) tutie dan lijkt het voor de compiler of je ((3)*(3)) had geschreven. Voor deze expressie wordt dan code gegenereerd. Bij gebruik van een functie wordt code gegenereerd voor aanroep van een functie, waarbij de parameteroverdracht over de stack plaatsvindt. Het enigszins paranoïde gebruik van haakjes in macro-definities moet ervoor zorgen dat de macro met willekeurige argumenten gebruikt kan worden. Zou KWAD(x) simpelweg zijn gedefinieerd als x*x, dan wordt de aanroep 3*KWAD(1+2) door de compiler gezien als 3*1+2*1+2, waardoor het resultaat 8 is in plaats van 36.

WAARSCHUWING!

Bij aanroep van een macro worden de parameters textueel gesubstitueerd, zonder te letten op typen en prioriteiten. Zet daarom in macrodefinities indien nodig haakjes om elk gebruik van de parameter, en haakjes om de gehele vervang-tekst.

Het is onverstandig om in macrodefinities vrije variabelen te gebruiken. Tekstuele substitutie trekt zich namelijk niets aan van blokstructuur. In het vol- macros en gende programma vermenigvuldigt de functie f zijn parameter met de globale blokstruktuur variabele a, maar de macro M vermenigvuldigt zijn parameter met de lokale variabele a zoals die geldt op het moment van aanroep:

    int a=3;
    #define M(x)  a*x
    int f(int x)
    {   return(a*x);
    }
    void main(void)
    {   int a=2;
        printf( "%d\n", f(5) );
        printf( "%d\n", M(5) );
    }

Het programma zal daarom de waarden 15 en 10 schrijven. Zinvol gebruik van macro’s zijn bijvoorbeeld de volgende definities:

    #define VOLG(x)   ((x)->buren->volgende)
    #define repeat    do {
    #define until(x)  } while (!(x))

De laatste twee voor verstokte Pascal-programmeurs.

Macro’s zijn in het algemeen sneller dan functies (omdat het kopiëren van de parameters op de stack vermeden wordt), maar kosten meer geheugen (omdat bij elke aanroep code gegenereerd wordt voor de complete tekst van de

efficiëntie van macro

definitie). Naast #define en #include zijn er nog een aantal commando’s voor de preprocessor. Met #ifdef, #else en #endif is het mogelijk een deel van het #ifdef programma alleen te compileren als een bepaalde constante is gedefinieerd met #define. Deze constructie kan goed gebruikt worden om een programma te schrijven dat, met kleine aanpassingen, op verschillende computers kan lopen. Om het programma op een andere computer te compileren, hoeft dan machine-

onafhankelijke programma’s slechts één definitie veranderd te worden. Een voorbeeld hiervan is een programma dat een file moet lezen waarin 16-bits getallen staan:

    #define KLEINE_INTS
    #ifdef KLEINE_INTS
        int x;
    #else
        short int x;
    #endif

Wordt dit programma gebruikt op een computer met 16-bits integers, dan laat je de eerste regel staan; op computers met 32-bits integers hoef je alleen de eerste regel te verwijderen. Een andere toepassing van #ifdef/#endif is het toevoegen van statements die alleen tijdens de ontwikkeling van het programma van belang zijn: debug-statements

    int f(int x)
    {
        #ifdef DEBUG
        printf("f wordt aangeroepen met %d\n", x );
        #endif
        return(.....);
    }

Tijdens het ontwikkelen van het programma wordt gecompileerd met een regel #define DEBUG; in het definitieve programma wordt deze regel weggehaald.

18 Unix-interactie

Onder de meeste operating-systemen is het mogelijk parameters mee te geven aan programma’s. Ook bij het uitvoeren van C-programma’s s het mogelijk om parameters mee te geven. Deze parameters worden in het programma beschikbaar gesteld als parameters van de functie main. Tot nu toe hebben we steeds aangenomen dat main een functie zonder parameters is. Voor programma’s die zonder parameters werken is dat prima (het operating systeem werkt ongetypeerd…). In programma’s die parameters van het operating systeem willen ontvangen, heeft de functie main echter twee parameters. Dit is het geval in het volgende voorbeeld: parameters van

                                                                     main
    int main(int argc, char **argv)
    {   int k;
        for (k=1; k<argc; k++)
            printf("De %de parameter is %s\n", k, argv[k] );
        printf("De programmanaam is %s\n", argv[0] );
        return 0;
    }

De parameter argv (‘argument-vector’) is een array met strings. Een string argv wordt immers gerepresenteerd als pointer naar het eerste character, en een array-als parameter is een pointer naar het eerste element – vandaar char **argv;. De parameter argc (‘argument-count’) geeft aan uit hoeveel elementen de argc argument-vector bestaat. De waarde van argv[0] is altijd de naam van het programma. Als het programma geen parameters heeft, is de waarde van argc dus 1. Als er één parameters is, is argc gelijk aan 2, en de parameter is te vinden in argv[1], enzovoort. De functie main levert een integer op. Deze waarde is in het operating systeem exit code beschikbaar als exit code van het programma. Bij veel programma’s wordt de conventie gebruikt dat parameters die met een

- beginnen, een optie aangeven. Het volgende programma kan de opties p en q optie herkennen, en bewaart de namen van de ‘echte’ parameters in de array paramv (en het aantal in paramc). Het kan gebruikt worden als raamwerk voor veel programma’s.

    int optieP=0, optieQ=0;
    void usage(void)
    {   printf("usage:  raamwerk [-p] [-q] file ...\n");
        exit(1);
    }
    int main(int argc, char **argv)
    {   int paramc=0; char *paramv[10];
        int k;
        for (k=1; k<argc; k++)
        {   if (argv[k][0]=='-')
                 switch(argv[k][1]
                 {  case 'p': optieP=1;
                              break;
                    case 'q': optieQ=1;
                              break;
                    default:  usage();
                 }
            else paramv[paramc++] = argv[k];
        }

        if (paramc==0) usage();
        for (k=0; k<paramc; k++)
            DoeIetsMet(paramv[k]);
        return 0;
    }

De functie exit kan gebruikt worden om het programma voortijdig af te breken exit vanuit andere functies dan main. Parameters van programma’s zijn over het algemeen namen van files. Deze files zul je meestal willen gebruiken om iets uit te lezen, of iets in te schrijven. In de file stdio.h zijn een groot aantal functies gedeclareerd waarmee files gemanipuleerd kunnen worden. Deze functies komen overeen met de functies die op de standaard-input en standaard-output gebruikt kunnen worden (zie hoofdstuk 12), maar hebben één parameter meer: de file die gebruikt moet worden. Elke file die je wilt gebruiken moet eerst geopend worden. Hiervoor is de functie fopen, die een pointer oplevert naar een object van het type FILE. FILE Een FILE is een of ander structure. Hoe dit structure precies is opgebouwd is niet belangrijk; het enige wat je met een FILE-pointer hoeft te doen is meegeven aan de lees- of schrijf-functies. In het volgende programma wordt het gebruik van files gedemonstreerd:

    #include <stdio.h>
    void main(void)
    {   FILE *in, *uit;
        int c; char a[100];
        /* openen van files */
        in  = fopen( "aap",  "r" );
        uit = fopen( "noot", "w" );
        /* low-level I/O: */
        c = fgetchar(in);
        fputchar(uit, c);
        /* high-level I/O: */
        fscanf (in,  "%s",   a   );
        fprintf(uit, "%d\n", 123 );
        /* sluiten van files */
        fclose(in);
        fclose(uit);
    }

In de meeste programma’s wordt òf low-level I/O (fgetchar en fputchar) òf high-level I/O (fscanf en fprintf) gebruikt, dus niet deze twee nivo’s door elkaar heen. Files die niet meer gebruikt worden moeten worden gesloten fclose door aanroep van fclose. Bij inputfiles is dit alleen om de ruimte die voor het beheer van de file wordt gebruikt weer vrij te geven, bij output-files is het nodig om de veranderingen definitief te maken. De eerste parameter van fopen is de naam van de file. De tweede parameter fopen is een string (geen character!) die aangeeft hoe de file geopend moet worden:

"r" ‘read’: de file wordt uitsluitend gebruikt om te lezen.

"w" ‘write’: er wordt een nieuwe file gecreëerd om in te schrijven. Als er al een file bestond met deze naam, dan wordt die weggegooid.

"a" ‘append’: als de file al bestaat, wordt de nieuwe informatie aan het eind toegevoegd. Als de file nog niet bestaat, wordt een nieuwe gecreëerd.

"r+" ‘read and update’: file kan zowel voor lees- als schrijfoperaties worden gebruikt. De te openen file moet al bestaan.

"w+" ‘write and update’: als "w", maar de geschreven informatie kan direct weer worden teruggelezen. "a+" ‘append and update’: als "w+", maar eventuele oude

informatie blijft bestaan

In elke file wordt een ‘huidige positie’ bijgehouden. Voor files van het type "r" en "w" wordt deze aan het begin van de file gezet; voor files van het type "a" aan het einde van de file. Voor files van de laatste drie typen is het mogelijk om de ‘huidige positie’ te verzetten. Daartoe is de functie fseek beschikbaar: fseek

    int fseek(FILE *f, int offset, int mode)

de parameter offset geeft hierbij de positie aan die de ‘huidige positie’ moet worden, ten opzichte van een punt dat wordt aangegeven door mode:

SEEK_SET het begin van de file;
SEEK_CUR de huidige positie;
SEEK_END het einde van de file.

Met de functie ftell

    int ftell(FILE *f)

kun je de waarde van de huidige positie opvragen.

Index

a.out 2 aanhalingstekens 14 accolade 2, 4 additieve operator 15 adres van variabele 19, 30 aftrekken 15 and 17 apostrophe 14 argc/argv 47 ariteit van operatoren 15 array 29

als parameter, 32 grenzen, 31 indicering, 5 lengte, 29 versus pointer, 33

ascii-code 11 assignment 3, 5, 17 atoi 37 auto 39

backslash 14 binaire boom 42 binaire operator 15 bitsgewijze operatoren 16, 19 boolean 11 boolean vector 17, 19 break-statement 8

call by reference 32 cartesisch product 44 case 8 cc 2 char 11 commentaar 3 compiler 2 compound statement 4 conditioneel statement 3 conditionele expressie 20 constante 5, 13 continue-statement 9 control string 26, 28 conversie van typen 13, 17 conversies 13 ctype.h 24, 38

declaratie 11 array, 29 constante, 22 extern, 40 globale, 11 initialiserende, 39 lokale, 4, 11 pointer, 29–30 struct, 40 type, 43 union, 44 declarator 38 decrement-operator 19 default 8

                                   #define 22, 44

delen 15

dereferencing 18 disjoint union 44 dispose (Pascal) 31 div 15 do-while-statement 7 double 12, 14 dynamisch geheugen 31

else 3

                                   #else 46
                                   #endif 46
                                   EOF 25

exit 48 exit code 47 expressie 4

als statement, 5 assignment, 5, 17 conditionele, 20 groepering, 5 operator, 5

extern 40

fclose 48

fieldwidth (printf) 27 file 48 flags (printf) 27 float 11–12, 14 floating point operaties 18 fopen 49 for-statement 6 fprintf 48 fputchar 48 free 31 fscanf 48 fseek 49 ftell 49 functie 20

aanroep, 21 main, 2 met parameters, 20 resultaat, 9 versus macro, 45 void resultaat, 22 zonder parameters, 20 zonder resultaat, 4, 11, 21

functiebibliotheek 24

gelijkheid 16 geneste loop 7 gereserveerd woord 3 getchar 25 globale declaratie 11

hexadecimaal getal 13, 26 hoofdprogramma 2

i/o-redirection 24 identifier 3 if-statement 3

#ifdef 46
#include 22–23

increment-operator 19 indirectie 18 initialisatie 39 int 11, 13

keyword 3

lege statement 10 letterteken 11 logische operator 17, 19 logische operatoren 19 lokale declaratie 4, 11 lokale variabele 21 long 12 loop 4

lege body, 10

onderbreken van, 8–9 oneindige, 7, 10–11

lvalue 4

macro 45 main 2, 46 malloc 30–31 math.h 24 mod 15 multiplicatieve operator 15

nauwkeurigheid 12

neveneffect 5 new (Pascal) 30 nil (Pascal) 34 not 19 NULL-pointer 34, 38, 42

octaal getal 13, 26

onderscheiden vereniging 44 oneindige loop 7, 10–11 ongelijkheid 16 optellen 15

pointers, 33 optie 47 or 17

parameter 20 by reference, 32 van macro, 45 van main, 46

pointer 29 als array, 31 declaratie, 30 naar lokale var, 30 naar struct, 41 naar variabele, 30 null, 34, 38, 42 optellen, 33 versus array, 33 volgen, 30 post-increment 19 postfix-operator 18 pre-increment 19 precision (printf) 27 prefix-operator 18 preprocessor 22, 44 printf 26, 38 prioriteit 15 procedure 4, 11, 21 puntkomma 4 putchar 25

real (Pascal) 11 record (Pascal) 40 record-selectie 5 redirection, I/O 24 register 39 relationele operator 16 repeat-until (Pascal) 7, 45 representatie 12 return-statement 9, 21

scanf 28 schuif-operator 16 seek in file 49 short 12 signed 12 sizeof 19, 30, 32, 44 spaties op invoer 29 sprintf 38 standaard input/output 24 statement 3

assignment, 3, 5, 17 break, 8 compound, 4 continue, 9 do-while, 7 expressie als, 4 for, 6 if, 3 lege, 10 procedure-aanroep, 4 return, 9, 21 switch, 8 while, 4

static 39–40 stdio.h 24 storage class 39, 43 strcat 37 strcmp 37 strcpy 35 string 14, 34 concateneren, 37 constante, 5, 14, 38 kopiëren, 35 lege, 38 lengte, 35 vergelijken, 37

string.h 24, 38

strlen 35 struct 40

pointer, 41 selectie, 41 switch-statement 8

terminatie 7

ternaire operator 15, 20 then 3 time.h 24 type modifier 12, 38 type-conversie 17 typedef 43

unaire operator 15, 18 union 44 unsigned 12

var-parameter 32 variabele 3, 5 variant record (Pascal) 44 vermenigvuldigen 15 void 11, 22 void-pointer 31

while-statement 4 whitespace 28 writeln (Pascal) 26