Universiteit Utrecht
Programmeren in
C Handleiding bij de computercursus studiejaar 2002/2003
Faculteit Natuur- en Sterrenkunde Julius Instituut Practicum Hoofdvak Natuurkunde
CC
Deze uitgave met medewerking van: Dik Takken en Guus Sliepen. Eindredactie: Aad van der Steen. Laatst gewijzigd: 19 september 2002
Programmeren in C
INHOUDSOPGAVE
Inhoudsopgave 1 Inleiding 1.1 Het kader . . . . . . . . . . . . . 1.2 Het doel . . . . . . . . . . . . . . 1.3 Wat is een computerprogramma? 1.4 De programmeertaal C . . . . . . 1.5 Waarom C? . . . . . . . . . . . . 1.6 Verder . . . . . . . . . . . . . . . 1.7 Gebruikte conventies . . . . . . . 1.7.1 Lettertypes . . . . . . . . 1.7.2 Wiskundige notaties . . . 1.7.3 Namen van tekens . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
2 De programmeeromgeving 2.1 Microsoft Visual C++ . . . . . . . . . . . 2.1.1 Het starten van een nieuw project 2.1.2 Nieuw programma starten . . . . . 2.2 GNU C . . . . . . . . . . . . . . . . . . . 2.2.1 Het schrijven van source code . . . 2.2.2 Het compileren . . . . . . . . . . . 2.2.3 Nieuw programma starten . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
. . . . . . .
. . . . . . . . . .
4 4 4 4 6 6 7 8 8 8 8
. . . . . . .
10 10 10 12 12 12 13 13
3 Een eenvoudig C programma
14
4 De opbouw van een C programma 4.1 De globale structuur . . . . . . . . 4.2 De preprocessor directives . . . . . 4.2.1 #include . . . . . . . . . . 4.2.2 #define . . . . . . . . . . . 4.3 Opbouw van functies . . . . . . . . 4.4 Opbouw van de functie main() . . 4.5 De layout van de source code . . . 4.5.1 Inspringen en afbreken . . . 4.5.2 Naamgeving . . . . . . . . . 4.5.3 Commentaar . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
16 16 16 16 17 17 18 19 19 20 21
5 Gereserveerde woorden en identifiers 5.1 Gereserveerde woorden . . . . . . . . . 5.2 Standaard identifiers . . . . . . . . . . 5.3 Normale identifiers . . . . . . . . . . . 5.4 Bereik van identifiers . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
23 23 24 24 25
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . wordt aangeroepen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
27 27 27 28 28 28 29 29 31
6 Statements 6.1 Inleiding . . . . . . . . . . . . 6.2 Declaraties . . . . . . . . . . 6.3 Assignments . . . . . . . . . . 6.4 Statements waarin een functie 6.5 Samengestelde statements . . 6.6 Keuze statements . . . . . . . 6.6.1 Het if-statement . . . 6.6.2 Het switch-statement INHOUDSOPGAVE
. . . . . . . . . .
1
INHOUDSOPGAVE
6.7
6.8
Programmeren in C
Herhalingsstatements . . . . . . . . 6.7.1 Het for-statement . . . . . 6.7.2 Het while-statement en het Overige statements . . . . . . . . . 6.8.1 Het break-statement en het 6.8.2 Het return-statement . . . 6.8.3 Het goto-statement . . . .
7 Variabelen 7.1 Inleiding . . . . . . . . . . . . 7.2 Het geheugen . . . . . . . . . 7.3 Namen in plaats van adressen 7.4 Pointers . . . . . . . . . . . . 7.5 Zelf geheugen reserveren . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . do..while-statement . . . . . . . . . . . . . continue-statement . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
33 33 35 37 37 38 39
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
40 40 40 41 42 44
8 Datatypes en operatoren 8.1 Inleiding . . . . . . . . . . . . . . . . . . . . . . . . 8.2 Elementaire datatypes . . . . . . . . . . . . . . . . 8.2.1 Het type char . . . . . . . . . . . . . . . . 8.2.2 Het type int . . . . . . . . . . . . . . . . . 8.2.3 Het type float en het type double . . . . 8.2.4 Het type enum . . . . . . . . . . . . . . . . 8.2.5 De voorvoegsels signed, unsigned, long en 8.2.6 Samenvatting . . . . . . . . . . . . . . . . . 8.3 Typeconversie . . . . . . . . . . . . . . . . . . . . . 8.3.1 Automatische typeconversie . . . . . . . . . 8.3.2 De cast-operator . . . . . . . . . . . . . . . 8.4 Operatoren . . . . . . . . . . . . . . . . . . . . . . 8.4.1 Rekenkundige operatoren . . . . . . . . . . 8.4.2 Relationele operatoren . . . . . . . . . . . . 8.4.3 Logische operatoren . . . . . . . . . . . . . 8.4.4 Verkorte schrijfwijzen . . . . . . . . . . . . 8.4.5 Prioriteiten . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . short . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
46 46 46 46 47 47 48 50 51 52 52 53 53 54 54 55 55 57
9 Arrays, strings en structs 9.1 Inleiding . . . . . . . . . . . . . . . . . . . . 9.2 Arrays . . . . . . . . . . . . . . . . . . . . . 9.2.1 De array als ge¨ındexeerde variabele . 9.2.2 Twee- en meerdimensionale arrays . 9.2.3 Arrays en pointers . . . . . . . . . . 9.2.4 Zelf geheugen voor arrays reserveren 9.3 Strings . . . . . . . . . . . . . . . . . . . . . 9.4 Structs . . . . . . . . . . . . . . . . . . . . . 9.5 Definitie van types . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
59 59 59 59 60 61 62 63 65 66
. . . . .
68 68 68 70 71 73
10 Functies 10.1 Inleiding . . . . . . . . . 10.2 Definitie van een functie 10.3 Het begrip void . . . . 10.4 Pointers en functies . . . 10.5 Arrays en functies . . . 2
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
INHOUDSOPGAVE
Programmeren in C
LIJST VAN TABELLEN
11 Gestructureerd programmeren 74 11.1 Leesbaarheid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 11.2 Uitbreidbaarheid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 12 Lezen en schrijven van bestanden
76
A Standaardbibliotheken A.1 Inleiding . . . . . . . . . . . . A.2 In- en uitvoer: stdio.h . . . A.3 String manipulatie: string.h A.4 Wiskundige functies: math.h A.5 Standaardfuncties: stdlib.h
. . . . .
80 80 80 83 84 84
. . . . . .
86 86 93 100 103 110 112
B C++ voor C kenners B.1 Nieuwe taalconstructies B.2 Classes . . . . . . . . . . B.3 Voorbeelden . . . . . . . B.4 Classes (vervolg) . . . . B.5 Input/output . . . . . . B.6 Slot . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
C Index
114
Lijst van tabellen 1 2 3 4 5 6 7
Speciale tekens en hun namen . . . . . . . . . Datatypes en hun bereik . . . . . . . . . . . . Prioriteit en associativiteit van de operatoren Format modifiers voor printf() en scanf() Format specifiers voor printf() en scanf() Modes voor fopen() . . . . . . . . . . . . . . Wiskundige functies . . . . . . . . . . . . . .
LIJST VAN TABELLEN
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
9 51 57 81 81 82 84
3
1
INLEIDING
1 1.1
Programmeren in C
Inleiding Het kader
Deze cursus programmeren in C is onderdeel van het tweedejaars programma van het practicum hoofdvak Natuurkunde. Voor de cursus zijn 8 practicummiddagen uitgetrokken. Bij het bepalen van het aantal te behalen studiepunten is er op gerekend, dat naast de practicummiddagen er daarnaast nog extra tijd aan de cursus wordt besteed. Bij dit cursusboek behoort een opgavenbundel. De cursus wordt als behaald genoteerd, wanneer de uitwerking van een nader aan te wijzen selectie van de opgaven aan begeleidende assistenten is gedemonstreerd.
1.2
Het doel
De bedoeling van deze cursus is dat de student na het afronden in staat is om zelfstandig computerprogramma’s te ontwerpen en uit te voeren die rekenintensieve problemen uit de natuurkunde oplossen. De vaardigheden worden getest door de student de gemaakte opgaven te laten demonstreren aan de practicumassistenten.
1.3
Wat is een computerprogramma?
Een computerprogramma is een serie vaste instructies die ´e´en voor ´e´en worden uitgevoerd door een processor met als doel om een set gegevens in het geheugen te manipuleren. Elke processor heeft zijn eigen instructieset, een verzameling eenvoudige opdrachten die het kan uitvoeren. De belangrijkste opdrachten die elke processor kan uitvoeren zijn:
Een getal uit het geheugen lezen Voordat de processor iets van doen met gegevens moeten ze eerst in de processor worden geladen. Processoren beschikken over een beperkte set registers om de gegevens tijdelijk in op te slaan. Een getal schrijven naar het geheugen Als de processor iets heeft uitgerekend kan het resultaat vanuit een register in het geheugen worden geplaatst. Bewerkingen op registers Elke processor beschikt over een aantal basis-operaties die het kan uitvoeren op ´e´en of meer getallen in zijn registers, zoals twee registers bij elkaar optellen, vermenigvuldigen en delen. Registers vergelijken Er kan worden gekeken of het getal in register A groter, kleiner, of gelijk is aan een getal in register B. Voorwaardelijk springen Normaal gesproken voert de processor alle instructies van een programma ´e´en voor ´e´en uit. Processoren kunnen ook springen naar een andere plek in het programma, om bijvoorbeeld instructies over te slaan of juist om instructies te herhalen. De processor kan beslissen of 4
Programmeren in C
1
INLEIDING
hij een sprong wel of niet zal uitvoeren afhankelijk van bijvoorbeeld een vergelijking tussen twee registers. Zo kan de processor bijvoorbeeld een heel ander gedeelte van het programma uitvoeren als register A kleiner is dan nul dan wanneer register A groter is dan nul.
Zoals je ziet zijn de instructies die een processor kan uitvoeren nogal simpel. Veel processoren kunnen zelfs geen wortel trekken of machtverheffen. De programmeur zal deze instructies zelf moeten samenstellen uit meerdere eenvoudiger instructies. Om je een idee te geven van hoe een programma eruit ziet geven we hier een voorbeeld van een programmaatje dat twee getallen (5 en 6) in het geheugen vermenigvuldigt en het resultaat wegschrijft. We hebben het wat vereenvoudigd voor aan te nemen dat de processor die dit programma draait direct bewerkingen kan uitvoeren op getallen in het geheugen, zonder dat het eerst naar registers gecopieerd hoeft te worden. Verder kent onze processor vier verschillende instructies, genummerd van 1 t/m 4, namelijk Jump, Copy, Multiply en Exit. Dit programma ziet er als volgt uit:
1,5,0,0,0,2,5,2,2,6,3,3,2,3,4,4
Om te begrijpen waarom deze serie getallen een vermenigvuldiging tot gevolg heeft hebben we in de volgende tabel weergegeven hoe de processor de serie interpreteert. De eerste kolom is het ’adres’ van het getal, wat aangeeft waar het getal in het geheugen komt te staan als het programma geactiveerd wordt. Het teken “X” staat voor het adres van de huidige instructie. Zoals je ziet is de eerste instructie de ’Jump’ instructie. Deze instructie springt naar positie 5 in het geheugen, omdat het getal 5 volgt na de Jump instructie. Adres 0 1 2 3 4 5
Inhoud Jump 5 0 0 0 Copy
6 7 8 9 10 11
5 2 Copy 6 3 Multiply
12 13 14 15
2 3 4 Exit
Betekenis Spring naar instructie op positie aangegeven door [X+1]. Gereserveerd voor een getal, variabele 1. Gereserveerd voor een getal, variabele 2. Gereserveerd voor een getal, variabele 3. Kopieer de inhoud van [X+1] naar de variabele op het adres in [X+2] en ga verder op positie [X+3].
Idem.
Vermenigvuldig de variabele op het adres [X+1] met de variabele op het adres[X+2], plaats het resultaat in de variabele op het adres [X+3] en ga verder op positie [X+4].
Be¨eindig het programma.
Als je zo door het programma heen loopt zie je dat de processor maar op 5 plaatsen in het programma komt. De rest bevat de gegevens die bewerkt worden. Het verschil tussen een instructie en gegevens is dus erg subtiel, het is een kwestie van interpretatie. Een ander type processor zal ons programma heel anders interpreteren en misschien vastlopen op een instructie die hij niet kent. 1.3
Wat is een computerprogramma?
5
1
INLEIDING
Programmeren in C
Het zal duidelijk zijn dat je op deze manier wel even bezig bent een tekenprogramma of een tekstverwerker te schrijven. Bovendien zal dat programma dan maar op ´e´en type processor werken. Daar heeft men iets op bedacht, namelijk de compiler. Een compiler is een vertaalprogramma dat een tekst geschreven in een makkelijk leesbare, universele computertaal omzet in de bijna onleesbare machinetaal die we hierboven gezien hebben. Ons vermenigvuldig-programma ziet er in de taal ’C’ bijvoorbeeld zo uit: int getal[3]; main() { getal[0] = 5; getal[1] = 6; getal[2] = getal[0] * getal[1]; } Als we dit C-programma voeren aan een C-compiler zal deze compiler de tekst vertalen in iets dat lijkt op het stukje machinetaal.
1.4
De programmeertaal C
Sinds in midden jaren veertig de eerste computertalen ontstonden, hebben al heel wat talen een rol gespeeld bij het programmeren van computers en bij het gebruik daarvan voor wetenschappelijke toepassingen. Tot voor enkele jaren was Pascal en daarvoor Basic de belangrijkste taal voor PC’s terwijl Fortran naast C op grotere machines de belangrijkste plaats inneemt. Hoewel Pascal een beter gestructureerde taal is, heeft C ook op PC’s Pascal verdrongen. De taal C is in 1972 ontworpen door Dennis M. Ritchie in de Bell laboratoria van AT&T, min of meer als een instrument voor eigen gebruik. De naam is puur de alfabetische aanduiding van een taal die als opvolger dient van een taaltje B dat daar eerder werd toegepast, wat weer de opvolger was van A(ssembler) zoals machinetaal ook wel wordt genoemd. C heeft zijn plaats als ´e´en van de meest gebruikte programmeertalen verworven omdat het zeer simpel is (de taal C kent slechts 32 gereserveerde woorden), omdat er eenvoudig zeer effici¨ente programma’s in te schrijven zijn, het erg uitbreidbaar is door middel van libraries, en omdat een groot deel van alle bekende operating systems in C geschreven zijn. Tevens zijn er veel standaard bibliotheken voor geschreven, waardoor C ´e´en van de meest platform-onafhankelijke talen is die er bestaan. Er bestaan enkele varianten van C, de meest gebruikte is echter ANSI C. In dit dictaat zal strict ANSI C worden gebruikt aangezien dit de meest gebruikte standaard is.
1.5
Waarom C?
Waarom C en waarom niet een andere taal zoals Ada, Algol, assembler, (Visual) Basic, Objective C, (Visual) C++, C#, Cobol, Delphi, Forth, Fortran, Haskell, IDL, Intercal, Java(script), Lisp, Logo, Modula, Pascal, Perl, PHP, Prolog, Python, Scheme, sendmail.cf, shell scripts, Smalltalk, Ruby of Tcl, om er maar een paar1 te noemen? Geen enkele taal is de beste, gekozen is echter voor C omdat 1 Zie http://internet.ls-la.net/mirrors/99bottles/ voor een lijst van meer dan 200 verschillende talen waarin op zijn minst “99 bottles of beer on the wall” uitgeprogrammeerd kan worden.
6
1.4 De programmeertaal C
Programmeren in C
1
INLEIDING
het zeer bekend is, op zeer veel platformen beschikbaar is, en een goede basis vormt om later over te stappen naar andere talen als C++, Java en PHP. We willen hier nog even heel duidelijk stellen dat Mathematica geen programmeertaal is op dezelfde manier als C dat is. De C code van een porgramma is ook niet te vergelijken met een Mathematica notebook.
• Een C programma is bedoeld om direct door de computer uitgevoerd te worden. • Een Mathematica notebook wordt via Mathematica uitgevoerd. • De volgorde van de opdrachten in een Mathematica notebook is niet heel belangrijk. • Een computer kent de waarde van π niet, Mathematica wel. • Een computer kan uitrekenen dat sin π ongeveer 0 is, Mathematica weet dat het zo is.
1.6
Verder
Een C programma dat voor een gebruiksdoel is geschreven, zal meestal van vele elementen uit de taal gebruik maken. Hierdoor is het moeilijk de individuele elementen beknopt te illustreren met een kort en bruikbaar maar toch compleet programma. De meeste voorbeeldprogramma’s hebben daardoor geen praktisch nut, ze maken alleen duidelijk hoe men met de verschillende elementen van de taal moet werken. Ook is het niet mogelijk om voorbeeldprogramma’s te maken die alleen uitgaan van die onderwerpen die in het voorgaand deel van de cursus zijn behandeld. In de programma’s zullen dus nog al eens elementen voorkomen die pas later worden verklaard. Voor het maken van een computerprogramma gaat men in het algemeen als volgt te werk. Voor het intypen van de broncode van een programma gebruikt men een tekstverwerker. De zogeheten preprocessor moet vervolgens de code behandelen om er bepaalde voorbewerkingen op uit te voeren. Daarna start men de compiler waarmee het programma wordt gecompileerd tot een zogenaamde object-file. De compiler zet daarbij de instructies die in “gewone tekst” aanwezig waren om in binaire codes die door de computer uitgevoerd kunnen worden. Vervolgens wordt de linker gestart. Deze voegt de instructies van deze object-file en instructies uit verschillende libraries (bibliotheken van veel gebruikte functies) samen tot een uitvoerbaar programma. Pas nadat al deze stappen zijn uitgevoerd, kan men het resulterende programma uitvoeren. In principe kunnen de tekstverwerker, de C preprocessor, de C compiler en de C linker aparte programma’s zijn. Bij het maken van een programma maakt iedere programmeur voortdurend fouten. De meest gemaakte zijn syntactische fouten (zondigen tegen de regels van de programmeertaal), en executiefouten. De syntax fouten blijken tijdens de compilatie. Executiefouten komen aan het licht door foute uitkomsten of door het hangen of aborteren van het programma (of helemaal niet: en dat zijn de ergste). Voorbeelden zijn dat men niet uitrekent wat beoogd was; of men rekent met een variabele die nog geen waarde heeft; of men voert een niet toelaatbare berekening uit. De taal C++ is een veelgebruikte “object-geori¨enteerde” uitbreiding van C. Zelfs al wil men nauwelijks gebruik maken van de uitbreidingen die C++ biedt, is het toch handig om eens te kijken naar de zogeheten “Call by Reference” en de eenvoudiger mogelijkheden voor input/output. Voor dit doel is voor deze cursus een Appendix C++ voor C kenners gemaakt door Jeroen Fokker, ´e´en van de medewerkers van het Informatica Instituut van de Faculteit Wiskunde & Informatica van de UU. 1.6
Verder
7
1
INLEIDING
1.7 1.7.1
Programmeren in C
Gebruikte conventies Lettertypes
In dit dictaat zullen we een aantal conventies handhaven. Ten eerste maken we gebruik van een speciaal lettertype voor alles wat letterlijke C code is. Soms willen we echter in een voorbeeld duidelijk maken dat er plekken zijn waar je zelf iets in kan vullen. Dat wordt op deze manier afgedrukt. Alle menu items die in Windows aangeklikt kunnen worden zullen op deze wijze worden afgebeeld.
1.7.2
Wiskundige notaties
Aangezien in C alle rationele getallen genoteerd worden met een decimale punt in plaats van een komma zullen we dit consequent handhaven in het dictaat. Er zal geen punt worden gebruikt voor het scheiden van duizendtallen. In paragraaf 8.2 wordt over het bereik van datatypes gesproken. Deze worden genoteerd als intervallen. Er worden blokhaken gebruikt voor gesloten intervallen en ronde haken voor open intervallen. Dus [1, 10] betekent “vanaf 1 tot en met 10”. In dit dictaat zal meestal het half-open interval voorkomen, dus [−5, 7) betekent “vanaf −5 tot 7”, dus wel inclusief −5 maar exclusief 7.
1.7.3
Namen van tekens
Naast letters en cijfers wordt er ook een groot aantal andere tekens gebruikt. In tabel 1 staan alle speciale tekens en hun namen op een rijtje.
8
1.7
Gebruikte conventies
Programmeren in C
1
Teken ~ @ # ^ & * _ | \ / " ’ ‘ () {} <> []
INLEIDING
Naam tilde apestaartje, at hekje, hash, pound dakje, accent circumflex, carret ampersand, en ster min-teken, streepje, dash underscore, onderliggend streepje pipe backslash schuine streep, slash aanhalingstekens, double quote apostrof, quote, accent aigu accent grave, backquote ronde haken, parentheses accolades schuine haken, angles blokhaken, rechte haken, brackets
Tabel 1: Speciale tekens en hun namen
1.7
Gebruikte conventies
9
2
DE PROGRAMMEEROMGEVING
2
Programmeren in C
De programmeeromgeving
Een programmeeromgeving bestaat uit een set programma’s waarmee het programmeren in een bepaalde taal mogelijk wordt gemaakt. Er zijn omgevingen waarbij deze set uit losstaande programma’s bestaat die elk een bepaalde functie verwezenlijken, of het kan een Integrated Development Environment (IDE) zijn, waar deze set programma’s ondergebracht is in ´e´en grafische gebruikersinterface. Als programmeur heb je de keus uit een verscheidenheid aan programmeeromgevingen. Geen enkele programmeeromgeving is de beste; elke programmeur heeft zo zijn eigen voorkeuren, en sommige programmeeromgevingen lenen zich beter voor bepaalde taken dan andere. Bij de faculteit Natuur- en Sterrenkunde zijn standaard twee programmeeromgevingen beschikbaar: de Microsoft Visual C++ IDE, die op alle Windows machines ge¨ınstalleerd is, en de GNU C omgeving die op de UNIX machines beschikbaar is, en te gebruiken is door in te loggen op de facultaire UNIX server ruunat. Naast het gebruik van de programmeeromgeving is het tevens nodig om met het besturingssysteem om te kunnen gaan. Dit wordt niet in dit dictaat behandeld. Er wordt vanuit gegaan dat men al enige basiskennis van Windows en/of UNIX heeft. Dit is reeds behandeld in de computercursus CA2 .
2.1
Microsoft Visual C++ Microsoft Visual C++ is een IDE met de standaard Windows gebruikersinterface. Microsoft Visual C++ wijkt op een beperkt aantal punten af van de ANSI C standaard. Dit is zo veel als mogelijk in de tekst aangeduid. Door verschillen in interpretatie/invulling van “witte plekken” in de formele beschrijvingen van C en door extra mogelijkheden/beperkingen van specifieke hardware is echter bijna elke implementatie anders. Je start het door vanuit het Start-menu via Programma’s en dan via Programmeren op Visual C te klikken, of door op het icoon van Microsoft Visual C++ dat op te desktop te vinden is te dubbelklikken.
2.1.1
Het starten van een nieuw project
Het opstartscherm van de Integrated Development Environment verschijnt. Kies File en vervolgens New. In het nu verschijnende venster moet het soort project en de projectnaam gekozen worden.
2 Zie het dictaat “Inleiding tot het Gebruik van de Personal Computer in het Facultaire Netwerk”, verkrijgbaar bij het Bureau Onderwijszaken.
10
Programmeren in C
2
DE PROGRAMMEEROMGEVING
Kies de tab Projects en in het onderliggende menu het item Win32 Console Application Dit betekent dat de uitvoer van het programma straks in een DOS-venster op het scherm zal komen te staan. Geef het te starten project een naam, (bij voorkeur je eigen naam, we zullen in de tekst echter “Student” aanhouden), kies via de browser de locatie (gebruik je eigen schijfruimte en zorg dat de knop Create new workspace aan staat). Druk tenslotte op OK. Microsoft Visual C++ biedt nu de keuze tussen verschillende projecten. We houden het op An empty project. Er wordt nu een aantal bestanden onder de folder “Student” gecre¨eerd, waaronder het werkplaatsbestand Student.dsw.
Door linksonder de tab FileView aan te klikken is het mogelijk om de bestandsstructuur zichtbaar te maken. Nu moeten we nog een sourcefile genereren bij het project “Student”.
Met New, Files, C++ Sourcefile kan men een naam toekennen aan de te maken sourcefile (gebruik bij voorkeur iets als “opgave1”), om vervolgens door aanklikken die aan het project “Student” toe te voegen. Er verschijnt dan een edit-venster waarin de tekst van het programma dat we als sourcefile “opgave1” willen gebruiken kan worden ingetypt. De sourcetekst wordt gecompileerd door in de menubalk Build en vervolgens Compile opgave1.cpp te kiezen (of op het toetsenbord met Ctrl-F7). In het onderste venster is het verloop van de compilatie te volgen en worden ook de door de compiler gevonden fouten (meestal alleen de syntactische fouten) in de source code gesignaleerd. Wanneer alle fouten zijn verbeterd, wordt het .exe-bestand aangemaakt door in het Build-menu Build Student.exe te kiezen (of F7). Het .exe-bestand wordt uitgevoerd door Execute Student.exe onder Build te kiezen (of Ctrl-F5). De uitvoer komt in een DOS-venster te staan.
Het werken met Visual C++ wordt desgewenst be¨eindigd door in het File-menu Exit te kiezen. 2.1
Microsoft Visual C++
11
2
DE PROGRAMMEEROMGEVING
2.1.2
Programmeren in C
Nieuw programma starten
Nu het project “Student” is aangemaakt kunnen we een volgend keer doorgaan door Microsoft Visual C++ op de bovenbeschreven manier te starten en via File en vervolgens Open Workspace (kies hier Student.dsw) het werk ter beschikking te hebben op het punt waar we gebleven waren. Alle opgaven in de cursus worden bij voorkeur binnen hetzelfde project gemaakt. Daarmee wordt het ruimtebeslag op de harde schijf binnen de perken gehouden. Een nieuw programma binnen het project wordt gestart door in het File-menu op New te klikken. Vervolgens moet weer C++ Sourcefile geselecteerd worden en het bestand moet een naam gegeven worden. Let wel op dat als je meerdere programma’s in je project hebt er altijd maar ´e´en actief is. Dat wil zeggen dat als je een Build of Execute commando geeft alleen het actieve programma zal worden gecompileerd en uitgevoerd. Je kan een ander programma actief maken door in FileView met de rechter muisknop te klikken op het programma waar je aan wilt werken. Er verschijnt dan een menu met de mogelijkheid om dat programma actief te maken. Er mag in een programma slechts ´e´en enkele sourcefile staan met daarin de functie main(). Daarom moet je voor ieder nieuw programma dat je maakt een nieuwe Console Application aan je project worden toegevoegd. Vergeet niet je nieuwe programma actief te maken!
2.2
GNU C
De GNU C compiler omgeving is een product van de Free Software Foundation3 en wordt op zeer veel verschillende UNIX systemen gebruikt. Zoals gebruikelijk voor UNIX worden er vele kleine, afzonderlijke programma’s gebruikt om grotere taken mee uit te voeren. Tevens kan vrijwel alles via de command line worden gedaan. Om in te loggen op de facultaire UNIX server kan je in Windows via het Start-menu op Uitvoeren klikken en op het invoerveld telnet ruunat intikken. Er zal een nieuw venster verschijnen waarin wordt gevraagd om een login naam. Geef hier dezelfde naam op waarmee je onder Windows bent ingelogd. Vervolgens zal om een password gevraagd worden; gebruik ook hier hetzelfde password dat je ook onder Windows gebruikt.
2.2.1
Het schrijven van source code
Voor het schrijven van source code zijn geen aparte programma’s beschikbaar. Daarentegen kan men uit een ruime keus van editors kiezen, waaronder pico, vi en emacs. Om source code te schrijven kan je bijvoorbeeld starten met: pico opgave1.c Als de source code af is kun je deze opslaan. In pico staan onderaan het scherm een serie toetsencombinaties met de fuctie die ze hebben. Bijvoorbeeld, Ctrl-X is opslaan en programma verlaten. 3
12
Zie http://www.gnu.org/.
2.2
GNU C
Programmeren in C
2
DE PROGRAMMEEROMGEVING
Hetzelfde commando kun je weer gebruiken om de bestaande source code aan te passen.
2.2.2
Het compileren
Om source code te compileren wordt de GNU C compiler gebruikt. Deze is op de volgende manier te gebruiken: gcc -o opgave1 opgave1.c Dit commando compileert de source code uit opgave1.c en zal een uitvoerbaar programma genereren genaamd opgave1. In de meeste gevallen zal dit voldoende zijn. Soms moeten er echter meer opties meegegeven worden aan gcc, bijvoorbeeld om extra libraries mee te linken (dit is nodig als de bibliotheekfuncties uit math.h gebruikt worden). Dit gebeurt op de volgende manier:
gcc -o opgave1 opgave1.c -lm Een andere handige optie is -Wall, waardoor de compiler meer waarschuwingen over mogelijke fouten in de source code zal geven. Omdat het intikken van deze regels nogal vermoeiend kan zijn, is het ook mogelijk om de environment variabele $CFLAGS in te stellen met alle opties, en vervolgens make gebruiken in plaats van gcc. Dit gaat op de volgende manier: setenv CC "gcc" setenv CFLAGS "-lm -Wall" make opgave1 Hier zal make er vanuit gaan dat om opgave1 te “maken”, de source code opgave1.c gebruik moet worden. Dit hoef je verder niet meer op te geven. De environment variabele $CC geeft aan dat make de GNU C compiler moet gebruiken.
2.2.3
Nieuw programma starten
Als het programma eenmaal gecompileerd is, kun je het op de volgende manier starten: ./opgave1 Tot slot is het nog handig te weten dat je alle commando’s die je eerder hebt gebruikt weer kan terughalen met de pijltjestoetsen (omhoog voor de vorige, omlaag voor de volgende). Als het commando verschijnt dat je zocht kan je het indien nodig aanpassen en daarna uitvoeren door op Enter te drukken. Dit kan veel tijd schelen als je bijvoorbeeld steeds afwisselend gcc en pico gebruikt. 2.2
GNU C
13
3
EEN EENVOUDIG C PROGRAMMA
3
Programmeren in C
Een eenvoudig C programma
Als inleiding op de regels waaraan een C programma moet voldoen bekijken we eerst een zeer eenvoudig programma. /* Een zeer eenvoudig C programma */ #include <stdio.h> main() { int getal1, getal2, som; printf("Dit programma berekent de som van twee gehele getallen.\n"); scanf("%i", &getal1); scanf("%i", &getal2); som = getal1 + getal2; printf("De som is: %i\n", som); } Regelsgewijs verklaren we wat hier gebeurt: /* Een zeer eenvoudig C programma */ Een tekst tussen “/*” en “*/” geldt als commentaar, dat alleen dient om de werking van het programma te verklaren en dat door de compiler ook verder niet wordt meegenomen. #include <stdio.h> Met deze opdracht wordt er voor gezorgd dat de computer tijdens het programma in- en uitvoer kan plegen. De benodigde routines daarvoor staan in een standaardbibliotheek (dit wordt aangegeven door middel van de schuine haakjes “<” en “>”) met de naam stdio.h. Deze wordt met de opdracht #include meegeladen. main() { Dit is het begin van het feitelijke programma. Een zelfstandig programma bestaat altijd uit de functie main(), eventueel in combinatie met andere functies. Het accoladeteken “{” duidt het begin van de acties aan die in main() moeten worden uitgevoerd. Bij het eind van het programma treffen we de complementaire sluit-accolade “}” aan. int getal1, getal2, som; 14
Programmeren in C
3
EEN EENVOUDIG C PROGRAMMA
We gaan drie getalvariabelen gebruiken, getal1, getal2 en som. Van deze variabelen moet het type bekend zijn en er moet ruimte worden gereserveerd. Het worden gehele getallen. In dat geval wordt het type int gebruikt. Door daar achter de namen van de variabelen te zetten, ligt de benodigde geheugenruimte vast. printf("Dit programma berekent de som van twee gehele getallen.\n"); Een printopdracht om de gebruiker het doel van het programma aan te geven. De functie printf() wordt hier aangeroepen en krijgt hier ´e´en argument mee, namelijk de af te drukken tekst. De tekens “\n” geven aan dat na het weergeven van de tekst de cursor naar het begin van de volgende regel moet springen. scanf("%i", &getal1); scanf("%i", &getal2); Met deze scanf()-opdrachten wordt steeds een geheel getal ingelezen van het toetsenbord en in de variabele getal1 respectievelijk getal2 geplaatst. De functie scanf() krijgt hier twee argumenten mee. Het eerste argument "%i" geeft aan dat we een geheel getal gaan inlezen, het tweede argument, respectievelijk &getal1 en &getal2, geeft aan waar het ingelezen getal opgeslagen moet worden. som = getal1 + getal2; Met deze opdracht wordt de werkelijke berekening uitgevoerd: het bij elkaar optellen van de ingevoerde getallen. Als deze opdracht uitgevoerd is bevat de variabele som het antwoord. printf("De som is: %i\n", som); Het resultaat wordt afgedrukt. De functie printf() krijgt nu naast de af te drukken tekst nog een argument mee. De speciale tekens “%i” zorgen ervoor dat de waarde van het volgende argument, in dit geval som, in plaats van de tekens “%i” zelf wordt afgedrukt. } De aangekondigde sluit-accolade voor de functie main(). De precieze werking van alle opdrachten in dit voorbeeldprogramma zal in de volgende hoofdstukken worden behandeld. Als dit programma wordt uitgevoerd dan gebeurt achtereenvolgens het volgende: • Op het scherm wordt de volgende tekst afgedrukt: Dit programma berekent de som van twee gehele getallen. • Het programma wachttotdat er twee gehele getallen worden ingevoerd. We nemen aan dat we 5 en 13 invullen. De ingevoerde getallen worden naar de variabelen gekopieerd. De variabele getal1 wordt dus 5 en getal2 wordt 13. • De expressie getal1 + getal2 wordt uitgerekend, het resultaat is 18. Het resultaat wordt vervolgens in de variabele som opgeslagen. • Op het scherm wordt de volgende tekst afgedrukt: De som is: 18 15
4
DE OPBOUW VAN EEN C PROGRAMMA
4
Programmeren in C
De opbouw van een C programma
4.1
De globale structuur
Een programma wordt opgebouwd door statements te combineren overeenkomstig de eisen die C aan de structuur van de opbouw van programma’s stelt. In C mogen statements niet zomaar aan elkaar geregen worden, ook al zijn die statements elk afzonderlijk syntactisch juist. Een programma moet uit vaste onderdelen bestaan. Sommige delen moeten hier, andere delen daar staan. De compiler is behoorlijk streng bij het controleren van de regels. Normaal gesproken worden C programma’s in de onderstaande volgorde opgebouwd: • preprocessor statements zoals #include en #define • declaratie van globale variabelen • de functies die gebruikt gaan worden • de functie main() Een vuistregel voor de volgorde is tevens dat variabelen, functies en bibliotheken die in een andere functie gebruikt worden, al gedeclareerd moeten zijn voor ze in die andere functie gebruikt mogen worden.
4.2
De preprocessor directives
De werking van de preprocessor kan men zich voorstellen als een zelfstandige eerste vertaal-stap. De preprocessor-statements #include en #define worden het meest gebruikt en zijn daarom de enige waar we hier aandacht aan besteden. Op het statement #include volgt de naam van een bestand tijdens het vertalen in het programma ingevoegd wordt. Het statement #define wordt gebruikt om een gedefinieerd symbool door de vertaler door een willekeurig te kiezen reeks karakters te vervangen. Een preprocessor-statement mag juist niet be¨eindigd worden met een “;”.
4.2.1
#include
#include Deze opdracht zal ervoor zorgen dat het bestand met de naam bibliotheek wordt ingelezen. Omdat de bestandsnaam tussen schuine haken (“<” en “>”) staat, zal het bestand worden gezocht in de standaard zoekpaden. Dit wordt meestal gebruikt om de definities van standaard bibliotheekfuncties in te lezen, zodat die bibliotheekfuncties gebruikt kunnen worden in het programma. #include "bibliotheek " Deze vorm leest ook het bestand bibliotheek in, maar zoekt deze in dezelfde directory als het C programma. Dit wordt meestal gebruikt bij grotere projecten waarbij een C programma in meerdere bestanden is opgesplitst. 16
Programmeren in C
4
DE OPBOUW VAN EEN C PROGRAMMA
In het algemeen bevatten de bestanden zoals stdio.h gewone C statements. Er kunnen dus variabelen en functies in gedeclareerd zijn, en er kan tevens ook weer van #include en #define gebruik worden gemaakt. De functies uit de bibliotheken zelf staan meestal niet in deze bestanden, alleen hun declaratie.
4.2.2
#define
#define SYMBOOL vervanging
Deze opdracht zorgt ervoor dat in het vervolg van het programma overal waar de letterlijke tekst SYMBOOL voorkomt, dit tijdens de verwerkingsslag van de pre-processor vervangen wordt door de letterlijke tekst vervanging . Men kan deze constructie bijvoorbeeld gebruiken voor het defini¨eren van een constante of een serie karakters. #define MAX 100 Tijdens de pre-processor verwerkingsslag wordt de tekst “MAX” in de rest van het bestand letterlijk vervangen door de tekst “100”. Bij het uitvoeren van het programma kan de waarde 100 die op die manier aan het symbool MAX is toegekend dus niet meer veranderd worden. Het op deze wijze vastleggen van een waarde is beter dan dat op elke plaats waar deze gebruikt wordt doen. Wijzigen in de programmatekst is zo veel simpeler. Om duidelijk te kunnen zien wanneer we een variabele gebruiken en wanneer we een definieerde constante gebruiken zullen we voor de definities hoofdletters gebruiken. Let er op dat de pre-processor alles letterlijk vervangt. Er kunnen problemen ontstaat als constructies worden gebruikt zoals in het volgende voorbeeld: #include <stdio.h> #define VIJF 2 + 3 main() { printf("Vijf maal drie is %i.\n", VIJF * 3); } Het bovenstaande programma zal concluderen dat het antwoord 11 is. Dit probleem is makkelijk te omzeilen door haakjes te gebruiken: #define VIJF (2 + 3)
4.3
Opbouw van functies
Een functie is als volgt opgebouwd: 4.3
Opbouw van functies
17
4
DE OPBOUW VAN EEN C PROGRAMMA
Programmeren in C
returntype functienaam (argument , ...) { declaraties statements return returnwaarde ; } Elke functie kan een aantal argumenten mee krijgen op het moment dat de functie wordt uitgevoerd. Deze argumenten worden gekopieerd naar lokale variabelen, zodat er statements geschreven kunnen worden die iets met die argumenten doen. Als de functie be¨eindigd wordt kan er een waarde teruggegeven worden aan de aanroepende functie. Van tevoren moeten de types van de argumenten en de returnwaarde bekend gemaakt worden. Dit wordt gedaan door aan elk argument een type te koppelen, en aan de functie zelf het returntype. Wat types precies zijn wordt behandeld in paragraaf 8.2. Een voorbeeld van een eenvoudige functie: int maximum(int a, int b, int c) { int grootste; grootste = a; if (b > grootste) grootste = b; if (c > grootste) grootste = c; return grootste; } Deze functie kan als volgt worden aangeroepen vanuit een andere functie: printf("Het maximum van 7, 3 en 9 is %i.\n", maximum(7, 3, 9));
4.4
Opbouw van de functie main()
Er is ´e´en speciale functie, namelijk main(). Deze functie wordt altijd als eerste aangesproken tijdens het uitvoeren van een programma. De opbouw is gelijk aan die van normale functies, je kunt echter de returnwaarde en de argumenten van de functie main() niet zelf kiezen. De correcte manier om een main()-functie te schrijven is: int main(int argc, char *argv[]) { 18
4.4
Opbouw van de functie main()
Programmeren in C
4
DE OPBOUW VAN EEN C PROGRAMMA
declaraties statements return returnwaarde ; } De argumenten en bevatten informatie over de commandline-argumenten4 waarmee het programma aangeroepen is. In deze cursus worden deze niet gebruikt, en het zou erg omslachtig zijn om toch telkens de correcte definitie van main() over te nemen. We zullen daarom altijd de volgende vorm gebruiken die de returnwaarde en de argumenten negeert: main() { declaraties statements }
4.5
De layout van de source code
Het schrijven van een programma is, zelfs in een gebruiksvriendelijke programmeeromgeving, een tijdverslindende activiteit. De tijd gaat niet alleen zitten in het bedenken en intypen van de programmatekst, maar vooral in het vinden van de fouten daarin. Door het programma een overzichtelijke layout te geven wordt het aantal fouten sterk verminderd en het opsporen van de toch nog gemaakte fouten vereenvoudigd. Zeker in het begin, als men nog aan alles moet wennen en veel informatie moet verwerken, bestaat de neiging zich tot het noodzakelijke te beperken. Men herstelt fouten in een programma, voegt “tijdelijk” even een paar extra statements toe om de werking van het programma nog beter te kunnen volgen, runt het programma opnieuw voegt hier nog wat toe en verbetert daar nog wat. Dit alles met de oprechte bedoeling nu “eerst even de fout te herstellen” en later de layout nog wel beter te verzorgen. Afgezien van het feit dat het er dan in de praktijk vaak op neer komt dat dit achterwege blijft, zijn er zeer zwaarwegende redenen om tijdens de ontwikkeling van een programma voortdurend de layout te blijven verzorgen. Voordat men het weet gaat er een veelvoud van de bespaarde tijd verloren aan het zoeken en herstellen van fouten die gemaakt worden doordat men het overzicht is kwijt geraakt.
4.5.1
Inspringen en afbreken
Als voorbeeld een programma met een slechte layout: #include <stdio.h> int main (void){ printf("\nEen goede layout ");printf("is een kwestie " 4
4.5
Zie Kernighan & Ritchie, The C Programming Language, 2e editie, hoofdstuk 5.10.
De layout van de source code
19
4
DE OPBOUW VAN EEN C PROGRAMMA
Programmeren in C
"van smaak.");printf("\nHet kan "); printf ("erg duidelijk maar ");printf("ook zeer rommelig!");getchar();} Het zal duidelijk zijn dat dit niet erg prettig leest. Om je source code netjes en overzichtelijk te houden kun je je het beste zo strict mogelijk aan de volgende vuistregels houden: • Niet meer dan ´e´en statement per regel. • Spring in na accolades of het gebruik van conditionele statements. • Gebruik lege regels om groepjes bij elkaar horende statements van elkaar te scheiden. In dit dictaat staan vele voorbeeldprogramma’s waar deze regels ook bij gebruikt zijn. Leer jezelf deze stijl zo goed mogelijk aan!
4.5.2
Naamgeving
Een lastiger punt van de layout is de naamgeving van functies en variabelen. Je kan ervoor kiezen om de namen zo kort mogelijk te houden, dat spaart immers tijd en ruimte uit tijdens het schrijven van een programma. Bijvoorbeeld: int s(int a, int b) { return a + b; } Je kan er ook voor kiezen om lange beschrijvende namen te gebruiken, zodat je later altijd weet waar de variabele of functie voor diende. De voorgaande functie kan bijvoorbeeld als volgt worden herschreven: int sommatie(int getal_1, int getal_2) { int som_van_getal_1_en_2; som_van_getal_1_en_2 = getal_1 + getal_2; return som_van_getal_1_en_2; } Geen van beide methodes is echt goed. Te kort lijkt slechter te zijn dan te lang, maar lange namen maken het later ontzettend lastig om stukken programma te herschrijven of aan te vullen. Het beste is om korte namen voor variabelen en functies te gebruiken die echter wel binnen de context waarin ze gebruikt worden een duidelijke betekenis hebben. Zo is de naam van de functie s() misschien niet echt verhelderend, die van de functie sommatie() iets te overdadig, maar als functie som() is het kort en toch duidelijk. Er wordt in programma’s vaak gebruik gemaakt van tijdelijke variabelen die slechts in een heel klein stukje code relevant zijn. In dat geval is het zeker toegestaan om een korte naam te gebruiken, zelfs 20
4.5
De layout van de source code
Programmeren in C
4
DE OPBOUW VAN EEN C PROGRAMMA
van slechts ´e´en letter. Zo is in het voorgaande voorbeeld a en b net zo informatief als getal_1 en getal_2. Wat ook vaak voorkomt is een zogeheten iteratie variabele in loops: for (i = 0; i < 10; i++) { statements } Vaak kan je voor het gebruik van de korte variabelenamen dezelfde P10regels toepassen als uit de wiskunde. Denk hierbij aan de naamgeving in formules als sin(x) en i=1 xi .
4.5.3
Commentaar
Commentaar dient ter verduidelijking. Te veel commentaar of commentaar op triviale zaken kan afbreuk aan de leesbaarheid doen. Bij het gebruik van commentaar moet men zich beperken tot noodzakelijke uitleg en kort en bondig formuleren. Door een goede layout en een sprekende keuze voor de namen waarmee men grootheden aanduidt kan men een programma in C al snel begrijpelijk maken. Voorbeelden: /* Deze functie berekent de faculteit van a */ int fac(int a) { if (a > 1) return a * fac(a - 1); else return 1; }
/* a! is immers a(a-1)! */ /* 1! is 1 */
Het volgende commentaar is echter totaal overbodig: som = som + getal;
/* Het getal wordt bij de som opgeteld. */
Net als bij de naamgeving van variabelen en functies moet er een goede balans gevonden worden. Gebruik commentaar alleen om uit te leggen wat een bepaald stuk van het programma doet als dat niet al duidelijk blijkt door de code zelf te lezen. Gebruik commentaar ook om motivatie te geven voor een bepaalde oplossing als de keus niet vanzelfsprekend is. Een andere toepassing van commentaar is het tijdelijk “buitenspel zetten” van een of meerdere stukken programma, bijvoorbeeld tijdens het testen van een programma. Door “/*” voor en “*/” achter het programmafragment te zetten wat tijdelijk buitenspel gezet moet worden zal dit tijdens het compileren als commentaar worden gezien en niet in het programma worden opgenomen: /* Deze functie berekent de grootste gemene deler van a en b */ int gcd(int a, int b) { 4.5
De layout van de source code
21
4
DE OPBOUW VAN EEN C PROGRAMMA
Programmeren in C
/* if (a < 0) a *= -1; if (b < 0) b *= -1; */ if (b == 0) return a; else return gcd(b, a % b); } In het bovenstaande voorbeeld staat het programmafragment dat ervoor zorgt dat a en b niet negatief zijn binnen het gebied dat als commentaar gezien wordt. De functie zal daarom mogelijk negatieve waarden als antwoord geven. Als de regels met “/*” en “*/” weggehaald worden zal deze functie alleen nog positieve resultaten leveren. De eerste “*/” die tegengekomen wordt na een “/*” zorgt ervoor dat het vervolg niet meer als commentaar gezien wordt. Het is daardoor niet mogelijk om commentaar binnen commentaar te plaatsen: /* Dit is commentaar. /* Dit is ook commentaar. */ Maar dit niet meer. */
22
4.5
De layout van de source code
Programmeren in C
5
5
GERESERVEERDE WOORDEN EN IDENTIFIERS
Gereserveerde woorden en identifiers
De opdrachten in C programma zijn opgebouwd uit eenvoudige Engelse woorden. Deze zijn in drie groepen te verdelen:
Gereserveerde woorden Dit zijn woorden die binnen C een speciale betekenis hebben. Zij mogen alleen in die betekenis worden gebruikt; deze betekenis kan niet worden veranderd. Als een gereserveerd woord niet strikt volgens de regels wordt gebruikt geeft de compiler onmiddellijk een foutmelding. Voorbeelden van gereserveerde woorden: double, if, while. Standaard identifiers Alles wat in een programma geen gereserveerd woord is, is een identifier. Sommige identifiers hebben een standaard betekenis: de compiler kent die betekenis. Deze standaard identifiers hebben net als de gereserveerde woorden voor de compiler dus ook een speciale betekenis. Een gebruiker mag de betekenis van een standaard identifier veranderen door de standaardidentifier te herdefini¨eren; in het algemeen is dit echter te ontraden omdat hierdoor snel misverstanden kunnen ontstaan. Een voorbeeld van een standaard identifier is de naam van de standaard functie printf(). Normale identifiers Deze worden door de programmeur zelf bedacht. Elke identifier die geen standaard betekenis heeft is een normale identifier. Zo zijn de namen van typen, variabelen en functies identifiers. Men mag aan een zelf gecre¨eerde identifier elke gewenste betekenis geven (uiteraard binnen de regels en grenzen van C), zolang die betekenis maar uniek is. Dit betekent bijvoorbeeld, dat wanneer er al een constante teller is gecre¨eerd, men niet ook nog een tweede variabele met dezelfde naam mag cre¨eren. Men mag dus aan elke identifier slechts ´e´en betekenis toekennen. Zodra de compiler merkt dat er aan dezelfde identifier twee verschillende betekenissen worden toegekend, volgt er een foutmelding. Een uitzondering hierop is wanneer twee gelijknamige identifiers in een verschillende context gedeclareerd zijn, zie paragraaf 5.4.
Men maakt statements door gereserveerde woorden te combineren met identifiers. Men maakt een programma door statements te combineren tot functies overeenkomstig de eisen die C aan de structuur van een programma stelt.
5.1
Gereserveerde woorden
In C bestaan de volgende gereserveerde woorden:
auto default float register struct volatile
break do for return switch while
case double goto short typedef
char else if signed union
const enum int sizeof unsigned
continue extern long static void
Er kunnen nog meer gereserveerde woorden zijn, afhankelijk welke C compiler wordt gebruikt. De gereserveerde woorden kunnen niet gebruikt worden als identifiers. 23
5
GERESERVEERDE WOORDEN EN IDENTIFIERS
5.2
Programmeren in C
Standaard identifiers
ANSI C heeft een aantal standaard bibliotheken met diverse nuttige functies. Wanneer in een programma gebruik gemaakt wordt van een functie uit een bibliotheek moet dit aan het begin van de sourcefile kenbaar gemaakt worden met het statement: #include De namen van de functies in deze bibliotheken noemt men standaard identifiers. Voorbeeld: Wil men een functie uit de standaardbibliotheek math.h gebruiken, dan zet men aan het begin van het programma het statement: #include <math.h> De library math.h bevat onder andere de volgende standaard identifiers: sin cos tan
asin acos atan atan2
sinh cosh tanh
exp log log10 pow
sqrt ceil floor fabs
ldexp frexp modf fmodf
De identifiers uit een meegeladen bibliotheek kunnen niet meer voor andere doeleinden gebruikt worden in een programma.
5.3
Normale identifiers
Normale identifiers zijn namen die men zelf aan typen, variabelen en functies geeft. Het is voor de leesbaarheid verstandig een naam te kiezen die informatie over de betekenis van de identifier geeft. Bij de keuze van namen voor normale identifiers moet men zich aan de volgende regels houden: • Elke identifier moet beginnen met een letter of met een “_”–teken (onderstrepingsteken ofwel ‘underscore character’). De letter mag hierbij een kleine letter of een hoofdletter zijn. In de rest van de naam mag men vrijelijk gebruik maken van letters, cijfers en van het “_”–teken. • Voorbeelden van toegestane identifiers zijn: som, SOM_van_getallen, SomVanGetallen, SOM7, _som, S, s7_677. • Niet toegestaan zijn bijvoorbeeld de volgende identifiers: 7som som% const
Een identifier mag niet met een cijfer beginnen. Het “%”–teken is niet toegestaan in een identifier. Een gereserveerd woord is niet toegestaan als identifier.
• De compiler maakt onderscheid tussen grote en kleine letters. Voor de compiler is er dus verschil tussen de volgende drie identifiers: som, Som en SOM. • In principe is er geen beperking aan de lengte van een identifier. In de ANSI C standaard worden door de compiler twee identifiers alleen als verschillend herkend als in de eerste 31 karakters verschillen aanwezig zijn. In Visual C++ is dit echter 247 karakters. 24
5.2
Standaard identifiers
Programmeren in C
5.4
5
GERESERVEERDE WOORDEN EN IDENTIFIERS
Bereik van identifiers
Identifiers hoeven niet uniek te zijn, en hoeven afhankelijk van de plaats waar ze gedeclareerd zijn niet overal beschikbaar te zijn. Men kan twee identifiers hebben met dezelfde naam, maar die eigenlijk naar verschillende variabelen verwijzen. De plaats waar een identifier gebruikt wordt bepaalt dan welke variabele gebruikt wordt. Het volgende voorbeeld laat zien dat identifiers enigszins plaatsafhankelijk zijn: #include <stdio.h> int i = 3; int j = 4; int somj(int i) { printf("In somj(): i = %i, j = %i\n", i, j); return i + j; } main() { int j = 5; printf("In main(): i = %i, j = %i\n", i, j); printf("De som is: %i\n", somj(i)); } Als dit programma uitgevoerd wordt zal het volgende resultaat afgedrukt worden: In main(): i = 3, j = 5 In somj(): i = 3, j = 4 De som is: 7 Identifiers van globale variabelen kunnen dus hergebruikt worden binnen functies. Diezelfde identifier zal dan als deze binnen de functie gebruikt wordt verwijzen naar de lokale variabele in plaats van de globale variabele. Indien een identifier van een globale variabele niet voor een lokale declaratie gebruikt wordt kan met die identifier gewoon de globale variabele worden aangesproken. Identifiers die alleen binnen een functie gedefinieerd zijn kunnen echter niet buiten die functie gebruikt worden. Het volgende programma is dus fout: #include <stdio.h> void kwadraat(void) { int getal, resultaat; resultaat = getal * getal; 5.4
Bereik van identifiers
25
5
GERESERVEERDE WOORDEN EN IDENTIFIERS
Programmeren in C
} main() { getal = 3; kwadraat(); printf("Het kwadraat van %i is %i.\n", getal, resultaat); }
26
5.4
Bereik van identifiers
Programmeren in C
6
6
STATEMENTS
Statements
6.1
Inleiding
Statements zijn (per definitie) combinaties van gereserveerde woorden en identifiers. Omdat in C de layout van een programma vrij is heeft men een teken nodig dat de compiler vertelt waar een statement eindigt. Dat teken is de puntkomma “;”. Behalve in een identifier mag men ´e´en of meerdere spaties plaatsen om de leesbaarheid te bevorderen. Voorbeeld: som = som + nog_iets; a = (b + c) / d; Er komen een aantal soorten statements voor, die in de volgende secties besproken worden.
6.2
Declaraties
Het declareren van variabelen is ´e´en van de belangrijkste soorten statements. Vrijwel altijd is het nodig om gegevens tijdelijk op te slaan, en door middel van declaraties wordt de benodigde geheugenruimte hiervoor aangemaakt en een naam gegeven aan dat stukje geheugen. Voordat een variabele gebruikt mag worden moet die dus eerst gedeclareerd zijn. De structuur van een eenvoudige declaratie is als volgt: type naam ; Hierbij kan type bijvoorbeeld char, int, float of een ander standaard of zelfgedefinieerde type zijn. Op de plek van naam kan je een zelfbedachte identifier invullen. Na deze declaratie kan je deze identifier gebruiken om deze variabele van het type type aan te spreken. In hoofdstuk 8 en hoofdstuk 9 worden er nog ingewikkeldere vormen van declaraties besproken. Direct na een declaratie van een variabele is daaraan nog geen waarde toegekend. In het volgende programma is het dan ook geheel onvoorspelbaar wat er zal worden afgedrukt: #include <stdio.h> main() { int getal; getal = getal + 1; printf("%i\n", getal); } Er is ten gevolge van de declaratie voor de variabele getal ergens in het geheugen een plaats voor de waarde van getal wordt gereserveerd. Met de toevallige inhoud van die geheugenplaats tijdens die declaratie gebeurt echter niets. Men mag niet aannemen dat getal na de declaratie de waarde 0 heeft! Het printf("%i\n", getal); statement drukt dus de onbekende inhoud af met 1 erbij opgeteld. De les die hieruit volgt is, dat men altijd eerst een waarde aan een variabele moet toekennen voordat het zinvol is iets met die variabele te doen. 27
6
STATEMENTS
6.3
Programmeren in C
Assignments
Voor het toekennen van een waarde aan een variabele wordt het symbool “=” gebruikt met de betekenis: “wordt” of “krijgt de waarde toegekend”. Men dient dit symbool niet te verwarren met het symbool “==” dat door C uitsluitend gebruikt wordt om dingen te vergelijken. We kennen in een C programma bijvoorbeeld aan de variabele zijde een waarde toe met een assignment: zijde = 2.3; De computer ziet dit als de opdracht: “Plaats het getal 2.3 in de geheugenplaats met de naam zijde”. Vaak is het gewenst om tijdens de declaratie van een variabele van te voren al een bepaalde waarde eraan toe te kennen. Dit kan gedaan worden door meteen bij de declaratie ook direct, zoals ook al gedaan was in paragraaf 5.4: float zijde = 2.3;
6.4
Statements waarin een functie wordt aangeroepen
Een voorbeeld van een functie-aanroep: printf("De som is: %i\n", som); In het voorbeeld wordt de functie printf() aangeroepen met twee argumenten, als eerste de string "De som is: %i\n" en als tweede de variabele som. Functies kunnen niet alleen argumenten meekrijgen maar ook een resultaat teruggeven. Het kan onder andere in een assignment gebruikt, bijvoorbeeld op de volgende manier: y = sin(x);
6.5
Samengestelde statements
Men kan een aantal statements combineren tot een zogenaamd samengesteld statement of compoundstatement door “{” en “}” om de statements te zetten. Voorbeeld: /* Bereken de som van de ingevoerde getallen. */ #include <stdio.h> main() { int som, getal; 28
6.3
Assignments
Programmeren in C
6
STATEMENTS
printf("Voer een getal in: "); scanf("%i", &getal); som = 0; while (getal != 0) { som = som + getal; printf("Voer nog een getal in: "); scanf("%i", &getal); }
/* /* /* /* /*
deze statements */ vormen */ met elkaar */ een */ samengesteld statement */
printf("De som is: %i\n", som); } In C mag men overal waar een statement gebruikt mag worden ook een samengesteld statement gebruiken. Het gebruik van samengestelde statements, vaak weer binnen andere samengestelde statements, is in C dan ook meer regel dan uitzondering. Het is ´e´en van de elementen waarmee de structuur in de C code tot uitdrukking wordt gebracht. Het is ten behoeve van de leesbaarheid sterk aan te raden dit met een goede layout, bijvoorbeeld inspringen, verder te ondersteunen. Ter onderscheiding van samengestelde statements worden “gewone” statements ook wel enkelvoudige statements genoemd.
6.6
Keuze statements
In principe worden de statements in een stuk C code in de volgorde van-boven-naar-beneden en van-links-naar-rechts uitgevoerd. Men kan hierin verandering brengen, bijvoorbeeld door afhankelijk van een bepaalde voorwaarde een keuze te maken tussen een aantal statements (keuze statements) of een aantal statements herhaaldelijk uit te voeren (herhalingsstatements, zie volgende paragraaf). Keuze constructies maken het mogelijk op grond van ´e´en of andere voorwaarde uit een aantal alternatieven te kiezen. Elk alternatief kan uit ´e´en of meer statements bestaan. De conditie is in het algemeen een expressie (uitdrukking) die “waar” of “onwaar” is. Met het if-statement wordt tussen twee alternatieven gekozen. Met het switch-statement wordt in principe uit een willekeurig aantal alternatieven gekozen.
6.6.1
Het if-statement
De structuur van een if-statement: if (conditie ) statement1 Of: if (conditie ) statement1 6.6
Keuze statements
29
6
STATEMENTS
Programmeren in C
else statement2
Als conditie waar is, wordt statement1 uitgevoerd, en als conditie onwaar is wordt statement2 uitgevoerd als deze gespecificeerd is. Daarna vervolgt het programma met het statement dat achter het if-statement staat. Zoals reeds eerder vermeld mogen statement1 en statement2 samengestelde statements zijn, die elk opnieuw if-statements kunnen zijn of mogen bevatten. Zo ontstaan geneste if-statements. Een voorbeeld:
#include <stdio.h> main() { int een, twee, drie; een = 1; twee = 2; drie = 3; /* Een volledig if-statement: */ if (twee == 2) printf("Twee is gelijk aan 2 zoals verwacht.\n"); else printf("Twee is niet gelijk aan 2... nogal vreemd.\n"); /* Een if-statement met een samengesteld statement: */ if (drie == 3) { printf("Drie is "); printf("gelijk aan "); printf("een plus twee.\n"); } /* Geneste if-statements: */ if (drie == een + twee) { if (twee - een == drie) printf("Er klopt iets niet!\n"); else printf("Alles is okee.\n"); } else printf("Ik kan niet tellen.\n"); } 30
6.6
Keuze statements
Programmeren in C
6
STATEMENTS
Bij het nesten van if-statements kan er onduidelijkheid ontstaan over welke else bij welke if hoort. De regel is dat een else bij de dichtstbijzijnde if hoort. Geneste if-statements zijn een belangrijke bron van fouten. De layout kan hier helaas soms verraderlijk zijn. Om duidelijk de juiste else aan de juiste if te koppelen moeten accolades gebruikt worden. Voorbeelden: if (n > if (a z = else z =
0) > b) a; /* else hoort bij de tweede if */ b;
Om ervoor te zorgen dat de else bij de eerste if hoort: if (n > 0) { if (a > b) z = a; } else z = b;
/* else hoort bij de eerste if */
Tenslotte een voorbeeld van een misleidende layout: if (n > 0) if (a > b) z = a; else z = b;
/* else hoort bij de tweede if */
Uit dit voorbeeld volgt uit de manier van inspringen heel duidelijk wat er wordt bedoeld. Maar een compiler let niet op inspringen en koppelt de else aan de tweede if.
6.6.2
Het switch-statement
De structuur van een switch-statement ziet er als volgt uit: switch (expressie ) { case waarde1 : statements1 break; case waarde2 : statements2 break; ... default: defaultstatements } 6.6
Keuze statements
31
6
STATEMENTS
Programmeren in C
Het switch-statement geeft de mogelijkheid uit meer dan twee alternatieven te kiezen. Er wordt eerst gekeken wat de waarde van expressie is. Vervolgens wordt van bovenaf begonnen met het doorlopen van alle case-statements. Als er een case-statement met dezelfde waarde als expressie wordt gevonden, worden alle statements die daarachter komen uitgevoerd. Let op: ook de statements van de volgende case-statements worden uitgevoerd. Om ervoor te zorgen dat met het uitvoeren van statements wordt gestopt kan met het optionele break-statement besloten wordt om het switchstatement te verlaten. Indien geen enkele waarde overeenkomt zal naar het optionele default: gesprongen worden en zullen de defaultstatements worden uitgevoerd. De opgegeven waardes bij de case-statements mogen alleen constanten zijn. Voorbeeld: #include <stdio.h> main() { char i; int k; printf("Tik een letter a, b, c, d, e, of f in.\n"); scanf("%c", &i); k = i - ’a’ + 1; /* Een char kan ook als integer worden gebruikt. Elk karakter heeft een rangnummer. De karakters liggen op alfabetische volgorde. */ printf ("Letternummer: %i\n", k); switch (k) { case 1: case 2: printf("i is a of b.\n"); break; case 3: printf("i is c.\n"); break; case 4: case 5: case 6: printf("i is d, e of f.\n"); } } Meestal zal men na iedere case een break-statement opnemen. Het volgende voorbeeld laat zien wat er gebeurt als het break-statement wordt vergeten. #include <stdio.h> main() { 32
6.6
Keuze statements
Programmeren in C
6
STATEMENTS
int i; printf("Tik het getal 1, 2, 3, 4 of 5 in: "); scanf("%i", &i); switch (i) { case 1: case 2: printf("i is een of twee.\n"); printf("Verlaat nu het switch-statement\n"); break; case 3: printf("i is drie.\n"); printf("Case 4 en 5 worden ook uitgevoerd.\n"); case 4: case 5: printf("i is vier of vijf.\n"); break; default: printf("Vergissing?\n"); switch (i) { case 0: printf("i is nul.\n"); break; case 6: printf("i is zes.\n"); break; default: printf("1 > i of i > 5.\n"); } } }
6.7
Herhalingsstatements
Met een herhalingsconstructie (lus) kan het stuk programma in de lus herhaaldelijk worden uitgevoerd totdat aan een bepaald criterium is voldaan. Het criterium is een expressie die waar of niet waar is. C kent drie lusconstructies: • Het for-statement wordt meestal gebruikt als het aantal malen dat het stuk programma dat binnen de lus staat herhaald moet worden, van tevoren bekend is. Dit is bijvoorbeeld handig bij het doorlopen van een array (zie hoofdstuk 9. • Het while-statement bepaalt eerst uitgaande van het criterium of de statements uitgevoerd moeten worden of niet. Na het uitvoeren van de statements wordt opnieuw naar het criterium gekeken, net zolang tot niet meer aan het criterium voldaan wordt. • Het do..while-statement voert eerst de stamements uit, en controleert daarna het criterium om te bepalen of de lus nogmaals doorlopen moet worden of niet, net zolang totdat niet meer aan het criterium voldaan wordt.
6.7.1
Het for-statement
De structuur van een for-statement ziet er als volgt uit: 6.7
Herhalingsstatements
33
6
STATEMENTS
Programmeren in C
for (expr1 ; expr2 ; expr3 ) statement Het for-statement is met name bedoeld voor situaties waarin van tevoren bekend is hoe vaak de lus doorlopen moet worden. Dit is echter niet de enig mogelijke toepassing. Zodra de computer een for-statement tegenkomt wordt het volgende recept uitgevoerd: 1. Voer expr1 uit. Dit is met name bedoeld om de beginvoorwaarden van de lus vast te leggen. 2. Controleer of expr2 “waar” of “onwaar” is. Als het “onwaar” is wordt de lus gestopt. 3. Voer statement uit. 4. Voer expr3 uit en spring terug naar punt 2. Dit is met name bedoeld om een variabele op te hogen of te verminderen. Zowel expr1 , expr2 als expr3 mogen worden weggelaten, maar de puntkomma’s moeten blijven staan. Als expr2 wordt weggelaten, wordt altijd aan de voorwaarde voldaan en krijgt men een eeuwigdurende lus. Om enige invloed op de lus uit te oefenen kan er in het statement een breakstatement gebruikt worden om de lus te verlaten, of een continue-statement om de uitvoer van de statement af te breken maar wel door te gaan met de lus. Na een continue wordt dus direct expr3 uitgevoerd en expr2 weer ge¨evalueerd. Voorbeeld: /* Druk alle getallen die niet deelbaar zijn door drie af totdat de wortel uit 1000 bereikt wordt. */ #include <stdio.h> main() { int i; for (i = 1; i < 1000; i++) { if (i * i > 1000) break; if (i % 3 == 0) continue; printf("%i "); } } Men mag de start- en/of stopconditie binnen de lus veranderen. Dit is echter sterk te ontraden, omdat het programma slecht leesbaar wordt. Aangezien het ophogen en verlagen van een variabele vaak gebruikt wordt, zijn er iets kortere notaties voor mogelijk: • getal++; is gelijk aan getal += 1; is gelijk aan getal = getal + 1; • getal--; is gelijk aan getal -= 1; is gelijk aan getal = getal - 1; 34
6.7
Herhalingsstatements
Programmeren in C
6.7.2
6
STATEMENTS
Het while-statement en het do..while-statement
De structuur van een while-statement ziet er als volgt uit: while (conditie ) statement De structuur van een do..while-statement ziet er als volgt uit: do statement while (conditie ); Bij het while-statement wordt voorafgaand aan elke lusdoorgang de conditie ge¨evalueerd. Bij een do..while-statement gebeurt dit daarentegen steeds achteraf. Daardoor wordt de lus bij een do..while-statement altijd minstens ´e´en maal doorlopen. Er wordt ook hier weer gestopt wanneer de conditie niet meer waar is. Wordt een while- of do..while-lus ´e´enmaal doorlopen dan is het de verantwoordelijkheid van de programmeur te zorgen dat het testcriterium ooit een keer niet waar wordt. Zo niet, dan raakt de computer opgesloten in een oneindige lus, het programma eindigt dan nooit. De enige remedie is dan het programma te stoppen met de gecombineerde toetsaanslag Ctrl-C (of Ctrl-Break). Om te zorgen dat de lus ooit eens stopt, moet er in de lus steeds iets worden veranderd dat de uitkomst van de conditie be¨ınvloedt, zodat na een aantal lusdoorgangen de lus zal eindigen. Eerste van een while-statement: #include <stdio.h> main() { char toets = ’x’; /* Ga door totdat de gebruiker de toets ’s’ invoert. */ while (toets != ’s’) { printf("Type om te stoppen de letter s: "); scanf("%c", &toets); } } Tweede voorbeeld: /* Vul de emmer tot er water overloopt. */ #include <stdio.h>
6.7
Herhalingsstatements
35
6
STATEMENTS
Programmeren in C
main() { int inhoud, erbij; printf("De emmer heeft een inhoud van 15 liter.\n"); inhoud = 0; printf("De emmer is nu leeg.\n"); while (inhoud < 15) { printf("Hoeveel liter erbij? "); scanf("%i", &erbij); inhoud = inhoud + erbij; } if (inhoud == 15) printf("De emmer is net vol.\n"); else printf("Er gutst %i liter over de emmer heen!\n", inhoud - 15); } Eerste voorbeeld van een do..while-statement: #include <stdio.h> main() { int keer = 0; char antwoord; do { keer = keer + 1; printf("Dit is de %i’e keer.\n", keer); printf("Voer om te stoppen de letter ’s’ in: "); scanf("%c", &antwoord); } while (antwoord != ’s’); } Tweede voorbeeld: #include <stdio.h> #define GRENS 28 main() { int getal; 36
6.7
Herhalingsstatements
Programmeren in C
6
STATEMENTS
printf("Voer een geheel getal in: "); scanf("%i", &getal); do { if (getal > GRENS) getal = getal - 1; else if (getal < GRENS) getal = getal + 1; printf("%i ", getal); } while (getal != GRENS); printf("\n"); }
6.8
Overige statements
6.8.1
Het break-statement en het continue-statement
Met het al eerder bij het switch-statement besproken break kan ook een for-, while- of do..whilelus vroegtijdig worden afgebroken. Voorbeeld: /* Zoek een ingevoerd meetpunt op. */ #include <stdio.h> #define AANTAL 10 int data[AANTAL]; main() { int i, w; for (i = 0; i < AANTAL; i++) { printf("Voer meetpunt %i in: ", i); scanf("%i", &data[i]); } printf("Geef de te zoeken waarde in:\n"); scanf("%i", &w); for (i = 0; i < AANTAL; i++) { if (data[i] == w) 6.8
Overige statements
37
6
STATEMENTS
Programmeren in C
{ printf("Gevonden op punt %i!\n", i); break; } } } Met een continue-statement wordt alleen het binnenste gedeelte van een lus onderbroken. Een voorbeeld: /* Toon alle positieve waarden van de ingevoerde meetpunten. */ #include <stdio.h> #define AANTAL 10 int data[AANTAL]; main() { int i; for (i = 0; i < AANTAL; i++) { printf("Voer meetpunt %i in: ", i); scanf("%i", &data[i]); } printf("Alle positieve waarden:\n"); for (i = 0; i < AANTAL; i++) { if (data[i] < 0) continue; printf("%i ", i); } printf("\n"); }
6.8.2
Het return-statement
Een return-statement ziet er als volgt uit: return expressie ; Functies worden bijna altijd afgesloten met een return-statement. Als tijdens de uitvoering het programma dit statement tegen komt betekent dit het einde van de functie en volgt een terugkeer 38
6.8
Overige statements
Programmeren in C
6
STATEMENTS
naar de programma locatie vanwaar deze functie werd aangeroepen. Indien men een expressie toevoegt, wordt de waarde daarvan doorgegeven aan de “aanroepende” functie.
6.8.3
Het goto-statement
Een goto-statement ziet er als volgt uit: label : statements goto label ; Wanneer een goto-statement wordt tegengekomen zal er worden teruggesprongen naar de plek gemarkeerd door label , vanaf daar zal door worden gegaan met het uitvoeren van het programma. Dit statement wordt hier alleen voor de volledigheid vermeld. Het gebruik van goto-statements staat haaks op het gestructureerd programmeren. Men heeft deze mogelijkheid toch in C opgenomen omdat enkele problemen met behulp van een sprong naar een label toch een effici¨entere wijze van oplossen mogelijk maken. Deze situaties zijn gelukkig zeldzaam. In vrijwel alle situaties waar een sprong naar een ander gedeelte van het programma gemaakt moet worden kan dit met behulp van conditionele- of lus-statements of door middel van het aanroepen van een functie. Het gebruik van het goto-statement wordt dan ook sterk afgeraden, en is in deze hele C cursus niet nodig. Toch een voorbeeld: #include <stdio.h> main() { int x = 1; begin: printf("%i ", x); x = x + 1; if (x < 10) goto begin; printf("\n"); } Bedenk zelf welke lus-constructie de hier gebruikte goto kan vervangen.
6.8
Overige statements
39
7
VARIABELEN
7
Programmeren in C
Variabelen
7.1
Inleiding
In beginsel is een computerprogramma een opeenvolging van bewerkingen op gegevens (data). De bewerkingen worden in C gerepresenteerd door statements. Deze worden door de compiler omgezet in machinecode, wat door de processor verwerkt kan worden. De gegevens staan in het geheugen van de computer. Om bij te houden waar de gegevens staan en om aan te geven op welke gegevens we bewerkingen willen uitvoeren gebruiken we variabelen.
7.2
Het geheugen
Het geheugen van een computer is opgedeeld in bytes. Elke byte heeft een nummer dat het adres van dat byte genoemd wordt. Het is mogelijk om gegevens in het geheugen op te slaan op een bepaald adres. Als later de gegevens weer nodig zijn, moeten we het adres onthouden hebben. Een adres vertelt je nog niet wat voor soort gegevens er op die plek in het geheugen staan, en hoe groot die gegevens zijn. Een vaak voorkomend type gegevens is bijvoorbeeld een geheel getal (type int). Deze neemt tegenwoordig op de meeste computers 4 bytes aan geheugen in beslag. Als we spreken over het adres van een int, dan bedoelen we het adres van de eerste byte die voor die int gebruikt wordt. Stel dat we een simpele optelsom willen uitprogrammeren. Als we alleen met nummers zouden werken, dan zou het programma ongeveer zo geschreven moeten worden:
1. Stop in het geheugen op adres 0, ter grootte van een int, het getal 9. 2. Stop in het geheugen op adres 4, ter grootte van een int, het getal 13. 3. Stop in het geheugen op adres 8, ter grootte van een int, de som van het getal ter grootte van een int dat op adres 0 staat en het getal ter grootte van een int dat op adres 4 staat.
In C kunnen we dat korter, maar niet minder cryptisch, op de volgende manier uitschrijven:
*(int *)0 = 3; *(int *)4 = 4; *(int *)8 = *(int *)0 + *(int *)4;
Hierbij staat (int *) telkens voor “verwijzing naar geheugen dat een int bevat”. De * die daarvoor weer staat betekent “de inhoud van adres ...”. Het geheel *(int *)0 betekent dus “de inhoud van adres 0 dat verwijst naar geheugen dat een int bevat”. Het geheugen ziet er na afloop van het programma als volgt uit: 40
Programmeren in C
Adres 0 1 2 3 4 5 6 7 8 9 10 11
7.3
7
VARIABELEN
Inhoud 9
13
22
Namen in plaats van adressen
Het is erg lastig om altijd met adressen te werken. Het nummer van een geheugenplaats vertelt bijvoorbeeld niet wat voor soort informatie er op die plek staat. Het is tevens lastig om uit te zoeken welke geheugenadressen er gebruikt mogen worden en welke niet. Het onthouden van nummers is ook lastig, en eigenlijk willen we meestal niet weten welk adres iets heeft, we willen alleen maar met de gegevens op dat adres werken. Om die redenen gebruiken we namen voor de gegevens (en niet voor de adressen ervan) die in het computergeheugen staan. Aan het begin van een functie in een programma kunnen we aangeven dat we geheugenruimtes nodig hebben voor gegevens van bepaalde types. Dit noemen we een declaratie. Het vorige voorbeeld wordt met het gebruik van variabelennamen als volgt: int a; int b; int som; a = 9; b = 13; som = a + b; Het is belangrijk om te beseffen dat de variabelen, bijvoorbeeld som, niet een adres representeren, maar alleen de inhoud van een stukje geheugen. Om het verschil te illustreren gaan we het vorige voorbeeld aanpassen zodat de getallen a en b van het toetsenbord ingelezen kunnen worden en het resultaat op het scherm afgedrukt wordt. Het afdrukken gaat als volgt: printf("De som is: %i\n", som); De functie printf() krijgt de tekst "De som is: %i\n" mee, en de inhoud van de variabele som, oftewel 22. Stel nu dat we op de volgende manier de functie scanf() aanroepen om een getal in te lezen in de variabele a: scanf("%i", a); 7.3
Namen in plaats van adressen
41
7
VARIABELEN
Programmeren in C
De functie krijgt de tekst "%i" mee, en de inhoud van a. Stel dat we weten dat a voor de aanroep van scanf() de inhoud 9 had, dan kunnen we het als volgt omschrijven zonder dat de functie iets anders zal doen: scanf("%i", 9); Dit slaat natuurlijk nergens op. De 9 is geen variabele. Als de functie scanf() het ingelezen getal in de variabele a wil zetten, dan moet de functie het adres van het geheugenstuk weten wat voor de variabele a gereserveerd is. Dit kan met de “&”–operator: &a betekent “het adres van de variabele a”. We moeten scanf() dus als volgt aanroepen: scanf("%i", &a);
7.4
Pointers
Soms is het handig om het adres van een variabele voor langere tijd te onthouden. Dit adres moet dan opgeslagen worden in het geheugen. Voor het opslaan van een adres gebruiken we gewoon weer een nieuwe variabele. Aangezien de nieuwe variabele een verwijzing bevat naar de oude variabele noemen we deze nieuwe variabele ook wel een pointer. De inhoud van een pointer variabele is dus geen getal (zoals een int of een float), maar een adres. We willen nu een variabele declareren waar we een adres in kunnen stoppen. Van welk type moet het zijn? Je zou kunnen denken aan een type pointer. We willen meestal echter verwijzen naar een geheugenplaats waar een bepaald type variabele in opgeslagen is. Willen we bijvoorbeeld verwijzen naar de geheugenplaats waar een int is opgeslagen, dan is die verwijzing van het type “pointer naar int”. We declareren een “pointer naar int” variabele als volgt: int *p; Let hier even niet op de plaatsing van de spatie. Deze declaratie bestaat uit twee delen: het type van de variabele, int *, wat “pointer naar int” betekent, en de naam van de variabele hier dus p. We hebben al eerder gezien dat we met de “&”-operator het adres van een variabele kunnen opvragen. We kunnen nu de pointer p naar de variabele a laten verwijzen: int a; int *p; p = &a; Nu willen we de pointer p gebruiken om de gegevens te lezen uit het geheugen dat gereserveerd is voor de variabele waarnaar p verwijst (in het bovenstaande voorbeeld is dat dus a). Hiervoor gebruiken we weer het karakter “*”, maar nu als operator: int a; int *p;
42
7.4
Pointers
Programmeren in C
7
VARIABELEN
a = 10; p = &a; printf("De inhoud printf("Het adres printf("De inhoud printf("De inhoud
van van van van
de de de de
variabele variabele variabele variabele
a is %i.\n", a); a is %p.\n", &a); p is %p.\n", p); waarnaar p verwijst is %i.\n", *p);
Let er op dat het karakter “*” in declaraties voor kan komen maar ook als unaire operator. In declaraties betekent het “pointer naar het voorgaande type”; als operator betekent het zoiets als “de inhoud van het geheugenadres”. Op het eerste gezicht lijkt dit erg verwarrend. De logica erachter is echter als volgt: gegeven de declaratie int *p, dan is p van het type int * (dus een pointer naar een int), en *p is van het type int (dus een integer getal). Overal waar je de variabele a zou gebruiken kun je op precies dezelfde manier *p gebruiken: #include <stdio.h> main() { int a, *p; p = &a; printf("Tellen met a:\n"); for (a = 1; a <= 10; a++) printf("a = %i, *p = %i, p = %p\n", a, *p, p); printf("Tellen met *p:\n"); for (*p = 1; *p <= 10; (*p)++) printf("a = %i, *p = %i, p = %p\n", a, *p, p); } Het bovenstaande voorbeeld laat tevens zien dat p zelf niet verandert. Het is mogelijk om p zelf te veranderen, maar dan verandert het adres, niet de inhoud van de variabele waarnaar verwezen wordt. Het was daarom ook nodig om (*p)++ te schrijven in plaats van *p++. Aangezien de operator “++” een hogere prioriteit heeft dan de operator “*”, zou namelijk eerst de inhoud van p verhoogd worden. Dit zou betekenen dat p een ander adres zou bevatten, en dus naar een andere variabele zou verwijzen, of misschien zelfs naar een niet gereserveerd stuk geheugen. Als daarna *p gebruikt werd zou de computer proberen de waarde uit het geheugen waarnaar verwezen werd te lezen, en zou dus de inhoud van een andere variabele tonen dan oorspronkelijk werd bedoeld, of dat het programma zou crashen omdat het adres niet zinnig was. Om het nog wat duidelijker te maken bekijken we de inhoud van het geheugen na het uitvoeren van dit programma: Adres 123 234 7.4
Variabele a p
Pointers
Inhoud 11 123 43
7
VARIABELEN
7.5
Programmeren in C
Zelf geheugen reserveren
Soms is het handig om zelf geheugen te reserveren voor variabelen. Vaak is niet van te voren5 bekend hoeveel gegevens een programma moet gaan verwerken. Het is dan onzinnig om te proberen “genoeg” variabelen (of arrays van voldoende grootte) te declareren. Meestal zijn het er dan te veel, er wordt dan geheugen verspild of mogelijk draait het programma dan niet meer op computers met wat minder geheugen. Ook is het mogelijk dat er toch een keer meer gegevens moeten worden verwerkt dan waarmee rekening gehouden was. Tijdens het uitvoeren van een programma kan ook nog geheugen gereserveerd worden. Dit gebeurt via de functie malloc() uit de standaardbibliotheek stdlib.h. Deze functie krijgt als argument het aantal bytes dat gereserveerd moet worden, en geeft als return-waarde het adres van het gereserveerde geheugen. Als we later van het geheugen gebruik willen maken, moeten we het adres dus opslaan in een pointer variabele. Als voorbeeld gaan we geheugen reserveren voor een integer variabele: #include <stdio.h> #include <stdlib.h> main() { int *p;
/* reserveer een variabele p om een adres dat naar een int verwijst in op te slaan */
p = (int *)malloc(sizeof(int));
/* resererveer geheugen voor een int en sla het adres op in de variabele p */
for (*p = 1; *p <= 10; (*p)++) printf("*p = %i, p = %p\n", *p, p); } Dit voorbeeld lijkt sterk op het voorbeeld daarvoor. Nu gebruiken we echter geheugen dat we zelf gereserveerd hebben in plaats van het geheugen van een andere variabele. Na het uitvoeren van dit programma ziet het geheugen er als volgt uit: Adres 345 456
Naam p geen naam
Inhoud 456 11
Na afloop van het gebruik van zelf gereserveerd geheugen moet dit echter weer vrijgegeven worden. Dit kan met behulp van de functie free(). Deze krijgt als argument het adres mee naar het begin van het gereserveerde stuk geheugen. In het bovenstaande voorbeeld had dus na de for-lus het volgende statement moeten staan: free(p); De kracht van het zelf reserveren van geheugen en het gebruik van pointers ligt in de mogelijkheid om in ´e´en keer geheugen voor een willekeurig aantal variabelen te reserveren. Dit voorbeeld maakt dat duidelijk: 5
44
Hiermee wordt bedoeld tijdens het compileren, dus voordat het programma wordt uitgevoerd.
7.5
Zelf geheugen reserveren
Programmeren in C
7
VARIABELEN
#include <stdio.h> #include <stdlib.h> main() { int *p; int n; printf("Hoeveel int variabelen reserveren? "); scanf("%i", &n); p = (int *)malloc(sizeof(int) * n); if (p == 0) { /* Als p exact 0 is betekent dit dat er niet genoeg geheugen vrij was om de gewenste hoeveelheid te reserveren. */ printf("Niet genoeg geheugen vrij.\n"); } else { printf("Beginadres van gereserveerd geheugen: %p\n", p); free(p); } } In het volgende hoofdstuk wordt besproken hoe de n variabelen van het type int die gereserveerd zijn aangesproken moeten worden.
7.5
Zelf geheugen reserveren
45
8
DATATYPES EN OPERATOREN
8
Programmeren in C
Datatypes en operatoren
8.1
Inleiding
In beginsel is een computerprogramma een opeenvolging van bewerkingen op gegevens (data). In een programma worden deze gegevens gerepresenteerd door variabelen. Variabelen kunnen worden bewerkt vanaf de plaats waar ze in het programma zijn gedeclareerd. Met zo’n declaratie wordt aan een stukje geheugen in de computer een naam gekoppeld. In relatie tot het (rekenkundige) probleem dat het programma oplost wordt bij de declaratie aangegeven om wat voor type variabele het gaat. In dit hoofdstuk wordt eerst een overzicht gegeven van de datatypes die in de taal C beschikbaar zijn; daarbij worden de elementaire datatypes met de operatoren die er op van toepassing zijn besproken. Ook wordt behandeld hoe het ene type variabele is om te zetten in een ander type (typeconversie), een operatie die voor een deel automatisch wordt uitgevoerd. Tot slot komt in dit hoofdstuk de prioriteit van de verschillende operatoren aan de orde.
8.2
Elementaire datatypes
De datatypes waarover men in C kan beschikken zijn onder te verdelen in elementaire en gestructureerde datatypes. Kenmerkende van de elementaire types is dat de naam van een variabele gekoppeld is aan ´e´en geheugenplaats in de computer. Een variabele van een elementair type bevat altijd slechts ´e´en getal of karakter. Elk type heeft echter zijn eigen karakteristiek.
8.2.1
Het type char
De variabelen van het type char zijn voor een tweetal soorten toepassingen bruikbaar. Allereerst zijn ze ontworpen voor de representatie van karakters. Karakters zijn onder andere de letters en andere tekens die via het toetsenbord op het scherm zichtbaar gemaakt kunnen worden. Voorbeelden van karakters zijn: A
c
]
?
"
Bij de toekenning van een karakter aan een variabele van het type char, wordt om het karakter een tweetal apostroffen “’” geplaatst. Willen we bijvoorbeeld in een programma het karakter “J” aan een variabele letter toekennen dan gaat dat als volgt: letter = ’J’; In dit statement wordt ’J’ een teken-constante genoemd. Het type char is een ordinaal type: aan elk karakter is een rangnummer toegekend. Om te weten welk rangnummer een bepaald karakter heeft kunnen we net doen of het karakter van het type int is: printf("Het rangnummer van het karakter A is %d.\n", ’A’); De rangnummers zijn van te voren al vastgelegd. De meeste computersystemen gebruiken de ASCII standaard om rangnummers aan karakters toe te kennen. Je mag hier echter niet vanuit gaan: er 46
Programmeren in C
8
DATATYPES EN OPERATOREN
bestaan computersystemen met afwijkende standaarden, en door de opkomst van multilinguale systemen zullen er ook nieuwere standaarden (zoals Unicode) gebruikt kunnen worden. Gebruik daarom zo vaak mogelijk het karakter wat je wil hebben tussen apostroffen in plaats van het rangnummer uit de ASCII tabel. Gelukkig mag men er wel vanuit gaan dat de karakters op alfabetische en numerieke volgorde gerangschikt zijn. In de taal C wordt van het feit dat er een getal gekoppeld is aan de inhoud van een variabele van het type char handig gebruik gemaakt. Het is mogelijk om met het getal (rangnummer) dat aan een karakter gekoppeld is te rekenen als met getallen van het type int. Met het volgende programma worden de eerste tien letters uit het alfabet afgedrukt: #include <stdio.h> main() { char letter; for (letter = ’a’; letter < ’a’ + 10; letter++) printf("%c ", letter); printf("\n"); } Omdat het type char zowel voor de representatie van karakters als voor gehele getallen gebruikt kan worden is het bereik op twee verschillende manieren te defini¨eren: het bereik omvat enerzijds een set van 256 karakters en anderzijds 256 rangnummers. Elk karakter is gekoppeld aan een rangnummer. De rangnummers liggen in het interval [−128, 128).
8.2.2
Het type int
Het elementaire type voor het werken met gehele getallen is het type int. Bij de declaratie van variabelen in een programma waar met gehele getallen wordt gemanipuleerd wordt daarom meestal dit type gebruikt. Er is een aantal variaties op dit type mogelijk waarop we later nog terugkomen. Voorbeelden van getallen waarvoor het type int gebruikt kan worden zijn: 0
1
-17
4529
Het elementaire type int neemt meestal vier bytes aan geheugen in beslag, en bestrijkt meestal het bereik [−231 , 231 ). Dit is echter niet vastgelegd in de C standaard; afhankelijk van de gebruikte compiler of de architectuur van de computer kan het bereik groter of kleiner zijn.
8.2.3
Het type float en het type double
De types float en double representeren getallen die in de wiskunde bekend staan als de verzameling van de re¨ele getallen. Populair gezegd zijn dit getallen waarin een decimale punt voorkomt. Van de twee types die in de taal C de re¨ele getallen voorstellen wordt tegenwoordig double het meest gebruikt. Bij het afdrukken van een float of double variabele komt er een decimale punt (Amerikaanse 8.2
Elementaire datatypes
47
8
DATATYPES EN OPERATOREN
Programmeren in C
schrijfwijze) in plaats van de bij ons gebruikelijke komma in het getal voor. Voorbeelden van de representatie van deze getallen zijn: 1.16
-3240.0
5.0
3.3670E+02
5E-6
De laatste twee getal zijn geschreven in de zogenaamde wetenschappelijke notatie. Het bestaat uit twee delen gescheiden door de letter “E”, met links de mantisse en rechts de exponent. Het getal uit het voorbeeld in deze notatie is gelijk aan 3.3670 · 102 , oftewel 336.70. Het bereik van een float kan op twee manieren aangegeven worden. Allereerst als het grootste negatieve tot het grootste positieve getal dat met deze types weergegeven kan worden. In deze definitie is het bereik van een float variabele [−3.4 · 1038 , 3.4 · 1038 ), en van een double van [−1.7 · 10308 , 1.7 · 10308 ). Maar soms is het van belang om met erg kleine getallen te kunnen rekenen. Het kleinste negatieve getal waarmee nog gerekend kan worden is voor een float −3.4·10−38 , het kleinste positieve getal is 3.4 · 10−38 . Een kortere manier om het bereik van deze types te defini¨eren is door te specificeren wat de kleinste absolute waarde en wat de grootste absolute waarde is die het type kan representeren. Hierbij hoeven we niet op het teken te letten, omdat de mogelijke waarden van float en double symmetrisch rondom 0 zijn gesitueerd. De nu volgende tabel geeft een overzicht: type float double
bereik ±3.4 · 10±38 ±1.7 · 10±308
significante cijfers 7 15
benodigd geheugen (bytes) 4 8
Het is aan te raden om waar mogelijk altijd van het type double gebruik te maken in plaats van het type float, gezien het grotere bereik en de grotere nauwkeurigheid van het eerstgenoemde type.
8.2.4
Het type enum
Strikt gesproken is enum niet echt een type, maar duidt het slechts aan dat er een nieuw zelf te cre¨eren type gedefinieerd gaat worden. Het is dan ook niet mogelijk om direct een variabele van het type enum te declareren. De volgende declaratie heeft dan ook geen betekenis: enum opsom; Om een variabele van een enum type te declareren, moet eerst het betreffende type gedefinieerd worden. Wanneer we bijvoorbeeld de dagen van de week als elementen van een type willen gebruiken dan kan de declaratie er als volgt uit zien: enum dagen {zondag, maandag, dinsdag, woensdag, donderdag, vrijdag, zaterdag}; Het daadwerkelijk declareren van een variabele van dit type gaat als volgt:: enum dagen vandaag; De variabele vandaag kan de 7 verschillende waarden zondag tot en met zaterdag aannemen. 48
8.2
Elementaire datatypes
Programmeren in C
8
DATATYPES EN OPERATOREN
Een type enum is een ordinaal (aftelbaar) type. Dit houdt in dat met de elementen van een gedefinieerd enum-type een getal geassocieerd is. Zo horen bij de elementen van het type dagen (zondag tot en met zaterdag) de rangnummers 0 tot en met 6. Afhankelijk van het aantal gedefinieerde elementen zal het bereik van een type enum groot of klein zijn. We kunnen bijvoorbeeld de maanden van het jaar als elementen van het type enum maand nemen: enum maand {jan, feb, mrt, apr, mei, jun, jul, aug, sep, okt, nov, dec}; Het bereik van de rangnummers die aan de elementen zijn gekoppeld zullen voor dit type op het interval [0, 12) liggen. De klasse van types die door enum wordt gedefinieerd is sterk gerelateerd aan het type int. De geheugenruimte die een variabele van het type enum inneemt is hetzelfde als het type int. Een gevolg hiervan is dat het maximale aantal elementen dat binnen een enum-type gedefinieerd kan worden gelijk is aan het aantal waarden dat een variabele van het type int kan aannemen; op de meeste computers zal dat dus 232 zijn. Het is niet te verwachten dat deze beperking als storend zal worden ervaren. In plaats van het defini¨eren met woorden (als zondag, maandag enzovoorts) zou desgewenst ook met getallen van het type int gewerkt kunnen worden. Neem bijvoorbeeld de volgende declaratie: int dagen; De waarde van de variabele dagen kan zo ge¨ınterpreteerd worden dat 0 = zondag, 1 = maandag enzovoorts. Echter een declaratie met woorden maakt het programma in het algemeen veel beter leesbaar en geniet dan ook de voorkeur. Soms is het handig om de rangnummers van een enum type niet bij 0 te laten beginnen, maar bijvoorbeeld bij 1. In het geval van het type dagen zou het logisch zijn om de rangnummers van 1 tot en met 7 te laten lopen. Om dit te bewerkstelligen moeten we expliciet aan elk element van het type een nummer meegeven: enum dagen {zondag = 1, maandag = 2, ..., zaterdag = 7}; Ook kan het soms zinvol zijn om aan de elementen een bepaald getal mee te geven: enum jaartal {Columbus = 1492, Nieuwpoort = 1600, WO1 = 1914, WO2 = 1939}; Het is niet mogelijk om direct de naam van de waarde van een variabele van een type enum uit te printen op het scherm, maar wel het rangnummer: enum jaartal {Columbus = 1492, Nieuwpoort = 1600, WO1 = 1914, WO2 = 1939}; enum jaartal j; j = Nieuwpoort; printf("%i", j); Dit geeft op het scherm niet “Nieuwpoort” te zien, maar het opgegeven rangnummer 1600. Nog een voorbeeld: 8.2
Elementaire datatypes
49
8
DATATYPES EN OPERATOREN
Programmeren in C
/* Bereken de dag waarop je met vakantie gaat. */ #include <stdio.h> main() { enum dagen {maa = 1, din = 2, woe = 3, don = 4, vri = 5, zat = 6, zon = 7}; enum dagen dag, getal; printf("Vandaag is het zondag.\n" "De hoeveelste dag na vandaag wil je op vakantie?\n"); scanf("%i",&getal); dag = getal % 7; if (dag == 0) dag = 7; printf("Je eerste vakantiedag is op "); switch (dag) { case maa: printf("maandag.\n"); break; case din: printf("dinsdag.\n"); break; case woe: printf("woensdag.\n"); break; case don: printf("donderdag.\n"); break; case vri: printf("vrijdag.\n"); break; case zat: printf("zaterdag.\n"); break; case zon: printf("zondag.\n"); break; } }
8.2.5
De voorvoegsels signed, unsigned, long en short
Zoals bij het type int reeds werd aangegeven zijn er een aantal variaties op de elementaire datatypes mogelijk. Afhankelijk van de toepassing wordt het type, maar ook het voorvoegsel van een variabele gekozen. De voorvoegsels signed en unsigned geven aan of de variabele een teken heeft of niet. Als een variabele een teken heeft, kan de waarde positief of negatief zijn. Als een variabele geen teken heeft kan deze alleen maar positief (of nul) zijn. Als men toch geen negatieve getallen gebruikt is het voordeel van een unsigned variabele het feit dat het bereik twee keer zo groot is geworden. Een unsigned char heeft namelijk het bereik [0, 256). Deze voorvoegsels werken alleen op de types char 50
8.2
Elementaire datatypes
Programmeren in C
8
DATATYPES EN OPERATOREN
en int. De voorvoegsels long en short zorgen ervoor dat er meer of minder geheugenruimte wordt toegekend aan de variabelen. Tegelijkertijd wordt dan ook het bereik groter of kleiner. In het algemeen zal long ervoor zorgen dat de geheugenruimte verdubbeld wordt en het bereik kwadratisch groter wordt, terwijl short ervoor zorgt dat de geheugenruimte gehalveerd wordt, en het bereik kwadratisch kleiner wordt. Het precieze effect op bereik en nauwkeurigheid is echter niet in een standaard vastgelegd; men kan er alleen vanuit gaan dat long ervoor zorgt dat het bereik niet kleiner wordt, en dat short ervoor zorgt dat het bereik niet groter wordt. Deze voorvoegsels werken in het algemeen alleen maar op het type int.
8.2.6
Samenvatting
Voorvoegsel (signed) unsigned (signed) unsigned (signed) long unsigned long (signed) short unsigned short
Type char char int int int int int int float double enum
Geheugen (bytes) 1 1 4 4 8 8 2 2 4 8 4
Bereik [−128, 128) [0, 256) [−231 , 231 ) [0, 232 ) [−263 , 263 ) [0, 264 ) [−215 , 215 ) [0, 216 ) ±3.4 · 10±38 ±1.7 · 10±308 232
Toepassing karakters en kleine gehele getallen gehele getallen
re¨ele getallen zelf te defini¨eren
Tabel 2: Datatypes en hun bereik
In tabel 2 worden de types en de bijbehorende bereiken samengevat. Deze tabel is niet voor alle systemen geldig. Deze tabel gaat uit van een 32-bits systeem met een 32-bits compiler en is van toepassing op de meeste PC’s. Het bereik van int types varieert nogal; de Borland TurboC compiler zal voor een int (zonder voorvoegsels) gewoonlijk slechts 2 bytes reserveren, het bereik is dan ook slechts [−215 , 215 ). Soms is het handig om tijdens het uitvoeren van een programma na te gaan hoeveel geheugenruimte voor een bepaald datatype gereserveerd is. Dat kan door middel van de sizeof-operator worden opgevraagd in C: printf("Het type int gebruikt %i bytes.\n", sizeof(int)); Hier kan int vervangen worden door een willekeurig datatype, ook zelfgedefinieerde. De operator sizeof kan ook gebruikt worden om erachter te komen hoeveel geheugenruimte door een variabele ingenomen wordt: unsigned short int a; printf("De variabele a gebruikt %i bytes.\n", sizeof(a)); 8.2
Elementaire datatypes
51
8
DATATYPES EN OPERATOREN
8.3
Programmeren in C
Typeconversie
In de voorgaande paragrafen hebben we kennis gemaakt met de elementaire datatypes die in C beschikbaar zijn. Een aantal types verschilt onderling zodanig van aard dat ze in berekeningen niet zonder meer door elkaar gebruikt kunnen worden. Neem bijvoorbeeld het optellen van een int en een float. Op het eerste gezicht lijkt dit geen probleem; het zijn beide immers getallen. Voor een computer is het echter alsof er gevraagd wordt appels en peren bij elkaar op te tellen. Het probleem kan opgelost worden door ´e´en van de operanden te converteren naar het type van de andere operator. Dit kan de compiler in de meeste gevallen zelf doen, soms is het echter noodzakelijk om dit zelf te bepalen.
8.3.1
Automatische typeconversie
Voor de rekenkundige operaties (+, -, *, / en %) is de oplossing van het hiervoor geschetste probleem standaard in C ingebouwd. Het type van ´e´en van beide operatoren wordt, zonder dat de gebruiker er iets van merkt, geconverteerd naar het type van de andere operand. De types van de twee operatoren zijn dan gelijk en de operatie kan plaatsvinden. Het resultaat van de operatie is van het type van de operatoren na conversie. Beschouw bijvoorbeeld de optelling van een geheel getal en een re¨eel getal:
2 + 3.56 De oplossing die C hier kiest is dat van het gehele getal (2) een re¨eel getal 2.0 gemaakt wordt, waarna gewoon twee re¨ele getallen (van het type float) bij elkaar opgeteld kunnen worden. Effectief wordt dit:
2.0 + 3.56 (= 5.56) Een belangrijke vuistregel die bij het automatisch converteren gebruikt wordt is dat wanneer de types van de linker en rechter operand verschillen ´e´en van beide operanden zodanig geconverteerd zal worden dat beide operanden van het type worden dat oorspronkelijk de meeste geheugenruimte innam. Zo zal tijdens de vermenigvuldiging van een char (1 byte) en een int (4 bytes) de waarde van de char variabele geconverteerd worden naar een int. Het resultaat wordt dan ook van het type long int. De oorspronkelijke variabelen blijven uiteraard ongewijzigd. Typeconversie treedt ook op bij assignments. Daarbij wordt het type van de rechter operand van de “=”–operator altijd omgezet naar het type van de variabele aan de linkerkant. Dat is logisch: na de declaratie van een variabele kan het type immers niet meer veranderen. Voorbeeld:
int i; float f; i = (3.1415 + 1.234E5) / 33.3; f = (3.1415 + 1.234E5) / 33.3; printf("i = %i, f = %f\n", i, f); 52
8.3
Typeconversie
Programmeren in C
8.3.2
8
DATATYPES EN OPERATOREN
De cast-operator
Soms is het wenselijk om een typeconversie uit te voeren die niet automatisch wordt uitgevoerd. In C kan daarvoor gebruik gemaakt worden van de cast-operator. De cast operator is niets anders dan het tussen haakjes zetten van het type waarnaar geconverteerd moet worden met er achter de variabele of constante waar het om gaat. Voor de conversie van de variabele i van het type int naar het type float is de schrijfwijze als volgt: int i = 10; printf("%f\n", (float) i); De cast operator is typisch een unaire operator (werkzaam op slechts ´e´en operand), in het voorbeeld werkzaam op de operand i. Een nuttige toepassing van de cast-operator is de re¨ele deling van twee gegeven int variabelen. Neem bijvoorbeeld het volgende fragment: int i, j; float x, y; i j x y
= = = =
14; 3; i / j; (float) i / (float) j;
Na afloop heeft x de waarde 4.0, aangezien de deling op twee operanden van het type int werkte. De computer zal dan ook een integer-deling uitvoeren, het resultaat hiervan is weer van het type int, en is dus (naar beneden) afgerond op een geheel getal. De waarde van y is echter 4.666667, aangezien de deling uitgevoerd is met float-operanden. Volgens de vuistregel voor automatische conversie had bij de berekening van y ´e´en van beide castoperatoren weggelaten kunnen worden. In het onderstaande voorbeeld zal j automatisch naar het type van i worden omgezet, wat inmiddels al naar het type float is omgezet: y = (float) i / j;
8.4
Operatoren
Door middel van operatoren kunnen wiskundige bewerkingen op gegevens (variabelen en constanten) worden toegepast. Vaak horen bij een operator twee zogeheten operanden. In geval van een operator “optellen” (de “+”–operator) zijn in de som c = a + b, a en b de operanden. Hierbij kunnen a en b variabelen zijn, maar ook ingewikkelder expressies van meerdere variabelen of constanten. Wanneer met operator twee operanden geassocieerd zijn dan wordt een dergelijke operator binair genoemd. Een operator die op slechts ´e´en operand werkzaam is wordt aangeduid als een zogenoemde unaire operator. Operatoren werken in het algemeen op alle elementaire datatypes. De operanden hoeven niet van hetzelfde type te zijn, indien bijvoorbeeld een binaire operator toegepast wordt op een int en een 8.4
Operatoren
53
8
DATATYPES EN OPERATOREN
Programmeren in C
float, dan zal de int automatisch om worden gezet naar een float, aangezien de laatste een grotere precisie heeft. Het resultaat van een bewerking hoeft niet altijd van hetzelfde type te zijn als de operanden. In het geval van de logische en relationele operatoren is het resultaat van het type int.
8.4.1
Rekenkundige operatoren
De binaire rekenkundige operatoren zijn: Operator + * / %
Operatie optellen aftrekken vermenigvuldigen delen modulo
Voorbeeld a = 2.5 + 3.5 a = 3 - 5 a = 2.0 * 3.7 a = 8.4 / 2.5 a = 17 % 5
Waarde van a 6.0 -2 7.4 3.36 2
Verder is er ook nog ´e´en unaire operator: Operator -
8.4.2
Operatie negatie
Voorbeeld a = -(2 + 3)
Waarde van a -5
Voorbeeld a = 5.3 == 2.8 a = 5.3 != 2.8 a = 2.4 > 3.7 a = 2.4 < 3.7 a = 3.5 >= 2.8 a = 3.5 <= 2.8
Waarde van a 0 1 0 1 1 0
Relationele operatoren
De relationele operatoren zijn: Operator == != > < >= <=
Operatie gelijk aan ongelijk aan groter dan kleiner dan groter of gelijk kleiner of gelijk
De uitkomst van een relationele operatie is altijd ´of “waar” (gerepresenteerd door een integerwaarde 1) ´ of “onwaar” (weergegeven met een waarde gelijk aan 0). Let daar goed op, in het volgende voorbeeld is het resultaat niet wat je op het eerste gezicht zou denken: int a = 23; if (1 < a < 10) printf("Het getal ligt tussen 1 en 10.\n"); else printf("Het getal is lager dan 1 of groter dan 10.\n"); Dit programmafragment zal je vertellen dat het getal tussen 1 en 10 ligt. if-statement zal de volgende expressie worden ge¨evalueerd: 1 < a < 10. ´e´en voor ´e´en alle operatoren afwerken in de juiste volgorde en prioriteit. er hier niet toe doet en de relationele operatoren links associatief zijn, zal 54
Hoe komt dat? In het De computer zal echter Aangezien de prioriteit er van links naar rechts 8.4
Operatoren
Programmeren in C
8
DATATYPES EN OPERATOREN
ge¨evalueerd worden. Eerst wordt dus 1 < a ge¨evalueerd. Het resultaat van 1 < 23 is “waar”, oftewel 1. Vervolgens wordt de volgende operator op zijn operanden toegepast, waarbij het net berekende resultaat gebruikt wordt. Er wordt nu dus 1 < 10 ge¨evalueerd, en ook dit is “waar”. Dit misverstand treedt op vanwege de gelijkenis met de wiskundige notatie 1 < a < 10. Dit is echter equivalent met 1 < a ∧ a < 10, wat wel om te schrijven is naar C: int a = 23; if (1 < a && a < 10) printf("Het getal ligt tussen 1 en 10.\n"); else printf("Het getal is lager dan 1 of groter dan 10.\n");
8.4.3
Logische operatoren
Logische operatoren werken op operanden die “waar” of “onwaar” zijn. Het resultaat van een logische operator is zelf ook “waar” of “onwaar”. De logische operatoren werken op precies dezelfde manier als de wiskundige operatoren ∧, ∨ en ¬. Operator && || !
Operatie en (∧) of (∨) niet (¬)
Voorbeeld a = 1 && 0 a = 1 || 0 a = !0
Waarde van a 0 1 1
Let op: a is hier van het type int. Er is geen apart type voor de zogenoemde Booleaanse waarde “waar” of “onwaar”, in tegenstelling tot sommige andere programmeertalen. In C wordt “waar” gekenmerkt door een int waarde ongelijk aan 0, “onwaar” wordt gekenmerkt door een int waarde wel gelijk aan 0.
8.4.4
Verkorte schrijfwijzen
Het komt nogal vaak voor dat we geen lange formules willen berekenen, maar slechts ´e´en operatie willen loslaten op een variabele. Bijvoorbeeld: “Vermenigvuldig de variabele a met 3 en stop het resultaat weer in a.” Normaal gesproken zou men dat zo uitschrijven: a = a * 3; De variabele a wordt hier echter twee keer genoemd. Dat is normaal gesproken geen probleem, totdat de variabalenamen wat ingewikkelder beginnen te worden. Zo is het best mogelijk dat er misschien had gestaan: persoon[i].bloeddruk[j] = persoon[i].bloeddruk[j] - afname[j]; Het is in C mogelijk om deze uitdrukkingen korter op te schrijven, en wel als volgt: a *= 3; persoon[i].bloeddruk[j] -= afname[j]; 8.4
Operatoren
55
8
DATATYPES EN OPERATOREN
Programmeren in C
Deze statements kunnen ge¨ınterpreteerd worden als “maak a 3 keer zo groot” en “verminder bloeddruk[j] van persoon[i] met afname[j]”. Het combineren van een assignment en een operatie werkt voor alle rekenkundige operatoren. Een nog kortere notatie bestaat er voor het ophogen of verlagen van variabelen met precies 1. Om dit te illustreren volgt hier een aantal uitdrukkingen die equivalent zijn: a = a + 1; a += 1; a++; ++a; Door de operator “+” door “-” in dit voorbeeld te vervangen wordt er 1 afgetrokken. Er zijn dus twee mogelijke korte schrijfwijzen voor het verhogen van een variabele: ofwel a++, waarin ++ de postincrement operator wordt genoemd, of ++a, waarin ++ de pre-increment operator wordt genoemd. De plaatsing van de ++-operator is van belang als deze in een andere uitdrukking voorkomt. Bijvoorbeeld: int a, b, c, d; a b c d
= = = =
13; ++a; 42; c++;
printf("a = %i, b = %i, c = %i, d = %i\n", a, b, c, d); De pre-increment operator zal ervoor zorgen dat eerst de variabele a wordt verhoogd, en dat daarna pas de rest van het statement wordt uitgevoerd. De post-increment operator doet het precies andersom. Het resultaat van het voorgaande voorbeeld is dus: a = 14, b = 14, c = 43, d = 42 Een andere korte schrijfwijze komt voor uit het feit dat logische expressies van het type int zijn, en omgekeerd int waarden ook als een logisch “waar” of “onwaar” ge¨ınterpreteerd kunnen worden. Bekijk het volgende programma dat aftelt vanaf een ingevoerd getal tot 1: #include <stdio.h> main() { int a; printf("Voer een getal in: "); scanf("%i", &a); if (!a) printf("Niets te doen.\n"); else 56
8.4
Operatoren
Programmeren in C
8
DATATYPES EN OPERATOREN
{ while (a--) printf("%i ", a); printf("klaar.\n"); } }
8.4.5
Prioriteiten
In veel computerprogramma’s komen statements voor waarin een aantal operatoren direct na elkaar worden toegepast. Het resultaat van zo’n statement wordt bepaald door de prioriteit die de computertaal aan de verschillende operatoren heeft toegekend. Een klassiek in de rekenkunde bekend voorbeeld is dat vermenigvuldigen een hogere prioriteit heeft dan optellen. Zo zal de uitdrukking 4 + 5 * 3 equivalent zijn met 4 + (5 * 3) en als resultaat 19 opleveren, en niet (4 + 5) * 3 = 27. Ook wat de prioriteit van operatoren betreft sluit de taal C aan bij wat in de wiskunde aan regels wordt toegepast. De onderstaande tabel geeft een overzicht van alle operatoren die C kent, waarvan een aantal in deze handleiding al besproken is of nog zal worden. Gezien de inleidende aard van deze cursus zullen niet alle operatoren behandeld worden; voor de betekenis en toepassing van de resterende operatoren wordt naar de Visual C++ hulpbestanden of naar een boek over de taal C verwezen. Operator () [] -> . ! ~ ++ -- - (type) * & sizeof (alle unair) * / % + << >> < <= > >= == != & ^ | && || ?: = += -= *= /= %= &= ^= |= <<= >>= ,
Associativiteit links rechts links links links links links links links links links links rechts rechts links
Tabel 3: Prioriteit en associativiteit van de operatoren In tabel 3 zijn de operatoren onder elkaar gerangschikt volgens dalende prioriteit. Operatoren op dezelfde regel hebben gelijke prioriteit. Tevens is aangegeven of de operatoren links- of rechtsassociatief zijn. Een uitdrukking met links-associatieve operatoren zoals 3 * 4 / 5 equivalent is met ((3 * 4) / 5). Rechts associatieve operatoren werken natuurlijk precies andersom. In een statement met meerdere operatoren is het in geval van twijfel raadzaam haakjes te plaatsen: a = 6 + (5 * b) + (3 / c); 8.4
Operatoren
57
8
DATATYPES EN OPERATOREN
Programmeren in C
In dit voorbeeld zijn de haakjes niet strikt noodzakelijk maar het geeft wel duidelijk aan dat 5 * b en 3 / c eerst uitgerekend worden voordat de twee “+” operatoren moeten worden toegepast. De uitdrukking y = (float) i / j; moet niet opgevat worden als y = (float) (i / j);. Gegeven i = 9 en j = 2 dan is het resultaat van de eerste uitdrukking 4.5, maar in de tweede uitdrukking komt de conversie te laat, omdat de integer-deling i / j die de int-waarde 4 oplevert dan al gebeurd is.
58
8.4
Operatoren
Programmeren in C
9
9
ARRAYS, STRINGS EN STRUCTS
Arrays, strings en structs
9.1
Inleiding
De datastructuren die tot nu toe aan de orde zijn geweest waren de elementaire types waarbij telkens ´e´en variabelenaam aan ´e´en geheugenplaats was gekoppeld. Bij de nu te behandelen gestructureerde datatypes wordt ´e´en variabelenaam aan meerdere geheugenplaatsen gekoppeld. In een array worden de geheugenplaatsen van elkaar onderscheiden door middel van een index. Alle elementen van een array zijn van hetzelfde type (vaak een van de elementaire types als float en int). In verband met arrays komt voor het begrip pointer wederom aan de orde. Pointers zijn variabelen waar een geheugenadres in kan worden opgeslagen. Een string is feitelijk niets anders dan een array van het elementaire type char. Een string is dus gewoon een rijtje tekens dat achter elkaar is geplaatst maar altijd eindigt met een zogeheten null-karakter (’\0’). Wanneer het rijtje alleen uit letters bestaat dan kunnen we de string direct associ¨eren met een woord of een zin. Dit is het meest voorkomende gebruik van strings. Willen we een combinatie van variabelen in ´e´en datastructuur met ´e´en naam onderbrengen dan zijn structuren (structs) de ge¨eigende optie die door C wordt geboden. De variabelen die binnen een struct worden gedeclareerd mogen van verschillende elementaire types zijn. De vrijheid voor het cre¨eren van datatypes is erg groot en wordt vaak gebruikt voor de opslag van gegevens die bij elkaar horen. Het datatype FILE is een struct die te vinden is in de standaardbibliotheek stdio.h. Dit type wordt in het bijzonder gebruikt voor de uitwisseling van gegevens met externe media. Zo wordt met behulp van dit type het lezen en schrijven van data van en naar de harde schijf of floppy geregeld. Deze struct wordt pas in een volgend hoofdstuk besproken.
9.2 9.2.1
Arrays De array als ge¨ındexeerde variabele
Een array is voor te stellen als een rij variabelen van hetzelfde type. Met welk element uit de rij we te maken hebben wordt bepaald door de waarde van de bijbehorende index. De declaratie van een array ziet er als volgt uit: type naam [aantal ]; Het enige verschil met de declaratie van een elementair datatype is dat achter naam een aantal volgt tussen blokhaken. Het aantal geeft aan hoeveel variabelen van het opgegeven type in ´e´en keer gereserveerd moeten worden. De declaratie van een array van 100 float getallen met als naam rij ziet er als volgt uit: float rij[100]; Het getal tussen blokhaken geeft aan uit hoeveel elementen de array bestaat. Na deze declaratie hebben we in ´e´en keer de beschikking over 100 float variabelen. De afzonderlijke elementen van een array kunnen gebruikt worden door middel van de index. Een index is altijd een geheel getal of een variabele van een ordinaal type. Een array begint altijd met index 0, en eindigt dus altijd met het aantal elementen minus 1: 59
9
ARRAYS, STRINGS EN STRUCTS
Programmeren in C
rij[0], rij[1], ..., rij[99] Een element van de array kan gebruikt worden alsof het een gewone variabele is. Alle bewerkingen die voor een elementaire variabele van het type als van de array beschikbaar zijn mogen ook op de afzonderlijke elementen van de array uitgevoerd worden. Ter illustratie volgt nu een voorbeeldprogramma waarin een tiental getallen van het toetsenbord wordt gelezen, het gelezen getal met 10 wordt vermenigvuldigd en in de array wordt opgeslagen. De array wordt tenslotte in omgekeerde volgorde op het scherm afgedrukt.
#include <stdio.h> main() { int i, rijtje[10]; printf("Voer 10 gehele getallen in:\n"); for (i = 1; i < 10; i++) { scanf("%i", &rijtje[i]); rijtje[i] *= 10; } printf("In omgekeerde volgorde en met 10 vermenigvuldigd::\n"); for (i = 9; i >= 0; i--) printf("%i ", rijtje[i]); printf("\n"); }
9.2.2
Twee- en meerdimensionale arrays
Het aantal indices dat bij een element van een array hoort is zonder problemen uit te breiden naar meer dan ´e´en. We behandelen hier de arrays waarbij een element door twee indices is vastgelegd. Een dergelijk array wordt tweedimensionaal genoemd. De declaratie van een tweedimensionaal array ziet er als volgt uit:
int matrix[3][3]; In dit specifieke geval is het array genaamd matrix 3 bij 3 groot. We beschikken nu over de volgende int variabelen:
matrix[0][0] matrix[1][0] matrix[2][0] 60
matrix[0][1] matrix[1][1] matrix[2][1]
matrix[0][2] matrix[1][2] matrix[2][2] 9.2
Arrays
Programmeren in C
9
ARRAYS, STRINGS EN STRUCTS
De bovenstaande weergave van de elementen van de array laat zien dat een tweedimensionaal array geassocieerd kan worden met een matrix met elementen matrix[i][j], waarbij i de rij en j de kolom van de matrix voorstelt. Dit is overigens een arbitraire keuze. Een gebied waar tweedimensionale arrays veelvuldig toegepast worden is de (medische) beeldverwerking. Daarbij stellen de indices (i en j) de co¨ordinaten van een beeldpunt op het beeldscherm voor en de waarde van de variabelen matrix[i][j] de intensiteit die bij het betreffende beeldpunt hoort. De matrices zijn dan wel veel groter dan 3 bij 3 zoals in het voorbeeld. Het onderstaande voorbeeld laat zien hoe de indices van een tweedimensionaal array gerelateerd kunnen zijn aan posities op het beeldscherm. #include <stdio.h> main() { int beeld[20][40]; int i, j, contrast; for (i=0; i < 20; i++) for (j=0; j <40; j++) { beeld[i][j] = (i - 10) * (j - 20); beeld[i][j] *= beeld[i][j]; } contrast = 50; for (i=0; i < 20; i++) { for (j=0; j < 40; j++) { if (beeld[i][j] < contrast) printf("*"); else printf(" "); } printf("\n"); } } Meerdimensionale arrays worden op analoge wijze gemaakt door nog meer indices te gebruiken.
9.2.3
Arrays en pointers
Een array bestaat uit een rij ge¨ındexeerde variabelen. Elk van de 6 elementen van een array a[6] heeft een adres. Het zijn immers 6 “losse” variabelen die door een index van elkaar onderscheiden worden. Het specifieke van een array is dat de adressen elkaar opvolgen. Uitgaande van a[0] hebben de elementen a[1] tot en met a[5] een adres dat telkens volgt op het voorgaande. De naam van de 9.2
Arrays
61
9
ARRAYS, STRINGS EN STRUCTS
Programmeren in C
array is tevens het adres van het eerste array element: a is dus equivalent met &a[0]. Feitelijk is de identifier a zonder de blokhaken dus een pointer. Het enige verschil met een echte pointer is dat je aan a later geen ander adres kan toekennen. In tabel ziet het er als volgt uit: Array element Adres
a[0] a
a[1] a + 1
a[2] a + 2
a[3] a + 3
a[4] a + 4
a[5] a + 5
Met het aantal elementen van de array dat bekend is, liggen ook de adressen van de andere elementen vast. Het is daarom voldoende om over a te beschikken. Omdat a het karakter van een pointer heeft kunnen we a ook toekennen aan een andere pointer: int a[6], *p; p = a; Na deze toekenning beschikken we naast de array elementen a[0]. . . a[5] ook over de array elementen p[0]. . . p[5]. De inhoud van de arrays p en a is altijd identiek, omdat ze aan dezelfde geheugenplaatsen zijn gekoppeld. Het is in C mogelijk indices te gebruiken die groter zijn dan het aantal dat gereserveerd is. Na het declareren van int a[5] is kan in het programma a[123] gebruikt worden zonder dat dit een foutmelding oplevert. Het zal echter duidelijk zijn dat op deze manier geheugen van een andere variabele of van een stuk ongereserveerd geheugen wordt aangesproken, waardoor het programma onverwachte resultaten zal geven. Zorg er dus voor dat indices altijd binnen het bereik van een array blijven. Als we het adres van een variabele weten kunnen we ook de inhoud ervan aanspreken door middel van de “*”-operator. Op die manier kan a[0] vervangen worden door *a, a[1] door *(a + 1) en a[i] door *(a + i). In het geval van meerdimensionale arrays, bijvoorbeeld int matrix[3][3], is niet alleen matrix een pointer, maar ook matrix[i], waarbij i natuurlijk 0, 1 of 2 is. Er geldt dat matrix gelijk is aan &matrix[0][0], en geheel analoog zal matrix[i] gelijk zijn aan &matrix[i][0]. Als we elementen willen aanspreken door middel van pointers in plaats van met indices, dan geldt analoog aan het ´e´endimensionale voorbeeld dat matrix[i][j] gelijk is aan *(matrix + (i * 3) + j).
9.2.4
Zelf geheugen voor arrays reserveren
Zoals eerder besproken is soms niet van te voren bekend hoeveel geheugen er gereserveerd moet worden. In dat geval kunnen we het geheugen zelf reserveren met behulp van de functie malloc(). Het onderstaande voorbeeld illustreert hoe we een door de gebruiker op te geven aantal meetpunten kunnen inlezen. Daarbij kunnen we pas nadat de gebruiker het aantal heeft opgegeven geheugenruimte voor de meetpunten reserveren: #include <stdio.h> #include <stdlib.h> main() { double *meting; 62
9.2
Arrays
Programmeren in C
9
ARRAYS, STRINGS EN STRUCTS
int aantal, i; printf("Voer het aantal in te lezen meetpunten in: "); scanf("%i", &aantal); meting = (double *)malloc(sizeof(double) * aantal); for (i = 0; i < aantal; i++) { printf("Voer meetpunt %i in: ", i + 1); scanf("%lf", &meting[i]); } free(meting); } Let op: omdat het aantal tijdens het compileren niet bekend is, kan de compiler het volgende programmafragment dus niet compileren: #include <stdio.h> main() { int aantal; printf("Voer het aantal in te lezen meetpunten in: "); scanf("%i", &aantal); /* Fout: */ double meting[aantal]; }
9.3
Strings
In de inleiding hebben we reeds vast gesteld dat een string een array van het type char is. We kunnen een stringvariabele dan ook op dezelfde manier declareren als voor arrays is aangegeven, bijvoorbeeld: char naam[20]; Hoewel s met de bovenstaande declaratie 20 elementen bezit is de maximale lengte van de string s slechts 19. Er dient namelijk altijd ´e´en geheugenplaats over te blijven voor een afsluitend nullkarakter (’\0’). Met dat karakter wordt het einde van de string kenbaar gemaakt. Een plezierig gevolg van deze constructie is dat we in de array naam ook een string van minder dan 20 karakters kunnen plaatsen. naam[0]=’K’; naam[1]=’a’; naam[2]=’r’; naam[3]=’e’; naam[4]=’l’; naam[5]=’\0’; 9.3
Strings
63
9
ARRAYS, STRINGS EN STRUCTS
Programmeren in C
Na deze statements ziet de array naam er als volgt uit: Element naam[0] naam[1] naam[2] naam[3] naam[4] naam[5] naam[6]. . . naam[19]
Karakter ’K’ ’a’ ’r’ ’e’ ’l’ ’\0’ ongedefinieerd
De elementen s[6]. . . s[19] zijn ongedefinieerd en worden voor de opslag van de string "Karel" in de array niet gebruikt. Voor het printen van een string op het scherm kunnen we weer de functie printf() gebruiken. In het speciale geval van strings wordt het gebruik van printf() erg eenvoudig. Om de inhoud van de string naam te printen is het voldoende om op te schrijven: printf("%s\n", naam); Na het uitvoeren van dit statement wordt “Karel” op het scherm getoond. Net zoals we eerder %i voor integers en %i voor characters hebben gebruikt geeft %s in de functie printf() aan dat er een string moet worden afgedrukt. Het karakter ’\0’ wordt niet geprint maar dient slechts ter markering voor het stoppen met het printen van karakters. Het is erg omslachtig om telkens met de individuele elementen van het karakter-array te moeten manipuleren. Om het werken met strings gemakkelijker te maken zijn er in C enkele functies beschikbaar in de standaardbibliotheek string.h. We zullen nu een aantal functies die in string.h gedeclareerd zijn gaan behandelen. Een belangrijke functie is strcpy(). Hiermee kan de inhoud van een string in een keer naar een andere gekopieerd worden. Zo kunnen de 6 statements (inclusief de laatste ’\0’) aan het begin van de paragraaf vervangen worden door: strcpy(naam, "Karel"); Soms is het wenselijk om een aantal karakters aan het eind van een string toe te voegen. Hiervoor is de functie strcat() beschikbaar: strcat(naam, " Appel"); Dit zorgt ervoor dat de string naam nu als inhoud "Karel Appel" heeft. Het vergelijken van twee strings gaat op een andere manier dan bij variabelen van types als int en char. Dit komt omdat in C een string een pointer naar een char is. Als we het op de volgende manier doen: if (naam == "James Bond") printf("De naam is Bond, James Bond.\n"); else printf("Ik heet %s.\n", naam); 64
9.3
Strings
Programmeren in C
9
ARRAYS, STRINGS EN STRUCTS
In dit voorbeeld wordt de pointer naam met de pointer naar de string "James Bond" vergeleken. Echter, ook al is de inhoud van de string naam “James Bond”, dan zal de inhoud van naam op een andere plek in het geheugen staan dan de inhoud van de string waarmee het vergeleken wordt. We willen dus niet de pointers vergelijken, maar de inhoud van de strings. Hiervoor is de functie strcmp() beschikbaar waarmee we twee strings op hun onderlinge gelijkheid kunnen testen: if (strcmp(naam, "James Bond") == 0) printf("De naam is Bond, James Bond.\n"); else printf("Ik heet %s.\n", naam); De functie strcmp() geeft als resultaat 0 als de strings identiek zijn, een negatieve waarde als het eerste argument alfabetisch gezien eerder komt dan het tweede, en een positieve waarde indien het eerste argument later komt dan het tweede argument. Of een bepaalde string eerder of later komt wordt op precies dezelfde manier bepaald zoals je dat in bijvoorbeeld een woordenboek zou verwachten. Behalve het toekennen van de ene string aan een andere kunnen we een string ook een inhoud geven door de string in te lezen van het toetsenbord. Dit gaat analoog aan de functie printf(): scanf("%s", naam); Merk op dat er geen &–teken staat voor naam: dit komt omdat de variabele naam al een pointer is. Deze ons reeds bekende functie werkt echter niet in alle gevallen bevredigend. Wanneer we namelijk een string in willen lezen waarin een spatie voorkomt dan zal alleen het gedeelte dat voor de spatie staat ingelezen worden. Zo zal na het intypen op het toetsenbord van “Pietje Puk” de inhoud van de string naam slechts "Pietje" zijn. Om dit probleem te ondervangen biedt de standaardbibliotheek stdio.h de functie gets() waarmee een hele regel ingelezen kan worden: gets(naam); Na aanroep van deze functie en het intikken van “Pietje Puk” gevolgd door een druk op Enter zal de inhoud van de string naam na afloop wel "Pietje Puk" zijn.
9.4
Structs
Een struct is een samenhangende verzameling variabelen die van verschillende types kunnen zijn. De verzameling wordt gekenmerkt door ´e´en hoofdnaam. De declaratie van een struct waarin persoonsgegevens kunnen worden opgeslagen ziet er bijvoorbeeld als volgt uit: struct vriend { char naam[20], adres[20], woonplaats[20]; int leeftijd; char geslacht; }; 9.4
Structs
65
9
ARRAYS, STRINGS EN STRUCTS
Programmeren in C
Deze statements declareren geen variabele, maar een nieuw datatype, namelijk het type struct vriend. Willen we variabelen declareren die van het net gedefinieerde type zijn dan gaat dat als volgt: struct vriend jan, kees, marloes; Hierna kunnen we de variabelen jan, kees en marloes in het programma gebruiken. Het bijzondere van de variabelen van het type vriend is dat ze uit meerdere componenten mogen bestaan. We kunnen de componenten van de variabelen gebruiken door achter de variabelenaam een punt te plaatsen, gevolgd door de naam van de component. Zo kunnen we bijvoorbeeld als volgt aan de componenten van vriend1 waarden toekennen: strcpy(jan.naam, "Jan Molenaar"); strcpy(jan.adres, "Achterstraat 3"); strcpy(jan.woonplaats, "Kleindorp"); jan.leeftijd = 24; jan.geslacht = ’m’; We kunnen de totale inhoud van een variabele van een type struct kopi¨eren in een andere variabele van hetzelfde type. We kunnen hierbij direct de toekenningsoperator gebruiken, bijvoorbeeld: kees = jan; Bijzonder is dat de arrays jan.naam, jan.adres en jan.woonplaats in ´e´en keer mee gekopieerd worden. Deze worden gebruikt als strings, maar als ze in plaats van als array als een pointer naar char gedefinieerd waren dan werden alleen de pointers gekopieerd, en niet de inhoud van de strings zelf! Willen we de strings in dat geval wel kopi¨eren dan moet er gebruik gemaakt worden van de functie strcpy(). Zoals uit het voorgaande blijkt, zijn de componenten van een struct als gewone variabelen te gebruiken. Met de functies scanf() en gets() kunnen we de waarden van de componenten van jan inlezen via het toetsenbord: gets(jan.naam); gets(jan.adres); gets(jan.woonplaats); scanf("%i", &jan.leeftijd); scanf("%c", &jan.geslacht); Een variabele van een struct type kan niet direct vergeleken worden met een andere variabele van hetzelfde type. Het volgende statement is dan ook niet mogelijk in C: if (jan == kees) printf("...\n");
9.5
Definitie van types
Vaak is het gewenst om aan een struct of een array van een bepaald type een eigen naam te geven. Dit kan door middel van een typedef declaratie. Dit gaat als volgt: 66
9.5
Definitie van types
Programmeren in C
9
ARRAYS, STRINGS EN STRUCTS
typedef struct { int lengte, breedte; float hoogte; } doos; Na deze declaratie kunnen variabelen van het type doos op dezelfde eenvoudige manier gedeclareerd worden als de elementaire variabelen, bijvoorbeeld: doos schoenendoos, gebaksdoos; Behalve voor struct types kan de typedef declaratie ook gebruikt worden voor de definitie van eenvoudiger types. Willen we als type een tweedimensionaal array declareren dan kan dat als volgt: typedef double matrix[3][3]; Dit versimpelt het declareren van andere arrays van dezelfde dimensies: matrix rotatie, spiegeling, inversie; Deze zelfgedefinieerde types kunnen op hun beurt weer ook voor structs en arrays gebruikt worden.
9.5
Definitie van types
67
10 FUNCTIES
10 10.1
Programmeren in C
Functies Inleiding
In de voorgaande hoofdstukken hebben we al kennis gemaakt met het gebruik van reeds bestaande functies binnen ons eigen programma. In het bijzonder zijn functies aan de orde gekomen voor het lezen van toetsenbord, bijvoorbeeld met de functie scanf(), en het voor schrijven naar beeldscherm met de functie printf(). Het gemak van het beschikbaar zijn van deze functies is evident: met het eenvoudigweg intypen van de naam van de functie en bijbehorende parameters wordt een bepaalde (ingewikkelde) actie als het lezen van toetsenbord of het schrijven naar scherm ondernomen. We hoeven ons daarbij niet af te vragen hoe de functie precies in elkaar zit. Een dergelijke functie is te vergelijken met een stuk gereedschap dat ieder moment gebruikt kan worden. Behalve het aanroepen van elders in header-files gedeclareerde functies is het mogelijk om binnen een C programma functies te gebruiken die in het programma zelf zijn gedefinieerd. Op die manier is het mogelijk om zelf stukjes gereedschap te ontwikkelen, die op willekeurige plaatsen in het programma te gebruiken zijn. Hoe het een en ander in zijn werk gaat komt in de volgende paragrafen aan bod. Een tweede belangrijke toepassing van functies is het gebruik er van om structuur in het programma aan te brengen. In een eerder hoofdstuk is al besproken dat het opdelen van een programma in deelprogrammaatjes de overzichtelijkheid vergroot. Functies zijn ook in die zin het ge¨eigende gereedschap: alle deelproblemen kunnen in detail uitgewerkt worden binnen de functie.
10.2
Definitie van een functie
Voordat we van een functie gebruik kunnen maken moet deze gedefinieerd worden. In de definitie wordt vastgelegd welke acties de functie uitvoert, en eventueel welke argumenten er mee worden gegeven en welke returnwaarde door de functie wordt teruggegeven. Meestal is de waarde die de functie teruggeeft afhankelijk van de argumenten die aan de functie mee worden gegeven. Ter illustratie volgt er een voorbeeld. #include <stdio.h> float parabool(float x) { float y; y = x * x; return y; } main() { float x, y; printf("Functiewaarden parabool door oorsprong met coefficient 1:\n"); for (x = -10; x <= 10; x++) { 68
Programmeren in C
y = parabool(x); printf("x = %5.1f
10
FUNCTIES
y = %5.1f\n", x, y);
} printf("Functiewaarde bij x = %5.1f: y = %5.1f\n", 20, parabool(20)); } Het eerste dat opvalt is de plaats van de functie parabool() in de programmatekst. Deze komt nog voor de functie main(). Dit is nodig omdat de functie gedefinieerd moet zijn voordat deze gebruikt kan worden, net zoals een variabele gedeclareerd moet zijn voordat die gebruikt kan worden. We hebben in het bovenstaande voorbeeld te maken met een functie die slechts van ´e´en argument (ook wel parameter genoemd) afhankelijk is: de float variabele x. De naam van de functie is zo gekozen dat het duidelijk is wat de functie doet, namelijk de waarde van de tweedegraads polynoom y(x) = x2 uit te rekenen. Het is overigens een goede gewoonte om functies en variabelen namen te geven die gerelateerd zijn aan het op te lossen (wiskundige) probleem. Hierdoor wordt de leesbaarheid van het programma vergroot. De functie parabool() wordt in de for-loop in totaal 21 keer aangeroepen, en op het eind van de functie main() nog een keer. Alhoewel het enige argument van de functie parabool() de naam x heeft, is dit niet dezelfde variabele x als in de functie main() gebruikt wordt. De declaratie van het argument is er alleen maar om binnen de functie parabool() een naam te kunnen geven aan de waarde die tijdens de aanroep van de functie mee is gegeven. In de allereerste regel van de definitie van de functie wordt de naam van de functie bepaald (in dit geval parabool), het type van de returnwaarde (in dit geval float), en tussen haakjes staat het type en de naam van het argument. Het type van het argument is van belang voor de andere functies: deze moeten namelijk een waarde van het juiste type meegeven als ze deze functie aanroepen. De naam van het argument is zoals eerder gezegd alleen van belang binnen de functie zelf. Vaak is het laatste statement binnen een functie het return-statement. Hiermee wordt de functie direct be¨eindigd en wordt de returnwaarde bepaald. Als een functie zo gedefinieerd is dat het een returnwaarde geeft moet er ook altijd minstens ´e´en return-statement zijn. De float variabele y is binnen de functie parabool gedeclareerd. Deze variabele wordt gebruikt om tijdelijk een waarde in op te slaan. De reden dat y binnen de functie is gedeclareerd is dat deze variabele alleen binnen de functie parabool() gebruikt wordt en niet er buiten. Buiten de functie is y zelfs niet eens bekend (daarom heet y ook een lokale variabele). Het volgende statement zou in de functie main() dan ook direct een foutmelding opleveren: printf("%f", y); In de functie main() wordt in het statement y = parabool(x); de waarde van x als argument meegegeven. Tijdens het uitvoeren van het programma wordt dus eerst gekeken wat de waarde van x is, bijvoorbeeld −10 aan het begin van de for-lus, en daarna wordt eigenlijk het volgende statement uitgevoerd: y = parabool(-10);. Het is belangrijk om te weten dat de functie parabool() niets weet over de variabele x uit de functie main(). De functie parabool() kan daarom ook nooit de inhoud van de variabele x uit main() veranderen. In plaats daarvan wordt de waarde −10 gekopieerd naar de lokale variabele x van parabool() zelf. De argumenten van een functie vormen het principe van parameteroverdracht: via de argumenten die tussen haakjes in de heading van een functiedefinitie staan worden er waarden van de aanroepende 10.2
Definitie van een functie
69
10 FUNCTIES
Programmeren in C
functie naar binnen gevoerd via een soort doorgeefluik. Natuurlijk is het ook mogelijk om meerdere waarden door te geven door het aantal argumenten van de functie uit te breiden. Willen we bijvoorbeeld in een functie de absolute waarde van een driedimensionale vector uitrekenen dan zullen we 3 argumenten mee moeten geven: float vector_abs(float x, float y, float z) { return sqrt(x * x + y * y + z * z); }
10.3
Het begrip void
De twee functies die in de vorige paragraaf besproken zijn leverden bij aanroep beide een waarde af. Deze waarde kon aan een variabele van hetzelfde type als de functie toegekend worden. Nu is het zo dat in C een functie niet noodzakelijk een functiewaarde hoeft af te leveren. Om aan te geven dat een functiewaarde niet van toepassing is, wordt op de plaats waar anders het type zou staan het type void gebruikt. #include <stdio.h> void print_grootste(int x, int y, int z) { int grootste; grootste = x; if (y > grootste) grootste = y; if (z > grootste) grootste = z; printf("Het grootste getal is %i.\n", grootste); } main() { int a, b, c; printf("Voer drie gehele getallen in:\n"); scanf("%i %i %i", &a, &b, &c); print_grootste(a, b, c); } Merk op dat in de functie print_grootste() het return-statement ontbreekt. Dit statement is overbodig omdat er geen waarde aan de functie wordt toegekend. De identifier void kan ook gebruikt worden om aan te duiden dat een functie geen parameters heeft. Op de plaats van de parameterlijst 70
10.3
Het begrip void
Programmeren in C
10
FUNCTIES
wordt dan enkel en alleen void geplaatst. Neem bijvoorbeeld deze variatie op het bovenstaande voorbeeld: #include <stdio.h> int lees_grootste(void) { int grootste, x, y, z; printf("Voer drie gehele getallen in:\n"); scanf("%i %i %i", &x, &y, &z); grootste = x; if (y > grootste) grootste = y; if (z > grootste) grootste = z; return grootste; } main() { int grootste; grootste = lees_grootste(); printf("Het grootste getal is %i.\n", grootste); }
10.4
Pointers en functies
Pointers werden reeds ge¨ıntroduceerd bij de behandeling van arrays. Om te begrijpen wanneer pointers in functiedeclaraties gebruikt worden, moeten we eerst meer weten over hoe de computer met variabelen en functies werkt. Stel, in ons programma staat een functie die de oppervlakte van een vierkant berekent: float oppervlakte(float zijde) { return zijde * zijde; } We kunnen deze functie in het programma bijvoorbeeld als volgt aanroepen: o = oppervlakte(z); De computer zal tijdens het uitvoeren van dat statement de volgende stappen ondernemen: 10.4
Pointers en functies
71
10 FUNCTIES
Programmeren in C
• Kijk welk getal er in het geheugen van de variabele z staat. • Roep de functie oppervlakte() aan met als argument dat getal. – Kopieer het meegegeven argument naar de lokale variabele zijde. – Bereken het kwadraat van zijde, en geef dat terug als returnwaarde. • Kopieer de returnwaarde naar de variabele o. De functie oppervlakte() krijgt alleen het getal mee bij de aanroep, en kan dus niet weten om welke variabele het gaat. Oftewel: bij de functie-aanroep oppervlakte(z) wordt alleen de waarde van z meegegeven aan de functie. Dit betekent, dat in het volgende programma de waarde van de variabele a niet verandert als de functie maak_nul() wordt aangeroepen: #include <stdio.h> void maak_nul(double x) { x = 0; } main() { double a; a = 3.1415; printf("Voor: %lf\n", a); maak_nul(a); printf("Na:
%lf\n", a);
} Het maakt niet uit of a een lokale of globale variabele is, het bedoelde effect wordt niet bereikt. Om ervoor te zorgen dat een functie toch de inhoud van een variabele van een andere functie kan veranderen, moet het adres van die variabele bekend zijn. In plaats van de waarde van een variabele moet het adres van die variabele doorgegeven worden aan de functie. Aangezien de aangeroepen functie een adres krijgt moet die opgeslagen worden in een pointer variabele. Om het bovenstaande voorbeeld wel te laten werken moeten we het dus als volgt veranderen: #include <stdio.h> void maak_nul(double *x) { *x = 0; } main() { 72
10.4
Pointers en functies
Programmeren in C
10
FUNCTIES
double a; a = 3.1415; printf("Voor: %lf\n", a); maak_nul(&a); printf("Na:
%lf\n", a);
}
10.5
Arrays en functies
Wanneer we aan een functie een array willen doorgeven of terugvragen gaat dat ook via het pointerprincipe. De naam van een array-variabele is, als deze zonder indices wordt gebruikt, een pointer naar het begin van het array. Een voorbeeldprogramma waarin de som van twee driedimensionale vectoren wordt bepaald heeft de volgende vorm: #include <stdio.h> void sommeer(double *a, double *b, double *som) { int i; for (i = 0; i < 3; i++) som[i] = a[i] + b[i]; } main() { double v1[3], v2[3], som[3]; printf("Voer twee driedimensionale vectoren in:\n"); scanf("%lf %lf %lf", &v1[0], &v1[1], &v1[2]); scanf("%lf %lf %lf", &v2[0], &v2[1], &v2[2]); sommeer(v1, v2, som); printf("De som van deze vectoren is: "); printf("%lf, %lf, %lf).\n", som[0], som[1], som[2]); }
10.5 Arrays en functies
73
11 GESTRUCTUREERD PROGRAMMEREN
11
Programmeren in C
Gestructureerd programmeren
Tot nu toe hebben we in deze cursus veel aandacht besteed aan de basiselementen van programmeren: je gebruikt de beschikbare statements en begint gewoon met het schrijven van code, Je werkt naar een functionerend programma toe. Minstens zo belangrijk als het functioneren is de kwaliteit van het programma. Daarmee bedoelen we onder andere of de source code makkelijk leesbaar is voor anderen en of het programma makkelijk kan worden gewijzigd en uitgebreid.
11.1
Leesbaarheid
Als je code goed leesbaar is voor anderen kan je programma veel makkelijker door andere mensen gebruikt en verbeterd worden. De leesbaarheid kan behoorlijk worden verbeterd door commentaar te plaatsen, duidelijke namen voor functies en variabelen te gebruiken, goed op de layout te letten door netjes in te springen en geen ingewikkelder taalconstructies gebruiken dan nodig is (zie ook 4.5). Ook het goed gebruik van functies is van groot belang. Van een goed programma kan je heel snel alleen aan de main functie zien wat het programma doet. Als je wilt weten hoe het programma dat doet ga je in de functies kijken die worden aangeroepen vanuit main. Wil je nog meer weten, dan ga je in de functies kijken die door die functies worden gebruikt, enzovoorts. Zo kan je een programma op verschillende niveaus bekijken en begrijpen zonder dat je door alle sourcecode heen hoeft te spitten. Het is dus aan te raden om programma’s op te splitsen in logische stappen en elke stap in een aparte functie verder uit te werken. Een mogelijk voorbeeld van de functie main() na deze opsplitsing: main() { invoer(); bewerking(); uitvoer(); }
11.2
Uitbreidbaarheid
Vooral programma’s die gebruikt worden om onderzoek mee te doen moeten vaak worden gewijzigd en uitgebreid. Het is daarom handig als je tijdens het schrijven van het programma vooruit denkt aan de dingen die je misschien ooit zou willen aanpassen en je programma dus niet dicht timmert. Een voorbeeld hiervan is het gebruiken van #define directives om bepaalde constanten in je source gemakkelijk te kunnen aanpassen, zonder dat je alles moet doorzoeken naar de plekken waar de constante gebruikt wordt. Een ander punt is het gebruik van lokale en globale variabelen. Functies kunnen bewerkingen doen op globale variabelen of op de lokale variabelen die ze als argument mee hebben gekregen. Een functie die op globale variabelen werkt zal echter nooit die bewerkingen op andere variabelen kunnen doen. Een functie die de variabelen als argument meekrijgt kan die bewerkingen op welke variabele dan ook doen. Het is dus duidelijk dat een functie die niet afhankelijk is van globale variabelen op meer manieren te gebruiken is. Het vorige voorbeeld kan bijvoorbeeld als volgt gebruik maken van lokale variabelen: 74
Programmeren in C
11
GESTRUCTUREERD PROGRAMMEREN
main() { struct gegevens a, b; invoer(&a); b = bewerking(a); uitvoer(b); } Het feit dat het programma op deze manier makkelijker uitbreidbaar is zal blijken als we een nieuwe functie toevoegen die twee sets gegevens met elkaar kan vergelijken. Aangezien de vorige functies nu op lokale variabelen werken kunnen ze zonder aanpassingen hergebruikt worden: main() { struct gegevens a, b, c, d; invoer(&a); invoer(&b); c = middeling(a, b); d = bewerking(c); uitvoer(d); }
11.2
Uitbreidbaarheid
75
12 LEZEN EN SCHRIJVEN VAN BESTANDEN
12
Programmeren in C
Lezen en schrijven van bestanden
Tot nu toe zijn datastructuren beschreven die met het interne geheugen van de computer verbonden waren. Iedere variabele (of set variabelen) was daarbij gekoppeld aan een geheugenadres. Bij het be¨eindigen van een programma zijn de gegevens die in het geheugen zijn opgeslagen niet meer bereikbaar. In veel gevallen willen we echter zeker een deel van de gegevens die door de computer zijn gegenereerd bewaren voor toekomstige bewerking en daarom opslaan in een bestand. We zullen nu laten zien op welke manier we vanuit een C programma gegevens in een bestand kunnen schrijven of er uit kunnen lezen. Hiertoe bespreken we een tweetal programma’s waarmee we respectievelijk een bestand num.txt kunnen wegschrijven op schijf of er uit kunnen lezen. /* Schrijf de getallen 1,2, ..., 10 naar het bestand num.txt */ #include <stdio.h> main() { FILE *fp; int i; fp = fopen("num.txt", "w"); for (i=1; i<=10; i++) fprintf(fp, "%i\n", i); fclose(fp); } Nieuw in dit programma is het type FILE. Dit type is gedefinieerd in de standaardbibliotheek stdio.h. Met behulp van het type FILE kunnen we een zogenaamde filepointer declareren. In het programma gebeurt dit in de regel: FILE *fp; De reden dat fp gedeclareerd is als het type “pointer naar FILE” komt omdat we zelf geen geheugen hoeven te reserveren voor een variabele van het type FILE, in plaats daarvan zal dit door de bibliotheekfuncties gedaan worden: fp = fopen("num.txt", "w"); Met dit statement wordt het bestand num.txt geopend en aan de filepointer fp gekoppeld. Alle inen uitvoer functies die ook met fp werken in het programma zullen acties uitvoeren op het bestand num.txt. De "w" als tweede argument van fopen() heeft tot gevolg dat het bestand op een manier is geopend dat we er in kunnen schrijven. Het tweede argument kan ook andere waarden aannemen. We komen daar later in de paragraaf op terug. Na fopen() kunnen we in het bestand gaan schrijven: fprintf(fp, "%i\n", i); 76
Programmeren in C
12 LEZEN EN SCHRIJVEN VAN BESTANDEN
Met de functie fprintf() kunnen we op precies dezelfde manier als we met de functie printf() op het scherm schreven gegevens naar een bestand gaan schrijven. Het enige verschil zit hem behalve de f voor printf ook in een extra argument dat we met de functie mee moeten geven. Door voor het eerste argument van fprintf() voor fp te kiezen, wordt ervoor gezorgd dat de gegevens naar het aan de filepointer gekoppelde bestand num.txt worden geschreven. Men moet er altijd voor zorgen dat bestanden weer netjes gesloten worden: fclose(fp); Deze functie zorgt ervoor dat fp niet meer aan het bestand num.txt is verbonden. Tevens wordt ervoor gezorgd dat alle schrijfopdrachten goed zijn afgerond, zodat het bestand in een consistente staat wordt achtergelaten. Het volgende programma leest de getallen uit de bestand num.txt in: #include <stdio.h> void main(void) { FILE *fp; int som, getal, i; som = 0; fp = fopen("num.txt", "r"); for (i = 0; i < 10; i++) { fscanf(fp, "%i", &getal); som += getal; } fclose(fp); printf("De som van de getallen is %i.\n", getal); } Dit programma heeft een aantal overeenkomsten met het vorige. Evenals bij het wegschrijven wordt het bestand geopend en gesloten met de functies fopen() en fclose(). Een belangrijk verschil hierbij is dat het tweede argument van fopen() geen "w" is maar "r", Hiermee wordt aangegeven dat uit het bestand gelezen gaat worden. In plaats van de functie fprintf() wordt hier de functie fscanf() gebruikt, analoog aan printf() en scanf(). Er zijn enkele belangrijke verschillen tussen het lezen van en het schrijven naar een bestand. Het kan voorkomen dat een bestand waaruit men wil lezen niet bestaat. Ook is het niet altijd bekend hoe groot een bestand is. Het niet bestaan van een bestand kan natuurlijk ook bij het schrijven voorkomen, maar in dat geval wordt het bestand automatisch aangemaakt en hoeft dat niet altijd een probleem op te leveren. Het lezen van een bestand dat niet bestaat zal een fout veroorzaken. Om na te gaan of een bestand bestaat kan de filepointer fp worden gecontroleerd. fp = fopen("num.txt", "r"); 77
12 LEZEN EN SCHRIJVEN VAN BESTANDEN
Programmeren in C
Wanneer na dit statement de variabele fp de waarde 0 of, wat vaak ook gebruikt wordt, NULL (wat gedefinieerd is in stdio.h) heeft dan betekent dit dat het bestand niet bestaat. Door te testen of fp == NULL is het mogelijk om er achter te komen of num.txt beschikbaar is, bijvoorbeeld door het volgende programmafragment op te nemen: fp = fopen("NUM.TXT", "r"); if (fp == NULL) printf("Het bestand num.txt bestaat niet.\n"); Wanneer de lengte van een bestand niet bekend is kunnen we bij het inlezen gebruik maken van de functie feof(): while (!feof(fp)) { fscanf(fp, "%i", &getal); som += getal; } Zolang de functiewaarde van feof() gelijk aan 0 is kan er nog een element uit het bestand worden gelezen. Is het einde van het bestand bereikt dan wordt feof() ongelijk aan 0. Om dezelfde reden dat we bij het lezen van strings van het toetsenbord de functie gets() in plaats van scanf() gebruikten is het veilig om bij het lezen van complete regels uit een bestand de functie fgets() te gebruiken. Dit gaat op de volgende manier: /* Druk de inhoud van tekst.txt af met regelnummers. */ #include <stdio.h> main() { FILE *fp; char tekst[100]; int regel = 0; fp = fopen("tekst.txt", "r"); while (!feof(fp)) { regel++; fgets(tekst, 100, fp); printf("%4i %s", regel, tekst); } fclose(fp); } Het bovenstaande voorbeeld leest gehele regels in uit het bestand tekst.txt, houdt het regelnummer bij, en drukt vervolgens het regelnummer en de tekst die op die regel stond af op het scherm. De 78
Programmeren in C
12 LEZEN EN SCHRIJVEN VAN BESTANDEN
functie fgets() krijgt drie argumenten mee: een string waarin de gelezen regel gekopieerd moet worden, een getal wat aangeeft wat het maximum aantal karakters is wat in de string geplaatst mag worden, en als laatste de filepointer. We hebben nu twee manieren besproken waarop een bestand geopend kan worden met fopen(). De ene was met "w" als tweede argument om aan te geven dat we gaan schrijven, de andere was met "r" als tweede argument om te gaan lezen. Een derde elementaire mogelijkheid is om gegevens toe te voegen aan een bestand. We gebruiken dan als tweede argument "a" (van append). In sommige gevallen zijn combinaties van bovengenoemde opties gewenst. In tabel 6 staan alle mogelijkheden die de standaardbibliotheek stdio.h hiervoor biedt.
79
A
STANDAARDBIBLIOTHEKEN
A
Programmeren in C
Standaardbibliotheken
A.1
Inleiding
In deze handleiding zijn we al een aantal standaard functies tegengekomen die door middel van het gebruik van standaardbibliotheken beschikbaar kwamen in ons eigen programma. Tot nu toe is er geen aandacht besteed aan de samenhang tussen die functies. De declaraties van de standaardfuncties zijn ondergebracht in header-files. Daarbij staan functies met gemeenschappelijke kenmerken telkens in ´e´en header-file. Zo zijn bijvoorbeeld alle functies die de in- en uitvoer verzorgen ondergebracht in stdio.h. Het doel van dit hoofdstuk is om de belangrijkste standaardfuncties op een rij te zetten. Hierbij zal uitgegaan worden van de header-files waarin ze zijn gedeclareerd. Een aantal bibliotheken worden besproken die tot de ANSI standaard behoren. De functies die tot die standaard behoren zijn onafhankelijk van het type C omgeving waarin geprogrammeerd wordt en staan dus toe dat programma’s die alleen die functies gebruiken op de meest uiteenlopende computersystemen gebruikt kunnen worden.
A.2
In- en uitvoer: stdio.h
De standaardbibliotheek stdio.h zorgt voor in- en uitvoer van gegevens tussen toetsenbord, beeldscherm en bestanden.
int printf(const char *format, ...);
De functie printf() schrijft de string format naar het scherm. In deze string kunnen zogenoemde conversies voorkomen. Deze beginnen met het karakter “%”, en zorgen ervoor dat de waarde van het volgende argument in de te schrijven string wordt tussengevoegd. De conversies bestaan uit het procent teken, “%”, optioneel gevolgd door enkele modifiers (zie tabel 4), en vervolgens door de conversion specifier (zie tabel 5). De laatste geeft aan wat het type van het argument is dat afgedrukt moet worden. De modifiers zorgen ervoor dat er kleine wijzigingen in de opmaak gemaakt kunnen worden. De belangrijkste modifier is degene die bepaalt hoeveel decimalen van een getal geprint moeten worden. Deze bestaat uit een getal dat de totale hoeveelheid karakters ruimte aangeeft dat voor het getal gereserveerd moeten worden. Eventueel staat daarachter een punt en nog een getal, wat aangeeft hoeveel karakters er achter de decimale punt moeten staan. Enkele voorbeelden: "%i" "%06i" "%f" "%.4f" "%8.4f"
Precies genoeg ruimte om een integer getal weer te geven. Minimaal 6 tekens; als er minder dan 6 tekens nodig zijn worden links nullen toegevoegd. Standaard om floating point waarden weer te geven. 4 decimalen na de punt worden gegeven. minimaal 8 tekens (links eventueel aangevuld met spaties) en 4 decimalen na de punt.
De functie printf() levert als returnwaarde het aantal geschreven tekens of een negatief getal als een fout optreedt.
int scanf(const char *format, ...) 80
Programmeren in C
A
Modifier 0 spatie + hh h l
STANDAARDBIBLIOTHEKEN
Betekenis Het getal moet opgevuld worden met nullen. Links uitlijnen. Ruimte bewaren voor het min-teken. Altijd het teken van het getal laten zien. Argument is een char. Argument is een short int. Argument is een long int of een double.
Tabel 4: Format modifiers voor printf() en scanf() Specifier d of i o x u c s f lf e le p %
Type int int int unsigned int char char * float double float double pointer
Betekenis Decimale notatie. Octale notatie. Hexadecimale notatie. Decimale notatie. Het karakter wordt afgedrukt. De string wordt afgedrukt totdat het karakter “\0” bereikt is. Decimale notatie. Decimale notatie. Wetenschappelijke notatie. Wetenschappelijke notatie. Decimale notatie van het adres. Het karakter “%” wordt afgedrukt.
Tabel 5: Format specifiers voor printf() en scanf()
De functie scanf() leest tekens van het toetsenbord. De gelezen tekens worden op basis van de conversiespecificaties, die in format zijn opgegeven, ge¨ınterpreteerd. De functie leest de waarden in van de variabelen die als argument na de formatstring worden genoteerd. Alle argumenten dienen pointers naar variabelen te zijn waar de ingelezen waarden opgeslagen moeten worden. De conversiespecificaties zijn vrijwel identiek aan die van printf() (zie tabel 4 en 5). De returnwaarde geeft het het aantal succesvolle conversies aan of een negatieve waarde als er een fout was opgetreden.
char *gets(char *s) De functie gets() leest een complete regel van het toetsenbord en zet die regel in de string s. Daarbij wordt de newline ’\n’, die de invoerregel afsluit, vervangen door ’\0’. De returnwaarde is de waarde van s of NULL indien een fout is opgetreden. Vanwege een klein verschil in het afhandelen van de newline is het niet aan te raden om scanf() en gets() door elkaar te gebruiken in ´e´en programma.
FILE *fopen(const char *pathname, const char *mode) De functie fopen() opent het bestand met de naam die in de string pathname staat. De manier waarom het bestand wordt geopend wordt bepaald door de parameter mode (zie tabel 6). A.2
In- en uitvoer: stdio.h
81
A
STANDAARDBIBLIOTHEKEN
mode "r" "r+" "w" "w+" "a" "a+"
Programmeren in C
Betekenis Open het bestand enkel en alleen om te lezen. Open het bestand om te lezen en te schrijven. Cre¨eer het bestand om enkel in te schrijven, gooi de eventuele inhoud weg. Cre¨eer het bestand om in te lezen en te schrijven, gooi de eventuele inhoud weg. Open het bestand om enkel gegevens toe te voegen, indien het niet bestond cre¨eren. Open het bestand om gegevens toe te voegen en te lezen, indien het niet bestond cre¨eren. Tabel 6: Modes voor fopen()
De returnwaarde is een pointer naar een variabele van het type FILE, die gebruikt kan worden als parameter in andere functies om in het geopende bestand te lezen of te schrijven. Indien er een fout is opgetreden is de returnwaarde NULL. int fclose(FILE *stream) De functie fclose() sluit het bestand waarnaar verwezen wordt met de parameter stream. Alle gegevens die nog niet waren weggeschreven worden dan naar het bestand geschreven. De returnwaarde is 0 indien succesvol, anders is er een fout opgetreden. int remove(const char *pathname) De functie remove() verwijdert het opgegeven bestand of map. De returnwaarde is 0 indien succesvol, anders is er een fout opgetreden. int rename(const char *oldpath, const char *newpath) De functie rename() verandert de naam van een bestand of map. De returnwaarde is 0 indien succesvol, anders is er een fout opgetreden. int fseek(FILE *stream, long offset, int origin) Met de functie fseek() wordt de positie van waaraf gelezen of geschreven wordt van het bestand veranderd. Bij aanroep van deze functie is de nieuwe positie offset bytes ten opzichte van het begin, het eind of de huidige positie in het bestand. Dit wordt bepaald door de parameter origin, die kan ofwel de waarde SEEK_SET (vanaf het begin), SEEK_CUR (vanaf de huidige positie) of SEEK_END (vanaf het einde) hebben. De returnwaarde is 0 indien succesvol, anders is er een fout opgetreden. int rewind(FILE *stream) Met de functie rewind() wordt de filepointer naar het begin van het bestand verplaatst. De returnwaarde is 0 indien succesvol, anders is er een fout opgetreden. int feof(FILE *stream) 82
A.2
In- en uitvoer: stdio.h
Programmeren in C
A
STANDAARDBIBLIOTHEKEN
De returnwaarde van de functie feof() is nul als het einde van het bestand nog niet bereikt is, ongelijk aan nul indien het al wel bereikt is. int fprintf(FILE *stream, const char *format, ...) De functie fprintf() is identiek aan de functie printf(), met als enige verschil dat de gegevens niet op het scherm worden afgedrukt maar naar een bestand wordt geschreven. int fscanf(FILE *stream, const char *format, ...) De functie fscanf() is identiek aan de functie scanf(), met als enige verschil dat de gegevens niet van het toetsenbord worden gelezen maar uit een bestand worden gelezen. int fgets(char *s, int n, FILE *stream) De functie fgets() leest ten hoogste n - 1 tekens in uit het bestand en kopieert deze naar de string s. Bij een newline wordt gestopt. De newline wordt in de string zelf opgenomen, en de string wordt tevens afgesloten met het karakter ’\0’. De returnwaarde is de waarde van s of NULL indien een fout is opgetreden. size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream) De functie fread() maakt het mogelijk om gegevens uit een bestand te lezen zonder dat daarbij een bepaald formaat of onderverdeling in regels gehanteerd wordt. Er worden size * nobj bytes uit het bestand gelezen en gekopieerd naar het geheugen waarnaar verwezen wordt met de pointer ptr, dit kan bijvoorbeeld een array zijn. De returnwaarde is het aantal blokken van size bytes dat gelezen is, indien er een fout opgetreden is is de returnwaarde kleiner dan nobj. size_t fwrite(void *ptr, size_t size, size_t nobj, FILE *stream) De functie fwrite() maakt het mogelijk om gegevens naar een bestand te schrijven zonder dat daarbij een bepaald formaat of onderverdeling in regels gehanteerd wordt. Het werkt geheel analoog aan de functie fread().
A.3
String manipulatie: string.h
In de bibliotheek string.h worden de functies voor het manipuleren van strings gedefinieerd. char *strcpy(char *dest, const char *src) De functie strcpy() kopieert de string source naar de string dest, waarbij de originele inhoud van de string dest overschreven wordt. De returnwaarde van de functie is gelijk aan dest. A.3
String manipulatie: string.h
83
A
STANDAARDBIBLIOTHEKEN
Programmeren in C
char *strcat(char *dest, const char *src)
De functie strcat() voegt de string source toe aan de string dest. De returnwaarde van de functie is gelijk aan dest.
int strcmp(const char *s1, const char *s2)
De functie strcmp() vergelijkt de strings s1 en s2. Dezelfde regels als in een woordenboek worden hierbij gehanteerd. De returnwaarde is negatief indien de string s1 eerder in een woordenboek zou voorkomen dan s2, nul als de strings identiek zijn, en een positief getal indien s1 later zou voorkomen dan s2.
A.4
Wiskundige functies: math.h
In de bibliotheek math.h zijn wiskundige functies gedeclareerd. Tabel 7 laat zien welke wiskundige functies corresponderen met de bibliotheekfuncties. Functie double double double double double double double double 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) sinh(double x) cosh(double x) tanh(double x) exp(double x) log(double x) log10(double x) sqrt(double x) fabs(double x) pow(double x, double y)
Returnwaarde sin x cos x tan x sin−1 x cos−1 x tan−1 x 1 x −x 2 (e − e ) 1 x −x 2 (e + e ) 2x (e − 1)/(e2x + 1) ex log x log x/ log 10 √ x |x| xy
Tabel 7: Wiskundige functies
A.5
Standaardfuncties: stdlib.h
In deze header staan o.a. de functies t.b.v. geheugenallocatie en conversie van getallen. We noemen de volgende: double atof(const char *s) De functie atof() converteert de inhoud van de string s naar een double indien dit een getal is. 84
A.4
Wiskundige functies: math.h
Programmeren in C
A
STANDAARDBIBLIOTHEKEN
int atoi(const char *s) De functie atoi() converteert de inhoud van de string s naar een int indien dit een getal is. long int atol(const char *s) De functie atol() converteert de inhoud van de string s naar een long int indien dit een getal is. int rand(void) De functie rand() levert als resultaat een pseudo-willekeurig geheel getal uit het interval [0, RAND_MAX). De waarde RAND_MAX is ten minste 32767. int srand(unsigned int seed) De functie srand() gebruikt seed als het kiemgetal voor een nieuwe reeks pseudo-willekeurige getallen. De beginwaarde van seed is 1. Indien hetzelfde kiemgetal gebruikt wordt levert dit ook dezelfde reeks pseudo-willekeurige getallen op. int abs(int n) De functie abs() levert als resultaat de absolute waarde van zijn n. long int labs(long int n) De functie labs() is identiek aan de functie abs(), maar werkt met long int getallen. void *malloc(size_t size) De functie malloc() levert als resultaat een pointer naar geheugenruimte van size bytes. Als aan het verzoek voor het reserveren van geheugen niet kan worden voldaan is het resultaat NULL. De gealloceerde geheugenruimte wordt niet ge¨ınitialiseerd of schoongemaakt. Het kan nodig zijn om de pointer die teruggegeven wordt te casten naar het juiste type. void *realloc(void *p, size_t size) De functie realloc() verandert de hoeveelheid gereserveerd geheugen waarnaar p wijst in size. Het resultaat is een pointer naar p of een ander stuk geheugen indien het geheel verplaatst is in het geheugen. Het resultaat is NULL indien niet aan het verzoek voldaan kon worden. void free(void *p) De functie free() geeft de ruimte waarnaar p wijst vrij. De geheugenruimte waarnaar p wijst moet met malloc() of realloc() zijn gereserveerd. A.5
Standaardfuncties: stdlib.h
85
B
C++ VOOR C KENNERS
B
Programmeren in C
C++ voor C kenners Jeroen Fokker Informatica Instituut Universiteit Utrecht INF/DOC 95-04
De taal C++ wordt steeds meer gebruikt als alternatief voor C. De taal is een object-geori¨enteerde uitbreiding van C. Het blijft dus mogelijk om alle constructies uit C te gebruiken, waardoor de taal voor C-programmeurs gemakkelijk te leren is. Het is echter aan te raden om sommige constructies en programmeertechnieken niet meer te gebruiken, omdat er betere voor in de plaats beschikbaar zijn. Deze tekst geeft een overzicht van de belangrijkste verschillen tussen C en C++. Eerst worden de nieuwe of uitgebreide taalconstructies uit C++ behandeld. In paragraaf B.2 en B.4 wordt het belangrijkste nieuwe concept, de class, behandeld; dit wordt onderbroken door een aantal voorbeelden in paragraaf B.3. In paragraaf B.5 worden de belangrijkste aspecten van de nieuwe input/output library besproken.
B.1
Nieuwe taalconstructies
Commentaar De manier waarop commentaar wordt aangegeven is een niet erg belangrijk, maar wel in het oog lopend verschil tussen C en C++. In C begint commentaar met /* en eindigt met */. Bijvoorbeeld: /* Deze functie berekent de omtrek van een cirkel. */ float omtrek(float r) { /* r is de straal */ return (2 * PI * r); } In C++ is er nog een tweede manier om commentaar aan te duiden: commentaar begint met // en eindigt aan het eind van de regel. Bijvoorbeeld: // Deze functie berekent // de omtrek van een cirkel. float omtrek(float r) { // r is de straal
86
Programmeren in C
B
C++ VOOR C KENNERS
return (2 * PI * r); } Hiermee is “uitcommentari¨eren” van een regel makkelijker geworden. De oude commentaar-stijl blijft ook geldig.
Constanten In C kun je met #define constanten defini¨eren: #define PI 3.14159 #define FALSE 0 Deze definities worden ge¨expandeerd door een preprocessor van de compiler. De eigenlijke compiler ziet dus gewoon de numerieke waarden staan. Daarom zijn aldus gedefinieerde constanten ook toegestaan in bijvoorbeeld array-declaraties. Dat #define inderdaad door een preprocessor wordt behandeld, blijkt uit het feit dat je er de taal mee kunt veranderen: #define als if Daarna lijkt de taal een als-statement te kennen. Constanten defini¨eren is handig, maar hoort eigenlijk in de taal zelf thuis, en niet in de preprocessor. Andere namen verzinnen voor keywords is enigzins vergezocht, en kun je eigenlijk beter niet gebruiken. In C++ is er daarom een constructie om constanten te defini¨eren, die beter aansluit op variabele-declaraties. Ze zien er uit als een initialiserende variabele-declaratie, voorafgegaan door het keyword const: const float pi = 3.14159; const int false = 0; Constante pointers zijn nu ook mogelijk: const int *pf = &false; Extra voordeel is dat deze constanten getypeerd zijn. Voor het defini¨eren van constanten is de preprocessor dus niet meer nodig.
Inline functies In C kunnen macro’s gedefinieerd met #define voorzien zijn van parameters. Je kunt hier een soort functies mee defini¨eren: B.1 Nieuwe taalconstructies
87
B
C++ VOOR C KENNERS
Programmeren in C
#define square(x) x * x Ook hier wordt de substitutie door de preprocessor uitgevoerd. Als de macro dus gebruikt wordt met square(1 + 2) dan wordt er code gegenereerd voor de expressie 1 + 2 * 1 + 2. Je ziet hier meteen het nadeel: door de syntactische substitutie is het resultaat niet 9 (het kwadraat van 1 + 2) maar 5. Een C-programmeur die dit een paar keer meegemaakt heeft zal daarom liever defini¨eren: #define square(x) ((x) * (x)) maar dat blijft lapwerk. De enige reden om zo’n definitie te schrijven in plaats van een gewone functieaanroep, is effici¨entie: je spaart een functieaanroep uit (ten kost van een langere objectcode). In C++ is dit eleganter opgelost. Je mag bij elke functie erbij schrijven dat de functie “inline” is. Voor inline-functies wordt niet apart code gegenereerd, maar wordt de body ge¨expandeerd bij elke aanroep. De definitie van een tijd-effici¨ente kwadraatfunctie is dus: inline int square(int x) { return x * x; } Voordeel is dat je door toevoegen en weghalen van het woord inline gemakkelijk kunt wisselen tussen een tijd- en een ruimte-effici¨ente versie. Bovendien worden de functies altijd gecontroleerd op syntax en type.
Declaraties In oude versies van C mochten lokale variabelen alleen maar gedefinieerd worden aan het begin van een functie. In ANSI-C mogen variabelen aan het begin van elk blok gedefinieerd worden, dus na elke accolade openen. In C++ zijn er helemaal geen voorwaarden meer aan de plaats van declaraties. Statements en declaraties mogen elkaar dus vrijelijk afwisselen. Voordeel is dat je declaraties dichtbij het gebruik kunt zetten, en gemakkelijk delen van een functie, compleet met declaraties, kunt verplaatsen. Je kunt zelfs een loop-teller ter plaatse declareren: for (int i = 1; i < 10; i++) p(i); Nadeel is dat de declaraties minder gemakkelijk te vinden zijn. Veel programmeurs blijven de declaraties daarom aan het begin zetten.
Typedeclaraties Structures zijn altijd een beetje lastig geweest in C. Op de plaats van een type mag je schrijven struct naam. Bij het eerste gebruik moeten ook de velden worden opgesomd. In het volgende voorbeeld wordt een variabele p van het type struct punt gedeclareerd (waarbij, omdat dit het eerste gebruik 88
B.1
Nieuwe taalconstructies
Programmeren in C
B
C++ VOOR C KENNERS
is, de velden worden opgesomd). Later wordt het type nog eens gebruikt in een parameter-declaratie. De velden hoeven nu niet meer te worden opgesomd: struct punt { int x; int y; } p; void f(struct punt q); In een variabele-declaratie mag je ook nul variabelen declareren. Meestal is dat natuurlijk onzin, maar veel mensen gebruiken dit om alvast maar de velden van een structure op te sommen: struct punt { int x; int y; }; struct punt p; Vervelend blijft dat je steeds het woord struct moet herhalen. Daarom is in C een typedef mogelijk: typedef struct punt { int x; int y; } PUNT; void f(PUNT p); omslachtig is dat je nu twee namen moet verzinnen: punt en PUNT. Het nieuwe type is PUNT, terwijl struct punt nu alleen nog maar nodig is voor recursieve typedefinities. Omdat dit allemaal maar ingewikkeld is, is het in C++ toegestaan om de naam achter struct, ook zonder typedef, te gebruiken als typenaam: struct punt { int x; int y; }; void f(punt p);
Call by reference In C is het lastig om variabelen door een functie te laten veranderen. Je moet dan een pointer naar die variabele als parameter gebruiken. Bij aanroep moet je dan ook het adres van de variabele B.1
Nieuwe taalconstructies
89
B
C++ VOOR C KENNERS
Programmeren in C
meegeven, in plaats de waarde ervan. Bijvoorbeeld de bekende wissel-functie: void wissel(int *x, int *y) { int h; h = *x; *x = y; *y = h; } main() { int a, b; wissel(&a, &b); } In C++ kun je bij declaratie van de functie aangeven dat de parameter by reference meegegeven moet worden. In de body van die functie hoef je dan geen sterretjes meer te zetten, en wat belangrijker is: bij de aanroep hoef je de adres-operator niet meer te gebruiken: void wissel(int &x, int &y) { int h; h = x; x = y; y = h; } main() { int a, b; wissel(a, b); } Qua implementatie gebeurt er overigens precies hetzelfde. Daarmee is ook duidelijk waarom in C++ de eis wordt gesteld dat bij aanroep van een functie het type bekend is: dit is van belang om te weten welke code gegenereerd moet worden voor wissel(a,b).
Overloading Als je in C een functie wilt schrijven op verschillende typen, moet je verschillende namen verzinnen: int iabs(int x) { 90
B.1
Nieuwe taalconstructies
Programmeren in C
B
C++ VOOR C KENNERS
return (x < 0 ? -x : x); } float fabs(float x) { return (x < 0.0 ? -x : x); } main() { float resultaat; resultaat = iabs(3) + fabs(-2.7); } In C++ mag je dezelfde naam gebruiken, mits de parameters van verschillend type zijn. Voor een functie-aanroep wordt door de compiler op grond van het type de juiste functie gekozen: int abs(int x) { return (x < 0 ? -x : x); } float abs(float x) { return (x < 0.0 ? -x : x); } main() { float resultaat; resultaat = abs(3) + abs(-2.7); }
Default parameters Met overloading kun je handige dingen doen. Bijvoorbeeld een machtsverhef-functie met twee parameters: float macht(float x, int n) { r = 0; for (i = 0; i < n; i++) r *= x; return r; } B.1 Nieuwe taalconstructies
91
B
C++ VOOR C KENNERS
Programmeren in C
en een aparte versie daarvan, waarbij de tweede parameter is weggelaten: float macht(float x) { return macht(x, 2); } Op deze manier zal macht als “default”-waarde voor de exponent 2 gebruiken. Nog handiger is het, dat je dit in C++ in ´e´en declaratie kunt aangeven: float macht(float x, int n = 2) { r = 0; for (i = 0; i < n; i++) r *= x; return r; } Parameters waarvoor je een default waarde wilt specificeren, moeten aaneengesloten aan het eind van de parameterlijst staan.
Operator overloading Behalve functies mag je in C++ ook zelf operatoren defini¨eren. De naam van de operator moet die van ´e´en van de standaardoperatoren zijn, zodat er altijd sprake is van overloading. De definitie ziet er hetzelfde uit als een functiedefinitie, behalve dat aan de naam het woord operator vooraf gaat. Als je zelf bijvoorbeeld een type Complex gemaakt hebt, kun je daarop de operator + als volgt defini¨eren: Complex operator +(Complex a, Complex b) { return makecomplex(a.re + b.re, a.im + b.im); } De volgende binaire operatoren mogen overloaded worden: && = <
|| ++ >
-<=
+ += >=
-= ==
* *= !=
/ % /= %= ->* ,
^ ^=
& &=
| |=
<< >> <<= >>=
en de volgende unaire operatoren: 92
~
! B.1
Nieuwe taalconstructies
Programmeren in C
B
C++ VOOR C KENNERS
Bovendien mogen de volgende symbolen overloaded worden als binaire operator: ->
[]
()
new
delete
Daarmee zijn zelfs “dereferencing”, array-indicering (zie sectie B.3), functie-aanroep, en object-creatie en -deletie (zie sectie B.1) te overloaden.
Dynamische allocatie In C levert de standaardfunctie malloc een blok geheugen van een bepaalde lengte. Het resultaat is van type void* en moet gecast worden naar het juiste type. Omdat pointers ook ge¨ındiceerd mogen worden, kun je hiermee arrays maken met een lengte die voor de compiler nog niet bekend is: int *data; data = (int *) malloc(n * sizeof(int)); f(data[4]); free(data); Het aantal te alloceren bytes is het aantal gewenste elementen n vermenigvuldigd met de afmeting van ´e´en element. De geheugenruimte wordt vrijgegeven met de standaardfunctie free. In C++ kan geheugen worden gealloceerd met het keyword new, gevolg door een type. Anders dan bij gewone declaraties mag als arraygrens hierbij een niet-constante expressie gebruikt worden. Een cast, zoals in C, is nu niet meer nodig. De geheugenruimte wordt weer vrijgegeven met het keyword delete: int *data; data = new int[n]; f(data[4]); delete data;
B.2
Classes
Abstracte datatypen in C In elke cursus over datastructuren leer je dat het handig is om eerst een type te defini¨eren, en vervolgens een aantal functies die op dat type werken. Dat type is meestal een structure met een heleboel velden. Het wordt een “abstract type” als je belooft variabelen van dit type alleen maar met behulp van de daarvoor bedoelde functies te gebruiken, en dus niet zelf in de structure gaat spitten. B.2
Classes
93
B
C++ VOOR C KENNERS
Programmeren in C
In C zou je als volgt een stack kunnen defini¨eren. De implementatie bestaat uit een structure met een array en een integer die aangeeft hoe ver die array gevuld is. In de typedefinitie permitteren we ons alvast de C++-notatie zoals besproken in sectie B.1: struct stack { char info[100]; int top; }; Op dit type werken de functies reset, push en pop, die dus alledrie een stack als parameter hebben. Omdat de stack ook veranderd wordt (althans bij reset en push) wordt een pointer naar de datastructuur meegegeven, en niet de structuur zelf: void reset(stack *s) { s->top = -1; } void push(stack *s, char c) { s->top++; s->info[s->top] = c; } char top(stack *s) { return s->info[s->top]; } Bij het schrijven van deze functies ontkomen we er natuurlijk niet aan om de opbouw van de struct te kennen. In het hoofdprogramma wordt men geacht alleen de abstracte operaties te gebruiken: main() { stack stapel; /* Netjes gebruik: */ reset(&stapel); push(&stapel,’a’); /* Stiekem gebruik: */ stapel.info[73] = ’x’; } Helaas is het niet af te dwingen dat een gebruiker zich aan de belofte houdt om het type alleen te benaderen via de daarvoor bedoelde functies. In het voorbeeld wordt de belofte in het laatste 94
B.2
Classes
Programmeren in C
B
C++ VOOR C KENNERS
statement gebroken. Dat is vervelend, want als je de implementatie zou vervangen door een andere (bijvoorbeeld met een lineaire lijst in plaats van een array), dan geeft het hoofdprogramma ineens een fout in de laatste regel.
Abstracte datatypen in C In C++ kunnen de functies die op een bepaald datatype werken gedefinieerd worden in de typedefinitie van de structure: struct stack { private: char info[100]; int top; public: void reset(void) { top = -1; } void push(char c) { top++; info[top] = c; } char top(void) { return info[top]; } }; Je hoeft niet meer op te schrijven dat deze functies een stack als parameter hebben. In de body van de functies kun je top en info direct gebruiken, zonder daarbij steeds s-> te hoeven schrijven. Bij aanroep van de functies moet wel duidelijk gemaakt worden op welke stack nu eigenlijk de operaties toegepast moeten worden. Daarvoor wordt de punt-notatie gebruikt; per slot van rekening staan de definities van de functies ‘in’ de structure-definitie, en kunnen ze dus ook ‘geselecteerd’ worden: main() { stack stapel; stapel.reset(); stapel.push(’a’); } B.2
Classes
95
B
C++ VOOR C KENNERS
Programmeren in C
In C++ declareer je in een structure-definitie naast data ook functies. Deze functies worden de memberfuncties van de structure genoemd. Met de keywords private en public kun je bovendien aangeven hoe de data en functies gebruikt mogen worden: • private: mogen alleen door de memberfuncties gebruikt worden. • public: zijn ook door buitenstaanders bereikbaar met de punt-notatie. Doordat in het voorbeeld de velden top en data achter private: staan, mogen ze alleen door de memberfuncties gebruikt worden. In het hoofdprogramma kan er dus geen misbruik van gemaakt worden. De drie memberfuncties zijn allemaal public, en mogen dus wel in het hoofdprogramma gebruikt worden. Er zijn echter ook memberfuncties denkbaar die alleen als hulpfunctie zijn bedoeld voor de andere memberfuncties, en die niet van buitenaf gebruikt mogen worden. Die worden dan in het private deel neergezet. Als je eraan twijfelt wat een memberfunctie nu precies inhoudt, dan is het misschien handig om je de implementatie ervan voor te stellen. De code die wordt gegenereerd voor het voorbeeld uit deze sectie is namelijk precies hetzelfde als die voor het C-programma uit de vorige sectie. Dat wil zeggen: • Het gebruik van private en public be¨ınvloedt alleen de scope van de naam van velden, en is voor codegeneratie niet van belang. • Memberfuncties worden niet echt opgeslagen in variabelen. • Memberfuncties hebben stiekem een extra parameter, namelijk een pointer naar de betreffende structure. Waar een memberfunctie de member-variabele x gebruikt, wordt eigenlijk p->x gebruikt, waarbij p de extra meegegeven pointer is.
Classes Een class in C++ is vrijwel hetzelfde als een struct met uitgebreide mogelijkheden, zoals besproken in de vorige sectie. Het enige verschil is de protectie van de members: • In een struct zijn de declaraties zonder tegenbericht public. • In een class zijn de declaraties zonder tegenbericht private. Het stack-voorbeeld had dus ook als class geschreven kunnen worden: class stack { char info[100]; int top; public: void reset(void) { top = -1; } 96
B.2
Classes
Programmeren in C
B
C++ VOOR C KENNERS
void push(char c) { top++; info[top] = c; } char top(void) { return info[top]; } }; Denk om het schrijven van de puntkomma aan het eind van de class-declaratie: het blijft een (uitgebreid soort) type-declaratie! Het woord “class” is ontleend aan het object-geori¨enteerde jargon. Variabelen met een type dat als class is gedefinieerd worden objecten genoemd. Een korte karakterisering van object-geori¨enteerd programmeren is: • Bij imperatief programmeren staan functies centraal, die objecten als parameter kunnen hebben. • Bij objectgeori¨enteerd programmeren staan objecten centraal, die functies als member kunnen hebben. Echt object-geori¨enteerd wordt een taal overigens pas als classes geordend kunnen worden in een hi¨erarchie (zie sectie B.4), en memberfuncties dynamisch gebonden kunnen worden (zie sectie B.4).
Definitie van memberfuncties Tot nu toe definieerden we de memberfuncties direct in de class-declaratie: class c { int x; public: int f(void) { return 2 * x; } }; Als de memberfuncties wat langer worden, wordt dit echter erg onoverzichtelijk, omdat je uit het oog kunt verliezen dat al die pagina’s functie-definities in feite deel uitmaken van ´e´en class-declaratie. Daarom is het ook toegestaan om in de class-declaratie alleen het prototype van de memberfuncties op memberfuncties dynamisch gebonden kunnen worden (zie sectie B.4), en te nemen: B.2
Classes
97
B
C++ VOOR C KENNERS
Programmeren in C
class c { int x; public: int f(void); }; De eigenlijke definitie van de memberfunctie kan daarna apart plaatsvinden. Daarbij moet, als onderscheid met gewone functies, de naam van de class opnieuw genoemd worden: int c::f(void) { return 2*x; } Meestal wordt de class-declaratie in de .h-file geschreven, en de definities van de memberfuncties in de .cc-file. De class-definitie is immers ook nodig in eventuele andere modules die deze class gebruiken, maar de implementatie van de memberfuncties niet. Een bijkomend verschil tussen memberfunctie-definities in de class-declaratie, en losstaande definities, is dat definities in de class-declaratie per default inline zijn, zoals beschreven in sectie B.1. Dit is dus alleen maar aan te raden voor zeer korte definities.
Constructors Vaak is het nuttig om objecten (dat wil zeggen variabelen waarvan het type een class is) te initialiseren. Hiervoor kan een speciale memberfuncties worden gedefinieerd: een constructor. Deze moet dezelfde naam hebben als de klasse, en heeft geen resultaattype (zelfs niet void). In het voorbeeld van de stack, zou je de top-wijzer bij creatie van een stack willen kunnen initialiseren. Dat kan als volgt: { char info[100]; int top; public: stack(void) { top = -1; } }; De constructorfunctie wordt automatisch aangeroepen op het moment dat een object wordt gecre¨eerd. Voor globale variabelen is dat voor uitvoer van main, voor lokale variabelen het moment dat de declaratie in het programma “uitgevoerd” wordt, en voor dynamische variabelen bij gebruik van new. Voorbeeld: 98
B.2
Classes
Programmeren in C
B
C++ VOOR C KENNERS
stack a; main() { print(1); stack b, *p; print(2); p = new stack; } Eerst wordt hier de constructor van a aangeroepen. Dan wordt in het hoofdprogramma de waarde 1 geprint. Vervolgens wordt de constructor van b aangeroepen. Dan wordt 2 geprint, en tenslotte wordt de constructor van de stack waar p naar wijst aangeroepen. Nu blijkt ook waarom het nuttig is dat declaraties en statements elkaar kunnen afwisselen: aanroep van de constructorfunctie van a vindt pas plaats na dat de waarde 1 geprint is. Constructorfuncties kunnen ook parameters krijgen. Actuele parameters moeten dan meegegeven worden bij declaratie en bij gebruik van new. Voorbeeld: class punt { int x, y; public: punt(int x0, int y0) { x = x0; y = y0; } }; main() { punt hier(12, 5), *p; p = new punt(2, 6); } Let op: in het laatste statement vervult punt dus zowel de rol van type (achter new) als van (constructor)functie (voor zijn parameters). Naast constructorfuncties zijn er ook destructorfuncties. Die worden automatisch aangeroepen aan het eind van de levensduur van een object. Bij globale variabelen is dat na afloop van de functie main, bij lokale variabelen aan het eind van die functie (en vlak voor elke return), en by dynamische variabelen bij gebruik van de delete constructie. Nu is dus ook duidelijk waarom er een aparte delete-constructie nodig is in plaats van de functie free uit C. Een voorbeeld hiervan volgt in sectie B.3. B.2 Classes
99
B
C++ VOOR C KENNERS
B.3
Programmeren in C
Voorbeelden
Dynamisch geheugen Vaak zal er in objecten gebruik gemaakt worden van dynamisch geheugen. Dit geheugen kan worden gealloceerd in de constructorfunctie, en worden opgeruimd in de destructorfunctie. Een voorbeeld is alweer de stack-klasse. In eerdere voorbeelden gebruiken we een array ter lengte 100, nu maken we een dynamische array. Bij de membervariabelen zit er alleen maar een pointer naar de data: class stack { char *info; int top; In de constructorfunctie laten we de pointer wijzen naar nieuw aangemaakt geheugen. De constructorfunctie heeft een parameter die aangeeft hoe lang de stack maximaal mag worden: public: stack(int n) { info = new char[n]; top = -1; } In de destructorfunctie wordt het geheugen weer opgeruimd. Net als de constructorfunctie heeft de destructorfunctie geen resultaattype. De naam is die van de class, voorafgegaan door een slangetje: ~stack(void) { delete info; } }; In het hoofdprogramma kunnen we nu stacks van verschillende lengte aanmaken: main() { stack a(100); stack b(1000); } Bij gebruik van stacks hoef je je nooit meer druk te maken over het opruimen ervan: dat gaat automatisch.
Open/sluit constructies Er zijn meer gevallen (dan dynamische geheugenallocatie) waarbij je in C geconfronteerd wordt met “vergeet vooral niet aan het eind . . . te doen”. Files bijvoorbeeld moeten aan het begin geopend 100
B.3
Voorbeelden
Programmeren in C
B
C++ VOOR C KENNERS
worden, daarna mag je er van alles mee doen, maar aan het eind moeten ze weer gesloten worden. In een typisch C programma kun je dan ook aantreffen: File f; f = open("aap"); seek(f, pos); read(f, data); close(f); In een C++-library zou het openen mooi in de constructorfunctie kunnen staan, en het sluiten in de destructorfunctie. Bovendien hoeft de file niet steeds meegegeven te worden bij operaties die op de file werken. Althans niet expliciet. Achter de schermen wordt de file natuurlijk wel meegegeven. { File f("aap"); f.seek(pos); f.read(data); }
Veilige arrays Een bekend nadeel van C is dat bij indicering van arrays de grens niet gecontroleerd wordt. Je kunt hier wat aan doen door een klasse te maken waarin array-operaties worden gedefinieerd met grens-controle. In dit voorbeeld noemen we deze klasse Vector. De elementen zijn integers. In de constructorfunctie wordt een array gealloceerd, maar bovendien wordt de bovengrens bewaard: class Vector { int *p; int size; public: Vector(int n = 10) { size = n; p = new int[n]; } ~Vector(void) { delete p; }
B.3
Voorbeelden
101
B
C++ VOOR C KENNERS
Programmeren in C
int &elem(int); };
Verder is er een memberfunctie elem, waarmee een element uit de array geselecteerd kan worden. Deze functie geeft een foutmelding als de index buiten het toegestane interval ligt.
int &Vector::elem(int i) { if (i < 0 || i >= size) printf("Bound error!\n"); return p[i]; }
De functie elem levert een referentie naar een integer op. Daardoor kan de functie worden gebruikt in expressies, maar ook aan de linkerkant van een assignment.
main() { Vector a(10), b(5); a.elem(1) = 17; b.elem(1) = a.elem(1) + 9; }
Helemaal mooi wordt het als we de functie elem als operator defini¨eren, en wel de operator [].
int &Vector::operator [](int i) { if (i < 0 || i >= size) printf("Bound error!\n"); return p[i]; }
Daarna lijken vectoren voor de gebruiker “echte” arrays:
main() { Vector a(10), b(5); a[1] = 17; b[1] = a[1] + 9; } 102
B.3
Voorbeelden
Programmeren in C
B.4
B
C++ VOOR C KENNERS
Classes (vervolg)
Static members Het woord static krijgt er in C++ een betekenis bij. In C wordt dit gebruikt voor variabelen die qua scope lokaal zijn voor een functie, maar waarvan slechts ´e´en instantie wordt gemaakt. Een static variabele overleeft dus een functieaanroep, en kan bijvoorbeeld gebruikt worden om te tellen hoe vaak de functie is aangeroepen: void f(void) { static int n = 0; printf("Ik ben %d keer aangeroepen.\n", n++); } Voor de implementatie is het alsof de variabele n buiten de functie was gedeclareerd; het enige verschil is dat het niet toegestaan is om de variabele buiten de functie te gebruiken. Op dezelfde manier kunnen in C++ member-variabelen static worden gedeclareerd. Ook nu wordt er slechts ´e´en instantie van die variabele gemaakt, die door alle instanties van de class wordt gedeeld: class c { static int n; int x; int y; public: void f() { n++; } void g() { n++; } void h() { printf ("%d keer memberfuncie gebruikt.\n", n); } }; Ook dit is eigenlijk een globale variabele, met de restrictie dat hij alleen in de memberfuncties van een bepaalde klasse mag worden gebruikt. Naast membervariabelen kunnen ook memberfuncties static worden gedeclareerd. Static memberfuncties mogen geen gebruik maken van de membervariabelen. Daarom hoeven ze in een implemenB.4
Classes (vervolg)
103
B
C++ VOOR C KENNERS
Programmeren in C
tatie dus geen verborgen parameter mee te krijgen. Het zijn eigenlijk gewone functies, die ook buiten de klasse gedeclareerd hadden kunnen worden. Het enige verschil is dat de functies buiten de klasse niet aangeroepen mogen worden.
Objecten als members Inmiddels is er een probleempje ontstaan, vanwege twee nieuwe mogelijkheden: constructorfuncties kunnen parameters hebben, en membervariabelen mogen zelf ook objecten zijn. Het probleem is: welke parameter moet meegegeven worden bij constructie van de deel-objecten? Bekijk het voorbeeld van een klasse waarin twee vectoren, zoals gedefinieerd in sectie B.3, worden gecombineerd: class Twee { Vector heen; Vector terug; public: void Twee(int n) {} }; Je kun bij declaratie van de vectoren niet n meegeven, want diens scope is beperkt tot de body van de constructorfunctie Twee. Maar je kunt heen en terug ook niet declareren in de body van de constructorfunctie Twee, want dan zouden ze weer worden opgeruimd aan het eind van de constructorfunctie. De oplossing is een speciale syntax voor dit geval: class Twee { Vector heen; Vector terug; public: Twee(int n): heen(n), terug(n) {} }; Tussen de header en de body van een constructorfunctie mogen dus de deel-objecten van parameters worden voorzien.
this Memberfuncties zijn bedoeld om op objecten te werken zonder dat het object steeds expliciet als parameter meegegeven hoeft te worden (achter de schermen gebeurt dat natuurlijk wel). Maar soms 104
B.4
Classes (vervolg)
Programmeren in C
B
C++ VOOR C KENNERS
wil je een object ook wel meegeven aan een andere functie dan de memberfuncties van zijn eigen klasse. In dat geval moet je de parameter expliciet noteren. Bekijk bijvoorbeeld een klasse, en een functie die een pointer naar een object uit die klasse meekrijgt als parameter: class klasse; void f(klasse *obj); Als je in het hoofdprogramma een object declareert, kun je dat object moeiteloos meegeven als parameter: main() { klasse a, *p; f(&a); p = new klasse; f(p); } Anders wordt het als de functie f aangeroepen moet worden vanuit ´e´en van de memberfuncties van de klasse. Die memberfuncties hebben toegang tot een object van hun klasse, want memberfuncties hebben achter de schermen een extra parameter. Maar hoe moeten ze deze extra parameter aan f doorspelen? Het is immers een extra parameter, die niet expliciet een naam heeft. De oplossing hiervoor is het keyword this dat in C++ beschikbaar is. Memberfuncties kunnen hiermee de pointer naar “hun” object aanduiden. class klasse { int x; public: void g(void); void h(void) { // Impliciete parameter gebruikt voor toegang tot andere members. print(x); g(); // Moet expliciet genoemd om "dit" object // door te geven aan niet-member functies. f(this); } }; B.4
Classes (vervolg)
105
B
C++ VOOR C KENNERS
Programmeren in C
Operatoren als members Omdat memberfuncties impliciet een extra parameter hebben, krijgen operatoren die als member worden gedefinieerd een parameter minder dan je zou verwachten. Een binaire operator wordt gedeclareerd met ´e´en parameter: class Polynoom { Polynoom operator +(Polynoom *y); }; De andere parameter is de impliciete “this” parameter.
Subklassen Het leuke van object-geori¨enteerd programmeren is dat je classes eenvoudig kunt uitbreiden met een paar extra members. Je kunt dus voortborduren op werk van anderen, en hoeft voor de nieuwe classes niet opnieuw alle oude members op te sommen. Stel bijvoorbeeld dat er een klasse Persoon is, met allerlei interessante persoonsgegevens, en de nodige memberfuncties: class Persoon { public: char naam[20]; int gebJaar; int leeftijd(void); void print(void); }; Je kunt dan zelf een klasse Student maken, die aan de persoonsgegevens een membervariabele toevoegt voor de studie. Daartoe moet je achter de naam van de klasse specificeren dat het een derived class is van de oorspronkelijke klasse: class Student: public Persoon { public: char studie[10]; void print(void); }; In dit voorbeeld hebben we niet alleen een extra membervariabele toegevoegd, maar ook een memberfunctie. Die heeft toevallig dezelfde naam als een memberfunctie in de oorspronkelijke klasse, maar dat is geen probleem: functies mogen immers overloaded worden. 106
B.4
Classes (vervolg)
Programmeren in C
B
C++ VOOR C KENNERS
Als je in het programma een variabele van het type Student declareert, dan mag je daarvan de nieuwe members gebruiken (s.studie), maar ook de oude (s.naam). Die laatste worden zogezegd ge¨erfd van de oorspronkelijke klasse. main() { Student s; s.naam = "Jan-Jacob"; s.studie = "Informatica"; s.print(); } Waar in het hoofdprogramma de functie print wordt aangeroepen, is dat de functie print zoals die in de klasse Student is gespecificeerd.
Hergedefinieerde memberfuncties Als tweede voorbeeld schrijven we een klasse die voortborduurt op de klasse Vector uit sectie B.3. Die luidde: class Vector { int *p, size; public: Vector(int); ~Vector(void); int &elem(int); }; We maken nu een subklasse BndVector voor een array die niet alleen een bovengrens heeft, maar ook een ondergrens (bij de oorspronkelijke vectoren moest die ondergrens 0 zijn). In de nieuwe klasse wordt, naast de ge¨erfde members, een nieuwe variabele toegevoegd: class BndVector: public Vector { int eerste; public: In de constructorfunctie van BndVector moet de constructorfunctie van Vector worden aangeroepen. Omdat die een parameter heeft, moeten we ons weer van speciale syntax bedienen: BndVector(int lo, int hi): Vector(hi - lo) { B.4
Classes (vervolg)
107
B
C++ VOOR C KENNERS
Programmeren in C
eerste = lo; } Let op het verschil met sectie B.4: daar stond achter de dubbele punt de naam van een member, die zelf een object was; nu staat er achter de dubbele punt de naam van de superklasse. Een ander probleem treedt op in de definitie van elem. Hierbij willen we de originele elem aanroepen, echter met een voor de ondergrens gecorrigeerde parameter. Zou je echter in de memberfunctie elem de functie elem aanroepen, dan resulteert dit in een recursieve aanroep. Daarom moeten we de naam elem qualificeren voor de juiste klasse. Dat gebeurt door de naam van de klasse waaruit we de functie willen hebben voor de naam te schrijven: int &elem(int i) { return Vector::elem(i - eerste); } }; Die klasse moet natuurlijk wel een superklasse zijn van de klasse waarin de aanroep gebeurt, of een superklasse van die superklasse, enz.
Statische en dynamische binding Binding van functienamen aan functies gebeurt statisch. Dat wil zeggen dat de compiler, op grond van de beschikbare type-informatie, beslist welke functie gebruikt wordt bij een bepaalde naam. Bekijk bijvoorbeeld een klasse A, en een subklasse B daarvan, die beide een functie f als member hebben: class A { public: int f(void) { return 1; } }; class B: public A { public: int f(void) { return 2; } }; Als in het hoofdprogramma een functie met de naam f wordt aangeroepen, dan kijkt de compiler naar het type van het ontvangende object: 108
B.4
Classes (vervolg)
Programmeren in C
B
C++ VOOR C KENNERS
main() { A a; B b; a.f(); b.f();
// levert 1 // levert 2
} Iets lastiger wordt het als we pointers naar objecten gebruiken. Bekijk het volgende programma: main() { A a, *pa; B b; pa = &a; pa->f(); pa = &b; pa->f();
// // // //
dit mag levert 1 dit mag ook! wat levert dit?
} We declareren een pointer die naar objecten van type A kan wijzen. In het eerste statement laten we de pointer naar a van type A wijzen. Aanroep van de functie f levert dan natuurlijk 1. We mogen de pointer echter ook laten wijzen naar objecten met als type een subklasse van A. Een subklasse is immers een “bijzonder” geval van de klasse, dus een pointer die naar objecten van type A kan wijzen, kan zeker naar objecten van type B wijzen. Dus pa = &b is toegestaan. Maar wat levert nu de aanroep pa->f()? Omdat de binding statisch is, moet de compiler beslissen. En omdat de voorgeschiedenis van de aanroep pa->f() erg ingewikkeld kan zijn, kan de compiler niet in alle gevallen nagaan wat het type is van het object waar pa naar wijst. Zeker is echter, dat dit het type A is of een subtype van A. Veiligheidshalve wordt daarom de functie f van A gebruikt. De aanroep pa->f() levert dus 1, ongeacht het type van het object waar pa toevallig naar wijst. Het is in C++ echter mogelijk om te kiezen voor dynamische binding van functies. Dat moet dan gespecificeerd worden bij de declaratie van de functie in de superklasse, met behulp van het woord virtual. class A { public: virtual int f(void) { return 1; } }; class B: public A { public: int f(void) // type moet hetzelfde zijn als A::f B.4
Classes (vervolg)
109
B
C++ VOOR C KENNERS
Programmeren in C
{ return 2; } }; Een restrictie is nu dat het type van de in de subklasse opnieuw gedefinieerde functie hetzelfde moet zijn als het origineel. Als we nu weer hetzelfde hoofdprogramma schrijven: main() { A a, *pa; B b; pa = &a; pa->f();
// levert 1
pa = &b; pa->f();
// levert nu 2!
} Dan wordt run-time nagegaan naar welk type object de pointer wijst. Afhankelijk daarvan wordt de juiste functie f gekozen. De aanroep p->f() levert nu dus 2 op, op het moment dat de pointer naar een object van type B wijst. Voor klassen waarin virtual memberfuncties worden gebruikt, zal dus het type van een object in dat object moeten worden opgeslagen. Dat kan, want bij creatie van een object is het type ervan bekend. In de meeste implementaties gebeurt dit in de vorm van een pointer naar een tabelletje waarin de implementatie van de functies behorend bij deze klasse staan. Run-time kan in dit tabelletje de juiste waarde worden opgezocht. Virtuele functies kosten dus iets meer tijd (een extra indirectie bij de aanroep) en ruimte (´e´en pointer per object). Daar staat tegenover dat ze een groot gemak bieden voor de programmeur: die kan nu immers een pointer laten lopen door een lijst met zowel “personen” als “studenten”, voor elke waarde de functie print aanroepen, en toch steeds de goede versie daarvan krijgen.
B.5
Input/output
Streams Dankzij de nieuwe taalconstructies die in C++ beschikbaar zijn, konden er nieuwe faciliteiten voor input/output gemaakt worden. Deze zijn gemakkelijker te gebruiken dan printf en dergelijke. De vertrouwde <stdio.h> blijft natuurlijk beschikbaar, maar is eigenlijk overbodig geworden. De nieuwe I/O-bibliotheek heet . In wordt een klasse stream gedefinieerd. Twee streams die alvast beschikbaar zijn, zijn cin en cout, corresponderend met de standaard-input en standaard-output. Op deze streams zijn de operaties <<, respectievelijk >> gedefinieerd. (Die operatoren bestonden al in C, met de betekenis “shift left” en “shift right”; ze zijn nu overloaded, zodat ze ook op streams kunnen werken). Deze operaties werken zo, dat het transport van data in de richting van de pijltjes plaatsvindt: 110
B.5
Input/output
Programmeren in C
B
C++ VOOR C KENNERS
#include main() { int n; cin >> n; cout << 2*n;
// lezen // schrijven
} Door middel van overloading zijn deze operatoren gedefinieerd voor alle standaardtypen. Afhankelijk van het type van de rechter parameter krijg je de goede versie. Het is dus niet meer nodig om met "%d" of "%s" en dergelijke het type aan te geven, zoals dat bij printf en scanf nodig was. (Het is ook niet meer mogelijk om daar fouten bij te maken.) Wat betreft het type van de I/O-operatoren: de linker parameter is altijd een stream. De rechter parameter kan van elk standaardtype zijn. In het geval van << is dat een value parameter, maar bij >> is het een reference parameter. Het is dus geen pointer-parameter, zoals dat bij scanf het geval was, en je kunt dus ook bij aanroep de &-operator niet meer vergeten op te schrijven. Als resultaat leveren << en >> hun linker parameter weer op. Dat is handig, want zo kun je meerdere dingen na elkaar lezen of schrijven: cin >> a >> b >> c; cout << "de som is " << a + b + c; Blijkbaar associ¨eren << en >> naar links. Om een nieuwe regel te schrijven, kun je de string "\n" naar een stream sturen, maar beter is het om de speciale constante endl te gebruiken: cout << "\n"; cout << endl;
// dit mag // dit is beter
Het voordeel van de laatste is dat naast het schrijven van de newline de stream “geflushd” wordt, zodat de uitvoer niet gebufferd wordt. Als lezen niet mogelijk is (bijvoorbeeld omdat het einde van de file bereikt is) levert cin een nullstream op. Daarmee kun je dus eenvoudig testen of het einde van de invoer bereikt is: while (cin >> n) cout << n; De klasse stream kent een aantal memberfuncties, waarmee extra aanwijzingen over de te gebruiken lay-out van uitvoer gespecificeerd kunnen worden. (Bij printf gebeurde dat in de “format-string”). Zo is er bijvoorbeeld de memberfunctie width, waarmee het aantal te gebruiken posities gespecificeerd wordt, en precision waarmee de precisie van uit te voeren floating-point getallen wordt aangegeven: cout.width(10); cout.precision(7); cout << pi; B.5
Input/output
111
B
C++ VOOR C KENNERS
Programmeren in C
Verder is er een memberfunctie get, waarmee ´e´en character gelezen kan worden. Deze is nodig omdat << alvorens een karakter te lezen spaties en newlines overslaat. Een kopieer-programma is dus: #include main() { char c; while (cin.get(c)) cout << c; }
Files In feite is er niet ´e´en klasse stream. Deze klasse heeft twee subklassen: istream en ostream. Memberfuncties die alleen relevant zijn voor outputstreams zijn gedefinieerd in ostream. Deze klassen hebben op hun beurt weer subklassen voor streams die gekoppeld zijn aan een file: ifstream respectievelijk ofstream. (De naam ifstream is een afkorting van “input file stream” en heeft dus niets met if-statements te maken.) De constructorfunctie van deze streams heeft een filenaam als parameter. Daarmee is het mechanisme uit sectie B.3 gerealiseerd: #include main() { ofstream f("aap"); f << "deze tekst wordt in een file gezet" << endl; } De file hoeft niet expliciet gesloten te worden: dat gebeurt in de destructorfunctie.
B.6
Slot
Andere constructies In dit korte bestek konden niet alle details van C++ behandeld worden. Een aantal dingen die niet genoemd zijn, en die je “in het wild” kunt tegenkomen zijn: • Access control. Naast public en private is er ook nog protected: members die alleen toegankelijk zijn voor subklassen. • Friends. Bepaalde klassen en/of functies kunnen aangewezen worden als “friend”, die toch gebruik mogen maken van de private members. 112
B.6
Slot
Programmeren in C
B
C++ VOOR C KENNERS
• Multiple inheritance. Klassen kunnen gelijktijdig subklasse zijn van meerdere klassen. De overervings-hi¨erarchie is dus niet een boomstructuur, maar een gerichte graaf. • Templates. Klassen kunnen worden geparametriseerd met een type, zodat polymorfe datastructuren kunnen worden gedefinieerd. • Container classes. In de praktijk kan het nuttig zijn om enkele libraries te kennen waarin een aantaal veelgebruikte datastructuren als klasse beschikbaar zijn. Vooral container classes, waarin dingen als lijsten en verzamelingen zijn ge¨ımplementeerd, zijn populair. Voor dit soort klassen worden vaak speciale iterator classes gedefinieerd, waarmee ze gemakkelijk langsgelopen kunnen worden. Op objecten van zo’n iterator class is typisch de operator ++ gedefinieerd, zodat het langslopen van bijvoorbeeld een lijst erg gaat lijken op het langslopen van een array. Waar je op moet letten bij container classes is of bij destructie van de containers ook de objecten die daar in zitten worden opgeruimd of juist niet.
Andere talen Veel van de in deze tekst genoemde onderwerpen, in het bijzonder de classes, kom je ook tegen bij het bestuderen van andere object-geori¨enteerde talen. Deze talen kunnen afwijken van C++ op grond van de keuzen die erin gemaakt zijn. Bijvoorbeeld: • Classes en typen. In sommige talen zijn er geen “gewone” typen meer, maar is alles een class. Ale functies zijn dan ook memberfuncties. • Binding van memberfuncties. In C++ is de binding van memberfuncties alleen bij virtual members dynamisch. In sommige talen is de binding altijd dynamisch. • Type van her-definities. In C++ moet het type van her-definities van virtual functies in subklassen precies hetzelfde zijn als in de oorspronkelijke klasse. Deze eis is in sommige talen minder streng. In een taal kan de keuze vallen op contravariante herdefinities of covariante herdefinities, elk met hun eigen problematiek.
Literatuur • Stroustrup, Bjarne. The C++ programming Language, second edition. Reading: AddisonWesley, 1991. • Ellis, Margaret and Bjarne Stroustrup. The annotated C++ reference manual. Reading: Addison-Wesley, 1994.
B.6
Slot
113
C
C
INDEX
Index
aanroep van functies, 28 abs(), 85 andere programmeertalen, 6 ANSI C, 6 argc, 19 argumenten, 68 arrays, 73 pointers, 71 argv, 19 arrays, 59 functies, 73 ge¨ındexeerd, 59 pointers, 61 reserveren, 62 twee- en meerdimensionaal, 60 ASCII, 46 assignment, 28 associativiteit van operatoren, 57 atof(), 84 atoi(), 85 atol(), 85 bereik van identifiers, 25 bibliotheken, 80 in- en uitvoer, 80 standaardfuncties, 84 string manipulatie, 83 wiskundige functies, 84 binaire operatoren, 53 break, 31, 37 case, 31 cast-operator, 53 char, 46 commentaar, 21 compiler, 6 continue, 37 conversie format string, 80 van types, 52 declaratie, 27 default, 31 #define, 17 do..while, 35 double, 47 enum, 48 exponent, 48 fclose(), 77, 82 114
Programmeren in C
feof(), 78, 83 fgets(), 78, 83 FILE, 76 float, 47 fopen(), 76, 81 for, 33 fprintf(), 77, 83 fread(), 83 free(), 44, 85 fscanf(), 77, 83 fseek(), 82 functies, 68 aanroep, 28 arrays, 73 definitie, 68 pointers, 71 fwrite(), 83 geheugen, 4, 40 arrays reserveren, 62 datatypes, 51 reserveren, 44 gereserveerde woorden, 23 gestructureerd programmeren, 74 getallen gehele, 47 re¨ele, 47 gets(), 81 GNU C, 10 goto, 39 herhalingsstatements, 33 IDE, 10 identifiers bereik, 25 normale, 24 standaard, 24 if, 29 #include, 16 index, 59 instructieset, 4 int, 47 Integrated Development Environment, 10 karakters, 46 keuzestatements, 29 labs(), 85 layout, 19 logische operatoren, 55
Programmeren in C
long, 50 main(), 18 malloc(), 44, 85 mantisse, 48 math.h, 84 Microsoft Visual C++, 10 NULL, 78 operatoren, 53 associativiteit, 57 binaire, 53 logische, 55 prioriteit, 57 relationele, 54 unaire, 53 verkorte schrijfwijzen, 55 wiskundige, 54 pointers, 42 functies, 71 post-increment, 56 pre-increment, 56 preprocessor, 16 printf(), 80 prioriteit van operatoren, 57 processor, 4 programmeeromgeving, 10 rand(), 85 rangnummer, 46 realloc(), 85 registers, 4 relationele operatoren, 54 remove(), 82 rename(), 82 return, 38 returnwaarde, 68 rewind(), 82 samengestelde statements, 28
C
INDEX
scanf(), 81 short, 50 signed, 50 sizeof, 51 srand(), 85 statement, 27 herhalings, 33 keuze, 29 overige, 37 samengestelde, 28 stdio.h, 80 stdlib.h, 84 strcat(), 64, 84 strcmp(), 65, 84 strcpy(), 64, 83 string.h, 83 strings, 63 struct, 65 structs, 65 switch, 31 typeconversie, 52 automatische, 52 cast-operator, 53 typedef, 66 types, 46 conversie, 52 definitie, 66 elementaire, 46 unaire operatoren, 53 unsigned, 50 variabelen, 40 declaratie, 27 globale, 16 lokale, 18 void, 70 wetenschappelijke notatie, 48 while, 35 wiskundige operatoren, 54
115