C
PROGRAMMEREN IN
CURSUS VOOR NATUUR- EN STERRENKUNDESTUDENTEN 1998/’99
MAURITS WIJZENBEEK 11 december 2003
ii
Inhoudsopgave
1 2 2.1 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.4 2.4.1 2.4.2 3 3.1 3.1.1 3.1.2 3.1.3 3.2 3.3 4 4.1 4.2 4.2.1 4.2.2 4.3 4.4 4.5 4.5.1 4.6 4.6.1 4.6.2 4.6.3 4.7 4.7.1 4.8 5 5.1
Inleiding en opzet cursus. 1 De computer 3 Digitale en analoge elektronica 3 Opbouw computer 3 Het geheugen 3 De computerbus 4 De processor 5 Randapparatuur 6 De stack 6 Subprogramma’s of functies 7 Getallen en variabelen 7 Meer over reals 8 Hexadecimale weergave van bitpatronen 9 Programmeren 11 Programmeermethodes en -talen. 11 De programmeercyclus 11 Manieren van programmeren. 12 Hogere programmeertalen 12 De programmeertaal C 14 Werken met een editor. 15 Programmeren in C 17 Variabelen 17 Lussen 19 De for-lus. 19 De while- en de do-lus. 21 Voorwaarden, if . . . else . . . ; 22 Eenvoudige invoer 23 Array’s 24 Karakterarray’s en strings 25 Pointers 25 Pointers en array’s 26 Types van pointers 27 De NULL-pointer 27 Functies 28 Functieresultaten en pointers 30 Grafieken en andere plaatjes 32 In- en uitvoer 35 Streams 35 iii
INHOUDSOPGAVE
iv 5.2 5.3 5.4 5.4.1 5.5 6 6.1 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.3 6.3.1 6.4 6.5 6.6 7 7.1 7.1.1 7.1.2 7.1.3 7.2 7.3 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.1 7.5.2 7.6 7.6.1 8 8.1 8.1.1 8.2 8.3 8.4 8.5 A A.1
Soorten in- en uitvoer 36 Karakter I/O 36 Geformatteerde in- en uitvoer 37 Format strings 37 Directe in- en uitvoer met streams 39 Meer over C 41 Assignatie-operator 41 Typeconversies en casts 41 Numerieke waarden 41 Lussen: break en continue 42 Modifiers 43 Switches 43 Enums 44 Structs 45 Typedefs 47 Pointers naar functies 48 Numeriek integreren 49 void pointers en vrij geheugen 49 Bitfields 50 Argumenten bij de aanroep 51 Achtergronden 53 De werking van de C-compiler 53 Preprocessor 53 Compiler 54 Linker of loader 54 Meer over de preprocessor 54 Geheugengebruik 55 Goed programmeren 55 Wat is een goed programma? 55 Inspringen bij een nieuw blok 56 Goed gebruik van variabelen 56 Information hiding 56 Commentaar 56 Bereik en precisie van variabelen 57 Operaties op integers 57 De precisie van real’s 58 Rekentijd 58 Het volume van een 8 dimensionale hyperbol 59 Numerieke natuurkunde 61 Inleiding 61 Voorbeeld: een kaatsende bal 61 Een leeglopend vat 62 Inwendig magneetveld 62 Warmtegeleiding in een staaf 63 Wet van Snellius 64 Syntax 67 Identifiers 67
INHOUDSOPGAVE A.2 A.2.1 A.2.2 A.2.3 A.2.4 A.2.5 A.2.6 A.2.7 A.3 A.4 B B.1 B.2 B.3 C C.1 C.2 C.3 C.4 C.5 D D.1 D.1.1 D.1.2 D.1.3 D.1.4 D.1.5 D.1.6 D.2 D.3 D.3.1 D.3.2 D.4 D.5 D.6 D.6.1 D.6.2 D.6.3 D.6.4 D.6.5 D.6.6 D.6.7 E F G H H.1
Operatoren 67 Rekenkundige operatoren 67 Vergelijkingsoperatoren 67 Toekenningsoperatoren 67 Logische operatoren 68 Bitgewijze operatoren 68 Unaire operatoren 68 Ternaire operator 68 Prioriteiten van operatoren 68 Bitfields 69 In- en uitvoer 71 I/O naar het scherm en van het toetsenbord 71 I/O naar bestanden 71 Data types and format specifiers 71 Tips en veel voorkomende fouten 73 Includefile vergeten 73 Geen prototype 73 Namen in #define’s niet met hoofdletters 73 Niet geinitialiseerde pointers 73 Stack overflow 74 Bibliotheekfuncties 75 In- en uitvoer (stdio.h) 75 Bestanden openen en sluiten 75 Tekst in- uitvoer 75 In- en uitvoer van tekens enz. 76 Binaire in- en uitvoer 76 Positionering in een file 76 Foutafhandeling 76 ctype.h 76 String- en geheugen-functies, string.h 77 String-functies 77 Beheer geheugen 77 math.h 77 stdlib.h 78 Andere functies 78 assert.h 78 setjmp.h 79 signal.h 79 limits.h 79 float.h 79 time.h 79 stdarg.h 79 De GNU C-compiler 81 Het hulpprogramma make 83 Het volume van een hyperbol 85 Gplot 87 Use of the GPLOT package 88
v
INHOUDSOPGAVE
vi H.2 H.2.1 H.2.2 H.2.3 H.3
Functions. 88 Preparation of the plot 88 Plot functions 88 Show plot on screen. 89 gplot.h 90
CE QUI PRODUIT L’ENTHOUSIASME POUR L’ORDINATEUR, CE N’EST PAS SON UTILITE, EFFECTIVE, MAIS LE FAIT QU’IL DONNE A N’IMPORTE QUI L’ILLUSION D’ETRE INTELLIGENT. Jacques ELLUL.
Hoofdstuk 1 Inleiding en opzet cursus.
Computers spelen een belangrijke rol bij onderwijs en onderzoek en het werken met computers en programmeren kan in onze opleiding dan ook niet worden overgeslagen. Deze cursus begint met een korte inleiding in de werking van computers. Daarna zal de programmeertaal C behandeld worden. Na een inleidend college volgen praktica (´ee´ n middag in de week). De gemaakte opgaven moeten worden ingeleverd en worden nagekeken en besproken. Iedere praktica bijeenkomst begint met een bespreking van de te maken opdrachten en ook kunnen dan algemene vragen worden gesteld. Het is belangrijk hierbij aanwezig te zijn. Halverwege het trimester is er een schriftelijke toets die meetelt voor het eind- schriftelijke toets cijfer. Bij de beoordeling zullen zowel de ingeleverde programma’s als de schriltelijke toets worden meegerekend. De cursus is opgezet voor de algehele beginner op computergebied. Om deze voorkennis doelgroep niet af te schrikken en hen geen definitief minderwaardigheidscomplex te bezorgen verzoek ik aan de op dit gebied meer ervaren studenten om hun superioriteit niet teveel te laten blijken en om zich te beperken in het gebruik van jargon. Deze syllabus is bedoeld als leerboek en is minder geschikt als naslagwerk. Ook kan de programmeertaal C hier niet tot in alle details behandeld worden. Ik raad daarom aan een goed boek te kopen1 . boek Een probleem bij het schrijven van een nederlandstalige tekst over computers is het jargon. Het gebruik van de Engelse termen heeft als voordeel dat de lezer de internationale terminologie leert kennen en dat het gebruik van geforceerde vertalingen vermeden wordt. Er bestaan echter voor veel begrippen voortreffelijke Nederlandse woorden en er dan ook is geen reden deze niet te gebruiken. In deze syllabus heb ik getracht een middenweg te bewandelen.
1. B.v. Al Kelly en Ira Pohl, A B OOK ON C (Addison Wesley, ISBN: 0-201-18399-4).
1
2
HOOFDSTUK 1. INLEIDING EN OPZET CURSUS.
Hoofdstuk 2 De computer
2.1
Digitale en analoge elektronica
De elektronische signalen in b.v. audioapparatuur zijn spanningen of stromen waarvan de grootte de waarde van het signaal vertegenwoordigd. Bij computers is dit anders: een signaal kan daar slechts twee waarden hebben: een lage of een hoge spanning, w`el of geen stroom, enz. Een dergelijke eenheid van informatie noemt men een bit . Een bit kan ook een getalwaarde hebben: een hoge spanning bit kan men b.v. ‘1’ noemen en een lage spanning ‘0’, of andersom. Een signaal met maar twee mogelijkheden kan maar weinig informatie bevatten. Daarom wordt gebruik gemaakt van meer bits parallel. Met acht bits kunnen b.v. 256 (28 ) verschillende combinaties van nullen en enen gemaakt worden die gebruikt kunnen worden om de getallen 0 t/m 255 weer te geven: 0000 0000 0000 0000 0000 0111 1000 1111
0000 0001 0010 0100 0101 1111 0000 1111
= 0 kleinste getal 1 2 4 5 127 128 255 grootste getal
byte
Een groepje van acht bits wordt een byte genoemd.
2.2
Opbouw computer
Een computer bestaat uit de volgende onderdelen (zie figuur 2.1): 2.2.1 Het geheugen Het computergeheugen bestaat uit bytes waar een patroon van nullen en enen in kan worden bewaard. De plaatsen in het geheugen zijn genummerd, de z.g. adressen. Met deze adressen wordt bepaald op welke plaats in het geheugen het adres bitpatroon wordt opgeborgen. Deze adressen zijn zelf natuurlijk ook bitpatronen en hebben een getalwaarde. 3
HOOFDSTUK 2. DE COMPUTER
4
Diskette drive
Monitor Toetsenbord
Harde schjf Bus
Geheugen
Processor
Figuur 2.1: Opbouw computer.
megabyte
De grootte van het geheugen, d.w.z. het aantal bytes dat in het geheugen kan kilobyte worden opgeborgen, wordt weergegeven in kilobyte (Kb) of megabyte (Mb). ’Kilo’ betekent hier niet 1000 maar 1024. Dit is gelijk aan 210 en de bytes in een geheugen van e´ e´ n kilobyte kunnen dus geadresseerd worden met een tienbits adres. Het maximaal beschikbare werkgeheugen in klassieke PC’s is 640 Kb, wat gelijk is aan 655360 bytes. E´en megabyte is gelijk aan 1048576 (220 ) bytes. De bitpatronen die in het geheugen worden opgeslagen kunnen van alles betekenen. Een paar voorbeelden:
ASCII
code data
Getallen. Belangrijk zijn positieve gehele getallen, gehele getallen die ook negatief kunnen zijn en getallen met een komma. Deze worden alle verderop besproken. Letters en cijfers: Hiervoor wordt de ASCII–code (American Standard Code for Information Interchange) gebruikt. In deze code zijn ook codes opgenomen voor spatie, tabulatie, nieuwe regel, e´ e´ n positie terug (backspace), de mogelijkheid om een bel te laten klinken (bell) enz. Op deze manier kan tekst in het geheugen worden opgeslagen. Opdrachten, met name opdrachten voor de computer zelf. Een programma is een verzameling van dergelijke opdrachten. Andere mogelijkheden: Allerlei gecodeerde gegevens, zoals windrichting (0000 0000 is noordenwind, 0000 0001 westenwind enz.), tekeningen, muziek enz. De opdrachten voor de computer worden vaak de code genoemd, de gegevens die verwerkt moeten worden zijn de data. Aan de in het geheugen opgeslagen bitpatronen is dus zonder meer niet te zien wat de betekenis is. Dit blijkt pas uit de manier waarop deze gebruikt worden.
2.2.2 De computerbus De bus is de bundel draden die de elementen van een computer met elkaar verbindt en is als het ware de snelweg waarlangs binnen de computer het transport plaatsvindt. Via de bus worden b.v. gegevens vanuit de processor in het
2.2. OPBOUW COMPUTER
5
geheugen opgeborgen of vanuit het geheugen weer naar de processor overgebracht. Er is e´ e´ n busmaster die het verkeer over de bus regelt. Meestal is dit een onderdeel van de processor. Deze geeft aan welk onderdeel van de com- busmaster puter gegevens op de datalijnen van de bus zet en welk onderdeel deze moet opnemen. De bus bestaat uit de volgende lijnen (draden), die ieder dus e´ e´ n bit kunnen vervoeren: Datalijnen. Het aantal datalijnen bepaalt hoeveel bits informatie per keer met de bus kan worden overgebracht. Men noemt dit de breedte van de bus. Het aantal datalijnen van de bus is meestal een veelvoud van acht. Het jargon is hier verwarrend: met data wordt hier alle informatie bedoeld die via de bus wordt uitgewisseld, dus ook de opdrachten voor de processor. Adreslijnen. De signalen op deze lijnen geven aan waar in het geheugen de data moet worden opgehaald of opgeborgen. Ook andere onderdelen van de computer die met de bus verbonden zijn hebben adressen. Stuurlijnen. Deze regelen de timing en geven aan of op het opgegeven adres een lees- of een schrijfoperatie moet worden uitgevoerd.
2.2.3 De processor De processor doet het eigenlijke werk. De opeenvolgende opdrachten voor de processor staan op opeenvolgende plaatsen in het geheugen en worden e´ e´ n voor e´ e´ n over de bus uit het geheugen opgehaald en door de processor uitgevo- programmateller erd. In de programmateller van de processor staat het adres van de plaats in het geheugen waar de eerstvolgende opdracht staat. De executiecyclus van de executiecyclus processor is dus: 1.
2. 3.
De processor vat het getal dat in de programmateller staat op als een geheugenadres en haalt de opdracht die op deze plaats in het geheugen staat op. De programmateller wordt e´ e´ n opgehoogd. De opdracht wordt uitgevoerd.
Er zijn ook processoropdrachten die een nieuwe waarde in de programmateller sprongen plaatsen. Dit heeft als resultaat dat de executie van het programma naar een andere plaats in het geheugen springt. In de opdrachten die op data werken moet aangegeven zijn waar deze gegevens zich in het geheugen bevinden. De processor bevat verder een rekeneenheid en enkele snelle hulpgeheugens, de registers. Naast de gewone registers is er ook een flag register . Ieder bit hiervan heeft een eigen betekenis. Zo is er een bit dat aangeeft of het resultaat van de laatste opdracht nul was, een bit dat aangeeft of dit resultaat negatief was, of het resultaat te groot werd (overflow of carry ) optrad etc. Er zijn instructies die dit bit testen en afhankelijk van het resultaat een andere waarde in de programmateller laden. Men noemt dit een voorwaardelijke sprong : op deze manier kan het verloop van een programma afhangen van tussenresultaten.
registers flag register
voorwaardelijke sprong
6
HOOFDSTUK 2. DE COMPUTER
De elementaire processoropdrachten zijn zeer simpel. Voor een beginner is het instructieset moeilijk om te geloven dat met een dergelijke beperkte instructieset ooit een redelijk programma kan worden gemaakt. Er zijn opdrachten om gegevens van het geheugen naar de registers te schrijven of terug. Verder kan de inhoud van twee registers worden opgeteld, afgetrokken, vermenigvuldigd of gedeeld. Ook kan het bitpatroon van een register naar links of naar rechts worden verschoven en kunnen bepaalde bits van een register 0 of 1 worden gemaakt. Ook zijn er opdrachten om een nieuwe adres in de programmateller te zetten, zodat het programma op een andere plaats in het geheugen verder gaat. 2.2.4 Randapparatuur I/O
interface of controller
De randapparatuur dient voor in- en uitvoer (input/output, I/O in jargon) en voor opslag van gegevens. Omdat in een computer alle datatransport via de bus gaat is ook alle randapparatuur hierop aangesloten. Het onderdeel dat aan de ene kant op de computerbus is aangesloten en dat aan de andere kant een randapparaat aanstuurt heet een interface of een controller. Het meest opvallend zijn het toetsenbord en het beeldscherm. Deze werken onafhankelijk van elkaar: de software moet er voor zorgen dat een aangeslagen toets op het beeldscherm zichtbaar wordt.
harde schijf diskette
Het werkgeheugen van de computer werkt elektronisch zodat alle informatie bij het uitschakelen verloren gaat. Voor permanente opslag van data en programma’s worden daarom harde schijven en diskettes gebruikt. Deze hebben een magnetische opslagmethode. Ze zijn gemonteerd in z.g. drives. Op de schijven worden de gegevens niet per byte geadresseerd maar per blok: een vast aantal bytes wordt in e´ e´ n keer door de diskcontroller zelf van het geheugen naar een blok op de schijf geschreven of terug. Een sterk punt van computers is dat een grote verscheidenheid van randapparatuur te krijgen is: geluidskaarten, muizen, modems, printers enz. Dit maakt het mogelijk om computers direct aan een meetopstelling te koppelen en zo te gebruiken voor de sturing van de opstelling en het registreren van de meetresultaten.
driver
Het stuk programma dat de aansturing van randapparatuur verzorgt wordt een driver genoemd. Zo spreekt men van een printerdriver, een keyboarddriver, enz. 2.2.5 De stack
push pop
stackpointer
Een deel van het geheugen is gereserveerd voor de stack. Men kan zich deze voorstellen als een stapel briefjes voor zaken die onthouden moeten worden: e´ e´ n gegeven per papiertje. Het is alleen mogelijk om iets op een nieuw briefje te schrijven en het bovenop te leggen (push in jargon) of om het bovenste papiertje er af te halen en het te lezen (pop ). Een speciaal register van de processor, de stackpointer, bevat het adres van de eerstvolgende lege plaats. Men zegt dat stackpointer naar deze plaats wijst. Bij
2.3. SUBPROGRAMMA’S OF FUNCTIES
7
een push wordt de op de bergen waarde op dit adres geschreven en wordt vervolgens de stackpointer e´ e´ n opgehoogd. Bij een pop gebeurt het omgekeerde. Een aantal belangrijke machineinstructies maakt gebruik van de stack. Dit zal verderop besproken worden.
2.3
Subprogramma’s of functies
Het komt vaak voor dat een groep opdrachten binnen een programma vaak moet worden uitgevoerd of een aparte taak heeft. Het is dan handig dit stuk programma afzonderlijk te programmeren om herhalingen te voorkomen en om de complexiteit van het programma laag te houden. In C gebeurt dit door de opdrachten onder te brengen in een functie. In het programma staat dan een machineinstructie die de uitvoering van het programma naar het begin van de functie laat springen. Het returnadres wordt op de stack returnadres bewaard. De laatste machineopdracht van de functie laadt de waarde op de top van de stack weer in de programmateller, zodat het programma weer op de oude plaats doorgaan. Grote programma’s bestaan bijna helemaal uit functies. Het ‘hoofdprogramma’ bestaat dan slechts uit een serie functieaanroepen. Door deze opbouw behandelt iedere functie een deelprobleem, wat het ook mogelijk maakt met verscheidene mensen aan e´ e´ n programma te werken. LET OP: met functies wordt in C iets h´ee´ l anders bedoeld dan in de wiskunde!
2.4
Getallen en variabelen
Omdat bij numerieke natuurkunde getallen een belangrijke rol spelen volgen hier enkele korte opmerkingen over de manier waarop deze in een bitpatroon worden weergegeven. Omdat de elektronica van de computer digitaal werkt en alle signalen slechts twee waarden kennen wordt meestal gebruik gemaakt van het tweetallig stelsel. Gehele getallen zonder teken (unsigned integers) Met een rij van n bits kunnen 2n verschillende bitpatronen gemaakt worden, ofwel 2n verschillende gehele getallen worden weergegeven. Als de getallen alleen positieve waarden hoeven te hebben kunnen zo de waarden 0 . . . 2n − 1 worden gemaakt. Men noemt deze unsigned integers. Een 8-bits unsigned integer heeft zo als kleinste waarde 0000 0000 = 0 en als grootste 1111 1111 = 255. 0 → 255 noemt men het bereik. Gehele getallen m`et teken (signed integers) Dit is een notatie waarmee, door de bitpatronen een andere betekenis te geven, ook negatieve getallen kunnen worden gemaakt. Met acht bits wordt het bereik −128 → +127:
HOOFDSTUK 2. DE COMPUTER
8
reals floats
1000 0000 = –128 kleinste getal 1000 0001 –127 1111 1111 –1 0000 0000 0 0000 0001 1 0000 0010 2 0111 1111 127 grootste getal Het belangrijkste bit geeft hier aan of een getal positief of negatief is. Bij deze representaties zijn de bitopraties voor optellen en aftrekken voor getallen met en zonder teken hetzelfde, wat de processorhardware eenvoudiger maakt. Gehele getallen hebben een beperkt bereik. Voor 16-bits getallen met teken is dit −32 768 → +32 767 en voor 32 bits −2 147 483 648 → +2 147 483 647. Karakters Dit zijn codes voor letters, cijfers enz. In C kan er echter mee gerekend worden alsof het hele getallen zijn: als ik drie optel bij de code voor ‘a’ krijg ik de code voor ‘d’. Een karakter heeft acht bits. Getallen met komma (floating point getallen of reals) Getallen die te groot zijn om als geheel getal te worden opgeslagen of getallen met cijfers achter de komma worden opgeslagen in een vorm die meestal real maar in C float genoemd wordt. Een getal wordt daarin weergegeven als een tekenbit en twee integers, de mantisse en de exponent. Het getal -123,456 wordt bijvoorbeeld geschreven als −123456 × 10−3 en opgeslagen als (−) (123456) (−3)
mantisse exponent tekenbit
Het eerste getal heet de mantisse, het tweede de exponent. In dit voorbeeld zijn deze getallen in het tientallig stelsel. Computers werken binair; intern wordt daarom het tweetallig stelsel gebruikt. 2.4.1 Meer over reals In de praktijk gebruikt men (teken) × 2exponent × mantisse × 2−n waarbij teken e´ e´ n bit is (plus of min), de exponent een signed integer en de mantisse een unsigned integer. n is het aantal bits van de mantisse, zodat mantisse× 2−n altijd kleiner is dan e´ e´ n. Reals worden dus opgeslagen als een combinatie van een enkel bit dat het teken aangeeft en twee gehele getallen: (tekenbit) (exponent) (mantisse) Deze representatie is niet eenduidig: een getal houdt dezelfde waarde als het bitpatroon van de mantisse naar rechts wordt geschoven (delen) en de exponent
2.4. GETALLEN EN VARIABELEN
9
wordt opgehoogd (vermenigvuldigen), natuurlijk onder de voorwaarde dat er bij de mantisse rechts alleen nullen uitschuiven. Het is daarom altijd mogelijk om het belangrijkste bit van de mantisse ‘1’ te maken door exponent en mantisse aan te passen. Er is dan een bit precisie te winnen door dit bit weg te laten. normalisatie Deze representatie is w`el eenduidig en heet genormaliseerd. Een probleem is dat genormaliseerde reals de waarde 0 niet kennen. Dit is opgelost door het kleinste positieve getal per definitie de waarde 0 te geven. De grootste positieve waarde die een real kan weergeven heet het bereik en de kleinste waarde waarmee een getal kan worden opgehoogd gedeeld door de waarde is de precisie. bereik Een voorbeeld: Een real heeft vier cijfers voor de mantisse en twee voor de exponent. Het bereik is dan 0.9999 × 1099 en de precisie is 0.0001. Het bereik van reals wordt dus bepaald door het aantal bits van de exponent en de precisie door het aantal bits van de mantisse. Deze twee begrippen zijn belangrijk bij rekenwerk.
precisie
Terwijl integers altijd exact de waarde hebben die ze moeten weergeven is dit bij reals niet altijd mogelijk, omdat vaak moet worden afgerond. Hiermee moet bij het programmeren rekening worden gehouden, b.v. door niet aan te nemen dat twee reals, die dezelfde waarde zouden moeten hebben maar op verschillende manier√zijn berekend, exact gelijk zijn. sin 45◦ hoeft dus niet exact gelijk te zijn aan 1/ 2! 2.4.2 Hexadecimale weergave van bitpatronen Voor mensen is het niet handig om bitpatronen op papier of op het beeld- binair scherm weer te geven in reeksen ‘nullen en enen’ (binair). De menselijke brein is niet geschikt deze te overzien of te onthouden. Een ‘normale’ decimale (10tallige) weergave heeft als bezwaar dat het bijbehorende bitpatroon moeilijk is decimaal te doorzien. Als niet de getalwaarde maar het bitpatroon belangrijk is wordt daarom vaak gekozen voor een hexadecimale (zestientallig) notatie. Het bitpa- hexadecimaal troon word daarvoor van achteren af opgedeeld in blokken van vier bits. Met de vier bits in een blok kunnen 24 verschillende bitpatronen gemaakt worden ofwel de getallen 0 → 15. De getalwaarden van deze blokken worden achter elkaar opgeschreven. De cijfers tien t/m vijftien worden geschreven als a t/m f. Een bitpatroon dat in de computer is opgeslagen als 111101001010100101 wordt dus opgedeeld in 11 1101 0010 1010 0101 en als 3d2a5 geschreven. In C wordt een hexadecimale notatie aangegeven door voor het getal 0x te zetten. Omdat het om bitpatronen gaat en niet om getalwaarden is een negatieve hexadecimale representatie zinloos.
10
HOOFDSTUK 2. DE COMPUTER
Zovele talen iemand spreekt, Zovele malen is hij een mens. Karel V
Hoofdstuk 3 Programmeren
Hoewel het de vraag is of bovenstaande uitspraak ook voor programmeertalen geldt is toch het leren programmeren een onderdeel van iedere opleiding tot natuur- of sterrenkundige. De tijd dat iedere computergebruiker zijn eigen programma’s moest maken is al lang voorbij. Tegenwoordig staan kant en klare programma’s en programmapakketten ter beschikking zoals WordPerfect of MathCad. Zelfgemaakte programma’s hebben echter het voordeel dat ze beter zijn aangepast aan het probleem en vaak sneller zijn. Commerci¨ele programma’s moeten geschikt zijn voor iedereen; dit maakt ze ingewikkeld en traag, als ze al te gebruiken zijn voor net dat ene probleem waar een oplossing voor gevonden moet worden. Het nadeel van zelf programmeren is dat het veel tijd kost. Voor kleine programma’s van een paar bladzijden valt dit wel mee, maar voor een middelgroot programma moet toch al gauw drie maanden tot een half jaar worden uitgetrokken. Het loont dan ook de moeite om zo veel mogelijk gebruik te maken van het programmeerwerk van anderen en programmatheken te gebruiken voor bijvoorbeeld rekenwerk of grafische weergave. Ook dit zal in deze cursus ter sprake komen.
3.1
Programmeermethodes en -talen.
Programmeren wordt geleerd door het te doen en veel fouten te maken. Dit laatste is geen probleem, zodat iedereen het kan leren. Het doel is om een reeks processoropdrachten te maken die, nadat ze in het geheugen geladen zijn, automatisch achter elkaar door de processor worden uitgevoerd en het gewenste resultaat leveren. 3.1.1 De programmeercyclus Een programma wordt meestal gemaakt in tekstvorm. Dit gebeurt met een pro- editor gramma dat editor genoemd wordt. De nieuw in te typen of te veranderen tekst staat bij het editten (wie weet een beter Nederlands woord?) in het geheugen. Als de tekst klaar is wordt deze naar een file op de schijf weggeschreven. De als tekst geschreven opdrachten worden met een vertaalprogramma omgezet naar 11
12
programmeercyclus
HOOFDSTUK 3. PROGRAMMEREN
machinecode en deze weer als file op de schijf opgeborgen. Hoopvol probeert de programmeur vervolgens of het programma ‘loopt’. Meestal zitten er echter nog fouten in, zodat, na enig nadenken, de programmatekst met de editor veranderd wordt, opnieuw wordt vertaald enz. Deze cyclus van editten . . . vertalen . . . testen . . . editten . . . heet de programmeercyclus . 3.1.2 Manieren van programmeren. Het schrijven van een programma kan op verschillende manieren:
compiler
Machinecode. De programmeur schrijft direct de instructies voor de processor. Deze manier van programmeren gebeurt alleen bij hoge uitzondering. Het is lastig en tijdrovend, niet alleen omdat iedere vergissing fataal is, maar ook omdat, als ergens een paar instructies worden tussengevoegd en de rest van de code opschuift, alle adressen die verderop staan niet meer kloppen en de verwijzingen daarnaar moeten worden bijgewerkt. Assembler. Assembler is de programmeertaal die geheel aan de processor is aangepast: iedere processor heeft zijn eigen assembler. E´en opdracht in assembler komt overeen met e´ e´ n opdracht in machinecode. De in assembler geschreven programmatekst wordt door een vertaalprogramma, verwarrenderwijs ook assembler geheten, vertaald in machinecode. De instructies hebben namen wat het programma voor mensen beter leesbaar maakt. Ook plaatsen in het geheugen worden met een naam (identifier) aangegeven. De assembler (het vertaalprogramma) beslist welk adres hierbij hoort. Het voordeel van assembler is dat de programmeur alles in de hand heeft en geheel vrij is om van alle mogelijkheden van de computer gebruik te maken. Een nadeel is dat hij niet alleen alles k´an doen, maar ook alles zelf m´oet doen. Het schrijven van grote programma’s in assembler is dan ook extreem tijdrovend. Interpreters. Hierbij wordt de programmatekst niet vertaald, maar opdracht na opdracht ingelezen door een interpreter -programma en uitgevoerd. BASIC is het meest bekende voorbeeld. Het programmeren is in dergelijke talen vaak gemakkelijk, maar de executiesnelheid kan traag zijn. Hogere talen. Ook bij hogere talen wordt tekst vertaald in machinecode, maar hier wordt e´ e´ n opdracht (statement) vertaald naar m´ee´ r machineopdrachten. Het vertaalprogramma wordt hier compiler genoemd en het vertalen van de programmatekst (de source ) naar machinecode heet compileren. 3.1.3 Hogere programmeertalen Een hogere programmeertaal is goed omschreven, wat maakt dat de programmeur bij een vergissing direct op zijn vingers wordt getikt. Voor beginners is dat hinderlijk, maar later leert men het waarderen omdat het domme fouten voorkomt. Ook hebben alle hogere talen voorzieningen om programma’s in delen te schrijven, ev. door verschillende programmeurs, en die later samen te voegen. Zo zijn er bibliotheken met kant en klare programmadelen beschikbaar die een gebruiker in zijn programma kan invoegen.
3.1. PROGRAMMEERMETHODES EN -TALEN.
13
Voor alle hogere talen geldt een internationale standaard die het mogelijk zou moeten maken dat programma’s die in deze talen geschreven zijn zonder aanpassingen gedraaid kunnen worden op alle computers waarvoor een compiler beschikbaar is. In de praktijk valt dit tegen, o.m. omdat veel compilers, naast de standaard, nog een paar handige extra’s hebben en veel programmeurs de verleiding niet kunnen weerstaan om deze te gebruiken. Ook is b.v. de grootte van de integers (b.v. 16, 32 of 64 bits) en het bereik van reals machineafhankelijk, zodat een programma op een andere machine niet zonder meer dezelfde resultaten hoeft te geven. Een aantal hogere talen wordt intensief gebruikt. Discussies over welke pro- taalkeuze grammeertaal de beste is hebben vaak een emotioneel verloop, waarschijnlijk omdat iedereen toch het liefst wil blijven werken in de taal die hem vertrouwd is. In principe kan in iedere programmeertaal alles worden geprogrammeerd en de verschillen lijken op het eerste gezicht vaak onbelangrijk. Toch kunnen kleine verschillen maken dat het in de praktijk met de ene taal veel prettiger werken is dan met de andere. Vaak hoort men dat een taal ‘effici¨enter’ zou zijn. Nu zijn er twee soorten ef- effici¨entie fici¨entie: effici¨entie in programmaexecutie en effici¨entie bij het programmeren. In het verleden waren computers en rekentijd duur, computers traag en het geheugen in de computers beperkt. Goed programmeren hield toen in dat programma’s snel waren en weinig geheugen nodig hadden. De tijden zijn echter veranderd. Computers zijn nu snel en goedkoop en het beschikbare geheugen is minder snel een beperking. Het is nu de tijd van de programmeur die duur is: hoe minder tijd aan het programmeren en het latere onderhoud hoeft te worden besteed hoe beter, ook al loopt het programma wat langzamer en heeft het meer geheugen nodig. De executiesnelheid hangt bovendien niet alleen van de taal af, maar ook van executiesnelheid de compiler. TurboPascal b.v. is een compiler die prettig snel compileert, ook door weinig tijd te besteden aan optimalisatie. Een met de Microsoft-C compiler gecompileerd C programma loopt twee maal zo snel. Dit zegt echter meer over deze compilers dan over C en Pascal. De talen die door natuur- en sterrenkundigen gebruikt worden zijn voornamelijk: Fortran is de oudste taal voor wetenschappelijk rekenwerk en wordt nog steeds veel gebruikt, ook omdat voor deze taal goede rekenbibliotheken beschikbaar zijn. De syntax van Fortran is echter zodanig dat bij een aantal voor de hand liggende fouten of vergissingen er toch nog correct Fortran ontstaat, zodat de programmeur bij het compileren niet gewaarschuwd wordt door een foutmelding. Het kan veel tijd kosten om deze fouten te vinden. Pascal is ontworpen als een taal om studenten ‘netjes’ te leren programmeren. De mogelijkheden zijn daarom vaak hinderlijk beperkt en een aantal, misschien minder nette maar wel praktische, mogelijkheden is niet toegestaan. Dit geeft Pascal iets frikkerigs. Alle bruikbare Pascalcompilers hebben daarom extra mogelijkheden, wat de standaardisatie en daarmee de
HOOFDSTUK 3. PROGRAMMEREN
14
overdraagbaarheid niet ten goede komt. Een bekende Pascalcompiler voor PC’s is TurboPascal van de firma Borland. Deze werkt erg prettig, maar staat dan ook ver van standaard-Pascal af. C is goed gedefinieerd, heeft veel mogelijkheden en is geschikt voor grote programma’s. Belangrijker is echter dat deze taal veel wordt gebruikt omdat hij veel wordt gebruikt. Computerfabrikanten zorgen ervoor dat voor hun computers goede C-compilers beschikbaar zijn en programmeurs kiezen C omdat hun programma’s dan op veel typen computers gebruikt kunnen worden. C is een praktische taal en laat ook constructies toe die niet ‘mooi’ zijn, maar wel handig. Voor wetenschappelijk rekenwerk is C langzamerhand Fortran aan het verdringen. Een nadeel van C is dat het meer dan twintig jaar oud is en dat C daardoor slecht is aangepast aan moderne eisen. Zo zijn de voorzieningen voor het schrijven van programma’s voor een windows systeem niet in de standaard opgenomen. Men moet daarbij nu gebruik maken van systeemafhankelijke eisen, wat de overdraagbaarheid tussen b.v. MS-Windows en XWindows lastig maakt.
3.2
De programmeertaal C
Programma’s voor wetenschappelijk onderzoek worden soms lang gebruikt en kunnen een paar generaties computers overleven. De aanpassingsproblemen die dan steeds ontstaan als een programmma weer moet worden aangepast aan een modernere computer zijn beduidend minder voor C-programma’s door de goede standaardisatie en omdat, door de vele mogelijkheden van C, het gebruik van computerafhankelijke trucs niet nodig is. K&R C
ANSI C
In 1978 verscheen het boek T HE C PROGRAMMING L ANGUAGE van Brian Kernighan en Dennis Ritchie. Dit boek is jarenlang de facto de standaard geweest, het z.g. K&R C, en heeft door zijn heldere uitleg ongetwijfeld bijgedragen tot de populariteit van C. In 1983 stelde het American National Standards Institute een commissie in om een C-standaard te maken waarin ook nieuwere idee¨en over programmeren en programmeertalen verwerkt moesten worden. Deze is nu klaar en dit z.g. ANSI-C wordt in deze cursus behandeld. Omdat programma’s die geschreven zijn in K&R C ook verwerkt moeten kunnen worden met een ANSI-C compiler zijn bepaalde verouderde constructies uit K&R C nog steeds toegestaan. Zij zullen hier niet behandeld worden. De tweede druk van T HE C PROGRAMMING L ANGUAGE behandeld ANSI-C. Een bezwaar van C is dat het langzamerhand aardig bejaard is en dat b.v. de benodigdheden voor het maken van programma’s voor een window systeem (Xwindows, Microsoft Windows) niet in de standaard zijn opgenomen. De programmeur moet hiervoor gebruik maken van systeemafhankelijke bibliotheken.
C++
Een verdere ontwikkeling van C is het door Bjarne Stroustrup ontwikkelde C++. Deze ondersteunt z.g. object-geori¨enteerde programmering (wat dit is doet hier niet ter zake) en ook de in- en uitvoer is handiger. Te verwachten is dat
3.3. WERKEN MET EEN EDITOR.
15
C++ op den duur C zal overvleugelen, al zullen de meeste gebruikers niet alle mogelijkheden benutten.
3.3
Werken met een editor.
Editors zijn er in vele soorten en iedere programmeur heeft zijn eigen voorkeur. Editors werken op ‘platte tekst’, d.w.z. op tekst die alleen de standaard ASCII karakters bevat en waarvan alle tekens op het scherm zichtbaar zijn. Dit in tegenstelling tot tekstverwerkingsprogramma’s zoals WordPerfect waar, onzichtbaar tussen de leesbare tekst, aanwijzingen staan voor de gebruikte letters, de printer enz. Hoewel de editor de tekst waarmee hij werkt in regels op het scherm laat zien staat deze in werkelijkheid achter elkaar in het geheugen, gescheiden door ‘nieuwe regel’ karakters. Als zo’n karakter wordt weggehaald worden dus twee regels samengevoegd. Alle editors hebben dezelfde basiscommando’s, hoewel de vorm waarin ze gegeven moeten worden verschilt. Daarnaast heeft iedere editor zijn eigen speciale mogelijkheden. De basiscommando’s zijn (de voorbeelden gelden voor PC-editors): Een file ‘openen’. Dit kan een bestaande file zijn die veranderd moet worden of een nieuw te maken file. Een file ‘saven’. Het (tussen) resultaat wordt weer op de schijf geschreven onder de oude naam. Vaak wordt de oude versie ook op de schijf bewaard, b.v. in een file met de extensie .bak. De cursor verplaatsen. De cursor geeft aan op welke plaats de tekst veranderd wordt. Verplaatsen van de cursor geeft dus geen verandering van de tekst. Dit kan b.v. met een muis of met de pijltjes- en PageUp / PageDown -toetsen op het toetsenbord. Een karakter toevoegen. Dit kan op twee manieren: door een karakter tussen te voegen (insert), of door het karakter op de plaats van de cursor te overschrijven (overstrike of replace). Op PC’s wordt dit omgeschakeld met de Insert -toets. Een karakter weghalen. Dit kan op twee manieren: o´ f het karakter op de plaats van de cursor wordt weggehaald met Delete o´ f het karakter links van de cursor met Backspace . Als deze toetsen een paar maal worden ingedrukt verdwijnt in het eerste geval de tekst rechts van de cursor en in het tweede geval de tekst links. Een regel tekst weghalen. Zoeken en ev. veranderen. Hierbij moet eerst de tekst opgegeven worden waarnaar wordt gezocht. De cursor springt dan naar de eerstvolgende plaats in de tekst waar deze karaktercombinatie voorkomt. Ook is het mogelijk om de tekst te veranderen. Dit kan ook automatisch met e´ e´ n opdracht voor de hele tekst. Blokken. Een gedeelte van de tekst, een z.g. blok kan een blok worden gemarkeerd en in zijn geheel verwijderd, gekopieerd, of verplaatst.
insert overstrike replace delete backspace
16
HOOFDSTUK 3. PROGRAMMEREN
Neem de tijd om een editor uit te proberen en ‘in je vingers’ te krijgen. Meestal is een ‘help’ aan te roepen, op PC’s met F1 , op UNIX met ? of h en kunnen daarin de opdrachten worden gevonden.
C is not a big language, and is not well served by a big book. Kernighan and Ritchie
Hoofdstuk 4 Programmeren in C
Een gecompileerd programma bevat opdrachten voor de processor, de z.g. code code en de data die moeten worden verwerkt. Deze zijn ook in het niet-vertaalde pro- data gramma terug te vinden. Daarnaast staan in de programma-tekst aanwijzingen voor de compiler. Het is belangrijk om bij het schrijven van een programma goed te weten wat een opdracht moet doen en of het een opdracht is voor de compiler of is een opdracht in het uiteindelijk uit te voeren programma.
4.1
Variabelen
Na het compileren is een variabele een plaats in het geheugen waar een gegeven wordt opgeslagen. In een programma is de definitie van een variabele is een opdracht aan de compiler om in het uiteindelijke programma een geheugenplaats te reserveren. Een variabele wordt gedefinieerd : definitie float versnelling; Dit is een variabele van het type float, d.w.z. een variabele waarvan de waarde in de vorm van een getal met een komma is opgeslagen, en met de naam ‘versnelling’. Met de in de variabelen opgeborgen waarden kan ik m.b.v. operatoren zoals ‘+’ en ‘–’ iets doen. De belangrijkste operator is ‘=’. Deze heeft niets te maken met het ‘=’-teken in de wiskunde, maar is de opdracht om een getal in een variabele op te bergen: versnelling = 9.81; of, nadat ik ook de variabelen plaats en tijd heb gedefinieerd en er de waarden aan heb gegeven: plaats = 0.5 ∗ versnelling ∗ tijd ∗ tijd; Het is beter om hier niet ‘de versnelling is 9.81’ te zeggen maar ‘de versnelling wordt 9.81’. Het aan een variabele toekennen van een waarde wordt assignatie assignatie genoemd. Het gedeelte links van het ‘=’-teken heet de left value, het gedeelte left value rechts, dat een getalwaarde heeft, is een expressie. Omdat het ‘=’-teken een expressie 17
HOOFDSTUK 4.
18
PROGRAMMEREN IN C
operator is vormt dat de hele opdracht (left value) = (expressie) ook weer een expressie, en heeft dus ook een getalwaarde. Het volgende programma rekent uit hoeveel 2 maal 3.5 is en laat het resultaat op het scherm zien. De regelnummers horen niet bij de programmatekst maar zijn toegevoegd voor de uitleg.
1
#include <stdio.h>
2 3
main()
4
{
5
float a, res;
6
a = 2;
7
res = 3.5 ∗ a;
8
printf("Twee maal drie en een half is %f\n", res);
9 10 11
return 0; }
Opmerkingen: • • • • • • •
•
Opdrachten (statements) eindigen op ‘;’. Regel 1 is daarop een uitzondering. Dit wordt later uitgelegd. De executie van het programma begint altijd bij main(). De in main() uit te voeren opdrachten staan tussen ‘{’ en ‘}’. Regel 5: definitie van twee variabelen met namen a en res waarin een ‘getal met een komma’ kan worden opgeslagen. Regel 6: berg het getal 2 op in de variabele a. Regel 7: reken uit hoeveel 3.5 maal de waarde in a is en berg het resultaat op in de variabele res. Regel 8: printf laat het de tekst tussen de dubbele aanhalingstekens (string) op het scherm zien en vervangt daarbij %f door de waarde van res. \n is het teken voor een nieuwe regel. Regel 10: wordt later behandeld.
Opdracht 1. Test dit programma.
constante
De getallen die in het programma voorkomen, zoals b.v. 3.5 op regel 7, worden constanten genoemd. Ook constanten hebben een type: ‘3’ is een int, een type variabele waarin gehele getallen worden opgeslagen, en 3.5 is een float. De code voor een karakter wordt aangeduid met het karakter tussen enkele quotes, b.v. int i;
4.2. LUSSEN
19
... i = 'a'; Met deze waarden kan gewoon gerekend worden. De volgende opdracht laat de letter ‘c’ op het scherm zien: printf("%c\n", 'a' + 2); Opdracht 2. Laat op het scherm de letters ‘A’, ‘B’, ‘.’, ‘nieuwe regel’ en ‘spatie’ op het scherm zien, samen met de intergerwaarde van hun code.
4.2
Lussen
4.2.1 De for-lus. C kent opdrachten om een deel van een programma herhaald uit te voeren. De for -lus wordt in C het meest gebruikt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <stdio.h> main() { int i, som = 0; for (i = 2; i < 8; i = i + 1) { som = som + i; printf("i = %d, som = %d\n", i, som); } printf("De gemiddelde waarde van de gehele" " getallen 2 t/m 7 is %d / %d = %d\n", som, i − 2, som / (i − 2)); return 0; }
Opmerkingen: Regel 5: Definitie van de integer variabelen i en som. som krijgt de beginwaarde 0. De beginwaarde van i is ongedefinieerd. Regel 7: Dit is een for-lus : het programmadeel tussen de accoulades, de lus, wordt een aantal malen uitgevoerd. Achter for staan, tussen de haakjes, drie opdrachten, gescheiden door puntkomma’s. De eerste wordt uitgevoerd voordat het lussen begint: de variabele i krijgt de waarde 0. De middelste is een test die steeds plaatsvindt v´oo´ r de lus wordt uitgevoerd: zolang i kleiner is dan 7 gaat het lussen door. De derde opdracht wordt steeds aan het einde van iedere lus uitgevoerd: hier wordt i met e´ e´ n opgehoogd.
HOOFDSTUK 4. PROGRAMMEREN IN C
20
Regel 13: In een string, tussen de dubbele aanhalingstekens, mag niet met een nieuwe regel worden begonnen. Als een string te lang wordt en van het scherm afloopt kan hij op deze manier over meer regels worden verdeeld. Let op: om bij de uitvoering van het programma een nieuwe regel te krijgen moet het ‘nieuwe regel karakter’ (\n) worden gebruikt. Regel 13: De gebruikte variabelen i en som zijn integers, d.w.z. variabelen waar een geheel getal in wordt opgeborgen. Dit maakt dat het resultaat van de deling som / i ook een geheel getal is en dat de cijfers achter de komma worden verwaarloosd. In dit programma kunnen ook variabelen van het type float (getallen met komma) gebruikt worden i.p.v. int’s (gehele getallen). In de printfopdracht moet dan %f moeten staan i.p.v. %d. Opdracht 3. Test het bovenstaande programma. Is het antwoord goed?
Als het gemiddelde van een andere rij getallen zou moeten worden uitgerekend, b.v. 5 t/m 17, op welke plaatsen moet het programma dan worden aangepast? voorwaarde
De test of het uitvoeren van de lus moet worden voortgezet, wordt de voorwaarde genoemd. Dit is een bewering die waar is of onwaar. Voorbeelden: a < 10 a <= 10 a == 10 a != 10 enz.
waar onwaar
waar als de waarde van a kleiner is dan 10 waar als a kleiner of gelijk is aan 10 waar als a gelijk is aan 10 waar als a ongelijk is aan 10
Waar en onwaar hebben in C een numerieke waarde en kunnen dus ook in een (integer) variabele worden opgeborgen: a = 5; b = a < 10; c = a > 10; printf("‘Waar is %d, onwaar is %d\n", b, c); C kent ook operatoren die op de waar en onwaar werken, zoals: a && b a || b !a
waar als a en b beide waar zijn waar als a waar is o´ f als b waar is o´ f als ze beide waar zijn. waar al a niet waar is en niet waar als a waar is.
Een lijst van operatoren is te vinden in aanhangsel A. In veel programmeertalen bestaat een type boolean, een type variabele dat slechts de waarden waar en onwaar kan aannemen. In C is dit anders, waar en onwaar hebben in C een getalwaarde: onwaar is gelijk aan nul en waar ongelijk nul. Dit maakt het volgende programma mogelijk om de tafels van 1 t/m 10 af te drukken:
4.2. LUSSEN
21
#include <stdio.h> main() { int i, j; for (i = 10; i; i = i − 1) { for (j = 10; j; j = j − 1) printf(" %3d", i ∗ j); printf("\n"); } return 0; }
De format specifier %3d geeft aan dat uit te printen integer minstens drie posities breed moet zijn, zodat de getallen netjes onder elkaar komen. Opdracht 4. Het vorige voorbeeld laat de tafels van tien tot e´ e´ n zien i.p.v. de tafels van e´ e´ n tot tien. Verander het om het goed te krijgen. 4.2.2 De while- en de do-lus. De while- en de do- lussen zijn eenvoudiger dan de for-lus.
1
#include <stdio.h>
2 3
main()
4
{
5
int i = 0;
6 do {
7 8
printf("eerste lus: i = %d\n", i);
9
i = i + 1; } while (i < 5);
10 11
while (i < 10) {
12 13
printf("tweede lus: i = %d\n", i);
14
i = i + 1;
15
}
16
return 0;
17
}
HOOFDSTUK 4. PROGRAMMEREN IN C
22 Opmerkingen: •
Regel 7: do-lus: de test of moet worden doorgegaan gebeurt iedere keer aan het einde van de lus. Na do, while, for, enz. mag als lus e´ e´ n statement staan of meer statements tussen ‘{}’. Dit heet een blok of een compound statement. Om het programma beter leesbaar te maken (voor mensen, niet voor de computer) is het een goed gebruik om de programmatekst te laten inspringen. Regel 12: while-lus: de test of moet worden doorgegaan is aan het begin van de lus.
• blok compound statement
•
Opdracht 5. Bereken de waarde van n! met n = 0, 1, 2, . . . voor die waarden van n waarbij het resultaat kleiner is dan 100 000.
4.3
Voorwaarden, if . . . else . . . ;
Met if en else kunnen delen van het programma worden uitgevoerd of overgeslagen.
#include <stdio.h> main() { int te testen = 100; if (te testen > 50) printf("Groter dan vijftig"); else printf("Kleiner dan vijftig"); if (te testen) printf(" en ongelijk nul\n"); if (!te testen) printf(" en gelijk nul\n"); return 0; }
Na een if kan dus een else komen maar dit hoeft niet. Een else hoort altijd bij de het laatst daarvoor staande if. If – else constructies kunnen berucht moeilijk leesbaar worden gemaakt. Het onderstaande is een voorbeeld van een zeer slecht geschreven programma. Ook de layout is geheel in stijl.
4.4. EENVOUDIGE INVOER
23
#include <stdio.h> main() { int eerste, tweede, derde, vierde; printf("Type vier getallen in ... "); scanf("%d, %d, %d, %d", &eerste, &tweede, &derde, &vierde); if (eerste == 30 && tweede == 10) if (!vierde) {if (derde < 7 || eerste >= 19) printf("Goedzo\n"); else if (eerste ∗ tweede < 65) printf("Dat valt tegen\n"); else if (tweede ∗ derde) if (derde < 15) printf("Ziezo\n");} else printf("Wat nu?\n"); else printf("Ik raak de draad kwijt\n"); return 0; }
Opdracht 6. Maak het bovenstaande programma beter leesbaar door inspringen en het gebruik van accoulades. Probeer of beide versies hetzelfde resultaat geven.
4.4
Eenvoudige invoer
Tijdens het uitvoeren van het programma kunnen getallen worden ingetypt en in een variabele worden opgeborgen met scanf.
#include <stdio.h> main() { float f, g;
do { printf("\nVermenigvuldigen, type twee getallen in ? scanf("%f %f", &f, &g); printf("Het resultaat is %f\n", f ∗ g); } while (f ∗ g); return 0; }
");
HOOFDSTUK 4. PROGRAMMEREN IN C
24
Let vooral op &f en &g. Later zal scanf() nader worden behandeld. Opdracht 7. Maak een programma dat test of ingetypte getallen deelbaar zijn door 19. Be¨eindig het programma als een getal deelbaar is door 13.
C kent een operator om de rest van een deling te bepalen: %.
4.5 array-index
Array’s
Een array is een rij variabelen van hetzelfde type met e´ e´ n gezamenlijke naam en een index nummer. Een voorbeeld maakt dit duidelijk.
#include <stdio.h> int array1[5], array2[5]; main() { int i; array1[0] =
4;
array1[1] = 17; array1[2] = 28; array1[3] = 8; array1[4] = 57; for (i = 0; i < 5; i++) array2[i] = array1[i] ∗ 3; for (i = 0; i < 5; i++) printf("3 * %d = %d\n", array1[i], array2[i]); return 0; }
Hier wordt twee array’s van vijf integers gedefinieerd met de namen array1 en array2. In de beide for-lussen wordt de variabele i gebruikt als index. LET OP: bij een array met n elementen (hier dus vijf) loopt de index van 0 tot n − 1. Array’s kunnen ook worden ge¨ınitialiseerd. Het bovenstaande voorbeeld wordt eenvoudiger door het array te definieren met int array1[] = {4, 17, 28, 8, 57}; Als grootte van het array niet tussen de rechte haken is opgegeven wordt deze bepaald door het aantal ge¨ınitialiseerde elementen.
4.6. POINTERS
25
Ook meerdimensionale array’s zijn mogelijk, b.v. double tweedim[5][6]; double driedim[5][6][5]; Opdracht 8. Bepaal de grootste, de kleinste en het gemiddelde van tien ingetypte getallen. Sla de ingetypte getallen eerst op in een array. 4.5.1 Karakterarray’s en strings Een char is een acht bits integer die meestal wordt gebruikt om een karaktercode in op te slaan. We kunnen een char-array definieren en initialiseren met char naam[] = {'P', 'i', 'e', 't', '\0'); ‘’\0’’ is het nul-karakter en heeft de getalwaarde nul. Het moet hier worden gebruikt om het eind van de naam aan te geven. Een dergelijke rij symbolen, afgesloten door een nul-karakter heet een string en komt zo vaak voor dat er een aparte notatie voor is: char naam[] = "Piet"; Deze string heeft dus vijf elementen. Als wij in ons programma iets met een string willen doen moet dit karakter voor karakter gebeuren. Het volgende voorbeeld kopieert een string: char naam1[] = "Piet", naam2[20]; int i; ... for (i = 0; naam2[i] = naam1[i]; i++); ... De voorwaarde van de for-lus wordt hier gebruikt om het karakter te kopi¨eren. i++ betekent hetzelfde als i = i + 1. Het bovenstaande is een voorbeeld van de compacte programmeerstijl die met C mogelijk is. Voor iemand die niet goed in C thuis is kan een zo geschreven programma moeilijk leesbaar zijn.
4.6
Pointers
Pointers zijn variabelen waarin een adres wordt opgeborgen. Ze worden in C veel gebruikt. In het volgende voorbeeld wordt het adres van i opgeborgen in de pointer p en wordt vervolgens de waarde van de variabale waar p naar ‘wijst’ (i dus) drie maal zo groot gemaakt: i = 10; p = &i;
HOOFDSTUK 4.
26
PROGRAMMEREN IN C
∗p = ∗p ∗ 3; printf("i is nu %d\n", i); &i is dus het adres van i. Omdat i een vaste plaats in het geheugen heeft, en dus een vast adres, is &i een constante. Een pointervariabele wordt gedefinieerd door ‘*’ voor de naar te zetten. Met int ∗p; wordt een pointer genaamd p naar een integer gedefinieerd. 4.6.1 Pointers en array’s In C is het gebruik van pointers nauw verbonden met het gebruik van array’s. Dit komt door een eigenaardigheid van C die op het eerste gezicht vreemd aandoet. De elementen van een array kan ik gebruiken d.m.v. de index: aaa[0] is het eerste element van het array aaa, aaa[1] het tweede element enz. Wat betekent het nu als ik in een programma aaa schrijf, dus zonder de rechte haken? Je zou verwachten dat daarmee het hele array bedoeld wordt en dat, als aaa en bbb array’s zijn van hetzelfde type en met dezelfde lengte, de opdracht bbb = aaa; alle elementen van aaa naar bbb zou kopi¨eren. Ik sommige programmeertalen is dit ook zo, maar niet in C: aaa betekent hier het adres waar het array aaa begint. Dit geeft verrassende mogelijkheden. Als ik b.v. een array nodig heb van negen elementen waarvan de index van -4 tot +4 loopt i.p.v. van 0 tot 8 kan ik dit maken met: float mijnarray[9], ∗p; ... p = &mijnarray[4]; ... for (i = −4; i <= +4; i++) p[i] = i; pointerarithmetic
Omdat het type variabele waar een pointer naar wijst bekend is wordt hier rekening mee gehouden bij het berekenen van een adres (pointerarithmetic): met een opdracht p++ kan ik door een array heenstappen en i.p.v. p = &mijnarray[4]; kan ik ook p = mijnarray + 4; schrijven. Het bovenstaande geldt ook voor strings: char ∗naam; int c; ... p = "Jan Klaassen";
4.6. POINTERS
27
c = p[3]; In c staat nu de code voor een spatie. Een doordenkertje, en volgens mij ook een voorbeeld van slecht programmeren, is de volgende manier om een string op het scherm te laten zien (putchar(c) toont e´ e´ n symbool): for (i = 0; c = "Deze string wordt geprint\n"[i]; i++) putchar(c); 4.6.2 Types van pointers Alle adressen in het geheugen hebben natuurlijk dezelfde vorm en het adres van een int heeft natuurlijk dezelfde vorm als het adres van een float. Toch moet bij de definitie van een pointer worden opgegeven naar wat voor soort variabele het erin opgeslagen adres wijst. Dit is niet alleen om fouten te voorkomen, maar ook om het goede resultaat te geven bij het berekenen van de adressen van arrayelementen.
int
aa[10];
double ∗pd; ... pd = aa;
/∗ poging om het adres van een int op te bergen in een pointer naar een double; compiler geeft een waarschuwing ∗/
... for (i = 0; i < 10; i++) ∗pd++ = 0;
Omdat een double meer geheugenruimte inneemt dan een int gaat de for-lus helemaal mis. 4.6.3 De NULL-pointer Een pointer kan een waarde NULL krijgen. Vanzelfsprekend is dit geen zinnig adres, maar er kan w´el op worden getest, b.v. om te zien of een pointer al een zinnige waarde gekregen heeft: float ∗p = NULL; ... if (!p) printf("p heeft nog geen zinnige waarde" " gekregen.\n"); ... Men spreekt altijd over de NULL-pointer terwijl dit eigenlijk het NULL-adres zou moeten zijn. Er zijn nog andere toepassingen van NULL die verderop ter sprake zullen komen.
HOOFDSTUK 4. PROGRAMMEREN IN C
28
4.7
Functies
Het is mogelijk om een deel van een programma af te splitsen en afzonderlijk te programmeren. Zo’n deel heet een functie. Een functie in C is dus heel iets anders dan een functie in de wiskunde. Het volgende voorbeeld laat zien hoe een functie met de naam ‘kwadraat’ geprogrammeerd en gebruikt wordt. Eigenlijk is een dergelijke functie, die maar uit e´ e´ n opdracht bestaat, in de praktijk zinloos: het is duidelijker en handiger om die ene opdracht gewoon in het hoofdprogramma te zetten. Meestal zullen functies veel langer zijn. argument functiewaarde
Bij de sprong naar een functie kunnen waarden worden meegegeven, de argumenten. Bij de sprong terug kan maximaal e´ e´ n waarde worden meegenomen, de functiewaarde.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include <stdio.h> double x, b; double kwadraat(int x) { x = x ∗ x; return x; } main() { b = 5.0; x = kwadraat(b + 10.0); printf("Resultaat: %f\n", x); return 0; }
Regel 5 t/m 10: Definitie van de functie ‘kwadraat’ met e´ e´ n argument met de naam ‘x’. De returnwaarde is een double en het type van het argument is een integer. Functies zijn herkenbaar aan de vorm ‘naam(. . . )’. Ook als een functie geen argumenten heeft zijn de haakjes verplicht. Regel 5: Het argument x is ge¨ınitialiseerd op de waarde die het bij de aanroep heeft meegekregen, maar kan verder worden gebruikt als een gewone variabele. x bestaat alleen binnen de functie. In dit programma is n´og een variabele gedefinieerd die x heet (regel 3), zodat een conflict dreigt. Binnen de functie wordt echter met x altijd het argument van de functie bedoeld, buiten de functie de x van regel 3. Men noemt dit de scope van een variabele.
4.7. FUNCTIES
29
Regel 8: De in de functie berekende waarde wordt teruggegeven aan het ‘hoofdprogramma’, d.w.z. dat deel van het programma vanwaar de functie werd aangeroepen. Regel 14: Hier wordt de functie aangeroepen met als argument het resultaat van ‘b + 10.0’. Als de uitvoering van het programma de functie ‘binnenkomt’ heeft x dus de waarde 15. De waarde die de functie teruggeeft wordt opgeslagen in de x van regel 3. Regel 18: Kennelijk is main() zelf ook een functie die hier de waarde nul teruggeeft. Hiermee wordt aan het bedrijfssysteem, b.v. UNIX of Windows, gemeld dat de uitvoering van het programma geen problemen gaf. Een andere waarde, naar keuze van de programmeur, betekent dat er iets mis ging. Het volgende programma laat meer eigenschappen van functies zien.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#include <stdio.h> unsigned long faculteit(unsigned long arg); main() /∗∗∗∗/ { unsigned long invoer, resultaat; int i; i = scanf("%lu", %invoer); if (i != 1) { printf{"Sukkel, je tikte onzin in.\n" "Doe het verder maar zelf.\n"); return 1; } resultaat = faculteit(invoer); printf("%d! = %ld\n", invoer, resultaat); return 0; } unsigned long faculteit(unsigned long arg) { unsigned long resultaat; if == 0 || n == 1) return 1; else { resultaat = arg ∗ faculteit(arg − 1); return resultaat; }
30
HOOFDSTUK 4. PROGRAMMEREN IN C
Waarde van scanf(). scanf() geeft als functiewaarde het aantal variabelen dat met succes een waarde gekregen heeft. Dit kan gebruikt worden om te testen op fouten bij de invoer. Prototype. In dit programma wordt de functie faculteit gedefinieerd n´a de functie main() waarin deze wordt aangeroepen. Dit maakt dat, als de compiler regel 18 compileert, nog niet bekend is wat voor type faculteit heeft en wat de argumenten zijn. In regel 3 staat een z.g. prototype waarin de compiler deze gegevens kan vinden. Achter een prototype staat altijd een puntkomma. Automatische variabelen. In regel 28, in de functie faculteit() wordt een variabele resultaat gedefinieerd. Een dergelijke variabele bestaat slechts zolang het blok waarin de definitie staat wordt uitgevoerd en heeft dus, als de functie opnieuw wordt aangeroepen, niet meer zijn oude waarde. Recursiviteit. Omdat bij aanroep een functie nieuwe en eigen argumenten en variabelen krijgt is het in C mogelijk recursieve programma’s te maken. Zo wordt hier de faculteit van een getal berekend door een functie zichzelf (in regel 31) te laten aanroepen. Doordenkertje. Het type ‘void’. Niet altijd heeft een functie een argument en niet altijd geeft een functie een waarde terug. Hiervoor wordt het type ‘void’ gebruikt, b.v. bij de definitie van een functie die een variabele met e´ e´ n ophoogt: int i = 0; ... void i plus 1(void) { i++; } Opdracht 9. Schrijf en test een functie die, gegeven de twee parameters die een rechte lijn definieren, het snijpunt van deze rechte lijn met de x-as berekent. Let op pathologische invoer.
Opdracht 10. Schrijf een functie die het oppervlak berekent van een regelmatige N -hoek waarvan de afstand van het midden tot de hoekpunten gelijk is aan 55 cm.
Gebruik deze functie om na te gaan hoe snel, voor stijgende waarden van N , de oppervlakte nadert tot het oppervlak van een cirkel. 4.7.1 Functieresultaten en pointers Bij de aanroep van een functie worden de waarden van de argumenten eerst gekopieerd en vervolgens doorgegeven. Dit maakt dat vanuit een functie de argumenten waarmee de functie wordt aangeroepen niet kunnen worden veranderd. Dit heeft tot gevolg dat een functie maar e´ e´ n waarde kan teruggeven, de functiewaarde.
4.7. FUNCTIES
31
Dit kan omzeild worden met pointers. Het volgende programma heeft een func- pointers als functieargument tie die het kwadraat, de derde en de vierde macht teruggeeft van het eerste argument.
#include <stdio.h> void machten{double x, double ∗p2, double ∗p3, double ∗p4) { ∗p2 = x ∗ x; ∗p3 = x ∗ ∗p2; ∗p4 = x ∗ ∗p3; return; } main() { double a, m2, m3, m4; a = 7; machten(a, &m2, &m3, &m4); printf("Het kwadraat van %f is %f.\n" "De derde en vierde macht zijn %f en %f\n", a, m2, m3, m4); return 0; }
Opdracht 11. Test het bovenstaande programma. Op deze manier wordt in C ook een array als argument aan een functie worden doorgegeven. De volgende functie hoogt b.v. alle elementen van het array ‘aaa’ met e´ e´ n op:
#define N 5 ... int aaa[N] = {10, 11, 12, 13, 14}; ...
HOOFDSTUK 4. PROGRAMMEREN IN C
32
void hoogop(int n, int ∗p) { int i; for (i = 0; i < n; i++) p[i]++; } main() { ... hoogop(N, aaa); ... }
4.8 Gplot
Grafieken en andere plaatjes
Voor het maken eenvoudige twee- en driedimensionale plaatjes kan gebruik worden gemaakt van het G PLOT-pakket. Hiermee kunnen zowel uitgerekende data als wiskundige functies geplot worden. Hieronder volgt een voorbeeld waarin de functie sin(x) en een set data berekend volgens cos(x) in e´ e´ n grafiek worden afgebeeld. Voor verdere informatie zie de appendix.
/∗ Demonstratieprogramma Gplot. ∗/ #include <stdio.h> #include <math.h> #include "gplot.h" #define N
50
#define MIN −10.0 #define MAX 20.0 float x[N], y[N]; main() { int i; float stap, hoek = MIN; gpl func("sin(x)", "Sin"); stap = (MAX − MIN) / (N − 1);
4.8. GRAFIEKEN EN ANDERE PLAATJES
33
for (i = 0; i < N; i++, hoek += stap) { x[i] = hoek; y[i] = con(hoek); } gpl data(N, x, y, "Cos"); gpl show();
/∗ laat de figuren op het scherm zien
∗/ return 0; }
#include <math.h> Deze file met prototypes enz. van wiskundige functies moet hier worden tussengevoegd. #define #define definieert een macro: overal waar in de tekst ‘N’ staat wordt dit vervangen door ‘100’. Dit heeft als groot voordeel dat als het hier nodig mocht zijn om een grafiek met b.v. 60 punten te maken alleen deze #define veranderd hoeft te worden. Hetzelfde geldt voor MAX en MIN. Komma operator. Op deze manier kunnen twee opdrachten worden geschreven komma operator op een plaats waar er maar e´ e´ n is toegestaan. De waarde van de opdracht is de waarde van het rechter deel. De komma-operator wordt meestal gebruikt bij for-lussen.
34
HOOFDSTUK 4. PROGRAMMEREN IN C
Hoofdstuk 5 In- en uitvoer
Zie voor gedetailleerde gegevens over I/O-functies enz. de appendix of een C-boek.
5.1
Streams
Voor in- en uitvoer maakt C gebruik van streams. Een stream is een ‘stroom’ gegevens die bij de uitvoering van een programma het programma verlaat of er wordt binnengehaald. Een invoer-stream komt b.v. van het toetsenbord of van een bestand, een uitvoer-stream kan naar het beeldscherm gaan of, b.v. de resultaten van een berekening, in een bestand worden weggeschreven. Een stream moet worden geopend met fopen() met opgave de naam van het stream bestand en of het om in- of uitvoer gaat. fopen() geeft als functiewaarde een filepointer terug. Dit is een pointer naar het adres van gegevens over de stream. fopen() Een stream kan worden geopend in verschillende modes, zoals lezen ("r"), filepointer schrijven ("w") of toevoegen ("a"). Mislukt het openen van een bestand, b.v. omdat het niet bestaat, dan geeft fopen() het NULL-adres terug. In het volgende voorbeeld wordt de file gegevens.dat geopend voor uitvoer.
... FILE ∗fp; ... fp = fopen("gegevens.dat", "w"); ...
35
HOOFDSTUK 5. IN- EN UITVOER
36
Aan het eind van een programma worden alle streams automatisch gesloten. Dit kan ook met fclose(fp), wat soms nodig is als naar een bestand weggeschrefclose() ven gegevens door het programma zelf weer moeten worden ingelezen. Bij de start van een programma worden drie streams geopend: stderr
stdin Invoer van het toetsenbord, standaardinvoer. stdout stdout Uitvoer naar het beeldscherm, standaarduitvoer. stderr Uitvoer naar het beeldscherm voor foutmeldingen of andere mededelingen.
5.2
Soorten in- en uitvoer
C kent drie soorten I/O: Karakter I/O In- en uitvoer van afzonderlijke karakters. E´en karakter komt overeen met e´ e´ n byte (8 bits). Geformatteerde I/O Dit is in- en uitvoer in de vorm van (door mensen leesbare) tekst. De interne (binaire) representatie wordt ‘vertaald’ naar tekst of van tekst vertaald naar de interne representatie. Directe I/O Hier worden direct de interne binaire waarden weggeschreven of ingelezen. Geformatteerde I/O is gemakkelijk te lezen en te controleren, zowel door mensen als door andere programma’s. Dit maakt het gemakkelijker om fouten te vinden en om de resultaten verder te verwerken. Een nadeel is dat de vertaling van en naar tekst rekentijd kost, wat bij programma’s die veel gegevens verwerken goed te merken is. Directe I/O is sneller omdat de getallen net zo als ze in het programma zijn opgeslagen worden weggeschreven en ingelezen. Dit is snel en compact, maar moeilijk controleerbaar en de uitvoer is beperkt bruikbaar door andere programma’s.
5.3 I/O, karakters
EOF
stdin
Karakter I/O
Met getchar() of getc(fp) worden karakters (char’s) stuk voor stuk ingevoerd. char’s zijn altijd 8 bits met bereik 0 . . . 255. Deze functies zijn integerfuncties, zodat de mogelijkheid bestaat om ook andere waarden terug te geven, met name de end of file (EOF) code. Het volgende voorbeeld telt de hoofdletters in de file ‘invoer.txt’:
#include <stdio.h> FILE ∗fp; int hoofdletters = 0, totaal = 0; int c;
5.4. GEFORMATTEERDE IN- EN UITVOER
37
char ∗naam = "invoer.txt"; main() { fp = fopen(naam, "r"); if (!fp) { fprintf(stderr, "De file ‘%s' bestaat niet.\n", naam); return 1; } while ((c = getc(fp)) != EOF) { totaal++; if (c >= 'A' && c <= 'Z') hoofdletters++; } printf("De file ‘%s' bestaat uit %d symbolen, " "waarvan %d hoofdletters\n", naam, totaal, hoofdletters); return 0; }
ungetc(c, fp) duwt het karakter c terug in de invoer. Bij de eerstvolgende ungetc() getc(fp) komt het dan weer tevoorschijn. Er kan maar e´ e´ n karakter worden teruggeduwd. Het is in C gebruik om karakters als int’s te behandelen, alleen de opslag, b.v. in een string, is als 8-bit char’s. Opdracht 12. Maak een programma dat alle invoer van het toetsenbord op het scherm laat zien totdat een ‘x’ wordt ingetypt.
5.4
Geformatteerde in- en uitvoer
5.4.1 Format strings fprintf(), scanf() enz. hebben als een van hun argumenten een format- format string string. Hierin staat hoe de vertaling van variabele naar tekst moet gebeuren. Bij de printf()-familie geeft dit weinig problemen, alleen moet het goede scanf() ‘vertaal-symbool’ worden gekozen, %d voor integers, %f voor doubles en floats enz. (zie aanhangsel), maar bij scanf() enz. is het soms ‘a pain in the neck’ en het goed lezen van de documentatie kan veel ellende besparen. De scanf()-familie kent drie functies: scanf() Leest van standaardinvoer (toetsenbord).
38
HOOFDSTUK 5. IN- EN UITVOER
fscanf() Leest van stream. sscanf() Leest van een string. Een paar voorbeelden van veel voorkomende scanf()-problemen: wit
Bij "%d" wordt er eerst het ‘wit’ overgeslagen. ‘Wit’ zijn spaties, tabs, nieuweregel en nieuwe-pagina symbolen. Dit is handig omdat het in een bestand met getallen dan niet uitmaakt hoe het bestand er verder uitziet. Vervolgens worden de cijfers van het getal ingelezen totdat er een symbool gelezen wordt dat duidelijk niet bij het getal hoort. Dit symbool wordt vervolgens ‘teruggeduwd’ in de invoer en komt de volgende keer als eerste aan de beurt. Bij "%d " maakt de extra spatie dat, na het omzetten van de int, wit wordt overgeslagen tot het eerste ‘niet-wit’ karakter. De arme gebruiker van het programma moet maar begrijpen dat hij of zij eerst een ‘niet-wit’ symbool moet intypen voordat scanf() begrijpt dat er geen wit meer komt. Een ander probleem ontstaat als bij het intypen van een getal een symbool wordt ingevoerd dat geen cijfer is en ook geen wit. Dit wordt gezien als het einde van het cijfer en teruggeduwd. Het volgende cijfer begint met dit symbool, de invoer mislukt en het symbool wordt weer teruggeduwd, enz. enz.: het programma ‘hangt’.
gets() sscanf()
Een betere manier is om eerst een regel tekst in te lezen met gets() of fgets() en deze met sscanf() te verwerken. sscanf() doet hetzelfde als scanf() maar haalt de invoer van een string i.p.v. van het toetsenbord. Als sscanf() niet de verwachte invoer vindt kan, na een foutmelding, om nieuwe invoer worden gevraagd:
... char buf[80]; int i, eerste, tweede, derde; ... do { printf("Voer drie integers in op een regel: "); gets(buf); i = sscanf(buf, "%d%d%d", &eerste, &tweede, &derde); if (i != 3) printf("Invoer fout, opnieuw.\n" } while (i != 3); ...
Opdracht 13. Maak met een programma een tekstbestand waarin de getallen 1 t/m 100 staan, ieder getal op een nieuwe regel.
Maak vervolgens een tweede programma dat de eerste honderd getallen van een bestand inleest en dat een nieuw bestand maakt met op iedere regel een ingelezen getal en het drievoud daarvan. Maak dit programma zodanig dat het niet vastloopt bij slechte invoer.
5.5. DIRECTE IN- EN UITVOER MET STREAMS
5.5
39
Directe in- en uitvoer met streams
Voor UNIX geldt dat bij het openen van een bestand voor binaire gegevens niet directe I/O hoeft te worden opgegeven dat het om een binair bestand gaat. Bij sommige binaire I/O systemen moet echter i.p.v. "w", "r" en "a" voor binaire files "wb", "wr" en ¨ab" als argument voor fopen() worden opgegeven. Bij directe I/O worden de elementen van een array weggeschreven of ingelezen. fread() De functies fread() en fwrite() hebben beide vier argumenten: 1. 2. 3. 4.
Een pointer naar het array. De grootte (in bytes) van de objecten, b.v. een double. Het aantal objecten dat gelezen of geschreven moet worden. De filepointer.
fwrite()
De functiewaarde is het werkelijke aantal gelezen of geschreven objecten. Hierop kan worden getest om te zien of alles goed is gegaan. De grootte (in bytes) van een object kan gevonden worden met de sizeof- sizeof operator operator. Deze geeft het aantal bytes dat een variabele in het geheugen inneemt. Opdracht 14. Schrijf een programma dat de grootte (in bytes) laat zien van variabelen van het type char, short int, int , long, float en double.
40
HOOFDSTUK 5. IN- EN UITVOER
Hoofdstuk 6 Meer over C
6.1
Assignatie-operator
In C, in tegenstelling tot veel andere programmeertalen, is ‘=’ een operator. Daarom is een opdracht als a = b + c ook in zijn geheel een expressie en heeft dus ook een waarde. We kunnen dus schrijven d = a = b + c; waardoor zowel a als d de waarde b + c krijgen en ook opdrachten als a = (b = c + d) − e; zijn toegestaan. Dit soort constructies kan een programma moeilijk leesbaar maken. Duidelijker is (misschien) b = c + d; a = b − e;
6.2
Typeconversies en casts
6.2.1 Numerieke waarden Om b.v. een int in een double op te bergen moet de waarde eerst naar het andere type worden geconverteerd. Dit gebeurt vaak automatisch: double d; int i = 5; ... d = i; doet wat men er van verwacht. Bij operatoren moet beter worden opgelet. Een vermenigvuldiging van twee integers is een heel andere operatie als het vermenigvuldigen van twee doubles. De belangrijkste regels zijn: 41
HOOFDSTUK 6. MEER OVER C
42 (int) * (int) (int) / (int) (int) * (float) (int) / (float) (int) * (double) (int) / (double) cast
→ (int) → (int) → (float) * (float) → (float) / (float) → (double) * (double) → (double) / (double)
→ (float) → (float) → (double) → (double)
Een conversie kan worden opgelegd met een cast.
int i = 3, j = 2; double d; ... d = i / j;
/∗ dit maakt d gelijk aan 1 ∗/
d = i / (double) j
/∗ dit maakt d gelijk aan 1.5 ∗/
Een cast kan ook worden gebruikt voor het converteren van pointers, b.v.
int i, ∗pi; double ∗pd; ... pi = (int ∗) pd;
/∗ conversie van een pointer naar een double naar een pointer naar een integer ∗/
i = (double) pi;
/∗ conversie van een pointer naar een integer naar een double; Het geeft vermoedelijk onzin, maar het mag ∗/
Casts zijn geheel voor de verantwoordelijkheid van de programmeur en als het resultaat onzin is en problemen geeft moet hij niet klagen. 6.2.2 Lussen: break en continue Met break kan een lus tussendoor verlaten worden. continue maakt dat een lus met een nieuwe slag begint. Voorbeeld:
/∗ Deze functie telt hoevaak de letters ‘a’ t/m ’k’ voorkomen en breekt af na de letter ‘z’.∗/ int tel ak(void) {
6.2. TYPECONVERSIES EN CASTS
43
int aantal = 0; for (;;) {
/∗ oneindige lus ∗/
int c = getchar(); if (c == 'z') break;
/∗ beeindig lussen ∗/
if (c < 'a' || c > 'k') continue; aantal++; } return aantal; }
6.2.3 Modifiers Bij de definitie van een variabele kan voor de type-definitie nog een modifier geplaatst worden. De kunnen zijn unsigned Een unsigned variabele heeft alleen positieve waarden. Dit wordt gebruikt bij integral types en is vooral handig bij b.v. tests: als de waarde van een variabele tussen 0 en 10 moet liggen hoeft alleen maar getest te worden op de bovengrens. Ook is het positieve bereik twee maal zo groot. const Een const variabele, niet verwarren met een constante, is een variabele waarvan de waarde niet veranderd mag worden. Als dit in een programma toch gebeurt geeft de compiler een foutmelding. extern Een extern definitie geeft aan dat de variabele ergens anders, b.v. in een andere file, is gedefinieerd. volatile Bij het compileren wordt het programma geoptimaliseerd. Hierbij kan het voorkomen dat de waarde van een variabele enige tijd in een register van de processor wordt opgeslagen en pas later weer teruggeschreven. Dit kan mis gaan als een interrupt functie deze variabele ook gebruikt. volatile wil zeggen dat de kans bestaat dat de waarde van een variabele tussendoor verandert en dat deze niet tijdelijk op een andere manier mag worden bewaard. register Alleen voor automatische variabelen. Geeft aan dat de waarde van een variabele, indien mogelijk, in een register van de processor moet worden bewaard. Over het algemeen heerst de mening dat dit beter door de optimalisator van de compiler geregeld kan worden, maar in speciale gevallen kan het gebruik hiervan toch een grote verbetering geven.
6.2.4 Switches Met if ... else kan onderscheid worden gemaakt tussen twee gevallen. Bij meer gevallen kunnen if-fen en else-en worden toegevoegd, maar dit wordt snel ingewikkeld en onleesbaar. Duidelijker is een switch. Het volgende voorbeeld gaat na welk getal wordt ingetypt.
HOOFDSTUK 6. MEER OVER C
44
#include <stdio.h> main() { int i; char buf[90]; while (gets(buf)) { if (sscanf(buf, "%d", &i) != 1) { printf("Je typte geen getal in.\n"); continue; } switch (i) { case 5:
printf("Je typte het getal 5 in.\n"); break;
case 3: case 2: case 1:
printf("Je typte 1, 2 of 3 in.\n"); break;
default: printf("Je typte een getal in.\n"); } }
/∗ einde switch ∗/ /∗ einde while-lus ∗/
return 0; }
break
Let op het gebruik van break.
sscanf() gets()
sscanf() heeft als functiewaarde het aantal geconverteerde waarden. gets() geeft het adres van het char-array als alles goed gaat en de NULL-pointer als het inlezen van de regel niet lukt, b.v. bij Einde data. Dit wordt in UNIX vanaf het toetsenbord opgegeven met Ctrl-D en op PC’s met Ctrl-Z . 6.2.5 Enums Een enum is een integer variabele waarvan de waarden een naam gekregen hebben, b.v.
... /∗ maak nieuw type ‘kleur’ ∗/ enum kleur {ROOD, GROEN, BLAUW, WIT, GEEL}; /∗ definieer twee variabelen van het type kleur ∗/
6.2. TYPECONVERSIES EN CASTS
45
enum kleur stoel, tafel; ... stoel = GROEN; tafel = ROOD; ...
ROOD heeft hier de waarde 0, GROEN is 1 enz. Wij kunnen ook andere waarden nemen met
... enum kleur {ROOD = 5, GROEN, BLAUW, WIT, GEEL}; ...
waardoor ROOD 5 is, GROEN 6, enz. Het is ook toegestaan om een enum zonder typenaam te specificeren: Er kunnen dan geen variabelen van dit type worden gedefinieerd, maar de waarden mogen in iedere expressie worden gebruikt: enum {ROOD, GROEN, BLAUW, WIT, GEEL}; ... int i = GROEN + 2; i krijgt hier de waarde 3. Enums zijn bedoeld om een programma beter leesbaar te maken. 6.2.6 Structs Een struct is een blok informatie die als e´ e´ n geheel behandeld kan worden. Het volgende voorbeeld geeft een struct waarin de prestaties van een voetbalspeler wordt bijgehouden en geeft een paar voorbeelden van het gebruik.
... /∗ definitie van de struct ∗/ struct speler { char ∗naam, /∗ naam voetbalspeler ∗/ char ∗functie; double salaris, rendement; unsigned int doelpunten[20], /∗ doelpunten per wedstrijd ∗/ aantalwedstrijden; } ... struct speler Jansen; /∗ definieer speler en ∗/
HOOFDSTUK 6. MEER OVER C
46
struct speler team[11];
/∗ een team van elf spelers ∗/
... Jansen.naam
= "Piet Jansen"
/∗ Het element ‘naam’ van de struct ‘Jansen’ van het type ‘struct speler’. ∗/
Jansen.functie = "spits"; ... /∗ Definieer een functie met een functie als argument en een struct als functiewaarde ∗/ struct speler meetrendement(struct speler s) /∗ bereken het totale aantal doelpunten ∗/ { unsigned int totaal = 0; int i; for (i = 0; i < s.aantalwedstrijden; i++) { totaal += s.doelpunten[i]; } s.rendement = totaal / s.salaris; return s; } ... for (i = 0; i < 11; i++) { team[i] = meetrendement(team1[i]); } ...
pointer struct ‘->’ operator
naar
Functies kunnen dus van het type ‘struct’ zijn en structs kunnen als argument aan een functie worden meegegeven. Vaak wordt echter als argument de pointer naar een struct gebruikt. Om de elementen van een struct te kunnen bereiken via een pointer is er zelfs een speciale operator ‘− >’:
... struct speler ∗p1, ∗p2; ... void rendement(struct speler ∗p) /∗ bereken het totale aantal doelpunten gedeeld door het salaris ∗/ { unsigned int totaal = 0; int i; for (i = 0, totaal = 0; i < p−>aantalwedstrijden; i++) { totaal += p−>doelpunten[i];
6.2. TYPECONVERSIES EN CASTS
47
} p−>rendement = totaal / p−>salaris; } ... p1 = team; for (i = 0; i < 11; i++) { meetrendement(p1++); }
Structs kunnen zeer nuttig zijn om bij elkaar behorende gegevens bij elkaar te houden. Een voordeel is ook dat er later gemakkelijk een element aan een struct kan worden toegevoegd zonder dat dit daarvoor het hele programma moet worden omgeschreven. Het gebruik van structs vereist een zorgvuldige opzet van de datastructuur van een programma. 6.2.7 Typedefs Met typedef kan aan een bestaand type een nieuwe naam gegeven worden.
... typedef struct { double temp; double druk; double weerstand[10]; double gem weerstand; } Meetpunt; ... Meetpunt serie1[50]; /∗ een Meetpunt middel(Meetpunt t); ...
/∗ temperatuur
∗/
/∗ gemiddelde weerstand
∗/
array voor vijftig meetpunten ∗/ /∗ functie prototype ∗/
Behalve een betere leesbaarheid en het gemak dat niet steeds struct hoeft te worden ingetypt heeft dit nog een ander voordeel. Een programma is b.v. geschreven voor 32 bits int’s: typedef int INT; Als dit programma gebruikt moet worden voor een systeem waarbij de integers 16 bits en de longs 32 bit zijn hoeft alleen deze typedef veranderd te worden: typedef long INT; en alle variabelen van het type INT zijn weer 32 bits. Met typedef kunnen in C geen nieuwe types worden gespecificeerd, maar kunnen alleen bestaande typen een nieuwe naam krijgen. Dit kan het programmeren handiger maken, maar komt de leesbaarheid niet altijd ten goede.
HOOFDSTUK 6. MEER OVER C
48
6.3
Pointers naar functies
In natuurkundig rekenwerk wordt vaak gebruikgemaakt van pointers naar functies. Een ‘pointer naar een functie’ bevat dus het adres van de plaats in het geheugen waar een functie begint. Voorbeeld: De definitie double ddd(double a); geeft het prototype van de functie ddd(). double (∗pfunc)(double p); definieert een pointer pfunc naar een functie: (*pfunc)() is een functie, dus pfunc is de pointer naar een functie. Om aan pfunc een waarde toe te kennen zijn zowel pfunc = &ddd; als pfunc = ddd; toegestaan: de compiler weet dat ddd een functie is en begrijpt dat met ddd (zonder haakjes) alleen als een pointer naar deze functie bedoeld kan zijn. pf = &ddd();
/* FOUT */
geeft onzin omdat de haakjes maken dat de functie wordt aangeroepen met als waarde niet het adres van de functie maar zijn functiewaarde. In het bovenstaande voorbeeld is pfunc een pointer naar een functie met een double als functiewaarde en een double als argument. Pointers naar functies worden gebruikt als functieargument. Bij natuurkundig rekenwerk komt het vaak voor dat een oplossing gevonden wordt door het bepalen van een nulpunt van een functie. Hiervoor kan een algemeen bruikbare nulpuntszoekfunctie worden geschreven, die dan de functie waarvan het nulpunt gezocht wordt als argument meekrijgt. Ander voorbeeld:
... typedef double (∗Ipfunc)(double); /∗ Ipfunc is het type van een pointer naar een functie ∗/ ... double kwadraat(double d) {return d ∗ d;} double derdemacht(double d) {return d ∗ d ∗ d;} ... /∗ som(): laatste argument is pointer naar functie ∗/
6.4. VOID POINTERS EN VRIJ GEHEUGEN
49
double som(int n, double ∗pd, Ipfunc pf)) { int i; double som = 0; for (i = 0; i < n; i++) som += (∗pf)(pd[i]); return som; } ... /∗ bereken de som van de kwadraten van de doubles in een array + de som van de derdemachten ∗/ resultaat = som(N, array, kwadraat) + som(N, array, derdemacht); ...
Bij het lezen van een programma kan het handig zijn als zelfgedefinieerde typen direct als zodanig herkenbaar zijn, b.v. door hun namen met een hoofdletter te laten beginnen. 6.3.1 Numeriek integreren De integralen van functies die analytisch niet integreerbaar zijn kunnen altijd met numerieke methodes worden benaderd. Een simpele methode is de trapeziummethode. Hierbij wordt het gebied waarover ge¨ıntegreerd moet worden opgedeeld in een aantal kleine gebieden en wordt in ieder gebied de functie benaderd door een rechte lijn: Z b x=a
f (x) dx ≈
N −1 X i=1
f (xi ) + f (xi+1 ) (xi+1 − xi ) 2
met x1 = a en xN = b. Opdracht 15. Schrijf de functie double integraal(double onder, double boven, double (∗pfunctie)());
die een door middel van de pointer pfunctie opgegeven functie tussen de grenzen onder en boven integreert met de trapeziummethode. Test de nauwkeurigheid van het resultaat.
6.4
void pointers en vrij geheugen
De grootte van array’s moet in het programma worden opgegeven. Als b.v. een groter array nodig is moet het programma opnieuw worden gecompileerd. Het
HOOFDSTUK 6. MEER OVER C
50
lijkt verstandig om dan maar flink grote array’s te nemen, maar dit kan er toe leiden dat het programma langzamer loopt of op computers met weinig geheugen niet meer kan worden gebruikt. malloc() NULL-pointer
C kent de mogelijkheid tijdens de uitvoering van het programma geheugen ‘aan te vragen’ met de functies malloc() of calloc(). De waarde die deze functies teruggeven is het adres waar het geheugen begint of de NULL-pointer als er niet voldoende vrij geheugen meer beschikbaar is. Het type van deze pointer is void, d.w.z. een adres naar iets wat niet nader is gespecificeerd. double ∗p; ... /∗ vraag geheugen aan voor een array van 100 doubles ∗/ p = (double ∗)malloc(100 ∗ sizeof double); if (!p) { fprintf(stderr, "Geen vrij geheugen beschikbaar\n"); exit(1); /∗ beeindig het programma ∗/ } for (i = 0; i < 100; i++) p[i] = 0; ... /∗ geef het geheugen weer vrij ∗/ free(p);
free()
Vergeet niet het geheugen weer vrij te geven. Het volgende voorbeeld kan een multi-user systeem voor alle gebruikers lam leggen: for (i = 0; i < 1000; i++) { p = (double ∗)malloc(10000 ∗ sizeof double); ... /∗ doe wat nuttigs ... ∗/ /∗ ... maar geef het geheugen niet vrij ∗/ }
Omdat p steeds een nieuwe waarde krijgt kan het eerder aangevraagde geheugen niet meer gebruikt worden en ook niet meer worden vrijgegeven.
6.5
Bitfields
De afzonderlijke bits van variabelen van een integral type1 kunnen ook met een identifier worden aangeduid. Dit kan handig zijn bij het aansturen van randapparatuur (interfacing). Voorbeeld: Definitie van de variabele vlaggen met afzonderlijk te zetten bits. 1. Een van de integerachtige types char, short, int of long.
6.6. ARGUMENTEN BIJ DE AANROEP
51
struct { unsigned int vlag0 : 1;
/∗ eerste bit
∗/
unsigned int vlag1 : 1;
/∗ tweede bit
∗/
: 3; unsigned int getal : 2;
/∗ drie bits overgeslagen ∗/ /∗ twee bits getal
∗/
} vlaggen;
Bit 1 van vlaggen wordt nu b.v. gezet met vlaggen.vlag1 = 1; Voor verdere informatie zie een C-boek.
6.6
Argumenten bij de aanroep
Bij veel programma’s moeten bij de aanroep argumenten worden opgegeven, b.v. copy filea.txt fileb.txt Hier krijgt het programma copy twee argumenten mee, kennelijk de namen van bestanden. Ook C kent deze mogelijkheid:
#include <stdio.h> main(int argc, char ∗argv[]) { int i; for (i = 0; i < argc, i++) printf("Argument %d: %s\n", argv[i]); return 0; }
De functie main() kan twee argumenten meekrijgen: de eerste is e´ e´ n meer dan het aantal bij de aanroep ingetypte argumenten. De tweede is een array van pointers naar strings. Deze strings zijn de argumenten. Argument 0 is de naam van de programmafile. Opdracht 16. Maak een programma dat de tafel van een bij de aanroep opgegeven getal laat zien.
52
HOOFDSTUK 6. MEER OVER C
Hoofdstuk 7 Achtergronden
7.1
De werking van de C-compiler
Zoals alle talen kent C voorzieningen om programma’s uit meer dan e´ e´ n file op te bouwen. Dit wordt gebruikt bij het maken van grote programma’s: het is onmogelijk om met verschillende programmeurs aan e´ e´ n file te werken. Verder is dit belangrijk om een programma overzichtelijk te houden. Dit kan o.m. bereikt worden door het programma op de splitsen in, zo los mogelijk van elkaar staande, deelprogramma’s, zoals in- en uitvoer, grafisch werk enz. en deze in aparte files onder te brengen. Op deze manier wordt het risico verminderd dat een kleine verandering in e´ e´ n deel van het programma onvoorziene gevolgen heeft in een ander deel. Ook bij het maken van kleine programma’s wordt gebruik gemaakt van onderdelen en functies die dan in een halfgecompileerde vorm (spaart compilatietijd) in bibliotheken bewaard worden. Belangrijk is de standaardbibliotheek die een onderdeel is van de compiler en waarvan altijd onderdelen aan het programma worden toegevoegd. Bibliotheken Object files
‘Included’ files
? Brontekst-
?
Preprocessor
-
Compiler
Objectfile
-
?
Linker
Executeerbare file -
Figuur 7.1: Schematische opbouw C-compiler Een C-compiler bestaat uit de volgende onderdelen: 7.1.1 Preprocessor Deze geeft een voorbehandeling aan de tekst: commentaar wordt verwijderd, .h-bestanden ingevoegd enz. Opdrachten voor de preprocessor staan in regels die beginnen met #. De invoerfiles hebben de extensie .c of .h. De .h-files headerfiles worden include- of header-files genoemd. Het is de gewoonte dat zij geen executeerbare opdrachten bevatten, maar declaraties en definities, b.v. van bibliotheekfuncties. 53
HOOFDSTUK 7. ACHTERGRONDEN
54 7.1.2 Compiler compiler objectfile
symbol table
Deze vertaalt per keer e´ e´ n sourcefile in machinecode-file (objectfile) met extensie .o of .obj. Deze vertaling is echter nog niet volledig: een sourcefile is altijd maar een gedeelte van een volledig programma. Daarom zijn de variabelen en functies nog niet allemaal bekend en kunnen de bijbehorende adressen nog niet in de code worden ingevuld. Behalve de machinecode bevat een objectfile daarom een symbol table, waarin de namen van alle gebruikte variabelen en functies staan met, indien bekend, de bijbehorende adressen. 7.1.3 Linker of loader De machinecode van de diverse objectfiles worden achter elkaar gezet en de onbekende adressen van de symbol tables opgezocht en wederzijds ingevuld. Dit gebeurt door een programma dat loader (UNIX) of linker (PC’s) heet. Daarna worden de opgegeven bibliotheken doorzocht op verdere nog niet gevonden variabelen en functies en worden de betreffende objectfiles ook aan het programma toegevoegd (‘geladen’).
bibliotheken
De bibliotheken worden gemaakt met een bibliotheek-programma (lib) door een aantal objectfiles samen te voegen. Bij C is het de gewoonte dat bij iedere bibliotheek een includefile (met extensie .h) hoort. De uitvoer van de linker is een executeerbare file. De naam van deze file hangt af van de compiler. Bij GNU-C en bij veel UNIX compilers heet deze file a.out, maar er is een optie voor een andere naam.
7.2
Meer over de preprocessor
De preprocessor werkt op tekst: de invoer is tekst en de uitvoer is tekst. De opdrachten voor de preprocessor hebben ‘#’ op de eerste plaats van de regel en zijn niet afgesloten met een puntkomma. De belangrijkste opdrachten zijn: #include
Voeg de tekst van het bestand file.h op deze plaats toe aan de tekst. Deze file staat in de include -directory van de compiler. #include ”file.h” Idem, alleen staat de file nu in directory van de gebruiker. #define AAA bbb Vervang overal ‘AAA’ door ‘bbb’. Het is een gebruik om het deel dat vervangen moet worden, hier dus ‘AAA’, met hoofdletters te schrijven. #define AAA(x) (een functie van x) Er mag ook een parameter worden meegegeven. Deze laatste mogelijkheid kan tot onverwachte resultaten leiden:
... #define KWADRAAT(x)
x ∗ x
/∗ FOUT ∗/
7.3. GEHEUGENGEBRUIK
55
... a = KWADRAAT(b);
/∗ expandeert tot
a = KWADRAAT(b + 5);
/∗ expandeert tot
a = b ∗ b; ∗/
a = b + 5 ∗ b + 5; #define KWADRAAT(x)
FOUT! ∗/
((x) ∗ (x)) /∗ GOED ∗/
Raadpleeg een C-boek voor meer pre-processor opdrachten en mogelijkheden.
7.3
Geheugengebruik
Het is nuttig enig inzicht te hebben in het geheugenorganisatie van een gecompileerd C-programma. Belangrijk is dat alle functie-argumenten en automatische variabelen op de stack1 gemaakt worden. Een definitie als double darr[50000] in een functie zal waarschijnlijk tijdens de executie leiden tot stackoverflow: stack overflow de stack groeit buiten het deel van het geheugen dat hiervoor is gereserveerd en overschrijft data of code. Normaal wordt hier niet op getest en er wordt geen foutmelding voor gegeven. Met een optie van de linker kan zonodig de maximale grootte van de stack worden aangepast. Een andere mogelijkheid is om variabelen extern of static te maken, waardoor ze een vaste plaats in het geheugen krijgen.
7.4
Goed programmeren
7.4.1 Wat is een goed programma? In een grijs verleden waren computers duur en traag en hadden weinig geheugen. Een goed programma was toen een programma dat snel was en weinig geheugen gebruikte. Het loonde om hier tijd aan de besteden. Tegenwoordig zijn computers sneller en goedkoper en is ook geheugen niet duur meer. Het is nu de programmeur die duur is. Tegenwoordig verstaat men dan ook onder een goed programma een programma dat snel klaar is en dat weinig onderhoud vergt. Programmeren is de kunst van het vinden van de fouten. Bij nodeloos ingewikkelde programma’s bestaat het gevaar dat de maker of iemand anders in het programma ‘verdwaalt’ en dat het uitzoeken wat er eigenlijk gebeurt nodeloos veel tijd kost. Goed geschreven programma’s maken dan ook vaak op het eerste gezicht een kinderachtige indruk. In dit prakticum komen alleen maar kleine programma’s aan de orde die altijd nog wel zijn te overzien. De nu volgende aanwijzingen lijken daarom overbodig. Bij ‘echte’ programma’s (een paar maanden werk) zijn ze echter noodzakelijk om te overleven. 1. Zie blz. 6.
HOOFDSTUK 7. ACHTERGRONDEN
56
7.4.2 Inspringen bij een nieuw blok compound statement blok
Een aantal opdrachten tussen accoulades heet een compound statement of een blok. Na while, if enz. mag o´ f e´ e´ n opdracht staan o´ f een blok met opdrachten. Blokken mogen worden genest, d.w.z. in een blok mag weer een blok. Het is een goed gebruik om de leesbaarheid te vergroten door aan het begin van een blok in te springen zodat snel duidelijk is van waar tot waar een blok loopt. 7.4.3 Goed gebruik van variabelen –
–
–
Kies de namen van variabelen met zorg, zodat ze ook werkelijk iets zeggen over betekenis van de variabele of functie. Namen als a, aa, a1, a2 enz. zijn in grote programma’s een ramp. Geen functies namen die van een werkwoord zijn afgeleid (functies doen iets) en variabelen namen die aangeven wat er in is opgeborgen. Beperk het gebruik van globale variabelen zo veel mogelijk. Als door een fout een globale variabele een foute waarde krijgt kan dit in het hele programma ellende veroorzaken. Voorkom ook dat je je steeds weer af moet vragen wat deze variabele ook al weer betekent door een duidelijke naam te kiezen en door commentaar bij de definitie. Het helpt om globale variabelen duidelijk als zodanig herkenbaar te maken door b.v. de naam met een hoofdletter ‘G’ te laten beginnen. Vooral in grotere programma’s is dit sterk aan te raden. Gebruik zoveel mogelijk automatische variabelen. Niet alleen bij functies, maar bij het begin van ieder blok kunnen variabelen worden gedefinieerd. Het bereik is op die manier direct duidelijk.
7.4.4 Information hiding Het is een goed beleid om het op te lossen probleem op te splitsen in deelproblemen en deze afzonderlijk te programmeren. Hoe minder de onderdelen met elkaar te maken hebben, hoe overzichtelijker het programma.
automatische variabelen statische variabelen
Information hiding betekent dat de zichtbaarheid van variabelen beperkt is tot die delen van het programma waar ze nodig zijn. Dit kan door zoveel mogelijk gebruik te maken van automatische variabelen . Als de source van een programma uit meer files bestaat verdient het aanbeveling om globale variabelen die alleen binnen e´ e´ n file nodig zijn als static te definieren zodat zeker is dat ze vanuit andere files onzichtbaar zijn. Ook kan soms een globale variabele vervangen worden door een variabele die als static binnen een blok wordt gedefinieerd. 7.4.5 Commentaar Goed commentaar op de goede plaats is een zeer plezierige ervaring, die helaas maar zelden voorkomt. Waar is commentaar nodig? Headercommentaar boven aan het programma met de naam van het programma, de naam van de maker en de organisatie waar hij bij hoort, de datum en een korte beschrijving van het programma. Daarnaast een korte uitleg hoe
7.5. BEREIK EN PRECISIE VAN VARIABELEN
57
het programma moet worden uitgevoerd, welke gegevens nodig zijn en hoe de resultaten er uitzien. Ook uitleg hoe het programma moet worden gecompileerd is vaak erg prettig. Commentaar bij globale variabelen: wat wordt er in opgeborgen? Commentaar bij functies: wat doet een functie, hoe moet hij worden aangeroepen en welke waarde wordt teruggegeven? Paragraafcommentaar. Een programma wordt beter leesbaar als zo nu en dan eens een regel wordt overgeslagen. Commentaar kan hier de werking verduidelijken. Regelcommentaar. Op dit laagste niveau hoort een goed geschreven programma voor zichzelf te spreken. Als je echter toch niet kunt laten om iets slims en onduidelijks te doen zet er dan tenminste uitleg bij. Ook verwijzingen naar gebruikte literatuur enz. is handig. Tot slot: denk niet ‘commentaar, dat komt later wel’, maar maak het commentaar tegelijk met je programma: het is er ook voor jezelf.
7.5
Bereik en precisie van variabelen
Bij rekenwerk zijn die typen belangrijk die op de een of andere manier gebruikt bereik worden om getallen op te slaan. In C zijn dit de z.g. integral types short, precisie int en long, en de real types float en double. In integral types kun- integral types nen alleen gehele getallen worden opgeslagen, in real’s ook getallen met cijfers achter de komma. De maximum en minimum waarden van de standaardtypen zijn gedefinieerd in limits.h, te vinden in de include-directory van de compiler. 7.5.1 Operaties op integers Bij operaties op integers zoals int’s, long’s enz. wordt niet gewaarschuwd bij underflow of overflow omdat dit te veel rekentijd zou kosten. De progammeur moet zelf in de gaten houden dat alles goed gaat. Bij klassieke C-compilers op PC’s, zoals Turbo-C en Borland-C moet men er rekening mee houden dat een integer slechts 16 bits heeft en dat het bereik dus slechts −32 768 → +32 767 is. Het volgende programma is hier een voorbeeld van. Omdat short (short integer) op bij alle compilers 16 bits heeft geeft dit programma ook een fout resultaat bij 32-bits compilers.
/∗ testint.c ∗/ #include <stdio.h> main() { short x, a, b, c, d;
HOOFDSTUK 7. ACHTERGRONDEN
58
x = 30000; a = x + 30000; b = 3 ∗ x; c = −30000 − x; printf("%d
%d
%d
%d\n", a, b, c, d);
return 0; }
geeft als resultaat −5536
24464
5536
0
Het bereik van integers is b.v. te testen door er telkens e´ e´ n bij op te tellen totdat het resultaat niet meer groter wordt. Ook integerdelingen kunnen onverwachte resultaten geven omdat het resultaat ook een integer is en de rest wordt weggegooid. Zo is 12/13 gelijk aan 0. 7.5.2 De precisie van real’s Bij reals treden problemen op als veel getallen bij elkaar worden opgeteld of als bijna gelijke getallen worden afgetrokken. Dit laatste is goed te zien met differenti¨eren: f (x + ∆x) − f (x) f 0 (x) = lim ∆x→0 ∆x Voor numeriek differenti¨eren wordt de limiet benaderd door een kleine waarde van ∆x te nemen. Het blijkt nu dat, voorbij een zekere grens, een kleinere ∆x geen verbetering meer geeft, maar juist een verslechtering in de benadering van de afgeleide functie. Opdracht 17. Ga de nauwkeurigheid van double’s na door een programma te schrijven dat de afgeleide van een sinus berekent. Laat ∆x hiervoor steeds met een factor tien kleiner worden en vergelijk de berekende en de theoretische waarde van de afgeleide.
Waarom werkt het niet als wij de afgeleide nemen in het punt x = 0? Opmerking: Het aantal cijfers dat met een printf-opdracht wordt afgebeeld staat los van de precisie van de gebruikte reals!
7.6
Rekentijd
De voor een berekening benodigde rekentijd kan soms danig uit de hand lopen. De volgende opdracht geeft hiervan een voorbeeld.
7.6. REKENTIJD
59
7.6.1 Het volume van een 8 dimensionale hyperbol Gegeven is een n-dimensionale ruimte met punten x = x1 . . . xn Om het volume van een hyperbol met straal r in deze ruimte te berekenen sluiten wij de bol in in een hyperkubus met ribbe 2r. De afstand van een punt tot de oorsprong is v u n uX |x| = t x2i i=1
We definieren nu een functie zodanig dat f (x) = 1 f (x) = 0
voor |x| ≤ r voor |x| > r
en berekenen het volume van de bol door deze functie over de hyperkubus te integreren: Vn =
Z +r −r
...
Z +r −r
f (x) dx1 . . . dxn
De waarde van de integraal kan worden benaderd door de hyper kubus op te delen in kleine subkubussen en daarover te sommeren. De functiewaarde van een subkubus wordt benaderd door die van het midden van de subkubus: ligt het binnen de bol, dan nemen we aan dat de hele subkubus binnen de bol ligt en geven wij zijn hele volume de waarde e´ e´ n, zo niet dan is de waarde nul. We hopen dan dat de fouten aan de rand van de bol zullen uitmiddelen. Door de bolling van het boloppervlak is dit laatste echter niet het geval: het bolvolume zal te groot uitkomen. We kunnen het resultaat verbeteren door de subkubussen kleiner te nemen. Hier grijpt echter de wet van behoud van ellende in: als we b.v. een achtdimensionale bol nemen en in iedere richting 100 subkubussen moeten we 1008 subkubussen sommeren en zal de benodigde rekentijd de duur van dit prakticum verre overschrijden. Een betere manier is om voor een groot aantal lukrake plaatsen binnen de hyperkubus na te gaan welk deel hiervan binnen de hyperbol ligt. Uit de verhouding (aantal punten binnen de bol)/(totaal aantal punten) is het volume van de hyperbol te benaderen. Voor het genereren van de willekeurige punten kan de standaard functie rand() gebruikt worden. De analytische oplossing2 voor het volume van een hyperbol met een even aantal dimensies is: π n/2 n Vn = r (n/2)!
2. Richard Becker, Theorie der W¨arme, Springer 1953.
60
HOOFDSTUK 7. ACHTERGRONDEN
Opdracht 18. Schrijf een programma dat het volume van een acht-dimensionale hyperbol met straal 1 berekent volgens de eerste methode. Ga na hoeveel subkubussen per richting een acceptabele (b.v. 2 minuten) rekentijd geeft. Vergelijk het berekende volume met de theorie.
Ga vervolgens na hoeveel random punten verwerkt kunnen worden in dezelfde rekentijd en vergelijk het verkregen resultaat met de vorige methode.
Hoofdstuk 8 Numerieke natuurkunde
8.1
Inleiding
Bij het maken van programma’s die natuurkundige of sterrenkundige problemen op te lossen moet het wetenschappelijke probleem ‘vertaald’ worden in een programma. Dit vraagt enige ervaring. Bijna altijd moeten waarden (getallen) worden berekend. De volgende richtlijnen kunnen helpen. 1. 2. 3. 4. 5.
Bedenk wat moet worden uitgerekend. Breng de formules in een vorm die hiervoor geschikt is. Kies de eenheden, de co¨ordinaten en het tijdstip 0. Kies de variabelen. Schrijf het programma in de vorm van operaties op de data in de variabelen.
8.1.1 Voorbeeld: een kaatsende bal Een bal gaat omhoog met een snelheid van 12 m/s onder een hoek van 47◦ . Bij iedere kaats verliest hij 11% van zijn energie. Neem aan dat de bal niet roteert en dat de hoek van inval steeds gelijk is aan de hoek van terugkaatsing. Bereken de plaatsen waar hij de eerste tien keer stuit. 1. 2.
Bij iedere kaats wordt opnieuw de afstand tot de volgende plaats waar de bal de grond raakt berekend. Op t0 geldt x0 = 0 vx = v(t0 ) cos φ,
vy = v(t0 ) sin φ
en op het moment t1 dat de bal de grond weer raakt: y1 = −0.5gt21 + vy t1 = 0 2vy g x 1 = v x t1 t1 =
Bereken daarna de nieuwe beginsnelheid: v12 = 0.89 v02 61
62
HOOFDSTUK 8. NUMERIEKE NATUURKUNDE
3.
De bal gaat de eerste keer omhoog op t = 0 en x = 0. Iedere keer wordt, uitgaande van de laatste keer kaatsen, een nieuwe x1 berekend en bij x opgeteld. ...
4.
Opdracht N1. Kies de variabelen en maak het programma af.
8.2
Een leeglopend vat
Voor een door een gat in de bodem leeglopend vat geldt, als h de hoogte is van de vloeistofspiegel boven het gat, g de versnelling van de zwaartekracht, m de massa van het water en v de snelheid van het uitstromende water (wet van behoud van energie): ghdm = 0.5v 2 dm of v=
p
2gh
Bovenaan het vat geldt (A is het oppervlak) dm = −ρAdh en onderaan (a is het oppervlak van het gat) dm = ρav dt Dit geeft
ap a 2ghdt dh = − v = − A A We kunnen dit benaderen door ap ∆h = − 2gh∆t A
(8.1)
De oplossing kan gevonden worden door het leeglopen te benaderen met stapjes ∆t die zo klein gekozen worden dat de hoogte h in die tijd niet noemenswaardig veranderd. Opdracht N2. Ga na hoe een rond vat met een diameter van 2 meter en een hoogte van 3.5 meter leegloopt door een gat in de bodem van 125 cm2 .
8.3
Inwendig magneetveld
In een magnetisch kristal kan het inwendige veld berekend worden door te sommeren over de velden van de afzonderlijke dipolen. Voor het veld van een magnetische dipool µ in de oorsprong geldt op het punt (x, y, z) (dipool in de z-richting): Bx =
µ0 µ 3xz 4π r5
(8.2)
8.4. WARMTEGELEIDING IN EEN STAAF µ0 µ 3yz 4π r5 Ã ! µ0 µ 1 3z 2 Bz = − − 5 4π r3 r By =
63
(8.3) (8.4)
µ0 is gelijk aan 4π.10−7 . Opdracht N3. We gaan uit van een kubusvormig kristal met een kubische structuur. Wij nemen een coordinatenstelsel met de oorsprong in het midden van het kristal en met de assen evenwijdig aan de ribben. Het kristal is opgebouwd uit magnetische dipolen die alle evenwijdig aan de z -as gericht zijn en die zich op de roosterpunten bevinden.
Bereken het inwendige veld op de roosterpunten langs de z -as en de x-as. Bereken buiten het kristal nog een paar extra punten. De afstand tussen de roosterpunten is 2 A◦ . De sterkte van de dipolen is 2.2 µB . Voor het Bohr magneton geldt µB =
eh ˙ −21 JT−1 = 9.2741010 2πmc
Bereken vervolgens hetzelfde voor een soortgelijke kristallen die in de z -richting twee maal zo groot en twee maal zo klein zijn. Men mag verwachten dat, voor steeds grotere kristallen, het veld in het midden tot een waarde nadert die onafhankelijk is van de vorm van het kristal. Probeer dit uit met bovenstaande vormen.
8.4
Warmtegeleiding in een staaf
Als het verloop van een proces niet exact te berekenen is kan het soms wel worden gesimuleerd. Opdracht N4. Een homogene stalen staaf van twee meter lengte heeft een temperatuur van 300 K. Vanaf het tijdstip t = 0 wordt e´ e´ n uiteinde constant op 400 K gehouden, terwijl de andere kant op 300 K gehouden wordt. Bereken het temperatuurverloop als functie van de plaats en de tijd. Voor de warmtestroom dQ/dt in een materiaal geldt dat deze evenredig is met de temperatuurgradi¨ent in het materiaal: ∂Q ∂T = −kA ∂t ∂x Hierin is A het oppervlak van de staaf en hangt k, de warmtegeleiding, van het materiaal af. De temperatuurverdeling zal na verloop van tijd lineair worden. Hieruit volgt 1 Φ = (T2 − T1 ) R Φ(x, t) =
HOOFDSTUK 8. NUMERIEKE NATUURKUNDE
64
R noemen we de warmteweerstand van de staaf. Met l als lengte van de staaf geldt l R= Ak Voor de simulatie delen we de staaf op in N elementen gescheiden door N − 1 laagjes isolatiemateriaal. De elementen, met warmtecapaciteit C, hebben een homogene temperatuur. De warmtestroom door de isolatielaagjes is evenredig met het temperatuurverschillen tussen de elementen en de temperatuurverandering die daar het gevolg van is hangt af van de warmtecapaciteit. De temperatuur van de elementen wordt met regelmatige tussenpozen berekend. Aangenomen wordt dat in deze tussentijden de temperatuur niet noemenswaardig verandert. Om een goed beeld te geven van de werkelijkheid mogen de temperatuurverschillen tussen de elementen en de temperatuursveranderingen per stap in de tijd niet te groot zijn. De weerstand van e´ e´ n isolatielaag is r=
l Ak (N − 1)
De temperatuurverandering van element n in een tijd ∆t is 1 ∆Tn = rC
µ
¶
(Tn+1 − Tn ) − (Tn − Tn−1 )
· ∆t
(8.5)
De warmtecapaciteit C is gelijk aan specifieke warmte maal de massa en de massa aan de dichtheid maal het volume van een element: C = cm = cρV = cρ dus rC =
cρl2 kN (N − 1)
Al N (8.6)
Voor staal is de warmtegeleidingsco¨efficient k 40 J/s.m.K. De specifieke warmte c is 450 J/kg.K en de dichtheid ρ 7,8.103 kg/m3 . Het is handig als alle temperaturen tegelijk op het scherm te zien zijn. Omdat na verloop van tijd de temperatuurverdeling lineair zal zijn zal, als we 21 elementen nemen, uiteindelijk de temperatuur in stappen van vijf Kelvin per element oplopen. Neem twee array’s van 21 elementen en bereken de nieuwe waarden in het tweede array uit de oude waarden in het eerste. Kopieer dan de waarden weer terug en begin opnieuw.
8.5
Wet van Snellius
In de volgende opdracht moet gebruik gemaakt worden van een pointer naar een functie.
8.5. WET VAN SNELLIUS
65
y P1 (0, 1)
n1 φ1 s
n2
S(s, 0)
x
φ2 P2 (1, −1)
Figuur 8.1: Breking van licht: een minimalisatieprobleem. Opdracht N5. Een lichtstraal gaat van punt P1 (0, 1) naar punt P2 (1, −1) (zie figuur 8.1 op blz. 65). De x-as scheidt twee media: boven deze as is de lichtsnelheid gelijk aan v1 , onder aan v2 . Bereken het punt S (s, 0) waar de lichtstraal de x-as snijdt. Controleer of de gevonden lichtweg voldoet aan de wet van Snellius. De lichtstraal volgt de snelste weg van P1 naar P2 . De tijd die hij daar over doet is √ p 1 + s2 1 + (1 − s)2 t= + v1 v2 Deze tijd is minimaal voor een waarde van s zodanig dat v1 s 1−s − f (s) = √ ·p =0 2 v 1 + (1 − s)2 1+s 2
(8.7)
Voor de brekingsindices in de twee media geldt n2 v1 = n1 v2 Gemakkelijk is in te zien dat voor een grote waarde van n2 /n1 S dicht bij (1, 0) ligt en voor een zeer kleine dicht bij (0, 0). Het zoeken naar het nulpunt van f (s) op het interval [0 . . . 1] gaat gemakkelijk (maar niet snel) door het midden van dit interval te nemen en te kijken of het nulpunt links of rechts hiervan ligt. Ligt nu het nulpunt in het linker interval, kies dan het middenpunt als nieuwe rechtergrens en ligt het nulpunt in het rechter interval neem dan het middenpunt als linkergrens. Neem nu van dit nieuwe interval weer het midden enz. Ga hiermee door tot de functiewaarde kleiner is dan een vooraf gekozen waarde.
66
HOOFDSTUK 8. NUMERIEKE NATUURKUNDE
Schrijf hiervoor een algemene functie die een nulpunt zoekt en die als argumenten een pointer naar de te minimaliseren functie en de linker- en rechterwaarde van het interval waarin het nulpunt ligt meekrijgt. Volgens de Wet van Snellius geldt voor de brekingsindex n2 v1 sin φ1 = = n1 v2 sinφ2
Bijlage A Syntax
A.1
Identifiers
De naam van een identifier mag bestaan uit hoofdletters, kleine letters, cijfers en ‘lage minnen’ (‘_’). Het eerste symbool moet een _ zijn of een hoofd of kleine letter. Hoofd en kleine letters zijn verschillend. De identifier mag willekeurig lang zijn en de compiler test minimaal de eerste 31 karakters om onderscheid te maken. Toegestaan zijn b.v.: sqrt, Sqrt, x1, x_1. Niet toegestaan zijn: 1f, gemiddelde waarde, a-2. Als een programma uit een aantal files is samengesteld worden vaak aan de identifiers die gemeenschappelijk worden gebruikt strengere eisen gesteld.
A.2
Operatoren
A.2.1 Rekenkundige operatoren + * / %
optellen en aftrekken vermenigvuldigen en delen modulus, rest van deling
A.2.2 Vergelijkingsoperatoren == != < <= > >=
gelijk, ongelijk kleiner, kleiner of gelijk groter, groter of gelijk
A.2.3 Toekenningsoperatoren = += -= *= /=
toekenning optellen bij, aftrekken van vermenigvuldigen met, delen door 67
BIJLAGE A. SYNTAX
68 A.2.4 Logische operatoren && || !
AND, OR NOT
A.2.5 Bitgewijze operatoren & | ˆ << >> ˜
bitgewijze AND, INCLUSIVE OR EXCLUSIVE OR schuif naar links, schuif naar rechts bitgewijze NOT
A.2.6 Unaire operatoren + ++ -sizeof ()
positief getal, negatief getal pre- of postincrement pre- of postdecrement grootte van een object (bytes) typecasting
A.2.7 Ternaire operator <expr1> ? <expr2> : <expr3> als expr1 waar is, dan expr2; anders expr3
A.3
Prioriteiten van operatoren
De prioriteiten worden aangegeven van hoog naar laag. In geval van twijfel is het vaak sneller en duidelijker om met haakjes te werken dan om de prioriteit uit te zoeken () [] -> . links naar rechts ! ˜ ++ -- + - * & () sizeof rechts naar links (uniaire operatoren) * / % + << >> < <= > >= == != & ˆ | && ||
links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts links naar rechts
?: = += -= *= /= %= &= |= <<= >>= ,
rechts naar links rechts naar links links naar rechts
A.4. BITFIELDS
A.4
69
Bitfields
De afzonderlijke bits van variabelen van een integral type kunnen ook met een identifier worden aangeduid. Dit kan handig zijn bij het aansturen van randapparatuur (interfacing). Voorbeeld: Definitie van de variabele vlaggen met afzonderlijk te zetten bits. struct { unsigned int vlag0 : 1; unsigned int vlag1 : 1; : 3; unsigned int getal : 2; } vlaggen;
/∗ /∗ /∗ /∗
eerste bit tweede bit drie bits overgeslagen twee bits getal
Bit 1 van vlaggen wordt nu b.v. gezet met vlaggen.vlag1 = 1;
∗/ ∗/ ∗/ ∗/
70
BIJLAGE A. SYNTAX
Bijlage B In- en uitvoer
B.1
I/O naar het scherm en van het toetsenbord
int getchar() gelijk aan getc(stdin) int putchar(int c) gelijk aan putc(c, stdin) int printf("format", var, ...) gelijk aan fprintf(stdout, format, vars) int scanf("format", &var, ...) gelijk aan fscanf(stdin, ...) char *gets(char *buf) lees een regel in van toetsenbord, zonder ’\n’
B.2
I/O naar bestanden
int getc(FILE *fp) lees het volgende karakter van een file int putc(FILE *fp, c) schrijf het volgende karakter naar een file int fprintf(FILE *fp, "format", var, ...) uitvoer naar een tekstfile int fscanf(FILE *fp, "format", &var, ...c) lees data van een tekstfile char *fgets(char *buf, int max. aantal chars, FILE *fp) lees een regel in van een tekstfile, met ’\n’
B.3
Data types and format specifiers
Integral types char short int
8 bits 16 bits
int
>= short
unsigned int long int
>= int
Real types float
%e, %E %f, %F %g, %G
%c %hd %hx %d %x %u %ld %lx
karakter decimaal hexadecimaal decimaal hexadecimaal positief decimaal hexadecimaal
notaite met komma notatie met macht van tien gemengde notatie
71
BIJLAGE B. IN- EN UITVOER
72 double long double string
pointer
printf(): %e, %f. %g enz. scanf(): %le, %lf. %lg enz. %Le, %Lf. %Lg enz. %s – + (spatie)
plaats getal links in het veld print teken printf(): spaties tussen getallen scanf(): sla wit over
%% %p
%-teken systeemafhankelijk
\n \r \t \f \b \a \\
nieuwe regel terug naar begin regel tab nieuwe pagina e´ e´ n plaats terug ‘let op’, tegenwoordig meestal zoemer het ‘\’-teken
Bijzondere karakters
wit
veldbreedte
scanf() slaat, als een getal moet worden ingelezen eerst alle wit (spatie, tab, nieuwe regel, nieuwe pagina) over. Om getallen netjes onder elkaar te krijgen kan de breedte worden opgegeven met een getal tussen ‘%’ en vertaalkarakter: %10d maakt het getal tien plaatsen breed, rechts aangeschoven. Met %12.2f wordt een getal met komma twaalf plaatsen breed met twee cijfers achter de komma. Zie voor verdere mogelijkheden een C-boek.
Bijlage C Tips en veel voorkomende fouten
Deze lijst is nog zeer onvolledig. Graag suggesties.
C.1
Includefile vergeten
Met name math.h. Default hebben alle functies het type integer. Aangenomen wordt dan b.v. dat de sin-functie een integer teruggeeft.
C.2
Geen prototype
Een functie wordt eerst aangeroepen en pas later in de file gedefinieerd. De compiler neemt dan eerst aan dat het type van de functie ‘int’ is. Als het type verderop anders blijkt te zijn volgt de melding dat het type van de functie veranderd is. Een prototype voorkomt dit.
C.3
Namen in #define’s niet met hoofdletters
Dit is een conventie. Zo wordt voorkomen dat dezelfde naam ook als identifier voor een variabele wordt gebruikt en door de preprocessor wordt ingevuld. Dit kan raadselachtige foutmeldingen geven.
C.4
Niet geinitialiseerde pointers
Externe pointers zijn op NULL geinitialiseerd. Als nu via een niet geinitialiseerde pointer een waarde naar het geheugen wordt geschreven komt deze op adres 0 terecht. Aan het eide van de executie van een programma wordt getest of adres 0 niet is veranderd. Is dit het geval dan volgt de foutmelding ‘NULL POINTER ASSIGNMENT’. Het is vaak lastig om na te gaan waar dit is gebeurt. Niet geinitialiseerde automatische pointers hebben een willekeurige waarde. Kan rare gevolgen hebben. 73
74
C.5
BIJLAGE C. TIPS EN VEEL VOORKOMENDE FOUTEN
Stack overflow
Automatische variabelen worden op de stack gamaakt. Definities als ‘double xx[200000]’ in een functie geven geen foutmelding, maar bij executie groeit de stack over de data heen en kan het programma vastlopen. Remedie: maak het array extern of static of pas de grootte van de stack aan.
Bijlage D Bibliotheekfuncties
Deze lijst bevat de ANSI C standaardfuncties. Voor een volledige beschrijving en voorbeelden zie het handboek en de header files. Veel C-compilers kennen meer functies dan de hier genoemde standaardfuncties.
D.1
In- en uitvoer (stdio.h)
D.1.1 Bestanden openen en sluiten FILE FILE int int int int FILE char int void
*fopen(const char *pfilename, const char *pmode); *reopen(const char *pfilename, const char *pmode, FILE *pstream); fflush(FILE *pstream); fclose(FILE *pstream); remove(const char *pfilename); rename(const char *pold, const char *pnew); *tmpfile(void); *tmpnam(char *ps); setvbuf(FILE *pstream, char *pbuf, int _mode, size_t size); setbuf(FILE *pstream, char *pbuf);
D.1.2 Tekst in- uitvoer int int int int int int int int int
fscanf(FILE *pstream, const char *pformat, ...); scanf(const char *pformat, ...); sscanf(const char *ps, const char *pformat, ...); fprintf(FILE *pstream, const char *pformat, ...); printf(const char *pformat, ...); sprintf(char *ps, const char *pformat, ...); vprintf(const char *pformat, va_list _ap); vfprintf(FILE *pstream, const char *pformat, va_list _ap); vsprintf(char *ps, const char *pformat, va_list _ap);
75
BIJLAGE D. BIBLIOTHEEKFUNCTIES
76
D.1.3 In- en uitvoer van tekens enz. int char int int int int char int int int int
fgetc(FILE *pstream); *fgets(char *ps, int _n, FILE *pstream); fputc(int _c, FILE *pstream); fputs(const char *ps, FILE *pstream); getc(FILE *pstream); getchar(void); *gets(char *ps); putc(int _c, FILE *pstream); putchar(int _c); puts(const char *ps); ungetc(int _c, FILE *pstream);
D.1.4 Binaire in- en uitvoer size_t fread(void *pptr, size_t size, size_t _nelem, FILE *pstream); size_t fwrite(const void *pptr, size_t size, size_t _nelem, FILE *pstream);
D.1.5 Positionering in een file int long void int int
fseek(FILE *pstream, long _offset, int _mode); ftell(FILE *pstream); rewind(FILE *pstream); fgetpos(FILE *pstream, fpos_t *ppos); fsetpos(FILE *pstream, const fpos_t *ppos);
D.1.6 Foutafhandeling void int int void
clearerr(FILE *pstream); feof(FILE *pstream); ferror(FILE *pstream); perror(const char *ps);
D.2
ctype.h
int int int int int int int int int int int int int
isalnum(int c); isalpha(int c); iscntrl(int c); isdigit(int c); isgraph(int c); islower(int c); isprint(int c); ispunct(int c); isspace(int c); isupper(int c); isxdigit(int c); tolower(int c); toupper(int c);
D.3. STRING- EN GEHEUGEN-FUNCTIES, STRING.H
D.3
77
String- en geheugen-functies, string.h
D.3.1 String-functies char char char char int int char char size_t size_t char char size_t char char
*strcpy(char *ps1, const char *ps2); *strncpy(char *ps1, const char *ps2, size_t n); *strcat(char *ps1, const char *ps2); *strncat(char *ps1, const char *ps2, size_t n); strcmp(const char *ps1, const char *ps2); strncmp(const char *ps1, const char *ps2, size_t n); *strchr(const char *ps, int c); *strrchr(const char *ps, int c); strspn(const char *ps1, const char *ps2); strcspn(const char *ps1, const char *ps2); *strpbrk(const char *ps1, const char *ps2); *strstr(const char *ps1, const char *ps2); strlen(const char *ps); *strerror(int errcode); *strtok(char *ps1, const char *ps2);
D.3.2 Beheer geheugen void void int void void int
D.4
*memcpy(void *pdest, const void *psrc, size_t n); *memmove(void *ps1, const void *ps2, size_t n); memcmp(const void *ps1, const void *ps2, size_t n); *memchr(const void *ps, int _c, size_t n); *memset(void *ps, int _c, size_t n); memcmp(const void *ps1, const void *ps2, size_t n);
math.h
double double double double double double double
sin(double _x); cos(double _x); tan(double _x); asin(double _x); acos(double _x); atan(double _x); atan2(double _y, double _x);
double double double
sinh(double _x); cosh(double _x); tanh(double _x);
double double double double double double
sqrt(double _x); log(double _y); log10(double _x); exp(double _x); pow(double _x, double _y); fmod(double _x, double _y);
double double
ceil(double _x); floor(double _x);
BIJLAGE D. BIBLIOTHEEKFUNCTIES
78
double double double double
D.5
fabs(double _x); frexp(double _x, int *ppexp); ldexp(double _x, int _exp); modf(double _x, double *ppint);
stdlib.h
/* conversie van een string naar een variabele */ double atof(const char *ps); int atoi(const char *ps); long atol(const char *ps); double strtod(const char *ps, char **pendptr); long strtol(const char *ps, char **pendptr, int _base); unsigned long strtoul(const char *ps, char **pendptr, int _base); /* random getallen */ int rand(void); void srand(unsigned seed); /* gebruik vrij geheugen */ void *calloc(size_t nelem, size_t size); void *malloc(size_t size); void *realloc(void *pptr, size_t size); void free(void *pptr); /* diversen */ void abort(void); void exit(int status); int atexit(void (*pfunc)(void)); int system(const char *ps); char *getenv(const char *pname); void *bsearch(const void *pkey, const void *pbase, size_t nelem, size_t size, int (*pcmp)(const void *pck, const void *pce)); void qsort(void *pbase, size_t nelem, size_t size, int (*pcmp)(const void *pe1, const void *pe2)); int abs(int _i); long labs(long _i); div_t div(int numer, int _denom); ldiv_t ldiv(long numer, long _denom);
D.6
Andere functies
D.6.1 assert.h assert(test)
/* geeft foutmelding als waar is.
D.6. ANDERE FUNCTIES D.6.2 setjmp.h Functies om ‘goto’s’ mogelijk te maken door het hele programma. D.6.3 signal.h Communicatie met het bedrijfssysteem en foutafhandeling . D.6.4 limits.h Definities van de grootte van de integrale types. D.6.5 float.h Definities van bereik en andere gegevens van float’s. D.6.6 time.h Manipuleren van datum en tijd. D.6.7 stdarg.h Functie voor het gebruik van functies met een wisselend aantal argumenten.
79
80
BIJLAGE D. BIBLIOTHEEKFUNCTIES
Bijlage E De GNU C-compiler
De GNU C-compiler is speciaal ontworpen om makkelijk aangepast te kunnen worden aan verschillende computers zoals workstations met UNIX en PC’s. Het is een goede compiler en iedereen mag er zonder kosten gebruik van maken. Zowel C als C++ programma’s, met extensie .CC, kunnen er mee vertaald worden. De documentatie is uitvoerig, maar niet speciaal gericht op beginners. Liefhebbers kunnen met het programma info in de on-line documentatie rondbladeren.
gcc is een commandline compiler en de programma’s moeten worden geschreven met een externe editor. Het resultaat van het compileren is de file a.out. Er bestaat ook een IDE1 : rhide. Voorbeelden van aanroepen: gcc try1.c try2.c Compileer de twee programma’s en voeg ze samen tot e´ e´ n a.out. gcc -c try1.c try2.c Maak de twee objectfiles2 try1.o en try2.o, maar link niet. gcc try1.c try2.o Compileer try1.c en link deze met try2.o. gcc try.c -lm Compileer try.c en link deze met de wiskundige bibliotheek libm.a. gcc -Wall try.c Schakel alle waarschuwingen in. gcc try.c -o try Vertaalde programma heet try i.p.v. a.out. De optie -lx geeft aan dat de linker de bibliotheek libx.a moet doorzoeken op nog niet gevonden functies. Zo wordt libpc.a, de bibliotheek met speciale functies voor PC’s, meegenomen met de optie -lpc. De PC-versie van GNU-C is een z.g. protected mode compiler, d.w.z. hij kan alleen gebruikt worden op PC’s met 80386 processor of nieuwer, heeft 32-bits protected mode integers en kan het hele geheugen adresseren. De compiler maakt twee files: a.exe en a.out. De eerste kan direct worden uitgevoerd, de tweede met het commando go32-v2 a.out. 1. Integrated Development Environment, Een editor van waaruit de compiler kan worden aangeroepen, dat de fouten meldt enz. 2. Zie blz. 54
81
82
BIJLAGE E. DE GNU C-COMPILER
De PC-versie kan op internet gevonden worden bij ftp.euro.net/d5/simtelnet/gnu/djgpp Lees djgppfaq.txt voor nadere informatie.
Bijlage F Het hulpprogramma make
Bij het schrijven en testen van een programma moet iedere keer de C-compiler worden aangeroepen en moet iedere keer opnieuw alle bestandsnamen en opties worden ingetypt. Dit wordt overgenomen door het programma make. In het bestand makefile, dat door make wordt ingelezen, staat omschreven wat er moet gebeuren. In de volgende makefile staat omschreven hoe een programma mijnprog moet worden gecompileerd. Er zijn twee bronbestanden: mijnprog.c en gplot.c en e´ e´ n headerfile gplot.h.
#−−− makefile #−−− #−−− Om te compileren: #−−−
make
(iedere keer als je je programma veranderd hebt)
#−−− # C−compiler CC
= gcc
# object files OBJS
= mijnprog.o gplot.o
# gebruikte bibliotheken, hier de bibliotheek met wisk. functies. = −lm
LIBS
# opties voor de C−compiler CFLAGS
= −ansi −Wall −O2
# recept om van een .c een .o file te maken %.o: %.c $(CC) $(CFLAGS) −c −o $∗.o $∗.c mijnprog:
mijnprog.o gplot.o $(CC) $(CFLAGS) $(OBJS) $(LIBS) −o mijnprog
gplot.o:
gplot.c gplot.h
83
84
mijnprog.o:
BIJLAGE F. HET HULPPROGRAMMA MAKE
mijnprog.c gplot.h
In de eerste regels worden de macro’s gedefinieerd: CC, de gebruikte C-compiler, OBJS, de objectfiles, LIBS, de te gebruiken bibliotheken en CFLAGS, de opties voor de C-compiler. Vervolgens volgt een algemeen recept hoe van een .c file een .o (object) file gemaakt moet worden. $* betekent dat hier de naam van het te vertalen .c bestand (zonder .c) moet worden ingevuld. mijnprog: specificeert het programma. Hier zijn de bestanden mijnprog.o en gplot.o voor nodig. De volgende regel laat zien hoe de compiler uit de objectbestanden het executeerbare programma moet maken. De volgende twee regels laten zien van welke bestanden de twee objectfiles afhankelijk zijn. make is een slim programma en maakt alleen een nieuwe objectfiles als de bronbestanden waar ze uit gemaakt worden veranderd zijn. Als b.v. alleen mijnprog.c is veranderd zal make geen nieuwe gplot.o maken. make verlangt dat tussen de eerste en de tweede kolom een ‘tab’ staat. Met spaties werkt het niet.
Bijlage G Het volume van een hyperbol
De afleiding van de formule voor het berekenen van het volume van een hyperbol met een even aantal dimensies kan gevonden worden in R ICHARD B ECKER, Theorie der W¨arme, Springer 1955:
85
86
BIJLAGE G. HET VOLUME VAN EEN HYPERBOL
Bijlage H Gplot
Version 3.1, 4 June 1998. Copyright 1992, 1993, 1998. Maurits Wijzenbeek, Eric Hennes. Van der Waals - Zeeman Lab., Univ. of Amsterdam. Valckenierstraat 64, 1018 XE Amsterdam, Netherlands. Fax: 00 31 (0)20 525 5788 E-mail: [email protected], [email protected] GPLOT is a library of functions to make graphs for C programs that calls GNUPLOT to do the real plotting. The GPLOT functions make scriptfiles and GNUPLOT is called (with system()) with these scriptfiles as argument. After a run of the program the scriptfiles can be edited so that the results can be printed or converted to Postscript etc. by GNUPLOT. GPLOT was made by Maurits Wijzenbeek. Eric Hennes added surface plots. Permission to use, copy, and distribute this software and its documentation for any purpose with or without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Permission to modify the software is granted, but not the right to distribute the modified code. Modifications are to be distributed as patches to released version. This software is provided a¨ s is”without express or implied warranty. The makers are not be responsible for any damage direct or indirect the use of this package might cause, especially damage caused by stupidity or use of the program in a way for which it was not designed. We would very much appreciate to be infored of any bugs or changes. On PC’s ‘real mode’ programs that use Gplot must be compiled with the ‘compact’ or ‘large’ model to make far pointers default. For more information read the GNUPLOT manual or the programm GPDEMO.C. 87
BIJLAGE H. GPLOT
88
H.1
Use of the GPLOT package
The GPLOT functions look for GNUPLOT in the path. This can be changed with the GPLOT environmental variable. Example: SET GPLOT=/SOMEDIR/GNUPLOT.EXE The GPLOT function makes the following temporary files to call GNUPLOT: gplotxx.scr Script files for GNUPLOT gplotxx.dat Data files, used in the script files. gplotxx.bat Batch files, to call GNUPLOT and look at the graphs at a later time. The script files contain information about how to send the graphs to a printer etc. In GPLOT.C some adaptations to the used system (UNIX or MSDOS) can be made.
H.2
Functions.
H.2.1 Preparation of the plot void gpl title(char *title) Title of plot. void gpl axis(Axis axis, float low, float high, char *label); Define axis: range and label. ‘axis’ can have the values ‘X’, ‘Y’ or ‘Z’. void gpl style(Style style); Set linestyle. The linestyle can be: LINES, POINTS, LINEPOINTS, IMPULSES, DOTS, DEFAULT, ERRORBARS. void gpl using(char *s); Set options like linestyle, color, column used in datafile etc., only for the next graph. Overides the linestyle set by ‘gpl style’. void gpl cstyle(Cstyle cstyle, int nlevels); Set surfaceplot-style: ‘cstyle’ define on which surface the contours are plotted: on the x-y plain (BASE), on the function surface (SURFACE), on both (BOTH) and in the previous defined way (CURRENT SET). ‘nlevels’ is the number of levels in the contour plot. The data can be stored in a two dim. array (TWO DIM), in a one dimentional array (LINEAR) or in an array of pointers, pointing to array’s (ARRAY OF POINTERS). Example: ‘gpl cstyle(SURFACE, 20);’ void gpl matrix(Mtype mtype); Set storage mode for surface plot array’s. void gpl label(float x, float y, float z, char *string); Set text in plot. The positioning is in units of the x- and y-axis and respective to the center of the top of the plot. void gpl command(char *s); Add GNUPLOT command to scriptfile. void gpl name(char *fname); Define filenames of script and datafiles. H.2.2 Plot functions void gpl func(char *funct, char *tag); Plot function. ‘funct’ is a pointer to a string. ‘tag’ is a label to the plotted line.
H.2. FUNCTIONS.
89
If tag == the name of the function is displayed in the graph. If tag == NULL no information is displayed. Example: ‘gpl func(”sin(x)”, ”sine function”);’ plots a sine function. The text ‘sine function’ is placed as a tag. void gpl data(int n, float *px, float *py, char *tag); Plot data. The x and y values of the plot are in the array’s ‘px’ and ‘py’. ‘n’ is the number of points. ‘tag’ is a label. If tag == the name of the function is displayed in the graph. If tag == NULL no information is displayed. void gpl hist(int n, float *hist, char *tag); Plot histogram of ‘n’ values; the data are in the array ‘hist’. void gpl 3data(int n, float *px, float *py, float *pz, char *tag); Make a plot of 3-dimensional data as a line or data points. void gpl 3func(char *function, char *tag); Make a plot surface plot of a function of two variables. Example: ‘gpl 3func(”x * y”, e¨ xample of surface plot”);’ void gpl contour(int nx, int ny, void *z, char *tag); Plot data as a surface; contours can be added. ‘nx’ and ‘ny’ are the number of points in the x- and y-direction. The data to be plotted can be stored in three ways: In an array: pf = (float*)z; f = *(pf + (x * ny) + y); With an array of pointers: ppf = (float **)z; f = ppf[i][y]; These two storage modes are called ‘TWO DIM’ or ‘LINEAR’ and ‘ARRAY OF POINTERS’, and are defined in the functions ‘gpl matrix’ (obsolete) or ‘gpl cstyle’. void gpl file(char *filename); Plot previously created datafile. H.2.3 Show plot on screen. void gpl show(void); Close scriptfile and let GNUPLOT show plot(s) on the screen. void gpl end(void); End of plot command to let a new plot be defined in the same scriptfile. All plots will be shown, one after the other, when gpl show() is called. void gpl append(void); Extend scriptfile with new plot after plot is shown on the screen with gpl show(). If gpl append() is not called the next plot will be made with a new scriptfile. If gpl show() is called again all plots in the scriptfile will be shown.
BIJLAGE H. GPLOT
90
H.3
gplot.h
/* gplot.h Includefile for for gplot package: call GNUPLOT from inside C-programs. Version 3.1, 4 June 1998. Copyright 1992, 1993, 1998. Maurits Wijzenbeek, Eric Hennes. Van der Waals - Zeeman Lab., Univ. of Amsterdam. Email: [email protected] E-mail: [email protected] [email protected] */ #ifndef _GPLOT #define _GPLOT #define GPOINTS 200 /* Axis types */ typedef enum {X, Y, Z} Axis; /* types of matrix arrays */ typedef enum {TWO_DIM,LINEAR,ARRAY_OF_POINTERS} Mtype; /* line- and contour- plotstyles :*/ typedef enum {LINES,POINTS,LINEPOINTS,IMPULSES,DOTS,DEFAULT,ERRORBARS} Style; typedef enum {BASE, SURFACE, BOTH, CURRENT_SET} Cstyle;
/* Plot preparing functions (optonal) void gpl_axis(Axis axis, float low, float high, char *label); void gpl_label(float x, float y, float z, char *string); void gpl_title(char *title); void gpl_style(Style style); void gpl_using(char *s); void gpl_cstyle(Cstyle cstyle, int nlevels); void gpl_matrix(Mtype mtype); void gpl_end(void); void gpl_append(void); void gpl_command(char *s); void gpl_name(char *fname);
*/ /* Define axis: range and label.
*/
/* print text in plot
*/
/* set title of plot */ /* set linestyle */ /* set plot options, reset by plotting; disables the effect of gpl_styel() */ /* /* /* /* /* /*
/* Plot-functions */ void gpl_data(int n, float *px, /* float *py, char *tag); void gpl_func(char *funct, /* char *tag); void gpl_hist(int n, float *hist, /* char *tag); void gpl_3data(int n, float *px, /* float *py, float *pz,char *tag); void gpl_contour(int nx, int ny, /* void *z, char *tag); void gpl_3func(char *function, /* char *tag); void gpl_file(char *filename); /*
set contourplot-style set matrix-type end of plot command append to current commandfile add command to commandfile change filename gplot.. to fname
*/ */ */ */ */ */
plot n datapoints + title-string
*/
plot function + title
*/
plot histogram + title
*/
make 3-dim plot of data + title
*/
contour and/or surface-plot +title */ 3-dim. function-plot + title
*/
plot previously created datafile
*/
/* plot-execution and error functions */ void gpl_show(void); /* close commandfile and show plot(s) #endif
/* _GPLOT */
*/
Index
#define, 33 ‘->’ operator, 46 adres, 3 analoge elektronica, 3 ANSI C, 14 argument, 28 array, 24, 26 array-index, 24 ASCII, 4 assembler, 12 assignatie, 17 assignment operator, 41 automatische variabelen, 30, 56 bereik, 57 bibliotheken, 12 binair, 9 binaire I/O, 39 bit, 3 bitfields, 50 blok, 22, 56 break, 42, 44 busmaster, 5 byte, 3 C++, 14 C-compiler, 53 cast, 41, 42 char, 25 code, 4, 17 commentaar, 56 compiler, 12, 54 compound statement, 22, 56 computerbus, 4 const, 43 constante, 18 continue, 42 controller, 6 data, 4, 17 decimaal, 9
definitie, 17 digitale elektronica, 3 directe I/O, 39 diskette, 6 do-lus, 21 driver, 6 editor, 11, 15 else, 22 enum, 44 EOF, 36 exponent, 8 expressie, 17 extern, 43 fclose(), 36 fgets(), 38 filepointer, 35 flag register, 5 floats, 8 fopen(), 35 for-lus, 19 format string, 37 Fortran, 13 fread(), 39 free(), 50 functie, 7, 28 functieresultaten, 30 functiewaarde, 28 fwrite(), 39 geheugen, 3 geheugen adres, 3 getallen, 7 gets(), 38, 44 GNU C-compiler, 81 Gplot, 32 harde schijf, 6 headerfiles, 53 hexadecimaal, 9 91
INDEX
92 I/O, 6 I/O, karakters, 36 identifier, 12, 67 if, 22 information hiding, 56 instructieset, 5 integral types, 57 interface, 6 K&R C, 14 kilobyte, 4 komma operator, 33 left value, 17 linker, 54 loader, 54 lus, 19 machinecode, 12 malloc(), 50 mantisse, 8 megabyte, 4 modifiers, 43 NULL-pointer, 27, 44, 50 numeriek integreren, 49 objectfile, 54 onwaar, 20 Pascal, 13 pointer, 25, 26 pointer naar struct, 46 pointerarithmetic, 26 pointers als functieargument, 31 pointers naar functie, 48 pop, 6 precisie, 57 preprocessor, 53, 54 processor, 5 programmeercyclus, 11 prototype, 30 push, 6 reals, 8 recursiviteit, 30 register, 43 registers, 5 returnadres, 7
scanf(), 37 signed integers, 7 sizeof operator, 39 sscanf(), 38, 44 stack, 6 stackpointer, 6 statische variabelen, 56 stderr, 36 stdin, 36 stdout, 36 stream, 35 string, 25 switch, 43 symbol table, 54 tekenbit, 8 typeconversie, 41 typedef, 47 ungetc(), 37 unsigned, 43 unsigned integers, 7 variabele, 17 variabelen, 7 veldbreedte, 72 void, 30 volatile, 43 voorwaarde, 20 voorwaardelijke sprong, 5 vrij geheugen, 49 waar, 20 while-lus, 21 wit, 38, 72