Leren C programmeren in Linux Versie April 2003 Vanaf het eerste begin tot het zelf schrijven van applicaties Door Pascal Schiks Een inleiding van de taal C voor Linux gebruikers. ------------------------------------------------------------------------------ Voorwoord ------------------------------------------------------------------------------ Regelmatig kom ik op het nedlinux forum de vraag tegen of er een goed C boek bestaat voor beginners. Zo af en toe duikt er weer eens een boek op, maar echt tevreden is men zelden. Dit was voor mij uiteindelijk aanleiding om dan maar zelf iets te schrijven. Niet domweg wat op papier zetten, maar ingaan op de vragen en antwoorden van de veelal jonge programmeurs die ons gezellige forum rijk is. Door deze werkwijze hoop ik een werkje samen te kunnen stellen dat gericht is op de vragen uit de praktijk. Veel plezier ermee Pascal. ------------------------------------------------------------------------------ Inleiding ------------------------------------------------------------------------------ De taal C is van oudsher onlosmakelijk verbonden met UNIX. Niet alleen is C een erg veel gebruikte taal voor het ontwikkelen van UNIX applicaties, het besturingssysteem zelf is ook vrijwel geheel in deze taal geschreven. Dat geldt zeker voor Linux. Door deze werkwijze wordt een grote portabiliteit gegarandeerd. Oftewel, een C programma geschreven voor Linux, kan zonder problemen op een ander UNIX systeem gecompileerd en uitgevoerd worden. Hoe dit in zijn werk gaat en waarom dit mogelijk is, probeer ik stap voor stap met wat eenvoudige voorbeelden te demonstreren. ------------------------------------------------------------------------------ Gebruikte standaarden ------------------------------------------------------------------------------ Om e.e.a. wat te verduidelijken heb ik me aan de volgende staandaard gehouden Nieuw hoofdstuk Zo wordt elk nieuw hoofdstuk aangekondigd Kommando's Alle kommando's die in de shell ingetypt dienen te worden zijn bold Programma code wordt in italics geschreven Onderlijnd is alles waar ik extra aandacht op wil vestigen ------------------------------------------------------------------------------ H1 Hello World ------------------------------------------------------------------------------ Hello World. Zoals iedereen zal ik beginnen met het introduceren van het programma "Hello World" De functie van dit programma is enkel het tonen van de tekst "Hello World" op het scherm. Het is dus een minimaal programma zonder enige nuttige functie. Hier volgt de code voor dit programma : /* Hello world */ #include main() { printf("Hello World\n"); } Het programma beslaat slechts drie instructies, waarvan slechts een de uitvoer verzorgt. C is een taal met een heel beperkte instructieset. Eigenlijk kan C niet eens in- en uitvoer verzorgen. Omdat we hiervoor toch graag enigerlei functionaliteit willen, heeft C de mogelijkheid om een aantal bibliotheken toe te voegen, waarin de meest gewenste functies zijn ondergebracht. Hier vind je al meteen de kracht van de porteerbaarheid. Deze bibliotheken hoeven n.l. niet echt bekend te zijn bij de programmeur. Deze hoeft enkel te weten welke functies er in de bibliotheken aanwezig zijn, en daar deze redelijk gestandaardiseerd zijn is het voldoende een nieuw programma op een andere machine opnieuw te compileren om het uit te kunnen voeren. Van de functie printf welke wij willen gebruiken staat een prototype in het bestand stdio.h. Dit bestand staat bij UNIX systemen in de directory /usr/include. Maar bij een ander systeem, bijvoorbeeld een DOS-machine kan dat best ergens anders zijn. Voor de programmeur niet noodzakelijk om te weten, de compiler weet die wel te vinden. Met de instructie #include vertellen we de compiler dat we gebruik willen maken van de functies van de standaard in en out bibliotheek. Let wel er staat in dit bestandje enkel een beschrijving van de functie. Hoe die verder werkt is niet relevant en staat er dus ook niet beschreven. Wil je weten welke functies er nog meer gedefineerd staan in stdio.h, tik dan in : man stdio en Linux laat je keurig een beschrijving van deze include file zien waarin weer verwijzingen naar de betreffende functies staan. De functie main is wel heel bijzonder, het is n.l. de enige functie die C zelf al kent. De functie main doet eigenlijk niets, de C compiler weet enkel dat hij bestaat. Bij het opstarten van het programma, wordt deze functie aangeroepen. De functie main is dus eigenlijk het programma. Ons programma doet niet zo veel, het voert enkel de functie printf uit. Waneer een functie wordt aangeroepen kun je deze informatie meegeven. Deze informatie moet je dan tussen de ronde haakjes zetten. Hoef je niets mee te geven dan hoef je niets tussen die haakjes te zetten. Maar ze moeten er wel zijn ! Deze regel geldt zelfs voor de functie main. Later zul je zien dat je zelfs main informatie kunt meegeven. Verder moet elke opdracht, bewerking of aanroep van een functie afgesloten worden met een puntkomma. Deze regel geldt niet waneer er een functiedefinitie volgt. In dit geval wordt de definitie begonnen en afgesloten met een accolade Een voorbeeld hiervan is de functie main van ons programma. De tekst die tussen /* en */ staat, wordt door de C compiler genegeerd. Op die manier is het mogelijk commentaar in je programma te zetten, waardoor de leesbaarheid groter wordt. In dit voorbeeld is /* hello world */ dus commentaar ------------------------------------------------------------------------------ H2 De C compiler ------------------------------------------------------------------------------ Voor dat het programma "hello world" uitgevoerd kan worden, dient het vertaald te worden in een voor de microprocessor begrijpelijke code. Hiervoor dient de compiler. Van oudsher heet de C-compiler bij een UNIX systeem CC, maar bij Linux kun je meerdere C compilers aantreffen, elk met zijn specifieke eigenschappen. De bekendste is gcc. Dit staat voor GNU-C-Compiler. Veelal is cc gewoon een link naar deze compiler. Een andere bekende is g++. Eigenlijk gaat het meestal om de zelfde compiler aangeroepen door verschillende scriptjes. gcc is er voor diverse platforms en verdient dus de voorkeur om te gebruiken. Waneer je n.l. een programa schrijft dat op je PCtje prima draait wanneer het gecompileerd is met gcc, dan kun je er redelijk zeker van uitgaan dat het ook valt te compileren op een SUN, FreeBSD systeem of mischien wel een IBM S390 mainframe. Om een programma te vertalen gebruiken we de compiler op de volgende manier gcc broncode.c -o programmanaam om ons hello world programma te compileren typen we dus gcc helloworld.c -o helloworld Waneer dit zonder problemen verloopt, dan wordt er een uitvoerbaar bestand met de naam helloworld gecreëerd. Dit kun je vervolgens gewoon met de opdracht helloworld uitvoeren. Op sommige systemen zal een "command not found" melding volgen. Dit komt dan omdat de huidige directory niet in het zoekpad staat. De oplossing hiervoor is het aanpassen van de opdracht in ./helloworld De GNU-C-Compiler heeft erg veel mogelijkheden en daarom ook erg veel opties. Het voert eel te ver om die allemaal te behandelen. Gelukkig bestaat er een uitgebreide manual voor de compiler, op te vragen met de opdracht man gcc. ------------------------------------------------------------------------------ H3 Variabelen ------------------------------------------------------------------------------ Variabelen Om met gegevens te kunnen werken en rekenen, kun je in C variabelen gebruiken. Deze kun je een zelf verzonnen naam geven. Voor eenvoudige programma's zijn namen als A,B,C al voldoende, maar waneer het programma ingewikkelder wordt, verkienen namen als afstand, tijd of snelheid meer de voorkeur. Afhankelijk van het doel van de variabelen moeten we de compiler vertellen van welk type deze zijn. C kent een aantal standaard type definities. char Een variabele ter grote van een byte, en neemt daarmee de minste plaats in. Deze kan 256 verschillend waarden hebben; de waarde kan dus lopen van 0 tot 255 of van -127 tot 128. Dit type word meestal gebruikt voor het bewaren van karakters. Maar ook voor een logische waarde is dit type geschikt. int Afhankelijk van je computer neemt dit type twee of meer bytes in. Beter gezegd, de grote van een integer is gelijk aan de grote van een woord. Op een ouder systeem als MSDOS of een CP/M machine is dat 16bits waardoor de waarde kan lopen van 0 tot 65535 of van -32767 tot 32768 Echter, Linux is een 32 bits besturing systeem en dus heeft een integer hier ook een lengte van 32 bits; groot zat dus. Het type int is dus bedoeld voor het opslaan van gehele getallen. float Vaak is het belangrijk dat je met decimale getallen kunt werken. we willen bijvoorbeeld de waarde 5.5 opslaan om er later mee te rekenen. Voor dit doel maken we gebruik van floating point variabelen. Later zul je zien dat er meer typen zijn en je er zelf kunt toevoegen. Onthoud echter wel, dat verschillende typen niet door elkaar te gebruiken zijn: immers de hoeveelheid plaats die ze innemen is verschillend. Dus of er is informatie te kort, of er valt informatie weg. Hoewel het mogelijk is van het ene type naar het andere over te gaan, is het dus zaak hiermee rekening te houden. Hier volgt een klein programma waarin de besproken typen worden toegepast. Het programma heeft geen in of uitvoer, en is dus van weinig nut. Het is dan ook maar ter ilustratie. /* H3 variabelen.c */ #include main() { /* definitie van de variabelen */ char karakter; int gehelewaarde; float decimaalwaarde; /* de variabelen worden met een waarde geladen */ karakter = 'A'; gehelewaarde = 280; decimaalwaarde = 25.3; /* en afgedrukt, de functie printf bespreek ik later */ printf("%c %i %f",karakter,gehelewaarde,decimaalwaarde); } ------------------------------------------------------------------------------ H4 Parameters doorgeven ------------------------------------------------------------------------------ Om iets met onze variabelen te kunnen doen, moeten we ze kunnen doorgeven naar de diverse functies waarvan we gebruik willen maken. Dit hadden we al gedaan bij de functie printf. Deze functie heeft echter zo uitgebreide mogelijkheden dat ik het gebruik ervan in een apart hoofdstuk beschrijf. Om een waarde aan een functie mee te geven, moet deze eenvoudigweg tussen de haakjes van de functie aanroep worden gezet. Dit lijkt heel eenvoudig, maar het is mogelijk een functie te definiëren waaraan verschillende waarden kunnen worden meegegeven. Op dat moment is de volgorde van de mee te geven waarden belangrijk. Ook het type variabele is iets om rekening mee te houden. Een functie kan slechts een waarde terug geven. Wil je meer waarden terug sturen moet je gebruik maken van pointers waarover later meer. Nu eerst weer een kort programmaatje dat de werking van een functie laat zien /* H4 parameters.c */ #include main() { int a,b,c; a = 5; b = 3; c = a+b; printf("%i\n",c); } Er zijn drie variabelen gedefineerd, a,b en c; aan c wordt de som van de waarden in a en in b gegeven. c wordt vervolgens naar de functie printf gestuurd. Maar niet zomaar! Er wordt aan printf eerst verteld wat er afgedrukt moet worden en hoe. Dit wordt in het volgend hoofdstuk uit de doeken gedaan. ------------------------------------------------------------------------------ H5 De functie printf ------------------------------------------------------------------------------ De functie printf is wel ongeveer de meest gebruikte functie binnen de C bibliotheek. Dat komt omdat er erg veel mee kan, en er ook diverse afgeleiden van deze functie zijn. In het algemeen wordt de functie ongeveer als volgt gebruikt. printf("tekst formaat specificatie tekst \n",parameter); De tekst en formaatspecificatie (engels:format specifier) moeten tussen dubbele quotejes staan. Daarna volgt een lijst met af te drukken parameters. Over de format specifiers valt al een heel boekwerk te schrijven, maar dat valt buiten het bestek van dit verhaal. In het kort ziet het er zo uit: c% er komt op deze plaats een karakter te staan. %s er volgen nu meerdere karakters %i hier komt een integer waarde te staan %f hier wordt een float waarde geschreven. %x er volgt nu een integerwaarde, maar die wordt hexadecimaal weergegeven. Verder zijn er nog een paar speciale symbolen die in deze functie gebruikt kunnen worden. Een paar laat ik er nu zien: \b backspace \n begin op een nieuwe regel \r Terug naar het begin van een regel (In DOS en Windows programma's nodig) \l formfeed voor printers \\ druk een \af \" druk een " af hier volgen een paar voorbeelden van het aanroepen van printf: printf("Het aantal jaren is %i \n",jaren); printf("De naam is %s \n",naam); printf("%i + %i = %i\n",a,b,a+b); Meer erover kun je terug vinden met de opdracht man 3 printf ------------------------------------------------------------------------------ H6 Functies ------------------------------------------------------------------------------ De taal C heeft een uitgebreide bibliotheek met nuttige functies. Echter niet voor elk doel bestaat een functie. In C voeg je zelf functies toe, die je in je programma (en andere programma's) kunt gebruiken. In dit hoofdstuk vertel ik hoe dit gaat. Als eerste moet je een prototype van je functie maken. Dit is nodig voor de compiler, omdat er in het programma al naar de functie kan worden verwezen, die op dat moment nog niet bekend is. Je mag echter de volledige functie declaratie ook meteen aan het begin van het programma zetten. In dat geval is een apart prototype niet nodig. Een functie definitie ziet er ongeveer als volgt uit. int berekensom(int getal_a, int getal_b) { int som; som = getal_a + getal_b; return som; } De functie is van het type int, hetgeen aangeeft dat er na het aanroepen een integer waarde wordt teruggegeven. Deze functie krijgt twee integer waarden als parameter mee. In de functie worden deze opgeteld en in de variabele som gezet. Let wel, de variabele som is enkel binnen de functie berekensom te gebruiken. Waneer de functie verlaten wordt, is de variabele som niet meer bekend, en zal de inhoud verloren gaan. Daarom wordt de functie afgesloten met de opdracht: return som; waarmee de inhoud van som wordt teruggegeven aan het programmadeel dat de functie aanriep. De prototypedefinitie wanneer nodig zou er in dit geval als volgt uitzien: int berekensom(int getal_a, int getal_b); Deze beschrijft dus aleen maar het gebruik van de de functie en zorgt ervoor dat het bestaan ervan bekend is. Nu een voorbeeldprogramma, waarin een functie wordt gebruikt om van euro's naar guldens om te rekenen. /* H6 functies.c */ #include int rekenom(int nieuwgeld) { return nieuwgeld*2.2; } main() { int gulden, euro; euro = 25; gulden = rekenom(euro); printf("%i Euro, komt overeen met %i Gulden\n",euro,gulden); } ------------------------------------------------------------------------------ H7 Arrays en strings ------------------------------------------------------------------------------ Het komt nogal eens voor dat we een hele reeks van waarden willen onthouden, die bij elkaar horen. Stel je bijvoorbeeld eens het volgende voor: Ik heb elke dag 5 vaste uitgaven bij het uitvoeren van mijn werk. - De benzine voor mijn auto - parkeerkosten - luchkosten - koffieautomaat - kopieermachiene Deze kosten wil ik in een variabele onder brengen, zodat ik ze straks makkelijk kan optellen. Dat kunnen we doen door een array te definiëren. Een array is een variabele met meerdere elementen van het gedefineerde type. Elk element krijgt een indexnummer. We doen dat op de volgende manier: int kosten[5]; Ik heb nu de int variabele kosten gedefinieerd, en deze heeft 5 elementen. Let wel op! C begint altijd vanaf 0 te tellen. Dus de index loopt van 0 tot 4! Met het volgende programma laat ik zien hoe je de variabelen kunt gebruiken. /* H7 arrays.c */ #include main() { int totaal, kosten[5]; kosten[0]=25; kosten[1]=3; kosten[2]=8; kosten[3]=6; kosten[4]=2; totaal = kosten[0] + kosten[1] + kosten[2] + kosten[3] + kosten[4]; printf("De totale kosten zijn %i Euro",totaal); } De variabelen totaal en kosten worden gedefinieerd, de elementen van de variabele kosten worden geladen en vervolgens wordt de som ervan in totaal geladen en afgedrukt. In het volgend hooftstuk zullen we overigens zien hoe het optellen efficiënter kan. Een speciale variant van een array is de string. Dit is niet meer dan een array van het type char, en bedoeld om tekst in op te slaan. Dit gaat dus als volgt: char naam[10]; Het probleem is hierbij dat C niet ineens een aantal variabelen kan initialiseren. In ons voorbeeld zou je het nog door de compiler kunnen laten oplossen door de volgende definitie te maken: char naam[]="linux"; Aan een string variabele van het type char met nog onbekende lengte, wordt als inhoud Linux meegegeven. Waneer ik echter in mijn programma het volgende assignment doe: naam = "linux"; dan volgt er een foutmelding. C kan n.l. maar een element tegelijk toewijzen. Om toch wat tekst in de variabele te kunnen zetten, gebruiken we de functie strcpy die in de bibliotheek string.h zit. E.e.a wordt met het volgende programma geillustreerd. /* H7 strings.c */ #include #include main() { char naam[6]; strcpy(naam,"Linux"); printf("%s\n",naam); } Hier is trouwens nog iets bijzonders aan de hand. Linux is slechts 5 karakters, echter de variabele naam is 6 karakters groot. Dit komt omdat het einde van een string wordt aangegeven door een 0. Die 0 neemt ook plaats in, vandaar de we een string altijd minstens 1 element groter moeten maken dan het maximaal gewenste. Wanneer voor de variabele naam meer elementen worden gereserveerd, dan wordt de string niet groter, maar het geheugen blijft wel in gebruik. Bij grote databases iets om rekening mee te houden. Wil je weten welke functies er nog meer in string.h zitten, bestudeer dan de man string ------------------------------------------------------------------------------ H8 Loops ------------------------------------------------------------------------------ Regelmatig komt het voor dat dingen bij herhaling moeten worden uitgevoerd. Voor dit doel zijn er drie mogelijkheden: - een for-loop, - een while-loop, - een do while constructie; Elk met zijn eigenschappen, welke ik steeds aan de hand van een voorbeeldje zal demonstreren. De for loop, met een bekend programma uit het vorige hoofdstuk. /* H8 loops.c */ #include main() { int element, totaal, kosten[5]; totaal = 0; kosten[0]=25; kosten[1]=3; kosten[2]=8; kosten[3]=6; kosten[4]=2; for(element=0;element<5;element++) totaal += kosten[element]; printf("De totale kosten zijn %i euro\n",totaal); } In de for-loop wordt gebruik gemaakt van de variabele element; aan deze wordt om te beginnen de waarde 0 toegewezen. De loop blijft zich herhalen zolang de waarde in element kleiner dan 5 is en bij elke herhaling wordt element met 1 verhoogd. note: element++ is een verkorte schrijfwijze voor element=element+1; een zelfde verhaal geldt voor de regel erna, waarin de kosten worden opgeteld. Let op dat de for-loop niet wordt afgesloten met een puntkomma! Want het kommando eindigt pas na het uitvoeren van de optelling. Voor de do while constructie maak ik gebruik van een compound statement. Dit wil zeggen dat ik een aantal opdrachten tussen accolades zet, welke dan allen worden uitgevoerd totdat aan de condities voldaan is. /* H8 dowhile.c */ #include #include main() { char woord[10]; int i; strcpy(woord,"Linux"); i=0; do { printf("%c\n",woord[i]); i++; } while((i<10)&&woord[i]); } Aan de stringvariabele woord, wordt een tekst toegewezen. De variabele i wordt gebruikt om de elementen van woord aan te wijzen. Met do wordt aangegeven dat en nu een lusconstructie volgt die wordt afgesloten met een while (dit keer dus wel voorzien van puntkomma). De print opdracht zorgt ervoor dat elke letter op een nieuwe regel begint. Het programma schrijft dus de letters onder elkaar, ipv naast elkaar. i++; zorgt dat de elementaanwijzer wordt verhoogd. En we zien nu gelijk een toepassing van while die met for veel lastiger zou zijn. Er moet n.l. aan twee voorwaarden worden voldaan. - de variabele i moet kleiner zijn dan 10 - waneer het element in woord 0 is, dan is het einde van de string gevonden en moet er dus ook gestopt worden. Er geldt dus voor beide condities een AND, aangegeven met &&. Verder valt nog op te merken dat een vergelijking (woord[i]>0) wel goed is, maar het resultaat van de vergelijking is een 0 of een 1. En daar we zoeken naar een 0 is de huidige schrjfwijze dus voldoende. Dat spaart een vergelijking (rekentijd) uit. Nu geen probleem, maar wel wanneer we eens een grote file moeten inlezen. De while-loop is eigenlijk hetzelfde als de do while constructie. Het verschil is dat aan de conditie moet worden voldaan voordat het volgende (compound) statement gestart wordt. Ook deze loop demonstreer ik weer met een eenvoudig voorbeeld. /* H8 while.c */ #include main() { int A; A=0; while(A<5) { printf("%i\n",A); A++; } } Verkorte schrijfwijze: In C kunnen een aantal veel voorkomende opdrachten verkort worden geschreven. a=a+1; verkorte schrijfwijze a++; a=a+b; verkorte schrijfwijze a+=b; ------------------------------------------------------------------------------ H9 Conditionele opdrachten ------------------------------------------------------------------------------ Het komt in een programma erg vaak voor dat we een beslissing moeten nemen aan de hand van beschikbare gegevens. Dit kunnen voor elkaar krijgen door het gebruik van de if opdracht. Na de if opdracht volgt een vergelijking die de condities waaraan voldaan moet worden beschrijft. Hier een eenvoudig voorbeeld: #include main() { int leeftijd; leeftijd = 14; if(leeftijd<16) printf("Je bent te jong om alcohol te drinken !\n"); } Wanneer aan de vergelijking leeftijd is kleiner dan 16 wordt voldaan, dan wordt de printf opdracht die volgt uitgevoerd. In het andere geval wordt deze opdracht overgeslagen. Nu kunnen we deze beslissing uitbereiden door in het geval dat er niet aan de conditie wordt voldaan een andere opdracht uit te laten voeren. Dit kan met de if else variant. Een uitgebreide versie van hetzelfde programma staat hier: /* H9 leeftijd.c */ #include main() { int leeftijd; leeftijd = 18; if(leeftijd>=16) printf("Geniet maar drink met mate !\n"); else printf("Je bent te jong om alcohol te drinken !\n"); } Het is mogelijk een vergelijking te maken waarmee meerdere condities, waaraan moet worden voldaan, worden beschreven. Dit kan worden geregeld door tussen de haken een aantal vergelijkingen te zetten gescheiden door een && voor een EN-conditie of een || voor een OF- conditie. Hier een voorbeeld ter verduidelijking : /* H9 schoenmaat.c */ #include main() { int schoenmaat; schoenmaat = 42; if((schoenmaat>31) && (schoenmaat<47)) printf("Waarmee kan ik U van dienst zijn?\n"); else printf("Het spijt me, wij hebben geen schoenen in uw maat\n"); } De if instructie kijkt eigenlijk of een vergelijking NIET 0 is. 0 wordt gezien als een FALSE conditie. De volgende constructie kan daarom probleemloos worden toegepast: /* H9 teller.c */ #include main() { int teller,totaal; teller = 0; totaal = 100; while(totaal) { if(teller) teller--; else teller = 9; printf("%i\n",teller); totaal--; } } Hieronder volgen een aantal vergelijkingen a>b waar als a groter dan b is a=b waar als a groter of gelijk aan b is a==b waar als a gelijk aan b is LET OP ! a=b is geen vergelijking, maar een assignment. Het resultaat zal dus heel anders worden dan verwacht. Regelmatig komt het voor dat we een variabele hebben waarvan afhankelijk de waarde een opdracht uitgevoerd moet worden. Dit is bijvoorbeeld het geval in een menu, waar aan de hand van een ingegeven nummer een functie aan geroepen moet worden. Voor dit doel kent C het switch statement. Hier onder een voorbeeld /* H9 switch.c */ #include int telop(int a,int b) { return a+b; } int trekaf(int a,int b) { return a-b; } int vermenigvuldig(int a,int b) { return a*b; } int deel(int a, int b) { if(b!=0) return a/b; else { printf("Fout, deling door 0\n"); return 0; } } main() { int A,B,R,bewerking; printf("De waarde voor A = "); scanf("%i",&A); printf("De waarde voor B = "); scanf("%i",&B); printf("geef nummer voor de bewerking\n"); printf("1 = optellen\n2 = aftrekken\n,3 = vermenigvuldiger\n4 = delen\n"); scanf("%i",&bewerking); switch(bewerking) { case 1: R = telop(A,B); break; case 2: R = trekaf(A,B); break; case 3: R = vermenigvuldig(A,B); break; case 4: R = dell(A,B); break; default: printf("Ongeldige bewerking \n"); break; } printf("Resultaat = %i\n",R); } Nadat de gegevens ingelezen zijn, komen we bij de switch opdracht. Deze analyseerd de waarde in de variabele bewerking elke gewenste mogelijkheid wordt met een case opdracht bekeken Komt de waarde bij de case overeen dan worden de opdrachten achter case uitgevoerd totdat er met een break uit de switch functie wordt gesprongen De opdracht achter default is optioneel. Deze wordt uitgevoerd waneer aan geen van de bovenstaande case's voldaan wordt. Let op, Als je de break; opdracht vergeet, worden de volgende functies ook aangeroepen. Accoulades In C zie je regelmatig een aantal opdrachten tussen accoulades staan. Hiermee word aangegeven dat de instructies ertussen allen behoren bij de functie die er boven staat, b.v. de if opdracht. We noemen deze constructie een "compound statement". Waneer er slechts 1 instructie volgt, dan mogen de accoulades weggelaten worden. zie volgende voorbeeld if(a>10) a=0; doet het zelfde als if(a>10) { a=0; } Echter bij de volgende constructie gaat het fout if(a>10) a=0; b++; De variabele b wordt n.l. altijd verhoogd, ongeacht de waarde van a de juiste wijze is dus if(a>10) { a=0; b++; } ------------------------------------------------------------------------------ H10 Pointers ------------------------------------------------------------------------------ C is een taal waarin erg veel met pointers gewerkt wordt. Daarvoor zijn verschillende redenen. Een van de belangrijkste daarvan is het efficient met geheugen omgaan. Waneer b.v. een heel blok tekst moet worden verplaatst omdat dit naar het scherm moet worden afgedrukt, dan staat deze tekst twee maal in het geheugen. Niet alleen is dat verspilling van geheugenruimte, maar ook het kopiëren kost tijd. Veel slimmer is het om de printf functie enkel te vertellen waar de tekst staat. Dit is eigenlijk al iets dat we gedaan hebben in het hello world programma, al is het daar niet echt duidelijk te zien. Dit aanwijzen van een variabele doen we met pointers. Deze declareren we door bij de declaratie een asterisk voor de naam te zetten. Bijvoorbeeld op de volgende manier char *tekstpointer; int *getalpointer; In deze variabele, zetten wij dus niet de waarde die wij willen onthouden, maar het geheugenadres ervan. Waneer wij naast de bovenstaande declaratie's nog eens de volgende twee hebben char tekst[10]; int getal; Dan kunen wij de pointer van een waarde voorzien op de volgende manier. tekstpointer = &tekst; getalpointer = &getal; Met &tekst wordt dus het aderes van de variabele tekst bedoeld. Met *tekstpointer bedoelen we de inhoud van de variabele waar de pointer naar verwijst. Werken met pointers is een belangrijke reden waarom in C snelle en efficiente code mogelijk is. We hoeven n.l. niet steeds een geheugenblok naar een functie te sturen. enkel het adderes ervan is voldoende. Ook kan een functie in C niet meer dan een waarde terug leveren. Maar door nu middels een pointer het aderes van een geheugenblok terug te geven,Kan dus ook de inhoud van b.v. een string doorgegeven worden. Een bekend voorbeeld daarvan is de functie scanf(), zie volgende hoofdstuk. Ook waneer we later met dynamisch gereserveerd geheugen gaan werken, is het gebruik van pointers onontbeerlijk. ------------------------------------------------------------------------------ H11 Data inlezen met scanf ------------------------------------------------------------------------------ In het vorige hoofdstuk hebben we gezien hoe je met pointers en adressen naar data kunt verwijzen zonder deze steeds weer te moeten verplaatsen. Een practische toepassing hiervan is het gebruik van de functie scanf, waarmee gegeven via het toesenbord kunnen worden ingevoerd. We beginnen gelijk met een voorbeeld programma: /* H10 inlezen.c */ #include main() { char mijnnaam[20]; printf("Geef je naam >"); scanf("%s",&mijnnaam); printf("De ingelezen naam is %s\n",mijnnaam); } Om in het programma de variabele mijnnaam in te vullen moeten we meerdere elementen adresseren. Een constructie als mijnnaam = scanf(); is daarom niet mogelijk. Want een functie kan slechts een waarde retourneren. Daarom wordt aan de functie scanf het adres van de variabele mijnnaam gegeven. Doordat de functie scanf intern gewoon met het adres van de variabele werkt, kan deze de ingelezen toetsaanslagen op het adres van de variabele schrijven, waardoor bij het beëindigen van de functie de variabele mijn naam keurig is voorzien van de ingetypte tekst. Net als de functie printf biedt de functie scanf vele mogelijkheden. Daarom moet deze ook worden voorzien van format specifiers. Wil je bijvoorbeeld een geheel getal in lezen, dan ziet de functie aanroep er als volgt uit: scanf("%i",&getal); scanf kan dus ook meerdere gegevens ineens inlezen. Het practische nut daarvan wordt bewezen wanneer de functie wordt gebruikt om informatie uit bestanden te lezen. Overigens is er een eenvoudigere methode om slechts een karakter te lezen. Deze maakt gebruik van de functie getc(). Dit gaat als volgt: char toets; toets = getc(); Echter om efficiënt het toetsenbord op enkele toetsaanslagen te scannen, is het gebruik van de ncurses bibliotheek een veel betere oplossing. Het gebruik hiervan wordt later besproken. ------------------------------------------------------------------------------ H12 Structures ------------------------------------------------------------------------------ We hebben gezien dat we met een array meerdere elementen onder een enkele variabelenaam kunnen onderbrengen. Deze elementen moeten echter wel allemaal van het zelfde type zijn. Nu kan het voorkomen dat we met een variabele met meerdere elementen willen werken, waarvan de elementen van een afwijkend type zijn. Deze situatie doet zich bijvoorbeeld voor bij een adressenbestand. Om dit probleem aan te pakken, kunnen we gebruik maken van een nieuw type dat we zelf bedenken. We noemen dit een structure. In het volgende voorbeeldprogramma wordt zo'n structure benut om wat persoonsgegevens te onthouden. /* H12_aderes.c */ #include #include #define maxaantal 10 main() { struct ADERES { char voornaam[16]; char achternaam[16]; char telefoonnr[12]; } aderes[maxaantal]; int teller,aantal; printf("Hoeveel aderessen ?"); do { scanf("%i",&aantal); if(aantal>maxaantal) printf("Zoveel geheugen is niet beschikbaar,\nprobeer opniew\n"); } while(aantal>maxaantal); teller = 0; do { printf("\nvoornaam = "); scanf("%s",aderes[teller]->voornaam); printf("achternaam = "); scanf("%s",aderes[teller]->achternaam); printf("telefoonnummer = "); scanf("%s",aderes[teller]->telefoonnr); teller++; } while(tellervoornaam, aderes[teller]->achternaam, aderes[teller]->telefoonnr); } ------------------------------------------------------------------------------ H13 Dynamisch geheugen alloceren ------------------------------------------------------------------------------ Tijdens het schrijven van een programma is het niet altijd bekend hoeveel geheugen er voor een bepaald doel igereserveerd moet worden. Bovendien is het niet altijd noodzakelijk dit geheugen gedurende de gehele looptijd van het programma bezet te houden. Daarom is het mogelijk om tijdens runtime geheugen te reserveren en weer vrij ete geven. Het begin ervan wordt dan aangegeven door een pointer, die tijdens het reserveren van het geheugen van een begin addres wordt voorzien Hier een eenvoudig voorbeeldje hoe dat gaat /* H13 maloc.c */ #define buffergrote 1000 #include #include main() { char *buffer; int teller; /* reserveer het geheugen */ buffer = malloc(buffergrote); /* controleer of het gelukt is */ if(buffer!=NULL) { printf("tiep een tekst in\n"); /* daar buffer al een pointer is, hoeft er geen & voor te staan */ scanf("%s",buffer); for(teller=0;teller<10;teller++) printf("%s\n",buffer); /* geeft de ruimte weer vrij */ free(buffer); } else printf("Fout bij het reserveren van het geheugen\n"); } Het is ook mogelijk geheugen voor complexere elementen zoal een structure te reserveren. Daarvoor moet dan wel bekend zijn hoe groot die structure is. Voor dit doel kent C de functie sizeoff() Als voorbeeld gebruik ik het aderessen bestandje van H12 /* H13 aderes.c */ #include #include #define maxaantal 10 main() { struct ADERES { char voornaam[16]; char achternaam[16]; char telefoonnr[12]; } *aderes; int teller,aantal; /* reserveer geheugenplaatsen * grote van de structure*/ aderes = malloc(maxaantal*sizeoff(ADERES)); if(aderes!=NULL) { printf("Hoeveel aderessen ?"); do { scanf("%i",&aantal); if(aantal>maxaantal) printf("Zoveel geheugen is niet gereserveerd,\nprobeer opniew\n"); } while(aantal>maxaantal); teller = 0; do { printf("\nvoornaam = "); scanf("%s",aderes[teller]->voornaam); printf("achternaam = "); scanf("%s",aderes[teller]->achternaam); printf("telefoonnummer = "); scanf("%s",aderes[teller]->telefoonnr); teller++; } while(tellervoornaam, aderes[teller]->achternaam, aderes[teller]->telefoonnr); /* Geef het geheugen weer vrij */ free(aderes); } else printf("Fout bij het reserveren van het geheugen\n"); } Waneer het geheugen niet meer wordt vrij gegeven, Dan gebeurd dat welliswaar automatisch naar het beeindigen van het programma, Maar het is slordig programeren en kan tot problemen leiden. Soms kan het programma plots stoppen en verschijnt de mededeling Segmentation fault. Dit duid erop dat geprobeerd wordt om geheugenplaatsen te lezen of te schrijven die niet door het programma gereserveerd zijn. Er is dus geen of niet genoeg geheugen gereserveerd, of het is alweer vrij gegeven. Let dus goed op wat je doet. ------------------------------------------------------------------------------ H14 Bestanden openen, lezen en schrijven ------------------------------------------------------------------------------ Wanneer we in een programma met bestanden willen werken, biedt de taal C daarvoor twee manieren om het probleem aan te pakken, n.l. door middel van streams of door middel van files. Op zich verschillen beiden methoden niet zoveel van elkaar. Streams zijn meer geschikt voor het werken met tekstbestanden, terwijl file-I/O meer geschikt is voor de overdracht van binaire data. Ook voor het besturen van devices verdient de werkwijze met files de voorkeur. Van beide geef ik nu een voorbeeld. Een programma dat middels een stream een bestand creëert, en een programma dat middels file-I/O het bestand weer leest. Hier volgt het eerste programma: /* H14 stream.c */ #include #define filename "bestand.txt" #define buffersize 1024 main() { FILE bestand; invoerbestand = fopen(filename,"a"); if(invoerbestand != NULL) { fprintf(bestand,"Dit is de inhoud van het bestand\n"); fclose(invoerbestand); printf("Het bestand %s is aangemaakt\n", invoerbestand); } else printf("Het bestand %s kon niet worden gecreëerd\n", invoerbestand); } Er wordt een stream "bestand" gedefineerd met het type FILE, en deze wordt vervolgens met de functie fopen gecreëerd. De functie fopen staat uitvoerig gedocumenteerd in man fopen. Wanneer het openen mislukt, dan heeft "bestand" de waarde NULL. Wanneer het wel lukt, kun je met de functie fprintf de stream schrijven. De functie fprintf werkt hetzelfde als de al bekende functie printf. Voor het lezen van de stream gebruiken we de functie fscanf. Om bij het lezen te kijken of er nog data staat te wachten kunnen we de functie feof(bestand) gebruiken. EOF staat voor End Of File, deze functie geeft TRUE terug als bij het lezen van een stream het einde is bereikt. Er zijn nog een paar functies beschikbaar voor stream I/O, deze kun je terug vinden in de man stdio. Voor het inlezen en schrijven van binaire bestanden is het vaak prettiger om met files te werken. De werkwijze verschilt niet echt bijzonder veel. Hier een voorbeeld van een kopieerprogramma: /* H14 copy.c */ #include #include #define infilename "bestand.txt" #define outfilename "bestand.cpy" #define buffergrote 1024 main() { int infile,outfile,aantalgelezenbytes,totaalaantalbytes; char buffer[buffergrote]; totaalaantalbytes = 0; infile = open(infilename,O_RDONLY); if(infile>=0) { outfile = open(outfilename,O_CREAT); if(outfile>=0) { do { aantalbytes = read(buffer,infile,buffergrote); write(buffer,outfile,aantalbytes); totaalaantalbytes+=aantalbytes; } while(aantalbytes); close(outfile); printf("%u bytes gecopieëerd naar %s\n", totaalaantalbytes,outfilename); } else printf("Het bestand %s kon niet worden aangemaakt\n",outfilename); close(infile); } else printf("Het bestand %s kon niet worden geopend\n",infilename); } ------------------------------------------------------------------------------ H15 Commandline parameters ------------------------------------------------------------------------------ Vaak willen we bij het opstarten van een programma er een of meer parameters aan megeven. Bijvoorbeeld bij het copy programma van Hoofdstuk 14. Het zou daar wel makkelijk zijn waneer je het de volgende odracht kon intikken, copy mijnfile.c /floppy/mijnfile.c C bied hiervoor de mogelijkheid met de variabelen argc en argv argc is hierbij het aantal parameters dat meegegeven is, argv is een array van strings waarin de parameters staan Het volgende programmatje laat zien hoe het werkt. /* H15 parameter.c */ #include main(int argc, char * argv[]) { int c; for(c=0;c #include #include #define mijnfile "~/.profile" static int stopvlag; void handler() { stopvlag=1; printf("\nSIGINT ontvangen\n"); } main() { int mijnfile,a; stopvlag = 0; mijnfile = open(filename,O_RDONLY); if(mijnfile>0) { signal(SIGINT,handler); do { /* een nutteloos stukje code mar het gaat om het idee */ a++; if(a>100) a=0; } while(!stopvlag); close(mijnfile); printf("\nHet programma is nu netjes afgesloten\n"); } else printf("Fout, %s kon niet worden geopend"); } Meer over het gebruik van signal kun je vinden met de opdracht man signal ------------------------------------------------------------------------------ H17 Devices en Lockfiles ------------------------------------------------------------------------------ Lockfiles worden bij UNIX systemen meestal gebruikt om aan te geven of een device in gebruik is. Het is een meestal leeg bestand, of een bestand met enkel het PID nummer erin. Waneer je bijvoorbeeld een verbinding opzet via een telefoonlijn, Dan is het zaak dat niemand anders dit gelijktijdig ook gaat proberen. Door nu gewoon een bestandje met de naam van het device te maken, wordt aangegeven dat het device al door iemand gebruikt wordt. Het is echter niet meer dan een waarschuwwing. De programeur bepaald uiteindelijk of hij het device met rust laat of niet. Waneer je dus van het modem gebruik wilt maken, dan is de juiste procedure Eerst kijken of er een lockfile voor het modem bestaat Bestaat er al een lockfile, geef een waarschuwing en verlaat het programma Zo niet, creeer er een, en dan pas open je het device modem. Waneer je klaar bent, eerst modem afsluiten. Dan pas lockfile weghalen. Omdat UNIX een multiuser systeem is, is het zaak ervoor te zorgen dat er niet tegelijkertijd twee lockfile's kunnen worden aangemaakt. Hier een voorbeeldje hoe je zo iets aanpakt. /* H17 lockfile.c */ #define devicename "/dev/modem" #define lockfilename"/var/lock/LCK..modem" #define telefoonnr "ATDT 123456789" main() { int modem,lockfile; /* probeer een lockfile te creeeren, */ lockfile = open(lockfilename,O_WRONLY|O_CREAT|O_EXCL,0644); /* als deze niet bestaat en het creeeren lukt ga je verder */ if(lockfile>=0) { /* je kunt er nu de PID in zetten, maar echt nodig is dat niet */ close(lockfile); modem = open(devicename,O_RDWR); if(modem>=0) { write(modem,telefoonnr,strlen(telefoonnr)); close(modem); } else printf("Fout, %s kom niet worden geopend\n"); /* niet vergeten de lockfile weer weg te halen ! */ remove(lockfilename); } else { /* Het aanmaken van de lock file lukte niet, es kijken waarom */ if(errno==EEXIST) printf("Fout, het device %s is gelock door\n", devicename,lockfilename); else /* waarschijnlijk niet de benodigde rechten */ printf("Fout, de lockfile kon niet worden aangemaakt\n"); } } Opmerking: Lockfile's voor devices zijn meestal in de directory /var/lock te vinden. Zij beginnen meestal met LCK.. gevolgd door de naam van het device. in ons geval dus LCK..modem Houdend aan deze standaard werkt het systeem van lockfile's redelijk betrouwbaar. Echter lockfiles zijn er enkel als een advisory. Waneer de programeur zich er niets van aantrekt, heeft het aanmaken ervan weinig zin meer. LET OP ! Om dit programma uit te kunnen voeren, heb je wel rechten nodig om het modem aan te spreken en om in de dir /var/lock te mogen schrijven. De beste manier om dat te regelen is via de /etc/group file. Dit valt echter buiten het bestak van dit boekje. ------------------------------------------------------------------------------ H18 Include files ------------------------------------------------------------------------------ In de voorbeeld programma's heb ik steeds gebruik moeten maken van de opdracht #include Deze opdracht voegt op de plaats waar deze staat een stukje code toe aan het programma. Deze includefile's staan voornamelijk in de directory /usr/include. In de include files staan enkel prototype's van de functies, niet de code van de functies zelf. ook staan er veel defines en macro's in. Van de meest bekende includefile's is een man voorhanden. Zo kun je er al e.e.a. over te weten komen door bijvoorbeeld een van de volgende manuals op te vragen : stdio, fcntl, printf, string, malloc, signal, errno Voor uitgebreide documentatie over al deze include file's is internet een prima informatie bron. Het is uiteraard ook mogelijk zelf include bestanden bestanden te maken. Om aan te geven dat deze niet in /usr/include maar in de huidige directory staan, worden deze op de volgende manier aangegeven #include "mijnfile.h" Meer daarover in het hoofdstuk over object bestanden. ------------------------------------------------------------------------------ H19 Object bestanden en Makefile's ------------------------------------------------------------------------------ Waneer het programma groter wordt, is het handig om dit op te delen in diverse bestanden. Dit levert een aantal voordelen op: - Alle deel programma's worden kleiner dus overzichtelijker. - Deel programma's kunnen ook in andere programma's worden gebruikt, zodat je niet steeds het zelfde hoeft te ontwikkelen. - Waneer je een wijziging in een deelprogramma maakt, hoef je niet het hele programma te compileren maar slechts het gedeelte dat veranderd is en de delen die daardoor beinvloed worden. Het volgende (een beetje onzinnige) programma bestaande uit drie bestandjes laat zien hoe je zoiets kunt opbouwen. /* Hxx mijnfunctie.c */ #include toontijd(int hr, int min) { printf("%2i:%2i\n",hr,min); } /* Hxx mijnfunctie.h */ toontijd(int hr, int min); /* Hxx mijnprog.c */ #includeo #include "mijnfunctie.h" main() { int uur,minuut; printf("geef uur : "); scanf("%i",&uur); printf("geef minuten : "); scanf("%i",&minuten); toontijd(uur,minuten); } Om het te compileren, moet je eerst mijnfunctie.c compileren op de volgende manier cc mijnfunctie.c -c De parameter -c betekend dat er geen functie main is gedefineerd, en er een object bestand wordt gemaakt. Deze hebben de extensie .o Vervolgens kunnen we de rest compileren op de volgende manier cc mijnprog.c mijnfunctie.o -o mijnprog Nu wordt mijnprog gecompileerd en mijnfunctie.o wordt daarna meegelinkt zodat alle functie's erin beschikbaar zijn. Omdat mijnprog niet weet welke functie's er in mijnfunctie.c zitten, heb ik mijnfunctie.h 'geinclude' In mijnfunctie.h staat enkel het prototype van de beschikbare functies vermeld. Mijnprog weet nu dat er straks tijdens het linken, de functie toontijd beschikbaar is. Wordt er nu wat in mijnprog.c veranderd, dan heeft dat geen invloed op mijnfuncite.c Dus hoeft enkel mijnprog.c gecompileerd te worden. Wordt mijnfunctie gewijzigd, dan zal ook mijnprog uiteindelijk beinvloed worden. Dus moeten in dit geval beiden worden aangepast. Met meerdere bestanden is dat lastig bij te houden, Daarom is er voor dit doel het hulp programma make bedacht. Make gaat op zoek naar een bestandje Makefile, waarin beschreven wordt op welke manier alle source bestandjes van elkaar afhankelijk zijn, en wat te doen om ze te compileren. Het Makefile bestandje voor ons voorbeeld ziet er als volgt uit mijnprog: mijnprog.c mijnfunctie.o cc mijnprog.c mijnfunctie.o -o mijnprog mijnfunctie.o: mijnfunctie.c mijnfunctie.h cc mijnfunctie.c -c to be continioud ---------------------------------------------------------------------------- H20 Processen forken ------------------------------------------------------------------------------ Linux is een multitaskting Operating System. Dit wil zeggen dat er gelijktijdig meerdere programma's kunnen draaien. Maar het kan ook zijn dat een programma meerdere keren moet draaien. Dit is bijvoorbeeld het geval bij een netwerk server, waar meerdere aanvragen gelijktijdig afgehandeld moeten kunnen worden, of een webbrowser die meerdere websites gelijktijdig moet kunnen tonen. Maar ook kan het zijn dat een programma uit twee onafhankelijke taken moeten bestaan. Bijvoorbeel een communicatie programma waarbij een aparte code bestaat voor inkomende en uitgaande berichten. Voor deze nogal ingewikkelde taak zijn er in Linux een aantal functies aanwezig die deze lastige klus van je overnemen. Een eenvoudig voorbeeld volgt hieronder /* H20 Fork.c */ #include main() { int pid,a; printf("Fork programma word gestart\n"); pid=fork(); if(pid) { for(a=1;a<=10;a++) { printf("Child process %i\n",a); sleep(1); } } else { for(a=10;a>0;a--) { printf("Mother process %i\n",a); sleep(1); } } } Wat Gebeurd hier nu eigenlijk ? Met de opdracht fork() word een tweede process opgestart dat gebruik maakt van de zelfde code en waarden van variabelen. Lees het goed ! De begin waarden in de variabelen zijn bij beide processen zijn hetzelfde, immers de variabelen zijn copien van elkaar Maar zodra een van de processen een variabele wijzigd, dan zal de tegenhanger in het andere process niet mee veranderen. Dit verschijnsel is zowel bij de variabele pid als bij a goed te zien. De variabele pid van het aanroepende process (mother genoemd) wordt door de funcite fork op 0 gezet. voor het child process, wordt aan deze waarde het ProcesID nummer toegekent. De vergelijking if(pid) beslist dus of de code die volgt voor het nieuwe (child) process is, of voor het aanroepende (mother) process. Ook zul je zien dan voor beide processen de variabele a een andere waarde heeft. Ik heb dit proberen te illustreren door in het child process de waarde te laten oplopen van 1 tot 10 terwijl in het child process de zelfde variabele een aflopende waarde heeft van 10 tot 1 waneer je tijden het uitvoeren van dit programma de UNIX opdracht ps uitvoerd, dan zul je zien dat het programma fork twee maal in de lijst staat. Het process loopt immers twee maal. Rest nog te vermelden dat net als de variabelen bij het forken een zelfde begin waarde hebben, en dit tot gevolg heeft dat ook al geopende files in beide processen bekend zijn. Houd hier terdege rekening mee, het kan tot nogal verwarrende resultaten leiden ! VFORK Vroeger toen geheugen nog een kostbaar goed was, maar operating systemen nog niet zo geavanceerd, bestond er naast fork een functie virtual fork. Het verschil met fork was, dat bij fork het process en de hele programmacode werd gecopieerd. de functie Virtual_Fork zorgde ervoor dat enkel het process maar niet de programmacode werd gecopieerd. Daar programmacode eenmaal opgestart niet kan en mag wijzigen, (dit is ivm met securety onmogelijk gemaakt dmv memory management) heeft het nooit zin om de code te copieren. In de meeste UNIX systemen en dus ook in Linux verwijzen fork en vfork daarom de zelfde functie. THREADS Naast multi user is Linux ook nog eens multithreading. Threads lijken wel veel op het forken van een process, echter er zijn nogal wat wezenlijke verschillen. Het nut en de problemen welke met multithreading gepaard gaan, vallen echter ruim buiten het bestek van een beginners cursus. ------------------------------------------------------------------------------ H21 Fraaiere programma's met ncurses ------------------------------------------------------------------------------ Linux is een op UNIX gebaseerd besturingssysteem. Zo'n systeem bestaat uit een rekeneenheid, de host genaamd, en een losse terminal. Vroeger zagen die machienes er fysiek ook werkelijk zo uit. In onze PC's zit de terminal console welliswaar in de zelfde kast ingebouwd, maar softwarematig maakt Linux geen onderscheid tussen het beeldscherm en toetsenbord van de PC en en tussen een losse terminal die via een netwerk of serieele kabel is aangesloten. Er waren vroeger uiteraard verschillende fabrikanten van terminals die allemaal hun eigen stuurcodes voor het verplaatsen van de cursor e.d. hanteerden. Dat leverde nogal eens wat problemen op waneer een terminal van een ander merk werd aangesloten. De programma's wilden dan niet meer correct reageren op de stuurcodes. Om dit probleem te ondervangen heeft met toen een universele applicatie interface bedacht. Deze probeerd van zoveel mogelijk eigenschappen van de terminal gebruik te maken zonder dat daarvoor het programma aangepast hoeft te worden. Om dit te realiseren, moet de scherm invoer en uitvoer wel via deze tussenstap uitgevoerd worden. een eenvoudig voorbeeld hiervan volgt hieronder. ------------------------------------------------------------------------------ H22 Grafische programma's met X ------------------------------------------------------------------------------ Voor het maken van grafische applicaties zijn er voor Linux diverse prachtige bibliotheken beschikbaar. Deze maken gebruik van de API van de X-server die het grafische beeld produceerd. ------------------------------------------------------------------------------ H23 Communicatie over een netwerk ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ H24 Het verloop van een programma en het afhandelen van fouten. ------------------------------------------------------------------------------ Een werkend programma schrijven is een ding. Het programma onder alle omstandigheden een voorspelbaar en dus gewenst resultaat te laten produceren is een ander verhaal. Naast het uitvoeren van de werkelijke opdracht, moet er dus ook gekeken worden wat er gebeurd waneer de opdracht niet naar behoren kan worden uitgevoerd. Het achterwege laten hiervan leid regelmatig tot de beruchte segment errors (de bekende blauwe schermen in MicroSoft Windows applicaties) Bij Linux leverd dat welliswaar geen verdere gevolgen op, maar de applicatie kan bij een foutsituatie bijvoorbeeld tot data verlies leiden. een voorbeeld hiervan is het volgende fictieve script dat eigenlijk een mv opdracht voorsteld. #!/bin/sh # Verplaats source=$1 destination=$2 cp $source $destination rm $source Het programma, kopieerd het bestandje van source naar destination en wist het dan van de source locatie. Echter waneer het kopieren mis gaat omdat er op destination niet geschreven kan worden. dan wordt het bron bestand toch gewist. dit leid dus tot data verlies. Om deze problemen te ondervangen is het dus zaak om goed op te letten dat bestanden die geopend zijn, ook weer netjes gesloten worden, en dat gereserveerd geheugen netjes wordt vrijgegeven, en dat processen die gestart zijn weer worden afgesloten. De slimste oplossing om dit te doen is er voor te zorgen dat bij het aanroepen van elke functie ook gecontroleerd wordt of de aanroep een sucses was. waneer dit niet het geval is, zorg er dan voor dat het programma de eropvolgende code overslaat totdat de code voor het beeindigen van het programma bereikt wordt. Er zijn nog wel meer technieken om dit voor elkaar te krijgen. Met name waneer programma's ingewikkelder worden bijvoorbeeld doordat zij eventdriven zijn wordt dit probleem steeds ingewikkelder. Het is daarom goed om in een begin stadium al gelijk aan te wennen met het voorkomen van fouten rekening te houden Als voorbeeld nemen we hierbij het kopieer programma van hoofdstuk 4 en passen we het aan zodat het de bestanden verplaatst. Hierbij proberen we met alle mogelijke fout situaties rekening te houden. Dit kun je ongeveer als volgt doen. Begin Programma Indien twee commandline parameters zijn gegeven { | indien bronbestand openen sucsesvol | { | | indien doelbestand openen sucsesvol | | { | | | indien lezen en schrijven NIET sucsesvol | | | { | | | | foutmelding | | | | sla de kopieer opdracht op en stop zodoende het programma | | | } | | | indien doelbestand afsluiten mislukt | | | { | | | | foutmelding | | | | bronbestand laten staan | | | } | | } | | indien doelbestand openen mislukt | | { | | | foutmelding | | | vervolg met afsluiten programma | | } | | | } indien bronbestand sluiten NIET sucsesvol | { | | foutmelding | | vervolg afsluiten programma | } | | Indien er tot nog toe geen fouten zijn opgetreden | { | | indien bronbestandverwijderern FOUT | | { | | | foutmelding | | | vervolg afsluiten programma | | } } Indien er NIET twee commandline parameters zijn gegeven { laat gebruiksaanwijzing zien } programma is sucsesvol afgesloten In de praktijk ziet het geheel er dan als volgt uit /* H22 verplaats.c */ #include #include #include #define buffergrote 1024 int main(int argc, char *argv[]) { int infile,outfile,aantalgelezenbytes,totaalaantalbytes; int foutcode=0; char buffer[buffergrote]; char infilename[32],outfilename[32]; /* kijk of er wel twee parameters (source en destination) zijn opgegeven daar de progamma opdracht regel ook word meegeteld moeten we dus kijken of argc de waarde 3 heeft */ if(argv!3) { /* we hebben twee parameters, die gebruiken we om de bestandnamen te bepalen */ strcpy(infilename,argv[1]); strcpy(outfilename,argv[2]); totaalaantalbytes = 0; infile = open(infilename,O_RDONLY); /* als we de het invoer bestand niet kunnen openen, hoeven we niet verder te gaan. Maar we sluiten we het programma wel netjes af met een foutmelding */ if(infile>=0) { outfile = open(outfilename,O_CREAT); /* aleen waneer we het uitvoer bestand kunnen openen, hoeven we verder te gaan. lukt dit niet, moeten we niet zonder mee uit het programma springen ! er staat nog immers een file open, en we willen ook een duidelijke foutmelding */ if(outfile>=0) { do { aantalbytes = read(buffer,infile,buffergrote); /* ook tijdens het schrijven naar een filedescriptor kan wel eens iets misgaan. we volstaan in dit geval met een foutmelding. doordat we het aantal gelezen bytes op 0 zetten, word in dit geval het programma op een nette manier beeindigd. dwz, naast de foutmelding worden zowel invoer als uitvoer bestand afgesloten */ if(write(buffer,outfile,aantalbytes)<0) { aantalbytes = 0; printf("Error 6, fout tijdens het schrijven naar %s\n",outfilename); foutcode=6; } totaalaantalbytes+=aantalbytes; } while(aantalbytes); /* Hoewel zeer zeldzaam, kan er iets mis gaan bij het afsluiten van een bestand In dit geval kunnen we niets anders doen dan een foutmelding te geven en het programma te vervolgen met het afsluiten van het invoerbestand. */ if(close(outfile)<0) { printf("Error 5, fout bij het afsluiten van %s\n",outfilename); foutcode=5; } } else { printf("Error 4, Het bestand %s kon niet worden aangemaakt\n",outfilename); foutcode=4; } close(infile); } else { printf("Error 3, het bestand %s kon niet worden geopend\n",infilename); foutcode=3; } /* Aleen als er tot nog toe geen foutcode is opgetreden, dan is het copieren goed gelukt en mag het bron bestand gewist worden */ */ if(!foutcode) { if(remove(infilename)>0) printf("%u bytes verplaats van %s naar %s\n", totaalaantalbytes,infilename,outfilename); else { printf("Error 2 %u bytes gecopieerd van %s naar %s\n" "maar het bron bestand %s kon niet worden gewist\n", totaalaantalbytes,infilename,outfilename,infilename) foutcode=2 } } else { printf("Error 1 fout in opdracht regel,\n" "Het juiste formaat is : verplaats bronbestand doelbestand\n"); foutcode=1 } /* In elk goed geschreven C programma wordt de funtie main gedeclareerd als een int Deze waarde kan in shell scripts worden beoordeeld zodat het aanroepende script er corrigerende actie op kan nemen. */ return foutcode; } ------------------------------------------------------------------------------ H25 Geschikte Documentatie en hulpmiddelen voor het schrijven van C programma's in Linux ------------------------------------------------------------------------------ De taal C is zo wijd verbreid dat het vinden van geschikte literatuur geen enkel probleem zal lijken te zijn. Echter in een overvloed aan informatie is het wel prettig enige houvast te hebben. Waneer je gewend bent aan een MS-DOS of MS-Windows platform dan zul je zien en er wat verschillen zijn in de gebruikte include files. Zo had de prachtige Borland C compiler vroeger een erg handige includefile CONIO.H die funties bevatte waarmee op een gemakkelijke manier scherm I/O mogelijk werd. Helaas waren op deze manier geschreven programma's niet zonder meer porteerbaar naar andere (UNIX) systemen. Hoewel ik wel eens geprobeerd heb een soortgelijke lib voor Linux te maken blijkt deze werkwijze toch ongeschikt voor dit systeem. Het is dus wel zo handig om documentatie te gebruiken welke zich op UNIX platformen richt. Gelukkig is er al erg veel online documentatie op je Linux systeem te vinden. Geef bijvoorbeeld maar een op je console de opdracht : man open En meteen verschijnt er een uitgebreid relaas over deze funtie. In UNIX bestaan er opdrachten die het zelfde heten al bepaalde C functies. Zo zal bijvoorbeeld de opdracht : man write Een beschrijving geven van het programma write, waarmee berichten verstuurd kunnen worden. Daarom zijn de manuals in meerdere catagorieen ingedeeld waarbij catagorie 2 tot de programmeurs manuals behoord. Om informatie over de C funtie write te vinden tik je dan in : man 2 write Er bestaan ook opdrachten die in verschillende programeertalen het zelfde zijn. Zo zal de opdracht : man printf een beschrijving geven van de functie printf. Deze bestaat echter in de UNIX shell, in de Programeertaal C, maar ook in de scripttaal PERL Veel voorbeeldjes kun je ook vinden in een aantal van de Howto's van het Linux Documentatie Project (ldp) Zo beschrijft de Serial-Howto bijvoorbeeld hoe je in Linux gebruik kunt maken van je RS232 interface. Ook op het internet is er een schat aan informatie te vinden, al zul je hier al snel door de bomen het bos niet meer zien. Een aantal interesante zaken kun je vinden in : X-API documentation SDL-doc Hiernaa volgt een lijstje van de boeken welke ik gebruikt heb. Hierbij moet ik wel opmerken, dat ik jaren geleden toen ik de overstap van MS-DOS naar Linux maakte ik al over een ruime C en C++ ervaring beschikte en ik me de taal zelf niet meer eigen hoefde te maken. De boeken die ik gebruikte zijn daarom niet allemaal zo voor de hand liggend. Titel : UNIX Schrijver : Ir Riem van Erk Uitgeverij: Maklu ISBN : 90-6215-144-2 Een wat ouder pocket boekje dat zich richt op UNIX systemen. Er word daarbij ook wat aandacht besteed aan het schrijven van eenvoudige C applicaties. Erg knap om zoveel zinnigs in zo'n klein boekje samen te brengen. Om het te bemachtigen zul je eens wat vaker bij tweedehands boekwinkels moeten neuzen. Ik vond mijn exemplaar ooit bij boekhandel De sleghte in Maastricht. Titel : C programmers Guide Schrijver : SUN Microsystems Uitgeverij: SUN Microsystems ISBN : nvt Deze behoord tot de stapel boeken welke vroeger door SUN werd meegeleverd bij werkstations welke het oude SUN-OS (voorloper van SUN-Solaris) Draaiden. Dit soort boeken vind je dus enkel nog op de antiekbeurs. Echter ik veronderstel dat fabrikenten van soortgelijke machienes ook dergelijke boeken meeleverden. Vroeger werd n.l. bij de aanschaf van een computer een schat aan boeken meegeleverd. Die zijn vaak veel interesanter dan de computer zelf. Titel : C Set ++ for AIX Schrijver : IBM Uitgeverij: Wordt bij IBM-AIX meegeleverd. ISBN : nvt Bepaald geen gezellig boek om te lezen. het is echter zeer compleet naslagwerk. Als je aan dit boek kunt komen, moet je het zeker meenemen ! Titel : Linux Application Development Schrijver : Michael K. Johnson en Erik W. Troan Uitgeverij: Addison-Wesley ISBN : 0-201-3082-5 Dit is echt een must have ! Hoewel dit boek je niet leert hoe je C programma's moet schrijven staat het vol met informatie over het ontwikkelen van C programma's voor Linux systemen. Het grootste deel is van toepassing op UNIX systemen in het algemeen. Beheers je de taal C al zodanig dat je zelf wat eenvoudige applicaties kunt schrijven, dan zal dit boek je flink op weg helpen met het uitpluizen van interesante problemen zoals I/O handling, communicatie over netwerken, het maken van libaries en nog meer van die zaken die het op verjaardagsfeestjes erg slecht doen. Titel : Developing Linux Applications with GTK+ and GDK Schrijver : Eric Harlow Uitgeverij: New Riders ISBN : 0-7357-0021-4 Tja... ik wil niet zeggen dat ik het boek nooit heb ingekeken, maar het lijkt er erg veel op. Mij spreekt het niet aan, maar dat komt ook doordat ik niet zo erg geintereseerd ben in het ontwikkelen van grafische applicaties. ------------------------------------------------------------------------------ Inhoud ------------------------------------------------------------------------------ Voorwoord Inleiding Gebruikte standaarden H1 : Hello World H2 : De C compiler H3 : Variabellen H4 : Parameters doorgeven H5 : De functie printf H6 : Functie's H7 : Array's en strings H8 : Loops H9 : Conditionele opdrachten H10 : Pointers H11 : Data inlezen met scanf H12 : Structures H13 : Dynamisch geheugen alloceren H14 : Bestanden openen, lezen en schrijven H15 : Commandline parameters H16 : Signals H17 : Devices en lockfile's H18 : Include file's H19 : Object bestanden en Makefile's H20 : Processen forken H21 : Fraaiere programma's met ncurses H22 : Grafische programma's met X H23 : Communicatie over een netwerk H24 : Het verloop van een programma en het afhandelen van fouten. H25 : Geschikte Documentatie en hulpmiddelen voor het schrijven van C programma's in Linux INDEX