INHOUD 1 INLEIDING 2 ENKELE SPELLINGSREGELS 2.1 NAMEN (indentifiers) 2.2 KEYWORDS 2.3 CONSTANTEN 2.3.1 INTEGER CONSTANTEN 2.3.2 KARAKTER CONSTANTEN 2.3.3 FLOATING GETALLEN 2.3.4 STRINGS 2.4 COMMENTAAR 3 EENVOUDIGE EXPRESSIES EN STATEMENTS 3.1 OPTELLEN, AFTREKKEN, VERMENIGVULDIGEN 3.2 DECLAREREN EN ASSIGNEREN 3.3 RELATIONELE EN LOGISCHE OPREATOREN 3.4 CONDITIONELE STATEMENTS 3.5 HERHALINGS STATEMENTS 3.6 DE SWITCH-, DE BREAK- EN DE CONTINUE STATEMENT 3.7 DE STANDAARDFUNCTIES GETCHAR EN PUTCHAR 4 MEER OPERATOREN 4.1 INCREMENT, DECREMENT, ASSIGNMENT 4.2 OPERATOREN VOOR BITMANIPULATIE 4.3 CONDITIONELE EXPRESSIES 4.4 DE COMMA OPERATOR 4.5 ASSOCIATIVITEIT 4.6 OVERZICHT VAN DE OPERATOREN 5 ARRAYS EN POINTERS 5.1 RIJEN 5.2 POINTERS EN ARRAYS 5.3 POINTERS EN PARAMETEROVERDRACHT 5.4 MEERDIMENSIONALE ARRAYS 5.5 EEN VOORBEELD: HET ZOEKEN IN EEN ARRAY 5.6 ARRAYS VAN POINTERS 5.7 HET BEGRIP LVALUE 6 TYPES EN CONVERSIE 6.1 NOG ENIGE ELEMENTAIRE TYPES 6.2 TYPE CONVERSIE 6.3 DE CAST OPERATOR 7 FUNCTIES EN DE PROGRAMMA STRUCTUUR 7.1 EEN EENVOUDIGE RESURSIEVE FUNCTIE 7.2 HET TYPE VAN ARGUMENTEN EN FUNCTIE WAARDE 7.3 EEN FUNCTIE VOOR DE MACHTSVERHEFFING 7.4 EXTERNE VARIABELEN 7.5 STATISCHE VARIABELEN 7.6 REGISTER VARIABELEN 7.8 INITIALISEREN 7.8.1 SIMPELE VARIABELEN 7.8.2 ARRAYS 7.8.3 ARRAYS EN KARAKTERSTRINGS 7.9 POINTERS NAAR FUNCTIES 8 STRUCTUREN 8.1 INLEIDING 8.2 FUNCTIES EN STRUCTUREN 8.3 DYNAMISCHE GEHEUGEN ALLOCATIE 8.4 DE OPERATOR SIZEOF 8.5 DYNAMISCHE STRUCTUREN; EEN BINAIRE BOOM 8.6 STRUCTUREN MET BIT VELDEN 8.7 UNIONS 8.8 TYPEDEF 9 PREPROCESSOR FACILLITEITEN 9.1 #DEFINE 9.2 #INCLUDE 9.3 CONDITIONELE COMPILATIE 9.4 #LINE 10 IN- EN UITVOER 10.1 INLEIDING 10.2 DE FUNCTIE PRINTF 11 DIVERSE ANDERE ONDERWERPEN 11.1 DE GOTO STATEMENT EN EXIT 11.2 DE PROGRAMMA ARGUMENTEN ARGV EN ARGV 11.3 ANDERE FUNCTIES 1 INLEIDING De taal is bruikbaar voor zeer veel toepassingen. In tegenstel- ling tot Pascal ,is C niet primair bedoeld voor het onderwijs. Goede implementeerbaarheid, efficiency, uitdrukkingskracht en enige affiniteit met machinetaal staan meer centraal in C dan 'veiligheid', d.w.z. bescherming van de gebruiker tegen zichzelf. Daarom is C vooral een taal voor specialisten die er genoegen mee nemen dat een fout niet altijd een duidelijke foutmelding tot gevolg heeft; zij nemen bewust zelf de verantwoordelijkheid voor de correctheid van het programma op hun schouders. Heeft men eenmaal dit niveau bereikt dan is C een bijzonder plezierige taal om mee te werken. Aan een geavanceerd wiskundeboek stelt men gewoonlijk niet de eis dat het leesbaar moet zijn voor leken. Velen stellen zo'n eis wel aan een computerprogramma, hetgeen leidt tot het ontstaan van programmeertalen waarin men alles overdreven verbaal moet noteren. De taal C behoort hier zeker niet toe. Voor de leek is een C-programma al gauw cryptisch; wie C eenmaal beheerst daarentegen, vindt een C-programma uitstekend leesbaar ,althans als bij het schrijven ervan zekere stijlregels in acht worden genomen. Dit laaste is geen overbodige waarschu- wing ,het is ook mogelijk vrijwel onleesbare C-programma's te produceren. Ten aanzien van de wijze van inspringen, zoals bij GFA basic of Pascal, wordt aanbevolen de hier toegepaste na te volgen of zelf een andere manier te kiezen en daar consequent de hand aan te houden. Het volgende C-programma dient als aller- eerste kennismaking. VOORBEELD 1 Lees twee gehele getallen a en b in en druk de som a+b af. Uitwerking: main() { int a, b; printf("geef twee gehele getallen: "); scanf("%d%d", &a, &b); printf("Som:%d\n", a+b); } Een C-programma bestaat uit een of meer functies. Voor een hoofdprogramma is de functie naam steeds main. In het programma kan gebruik gemaakt worden van andere functies. Dit kunnen zijn: a. standaardfuncties, zoals de hier gebruikte functies printf en scanf; b. functies die dezelfde file opgenomen zijn; c. functies die in een andere file voorkomen en separaat zijn vertaald. Functies zijn gemakkelijk te herkennen ,hun naam wordt in de regel gevolgd door een stel ronde haakjes, waartussen parameters geplaatst kunnen zijn. Een parameter noemt men ook dikwijls argument. In het bovenstaande programma wordt gebruik gemaakt van de standaard functies printf en scanf. Deze dienen voor in-, respectivelijk uitvoer. Het eerste argument van de functies scanf en printf is een zogenaamd format-string. Deze omschrijft het externe formaat van de in te lezen of af te drukken gegevens. De letter d in %d vertelt dat de te lezen of te schrijven gehele getallen decimaal worden genoteerd. De aanduiding \n zorgt ervoor dat op een nieuwe regel wordt overgegaan. Een format string begint en eindigt met een aanhalingsteken. Bij printf dient de format string in eerste plaats om duidelijk te maken hoe er moet worden afgedrukt. Bovendien kunnen af te drukken stukjes tekst, zogenaamde strings, erin worden opgenomen. In ons programma is dat de eerste keer Geef twee gehele getallen: en de tweede keer Som: De eventuele volgende argumeten van printf, zoals a+b, vertellen uitsluitend wat er moet worden afgedrukt. Door printf("Som:%d\n", a+b); wordt eerst de string Som: afgedrukt. Daarna t.g.v. %d de getalwaarde van a+b als een geheel decimaal getal afgedrukt. Tenslotte wordt door \n naar het begin van een nieuwe regel overgegaan. De hieraan voorafgaande regel scanf("%d%d", &a, &b); zorgt ervoor dat de geheeltallige variabelen a en b de waarden krijgen die in decimale vorm worden ingetypt. We schrijven hier &a en &b i.p.v. a en b. De notatie is hier anders dan bij printf, maar er gebeurt ook wat anders! Bij printf is a+b een getal. Bij scanf zijn &a en &b bedoeld om aan te geven waar de te in te lezen getallen in het geheugen moet worden komen te staan. Anders gezegd: de waarden &a en &b zijn geen gewone getallen, maar zogenaamde 'adressen' ,dwz. de nummers van de geheugenplaatsen a en b. De technische term voor grootheden als &a en &b is pointer. Pointers en parameteroverdracht zijn onderwerpen die in hoofdstuk 5 uitvoeriger aan de orde komen. 2 ENKELE SPELLINGSREGELS 2.1 NAMEN (indentifiers) Een naam ofwel indentifier is een rij letters en/of cijfers; tevens mag een underscore (_) en een naam voorkomen. Een naam mag niet beginnen met een cijfer. Hoofdletters en kleine letters worden als verschillend beschouwd: er zijn dus 52 verschillende letters. Veelal zijn bijvoorbeeld allen de eerste 8 tekens significant; de overige worden eenvoudig genegeerd. Geldige namen zijn bijvoorbeeld a grootst_element ditiseenheellangwoord 2.2 KEYWORDS Somminge namen zijn gereserveerd als keyword. Zij hebben een vaste betekenis en mogen niet op andere wijze worden gebruikt. Het zijn: auto, break, case, char, continue, default, do, double, else, entry, extern, float, for, goto, if, int, long, register, return, short, sizeof, static, struct, switch, typedef, union, unsigned, while. geleidelijk aan zullen we met al deze keywords vertrouwd raken. Aan deze lijst kunnen per C-implementatie nog andere keywords toegevoegd zijn. 2.3 CONSTANTEN Constanten vallen uiteen in verschillende categorieen, afhankelijk van hun type. 2.3.1 INTEGER CONSTANTEN Uit de volgende voorbeelden wordt duidelijk welke vorm integer constanten kunnen aannemen. 123 (decimaal) 0777 (octaal) 0XFF3A (hexadecimaal) 123L (decimaal, lang) Als een geheel getal begint met het cijfer 0 , dat direct door andere cijfers wordt gevoldt dan wordt het geacht octaal te zijn genoteerd; alleen de cijfers 0...7 mogen er dan in voorkomen. begint 0X of 0x ,dan staat het er in hexadecimale notatie. Voor 10...15 schrijven we A...F of a...f, Staat er aan het eind van de (decimaal , octaal of hexadecimaal genoteerde) constante een L of een l, dan wordt hij als 'lang' beschouwd: bij sommige machines wordt voor 123L meer ruimte gereserveerd dan voor 123. 2.3.2 KARAKTER CONSTANTEN Een karakter constante is een karakter tussen apostrofs , bijvoorbeeld 'x'. De waarde van een karakter is de numerieke waarde van de bitrij waardoor het karakter intern wordt voorge- steld. Bij veel C-implementaties is dit een waarde die te vinden is met de bekende ASCII tabel; daaruit blijkt bijvoorbeeld dat de waarde van 'A' gelijk is 65 , of, wanneer men het pariteitsbit gelijk aan 1 kiest, 65+128=193. De in Pascal gebruikelijke ord- functie blijft in C gewoon achterwege: hier is bijvoorbeeld 'A' + 1 correct en gelijk aan 66, resp.194 Enkele bijzondere karakters noteren we op een speciale manier en wel mbv. de backslash (\) als escape-symbol: notatie | betekenis . '\n' | NL (LF) newline, overgang naar begin van de volgende regel '\r' | CR carriage return, terug naar het begin van | dezelfde regel '\t' | HT horizontale tab '\b' | BS backspace '\f' | FF form feed '\\' | \ back slash '\'' | ' single quote '\ddd' | ddd bit patern In het laaste geval staat ddd voor ten hoogste drie octale cijfers. Zo noteren we dmv. '\73' het bit patroon 0...0111011, waarin het totale aantal bits gelijk is aan de woordlenget van de machine. Een bijzonder geval is '\0', het zogenaamde nulkarakter, bestaande uit een bitrij van alleen nullen (niet verwarren met '0', dat volgens de ASCII tabel als '\60' ofwel 48 kan worden genoteerd). 2.3.3 FLOATING GETALLEN Deze stellen getallen voor die net geheel behoeven te zijn en ook grotere waarden dan integers kunnen aannemen. Daar staat tegenover dat we er niet op kunnen rekenen dat zij exact kunnen worden gerepresenteerd: zij worden zo goed als mogelijk benaderd. In hun notatie komt op de gebruikelijke wijze een decimale punt of de letter e (of E) voor, bv: 123.45 .89 25. 36e-3 1.5e7 3.e+4 Alle floating constanten hebben dubbele precisie, zij zijn van het type double, dat nog behandeld zal worden. 2.3.4 STRINGS Een string is een rij tekens tussen aanhalingstekens, bijvoorbeeld: "Hoeveel getallen?" "a" Let op het verschil tussen karakter constante 'a' en de string "a". Als een aanhalingsteken binnen in een string moet voorkomen dan zetten we er een backslash voor dus \". De 'escape-sequences' zoals \n, die als karakter constanten mogen optreden, mogen ook binnen een string worden gebruikt, bijvoorbeeld: "Einde.\n" Het 'afdrukken' van \n houdt in dat naar het begin van een nieuwe regel wordt overgegaan. Met deze uitspraak geldt dat alles tussen de aanhalingstekens in nr=123; n=45; printf("Nummer %d\nAantal %d stuks\n", nr,n); inclusief de spaties, maar met uitzondering van de 'conversiespecificatie' %d, letterlijk wordt afgedrukt. De uitvoer is : Nummer 123 Aantal 45 stuks Hier staat \n niet alleen aan het eind, maar ook ongeveer in het midden van de string. Het gevolg daarvan is dat na het afdrukken van het getal 123 op een nieuwe regel wordt overgegaan. Het type van een string is 'array of characters'. Intern wordt in zo'n array van karakters het laatste karaktervan de string gevolgd door een nulkarakter, dat fungeert als afsluitcode. Dit gebeurt automatisch; we hoeven dus niet bijvoorbeeld niet te schrijven: "Einde.\n\0" We zullen op dit nulkarakter terugkomen in Ý 7.8.3 Een backslash (\) en een direct daarop volgende overgang naar een nieuwe regel worden samen genegeerd. Als gevolg hiervan is "Dit is een\ voorbeeld" gelijk aan "Dit is een voorbeeld" 2.4 COMMENTAAR In C heeft commentaar steeds de volgende gedaante: /* ........ */ 3 EENVOUDIGE EXPRESSIES EN STATEMENTS Om te kunnen beginnen met het schrijven van C-programma's hoeft slechts een klein gedeelte van de taal bekend te zijn. We beginnen met dit gedeelte ,waardoor de onderwerpen waarin C sterk afwijkt van de andere talen pas later aan de orde komen. 3.1 OPTELLEN, AFTREKKEN, VERMENIGVULDIGEN Optellen en aftrekken noteren we ,zoals gebruikelij, mbv de operatoren + en -. De vermenigvuldigingsoperator is *. Een voorbeeld van het gebruik van deze operatoren is: x=a-(b-c*d)+e; De delingsoperator is /. Daarbij moeten we er goed op letten ,dat het resultaat van het type integer is, dus geheeltallig, is als beide operanden van het type integer is, dus 17/3 levert 5 op. Is ten minste een van beide operanden float (of double), dan is het resultaat datgeen wat we verwachten: 17./3 is gelijk aan 5.666.... Bij geheeltallige deling verkrijgen we de rest mbv de operator %: 17 / 3 = 5 17 % 3 = 2 Als a en b integer zijn en a of b is negatief ,dan zijn a/b en a%b machine afhankelijk. De operatoren *, / en % hebben onderling dezelfde prioriteit. Deze is hoger dan die van + en -. Dit alles is in overeenstemming met wat in programmeertalen gebruikelijk is. We noemen vormen als: x=a-(b-c*d)+e 17/3 17 a expressies. 3.2 DECLAREREN EN ASSIGNEREN Een indentifier kan worden gedeclarred als variabele. Het doel hiervan is van het type te specificeren. Daarna kunnen alle waarden van dat type aan de variabel worden toegekend ('assigneerd'). Het volgende programma toont voorbeelden hiervan. main() { int i, j, k; float x, temp; char ch; i=38; j=35; k=i%j+1; /* nu is k gelijk aan 4 */ x=37.7; ch='?'; temp=i; /* conversie van int naar float */ j=x; /* j krijgt nu de waarde 37 */ } Iets als i=38; is een assignment-statement. Let op de puntkomma, die bij de assignment-statement behoort. Ook is i=j=k=0 toegestaan 3.3 RELATIONELE EN LOGISCHE OPREATOREN Om beslissingen en herhalingen te kunne formuleren moeten we de volgende operatoren kennen: C-notatie | betekenis . < | < kleiner dan > | > groter dan <= | ó kleiner dan of gelijk aan >= | ň groter dan of gelijk aan == | = is gelijk aan != | ongelijk aan (is niet gelijk aan) && | and, logisch EN || | or , logisch OF ! | not, logisch NIET In C wordt waar (ofwel TRUE) gecodeerd door 1 en niet waar (ofwel FALSE) door 0 Voorbeelden 2*2 == 3+1 is gelijk aan 1; 37 < 5 is gelijk aan 0. Belangrijk is dat een operanrand na && of || niet meet wordt berekend als de operand ervoor al uitsluitend over het resultaat geeft, immers 0 && ... heeft de waarde 0 (false) 1 || ... heeft de waarde 1 (true) Het volgende levert hiervoor niet het gevaar op dat er door 0 wordt gedeeld: if (n != 0 && q < k/n) ... (in Pascal vormt dit een vervelend probleem!) De nu bekende operatoren zijn hieronder weergegeven. De operatoren op ‚‚n regel hebben hebben onderling gelijke prioritijd. Deze is hoger dan die van de operatoren op de daarop volgende regel. ! * / % + - < <= > >= == != && || Let op het dubbele gelijk teken ==. Voor wie de taal Pascal kent, is het volgende verhelderend: C-notatie | Pascal-notatie = | := == | = != | <> 3.4 CONDITIONELE STATEMENTS Om te kunnen laten beslissen of een statement al dan niet moet worden uitgevoerd, kunnen we gebruik maken van een conditionele statement van de vorm: if (expressie) statement1 else statement2 waarvan het gedeelte else statement2 ook achterwege mag blijven. Let op de cverplichte haakjes en het ontbreken van then. Als de expressie de waarde 1 oplevert, dan wordt statement1 uitgevoerd; levert de expressie daarentegen de waarde 0 op, dan wordt niet statement1 ,maar wel statement2 ,als deze aanwexzig is, uitgevoerd Volledigheidhalve dient te worden vermeld dat iedere expressie- waarde ongelijk aan 0 hezelfde gevolg heeft als de waarde 1. Voorbeeld: if (x<0) y=-x; else y=x; Na de uitvoering hiervan is y gelijk aan de absolute waarde van x. Voorbeeld: if (x==y) {p=q+1; r=q-1;} Hier ontbreekt het gedeelte else statement2. Als statement1 is hier een zogenaamd compound-statement gebruikt, dwz. een statement in de vorm { .... } Deze wordt alleen uitgevoerd als x gelijk is aan y. Bij een compound-statement dienen de accoladen ervoor om van twee of meer statements ‚‚n statement te maken. Dit is hier nodig om aan te geven dat ook het uivoeren van r=q-1; afhankelijk is van de voorwaarde x==y gesteld moet worden. Een woord over plaatsing van de puntcomma's is hier op zijn plaats. Ander dan in Pascal, behoort de puntcomma in bijvoorbeeld r=q-1; tot de statement. Maar niet alle statements eindigen op een puntcomma! Een assignment statement eeindigt wel op een puntcomma, een compound statement niet. Vandaar dat een puntcomma direct gevolgd wordt door een sluitaccolade in: if (x==y) {p=q+1; r=q-1;} De assignment statement r=q-1; maakt hier deel uit van een compuond-statement, die op zijn beurt weer deel uitmaakt van een conditionele statement. 3.5 HERHALINGS STATEMENTS De while statement in C vertoont veel gelijkenis met die in Pascal maar let op de verplichte haakjes en het ontbreken van do: while (expressie) statement voorbeeld while (b != 0) {p=p+a; b=b-1;} Hier wordt gekeken of b ongelijk is aan 0. Is dit het geval, dan wordt de compound statement {p=p+a; b=b-1;} uitgevoerd en dan wordt daarna weer gekeken of b ongelijk is aan nul, enz. Zodra blijkt dat b gelijk is aan nul is geworden, is de uitvoering van de while statement voltooid. Ter introductie van de for statement bekijken we nog het volgende voorbeeld: i=1; while (i<=n) {s=s+i; i=i+1;} Dit geheel kan als volgt worden herschreven : for (i=1; i<=n; i=i+1) s=s+i; Meer in het algemeen kan expr1; while (expr2) {statement expr3;} Ons voorbeeld is een bijzonder geval hiervan, dat we verkrijgen als we kiezen: expr1: i=1 expr2: i<=n expr3: i=i+1 statement: s=s+i; In deze omschrijving vormen expr1, expr2 en expr3 elk een expressie. Een assignment statement bestaat uit een expressie, gevolgd door een puntcomma: i=i+1; is een assignment ststement; i=i+1 is een expressie; bijvoorbeeld na i=10; j=2*(i=i+1)+3; is gelijk aan 11 en j aan 25. We demonstreren de for statement aan de hand van een programma dat de daarna volende tabel produceert. Het programma luidt: main() { int i; printf(" x 1/x 1/(x*x)\n\n"); for (i=1; i<=10; i=i+1;) printf("%2d %8.4f %10.6f\n", i, 1.0/i, 1.0/(i*i)); } x 1/x 1/(x*x) 1 1.0000 1.000000 2 0.5000 0.250000 3 0.3333 0.111111 4 0.2500 0.062500 5 0.2000 0.040000 6 0.1667 0.027778 7 0.1429 0.020408 8 0.1250 0.015625 9 0.1111 0.012346 10 0.1000 0.010000 Nieuw zijn hier de conversie specificaties %2d. %8.4f en %10.6f. Om te beginnen moet %d worden vervangen door %f als geen integer maar een float waarde moet worden afgedrukt. Voor de laaste 2 getallen van de regel is dat hier het geval. maar bovendien willen we dikwijls iets over het formaat van de te af te drukken getallen opgeven. Deze gegevens staan tussen % en d resp. f; in het volgende stellen m en k getallen voor: %m.kf zorgt voor het afdrukken van een waarde van het type float (of double) ,in m posities, met k cijfers achter de decimale punt. %md zorgt voor het afdrukken van een int waarde ,rechts in het veld van m posities; links wordt aangevuld met spaties. Let erop dat in ons laatste voorbeeld tussen %2d en %8.4f een spatie staat. Deze komt in de uitvoer tussen de getallen te staan! We keren nu terug tot ons eigenlijke onderwerp,nl. herhalings- statements. Analoog aan de repeat statement in Pascal kent C ook een constructie voor een lus waarbij de test voor het be‰indigen aan het eind is geplaatst. Het is de do while constructie, die de volgende vorm heeft: do statement while (expressie); De hierin voorkomende statement wordt uitgevoerd. Als daarna de expressie iets ansers dan nul oplevert, wordt de statement nogmaals uitgevoerd, enzovoort. Het volgende voorbeeld toont het gebruik ervan. Nadat s gelijk aan nul is gemaakt ,wordt herhaaldelijk (tenminste ‚‚n keer) een geheel getal gelezen e opgeteld bij s. Zodra het ingelezen getal nul is , wordt (na de zinloze optelling van 0 bij s) gestopt: s=0; do { scanf("%d, &x); s=s+x; } while (x != 0); Na afloop hiervan is s dus gelijk aan de som van de ingelezen getallen. Let erop ,dat de puntcomma an het eind verplicht is. Tot slot van deze paragraaf volgt nu een programma fragment dat een fout bevat, waardoor er iets heel anders gebeurt dan de bedoeling is. Iemand wil bij het lezen van een rij gehele getallen de nullen aan het begin overslaan en het eerste getal ongelijk aan 0 toekennen aan de variabele i. Dus daarbij in de invoerrij 0 0 0 0 5 moet i gelijk worden aan 5. Hij schrijft daartoe: do scanf("%d",&i); while (i=0); Tot zijn verwondering heeft dit niet het gewenst effect. Na afloop hiervan is i niet gelijk aan 5 maar aan 0. Welke verbetering moet hierin aangebracht worden? aanwijzingen: a Ga zorgvuldig na hoe we in C testen of twee expressies aan elkaar gelijk zijn. b Wat betekent in C de expressie i=0 en welke waarde heeft deze expressie? Ga ook na wat het effect is als in het bovenstaande ,zowel in de invoer als in het programma, elke 0 wordt vervangen door een 1. 3.6 DE SWITCH-, DE BREAK- EN DE CONTINUE STATEMENT Bij een keuze uit ee wat groter aantal mogelijkheden kan de switch statement worden gebruikt. Een voorbeeld hiervan is: switch (letter) { case 'A': printf("Amsterdam\n"); break; case 'R;: printf("Rotterdam\n"); break; case 'H': printf("Hilversum\n"); break; default: printf("elders\n"); break; } Tussen de haakjes achter switch moet een expressie staan van het type int of van het type char. De waarde van deze epressie wordt vergeleken met elk van de constanten ,vermeld achter case. Die constanten moeten ook int dan wel char zijn; bovendien moeten zij aal verschillend zijn. De waarde die de genoemde expressie oplevert, wordt nu vergeleken met elk van deze constanten. Als gelijkheid wordt geconstateerd, wordt met de eerst volgende statement na die constante verder gegaan. Als de expressie een andere waarde heeft dan al die constanten, dan wordt verder gegaan met de statement achter default. Steeds is break nodig als men wil voorkomen dat de statements na een eventuele volgende case daarna ook worden uitgevoerd. In ons voorbeeld wordt bij letter = 'R' afgedrukt: Rotterdam maar bij afwezigheid van de break statement zou zou dat worden: Rotterdam Hilversum elders De case regels en default mogen in willekeurige volgorde staan; men mag dus bijvoorbeeld ook met default beginnen. Aan het eind, dus hier bij default ,is break overbodig, maar de dubbele punt is willekeurig, dus ook bijvoorbeeld switch(i) { case 123: j=1; k=2; l=3; break; case 456: case 789: m=4; break; { is mogelijk. We zien tevens dat default ook achterwege kan blijven. Is i=123 ,dan wordt j=1; k=2; l=3; uitgevoerd. Als i=456 of i=789, dan wordt m=4; uitgevoerd. Heeft i een andere waarde dan 123, 456 of 789, heeft de switch statement geen enkel effect. De break statement kan niet alleen in een switch, maar ook in een herhalings statement (for, while of do) worden gebruikt om de uitvoering hiervan af te breken en met de statement daarna verder te gaan. voorbeeld s=0; do { scanf("%d", &x); if (x==-1) break; s=s+x; } while (s<100); Hier worden gelezen gehele getallen bij elkaar opgeteld, waarbij het proces op twee rijen kan stoppen, nl. door het lezen van een -1 als afsluitcode of door het bereiken van een som die niet meer kleiner is dan 100. Er is ook nog een continue statement, die wat minder vaak wordt gebruikt. We kunnen deze alleen in herhalings statements toepassen Door continue wordt meteen de test op be‰indiging uitgevoerd, wat dus veel minder radicaal is dan wat er bij break gebeurt. Bij de volgende toepassing van de continue statement worden positive gehele getallen gelezen en gesommeerd. Negative getallen worden bij het sommeren overgeslagen; bij nul wordt gestopt. som=0; do { scanf("%d", &x); if (x>0) som=som+x; } while (x != 0); Het spreekt overigens vanzelf .dat het gestelde doel ook gewoon zo bereikt wordt: som=0; do { scanf("&d", &x); if (x>0) som=som+x; } while (x != 0); 3.7 DE STANDAARDFUNCTIES GETCHAR EN PUTCHAR Het komt bijzonder veel voor dat we in een programma telkens ‚‚n karakter willen lezen. De gebruiker van het programma typt dan deze karakters op het toetsenboard in. Hiervoor dient de standaardfunctie getchar. Deze heeft geen parameters; hij levert het ingetypte karakter als waarde af. We schrijven bijvoorbeeld: char ch: ... ; ch=getchar;(); (het type van getchar() is eigenlijk niet char ,maar int. De meest rechtseacht bits van deze integer vormen de binaire code van het gelezen karakter; deze waarde wordt aan ch toegekend. We komen hierop terug in 11.4) Ook het omgekeerde, dus het schrijven van een karakter op het beeldscherm van de terminal kan zo'n elementaire functie gebeuren en wel met de functie putchar. Deze heeft ‚‚n parameter, nl. het te schrijven karakter. We kunnen dus bijvoorbeeld schrijven: putchar(ch); Zoalsin de hoofstukken 10 en 11 duidelijk zal worden, vereist het gebruik van getchar en putchar dat bovenaan in het programma staat #include 4 MEER OPERATOREN 4.1 INCREMENT, DECREMENT, ASSIGNMENT In het voorgaande hebben we de assignment statements s=s+i; i=i+1; gebruikt. In de praktijk zullen we deze niet veel gebruiken, omdat C hiervoor kortere en meer effici‰nte constructies biedt. In plaats van de beide laatste statements kunnen we het volgende schrijven: s+=i; i++; of zelfs: s+=i++; In het nu volgende zullen deze vormen duidelijk worden. Statements als i=i+1; en j=j-1; komen zo vaak voor, dat hiervoor een verkorte notatie is, nl. i++; j--; is ingevoerd. We noemen ++ een increment- en -- een decrement operator. Zoals eerder vermeld, onstaat een expressie als we de puntcomma aan het eind van een assignment statement weglaten. Schrijven we haakjes om de aldus onstane expressie, dan kan het geheel weer als onderdeel van een andere expressie optreden. Dit is het geval met de expressie j=3 en i=i+1 in i=5; k=(j=3)+(i=i+1); Hierna is i=6, j=3 en k=9. Trachten we dit nl. i=6, j=3, maar k=8 ipv. 9. Dit komt omdat bij het gebruik van i++ het verhogen van i pas plaats vindt als de oude waarde, hier 5, gebruikt is. Men kan ipv. i++ ook ++i schrijven. In dat geval wordt i eerst verhoogd en pas daarna gebruikt. Dus i=5; k=(j=3)+(++i); is wel equivalent met de eerste versie: nu is het resultaat weer i=6, j=3 en k=9. De operatoren ++ en -- hebben een zeer hoge prioriteit, hoger zelfs dan *, / en %. Dit houdt dat --a*b correct is en gelezen moet worden als (--a)*b zodat a eerst met 1 wordt verlaagd en daarna met b wordt vermenigvuldigd. De operatoren ++ en -- zijn zogenaamde unaire operatoren, dwz. dat er maar ‚‚n operand bij hoort, evenals dat het geval is met minteken in x = -b+D; Er is in C geen unair plusteken, dus ipv. x=5 is x = +5 niet toegestaan, terwijl x = -5 wel correct is. Ook de operator "!" met de betekenis "not" is een unaire operator. De meeste operatoren, zoals bijvoorbeeld + ,zijn binair: er behoren twee operanden bij. We bespreken nu een aantal nieuwe operatoren, waarbij een rekenbewerking gecombineerd wordt met de assignment. Zij corrosponderen met reeds bekende binaire operatoren: bekende | bijbehorende operator | assignment operator + | += - | -= * | *= / | /= % | %= In principe is ook van toepassing op de binaire operatoren << >> & ^ | die we nog zullen bespreken. We kunnen bijvoorbeeld de assignment statement x = x*3; herschrijven als a *= 3; Dit lijkt geen groot voordeel ,maar het nut wordt duidelijk als we iets ingewikkelds voor x in de plaats stellen. In het volgende hoofdstuk worden arrays besproken. We kunnen dan schrijven: a[(i-1)*n+j] *= 3; wat gemakkelijker schrijft en vermoedelijk sneller wordt uitvoerd dan: a[(i-1)*n+j] = a[(i-1)*n+j] * 3; Afgezien van de nog te bespreken komma operator, hebben we de assignment de laagste prioriteit. Dus i -= j + k; betekent i -= (j+k); ofwel i = i - (j+k); 4.2 OPERATOREN VOOR BITMANIPULATIE Op operanden van een wiilekeurig elementair type, maar niet float of double, kunnen de volgende operatoren voor bitmanipulatie worden toegepast: & logische EN (and), bitgewijs | logische OF (or), bitgewijs ^ logische OF (exor),bitgewijs << schuiven naar links >> schuiven naar rechts ~ 1-complement (unaire operator) Dus bijvoorbeeld: 23 & 26 heeft de waarde 18, hetgeen als volgt is in te zien voor wie bekend is met de binaire schrijfwijze van getallen: 0...010111 = 23 0...011010 = 26 & 0...010010 = 18 Zoals dit voorbeeld laat zien ,leveren bij & twee bits in dezelfde positie een 1 op als zij beide 1 zijn en anders een 0. Analoog hieraan wordt bij | een nul opgeleverd als de bits beide 0 zijn, zoniet, dan is het resultaat een 1. Als i van het type int is, dan kan een vermenigvuldiging met 2n zeer effici‰nt worden gerealiseerd dmv. i << n mits uiteraard het resultaat niet te groot wordt. Moet het resultaat ook weer worden toegekend, dan kan weer een assignment operator worden gebruikt: i <<= n Door ~ worden van alle enen nullen gemaakt en omgekeerd, dus ivm. de volgende binaire notatie van negative getallen volgens 2- complement 0...01 = 1 1...10 = -2 is ~1 gelijk aan -2 en omgekeerd ~-2 gelijk aan 1. 4.3 CONDITIONELE EXPRESSIES We kunnen in C het effect van de conditionele statement if (a0) s+=n; De lus bestaat hier uit drie acties, nl.: 1 het lezen van een getal: scanf("%d", &n), 2 de test: n>0 3 het verhogen van s met n: s+=n Het bijzondere is dat de test hier noch aan het begin, noch aan het eind van de lus, maar in het midden wordt uitgevoerd. Minder mooi, kan dit overigens ook zo: s=0; do { scanf("%d", &n); if (n<=0) break; s+=n; } while (1); Omdat 1 de betekenis 'TRUE' heeft ,wordt hier de lus nooit be‰indigd door wat achter while staat, maar uitsluitend door if...break. Ook minder handig is de de volgende oplossing met twee aanroepen van scanf: s=0; scanf("%d", &n); while (n>0) { s+=n; scanf("%d", &n); } In een taal als Pascal is een constructie die hiermee overeenkomt heel gebruikelijk. Dank zij de comma operator kunen we in C de eerste van de drie getoonde oplossingen kiezen. Van de volgende toepassingen van de comma operator is het minder evident, maar het is goed. dit soort constructies eens gezien te hebben: x =10*(a=2. b=3, a*b)+1; /* Nu is a=2, b=3 en x=61 */ printf("%d", (k=10, l=20, m=30, n=k+l+m)); /* Drukt 60 af */ 4.5 ASSOCIATIVITEIT Schrijven we a - b - c dan dient dit opgevat te worden als (a - b) - c niet als a - (b - c) We zeggen daarom dat de binaire operator "-" van links naar rechts associeert. Dit geldt niet alleen voor deze operator, maar ook voor de meeste andere binaire operatoren. Alleen de volgende operatoren gedragen zich anders, dwz. zij associ‰ren van rechts naar links: - alle unaire operatoren ,waaronder ! ~ ++ -- - de operator ?: voor de conditionele expressie - de assignment operatoren = += -= *= etc. Dat ?: van rechts naar links associeert houdt in dat a . ! ~ ++ -- - (type) * & sizeof (alle unair) * / % + - << >> < <= > >= == != & ^ | && || ?: = += -= *= /= %= <<= >>= &= ^= |= , De betekenis van de operatoren ,voor zover besproken ,is hieronder nog eens kort weergegeven: ! logische ontkenning (not), unaire operator ~ 1 complement, bitsgewijs, unaire operator ++ increment, verhogen met 1, unaire operator -- decrement, verlagen met 1, unaire operator - minus zowel unair als binair + plus * maal / gedeeld door % rest bij geheeltallige deling << schuiven naar links >> schuiven naar rechts < kleiner dan > groter dan <= kleiner dan of gelijk aan >= groter dan of gelijk aan == gelijk aan != ongelijk aan & bitsgewijs EN | bitsgewijs OF ^ bitsgewijs exclusief OF && logisch EN || logisch OF ?: conditionele expressie = assignment (toekenning) += plus operator met assignment; analoog met -=, *=, etc. , comma operator. 5 ARRAYS EN POINTERS 5.1 RIJEN Door de array declaratie int a[100]; ontstaat de mogelijkheid gebruik te maken van de 100 variabelen a[0], a[1], ... , a[99] Let erop dat het getal 100 in de declaratie voorkomt ,maar dat het hoogste rangnummer kleiner, nl. 99 is. Ipv. 100 kan uiteraard een ander positief geheel getal geneomen worden. vaak is het mooier een constante als 100 een naam te geven en ver concequent deze naam te gebruiken. Dit kan als volgt: #define N 100 ... int a[N]; Overal waar nu in het programma de 'constante' N wordt gebruikt, wordt in werkelijkheid 100 genomen. In hoofdstuk 10 komen we er op het gebruik van #define terug. 5.2 POINTERS EN ARRAYS In hoofdstuk 1 zijn, ivm. scanf, pointers te sprake gekomen. Er werd daar gebruik gemaakt van de poiters &a en &b. Hun waarden zijn de adressen van de variabelen a en b. Er is naast & nog een unaire operator die veel met poiters te maken heeft,nl. *. De betekenis van * is juist tegengesteld aan die van &. Als p een pointer is naar de variabele v, dan geldt: p = &v v = *p Dus *p is het object waar pointer p naar wijst. Om dit nog beter duidelijk te maken, stellen we ons de volgende situatie in het geheugen voor: adres | inhoud ---------+---------- 2000 | 3000 = p (ofwel &v) ... | ... 3000 | 123 = v (ofwel *p) Hier wijst inhoud 3000 naar adres 3000, en dit wijzen heet pointer. De getallen 2000 en 3000 zijn slechts voorbeelden van mogelijke adressen. Afgezien van deze getalwaarden, kan de geschetste situatie worden bereikt door : int *p, v; v=123; pp=&v; Door int *p, ... wordt uitgedrukt dat *p, dus het object waar p naar wijst, integer is. Daaruit volgt dan dat p een pointer naar een integer is. na dit programma fragment is het getal 123 niet alleen de waarde van v ,maar ook van *p. We kunnen nu v ook bereiken via p. Laten we bijvoorbeeld volgen: *p=987; printf("%d", v); dan wordt niet 123, maar 987 afgedrukt. We bezien nu het nauwe verband dat er is tussen pointers en arrays. In C is een array-naam als a steeds een pointer naar het eerste element a[0]. Dit houdt in dat na int a[100]; een pointer naar a[0] op twee manieren kan worden genoteerd, nl. als &a[0], maar ook gewoon als a. Anders gezegd, de arraynaam a en &a[0] zijn twee verschillende notaties voor dezelfde pointer. Doordat a een pointer naar a[0] is, kunnen we a[0] ook noteren als *a. Bij een pointer als a kunnen we iets optellen. In het algemeen is a* een andere notatie voor a[0], *(a+1) een andere notatie voor a[1], *(a+2) een andere notatie voor a[2], enzovoorts. De equivalentie blijft van kracht als per array-element meer dan 1 geheugenplaats in beslag wordt genomen, dus bijvoorbeeld als de elemeneten van het type double zijn. 5.3 POINTERS EN PARAMETEROVERDRACHT In hoodfstuk 7 komen functies uitvoering aan de orde. De paragraaf loopt daarop vooruit. De bestudering ervan kan eventueel worden uitgesteld totdat 7.2 bestudeerd is. In C hebben functies alleen value-parameters. Toch kunnen we een functie via een parameter een waarde laten afleveren. Dit is mogelijk en zelfs eenvoudig, dankzij hetgeen zojuist over pointers is behandeld. We bekijken het volgende programma: main() { int v; f(&v); printf("%d\n", v); } f(p) int *p; { *p=123; } Dit programma drukt 123 af. De formele parameter p van functie f is een pointer naar een integer variabele. Dit geven we aan door deze parameter te declareren als int *p; Let er ook op dat de declaratie van parameters geplaatst wordt v˘˘r de eerste accolade van de functie. Door *p=123; wordt aan de variabele waar p naar wijst (dus niet aan p!) de waarde 123 toegekend. In de aanroep f(&v) is het argument &v een pointer naar de variabele v. Niet de waarde van v maar (de waarde van) het adres van v treedt dus op als argument. Hieruit volgt dat de waarde 123 aan de variabele v wordt toegekend. Nog eenvoudiger is het als het argument van een functie een array is. De naam van een array zelf is al een pointer, dus de pointer operator & wordt hier niet eens gebruikt. Zo heeft het programma main() { int a[3]; g[a]; printf("%d %d %d\n", a[0], a[1], a[2];)} g(b) int b[ ]; { b[0]=b[1]=b[2]=5; } de uitvoer : 5 5 5 Merk op dat in de declartie int b[ ]; van de formele parameter b de arrygrootte niet vermeld hoeft te worden. De reden is, dat hier geen ruimte voor b wordt gereserveerd. In feite gebeurt in de aanroep g[a] hetzelfde wat in bijvoorbeeld in de taal Fortran gebruikelijk is: het adres van het eerste arry-element wordt aan de functie als argument meegegeven. Maar door dat a volgens de afspraak een pointer naar a[0] is, is dit formeel gesproken value-parameteroverdracht. 5.4 MEERDIMENSIONALE ARRAYS Een tabel of matrix kan worden opgevat als een array ,waarin de elementen weer een array zijn. De notatie voor gebruik van zo'n tabel element is hiermee overeenstemming. We schrijven nl. tabel[i][j] als we het element in de i-de rij en de j-de kolom bedoelen (waarbij vanaf nul wordt geteld!). We spreken in dit geval van eeen tweedimensionaal array. Moet de tabel bijvoorbeeld 20 rijen en 5 kolommen hebben ,en moeten de waarden van het type float zijn, dan declareren we: float tabel[20][5]; Als parameter van een functie moet nu in elk geval de tweede afmeting, dus 5 in dit voorbeeld wel in de parameterdeclaratie worden vermeld, dus h(a) float a[ ][5]; { ... } of voluit h(a) float a[20][5]; { ... } De elementen van de matrix worden rij voor rij opgeslagen. De positie van element a[i][j] komt dan in ons voorbeeld overeen met die van array element b[5*i+j], als we ons voorstellen dat een fictief een-dimensionaal array b, eveneens met float elementen, diezelfde geheugenruimte in beslag zou nemen als array a. Hierdoor wordt het begrijpelijk dat de machine niet de eerste afmeting 20, maarwel de tweede afmeting 5 nodig heeft om binnen de functie h de positie van element a[i][j] te bepalen. 5.5 EEN VOORBEELD: HET ZOEKEN IN EEN ARRAY Om het gebruik van arrays en pointers te illustreren, behandelen we de volgende opgave. Schrijf een programma dat een rij van ten hoogste 20 positive gehele getallen leest, die onderling alle verschillend zijn. Op deze rij volgt een 0 als afsluitcode. Daarna wordt nog een geheel getal gelezen.Als dit getal in de eerder gelezen rij voorkomt, moet afgedrukt worden op welke positie dat getal staat. Komt het daar niet voor dan dient dat gemeld te worden. De invoer 4 9 6 0 9 levert: aanwezig op plaats 2; de invoer 4 9 6 0 8 levert: niet aanwezig. We geven twee versies van de oplossing. De eerste luidt: main() { int a[21], x, n; /* liniair zoeken */ n=0; printf("Geef max. 20 pos. gehele getallen, gevolgd door 0\n"); while (scanf("%d", &x), x>0) a[++n]=x; printf("Geef nog een getal\n"); scanf("%d", &x); a[0]=x; while (a[n] !=x) n--; if (n==0) printf("niet aanwezig\n"); else printf("aanwezig op de plaats: %d\n", n); } Let op de comma operator tussen de aanroep van scanf en x>0. Het op te zoeken getal wordt als een zogenaamde schildwacht (sentinel) op positie 0 gezet, waardoor binnen de lus maar ŠŠn test uitgevoerd hoeft te worden. Het programma is vrij conventioneel, in die zin, dat meer arrays dan met pointers wordt gewerkt. De volgende versie is meer een typisch C-programma. Er wordt met pointers gewerkt op een wijze die sterk afwijkt van wat in andere hogere talen gebruikelijk is. De werkwijze doet eerder denken aan de machientaal van sommige computers, waarbij het effectieve adres in een indexregister staat en telken met 1 wordty verhoogt of verlaagd. main() { int a[21], x, n; /* liniair zoeken */ n=0; printf("Geef max. 20 pos. gehele getallen, gevolgd door 0\n"); while (scanf("%d", &x), x>0) *(++p)=x; printf("Geef nog een getal\n"); scanf("%d", &x); *a=x; while (*p !=x) p--; if (p==a) printf("niet aanwezig\n"); else printf("aanwezig op de plaats: %d\n", p-a); } Als we deze versies aandachtig vergelijken, dan ontdekken we een bijzondere sterke analogie. Omdat unaire operatoren van rechts naar links associ‰ren, hadden we op de vijfde regel ook *++p ipv. *(++p) mogen schrijven. De (hier niet gebruikte) vorm *p++ betekent hetzelfde als *(p++). 5.6 ARRAYS VAN POINTERS Zoals in 8.3 zal blijken zijn er nuttige aan arrays waarvan de elementen pointers zijn. Om de notatie hiervan duidelijk te maken bekeijken het volgende programma: main() { int a[4], *p[4], i; for (i=0; i<4; i++ { a[i]=10*i; p[i]=&a[i]; /* of : p[i]=a+i; */ printf("%3d%3d",*p[i],**(p+i)); } printf("\n"); } De uitvoer ervan is: 0 0 10 10 20 20 30 30 Hier is p[i], ofwel *(p+i), een pointer naar a[i]. Om de vormen &a[i] *p[i] goed te kunnen lezen ,moeten we letten op het prioriteiten overzicht in 4.6. We zien daar { } hogere prioriteit heeft dan & en *. Bovenstaande vormen moeten daarom worden ge‹nterprtreed als &(a[i]), resp. *(p[i]) . 5.7 HET BEGRIP LVALUE Het linker lid van assignment kan ingewikkelder zijn dan een simpele variabele. Datgene wat in ... = .....; links van het gelijkteken mag staan, wordt aangeduid door de term Lvalue. We bespreken het begrip lvalue hier ,omdat we ervan op de hoogte moeten zijn dat de naam van een array geen lvalue is. Dit betekent dat na int a[4],b[4],*p,i; de vorm a=b; geen correcte assignment statement is. Een heel redelijke foutmelding van de C compiler is in dit geval: "The left operand to this assignment expression is not an lvalue" De unaire operator & maag alleen valk voor lvalue worden geplaatst, zoals bijvoorbeeld in &i daarentegen is bij de unaire operator * juist het resultaat een lvalue, zodat een expressie als *(p+i) het linkerlid van een assignment statement kan zijn. 6 TYPES EN CONVERSIE 6.1 NOG ENIGE ELEMENTAIRE TYPES De taal C kent de volgende elementaire types: char int long int (of kortweg: long) short int (of kortweg: short) unsigned int (of kortweg: unsigned) float double De aanduiding long en short hebben betrekking op de interne lengte van een gehhel getal, dwz het aantal bits dat ervoor beschikbaar is. Die lengte is machine afhankelijk. Het kan ook zijn dat het voor een zekere machine niets uitmaakt of we long dan wel short aan int laten voorafgaan. Wel mogen we uitgaan van het volgende: lengte (short) ó lengte (int) ó lengte (long) Zowel float als double betreffende interne drijvende komma (floating point) voorstelling van getallen. Hiermee kunnen ook niet gehele getallen, zij het met eindige precisie, intern worden gerepresenteerd. Bij double is die precisie meestal twee keer zo groot als bij float en double zeer veel groter dan voor long int. Ook deze waarden zijn machine afhankelijk, zodat we er weinig concreets over kunnen zeggen. 6.2 TYPE CONVERSIE Als operanden van verschillend type in een expressie voorkomen, dan wordt naar een gemeenschappelijk type geconverteerd. Dit gebeurt in de regel zoals we het ook verwachten, zodat we niet voor ongename verrassingen komen te staan. We bekijken enkele voorbeelden en gaan er daarbij steeds vanuit dat er gedeclareerd is int i; float f; char ch; long l; double d; Komt er nu verderop in het programma f+i voor ,dan is dat correct: voordat de optelling wordt uitgevoerd, wordt i naar float geconverteerd. Ook worden operanden van het type char naar het type int geconverteerd, zoals bijvoorbeeld in ch+i Als we uitgaan van de ASCII code ,dan staat 'A' op dezelfde wijze intern gecodeerd als het getal 65. Dan is 'A'+1 van het type int gelijk aan 66. Bij toekenningsoperatoren is uiteraard het linkerlid bepalend voor het type van het resultaat. Dit is het geval in de volgende situaties: i=f i+=f In deze gevallen wordt niet afgerond, maar afgehakt, dus door i=5.9 wordt i gelijk aan 5. In deze minder fraaie assignment (nog net) toegestaan, men is gelukkig nog niet zo ver gegaan, een float waarde als subscript van een array toe te laten. De vorm a[f] is dus fout. Zoals we gezien hebben, wordt in gemengde expressies char geconverteerd naar int en evenzo int naar float. Dit houdt in dat in twee stappen ook de conversies van char naar float mogelijk is. De vorm 3.14+'A' levert volgens de ASCII code de volgende waarde op: 3.14 + 65 = 68.14 Analoge 'meertrapscoversies' kunnen ontstaan uit de conversies: van short naar int, van int naar long, van float naar double, van int naar unsigned. De conversie van long naar int zoals in i=l; is anders van karakter (eveneens die van float naar int) omdat hierbij de waarde wezenlijk kan veranderen. Als de getal waarde van l te groot is voor i, dan gebeurt het volgende. Als de lengten van i en l respectivelijk 16 en 32 zijn, dan worden eenvoudig de 16 rechterbits van l gecopieerd en aan i toegekend. Er volgt dus geen foutmelding; we moeten er zelf voor zorgen dat hierbij geen ongewenste effecten optreden. Een soortgelijk geval is de conversie van int naar short en die van int naar char. Minder gevaarlijk is de overgang van double naar float door f=d; Nu wordt nl. netjes afgerond; wel wordt de precisie geringer, maar afgezien daarvan blijft de waarde onverandert. 6.3 DE CAST OPERATOR Naast impliciete type coîversie, behandeld in 6.2, bestaat ook de mogelijkheid expliciet een conversie voor te schrijven. We vermelden daartoe het gewenste type tussen haakjes; dit geheel vormt een unaire operator, cast operator geheten. voorbeeld: int i; i=12; printf("%7.3f\n", (float)i); Hier is i van het type integer, maar (float)i van het type float. Er wordt afgedrukt 12.000, met een spatie voor de 12. Ivm %7.3f zou alleen i ipv. (float)i hier niet goed geweest zijn. Als gedeclareerd is int *pint; dan is pint een pointer naar een integer. Zouden we de waarde van pint willen gebruiken in een context waar 'pointer to char' vereist is, dan kan dat door daar ipv pint te schrijven. (char *)pint Veel C compilers zullen niet protesteren als in zo'n geval de cast achterwege blijft, maar het is netter als we toch deze operator gebruiken. 7 FUNCTIES EN DE PROGRAMMA STRUCTUUR In sommige talen wordt onderscheid gemaakt tussen procedures (of subroutines) en functies, tussen diverse soorten parameter overdracht en tussen interne en externe routines. In C is dat allemaal eenvoudiger. Er zijn slechts functies, er is alleen value parameter overdracht en de functies zijn altijd extern: een functie kan niet worden gedefinieerd binnen een andere functie. Merkwaardig is dat deze eenvoud ons niet of nauwelijks beperkt in onze mogelijkheden of op andere wijze als tekortkoming gevoeld wordt. 7.1 EEN EENVOUDIGE RESURSIEVE FUNCTIE De algorithme van Euclides om de grootste gemene deler van twee natuurlijke getallen te vinden berust op de volgende recursieve betrekking: a als b nul is ggd(a, b) = { ggd(b, r) als b niet nul is waarin r de rest is bij de geheeltallige deling van a door b. Zo is volgens deze regel bijvoorbeeld ggd(24, 60) = ggd(60, 24) = ggd(24, 12) = ggd(12, 0) = 12 en inderdaad het grootste getal dat deelbaar is op 24 en 60. In C kan deze functie als volgt worden genoteerd: int ggd(a, b) int a, b; { if (b==0) return a; else reurn ggd(b, a%b); } of mbv de conditionele expressie int ggd(a, b) int a, b; { return b==0 ? a : ggd(b, a%b); } Als we deze functie willen demonstreren, dan moeten we er een hoofdprogramma aan toevoegen. main() { printf("%d %d\n",24, 60, ggd(24, 60)); } De functie ggd en de functie main kunnen, in willekeurige volgorde in ‚‚n file staan. Deze file kan aan de compiler ter verwerking worden aangeboden, waarna aan de machine opdracht kan worden gegeven het programma te gaan uitvoeren. Welke commando's hier precies voor beschikbaar zijn is in de regel te vinden in de documentatie die bij de gebruikte computer hoort. Zij zijn niet overal gelijk en zij behoren niet tot de taal C, zodat wij er niet verder op in gaan. De functie ggd laat zien dat dmv de return statement een waarde door de functie kan worden afgelevert. Tevens wordt door de return statement van de functie ggd teruggekeerd naar de functie die ggd aanriep. Overigens gebeurt dit laatste ook automatisch als de laatste statement van de functie is uitgevoerd. De algemene gedaante van de return statement is return expressie; of als er geen waarde moet worden afgeleverd , return; Een return statement eindigt dus altijd op een puntcomma. We zien verder aan de fuctie ggd dat een functie niet alleen andere functies, maar ook zichzelf kan aanroepen: recursie is dus toegestaan. Een functie kan ook een andere functie die pas later in de programma tekst voorkomt, aanroepen. Functies kunnen nul, een of meerdere parameters ofwel argumeneten hebben. Heeft een functie geen argumenten, dan zijn toch de haakjes verplicht, zoals we kunnen zien aan de functie main. 7.2 HET TYPE VAN ARGUMENTEN EN FUNCTIE WAARDE Bij de besproken functie ggd is het type van zowel argumenten als functiewaarde integer. Het type integer hoeft voor argumenten en functiewaarde niet expliciet opgegeven te worden, maar het is wel aan te bevelen. In int ggd(a, b) int a, b; { return b==0 ? a : ggd(b, a%b); } staat op de eerste regel twee keer int; de eerste int betreft de functiewaarde, de tweede de argumenten. Let erop dat de argumenten aleen gedeclareerd kunnen worden v˘˘r de eerste open-accolade van de functie. Is het type van een argument of van de fuctiewaarde iets anders dan int, dan is het vermelden van dat type verplicht. Is het niet vermeld, dam wordt int verondersteld. Eventuele lokale variabelen van de functie mogen pas na de open-accolade van de functie worden gedeclareerd. We demonstreren dit aan de hand van een ander rekenkundig voorbeeld, nl. de berekening van n-faculteit n! = 1 * 2 * 3 * .... * n waarbij we met opzet een niet recursieve functie kiezen. Ook nu zullen we het type van functiewaarde en argument vermelden: int fac(n) int n; { int i, f; f=1; for (i=2; i<=n; i++) f*=i; return f; } Het is overigens ook toegestaan een formele parameter als lokale variabele te gebruiken. De volgende versie laat dit zien: int fac(n) int n; { int f; f=1; while (n>1) f*=n--; return f; } De sattement f*=n--; houdt in dat f wordt vermenigvuldigd met de oude waarde van n, waarna n met 1 wordt verlaagd; meer conventioneel zouden we dus ook kunnen schrijven: while (n>1) { f=f*n; n=n-1: } We bespreken nu ook een functie waarbij andere types dan integer betrokken zijn. De functie zelf is weinig interesant, maar de essentie van wat we willen bespreken wordt er goed door gedemonstreerd. De hoofdregel is dat het type van de af te leveren functiewaarde bekend moet zijn voordat de functie gebruikt wordt. Is dat type op dat moment nog niet gegeven ,dan wordt het type integer aangenomen. We kiezen de functie invert, die bij het argument x (ongelijk aan nul) de waarde 1/x oplevert. Het gaat daarbij niet alleen om de functie zelf ,maar vooral ook om de plaats waar hij wordt aangeroepen. We kunnen schrijven: double invert(); main() { printf("%f\n", invert(3.0)); } double invert(x) double x; { return 1/x; } Dit is een compleet programma. de uitvoer ervan is: 0.333333 Op de eerste regel wordt alleen medegedeeld wat het type van de functiewaarde is: dat type is nu bekend voordat de aanroep invert(3.0) plaatsvindt. Mooier is het volgende: main() { double invert(); printf("%f\n", invert (3.0)); } double invert(x) double x; { return 1/x; } We zien dus de declaratie double invert(); vooraf mag gaan aan de aanroepende functie, maar ook daarbinnen geplaatst mag zijn. Overigens kan een functie-aanroep waarbij een waarde wordt afgeleverd ook, met een puntcomma erachter, als zelfstandige statement optreden. Uiteraard heeft dat aleen zin, als er neveneffecten zijn, zoals in: int lees(p) int *p; { scanf("%d", p); return *p>0; } We kunnen nu de functie lees op twee manieren aanroepen, nl. met de controle op het positief zijn van het gelezen getal: if (lees(&i)) { /* De gelezen waarde van i is positief */ ..... } else { /* De gelezen waarde van i is niet positief */ ... } of zonder die controle: lees(&i); De functie lees levert goed beschouwd twee waarden af, nl. een 1 of een 0 als functie waarde en het gelezen getal via de parameter. De hier toegepaste wijze van parameteroverdracht is ook besproken in 5.3. Tot slot moet nog de nadruk gelegd worden op de eis dat een argument van hetzelfde type is als de formele parameter die correspondeert. Zo zal de volgende functie: schrijf(x) float x; { printf("%f", x); } door de aanroep schrijf(123); beslist niet iets afdrukken wat lijkt op het getal 123. Wel gaat het goed bij de aanroep: schrijf(123.0); 7.3 EEN FUNCTIE VOOR DE MACHTSVERHEFFING Er volgt nu een functie die op effici‰nte wijze de macht an berekent, waarin n niet negatief en geheel is. Zowel het grondtal a als het resultaat an zal van het type double zijn. Er wordt zoveel mogelijk gekwadrateerd. Ook met de hand zouden we zo rekenen. Bv. a50 berekenen we met de hand niet door 49 vermenigvuldigingen uit te voeren, maar volgens a50 = a32 x a16 x a2 Hierin wordt a16 berekend als [{(a2)2}2]2 en a32 als (a16)2. Al met al zijn er zodoende niet 49, maar slechts 7 vermenigvuldi- gingen nodig. Meer algemeen geldt: | 1 als n=0 is, an = { (a2)«n als n>0 en even is, | a.an-1 als n>0 en oneven is. In C kunnen we schrijven double power(a, n) double a; int n; /* n>= 0 */ { return n==0 ? 1.0 : n%2==0 ? power(a*a, n/2) : a*power(a, n-1); } Een hoofdprogramma dat deze functie gebruikt zou kunnen luiden: main() /* berekening van 10 toe de macht 20 */ { double power(); printf("10**20 = %f\n", power(10.0, 20)); } Omdat de functie power een waarde aflevert die niet van het type integer is en omdat we de mogelijkheid willen hebben het hoofdprogramma eraan vooraf te laten gaan, is power apart binnen het hoofdprogramma gedeclareerd. We hebben in 7.2 deze kwestie uitvoerig besproken aan de hand van de functie invert. 7.4 EXTERNE VARIABELEN De tot nu toe gebruikte variabelen werden binnen een functie gedeclareerd. De geheugenruimte die ervoor nodig is, wordt 'automatisch' gereserveerd als de de functie wordt aangeroepen. Die ruimte wordt ook weer automatisch vrijgegeven als de uitvoering van de functie wordt be‰indigd. Men noemt zo'n variabele daarom automatisch (automatic). Zijn betekenis strekt slechts uit over de functie waarin hij is gedeclareerd. In tegenstelling tot autmatische variabelen zijn er ook externe variabelen. Deze worden niet binnen een functie gedeclareerd, maar daarbuiten. Zij kunnen in meer dan ‚‚n functie worden gebruikt. Het volgende voorbeeld laat dit zien. int i; /* definitie van i */ main() { f(); printf("%d\n", i); } f() { i=123; } Dit is een programma in ‚‚n file. Het bestaat uit de definitie van de externe variable i en de functies main en f. die beide toegang hebben tot de variabele i. Bij externe variabelen moeten we onderscheid maken tussen hun definitie en hun declaratie. Door de definitie worden niet alleen eigenschapeen van de variabele vastgelegd, maar er wordt tevens ruimte voor gereserveerd. Een externe variabele mag daarom maar ‚‚n definitie hebben. Een declaratie van een externe variabele daarentegen vermeldt aleen de eigenschappen van die variabel; er wordt geen ruimte voor gereserveerd. Men zou zo kunnen formuleren: definitie = declaratie + ruimtereservering In ons voorbeeld doet de definitie van i tevens dienst als declaratie. Dit is hier mogelijk, omdat deze in dezelfde file voorkomt als de functies main en f die er gebruik van maken en omdat de definitie aan die functies voorafgaat. Als functies in verschillende files opgenomen zijn en toch dezelfde externe variabele gebruiken, dan moet die variabele expliciet worden gedeclareerd in die files waar hij niet is gedefinieerd en toch wordt gebruikt: file 1: int i; /* definitie */ main() { f(); printf("&d\n", i); } file 2: extern int; /* declaratie */ f() { i=123; } Een compiler die file 2 te verwerken krijgt, zonder op dat moment te beschikken over file 1, moet bij de vertaling van i-123 weten wat het type van i is. Anderzijds mag in file 2 geen ruimte voor i worden gereserveerd, want dat is al in file 1 gebeurd. Als de externe variabele arrays zijn ,dan moeten de grenzen bij de definitie worden opgegeven. Bij declaratie hoeft dat niet, dus bv: int a[1000]; /* definitie (tevens declaratie) */ maar: extern int a[]; /* declaratie */ De scope van een naam is het deel van het programma waar de naam bekend is. De scope van een externe variabel strekt zich per file uit vanaf het punt waar hij is gedeclareerd tot eind van die file. De aldus bepaalde file gedeelten vormen tezamen de scope. Samenvatting - Externe variabelen worden gedefinieerd op het buitenste niveau , dus niet binnen functies. - Een externe variabele wordt slechts ‚‚n keer gedefinieerd. Deze definitie geldt tevens als declaratie. Een declaratie die niet tevens definitie is, is te herkennen aan het keyword extern. - Een externe variabele kan in een file worden gebruikt na het punt waar hij in die file is gedeclareerd. 7.5 STATISCHE VARIABELEN Er zijn ook statische variabelen; men herkent hun declaratie aan het keyword static. Statische variabelen kunnen intern of extern zijn. Interne statische variabelen zijn ,net als automatische variabelen ,slechts binnen een functie bekend. Het verschil is evenwel, dat voor automatische variabelen geen statische variabe- len geen permanente geheugenruimte wordt gereserveerd en voor statische variabelen wel. Bij het be‰indigen van de uitvoering van een functie gaat de waarde van een statische variabele dan ook niet verloren. Wordt de functie even later weer opgeroepen ,dan heeft de statische variabele nog steeds dezelfde waarde. In het volgende voorbeeld is s een interne statische variabele. main() { int i; for (i=1; i<=5; i++) printf("%3d %3d\n", i, f(i)); } int f(i) int i; { static int s=100; return s+=1; } Dit programma druk af: 1 101 2 103 3 106 4 110 5 115 De statische variabele s krijgt een vaste geheugenplaats toegewe- zen. De waarde 100 wordt alleen aan het begin in deze geheugen- plaats gezet. We zien hier een voorbeeld van het inialiseren van een variabele, een onderwerp dat in 7.8 uivoerig zal worden besproken. Het initialiseren vindt plaats op het momet dat ruimte wordt gereserveerd. Voor een statische variabele gebeurt dat alleen aan het begin, voordat de feitelijke uitvoering van het programma begint. Hier krijgt s dus aleen aan het begin van het programma de waarde 100. Daarna wordt s telkens met i verhoogd en de daarbij onstane waarde blijft bestaan bij de terugkeer uit f naar main. Voor een automatische variabele zou dit niet het geval geweest zijn. Let erop dat s als het ware prive bezit is van de functie f: de functie main heeft er geen toegang toe, evenmin als dat het geval geweest zou zijn bij een automatische variabele. Dit in tegenstelling tot externe variabelen. Anderzijds heeft een staische variabele met een externe variabele gemeen dat er permanente geheugenruimte voor wordt gereserveerd. Tot zover de interne statische variabele. Er zijn ook externe statische variabelen. In tegenstelling tot externe variabelen die niet statisch zijn ,kunnen externe statische variabelen alleen gebruikt worden in de file waarin zij zijn gedefinieerd. In een andere file kunnen we er dus niet bij dmv. een declaratie , wat bij gewone externe variabele wel mogelijk is. Het eerder genoemde aspect van prive bezit is dus ook hier aanwezig, maar nu betreft het de hele file. Het belang hiervan bij grote soft-ware systemen is duidelijk. Men kan zich voorstellen dat gewone gebruikersprogramma's uit oogpunt van veiligheid geen toegang mogen hebben tot bepaalde 'systeemvariabelen' ,maar alleen tot functies die hiervan gebruik maken. We kunnen dan de volgende situatie krijgen: Systeemmodule (‚‚n file) static int s; /* s is een externe statische variabele */ f(i) { ... s ... } g(i) { ... s ... } Gebruikersprogramma: main() { ... f(...) ... g(...) ... } Het gebruikersprogramma heeft niet rechtstreeks toegang tot de variabele s, maar kan er slechts via de functie f en g gebruik van maken. Zou in de systeemmodule het voorvoegsel static in de declaratie ontbreken, dan zou s een niet statische externe variabele zijn. Het gebruikersprogramma zou er dan ,na declara- tie, toegang toe hebben. We zien dus ook hier het privacy aspect van de statische variabelen duidelijk gedemonstreerd. 7.6 REGISTER VARIABELEN In de functie f(n) register int n; { register char ch; ... } zijn c en ch registervariabelen. Dit betekent dat zo mogelijk machineregisters ipv geheugenplaatsen aan deze variabelen zullen worden toegewezen, waardoor zij bijzonder snel toegangkelijk zijn. Als dat niet mogelijk is, bv omdat er niet voldoende registers beschikbaar zijn, dan wordt het voorvoegsel register eenvoudig genegeerd. Alleen automatische variabelen en formele parameters kunnen optreden als registervariabele. Pointers naar registervariabelen zijn niet mogelijk; pointerwaarden zijn immers adressen van geheugenplaatsen en bij de meeste machines is een register iets anders dan een geheugenplaats. 7.7 BLOKSTRUCTUUR In C kunnen functies niet optreden binnen andere functies, zoals dat wel in bijvoorbeeld Pascal kan. Daar staat iets vergelijk- baars tegenover dat juist wel in C maar niet in Pascal mogelijk is, nl. het declareren van een variabele binnen een willekeurige compound statement. Het volgende voorbeeld demonstreert dit principe. main() { int i; i=1; if (i>0) { int i; i=2; printf("%d ", i); } printf("%d\n", i); } dit programma druk af: 2 1 In de binnenste compound statement komt de variabele i voor ,die de waarde 2 krijgt ,maar niets te maken heeft met de i in de buitenste compound statement. De tweede keer wordt door printf dan ook 1 afgedrukt. Een compound statement wordt in C ook wel een blok genoemd. Als in een taal blokken binnen andere blokken kunnen voorkomen, spreekt men van blokstructuur. Ook de volgende situatie kan voorkomen. int j; main() { int j; j=1; f(); printf("%d\n", j); } f() { j=2; } Dit programma drukt 1 af. Door de aanroep van f wordt welliswaar j gelijk aan 2 gemaakt, maar dit betreft de externe variabele j. Doordat binnen de functie main de automatische variabele j is gedeclareerd, is de externe variabele j daar niet toegankelijk. Formele parameters gedragen zich in dit opzicht als automatische variabelen: int k; f(k) char k; { ... k ... } De k die binnen de functie f wordt gebruikt is van het type char. 7.8 INITIALISEREN Onder het initialiseren verstaan we het toekennen van een beginwaarde aan een variabele op het moment dat er ruimte voor wordt gereserveerd. Een voorbeeld hiervan is: int i=123; 7.8.1 SIMPELE VARIABELEN Voor externe en statische variabelen gebeurt bij het initialiseren de daadwerkelijke toekenning ‚‚n kee en wel door de compiler. We mogen daarom bij deze variabelen als begin waarden alleen constante kiezen, of expressies die daaruit zijn opgebouwd int week=7*24; Voor automatische en registervariabelen gebeurt het initialiseren elke keer dat de functie of het blok binnengekomen wordt. De beginwaarde mag voor deze variabelen elke geldige expressie zijn; zelfs functie aanroepen mogen in deze expresie voorkomen. int i=12; /* i is extern */ main() { int a=i+1; b=f(i) /* a en b zijn automatisch */ printf("%d %d\n", a, b); } int f(x) int x; { static int s=6; /* s is statisch */ return x*x+s; } Ga na dat afgedrukt wordt: 13 150 Als we externe en statische variabelen niet initialiseren, dan wordt gegarandeerd dat hun beginwaarde 0 is. Dit geldt niet voor automatische variabelen en registervariabelen: zonder ze te initialiseren is hun beginwaarde ongedefini‰rd. Bij het initialiseren treedt zonodig typeconversie op, volgens dezelfde regels als bij de assignment. Na float x=4; int i=7.9; geldt x=4.0 en i=7. 7.8.2 ARRAYS Voor automatische arrays is het mogelijk ze te initialiseren; voor staische en externe arrays kan dat wel: int a[5] = {10, 20, 30, 40, 50 } /* extern */ f() { static float b[3] = {5, 13, 8} /* statisch */ char c[6]; /* automatisch */ . . . Hier zijn integer constante voor een float array opgegeven; er treedt weer automatisch typeconversie op. Ook hier geldt weer dat beginwaarde die niet zijn opgegeven zijn, terwijl dat wel mogelijk was geweest, dus bij extern en statisch, gegarandeerd 0 zijn. Door: int a[5] = {20, 30}, b[3]; /* extern */ wordt a[0]=20, a[1]=30, a[2]=a[3]=a[4]=b[0]=b[1]=b[2]=0 Als men en array volledig initialiseert, mag men de array-grootte ook achterwege laten. Het aantal elementen dat bij het initialiseren wordt opgegeven, bepaaldt de grootte: int a[ ] = {5, 6, 2} is gelijk aan int a[3] = {5, 6, 2} 7.8.3 ARRAYS EN KARAKTERSTRINGS Natuurlijk kunnen we ook externe of statische arrays initialise- ren als de elementen hiervan, bijvoorbeeld: static charnaam[ ] = {'J', 'a', 'n', '\0'} Gelukkig kan dat ook eenvoudiger; precies hetzelfde effect wordt verkregen door: staic char strnaam[ ] = "Jan"; Ook nu wordt er een array van vier karakters gedeclareerd, waarna geldt: strnaam[0]=J', strnaam[1]='a', strnaam[2]='n', strnaam[3]='\0' Een array als str wordt wel string genoemd. Het nulkarakter aan het eind van de string biedt de mogelijkheid de actuele lengte te bepalen. Deze kan kleiner zijn dan een eventueel in de declaratie opgegeven afmeting. Na bijvoorbeeld static char naamnaam[5] = "Jan"; is naam[0] = 'J' naam[1] = 'a' naam[2] = 'n' naam[3] = '\0' Dezelfde situatie onstaat overigens door static char naam[5]; strcpy(naam, "Jan"); Door de aanroep van de standaard functie strcpy wordt een string gekopieerd en wel van het tweede naar de eerste argument. Voor naam[4] is wel ruimte gereserveerd. De inhoud van zo'n string tot aan het nulkarakter kan eenvoudig als volgt worden afgedrukt: printf("%s", naam); De functie printf kan zelf aan de hand van het nulkarakter bepalen hoevee elementen van array naam moeten worden afgedrukt. In ons voorbeeld zijn dat er drie, nl. 'J', 'a' en 'n'. Let op de schrijfwijze "%s"; het conversiekarakter s hierin dient speciaal voor het afdrukken van strings. In dit voorbeeld kunnen vier karakters in de string aan het nulkarakter voorafgaan, immers het array heeft vijf elementen en het nulkarakter neemt ook een plaats in beslag. We kunnen daarom na de declaratie static char naam[5] = "Jan"; niet de naam Jan vervangen door Johan ,maar wel door Jaap. Dit kan eenvoudig door strcpy(naam, "Jaap"); maar ook desgewenst ook door strcpy(naam+2, "ap"); 7.9 POINTERS NAAR FUNCTIES Niet alleen variabelen ,maar ook functies hebben een (begin-) adres. De vraag rijst of ook zo'n adres als pointer kan optreden. Dit is inderdaat het geval. De notatie voor zo'n pointer bestaat eenvoudigweg uit de naam van de functie, waar dan geen haakjrs achter staan. Deze eenvoud van notatie betreft het gebruik; de declaratie daarentegen wordt wat ingewikkelder genoteerd. We geven een voorbeeld: main() { float (*h)(), g(); h=g; printf("g(5) = %f\n", (*h)(5)); } float g(i) int i; { return i+1.0/i; } De uitvoer van dit programma is: g(5) = 5.200000 Hier volgt uit de declaratie float (*h)(), g(); dat de waarde van de variabeleh het beginadres zal zijn van een functie die een float-waarde aflevert. Door h=g; wordt zo'n adres aan h toegekend. Het object waar de pointer h naar wijst, dus de functie zelf, is dan *h, vandaar dat in dit voorbeeld (*h)(5) in feite neerkomt op g(5). Ten aanzien van pointers is er een analogie tussen een functie f en een array a: f is het begin adres van de functie f(); a is het adres van a[0]. Als het bovenstaande eenmaal duidelijk is, kunnen we met pointers naar functies ook interesante dingen doen. Als we een functie ff als argument aan de functie willen meegeven, dan kan dat als volgt: float ff(); ... gg(ff); De definitie van gg ziet er dan als volgt uit: gg(fpar) float (*fpar)(); { ... (*fpar)(); ... } Ook kunnen we een functie een pointer naar een andere functie als functie waarde laten opleveren. Verder zijn ook arrays van pointers naar functies mogelijk. Verder is het vermelden waard dat ook daarin pointers naar functies opgenomen kunnen zijn. 8 STRUCTUREN 8.1 INLEIDING Een structuur (structure) is een samenhangende collectie variabelen, die van verschillend type kunnen zijn; de structuur in zijn geheel kan ook als variabele worden opgevat. (In Pascal noemt men het overeenkomstige begrip een record). Door struct { int nummer; int aantal; char naam[10]; } s, t; declareren we de structuren s en t. Een structuur ,mits extern of statisch, kan ook worden ge‹nitialiseerd. Dit gaat bijvoorbeeld als volgt: static struct { int nummer; int aantal; char naam[10]; } s = {1234, 5, "Jan"} t = {5678, 6, "Piet"} Westellen ons deze structuren als volgt voor: nummer aantal naam +--------+---------+--------------+ s | 1234 | 5 | Jan | +--------+---------+--------------+ nummer aantal naam +--------+---------+--------------+ t | 5678 | 6 | Piet | +--------+---------+--------------+ De componenten van een structuur zijn gewone variabelen; in dit geval noteren wij ze als s.nummer s.aantal s.naam t.nummer t.aantal t.naam Zo kunnen we bijvoorbeeld schrijven: s.aantal = t.aantal + 100; We kunnen bovenstaande declaratie van s en t ook in twee stappen realiseren, nl. door eerst het deel tussen de accoladen een naam te geven, bijvoorbeeld artikel en de naam vervolgens te gebruiken bij de declaratie van de variabelen s en t: struct artikel { int nummer; int aantal; char naam[10]; }; struct artikel s, t; Tenslotte kunnen deze twee stappen ook worden gecombineerd, zodanig dat bij het declareren van de variabelen s en t en passant de naam artikel opgegeven wordt: struct artikel { int nummer; int aantal; char naam[10]; } s, t; De naam artikel is nu beschikbaar als template; de gedaante van een structuur wordt daarmee vastgelegd. We zullen zien dat van templates nuttig gebruik kan worden gemaakt. 7.8.3 ARRAYS EN KARAKTERSTRINGS Natuurlijk kunnen we ook externe of statische arrays initialise- ren als de elementen hiervan, bijvoorbeeld: static charnaam[ ] = {'J', 'a', 'n', '\0'} Gelukkig kan dat ook eenvoudiger; precies hetzelfde effect wordt verkregen door: static char strnaam[ ] = "Jan"; Ook nu wordt er een array van vier karakters gedeclareerd, waarna geldt: strnaam[0]=J', strnaam[1]='a', strnaam[2]='n', strnaam[3]='\0' Een array als str wordt wel string genoemd. Het nulkarakter aan het eind van de string biedt de mogelijkheid de actuele lengte te bepalen. Deze kan kleiner zijn dan een eventueel in de declaratie opgegeven afmeting. Na bijvoorbeeld static char naamnaam[5] = "Jan"; is naam[0] = 'J' naam[1] = 'a' naam[2] = 'n' naam[3] = '\0' Dezelfde situatie onstaat overigens door static char naam[5]; strcpy(naam, "Jan"); Door de aanroep van de standaard functie strcpy wordt een string gekopieerd en wel van het tweede naar de eerste argument. Voor naam[4] is wel ruimte gereserveerd. De inhoud van zo'n string tot aan het nulkarakter kan eenvoudig als volgt worden afgedrukt: printf("%s", naam); De functie printf kan zelf aan de hand van het nulkarakter bepalen hoevee elementen van array naam moeten worden afgedrukt. In ons voorbeeld zijn dat er drie, nl. 'J', 'a' en 'n'. Let op de schrijfwijze "%s"; het conversiekarakter s hierin dient speciaal voor het afdrukken van strings. In dit voorbeeld kunnen vier karakters in de string aan het nulkarakter voorafgaan, immers het array heeft vijf elementen en het nulkarakter neemt ook een plaats in beslag. We kunnen daarom na de declaratie static char naam[5] = "Jan"; niet de naam Jan vervangen door Johan ,maar wel door Jaap. Dit kan eenvoudig door strcpy(naam, "Jaap"); maar ook desgewenst ook door strcpy(naam+2, "ap"); 7.9 POINTERS NAAR FUNCTIES Niet alleen variabelen ,maar ook functies hebben een (begin-) adres. De vraag rijst of ook zo'n adres als pointer kan optreden. Dit is inderdaat het geval. De notatie voor zo'n pointer bestaat eenvoudigweg uit de naam van de functie, waar dan geen haakjrs achter staan. Deze eenvoud van notatie betreft het gebruik; de declaratie daarentegen wordt wat ingewikkelder genoteerd. We geven een voorbeeld: main() { float (*h)(), g(); h=g; printf("g(5) = %f\n", (*h)(5)); } float g(i) int i; { return i+1.0/i; } De uitvoer van dit programma is: g(5) = 5.200000 Hier volgt uit de declaratie float (*h)(), g(); dat de waarde van de variabeleh het beginadres zal zijn van een functie die een float-waarde aflevert. Door h=g; wordt zo'n adres aan h toegekend. Het object waar de pointer h naar wijst, dus de functie zelf, is dan *h, vandaar dat in dit voorbeeld (*h)(5) in feite neerkomt op g(5). Ten aanzien van pointers is er een analogie tussen een functie f en een array a: f is het begin adres van de functie f(); a is het adres van a[0]. Als het bovenstaande eenmaal duidelijk is, kunnen we met pointers naar functies ook interesante dingen doen. Als we een functie ff als argument aan de functie willen meegeven, dan kan dat als volgt: float ff(); ... gg(ff); De definitie van gg ziet er dan als volgt uit: gg(fpar) float (*fpar)(); { ... (*fpar)(); ... } Ook kunnen we een functie een pointer naar een andere functie als functie waarde laten opleveren. Verder zijn ook arrays van pointers naar functies mogelijk. Verder is het vermelden waard dat ook daarin pointers naar functies opgenomen kunnen zijn. 8 STRUCTUREN 8.1 INLEIDING Een structuur (structure) is een samenhangende collectie varabelen, die van verschillend type kunnen zijn; de structuur in zijn geheel kan ook als variabele opgevat worden. (In Pascal noemt men het overeenkomstige begrip een record). Door struct { int nummer; int aantal; char naam[10]; } s, t; declareren we de structuren s en t. Een structuur, mits extern of statisch, kan ook worden geinitialiseerd. Dit gaat bijvoorbeeld als volgt: struct { int nummer; int aantal; char naam[10]; } s = {1234, 5, "Jan"} t = {5678, 6, "Piet"} We stellen ons nu deze structuren als volgt voor: nummer aantal naam ________________________ s | 1234 | 5 | Jan | ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ nummer aantal naam ________________________ t | 5678 | 6 | Piet | ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ De componenten van een structuur zijn gewone variabelen; in dit geval noteren wij ze als s.nummer s.aantal s.naam t.nummer t.aantal t.naam Zo kunen we bv schrijven: s.aantal = t.aantal + 100; We kunnen bovenstaande declaratie van s en t ook in twee stappen realiseren, nl. door eerst het deel tussen de accoladen een naam te geven, bijvoorbeeld artikel en deze naam vervolgens te gebruiken bij de declaratie van de variabelen s en t: struct { int nummer; int aantal; char naam[10]; }; struct artikel s, t; Tenslotte kunnen deze twee stappen ook worden gecombineerd, zodanig dat bij het declareren van variabelen s en t en passant de naam artikel opgegeven wordt: struct artikel { int nummer; int aantal; char naam[10]; } s, t; De naam artikel is nu beschikbaar als template; de gedaante van een structuur wordt ermme vastgelegd. We zullen zien dat van templates nuttig gebruik kan worden. 8.2 FUNCTIES EN STRUCTUREN Anders dan Pascal, kent C geen keyword function. Daar staat tegenover dat in C bij functies altijd haakjes worden gebruikt, ook als die functies geen parameter hebben. We moeten in de volgende regel dan ook vooral op de haakjes letten, omdat zij ons duidelijk maken dat het om een functie gaat: struct artikel *max(p,q) Hier is weer gebruik gemaakt van het type struct artikel uit de vorige paragraaf. Er volgt nu de volledige definitie van de functie max, die bij twee gegeven structuren van dat type bepaalt van welke het daarin voorkomende aantal het grootst is. Een pointer naar die structuur wordt als functiewaarde afgeleverd: struct artikel *max(p,q) struct artikel *p, *q { return (*p).aantal>(*q).aantal ? p : q; } We letten eerst op de tweede regel ,waar iets over parameters p en q wordt gezegd. Omdat *p en *q structuren zijn, zijn p en q pointers daarnaar. In de eerste regel staat dat *max(p, q) een structuur is. Dan is de functie waarde max(p, q) een pointer naar een structuur. Let daarbij op dat het prioriteiten in paragraaf 4.6, waar () op de eerste regel staat en de unaire operator * op de tweede regel. In bovenstaande functie is op de derde regel te lezen dat de component (*p).aantal van de structuur *p vergele- ken met overeenkomstige component van de structuur *q. De haakjes in (*p).aantal mogen niet achterwege blijven, omdat de punt operator (.) volgens genoemd prioriteiten overzicht een hogere prioriteit heeft dan *. In hetzelfde overzicht komt de operator -> voor, die net als de punt de allerhoogste prioriteit heeft. Deze operator dient om een veel voorkomende vorm als (*p).aantal eenvoudiger en duidelijker te noteren, nl. als p -> aantal Dit betekent dta we de functie max ook kunnen schrijven als struct artikel *max(p, q) struct artikel *p, *q { return p -> aantal > q -> aantal ? p : q; } Merk op dat om p -> aantal geen haakjes nodig zijn, omdat -> een hogere prioriteit heeft dan >. Na bovenstaande programma fragmenten bestudeerd te hebben ,is het goed eens een compleet programma te bekijken waarin e.e.a. voorkomt. Ga na dat volgende programma het getal 5678 afdrukt. struct artikel { int nummer; int aantal; char naam[10]; }; main() { static struct artikel s ={1234, 5, "Jan"}; /* static: initialisatie is mogelijk */ struct artikel t; /* auto : initialisatie is niet mogelijk */ struct artikel *max(); /* declaratie van de functie max */ t.nummer=5678; t.aantal=6; strcpy(t.naam, "Piet"); /* string assignment,zie 11.3 */ printf("%d\n", max(&s, &t) -> nummer); } struct artikel *max(p, q) /* definitie van de functie max */ struct artikel *p, *q /* de parameters zijn pointers */ { return p -> aantal > q -> aantal ? p : q; } 8.3 DYNAMISCHE GEHEUGEN ALLOCATIE Bij veel machines is een byte de kleiste adresseerbare eenheid. Een byte bestaat meestal uit 8 bits; er past precies 1 karakter in. Willen we nu ruimte voor n karakters aanvragen (memory allocation), dan kan dat door de functie aanroep malloc(n) Dit is een standaardfunctie ,die een aaneengesloten ruimte ter grootte van n bytes reserveert en een pointer naar het begin daarvan aflevert. Er komt dus voor ons ruimte voor n karakters beschikbaar. Omdat de functie geen waarde van het type int aflevert ,moeten we hem declareren. De afgeleverde waarde is een pointer naar het het eerste karakter, zodat de declaratie luidt: char *malloc(); Ons eerste programma waarin met malloc ruimte wordt aangevraagd doet het volgende. Er wordt eerst een natuurlijk getal n gelezen. Daarna wordt, alles du ook een eventuele overgang naar een nieuwe regel, overgeslagen tot en met een punt. Na deze punt wordt een rij van precies n karakters gelezen, bijvoorbeeld: 6 .Jacoba De bedoeling is nu dat de rij van n karakters wordt opgeslagen om er daarna iets mee te doen. Om het bijvoorbeeld eenvoudig te houden, zullen we alleen nagaan of het karakter , dus hier 'a', al eerder is voorgekomen. In dit voorbeeld is dit inderdaad het geval: ook de tweede letter is een 'a'. Een belangrijk punt is, dat het getal n i.p.v. 6 ook heel groot kan zijn. De rij karakters kan zich ook over een groot aantal regels uitstrekken; tussen elk tweetal regels bevindt zich dan het newline karakter in een array '\n'. Doordat we geen bovengrens voor n willen opgeven kunnen we de meest voor de hand liggende oplossing, waarbij alle gelezen karakters in een array worden opgeslagen, niet toepassen. We gaan dit probleem nu oplossen met dynamisch geheugen allocatie. We reserveren pas ruimte als we weten hoe groot n is: #include main() { int i, n; char *malloc(), ch, *p; printf("Geef n; type op de volgende regel\n"); printf("een punt, gevolgd door n karakters.\n"); scanf("%d", &n); while (getchar() != '.') /* empty statement */ /* nu is alles t/m de punt overgeslagen */ p=mallac(n); for (i=0; i= 2 * sizeof(int) + 10 sizeof(char) Hier kan het gelijk teken van toepassing zijn, maar in het algemeen mogen we daar niet vanuitgaan: er kan "loze ruimte" tussen de componenten van een structuur aanwezig zijn, waardoor het geheel groter wordt dan de som der delen. Stel nu dat we ruimte willen aanvragen voor een variabele van bovengenoemd type. Na het bovenstaande, aangevuld met char *malloc(); struct artikel *ps; kan als dit volgt: ps = (struct artikel *) malloc(sizeof(struct artikel)); Let ook hier weer op de cast operator, die ervoor zorgt dat het juiste pointertype onstaat. 8.5 DYNAMISCHE STRUCTUREN; EEN BINAIRE BOOM Als onderdeel van een structuur kunnen we pointers naar soortgelijke structuren opnemen. Combineren we dit met dynamische geheugen allocatie, dan onstaan dynamische structuren. Een voorbeeld hiervan is een binaire boom. Deze is opgebouwd uit knopen, waarvan twee pointers als component voorkomen. Elk van deze pointers kan de waarde NULL (=0) hebben, of wijzen naar een andere knoop, die we dan een zoon van de oorspronkelijke knoop noemen. We zullen het gebruik van een binaire boom demonstreren aan de hand van het volgende probleem. Gegeven is een rij positieve gehvele getallen, die alle verschillen zijn. Als afsluitcode fungeert een nul. We willen deze positieve getallen inlezen en in een binaire boom plaatsen. Is deze rij gelezen getallen 20 30 10 15 25 5 0 dan willen we dat de boom er als volgt uit komt te zien: | ____V___ | |20| | /˙˙˙˙˙˙\ / \ ________ ________ | |10| | | |30|0| /˙˙˙˙˙˙\ ˙\˙˙˙˙˙˙ / \ ˙˙˙˙\ _______ ________ ________ |0|5|0| |0|15|0| |0|25|0| ˙˙˙˙˙˙˙ ˙˙˙˙˙˙˙˙ ˙˙˙˙˙˙˙˙ We maken onderscheid tussen een linker en een rechter zoon. Als een knoop getal k bevat heeft een linker zoon die getal l bevat, dan is lk. Een pointer naar de 'bovenste' knoop van de boom, dus hier naar de knoop van het getal 20, heet de wortel van de boom. De pointers die in die knoop voorkomen zij wortels van deelbomen, enz. Aan de boom wordt als vogt een getal toegevoegd. Is wortel van de boom NULL, dan laten we een knoop ontstaan en de wortel daarnaar wijzen; het getal komt in die knoop te staan en de beide pointers van die knoop worden NULL. Is de wortel van de boom niet NULL, dan gaan we kijken of het toe te voegen getal kleiner is dan het getal dat staat in de knoop waarheen de wortel wijst. Zo ja, dan volgt de opdrachtom het getal toe te voegen aan de linker deel boom; zo neen, dan moet het getal aan de rechter deelboom worden toegevoegd. Voor het toevoegen van een getal aan een deelboom gelden weer dezelfde regels als voor het toevoegen van het getal aan de oorspronkelijke boom. Daardoor is er een korte recursieve oplossing, zoals we zullen zien. Nadat de getallen in de boom geplaatst zijn, worden ze in opklimmende volgorde afgedrukt. Dit gebeurt door de boom op de juiste wijze te door lopen. Beginnend bij de wortel van de boom, gaat in de volgorde: 1. de gehele linker deelboom van de knoop; 2. de knoop zelf; 3. de gehele rechter deelboom van de knoop. Voor elke deelboom worden dezelfde regels als voor de gehele boom gevolgd, zodat ook dit afdrukken als het ware vraagt om recursie. Nu volgt het programma. /* begin van he programma zoekboom.c */ #define NULL 0 struct knoop { struct knoop *links; int getal; struct knoop *rechts; }; main() { int i; struct knoop *wortel, *bouwop(); wortel=NULL; printf("Type gehele getallen, afgesloten door 0\n"); while (scanf("%d", &i), i != 0) wortel = bouwop(wortel, i); printf("\nDe getallen komen nu in opklimmende volgorde:\n"); drukaf(wortel); } struct knoop *bouwop(p, i) struct knoop *p; int i; { char *malloc(); if (p == NULL) { p = (struct knoop *) malloc(sizeof(struct knoop)); p -> getal = i; p -> links = p -> rechts = NULL; } else if (i < p -> getal) p -> links = bouwop(p->links, i); else p -> rechts = bouwop(p->rechts, i); return p; } drukaf(p); struct knoop *p; { if (p != NULL) { drukaf(p -> links); printf("%d\n", p -> getal); drukaf(p -> rechts); } } /* einde van programma zoekboom.c */ 8.6 STRUCTUREN MET BIT VELDEN Soms willen we bits gebruiken als 'vlag', ander gezegd, we willen aan elk bit afzonderlijk een betekenis hechten. De reeds behan- delde taalelementen zijn hiervoor eigenlijk al toereikend. Toch biedt C op dit gebied nog een speciale faciliteit. Stel bijvoor- beeld dat bij persoonsgegevens onthouden moet worden of iemand - mannelijk is, - gehuwd is, - ouder dan 60 jaar is. Voor elk van deze drie aspecten zijn er maar twee moelijkheden, nl. waar of niet waar. We kunnen als volgt een array van 1000 structuur-elementen declareren; elke structuur bevat een stamnummer, een naam en daarna de drie bedoelde bitvelden: struct { int stamnummer; char naam[30]; unsigned mannelijk: 1; unsigned gehuwd: 1; unsigned ouderdan60: 1; } persoon[1000]; De drie hier getoonde bitvelden bestaande elk uit een bit. Zij kunnen daardoor de waarden 0 of 1 krijgen en geen andere. Mogelijk is nu bijvoorbeeld: persoon[i].mannelijk = 0; De 1 achter de dubbele punt is het aantal bits van elk veld. Dit aantal kan ook groter dan 1 zijn, maar niet zo groot dat de bits vaneen veld niet meer in een machinewoord passen. In het algemeen worden bitvelden compact opgeborgen, zodat zuinig met de geheugenruimte wordt omgesprongen. Vooral bij een groot array, zoals in dit voorbeeld, kan de ruimtewinst aanzienlijk zijn. Omdat bitvelden slechts een onderdeel van een machinewoord zijn, hebben zij geen eigen adres; anders gezegd, een pointer ernaar, dus bijvoorbeeld &(persoon[i].mannelijk) is niet mogelijk. 8.7 UNIONS De geheugenruimte die door een structuur in beslag wordt genomen is ten minste gelijk aan de som van de ruimte van de componenten. Alle componeneten van een structuur zijn gelijktijdig fysiek aanwezig. Het komt ook voor dat iets anders de bedoeling is, nl. dat er verschillende varianten mogelijk zijn, die niet gelijktijdig optreden. De benodigde geheugen ruimte behoeft dan niet groter te zijn dan de grootste van de verschillende varianten. Dit wordt gerealiseerd met een union. In het volgende voorbeeld is u een union en p een pointer naar zo'n union. union intfloatstring { int i; float f; char str[8]; } u, *p; Net als bij structuren zijn hierna de volgende notaties voor variabelen mogelijk. u.i u.f u.str p->i p->f p->str Nadat nu bijvoorbeeld u.i = 123; is uitgevoerd zijn eventueel eerder aan u.f of u.str toegekende waarden verloren gegaan, omdat u.i dezelfde geheugenruimte als u.f en u.str, of althans een deel daarvan, in beslag neemt. Een naam als intfloatstring, die tussen union en de open accolade staat is niet verplicht, evenmin als dat bij structuren het geval is. Wel kan zo'n naam handig zijn voor later gebruik, zoals bijvoorbeeld in f(q) union intfloatstring *q; { ... } op analoge wijze als dat bij structuren mogelijk is. 8.8 TYPEDEF We kunnen, terwille van de leesbaarheid of bij wijze van afkorting, aan een type een andere naam geven. Schrijven we typedef double REAL; waarbij REAL willekeurig gekozen is, dan kunnen we daarna REAL x, y, z; schrijven in plaats van double x, y, z; REAL en double zijn nu twee verschillende namen voor precies hetzelfde type. Een iets minder eenvoudig voorbeeld is: typedef char *STRING; Hierna is STRING p; equivalent met char *p; met andere woorden, STRING is nu een type naam voor een pointer naar karakter. Typedef kan ook voor structuren worden gebruikt. Na typedef struct { int nummer; int aantal; char naam[20]; } ART; kan bijvoorbeeld worden gedeclareerd worden als: ART s, t; We laten nu niet ART vooraf door struc; dit was wel het geval met de naam artikel die in 8.1 optrad als template. Tenslote nog iets moeilijker voorbeeld. Door typedef char *FPC(); kunnen we schrijven FPC f, g; in plaats van char *f(), g(); Hier zijn f en g die als functiewaarde een pointer afleveren die wijst naar een karakter. 9 PREPROCESSOR FACILLITEITEN Als we een compiler nog wat kunnen laten manipuleren met de aangeboden programmatekst voordat het eigenlijke vertaalwerk begint, spreken we over preprocessor facilitieten. De opdrachten die we daartoe in de programma tekst notern , heten compiler control lines. 9.1 #DEFINE Zoals we hebben gezien, kunnen we schrijven #define AANTAL 1000 waarna we verder overal AANTAL in plaats van 1000 kunnen schrijven. We noemen AANTAL een macro. De regel in zijn geheel is een macrodefinitie. Deze macrodefinitie is van de vorm: #define indentiefier string De indentifier wordt nu overal vervangen door de string. Deze vereenvoudige vorm van een macrodefinitie kan ook worden uitgebreid met parameters. Eerst weer een voorbeeld: #define max(a, b) ((a) > (b) ? (a) : (b)) Schrijven we nu y = 2 * max(i+1, j-1) + 3; dan wordt dit vervangen door y = 2 * ((i+1 > (j-1) ? (i+1) : (j-1)) + 3; Het valt de lezer misschien op dat in de macrodefinitie nogal veel haakjes voorkomen. In het algemeen is dat een goede gewoonte. Zijn we zuinig met haakjes, dan kan de volgende ongewenste situatie ontstaan. Na de macrodefinitie #define kwadraat(x) x * x leidt de macro aanroep q = kwadraat(a+b); tot de macro expansie q = a+b * a+b; Hetgeen niet de bedoeling zal zijn. Schrijven we nu #define kwadraat(x) ((x) * (x)) dan is er geen probleem. Ga dat na! Als een macro definitie te lang wordt, kunnen we een backslash (\) als continueringskarakter noteren en op de volgende regel verder gaan. Als wij een macro aanroep schrijven dan zal de compiler precies dezelfde machine code produceren als wanneer wij zelf de expansie ervan hadden geschreven. Dit is een verschil met functies. In dit laatste voorbeeld wordt bijvoorbeeld de optelling a+b twee keer verricht; als kwadraat een functie geweest was, zou het argument a+b maar een keer zijberekend. De algemene vorm van een macrodefinitie met parameters is #define indentifier(indentifier, ... ,indentifier) string De tussen haakjes geneomde indentifier worden, als zij voorkomen in de geneomde string, daar steeds vervangen door de overeenkomstige (actuele) argumenten, die in de macro aanroep staan. Het aantal argumenten in de aanroep moet gelijk zijn aan het aantal parameters van de macro. Een belangrijk punt is dat het open haakje direct op de macronaam moet volgen. Er mag geen spatie tussen staan. Zouden we schrijven #define kwadraat(x) ((x) * (x)) dan wordt de macro geheel anders opgevat dan we bedoelen; nu wordt namelijk overal de indetifier kwadraat vervangen door de string (x) ((x) * (x)) wat niet de bedoeling zal zijn. Deze laatste macrodefinitie is immers weer in de vorm #define indentifier string zodat x niet als parameter wordt herkend. We kunnen de compiler ook opgegeven dat hij de definitie van een macro verde moet vergeten. Dit gaat eenvoudig door #undef indentifier waarin indetifier de bedoelde naam van de macro is. 9.2 #INCLUDE We kunnen de inhoud van een file in onze programma tekst inlassen door de naam van die fiel te vermelden achter #include. Schrijven we bijvoorbeeld #include "HULPTEKST" dan wordt gezocht naar de file HULPTEKST, eerst in onze eigen directory en daarna ook in algemeen voor dit doel toegankelijke directories. Welke dit precies zijn is systeem afhankelijk. We kunen ook iets schrijven van de vorm: #include < ... > Nu wordt de in ... genoemde file alleen gezocht in de bovenbedoelde algemene directories ,dus niet in onze eigen directory. Voorbeelden van het gebruik hiervan zijn: #include #include In 'header files' zoals stdio.h kunnen ook weer #include regels voorkomen, met andere woorden, #include regels kunnen genest zijn 9.3 CONDITIONELE COMPILATIE We kunnen het compileren van gedeelten programma tekst ook afhangen van een voorwaarde. We krijgen dan iets wat lijkt op if statements, maar we moeten ons steeds realiseren dat de 'preprocessor' ermee uit de voeten moet kunnen: er mogen dus geen programmavariabelen in de voorwaarden voorkomen, want die hebben in deze vroege fase nog geen waarde gekregen. Wel kunnen we constanten gebruiken, ook die welke met #define gedefinieert zijn. We geven een voorbeeld: #define AANTAL 1000 #define MAX 1200 #if AANTAL+100 10.2 DE FUNCTIE PRINTF Een aanroep van printf heeft de vorm : printf(format-string, arg1, arg2, ... ) De format string is in zijn meest algemene vorm een expressie die een string oplevert, bijvoorbeeld x<0 ? "Negatief\n" : (x>0 ? "Positief\n" : "Nul\n") In deze string kunnen twee soorten objecten voorkomen: - gewone af te drukken karakters, - conversie specificaties. Voor elk van de argumenten arg1, arg2, ... is er een conversie specificatie in de string. Elke conversie specificatie begint met een procentteken (%) en eindigt op een conversie karakter. Daartussen kan ook nog iets staan, bijvoorbeeld iets over de precisie. We bespreken eerst de conversie karakters zelf: d Het argument wordt geconverteerd naar decimale notatie. o Het argument wordt geconverteerd naar octale notatie, zonder teken en zonder leidende nul. x Het argument wordt geconverteerd naar hexadecimale notatie, zonder teken en zonder leidende 0x. u Het argument wordt geconverteerd naar 'unsigned' decimale notatie c Het argument is een enkel karakter of wordt als zodanig opgevat. s Het argument is een string. Karakters van een string worden afgedrukt totdat een nulkarakter wordt bereikt, of totdat zoveel karakters zijn afgedrukt als de precisie (tussen % en s) aangeeft. f Het argument wordt opgevat als float of double en geconver- teerd naar decimale notatie van de vorm [-]mmm.nnn waarin het aantal cijfers n achter de punt bepaald wordt door de precisie (tussen % en f). Als we geen precisie opgeven is dit aantal 6. e Het argument wordt opgevat als float of double en geconver- teerd naar decimale notatie van de vorm [-]m.nnnnnn[+/-]xx waarin het aantal cijfers n achter de punt bepaald wordt door de precisie (tussen % en e). Als we geen precisie opgeven is dit aantal 6. Als we in de uitvoer geen kleine letter e maar een hoofdletter E willen, dan moeten we ook als conversiekarakter E in plaats van e schrijven. g Afhankelijk van wat de kortste vorm oplevert, wordt %f of %e gekozen. Tussen % en het conversiekarakter kan staan: - Een minteken, dat hier betekent dat het geconverteerde argument links in de beschikbare ruimte moet komen. - Een rijtje cijfers dat de veldbreedte aangeeft, dat wil zeggen het aantal posities dat beschikbaar is. De ruimte die over is wordt opgevuld met spaties, of, als genoemd rijtje cijfers met een nul begint, met nullen. Deze opvulling vindt plaats aan de linker kant, tenzij het zo even genoemde minteken gebruikt is. - Een punt, die het rijtje cijfers voor de veldbreedte scheidt van het volgende rijtje cijfers. - Een rijtje cijfers dat precisie aangeeft. Voor een getal is dit het aantal cijfers dat rechts van de punt komt; voor een string het aantal karakters van de string dat afgedrukt moet worden. - Ed kleine letter l (L), die aangeeft dat het argument van het type long int is in plaats van int. Als na % een karakter komt dat niet met de genoemde mogelijkheden overeenkomt, wordt dat karakter gewoon afgedrukt. We kunnen dus % laten afdrkken door %% in de format-string te noteren. Als voorbeeld van het gebruik van het bovenstaande bekijken we: printf("%-10.6s???%c\n", "Vreemde dingen", '!'); afgedrukt wordt: Vreemd ???! Zonder minteken, dus met %10.6, zou afgedrukt worden Vreemd???! met 4 spaties links van de V. Let ook op dat ??? en op \n. Deze karakters staan buiten de conversie specificaties %-10.6s en %c, zodat zij gewoon worden afgedrukt. Zoals in 2.3.2 bekend is, heeft \n de betekenis newline. Waarschuwing: We moeten er zelf voor zorgen dat de types van de af te drukken argumenten overeenstemmen met het gebruikte coversiekarakter en dus bv. geen int- argument afdrukken met %f. Voor de volledigheid melden we nog dat printf ook een functie waarde aflevert. Deze is gelijk aan het aantal overgedragen karakters, of negatief in geval van een fout. Hetzelfde geldt voor de nog te behandelen functies fprintf en sprintf. 10.3 DE FUNCTIE SCANF Een aanroep van de functie scanf. die zoals bekend dient voor invoer vanaf het toetsenbord, heeft de volgende vorm: scanf(format-string, arg1, arg2, ... ); De argumenten arg1, arg2, ... zijn pointers, die vertellen waar de ingelezen gegevens moeten worden opgeborgen. Een byzonder veel voorkomende vergissing is dat men iets schrijft als scanf("%d", n); terwijl men bedoeld scanf("%d", &n); Het eerste argument van scanf bevat, net als bij printf, coversie specificaties. We onderscheiden nu de volgende conversiekarakters d In de invoer wordt een decimaal genoteerd geheel getal verwacht. Het corrosponderende argument (arg...) moet een pointer naar een integer zijn. o In de invoer wordt een octaal genoteerd geheel getal verwacht. Een leidende 0 is niet vereist. Het corrosponde- rende argument moet een pointer naar een integer zijn. x In de invoer wot een hexadecimaal genoteerd geheel getal verwacht, met of zoder 0x aan het begin. Het corrosponde- rende argument moet een pointer naar een integer zijn. c In de invoer wordt een enkel karakter verwacht. Het corros- ponderende argument moet een pointer naar een karakter zijn. Dit ingelezen karakter kan ook een spatie zijn. s In de invoer wordt een rij karakters verwacht, waarbij spaties aan het begin worden overgeslagen. Als geen veld- breedte wordt opgegeven (zie verder), dan wordt er gelezen tot aan de eerstvolgende spatie (of newline of tab). Het corrosponderende argument moet een karakter-pointer zijn, die wijst naar een punt in een array van karakters en daarna ook nog een nulkarakter (\0) erin te kunnen plaatsen. f In de invoer wordt een 'floating point number' verwacht. Dit houdt in dat iets als 123, maar ook -123.45E-6 of 1.e30 toegestaan is. Het corrosponderende argument moet een poin- ter zijn naar een variabele van het type float. De conversie karakters d, o en x worden vooraf gegaan door de letter l (L) om aan te geven dat het corrosponderende argument een pointer naar long in plaats van int is. Zij kunnen ook worden voorafgegaan door de letter h in geval van short. Een l kan ook aan de f voorafgaan om aan te geven dat het corrosponderende argument een pointer naar double in plaats van float is. Buiten de coversie specificaties kunnen in de eerste parameter van scanf nog voorkomen: - spaties, tabs en newlines, die gegenereerd worden; - gewone karakters (niet %), die ook in de invoer moet voorkomen Tussen % en het conversieteken kan staan: - en * die aangeeft dat wel iets gelezen moet worden, maar dat geen toekenning aan een variabele moet plaatvinden; - een getal, de maximale 'veldbreedte'. Als voorbeeld bekijken we int i, j, k; float x; char str[6], ch; scanf("%c %d %f ++ %3d %2d %*d %s", &ch, &i, &x, &j, &k, str); waarbij q 123 3.14++98765432 abc dan geldt na afloop ch = 'q' i = 123 x = 3.14 j = 987 k = 65 str = "abc\0" Ook de functie scanf levert een functie waarde af. Deze is gelijk aan het aantal gelezen waarden. Als het lezen niet is gelukt, dan is de functie waarde 0 of EOF (de waarde EOF is gedefinieerd in stdio.h). Hetzelfde geldt voor de nog te bespreken functies fscanf en sscanf. 10.4 FILES: FOPEN, GETC, PUTC, FCLOSE Tot nu toe had invoer betrekking op het toetsenbord; uitvoer kwam op het beeldscherm. We willen ook informatie kunnen lezen van en schrijven op andere media, waartoe in eerste plaats een schijvengeheugen (disk) behoort. In het algemeen zeggen we dat we lezen van of schrijven op een file. Zo'n file heeft een naam. Om een aantal nieuwe begrippen op samenhangende wijze te kunnen bespreken, tonen we een compleet programma. Dit programma kopieert de file fff naar de file ggg: #include main() { FILE *p, *q; int ch; p=fopen("fff", "r"); q=fopen("ggg", "w"); while ((ch=getc(p)) != EOF) putc(ch, q); fclose(p); fclose(q); } Dankzij #include kunnen we in dit programma de volgende indentifiers gebruiken: FILE fopen getc putc EOF fclose De in stdio.h gedefinieerde waarde van EOF, bijvoorbeeld -1, is de waarde die we krijgen als we proberen te lezen terwijl het eind van de file (End Of File) bereikt is. Om de waarde van EOF te kunnen ondescheiden van alle mogelijkheden karakterwaarden, nemen we als type niet char, maar int. De karakters worden als het ware ingebed in de grotere verzameling van de integers, zodat er naast de gewone karakters ook plaats is voor EOF. De hier gebruikte indetifier FILE is de naam van een zeker type en wel een structuur type. Er is in stdio.h iets gedefinieert van de vorm: typedef struct { ... } FILE; We moeten ons voorstellen dat we alleen met een file (op disk) kunnen werken als er op dat moment in het snelle werkgeheugen een blok administratie gegevens aanwezig. Zo'n blok gegevens over een file zullen we een file control block noemen. We hoeven niet precies te weten wat er allemaal in zo'n structuur staat, maar we zullen een pointer ernaar gebruiken m te vertellen met welke file we iets willen gaan doen. Eerst moeten we ervoor zorgen dat er een file control block onstaat. Door FILE *p, *q; maken we duidelijk dat we p en q als pointer naar een file control block willen gebruiken. Kortheidshalve noemen we zulke pointers file pointers. De functie fopen, die een file pointer oplevert en hier als volgt wordt aangeroepen: p = fopen("fff", "r"); q = fopen("ggg", "w"); doet door elke aanroep een file control block ontstaan. We zeggen dan dat er files worden geopend. Het eerste argument van fopen is de naam van de file. Voor het tweede argument zijn er vrij veel mogelijkheden, die we voor de volledigheid alvast zullen noemen. Een moeilijkheid is hierbij, dat niet alle operating systems dezelfde mogelijkheid bieden. Voor diegene die Unix gebruiken kunnen als tweede argument van fopen de volgende strings noteren: "r" Open een bestaande file om te gaan lezen (read). "w" Open een file om te gaan schrijven (write). Als de file nog niet bestaat, dan wordt hij gecreeerd; bestaat hij al, dan gaat de oude inhoud verloren. "a" Open om aan het eind van de file te gaan schrijven (append). Als de file nog niet bestaat, dan geldt hetzelfde als bij "w". "r+" Open een bestaande file om die bij te werken (update), dwz. om te lezen en te schrijven. Zie 10.8. "w+" Als "w", maar nu is ook lezen mogelijk. "a+" Als "a", maar nu is ook lezen mogelijk. De functiewaarde die fopen aflevert is een pointer naar het zojuist gecreeerde file-control-block, althans als het mogelijk is de file te openen; zo niet, dan levert fopen de waarde NULL af. De controle of het openen gelukt is, is hier achterwege gelaten. We hebben de afgeleverde pointerwaarde nodig als we iets met de file willen doen. Door ch = getc(p); wordt een karakter gelezen uit de desbetreffende file. Dit karakter wordt toegekend aan de variabele ch, zodat we het daarna met putc(ch, q); kunnen schrijven op de eveneens vooraf geopende file ggg, waarbij we gebuik maken van de file pointer q. Ook nu wordt EOF als functie waarde opgeleverd als het schrijven niet lukt. De samenhang tussen de pointer p, het file control block *p en de file fff en evenzo tussen q, en *q en ggg kan als volgt worden uitgebeeld: filepointer file | control file | block | | | | ------------------------------------- | p ---> *p ---> fff | | q ---> *q ---> ggg | ------------------------------------- werkgeheugen disk We zien dat uitgaande van een file pointer via een file control block een file bereikt kan worden. We merken op, dat getc en putc met een willekeurige file hetzelfde doen wat getchar en putchar doen met een toetsenbord, respectievelijk beeldscherm. In feite zijn getchar en putchar macro's, in stdio.h gedefinieerd door #define getchar() getc(stdin) #define putchar(ch) putc(ch, stdout) Hierin zijn stdin en stdout file pointers, die gedefinieerd zijn in stdio.h. Ook het toetsenbord en het beeldscherm worden als een file beschouwd. Het openen van deze files blijft in ons programma achterwege. Bovenstaande macrodefinities voor getchar en putchar houden het volgende in: ch=getchar(); is equivalent met ch=getc(stdin); putchar(ch); is equivalent met putc(ch, stdout); Let er in het bijzonder op dat een file pointer bij putc optreedt als tweede argument. Naast stdin en stdout valt ook de file pointer stderr te vermelden. Deze biedt de mogelijkheid foutmeldingen te scheiden van de gewone uitvoer op stdout. Dit is nuttig als foutmeldingen op het beeldscherm moeten komen, terwijl stdout als het ware omgeleid wordt naar een file op disk. Als we klaar zijn met het lezen van of het schrijven op een file, kan het filecontrol block worden opgeruimd. Dit gebeurt door: fclose(p); fclose(q); Het kan voordelig zijn de 'verbinding' met een file niet langer in stand te houden dan nodig is. Als een file aldus is gesloten, kan hij opnieuw worden geopend, waardoor weer op het begin wordt gepositioneerd. 10.5 FPRINTF,FSCANF,SPRINTF,SSCANF,UNGETC Hoewel met de behandelde functies getc en putc bijzonder veel kan worden gedaan, zijn er meer functies voor in- en uitvoer die onze aandacht verdienen. In eerste plaats zijn dat enkele functies die nauw verband houden met de bekende functies printf en scanf. Als we 'geformateerd' willen schrijven willen schrijven op een file met file pointer p, dan gaat dat als volgt: fprintf(p, format-string, arg1, arg2, ... ); Net zoals printf uitvoer geeft op het beeldscherm, geeft fprintf uitvoer op de file met file pointer p. De eerste f van fprintf duidt op file uitvoer., de f aan het eind van de namen fprintf en printf is de f van format. Analoog aan het voorgaande is fscanff(p, format-string, arg1, arg2, ... ); vergelijkbaar met de bekende wijze waarop scanf wordt gebruikt; nu wordt niet van het toetsenbord gelezen, maar van de file met file pointer p. Het komt voor dat we helemaal niets willen schrijven of lezen, maar wel dezelfde soort conversie als bij printf of scanf wensen. Men spreekt dan wel van 'in memory format conversion'. Dit gaat als volgt: sprintf(stringvariabele, format-string, arg1, arg2, ... ); sscanf(stringvariabele, format-string, arg1, arg2, ... ); Bij sprintf komt het resultaat niet zoals bij printf op het beeldscherm , maar in de stringvariabele te staan. Na bijvoor- beeld char str[10]; float f; int i; f=65.4321; heeft de aanroep sprintf(str, "%7.2f", 2*f); hetzelfde effect als strcpy(str, " 130.86"); Laten we dit volgen door sscanf(str, "%3d", &i); dan heeft dit hetzelfde effect als i = 130; Soms hebben we behoefte aan de mogelijkheid een rij karakters in te lezen tot aan een zeker karakter, maar zonder dat dit karakter zelf. We zouden dan ahw. een karakter vooruit willen kijken, om te zien welk karakter aan de beurt is om gelezen te worden. In Pascal kan dit met een zogenaamde buffervariabele. In C is dit anders opgelost: we lezen dat karakter eerst, maar maken dat daarna weer ongedaan. Het is alsof we het karakter weer in de invoer terugzetten. Dit gaat door ungetc(ch, p); waarin ch dat karakter en p de betrokken file pointer is. Als voorbeeld van een nuttige toepassingvan ungetc tonen we de functie leesspaties. Deze leest van een willekeurige file alle spaties tot aan het eerste karakter dat geen spatie is. Dit kan ook de waarde EOF zijn. Let erop dat hier weer het type int is gekozen, zodat we dat ook zagen aan het begin van 10.4. #include leesspaties(p) FILE *p; { int ch; do ch = getc(p); while==' '); ungetc(ch, p); } Gebruiken we deze functie als volgt : leesspaties(p); ch=getc(p); dan zijn we er zeker vanmet ungetc twee of meer karakters direct na elkaar in de file terug zetten. Verder kan nog vermeld worden dat ook na aanroepen van getchar en van scanf, dus nadat karakters van het toetsenbord gelezen zijn, ungetc(stdin) moge- lijk is. 10.6 IN- EN UITVOER VAN EEN STRING: FGETS,FPUTS,GETS,PUTS We kunnen ook een regel in zijn geheel lezen of schrijven. Functie aanroepen hiervoor hebben de vorm fgets(str, maxlen, p); respectievelijk fputs(str, p); Hierin is str een string, dus een array van karakters, p is een file pointer en maxlen is de maximale lengte die de gelezen string, aangevuld met \0, zal hebben; het aantal gelezen karak- ters is dus ten hoogste maxlen-1. Als voorbeeld na char str[20]; ... fgets(str, 20, stdin); op het toetsenbord intypen: Jansen dan krijgt str dezelfde waarde als door strcpy(str, "Jansen\n"); In dit geval wordt tot en met het newline karakter gelezen. Hadden we in bovenstaande aanroep van fgets als tweede argument 4 in plaats van 20 genoteerd, dan zou het resultaat vergelijkbaar zijn geweest met strcpy(str, "Jan"); De letters "sen" worden in dit geval niet meer gelezen. Het effect van fputs bestaat, zoals zich laat raden, daaruit dat de karakters van de meegegeven string, tot aan \0, worden geschreven in de file met file pointer p. We kunnen het effect van fgets en fputs uitdrukken in de meer elementaire functies getc en putc. We zullen dit doen, enerzijds als oefening in het lezen en begrijpen van C tekst: #define char *fgets(s, n, p); char *s; int n; FILE *p { int ch; char *cs; cs=s; while (--n>0 && (chgetc(p)) != EOF) if ((*cs++ =ch) == '\n') break; return ch==EOF && cs==s ? NULL : s; /* de pointerwaarde NULL (=0) is gedefinieerd in stdio.h */ } net als in 10.4 is de variabele ch het type int ipv. char gekozen. Ook hier zijn er apart functies voor het belangrijke bijzondere geval dat invoer van stdin, dus van het toetsenbord, en uitvoer op stdout, dus op het beeldscherm, plaats vindt. We gebruiken dan: gets(str); puts(str); Behalve het ontbreken van een file pointer p als argument, zijn er nog enkele verschillen met fgets en fputs. Bij gets ontbreekt het argument maxlen. Als we een string lezen met gets, wordt het afsluitende newline-karakter niet in de string opgenomen. In verband hiermee wordt als we met puts een string schrijven, aan het eind een newline-karakter toegevoegd, zodat toch op een nieuwe regel wordt overgegaan. Zowel fgets als gets levert als functiewaarde een pointer naar een karakter op. Deze is gelijk aan het argument str als het lezen gelukt is en NULL als dat niet het geval is. 10.7 DIRECTE TOEGANKELIJKHEID : FSEEK Bij de tot nu toe besproken voorbeelden is de in- en uitvoer strikt sequentieel, d.w.z. dat we in een file van voren naar achteren lezen of schrijven. We kunnen ook aan het eind van een file iets toevoegen dor als argument van fopen de "a" van append te kiezen. Er zijn veel computer toepassingen waar deze mogelijkheden niet voldoende zijn. We zouden ergens in een file een willekeurige punt willen aanwijzen en daarna vanaf dat punt willen gaan lezen of schrijven. Men spreekt dan van direct access ofwel random-access. In C gaat het aanwijzen van een positie in een file met file pointer p als volgt: fseek(p, positie, code); Het tweede argument is van het type long int. Het geeft de relatieve positie aan, gerekend vanaf een zeker startpunt dat bepaald wordt door code, het derde argument: code | startpunt ----------+------------------------------- 0 | het begin van de file 1 | de huidige positie 2 | het einde van de file De relatieve positie t.o.v. het startpunt wordt uitgedrukt in bytes, waarbij we tellen vanaf 0. We gebruiken fseek in de regel in combinatie met de functies fread en fwrite. Deze worden besproken in de volgende paragraaf. De functiewaarde van fseek is 0 als alles goed is gegaan en anders ongelijk aan 0. Desgewenst kunnen we dus als volgt de aanroep combineren met een controle: if (fseek(p, positie, code)) {... /* iets misgegaan */ } 10.8 FREAD, FRWRITE, FERROR, FOEF In tegenstelling tot de functies fscanf en fprintf voor een geformateerde in- en uitvoer, zijn er ook de functies fread en fwrite, waarbij de in- en uitvoer ongeformateerd ofwel binair is. De externe representatie van de informatie is hierbij dezelfde als de interne, anders gezegd, er wordt geen conversie uitgevoerd Deze functies worden als volgt aangeroepen: fread(bufptr, size, n, fp); fwrite(bufptr, size, n, fp); Hierin is : bufptr het adres van (dus een pointer naar) een gebied in het werkgeheugen; size de grootte (in bytes) van een element uit dit gebied; n het aantal van die elementen die door een aanroep van fread of fwrite moeten worden overgedragen; fp de file pointer Het type van deze argumenten volgt uit de declaratie: char *bufptr; int size, n; FILE *fp; De functies fread en fwrite leveren ook een functie waarde af; deze is gelijk aan het aantal gelezen of geschreven elementen. We hoeven hier geen gebruik te maken, maar we kunnen ermee controleren of er geen fouten opgetreden zijn. Bij fread is de functie waarde ook 0 als er niets is gelezen doordat we aan het einde van de file gekomen zijn. Na char str[1000]; wordt dit gehele array geschreven in de file met file pointer fp door: fwrite(str, 1, 1000, fp); Anderzijds kunnen we bijvoorbeeld een enkele integer in die file schrijven door : int i; i=123; fwrite((char *)&i, sizeof(int), 1, fp); Deze integer wordt hierdoor binair geschreven, bijvoorbeeld in 32 bits en dus niet in de vorm van een rijtje decimale cijfers. Als we lezen van of schrijven op de file met de file pointer fp, dan kan dat mis gaan, zowel door een fout in ons programma, als door een fout in de apparatuur. Behalve door op de functie waarde van fread en fwrite te testen, is er nog een ander middel om een fout te detecteren, namelijk: if (ferror(fp)) { /* er is iets misgegaan */ ... } De functie ferror levert een waarde ongelijk aan nul op als er een fout is opgetreden; is alles goed gegaan, dan is de functie waarde nul. Op soortgelijke wijze kan worden vastgesteld of bij het lezen het einde van de file is bereikt: if (feof(fp)) { /* het einde van de file is bereikt */ ... } De waarde van feof(fp) is nul, totdat aan het einde van de file een aanroep van fread heeft plaats gevonden waarbij het lezen als het ware is mislukt. We moeten dus niet, zoals in Pascal, op 'end-of-file' testen voordat we lezen, maar erna! Het gebruik van fread en fwrite kan worden voorafgegaan door een aanroep van de functie fseek. Op deze wijze krijgen we 'random access': we kunnen, net als bij een array, een plaats in een file direct bereiken en daar dan lezen en schrijven. Een eenvoudige toepas- sing die het essentiele van het voorafgaande demonstreert, is het volgende. In een magazijn heeft elk artikel een nummer. Dit is een geheel getal, kleiner dan 20000 en niet negatief. Van elk artikel wordt het aantal stuks bijgehouden in de file voorraad. Deze file bevat niet de artikel nummers zelf, maar alleen de ermee corrosponde- rende aantallen, precies 20000 stuks. Toen heeft men het volgende programma uitgevoerd: #include #define n 20000 main() { int i, buf, k; FILE *fp; fp=fopen("voorraad", "w"); /* "w" bij gebruik van unix; */ /* "o" bij prime */ if (fp==NULL) printf("openen lukt niet\n"); else { buf=0; k=sizeof(int); for (i=0; i #define n 2000 main() { int artnr, toename, k, buf; long positie; FILE *fp; fp=fopen("voorraad", "r+"); /* "r+" bij Unix en "i+" bij Prime */ if (fp==NULL) printf("openen lukt niet\n"); else { printf("Geef steeds een artikelnummer en de toename;\n"); printf("geef 0 0 aan het eind.\n"); k=sizeof(int); while (scanf("%d %d", &artnr, &toename), artnr != 0 || toename != 0) { positie = artnr*(long)k; fseek(fp, positie, 0); /* code voor begin punt */ if (fread((char *)&buf, k, 1, fp) ==0) { printf("lezen lukt niet\n"); continue ; } buf += toename; fseek(fp, positie, 0); fwrite((char *)&buf, k, 1, fp); if (ferror(fp)) { printf("schrijven lukt niet\n"); break; } } fclose(fp); } } Als we dit programma vlak na het eerste uitvoeren en 200 5 150 10 200 7 100 8 150 80 0 0 als invoer intypen, dan is de situatie als volgt: ---------------------------------- art.nr. aantal 100 8 150 90 200 12 De file bevat 20000 getallen. ---------------------------------- Het is aardig en nuttig, hetgeen hier tussen de horizontale lijnen staat automatisch te laten produceren, met de file voorraad als enige bron van informatie. Let erop dat in het volgende programma hier geen gebruik is gemaakt van de functie foef, maar dat gestopt wordt als fread de waarde 0 aflevert. In de praktijk wordt heel vaak op deze wijze vastgesteld dat het einde van de file is bereikt. #include main() { int n=0, buf, k; FILE *fp; fp=fopen("voorraad", "r"); /* "r" bij Unix en "i" bij Prime */ k=sizeof(int); printf("art.nr. aantal\n"); while (fread((char *)&buf, k, 1, fp)) { if (buf != 0) printf("%5d%8d\n", n, buf); n++; } printf("\nDe file bevat%6d getallen. \n"), n); fclose(fp); } 11 DIVERSE ANDERE ONDERWERPEN 11.1 DE GOTO STATEMENT EN EXIT Een statement die in legere programmeertalen dan C veel wordt gebruikt, is de goto statement. In C hoeven we er zelden of nooit gebruik van te gebruiken. Toch kan men desgewenst ook in C de goto statement gebruiken. Deze heeft de vorm: goto indentifier waarbij een andere statement in dezelfde functie 'gelabeld' moet zijn met deze indentifier. Uit een voorbeeld blijkt hoe dat gaat: goto ginds; ... ginds: x=y+1; Hierbij dient vermeldt te worden dat C ook de empty statement ofwel null statement kent, die uit alleen een puntkomma bestaat. we kunnen dus als volgt naar het einde van een compound statement springen: { ... ; goto klaar; ... ; klaar: ; } Omdat met een goto statement alleen een sprong binnen een functie kan worden uitgevoerd, kan bijvoorbeeld niet uit een hulpfunctie gesprongen worden naar het einde van de functie main. Er is daarom behoefte aan een ander middel om de uitvoering van het programma op eenvoudige wijze te kunnen beeindigen. Hiervoor dient de functie exit. De gebruikelijke manier van aanroepen is: exit(0); Het argument is bedoeld om aan een ander proces dat ons programma aanroept, informatie door te geven. daarbij betekent 0 dat alles in orde is. Is dat niet geval dan kunnen we ook exit(1) schrijven. Hoe een proces een programma aanroept is systeemafhan- kelijk en blijft daarom onbesproken. Voor ons is het belangrijk te weten dat door een aanroep van exit gestopt wordt zoals wij het verwachten, dwz dat eventuele in- en uitvoerbewerkingen eerst worden afgemaakt en dat geopende files worden gesloten. 11.2 DE PROGRAMMA ARGUMENTEN ARGV EN ARGV Het is soms prettig argumenten aan een programma te kunnen meegeven, bijvoorbeeld namen van files. Als voorbeeld bespreken we een kopieerprogramma, dat twee invoerfiles leest en ze na elkaar schrijft op een uitvoerfile. Als het programma kopieer heet dan luidt het programma: #include main(argc, argv) int argv; char *argv[ ]; { FILE *pin1, *pin2, *puit, *openen(); int ch; if (argc != 4) { printf("drie argumenten zijn vereist\n"); exit(0); } else { pin1=openen(argv[1], "r"); pin2=openen(argv[2], "r"); puit=openen(argv[3], "w"); while ((ch=getc(pin1)) != EOF) putc(ch, puit); while ((ch=getc(pin2)) != EOF) putc(ch, puit); fclose(pin1); fclose(pin2); fclose(puit); } } FILE *openen(pargv, s) char *pargv, *s; { FILE *p; if ((p=fopen(pargv, s)) == NULL) { printf("Er is een probleem met het openen van file %s\n", pargv); exit(1); } else return p; } De namen argv en argc zijn gebruikelijk voor het aantal (count), respectievelijk de pointer (vector) naar argumenten. Hierbij is in argc de programma naam zelf, hier kopieer, meegeteld. Nadat kopieer in1 in2 uit is ingetypt, wordt met de uitvoering van het programma begonnen. Er geldt dan: argc = 4 argv[0] = "kopieer" argv[1] = "in1" argv[2] = "in2" argv[3] = "uit" Het programma zorgt zelf voor een nette foutboodschap als achter kopieer niet precies drie argument worden opgeven of als de file in1 of in2 niet bestaat. Let erop dat argv[0], argv[1], ... strings zijn, maar dat argv zelf een pointer naar een array van strings is. 11.3 ANDERE FUNCTIES Wie in C programmeert, kan gebruik maken van een grote hoeveelheid functies (of macro's) die hij niet zelf hoeft te schrijven. De volgende zijn al bekend en zullen niet opnieuw besproken: getchar, putchar, getc, ungetc, putc, scanf, printf, fscanf, fprintf, sscanf, sprintf, fopen, fclose, fseek, fread, fwrite, ferror, feof, fgets, fputs, gets, puts, malloc, realloc, free, exit. Voor de gebruiker is het meestl niet van belang of hij met een functie dan wel met een macro te maken heeft; we spreken daarom gemakshalve in het vervolg over functies, ook daar waar het macro's betreft. Wel dienen we ons te realiseren dat het gebruik van pointers naar functies beperkt is tot echte functies en dus niet opgaat voor macro's. We zullen nog een aantal van zulke functies noemen. Voor volledige informatie dient men de documen- tatie van de desbetreffende C implementatie te raadplegen. Daarin is tevens te vinden of er voor de te gebruiken functies, met behulp van #include, header-files in het programma vermeld moeten worden, en zo ja, welke. In de documentatie treft men in de regel veel meer functies aan dan de hier genoemde. Er is een selectie gemaakt van functies die algemeen beschikbaar en/of van buitenge- woon belang zijn. We beginnen met enkele functies voor het classeren van karakters. Elk van deze functies geeft voor een gegeven karakter ch het antwoord op de vraag die erbij is vermeld. Het antwoord 'ja' komt zoals gewoonlijk, beschikbaar als een functie waarde ongelijk aan 0; het antwoorde 'neen' als 0. Men dient er er rekening mee te houden dat vereist kan zijn dat #include aan het gebruik van deze functies voorafgaat. isalpha(ch) Is ch alfabetisch (een letter)? isupper(ch) Is ch een hoofdletter? islower(ch) Is ch een kleine letter? isdigit(ch) Is ch een cijfer? isalnum(ch) Is ch een letter of een cijfer? isspace(ch) Is ch een spatie, een tab of een newline? Zo heeft bijvoorbeeld isalpha('*') de waarde 0; daarentegen is isalpha('a') ongelijk aan 0. De volgende functies converteren een karakter in een ander karakter: toupper(ch) maakt van de kleine letter ch een hoofdletter tolower(ch) maakt van een hoofdletter ch een kleine letter Bijvoorbeeld: toupper('a') = 'A' tolower('A') = 'a' Een gebruiker van een functie moet uiteraard van de argumenten en van de functie waarde het type kennen. Deze gegevens kunnen kort en precies worden genoteerd door te laten zien hoe het begin er uit zou zien als we de functie zelf zouden moeten uitschrijven, bijvoorbeeld: int isalpha(ch) char ch; char toupper(ch) char ch; We zullen deze notatie gebruiken voor de volgende functies. char *strchr(str, ch) char *str, ch; levert een pointer op naar de (eerste) positie van ch binnen str. char *strrchr(str, ch) char *str, ch; idem, maar nu de meest rechtse positie van ch binnen str. int strcmp(str1, str2) char *str1, *str2; vergelijkt de strings str1 en str2. In de gevallen <, = en > wordt een integer <0, =0, respectievelijk >0 afgeleverd. int strcmp(str1, str2) char *str1, *str2; int n; Als strcmp, maar nu worden niet meer dan de eerste n karakters van de string vergeleken. char *strcpy(str1, str2) char *str1, *str2; copieert de string str2 in str1 en levert str1 als functiewaarde af. char *strcpy(str1, str2) char *str1, *str2; int n; Als strcpy, maar nu overschrijven alleen de eerste n karakters van str2 die van str1. int strlen(str) char *str; Stringlengte: strlen("ABC") = 3 char *strcat(str1, str2) char *str1, *str2; Plaatst str2 achter str1; str1 moet lang genoeg daarvoor zijn. De functiewaarde is str1. char *strcat(str1, str2) char *str1, *str2; int n; Plaatst de eerste n karakters van str2 achter str1, of kleiner aantal als \0 in str2 eerder wordt bereikt; str1 moet lang genoeg zijn om het resultaat te bevatten. De functiewaarde is str1. int chrcheck() Kijkt of er een karakter is ingetypt, dat nog niet is ingelezen (als dit geval is ,is de functiewaarde ongelijk aan nul). Deze functie is niet overal beschikbaar. srand(seed) int seed; Wordt aangeroepen met een willekeurig integer argument om de functie rand te initialiseren. int rand() Levert en integer 'random number' op. long time(p) long *p; Als p wijst naar de tijd in seconden die verstreken is sinds een vast tijdstip namelijk sinds 1 januari 1970, 0.00 GMT. long *ctime(p) long *p; Als p wijst naar de tijd in seconden die verstreken is sinds 1 januari 1970, 0.00 uur, dan levert deze functie en string van 26 karakters op. Bijvoorbeeld: Mon sep 24 01:03:52 1984\n\0 unlink(str) char *str; Verwijdert de file waarvan de (pad-) naam in str genoteerd is. Voor het gebruik van een aantal mathematische functies kan #include nodig zijn. De belangrijkste fuuncties uit deze categorie zijn: double cos(x) double x; /* cos x */ double sin(x) double x; /* sin x */ double tan(x) double x; /* tan x */ double exp(x) double x; /* exp x */ double log(x) double x; /* ln x */ double log10(x) double x; /* log x (grondtal 10) */ double pow(x,y) double x,y; /* x tot de macht y */ double sqrt(x) double x; /* de vierkantswortel uit x */ double floor(x) double x; /* floor(4.9)=4.0 enz. */ double ceil(x) double x; /* ceil(8.1)=9.0 enz */ int abs(i) int i; /* de absolute waarde van i */ double fabs(x) double x; /* de absolute waarde van x */ double acos(x) double x; /* arccos x */ double asin(x) double x; /* arcsin x */ double cosh(x) double x; /* cosh x */ double sinh(x) double x /* sinh x */ double tanh(x) double x; /* tanh x */ int atoi(str) char *str; /* atoi("12345")=12345 */ long atol(str) char *str; /* atol("1234567")=1234567 */ double atof(str) char *str; /* atof("12.3E-1")=1.23 */