Introduksjon til C En kort innføring i prosedyreorientert programmering. Åge T Johansen, HiØ 1 Innledning C er et høynivå programmeringsspråk som også danner grunnlaget for andre programmeringsspråk - så som C++, Perl og Java/JavaScript. Dennis Ritchie utviklet språket i 1972, og det var på den tiden nært knyttet til operativsystemet UNIX. Ritchie ga språket navnet C fordi det delvis var en etterfølger av språket B. Siden den gang er språket standardisert av standardiseringsorganisasjonen ANSI (amerikansk) og er derfor nærmest universelt anvendbart. Dataprogrammer kan skrives i C. Datamaskiner forstår i utgangspunktet ikke C - programmene eller koden (som programmer også kalles) må kompileres og gjøres om til binær maskinkode før programmene kan utføres av datamaskinen. Binærkode (maskininstruksjoner) er det laveste nivået for et dataspråk og består av kodeordbestående av kombinasjoner av 0-er 0g 1-ere. Kompilatoren som benyttes for å oversette C-programmene er selv et dataprogram. Hva er grunnen til at du skal lære C? Det åpenbare svaret er vel at faget står på timeplanen i ingeniørutdanningen. Dernest kommer det faktum at C nærmest er for en standard å regne innenfor visse tekniske programmeringsoppgaver. Videre vil C være en god basis for å lære andre beslektede språk, som nevnt ovenfor. C++ er et slags supersett av C. Det betyr at (nesten) alt som er lovlig i C også er lovlig i C++. Derfor kan C-programmer utvikles på de fleste utviklingsverktøy for C++ også. 1.1 Syntaks. Selve skrivereglene for et dataspråk kalles syntaks. Denne kan sammenlignes med rettskrivning og grammatikkreglene i et vanlig språk. Syntaksen for et C-program er en blanding av C-nøkkelord som er forhåndsdefinerte engelsklignende ord som C-kompilatoren kjenner igjen. Eksempler her er int, for, return. Dette finnes noen titalls slike ord definert i C, men ikke alle forekommer like ofte. Konstanter og variable. Konstanter kan foreløpig betraktes som tallverdier som 1001, 0,34 eller 0. Variable representerer også tall, men verdien til en variabel kan (selvfølgelig) forandres. Variablene i et programmeringsspråk er ikke mye ulike variable som man kjenner fra læren om ligninger og funksjoner i matematikken. Operatorer og andre tegnsymboler. Operatorer er f. eks. + og -, men C har mange flere. Under andre tegnsymboler kan vi tenke på parenteser {[()]} og apostrofer, anførselstegn. NB! Noter deg med det samme at C er såkalt CASE SENSITIVE. Det betyr at man skiller strengt mellom små og store bokstaver i nøkkelord og andre navn. Det betyr f. eks. at hvis man ønsker å benytte nøkkelordet return og skriver dette Return eller RETURN vil kompilatoren oppfatte det som en syntaksfeil. Et program må være feilfritt syntaksmessig for å kunne benyttes. Et program med feilfri syntaks er en nødvendig forutsetning for at programmet skal virke, men det omvendte gjelder ikke. akkurat som en norsk stil kan være fullstendig ubrukelig selv om rettskrivningen og tegnsettingen er 100% korrekt. Dette forhindrer jo ikke at innholdet i stilen er tøvete. På samme måte kan et program som er feilfritt mht til syntaks inneholde bare tull, når det gjelder den funksjonen det skal utføre. 1.2 Formatet Et C-program kan skrives med et relativt fritt format. Det betyr at det ikke er så nøye med hvor vi bryter linjer eller hvor mange mellomrom vi har mellom to ord i syntaksen. Det er imidlertid noen begrensninger på dette. Spesielt gjelder det at vi ikke kan dele ord. Vi kan helle ikke sette ord eller tegn helt inntil hverandre slik at disse lett kan forveksles med andre ord og uttrykk. Vi kan for eksempel ikke skrive ret urn når vi mener return. Heller ikke kan vi skrive return24 når vi mener return 24. C-kompilatoren godtar at et helt C-program skrives på en eneste linje eller at hver linje kun inneholder et ord. En helt annen sak er hvor leselig et slikt program vil være for oss mennesker. Det er derfor viktig å tilstrebe et format og en struktur på programmet som er naturlig og lett og forstå. Tegn som vi kan benytte i C-programmene for å gjøre formatet mer leselig er alle former for mellomrom. Disse tegnene kaller vi ofte for "blanke" og de mest vanlige er tabulator (TAB), mellomrom (" "), linjeskift (LF) og vognretur (CR). På engelsk kalles disse tegne "white space". For å vise hvordan formatet kan variere for et lite C-program, vises programlistene nedenfor. Det samme programmet er vist to ganger: først med en normal formatering - deretter med en lite leservennlig formateringen. Programmene er identiske og C- kompilatoren vil ikke gjøre forskjell. 1.3 Kompilatorer. Kompilatoren er altså det programmet som oversetter C-koden til binærkoder. Disse programmene var en gang i tiden ganske kostbare og bare de færreste kunne få tilgang til et slikt program. I dag er enkelte profesjonelle kompilatorer fortsatt kostbare (10- tusener av korner), men det finnes også gode alternativer for studenter og andre som er svært rimelige og til dels gratis. 1.3.1 Eksempler på kompilatorer er Microsoft Visual C++ (tar også ren C-kode) Borland C++ Builder Developers C LCC GNU GCC Alle disse kompilatorene oversetter programmer som kan kjøres på en PC. Microsoft sin kompilator koster fra en 1000-lapp og oppover alt etter hvor mange tilleggsfasiliteter man får med. Borlands kompilatorer koster omtrent det samme, men her kan man ofte hente grunnversjonen (kanskje ikke nyeste versjon) gratis fra Internett. Developers C og LCC er gratisprogramvare fra Internett, men er derfor ikke godt fulgt opp og støttet fra utviklerne. Det kan derfor være en del feil og problemer her, men for en som ønsker å lære C burde disse også å være gode nok i massevis. Det siste programmet GCC er i en særstilling da det primært er utviklet for å benyttes på UNIX/LINUX maskiner. Det finnes også versjoner som fungerer på Windows-baserte PC- er. Programmet er gratis og av høy kvalitet. 1.4 Krysskompilatorer. Programutvikling foregår gjerne på en arbeidsstasjon som f. eks. en Windows-basert PC. Men det finnes også mange andre datamaskintyper man kan tenke seg å utvikle programmer for. Det gjelder ikke mist små billige mikrokontrollere av forskjellige slag som skal inngå i forskjellig slags teknisk utstyr fra kopimaskiner, fotoapparater, mobiltelefoner til medisinske analyseapparater. Felles for denne typen utstyr er at de ikke egner seg for programutvikling - ingen skjerm, tastatur, mus, disk etc. Løsningen er da å benytte en såkalt krysskompilator. Dette er kompilatorsystemer som kan kjøres på en PC, men som oversetter programmene til binærkoder som passer for andre maskiner enn en PC. De ferdige programmene må på forskjellig e måter overføres til utstyret der den aktuelle datamaskinen befinner seg. 1.5 Kommentarer. Kommentarer er i et programmeringsspråk tilleggsopplysninger som kan legges inn i programkoden. Disse opplysningene tjener til å hjelpe en som forsøker å forstå programmet du har skrevet. Kommentarene skal også være en hjelp for deg selv til å fuske hva du har gjort i programmet og hvorfor. Det er god programmeringsskikk å være raus med kommentarer. Du kan legge kommentarer til C-programmet ditt ved å skrive en vilkårlig tekst mellom tegnkombinasjonene /* og */. Alt som skrives mellom disse tegnkombinasjonene ignoreres fullstendig av kompilatoren. En annen type kommentartegn som de fleste nyere C-kompilatorer forstår, men som ikke hører med til standarden er dobbel skråstrek //. Alt som kommer etter // på den samme linja ignoreres av kompilatoren. Fra starten på neste linje vil så kompilatoren sjekke C-syntaksen igjen. Eksempler: /* Dette er en kommentar */ // Dette er også en kommentar. /* Dette er en kommentar som strekker seg over to linjer */ 1.6 Å lage utførbare programmer. Et utførbart program er som savnet sier, et program som en gitt datamaskin forstår og kan utføre. Det er ikke sikkert at et program som er utførbart på en type datamaskin vil være det på en annen vilkårlig datamaskin. For å lage et utførbart program må man gå gjennom 3 faser - koding, kompilering og linking. 1.6.1 Koding Dette er prosessen med og utvikle og skrive C-programmet inn i en programeditor. Det er her du som utvikler må legge inn hovedtyngden av innsatsen. Resultatet av kodingen er en tekstfil med C-kode - en kildefil. 1.6.2 Kompilering Som tidligere nevnt må C-koden oversettes til binærkode i en kompilator. 1.6.3 Linking Selv om programmet er kompilert, vil det ennå være noen løse ender i det som gjør at programmet ikke inneholder nok informasjon til å kunne utføres før det har vært gjennom ennå en prosess, nemlig linkingen. Link-prosesssen sørger bl. a. for at det gis tilgang til standard programmoduler som gjør det mulig å skrive informasjon fra programmet til skjerm eller filer og å hente informasjon fra tastatur og filer til programmet. Link-prosessen vil for enkle programutviklingssystemer nærest være usynlig for programmereren. Den kjøres som regelautomatisk etter en vellykket kompilering er ferdig. 1.7 Oppgaver og spørsmål til kapittelet Hva er en kompilator? Hva er en krysskompilator? Beskriv forskjellen på syntaksen og formatet for et progam. Hvilke andre språk er alternativer til C? Hvorfor legger man ofte til kommentarer i et dataprogram? Hva er et nøkkelord (keyword)? Hvorfor trenger man link-prosessen i tillegg til kompilering? Nevn noen aktuelle kompilatorer og deres karakterisika. Hva menes med koding? Hvorfor kan man utvikle C-programmer vha. av C++-kompilatorer? 2 Hello World 2.1 Hovedstrukturen til et enkelt C-program Nedenfor skal hovedstrukturen til et enkelt C-program skisseres. De enkelte delene i programmet skal ikke kommenteres her. Vi vil komme tilbake til disse etter hvert. La figuren tjene som et rammeverk som de forkjellige delen i et program kan plasseres inn i etter behov. Rammeverket (modellen) vil også detaljeres når det blir behov for det. Merk at ikke alle delene i denne modellen må være med i et gitt program. Den eneste nødvendige delen er faktisk hovedfunksjonen main(). | | |#include - setninger | |Innlemming av andre C-filer i dette programmet | | | |#define - setninger | |Definisjoner av konstanter etc. | | | |Deklarasjon av globale variable | |Deklarasjon av egendefinerte funksjoner i dette| |programmet | | | |Definisjon av startfunksjonen - main() { } | |Definisjoner av egne funksjoner | | | | | 2.2 Ditt første program La oss skrive og kompilere ditt første C-program! Skriv inn følgende programlinjer inn i programeditoren og lagre programmet som en fil med filtype .C (f. eks. world.c). Hvis du bruker et komplett programutviklingssystem som f. eks. Visual C++, vil man få filtypen .C automatisk når man lagrer filen. Hvis man derimot benytter en vanlig teksteditor, må man spesifisere filtypen selv. Vær spesielt oppmerksom hvis du benytter Windows-programmet NOTEPAD. Her kan det lønne seg å benytte anførselstegn rundt filnavnet slik: " world.c" , for å unngå at programmet ikke legger til txt til filtypen automatisk. Man kan benytte et tekstbehandlingsprogram (f. eks. WORD) til å redigere kildefilen, men det er da helt nødvendig å passe på at filen lagres i tekstmodus og ikke i det vanlige tekstbehandlingsformatet. 2.3 #include direktivet Hvis en linje i C-programmet begynner med tegnet '#' vil kompilatoren tolke linjen på en spesiell måte - nemlig at denne linje skal tolkes i en del av kompilatoren som kalles PREPROSESSOR. Preprosessoren kjøres alltid som første trinn av en normal kompileringsjobb, men det meste av programkoden passerer denne delen uten modifikasjoner. Møter derimot preprosessoren en linje som innledes med '#', vil denne tolkes som en spesialkommando til selve kompilatoren. En slik kommando kalles et direktiv. Et av de vanligste direktivene er #include. Dette benyttes for å fortelle at på dette stedet i programmet skal koden fra en annen programfil inkluderes. Effekten er den samme som om hele innholdet i den angitte filen hadde vært skrevet rett inn der #include-direktivet er plassert. Hvis filen inneholder programkode som benyttes i flere programmer, er dette metode for å få din egen programkode ryddig og oversiktlig. For å få skrevet ut tekst til skjermen fra programmet og for å lese tastetrykk fra tastaturet, er det ganske mye informasjon som skal legges inn i programkoden. Dette gjøres enklest og best ved å inkludere en systemfil som ordner med de nødvendige definisjonene. Denne filen er standard for alle C-kompilatorer som følger ANSI- standarden og kalles stdio.h. Denne filen har filtype .H og kalles en header-fil. Header-filer inneholder C-kode, men er beregnet på å bli inkludert i andre progamfiler. Header-filer inkluderes som oftest i starten av programmet. Det er to former #include- direktivet kan skrives på: #include "stdio.h" #include I det første tilfellet angir anførselstegnene at kompilatoren først skal søke etter den aktuelle header-filen i samme mappe som C-programmet for øvrig er lagret. Deretter skal det søkes i systemmapper som er bestemt av kompilatoren. I det andre tilfellet angir vinklene at kompilatoren kun skal søke etter header-filen i systemmappene. 2.4 Generell struktur for funksjoner Her følger en skjematisk oversikt over strukturen til en C-funksjon. Alle C-funksjoner er bygget over denne modellen, selv om de likevel kan være svært, svært forskjellige. | | |Funksjonshodet: | | | |returtype funksjonsnavn ( | |funksjonsargumentliste ) { | |Funksjonskroppen: | | | |Deklarasjon av lokale variable som bare | |har gyldighet inne i denne funksjonen. | | | |En sekvens av aktive C-setninger | | | |retur av funksjonsverdi | |} | 2.5 Funksjonen main. En funksjon kan betraktes som en serie med instruksjoner (setninger) som utføres når funksjonen kalles (startes). En funksjon kan kalle (starte) andre funksjoner ved navn. Et C-program består typisk av en rekke funksjoner som kaller hverandre. Et sted må jo det hele begynne. Det er derfor bestemt at alle C-programmer må ha en funksjon med navn main. Når datamaskinen starter programmet, starter den alltid med første setning i funksjonen main. Likeledes avsluttes (nesten) alltid C-programmet med siste setning i main. Du kan kun ha en main-funksjon i C-programmet, men denne kan til gjengjeld plasseres hvor som helt i programmet. En funksjon beregner typisk en verdi som blir tilgjengelig i den delen av programmet som kalte funksjonen. Dette kalles i C-sjargongen å returnere en verdi. Hvilken verdi som gjøres tilgjengelig på denne måten bestemmes i return setningen. Main-funksjonen returnerer verdien sin direkte til operativsystemet (Windows/Unix) som startet selve programmet. Når returverdien er 0, er dette et tegn på at programmet avsluttet på en normal måte uten feil. Når vi skriver inn en funksjon i programmet, må vi fortelle kompilatoren hva slags verdier som vil bli returnert fra funksjonen - heltal, desimaltall, tekst etc. Når det skal returneres et heltall (integer) må nøkkeloder int settes inn foran selve funksjonsnavnet. Alle funksjoner kan kjennes igjen ved at "vanlige" parenteser "( )"følger etter funksjonsnavnet. Det er i mange tilfeller mulig å legge til informasjon inne i parentesen. Funksjoner tilsvarer det som ofte kalles subrutiner i assembly-programmering. 2.6 Funksjonen printf Printf (print formatted) er en funksjon som skriver tekst til en dataskjerm. Teksten som skal skrives ut angis inne i funksjonsparentesene og igjen inne i anførselstegn. Faste tekster angis alltid i anførselstegn. En sekvens av tegn inne i anførselstegn kalles en tekststreng eller bare en streng. Tekststrengen som skal skrives ut og som angis inne i funksjonsparentesen, kalles funksjonsargumentet. til funksjonen printf. Tegnene i tekststrengen i "Hello World" eksempelet er vanlige skrivbare tegn bortsett fra kombinasjonen av de to siste tegnene "\n" (new-line). Dette er måten som benyttes for å fortelle printf-funksjonen at vi ønsker et linjeskift etter at "Hello World!" er skrevet ut. 3 Konstanter og variable 3.1 Hovedideen Variable kan sammenlignes med celler eller beholdere i datamaskinens hukommelse der du kan lagre verdier av forskjellig slag. Verdier (data) av forskjellig type - f. eks. heltall eller desimaltall kan lagres. Verdiene i variablene kan hentes fram og modifiseres når det er behov for det. En variabel identifiseres ved sitt navn - variabelnavnet. Variabelnavnene bestemmer vi selv når vi programmerer. Konstanter er også verdier. Som det framgår av navnet, kan ikke disse verdiene forandres - de bestemmes en gang for alle i programmet. Noen konstanter gis navn på samme måte som variable, mens andre skrives rett inn i programmet som en tallrekke, f. eks. 125. 3.2 Variabelnavn Selv om du selv kan bestemme variabelnavnene, er det visse faste regler som må følges: |Variabelnavn - - - - |Eksempel | |- kan ikke starte med et siffer |4kant | |- kan inneholde sifre på alla andre steder |kant4 | |- kan ikke inneholde matematiske tegn |a*b+c-d | |- kan ikke inneholde skilletegnene |#@%£&!,. | |- kan begynne med og inneholde "underscore" |_hallo | |- kan ikke være et C nøkkelord |while | |- kan ikke inneholde blanke tegn |fir kant | |- kan ikke inneholde "norske" tegn |blåbær | |- kan inneholde en blanding av små og store |FirKant | |bokstaver | | Sagt på en annen måte: Variabelnavn kan bestå av bokstavene "A-Z", "a-z", sifrene "0-9" samt underscore "_". Av disse tegnene kan ikke et av sifrene "0-9" være første tegn i navnet. 3.3 Litt terminologi EXPRESSIONS / UTTRYKK består av en blanding av konstanter, variable og operatorer (mer om operatorer senere). Uttrykk vil alltid medføre en (eller annen) utregning som gir en verdi tilbake. Noen eksempler på uttrykk: 17 // en konstant x // en variabel x + 7 // en variabel pluss en konstant STATEMENTS / SETNINGER er aktive instruksjoner og avsluttes med semikolon ";". Setninger består av en blanding av uttrykk, operatorer, funksjonskall og diverse nøkkelord. En setning i C tilsvarer gjerne flere maskin- (assembly-) instruksjoner Noe eksempler på setninger: x = 1 + 8; printf("Hurra, en utskrift"); int x, y, z; // En deklarasjon av heltallsvariablene x, y, z Det benyttes svært mange forskjellige OPERATORER i C for beskrive hvilke beregninger og andre manipulasjoner som skal utføres på aktuelle data. Vi skal her nevne operatorene for de fire standard regneartene samt tilordningsoperatoren: |Operato|Eksempel |Beskrivelse | |r | | | |+ |a + b |Addisjon | |- |a - 4 |Subtraksjon | |* |5 * b |Multiplikasjon | |/ |a / c |Divisjon | |= |d = a + 32; |Tilodning. Variabelen på venstre side av | | | |tilordningsoperatoren gis verdien til | | | |resultatet av uttrykket på høyre side av | | | |tilordningsoperatoren. | STATEMENT BLOCKS / SETNINGSBLOKKER er grupper av setninger som i de fleste tilfeller følger de samme reglene som enkle setninger. Det er enkelt å danne en setningsblokk - omslutt bare grunne av setninger du ønsker skal inngå i setningsblokken av klammeparenteser "{ }". En setningsblokk kan inneholde andre setningsblokker også (nestede blokker). Eksemplet nedenfor viser en kodebit der det inngår to setningsblokker. Det er ikke meningen at du skal forstå funksjonen av denne koden foreløpig, men legg merke til hvordan setningsblokkene dannes. Legg spesielt merke til at den innledende { plasseres i slutten av linjen før selve blokken, mens den avsluttende } plasseres alene (bortsett fra eventuelle kommentarer) på linjen etter blokken. Plasseringen av { og } er ikke noe som dikteres i C-standarden, men er uttrykk for en fornuftig programmeringsstil. Neste ramme viser eksempel på en kodestil som noen foretrekker, der alle blokkparentesene og kommentarblokksymbolene står på linjer for seg selv. Man får flere linjer i programmet på denne måten, men til gjengjeld kan det virke luftigere og lettere å lese. Gjør et stilvalg og hold deg hovedsakelig til dette. 3.4 Konstanttyper Konstanter som skrives direkte inn i programmet som tall (sekvenser av sifre) betegnes LITERALS på engelsk. (Siden det knapt finnes noe tilsvarende norsk begrep vil den engelske formen bli benyttet her.) Eksempler er: 24657 1 0 24 27.56 Merk at vi ikke benytter desimalkomma, men desimalpunktum. Motsvarigheten til LITERALS er SYMBOLSKE KONSTANTER. Symbolske konstanter representeres av et navn. En verdi kan tilordnes dette navnet i programmet ved initialisering. Det er flere måter å definere symbolske konstanter på i C. Vi kommer senere til å møte #define direktivet og nøkkelordet enum. Men vi fokuserer her foreløpig på bruken av nøkkelordet const: const int maksimum = 50; Heltallskonstanten maksimum er her gitt verdien 50. Det vil da være ulovlig å seinere i programmet forsøke å forandre verdien til maksimum til f. eks. 55 slik: maksimum = 50; Symbolske konstanter medfører mange fordeler fram for å bruke LITERALS. De to viktigste er: Programmet blir lettere å lese (for mennesker) når vi benytter navnet på en konstant isteden for tallet alene. Hvis vi referer til konstanten med navn maksimum aner vi at det her dreier seg om en maksimumsverdi og ikke en minimumsverdi eller noe annet. Tallet 50 alene kan jo representere hva som helst. Programmet bli enklere å vedlikeholde. Si at et program benytter den samme konstanten mange steder, f. eks. vil konstanten 3.14 sannsynligvis forekomme mange ganger i et program som beregner arealer av sirkler og sylindere. Hvis vi skulle ønske å ha med flere desimaler i pi, måtte vi således gå gjennom programmet og gjøre om alle forekomster av 3.14 til 3.14159. Ved bruk av en symbolsk konstant vil vi bare trenge å forandre definisjonen av denne konstanten. 3.5 Tallsystemer og konstanter Når man skriver konstanter (LITERALS) i et C-program, benyttes tall skrevet i det desimale tallsystemet som standard. Selve datamaskinen operer alltid på binærform intern, men C-kompilatoren oversetter tall skrevet på desimalform til binærform automatisk. Imidlertid kan det være ønskelig å angi konstanter på heksadesimalform eller på oktalform som alternativ til standard desimalform. Dette støttes av C-språket på følgende måter: 3.5.1 Heksadesimale konstanter Vi angir en konstant på hex-form ved å innlede tallet med sekvensen - 0x eller 0X - etterfulgt av en eller flere av sifrene 0-9 og a-f (eller A-F). Eksempler: 0x00 (( 0 0x09 (( 9 0x0A (( 10 0x10 (( 16 0xFF (( 255 Merk at for datamaskinen blir det det samme om vi benytter hex- eller desimalform, den regner med binærtall uansett. Hex-formen er spesielt nyttig fordi hvert siffer tilsvarer en 4-bits gruppe i det tilsvarende binærtallet. Det blir således enkelt å konvertere mellom tall på binærform og hex-form. 3.5.2 Oktale konstanter Vi angir en konstant på oktalform ved å innlede tallet med - 0 - etterfulgt av en eller flere av sifrene 0-7. Eksempler: 07 (( 7 010 (( 8 020 (( 16 0377 (( 255 I et oktalt tall tilsvarer hvert siffer en 3-bits gruppe i tilsvarende binærtall. Den oktale formen benyttes imidlertid ikke i dag på langt nær så ofte som hex-formen. 3.5.3 Tegnkonstanter C benytter en standard kodemetode -ASCII - for å kode 8-bits tegn (skrivbare tegn og kontrolltegn). ASCII-koden for et gitt skrivbart tegn fås ved å innramme tegnet i apostrofer ('A'). Eksempler: 'A' (( 0x41 (( 65 'B' (( 0x42 (( 66 '0' (( 0x30 (( 48 '9' (( 0x39 (( 57 Mer om tegnkonstanter i neste kapittel. 3.5.4 Flyttallskonstanter Se neste kapittel. 3.5.5 Konstanter med og uten fortegn Når man angir en heltallskonstant er den i utgangspunktet å betrakte som en konstant med fortegn. For desimale tall er fravær av fortegn å betrakte som +. Minus uttrykkes med minustegn, -. Ønsker man å betrakte en konstant som et tall uten fortegn (unsigned), kan man sette en 'U' eller 'u' etter konstanten. Dette gjelder også for heksadesimale og oktale konstanter. Eksempler: 0xFF (( -1 0xFFU (( 255 4 De 4 grunnleggende datatypene I C er det fire grunnleggende datatyper: char, int, float og double. Hver av disse har forskjellige egenskaper. For eksempel opptar alle forskjellig plass i hukommelsen som variable. Størrelsen til en variabel kan beskrives av antall bytes som går med til å lagre variabelen. For en typisk C-kompilator vil størrelsen på variable av disse datatypene variere mellom 1 og 8 bytes. Vi skal siden se hvordan operatoren sizeof kan benyttes for å bestemme størrelsen av forskjellige datatyper. Her følger en beskrivelse av de fire datatypene. 4.1 Datatypen char Variable av datatypen char kan lagre et enkelt tegn fra et tegnsett på 256 tegn. Det betyr at størrelsen eller er 1 byte eller 8 bit. Alle tegn fra tastaturet på datamaskinen har en unik numerisk kode knyttet til seg. I realiteten er altså et tegn representert ved et tall mellom 0 og 255. Det vanligste tegnsettet heter ASCII-tegnsettet og fantes opprinnelig i en 7-bits variant som kun kunne håndtere de mest vanlig bokstaver, sifre, skilletegn og kontrolltegn. I dag benyttes oftest en utvidet 8-bits variant der flere spesialtegn og mange nasjonale skrifttegn er representert. ASCII står for American Standard Code for Information Interchange. Hvilke tegn som vil presenteres på skjermen hvis man forsøker å skrive ut alle de forskjellige kodene dit, vil variere noe fra datamaskin til datamaskin - alt etter hvilket nasjonalt tegnsett som er lagt inn. Den følgende kodebiten er et program som skriver alle de 256 mulige verdiene en char variabel kan ha til skjermen. Figuren nedenfor viser et eksempel på hvordan resultatet kan bli. Det er ikke meningen du skal kunne forstå virkemåten for programmet nå. #include int main() { int i, j; for (i = 0; i < 26; i++) { for (j = 0; j < 10; j++) { printf("%d=%c ", 10 * i + j, 10 * i +j); } printf("\n"); } return 0; } De sorte rektanglene i figuren ovenfor representerer blant annet tegn som ikke har noe grafisk symbol, men som er såkalte kontrolltegn. Ut fra figuren går det fram at for eksempel "a" tilsvarer verdien 97, mens "A" tilsvarer 65. Siden et tegn egentlig lagres som et heltall mellom 0 og 255 betyr det at datatypen char kan benyttes for variable som skal benyttes for å lagre små heltallsverdier av generell natur, ikke bare tegn som benyttes til utskrift og innlesning fra tastatur. Avhengig av kompilatoren, kan datatypen char lagre heltall med fortegn (-128 - +127) eller uten fortegn (0 - +255). 4.1.1 Deklarere og initialisere variable. Å deklarere en variabel vi si å avsette plass til denne i hukommelsen. Dette involverer å bestemme navn for variabelen samt dens datatype. Man begynner med å angi datatypen - deretter følger en sekvens av 1 eller flere variabelnavn adskilt med komma og avsluttet med semikolon ";". Man kan altså avsette plass til flere variable om gangen. Deklarasjonen alene medfører ikke at det settes inn noen instruksjoner i dataprogrammet - kun plassreservasjon. char ch; char ch1, ch2; Å initialisere en variabel vil si å gi verdi til variabelen første gang. Dette gjøres vha. tilordningsoperatoren "=" . Det er mulig å kombinere initialisering og deklarasjon av variable i en felles setning, eller man kan utføre disse to oppgavene hver for seg. char ch = 65; // Bokstaven A char ch1 = 48, ch2 = 97; Initialisering av variable kan medføre at det settes inn instruksjoner i programmet i noen tilfeller, i andre tilfeller ikke. Vi skal ikke gå nærmere inn på dette nå. Å definere en variabel betyr i mange tilfeller det samme som å deklarere variabelen. I noen tilfeller må vi imidlertid skille på betydningen av disse begrepene. Da er det defineringen av variabelen som fører til at plass avsettes, mens deklarasjonen bare er en henvisning til en variabel som er definert et annet sted i programmet. 4.1.2 Initialisere char-variable Når man skal angi en tegnkonstant, kan man benytte den numeriske verdien direkte, som vist ovenfor. Det krever at man vet hvilken numerisk kode de enkelte tegnene har. Man kan også angi tegnet man ve å skrive inn tegnet selv omsluttet av apostrofer - f. eks. 'a'. Det er apostrofen som finnes på samme tast som * på tastaturet. Merk at det ikke skal benyttes anførselstegn for å angi enkelttegn. Anførselstegn er reservert for å angi strenger - f. eks. "Hello World!". Legg spesielt merke til at "A" oppfattes forskjellig fra 'A' av C-kompilatoren. Følgende kodebit inkluderer deklarasjoner og initialiseringer av char-variable. Detaljforståelse av virkemåten er ikke nødvendig her. #include int main() // Dette programmet skriver ut "Hallo" { char a, b, c, d; // Deklarasjon char e = 'o'; // Deklarasjon + initialisering a = 'H'; // Initialisering b = 'a'; c = 'l'; d = 108; // Num. verdi for 'l' printf("%c%c%c%c%c\n", a, b, c, d, e); return 0; } Utskriften vil bli: Hallo 4.1.3 Deklarasjon av tekststrengvariable Det kan passe å ta med en variant av char-variable her, nemlig tekststrengvariable. Tekststrengkonstanter er som vi før har sett, en sekvens av tegn innrammet av anførselstegn. Det er også mulig å deklarere en variabel som kan inneholde en tekststreng. Dette gjøres på følgende måte: char melding[100]; Her er melding navnet på strengvariabelen. Hakeparentesene med konstanten 100 indikerer at det skal være mulig å lagre opp til 100 tegn i denne strengen. Initialisering av tekststrenger kan uttrykkes slik: char melding[100] = "Hei du der borte!"; Her plasseres strengen "Hei du der borte!" på de første 17 posisjonene i variabelen melding. De resterende posisjonene[1] forblir ubrukt foreløpig. Man kan ikke utføre aritmetiske operasjoner som addisjon og subtraksjon (eller andre operasjoner) på denne typen variable. Heller ikke kan man kopiere innholdet fra en tekstvariabel til en annen ved hjelp av tilordningsoperatoren '='. Språket C har egentlig svært lite støtte for tekstbehandling. Andre språk er mye bedre her, men så er C også i første rekke et språk som egner seg i tekniske anvendelser. Mer om strenger under kapittelet om tabeller/arrays. 4.2 Datatypen int Datatypen int (integer) benyttes for å lagre heltall. Hvor store tall som kan lagres i en int variabel er ikke spesifisert i standarden, men størrelsen til variabelen pleier å tilsvare registerlengden i CPU-en programmet skal kjøres på. Typiske verdier for størrelsen på en int er 2 bytes (16 bit) eller 4 bytes (32 bit). Med 16 bit er omfanget av tallverdiene fra -32768 til +32768. Med 32 bit er omfanget fra -2147483 48 til +2147483 47. I begge tilfellene kan vi lagre tall med fortegn. Hvis man tilordner et tall med desimaler til en int-variabel, blir tallet avkortet til nærmeste heltall med lavere eller lik numerisk verdi. Merk at man ikke vil få noen avrunding til nærmeste heltall. Følgende kodebit illustrerer dette: #include int main() { int a, b, c, d, e; a = 10; b = 4.3; c = 4.8; d = -4.8; e = 4.3 + 4.8; printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); printf("d = %d\n", d); printf("e = %d\n", e); return 0; } Dette programmet fører til utskriften: a = 10 b = 4 c = 4 d = -5 e = 9 4.3 Datatypen float Variable med datatype float (floating point) kan lagre desimaltall innen et vidt område. Vi kaller tall som kan lagres av denne datatypen flyttall - hvor det inngår sifre både foran og etter desimalpunktet. For de fleste beregningsoppgaver der variabelverdier eller resultater av beregninger kan ha desimaler, vil denne datatypen være et godt alternativ. Størrelsen til en float-variabel er definert i C-standarden og er 4 bytes (32 bit). Lagringsformatet er også fast og definert i en IEEE standard. Trenger man desimalverdier som skal beskrives med svært stor nøyaktighet, eller at tallverdiene er svært store, vil man i stedet måtte benytte datatypen double. Datatypen double beskrives nærmere i neste avsnitt. Man kan blande variable og konstanter av typen int og float i uttrykk etter følgende hovedregler, der man tenker seg et uttrykk på formene: (op1 + op2) eller (op1 - op2) eller (op1 * op2) eller (op1 / op2) |Datatype |Datatype |Datatype | |op1 |op2 |resultat | |int |int |int | |int |float |float | |float |int |float | |float |float |float | Det er altså tilstrekkelig at én av operandene i et enkelt uttrykk som vist ovenfor er av type float for at resultatet også skal være av typen float. Husk at et resultat av typen float også må lagres i en variabel av type float, hvis ikke vil desimalene avkortes til nærmeste int med mindre eller lik tallverdi. Hvis man ønsker at resultatet av et uttrykk der to heltall inngår skal tvinges til å være av typen float, kan man benytte en teknikk som kalles CASTING. Casting angis ved å sette den ønskede datatypen i parentes før selve operanden. Eksempel på setninger med og uten casting er vist i programbiten nedenfor: #include int main() { float a,b,c,d,e,f; a = 1/3; b = 1/3.0; c = 1.0/3; d = 1.0/3.0; e = (float)1/3; f = (float)(1/3); printf("1 divided by 3 is %f\n", a); printf("1 divided by 3.0 is %f\n", b); printf("1.0 divided by 3 is %f\n", c); printf("1.0 divided by 3.0 is %f\n", d); printf("\nThe float-casting of 1, divided by 3 is %f\n", e); printf("\nf equals %f\n", f); return 0; } Utskriften fra dette programmet blir: 1 divided by 3 is 0.000000 1 divided by 3.0 is 0.333333 1.0 divided by 3 is 0.333333 1.0 divided by 3.0 is 0.333333 The float-casting of 1, divided by 3 is 0.333333 f equals 0.000000 Kommentarer til program og utskrift: Først blir 5 float-variabler deklarert og tilordnet verdier etter resultatene til forkjellige aritmetiske uttrykk. Variabel a tilordnes resultatet av en divisjon av to heltall. Dette burde gi verdien 0.3333.. .men på grunn av avkorting er resultatet av divisjonen 0. 0 blir da også lagret i a. Variabel b tilordnes resultatet av en divisjon mellom et heltall og en float-konstant. Dette resultatet vil være en float og b får verdien 0.3333. For variabel c er situasjonen tilsvarende, bare med den forskjell at rekkefølgen for float og int er byttet om. Resultatet blir det samme. For variabel d har man to float verdier som inngår, resultatet blir av typen float. Variabel e tilordnes verdien av et heltall (1) castet til en float (1.0) dividert med et heltall (3). Situasjonen blir den samme som for variabel b. Variabel f tilordnes verdien av en heltallsdivisjon castet til float. PÅ grunn av parentesene, utføres heltallsdivisjonen før casting finner sted. Da er allerede resultatet avkortet til 0. Situasjonen blir den samme som for variabel a. Merk til slutt at det er umulig å lagre verdien til visse brøker som f. eks. 1/3 100% nøyaktig på en datamaskin med endelig ordlengde. Det samme gjelder for irrasjonale tall som f. eks. (. 4.4 Datatypen double Navnet står for "double precision". Denne datatypen gir en svært nøyaktig angivelse av flyttall (inntill 10 desimalsifre). Double tar imidlertid opp dobbelt så mye plass i datamaskinens hukommelsen som float - 8 bytes (64 bit). Det er derfor sjelden nødvendig å benytte denne typen. Datatypene double og float kan ofte benyttes om hverandre, men hver oppmerksom på forskjeller når det gjelder utskrift og innlesning av verdier. double d = 1.00000001; 4.5 Avgivelse av numeriske konstanter Numeriske konstanter (tallkonstanter) skrives som regel inn i programmet på en intuitiv naturlig måte. 12, 134, 40004, 56.254, 80000.005 Merk igjen at man bruker desimalpunktum isteden for desimalkomma i konstanter. Komma benyttes i C som skilletegn i en del andre sammenhenger, bl. a. ved opplisting av variabelnavn ved variabeldeklarasjoner. Husk også at man angir tallverdien til et tegn ved å omslutte det aktuelle tegnet av apostrofer: '?'. Det er også mulig å benytte såkalt vitenskapelig notasjon for å angi flyttall. Dette innebærer at man kan skrive inn en eksponentverdi som tallet 10 skal opphøyes i, før hele potensen multipliseres med et flyttall. Eksponenten kan være positiv eller negativ: 1.2e3 ( 1.2 x 1000 = 1200 1.23e4 ( 1..23 x 10000 = 12300 4.5e-2 ( 4.4 x 0.01 = 0.045 5 Typemodifikatorer De fire grunnleggende datatypene som ble beskrevet i forrige avsnitt, kan gis noen tilleggsegenskaper vha. typemodifikatorer. 5.1 Nøkkelordene signed / unsigned Variable at typen int kan i utgangspunktet representere både negative og positive heltallsverdier - f. eks. -32768 til +32767. Datatyper som kan være både positive og negative kalles signed. Der det bare er aktuelt å benytte positive tall, som for eksempel når du skal fortelle hvor gammel en person er, kan man få et større tallområde ved å deklarere variabelen som unsigned (uten fortegn). Da vil antall lovlige verdier for en 16-bits int gå fra 0 til 65535. Eksempler - antar 16 bits heltall: int i = 0; unsigned int u = 0; i = i -1; // i får verdien -1 u = u-1; // u får verdien 65535 vha. aritmetikk i 2' komplement i = u = 32767; i = i+1; // i får verdien -32767 u = u+1; // u får verdien 32768 Det gjelder altså å holde tunga rett i munnen her. Det er også (for sikkerhets skyld) lov å angi direkte at vi ønsker å benytte fortegn. For datatypen int er dette unødvendig fordi int alltid i utgangspunktet er signed. For datatypen char derimot, er dette svært relevant, da char både kan være signed og unsigned (avhengig av kompilator): char c; unsigned char cu; signed char cs; 5.2 Nøkkelordene short / long Disse typemodifikatorene er kun aktuelle for i forbindelse med den grunnleggende datatypen int. Under beskrivelsen av datatypen int ble det påpekt at størrelsen av denne datatypen kan variere avhengig av kompilator og datamaskin. Modifikatorene short og long påvirker størrelsen av datatypen på følgende (noe uspesifikke) måte: short int er mindre enn eller lik størrelsen til int, men samtidig større eller lik størrelsen til char. long int er større eller lik størrelsen til int. For detaljer vedrørende dette må man undersøke håndboken for kompilatoren man benytter. I de tilfellene det er viktig å vite hvor mange bytes de enkelte variablene opptar, er det derfor vanlig å definere sine egne datatyper. Disse definisjonene må da gjøres om hvis man skifter kompilator. Eksempler som passer for de fleste kompilatorer på Windows-baserte (32 bit) PC-er: #define uint_32 unsigned int // 32 bits heltall uten fortegn #define int_16 short int // 16 bits heltall med fortegn I eksemplene ovenfor vil C-kompilatoren (egentlig C-preprosessor) erstatte symbolene som følger direkte etter define med alt som kommer på resten av linjen (minus kommentarer). Som vi har sett tidligere, kan define-direktivene også benyttes til å definere symbolske konstanter. En alternativ måte å definere de samme datatypene er slik: typedef unsigned int uint_32; // 32 bits heltall uten fortegn typedef short int int_16; // 16 bits heltall med fortegn Denne metoden er egentlig bedre og bør benyttes ettersom denne metoden ikke benytter preprosessordirektiver, men mekanismer som selve C-kompilatoren håndterer. 5.3 Operatoren sizeof Ved hjelp av operatoren sizeof kan man finne ut hvor mye plass en variabel eller datatype opptar. Mer kat dette er en standard operator i C og ikke en funksjon, selv om bruken kan minne om bruken av funksjoner. Sizeof-operatoren tar en operand som plasseres inne i et par etterfølgende parenteser. Operanden kan være en datatype eller et variabelnavn. double d; int x; x = sizeof(unsigned long int); x = sizeof(d); Et større eksempel: Utskriften fra programmet viser et eksempel på hvor mye plass i hukommelsen som må reserveres for de enkelte datatypene med modifikatorer. Det framgår også at størrelsen ikke påvirkes av modifikatorene signed eller unsigned. 6 Aritmetiske operatorer Aritmetiske operatorer benyttes flittig i de fleste programmeringsspråk. C er intet unntak i så måte. Her finner vi 5 forskjellige operatorer. 4 av dem er kont kjent fra matematikken. Den 5. operatoren ('%') kalles modulus-operatoren og finner resten etter en heltallsdivisjon. |Operatornavn |Symbol | |Multiplikasjon |* | |Divisjon |/ | |Modulus |% | |Addisjon |+ | |Subtraksjon |- | Operatorene i tabellen ovenfor er listet etter synkende PRESEDENS. Presedens beskriver hvilke operatorer som utføres først hvis flere operatorer inngår i et uttrykk. Multiplikasjon utføres før addisjon etc. Det er naturlig for oss å tenke slik også, jfr. matemakikken. Hvis man ønsker å forandre de innebygde presedensreglene i C, må man sette parenteser rundt de uttrykkene man ønsker å utføre først. Man sartere alltid med å beregne uttrykkene i de innerste parentesene først. 2 + 4 * 3 ( 2 + (4 * 3) ( 2 + 12 ( 14 Dersom det benyttes flere aritmetiske operander med samme presedens i samme uttrykk, begynner man med operatoren lengst til venstre for så å fortsette mot høyre. Deretter benyttes operatorene på neste presedensnivå osv. 2 * 3 + 8 / 2 ( 6 + 8 / 2 ( 6 + 4 ( 10 6.1 Heltallsdivisjon Stor sett virker de kjente aritmetiske operatorene som man vil forvente i sammenligning med sine matematiske brødre. Divisjonsoperatoren fører imidlertid ofte til uvante resultater i forbindelse med divisjon av to heltall - heltallsdivisjon. Heltallsdivisjon virker som når vi først lærte å dividere i grunnskolen. Da gikk jo ikke alle divisjonsstykker opp - vi fikk en kvotient og en rest: Eksempler: 8 : 2 = ( Kvotient 4, Rest 0 9 : 2 = ( Kvotient 4, Rest 1 9 : 3 = ( Kvotient 3, Rest 0 8 : 5 = ( Kvotient 1, Rest 3 17 : 6 = ( Kvotient 2, Rest 5 6 : 7 = ( Kvotient 0, Rest 7 Slik fungerer heltallsdivisjon i C også. Operatoren / gir oss bare kvotienten. Resten ignoreres når vi benytter heltallsdivisjon. 3 / 2 ( 1 og ikke 1.5 som svar. Dette er det viktig å huske. Mange feil blir gjort i forbindelse med dette. Spesielt lett er det å tenke feil er det når operanden på venstre side av " / " er mindre enn operanden på høyre side: 777 / 778 ( 0. Ønsker vi å få et flyttall som svar etter en heltallsdivisjon, må vi typekonvertere (cast) en eller begge operandene. Det er imidlertid for sent å typekonvertere resultatet av divisjonen. Se følgende eksempler: (float) 3 / 4 ( 0.75 // Første operand typekonverteres (float) (3 / 4) ( 0 // Resultatet typekonverteres pga parenteser 6.2 Modulus-operatoren % For å finne resten av en heltallsdivisjon benytter vi modulus-operatoren %. Denne fungerer bare ved heltallsdivisjoner. Eksempler på bruk er: 24 % 8 ( 0 21 % 12 ( 9 44 % 40 ( 4 35 % 50 ( 35 24 % 27 ( 24 Man kan selvfølgelig også finne resten av en divisjon også på denne måten: R = A - (A / B) * B (( R = A % B men det blir jo mer tungvindt. 6.3 Aritmetiske tilordningsoperatorer Tilordningsoperatoren i C er tegnet " = ". Denne operatoren fører til at resultatet av uttrykket på høyre side av operatoren kopieres over til operatoren på venstre side av operatoren. Dette betyr at venstre side av tilordningen alltid må være en variabel[2]. Høyre side kan være både variable, konstanter og andre generelle uttrykk. Skal man forandre verdien til en variabel i forhold til den opprinnelige verdien til variabelen kan man f. eks. skrive: a = a + b; // Tar a+b og lagrer resultatet tilbake til a. c = c / 2; // Forandrer c til halvparten av opprinnelig verdi. I C har man en kortform for å uttrykke at man modifiserer innholdet av en variabel: tilordningsoperatorer. Alle aritmetiske operatorer har sin egen form av tilordningsoperator også. Tilordningsoperatorer er kombinasjoner av tilordninger og operasjoner. De to eksemplene ovenfor kan ved bruk av tilordningsoperatorer skrives: a += b; // Tar a+b og lagrer resultatet tilbake til a. c /= 2; // Forandrer c til halvparten av opprinnelig verdi. Denne formen er svært utbredt og gir kompakte og lettleste programmer. |Fullt utskrevet |Kortform | |x = x * y; |x *= y; | |x = x / y; |x /= y; | |x = x % y; |x %= y; | |x = x + y; |x += y; | |x = x - y; |x -= y; | 6.4 Inkrement- og dekrementoperatorer Enda en kortform - nærmest spesialtilfeller av tilordningsoperatorene - finnes i C. Inkrementoperatoren angir at verdien til en heltallsvariabel økes med 1. Dekrementoperatoren angir at verdien til en heltallsvariabel skal reduseres med 1. 6.4.1 Inkrementoperator: a++; // Det samme som a += 1 eller a = a + 1 ++a; // Det samme som a += 1 eller a = a + 1 6.4.2 Dekrementoperator: a--; // Det samme som a -= 1 eller a = a - 1 + a; // Det samme som a -= 1 eller a = a - 1 6.4.3 Post - pre-inkrement Som det framgår ovenfor finnes det to varianter av inkrementoperatoren: x++ og ++x. Tilsvarende for dekrement. Hva er forskjellen? Er formene ekvivalente? Svaret er nei, selv om det i enkelt tilfeller ikke spiler noen rolle hvilken form som benyttes. Det gjelder i alle eksemplene ovenfor. Setter vi derimot opp følgende uttrykk: a = 5; b = ++a; // b <-- 6 c = a++; // c <-- 6 a = 5; d = a++; // d <-- 5 e = ++a; // e <-- 7 vil bruken av a++ og ++a være viktig for innholdet til b, c, d og e som vist. Reglene er slik at settes ++ (eller --) foran variabelen, vil variabelen inkrementeres før den benyttes videre i uttrykket. Dette kalles pre-inkrement. Hvis derimot ++ (--) plasseres bak variabelen, vil variabelen først benyttes i uttrykket, som den er, deretter blir den inkrementert. 7 Standard IO. IO er en forkortelse for det engelske Input/Output eller norsk Inn/Ut. Fordi IO og også begrepene Input og Output er svært vanlig brukt i programmeringssammenheng, skal vi benytte disse engelske ordene og forkortelsene her uten oversetting. Standard Output vil i de fleste tilfeller bety utskrift til skjerm. Det kan også i noen tilfeller bety utskrift til en fil eller til et annet program. Standard output er der utskriften havner, hvis vi ikke spesielt angir at den skal dirigeres et annet sted. Standard Output betegnes ofte stdout. Standard Input er den enheten vi leser informasjon fra i programmet hvis vi ikke spesielt angir noe annet. Vanligvis er dette tastaturet, men kan også være fra en fil eller fra standard output fra et annet program. Standard input betegnes ofte stdin. For å kunne benytte funksjoner fra standard IO-biblioteket, må vi inkludere definisjoner fra header-filen : #include Tidligere i dette kompendiet har det blitt benyttet funksjonen printf(). Dette er en funksjon fra standard IO-biblioteket i C. Noen av de mest benyttede IO-funksjonene skal forklares litt nærmere. 7.1 getchar() Denne funksjonen benytter vi når vi ønsker å lese inn ett og ett tegn fra tastaturet og finne ASCII koden til dette tegnet. Resultatet kan lagres til en variabel, eller benyttes direkte i en test[3]. Når vi kaller funksjonen getchar(), vil programmet stanse opp og vente til vi har trykket en eller flere taster på tastaturet. Programmet fortsetter videre når vi har trykket RETUR / ENTER-tasten. ASCII-kodene til tastetrykkene, fram til RETUR-tasten, lagres i et mellomlager i C programmet som vi ikke har direkte tilgang til - et input-buffer. Alle funksjoner har en verdi. Funksjonsverdien getchar() er ASCII-koden til første ikke- leste tegn i input-bufferet. Neste kall til getchar() gir en funksjonsverdi som er lik ASCII-koden til neste tegn i input-bufferet osv. Når vi har lest alle tegn i bufferet, vil et nytt kall til getchar() igjen føre til at programmet venter på en sekvens tastetrykk etterfulgt av RETUR. Vær oppmerksom på at også tastetrykk på returtasten (evt. ENTER) resulterer i et tegn som leses av getchar() som verdien til '\n': 10 (0x0A). Funksjonsprototype[4]: int getchar ( void ); Eksempel: /* Anta at input bufferet er tomt når de tre neste instruksjonene utøres. Variablene a, b og c er definert som int. */ a = getchar(); b = getchar(); c = getchar(); printf("%d - %d - %d\n", a, b, c); Dette fører til følgende utskrift hvis tegnene '0', 'a' og RETUR trykkes på tastaturet: 48 - 65 - 10 Dette tilsvarer ASCII-kodene på desimalform for de innleste tegnene. 7.2 putchar() Denne funksjonen benyttes når man trenger utskrift til skjerm av et enkelt tegn. Funksjonsargumentet er ASCII-koden til tegnet man vil skrive ut. Tegnet skrives ikke til skjermen før linjeskifttegnet - '\n' skrives eller programmet forsøker å lese fra standard input[5]. Merk forskjellen mellom putchar() og printf(). Printf() kan formatere og skrive ut en tekststreng eller tegnsekvensen som representerer en tallverdi f. eks. på desimalform. Putchar() skriver ut et enkelt tegn ut fra angitt ASCII-kode. Funksjonsprototype: int putchar (int ch); Funksjonsparameteren i funksjonsprototypen er ASCII-koden til tegnet som skal skrives ut. Funksjonsverdien er av type int, og er enten ASCII-koden til tegnet som ble skrevet ut eller en spesialkode[6] som indikerer feil. Som regel tilordnes ikke funksjonsverdien til putchar() til noen variabel - den ignoreres. Følgende lille C-program viser bruk av putchar() og getchar() med kommentarer. Printf() benyttes også. Denne funksjonen diskuteres nærmere nedenfor. Hvis man redigerer, kompilerer og kjører dette programmet vil følgende tekster vises på skjermen. Anta at tegnene som er vist i uthevet kursiv (A8 etterfulgt av RETUR / ENTER) er tastet inn av brukeren. Enter a 2 letter word and press return: A8 The first letter was: A Followed by: 8 Goodbye! 7.3 printf() Dette er en kort beskrivelse av funksjonen printf() som allerede er benyttet i flere eksempler. Funksjonen er svært kraftig og allsidig og kan benyttes i de fleste sammenhenger der man ønsker utskrift til skjerm av mer kompleks natur enn enkle tegn. Dette inkluderer utskrift av: tekststrenger heltall på desimalform heltall på heksadesimalform flyttall - vanlig flyttall - vitenskaplig notasjon enkle tegn alle kombinasjoner av de ovenstående punktene. Antall funksjonsargumenter i printf() kan variere. Dette er litt spesielt, men også andre C-funksjoner kan gis denne egenskapen. Funksjonsargumentene er delt inn i to kategorier. I den første kategorien inngår en såkalt formatstreng som inneholder 0 eller flere formatspesifikatorer. Formatstrengen er alltid første argument i argumentlisten og har form av en vanlig tekststreng avgrenset med anførselstegn "- - -". I de tilfeller man ikke har noen formatspesifikatorer, inneholder formatstrengen kun en tekststreng som skal skrives ut. printf("Enkel formatstreng uten formatspesifikatorer\n"); De eventuelle resterende argumentene som inngår i et kall til printf(), er en liste av de verdiene som skall skrives ut i henhold til formatspesifikatorene. 7.3.1 Formatspesifikatorer Formatspesifikatorer er tegnkombinasjoner på 2-5 tegn i formatstrengen som innledes med et prosenttegn %. I sin enkleste form dannes formatspesifikatorene ved at et entelttegn føyes til % - f. eks. %d, %c eller %x. En formatspesifikator er en plassholder for en variabelverdi (, konstant eller uttrykk) som skal skrives ut. Selve variabelen listes etter formatstrengen. Husk komma som separator mellom formatstreng og første utskriftselement. Utskrift av variable og uttrykk på denne måten kommer i tillegg til den ordinære teksten fra formatstrengen. Formatstrengen kan i prinsippet inneholde så mange formatspesifikatorer man ønsker, men man må da liste opp like mange variable (uttrykk) etter formatstrengen som man har spesifikatorer. Rekkefølgen til formatspesifikatorene bestemmer rekkefølgen til variablene: 1. spesifikator gjelder første variabel etc. 7.3.1.1 %d, %i Den enkleste (?) formatspesifikktoren er kanskje %d som angir at verdien til et heltall skal skrives ut på desimalform. x = 7; printf("Konstanten er %d", 45); printf("X = %d", x); printf("Konstanten er %d og X = %d", 45, x); Utskriften fra ovenstående C-setninger er: Konstanten er 45 X = 7 Konstanten er 45 og X = 7 Verdiene som skrives ut med %d må være av type int, short int eller char. %i virker som %d. 7.3.1.2 %x Denne formatspesifikatoren virker tilsvarende %d, men fører til utskrift på heksadesimalform. 7.3.1.3 %c %c benyttes hvis man ønsker å skrive ut et enkelt tegn vha. printf(). Dette kan kanskje synes litt tungvindt når man har putchar(), men vha. av %c kan man blande strenger enkle tegn og tall: ch = '#'; printf("Ascii-koden til %c er: %d (des) - %x (hex)\n", ch, ch, ch); Utskriften blir i dette tilfellet: Ascii-koden til # er: 35 (des) - 0x23 (hex) 7.3.1.4 %s Utskrift av ekstra tekststrenger. Det er i C mulig å opprette variable som angir tekststrenger. Ved hjelp av %s kan man hente en del av utskriften fra en slik strengvariabel, mens resten er fast. Anta i eksemplet nedenfor at str er en variabel[7] som inneholder verdien "april". Denne variabelen kan eksempelvis benyttes i en printf() på følgende måte: printf("Dato: %d. %s\n", 5, str); Utskrift: Dato: 5. april 7.3.1.5 %f %f benyttes ved utskrift av flyttall (float og double). printf("%f", 3.5); Utskrift: 3.500000 Som standard skrives det ut 6 desimaler etter desimalpunktet. Det er mulig å spesifisere et annet antall gyldige sifre, men det skal ikke omtales foreløpig. 7.3.1.6 %e Dette er et alternativ til %f der utskriften er i vitenskaplig notasjon. printf("%e", (2000.0/3.0)); Utskrift: 6.666666e+2 7.3.1.7 %% Ønsker man rett og slett utskrift av et prosenttegn benyttes sekvensen %% uten noen tilhørende variabel/konstant. 7.4 scanf() Funksjonen scanf() er motsvarigheten til printf() når det gjelder innlesning av tall og bokstaver fra tastaturet. Akkurat som printf(), består funksjonsargumentene til scanf() av en formatstreng pluss angivelse av minst en variabel som skal ha verdien/e som innleses. Scanf() tilbyr kraftigere og mer fleksible metoder for innlesning enn getchar(). Imidlertid viser det seg i praksis at scanf() også kan by på en del overraskende resultater. Vi skal se på noen enkle (og sikre?) eksempler på bruk her. Studer kodeeksemplet nedenfor: printf("Enter a number and press Enter: "); scanf("%d", &a); printf("The number was %d\n", a); Før noe annet kommenteres, legg merke til tegnet & som benyttes før variabelnavnet a. Benyttet på denne måten[8] er & en adresseoperator. Det betyr at &a angir adressen til variabelen a i datamaskinens hukommelse. Foreløpig kan du bare godta at det må være slik at variablene som skal gis verdier ved hjelp av scanf() alltid[9] må ha dette &- tegnet foran navnet i parameterlista. Tilbake til eksempelet. Først skrives det ut en tekst som oppfordrer brukeren av programmet til å taste inn et tall (gjerne flersifret) etterfulgt av RETUR / ENTER. Deretter kalles scanf() opp med 2 argumenter. Det første argumentet er, som alltid, formatstrengen med formatspesifikatoren %d. Dette indikerer at vi skal lese inn et heltall. Antall formatspesifikatorer i formatstrengen må matche antall variable som listes etter formatstrengen - én variabel i vårt tilfelle. Merk at eventuelle ekstra tegn i formatstrengen utenom formatspesifikatorene ikke skrives ut som tekst, men kan ha spesielle betydninger ved innlesning. Det enkleste er å bare benytte formatspesifikatorer atskilt med mellomrom. Utskrift fra programlinjene ovenfor, dersom brukeren taster inn 543 (uthevet) blir: Enter a number and press Enter: 543 scanf("%d", &a); The number was 543 7.4.1 Å lese inn flere verdier Hvis du benytter flere formatspesifikatorer i samme formatstreng, kan du lese inn verdier til flere variable i samme scanf() funksjonskallet. Husk å skille spesifikatorene med mellomrom. Eksempel: scanf("%d %d", &x, &y); Hvis det skives inn 1 23 etterfulgt av RETUR, vil variabelen x få verdien 1 og y verdien 23. 7.4.2 Formatspesifikatorer for scanf() Generelt gjelder de samme formatspesifikatorene for scanf() som for printf(). Noen forskjeller er det likevel. Den viktigste forskjellen er kanskje at ved innlesing av en double datatype må benytte formatspesifikatoren %lf i motsetning til %f som kan benyttes for utskrift av samme datatype. En annen spesialitet for scanf() er å kunne lese inn en hel tekstlinje til en tekstvariabel. I dette tilfelle er det 2 ting å passe på. For det første må en spesiell formatspesifikator benyttes: %[^\n]. Denne formatspesifikatoren instruerer scanf() til å lese alle tegn som forekommer på linjen fram til linjeskift. For det andre skal man ikke benytte & foran strengvariabelen. Dette kommer av at strengvariabelen egentlig er en adresse[10] i seg selv. Her er et eksempel på et lite program som leser inn en hel linje med tekst fra tastaturet og skriver den samme tekste til bake til skjermen: #include int main() { char str[100]; printf("Skriv inn en linje med teskt: "); scanf("%[^\n]", str); printf("Tekst: %s\nOK\n", str); return 0; } Kjøring av program gir f. eks.: Skriv inn en linje med tekst: Hei, alle sammen! 1 2 3 4 SLUTT Tekst: Hei, alle sammen! 1 2 3 4 SLUTT OK Merk at også %s kan benyttes for innlesning av tekst, men da vil innlesningen slutte etter at første mellomrom er nådd i den innleste teksten. Altså ville man bare få lest inn Hei, i eksempelet ovenfor med formatspesifikatoren %[^\n] byttet til %s. 7.5 fflush() I forbindelse med innlesing av data fra tastaturet får man av og til problemer med at tidligere inntastede tegn ikke er lest ut av input-bufferet. F. eks. vil vi ved scanf() ikke lese inn '\n'-tegnet som gis av RETUR- / ENTER-tasten som avslutning på en linje. Dette skaper gjerne forvirring hvis man etter en scanf() forsøker å lese et inntastet tegn med getchar(). Det hadde da vært gunstig å kunne tømme input-bufferet helt før neste tegn skulle leses. fflush() er en standardfunksjon som lar oss tømme input-bufferet for alle allerede innleste tegn. Som funksjonsargument må vi angi stdin. fflush(stdin); // Tøm input-buffer fflush() kan også benyttes for å tvinge utskrift av gjenværende tegn i output-bufferet. Dette tilfellet må funksjonsargumentet være stdout. fflush(stdout); // Skriv ut alle tegn fra outputbuffer. 7.6 Oppgaver Lag et program som leser inn en persons navn, vekt, høyde og alder, årslønn og skatt. Det skal gis ledetekster slik at brukeren vet hva som skal tastes inn. Programmet skal så skrive ut en samlet oversikt over denne personens innsamlede data - pent formatert. Skriv et program som leser inn et beløp i norske kroner og gjør om dette til svenske kroner, dollar og euro. En lønnsøkning på 9.9% er gitt de ansatte i en bedrift. Skriv et program der en ansatt kan oppgi sin tidligere årslønn og få informasjon om ny årslønn, månedslønn og ukeslønn tilbake, beregnet av programmet. (Anta at det er akkurat 52 uker i et år.) Et beløp er investert med en rente på 12% pa. Skriv et program som leser inn startbeløpet og viser hvor stort beløpet har blitt i slutten av hvert år for de 3 første årene. Gulvet i et rom skal dekkes av kvadratiske fliser med sidekant 15 cm. Flisene skal legges med 2 cm mellomrom. Skriv et program som leser inn rommets dimensjoner og beregner hvor mange hele fliser det vil gå med. (Altså: glem antall fliser som må deles for å få dekket hele gulvet.) 8 Relasjonsoperatorer og Logiske operatorer Til nå har vi bare sett på programeksempler som utfører setning for setning i programmet helt sekvensielt, eventuelt men kall til funksjoner. Programmene som kan lages på denne måten blir ganske begrensede i og med at den samme veien fra start til mål følges hver gang programmet utføres. I realistiske programmer trenger vi å kunne ta avgjørelser på bakgrunn av data som brukeren taster inn, eller resultater som beregnes i programmet. Disse avgjørelsene vil så kunne bestemme hvilken av en rekke alternative setningssekvenser som velges. Muligheter for å ta en avgjørelse får vi etter å ha foretatt en test. F. eks.: Hvis det regner ute drar vi til byen, hvis ikke, drar vi på stranda. Her "testes" været for regn ikke-regn. Andre tester kan være: er verdien til variabelen x større enn 7 ? er verdien til a lik verdien til b ? er a = 7 og / eller b = 7 ? er d = e samtidig med at f > 4 ? Hvis man kan svare ja på en slik test, sier vi at utfallet til testen er SANN (TRUE), hvis ikke, er utfallet FEIL (FALSE). I C tolkes en heltallsverdi forskjellig fra 0 som SANN, mens en 0 tolkes som FEIL. Vi har et sett av operatorer som gjør oss i stand til å utfører en mengde forskjellige tester. Disse kalles relasjonsoperatorer og logiske operatorer. 8.1 Relasjonsoperatorer Disse 6 operatorene lar oss teste numeriske verdier mot hverandre to og to. Operatorene er intuitive og enkle å ta i bruk. Tabellen nedenfor beskriver de enkelte relasjonsoperatorene. Alle operatorene gir 0 (FEIL) eller 1 (SANN) til resultat. |Operatorens navn |Symbol | |Mindre enn |< | |Mindre enn eller lik |<= | |Større enn |> | |Større enn eller lik |>= | |Lik |== | |Ikke lik (forskjellig fra) |!= | De fire øverste operatorene i tabellen har høyere presedens enn de to nederste. Vær spesielt oppmerksom på at test for likhet benytter to / 2 påfølgende likhetstegn "=="og ikke bare ett. Ett likhetstegn i C betyr tilordning - uten unntak. Alle aritmetiske operatorer har høyere presedens relasjonsoperatorene. Eksempler: 5 > 1 ( 1 (SANN) 4 >= 3 ( 1 (SANN) 4 < 2 ( 0 (FEIL) 4 <= -1 ( 0 (FEIL) 2 >= 2 ( 1 (SANN) 7 != 1 ( 1 (SANN) 5 == 4 ( 0 (FEIL) Det er altså venstre side i relasjonsuttrykkene som må leses først - f. eks.: Er 5 større enn 1 ? Er 4 større enn eller lik 3 ? Osv. 8.2 Logiske operatorer Testene i avsnittet ovenfor er en form for logiske betingelser (uttrykk). Resultatet av en logisk betingelse er som nevnt 0 (FEIL) eller 1 (SANN). C har altså ingen spesiell datatype for logiske størrelser, vi bruker et heltall og tolker verdien som SANN / FEIL. Når et heltall tolkes som en logisk størrelse betyr igjen 0 FEIL, mens alt forskjellig fra 0 betyr SANN. Man kan kombinere flere logiske uttrykk (betingelser) og logiske størrelser til kompliserte logiske uttrykk ved hjelp logiske operatorer som vi har 3 av i C[11]. Funksjonen til de logiske operatorene beskrives i sannhetstabeller. De logiske operatorene i C tilsvarer de logiske operatorene fra digitalteknikk og mengdelære. |Operator NOT (logisk negasjon) | |Uttrykk |Resultat | |! 1 |0 | |! 0 |1 | |Operator AND | |Uttrykk |Resultat | |0 && 0 |0 | |0 && 1 |0 | |1 && 0 |0 | |1 && 1 |1 | |Operator OR | |Uttrykk |Resultat | |0 || 0 |0 | |0 || 1 |1 | |1 || 0 |1 | |1 || 1 |1 | Merk at i kolonnene for uttrykk, kan man erstatte 1 med en hvilken som helst heltallsverdi forskjellig fra 0 - men for oversiktens skyld benyttes bare verdien 1 i tabellene. Legg også merke til at man må bruke doble && og || i logiske betingelsesuttrykk. Eksempler: (1 == 0) || (1 > 2) ( 0 !(1 == 0) || (1 > 2) ( 1 1 && (34 <= 43) ( 1 8.3 Foreløpig oversikt over operator-presedens . Nedenfor er det vist en oversikt over presedens (prioritet) for de operatorene som har vært omtalt til nå. Operatorene er listet med fallende presedens fra øverst til nederst. I et uttrykk med flere operatorer vil operatorene med høyest presedens benyttes først i utregningen. |Høyest presedens | |( ) ! | |* / % | |+ - | |< <= > >= | |== != | |&& | ||| | |Lavest presedens | Husk at operatorpresedens kan omgås ved bruk av parenteser. Det er imidlertid alltid en god regel å benytte parenteser for å tydeliggjøre hvordan man tenker seg at uttrykkene skal oppfattes selv om det i enkelte tilfeller kan være unødvendig. Bedre med for få enn for mange parenteser. Eksempler: Operatoren ! (NOT) har høy presedens. Skal et helt AND-uttrykk inverteres, må vi benytte parenteser rundt uttrykket, hvis ikke vil bare venstre operator bli invertert. !(A && B) // Invertering av and-operasjonen (NAND) !A && B // ( (!A) && B A > B || C == D // ( (A > B) || (C == D) x + 5 == 6 // ( (x + 5) == 6 Det siste uttrykket får verdien 1 hvis x er 1, hvis ikke er verdien 0 x < 3 && y < 7 || y == 5 // ( ((x<3) && (y<7)) || (y == 5) Hvis y er lik 5 blir resultatet av y == 5 lik 1 og hele uttrykket blir 1 på grunnlag av OR-operasjonen. Hvis ikke, må man sjekke om begge relasjonsuttrykkene i forbindelse med AND er SANNE. I så fall blir resultatet SANT. Hvis en eller begge AND-uttrykkene er FEIL, blir totalresultatet FEIL. Anta at x har verdien 4 og y verdien 5. Setter vi dette inn i uttrykket ovenfor får vi: ((4 < 3) && (5 < 7)) || (5 == 5) fører til: (0 && 1) || 1 fører til: 0 || 1 fører til: 1 8.4 Oppgaver Undersøk om uttrykkene nedenfor vil gi resultatet SANN eller FEIL: a = 1; b = 2; c = 3; d = 4; b == 1 ( ? b >= a ( ? (c > a) && (c >= d) ( ? c > a && c >= d ( ? d == a || b < c ( ? (a > 1.356) || !(b == c) && (d != 3) ( ? 9 Betingede valg I assemblyprogrammering har vi som regel mange instruksjoner som gir oss muligheten å styre programutførelsen ved å hoppe til forskjellige steder i programmet avhengig av resultatet av en tidligere instruksjon eller en bitverdi på en port eller i et register. I C har vi også muligheten for å styre programutførelsen, men på en mer overordnet og strukturert måte. 9.1 Hvis - i fall Det viktigste kontrollelementet som lar oss få velge en av flere veier gjennom programmet er if. [pic] I sin enkleste form har kontrollelementet denne strukturen: if () ; En enkel C-setning kan generelt sett erstattes av en blokk: if () { ; ; ; } Her er det som er betegnet et aritmetisk eller logisk uttrykk som når verdien av uttrykket er 0, tolkes som FEIL, og når verdien er forskjellig fra 0, tolkes som SANN. Setningen eller setningsblokken etter if utføres altså bare når betingelsen er SANN. Hvis Resultatet av betingelsesuttrykket er FEIL, skjer det ingen ting. if (a == 1) { printf("A er 1\n"); a = 2; } if (a == 1) printf("Hit skal vi aldri komme\n"); Ofte ønsker vi at det også skal utføres alternative setninger når resultatet av betingelsesuttrykket er FEIL. Dette får vi til ved å utvide if-setningen med et else- ledd. [pic] if () ; else ; Alternativt: if () { ; ; } else { ; ; } Eksempel: if (a == 1) printf("A er 1\n"); else printf("A er forskjellig fra 1\n"); Et større eksempel: Dette programmet kan gjøres litt smartere og kortere uten bruk av else: Det er altså ikke alltid at bruk av de mest kompliserte kontrollelementene, fører til de enkleste programmene. 9.2 Jamen . hvis derimot . For de testene som er vist i avsnittet ovenfor, er det bare to alternative programveier - if-grenen eller else-grenen. I mange praktiske tilfeller kan det være flere alternative programveier. Når vi kommer til et gatekryss, kan vi f. eks. gå rett fram, ta til høyre eller ta til venstre. Noe tilsvarende kan vi få til i C, selv om det ikke er nødvendig å innføre noe nytt kontrollelement. Nøkkelen til løsning ligger i at etter else, kan vi sette en hvilken som helt lovlig C-setning, også en ny if. Vi kan da sette opp en struktur som dette: if (a == 1) printf("A er 1\n"); else if (a == 2) printf("A er 2\n); else printf("A er forskjellig fra 1 og 2\n"); I dette eksempelet oppfattes den siste if-else setningen som en enkel setning. Innrykket viser hvilken if som tilhører hvilken else. Imidlertid er det vanligere å skrive strukturen ovenfor på en litt annen måte. Husk at vi kan skrive C-programmet i "fritt format" med hensyn til hvor vi legger inn linjeskift , setter in blanke tegn osv. Den siste if-testen kan derfor flyttes til samme linja som else, slik at strukturen får dette utseendet: if (a == 1) printf("A er 1\n"); else if (a == 2) printf("A er 2\n); else printf("A er forskjellig fra 1 og 2\n"); Vi kan utvide denne formen med så mange "else if" tester vi vil, men husk at når en av testbetingelsene blir SANN og en av grenene utføres, ignoreres alle de andre grenene. Altså: kun en gren i en if - else if - else kontrollstruktur utføres. Neste eksempel viser en mer avansert bruk av et flervalgsprogram: Hvis man i det generelle tilfellet (etter den siste else) ikke har noen oppgaver som skal utføres, kan man sløyfe else-leddet helt, eller man kan sette inn en tom setningsblokk som vist nedenfor: if(x == 0) { printf("x er null\n"); } else if (x == 1) { printf("x er 1\n"); } else { /* Ingenting */ } 9.3 Nestede if-blokker If-setninger kan plasseres inne i andre if-blokker. Et eksempel på dette er if - else - if -else strukturen som ble vist i forrige delkapittel. Prinsippet gjelder også generelt. Vi kan plassere en if-setning der vi kan plassere en hvilken som helt annen setning. Når en if-setning plasseres inne i en annen if-blokk, kalles det nestede if-setninger. Selv om det ikke alltid er nødvendig, lønner det seg som regel å bruke klammeparenteser for å synliggjøre de forskjellige blokkene for lesbarhetens skyld. I nestede if-setninger kan det av og til være vanskelig å finne ut hvilke else-ledd som hører til hvilke if-ledd. Spesielt gjelder dette hvis programmereren ikke er nøye med innrykk og med klammeparenteser for å adskille de enkelte blokkene. Et eksempel: if (a == 1) putchar('1'); if (b == 2) putchar('2'); else putchar('3'); Her kan man spørre seg om else-leddet hører til første eller andre if-ledd. Regelen for svaret på dette er at else-leddet hører til den nærmeste if-setningen som ikke allerede er avsluttet med et else-ledd. Altså tilhører else i eksempelet den andre if-setningen. Ønsker man at else skal tilhøre andre if-ledd enn det som hovedregelen foreskriver, må man bruke klammeparenteser som vist i eksempelet nedenfor, der else vil høre til den første if på grunn av blokkgrensene: if (a == 1) { putchar('1'); if (b == 2) putchar('2'); } else putchar('3'); 9.4 Oppgaver Skriv et program som leser inn lengden til en person til en variabel - L. Hvis L er mindre enn 1,50 m, skriv ut meldingen "Kort". Hvis ikke, skriv ut meldingen "Lang". Som forrige oppgave, men differensier meldingene ved å skrive meldingen "Veldig lang" hvis L er større en 1,80 m. Skriv et program som finner det største av 2 tall som leses inn og deretter skriver ut dette tallet. Skriv et program som finner det største av 3 tall som leses inn og deretter skriver ut dette tallet. Skriv et program som skriver ut 3 tilfeldige tall som leses inn i stigende rekkefølge (minste først osv.). I en butikk gis det 13% rabatt på alle salg over 500kr og 20% rabatt på salg over 1000kr. Lag et program som lar deg skrive inn beløpet som det er handlet for, og skriver ut beløpet som skal betales etter at eventuell rabatt er trukket fra. Skriv et program som leser inn nummeret til en måned (1 - 12) og deretter skriver ut antall dager det er i denne måneden. Denne oppgaven kan også utvides med å la programmet skrive ut navnet på måneden. En skole ønsker å forandre karaktersystem. Før ble det benyttet tallkarakterer fra 1.0 til 6.0. Nå skal det benyttes bokstavkarakterer fra A til F. For å lette lærerne i en overgangsfase skal du skrive et program som leser inn en karakter av gammel type og konverterer denne automatisk til en bokstavkarakter etter følgende konverteringstabell: 1,0 - 1,5 ( A 1,6 - 2,3 ( B 2,4 - 3,0 ( C 3,1 - 3,5 ( D 3,6 - 4,0 ( E 4,1 - 6.0 ( F Lag et program som konverterer et kronebeløp til enten dollar eller euro. Brukeren av programmet skal etter å ha tastet inn kronebeløpet gis valget mellom å taste inn 'D' for dollar eller 'E' for euro. Lag en utvidelse av forrige oppgave der brukeren kan konvertere beløpet alle veier ved å skrive ND for kroner til dollar, DN for dollar til kroner, EN for euro til kroner osv. Vi får 6 kombinasjoner. Vær nøye med å få med passende ledetekster. 10 Løkker Begrepet løkker benyttes i programmering for å beskrive at en setning eller en sekvens av setninger utføres igjen og igjen før løkken brytes og den naturlige sekvensen av setninger kan fortsette. Antallet ganger som løkken gjennomløpes, kan variere fra 0 eller 1 gang til uendelig antall ganger. Det siste kalles en evig- eller en uendelig løkke. I C har vi 3 forskjellige løkketyper, med hver sine særegenheter for while do-while Bruken av disse 3 løkketypene er noe overlappende, da alle løkkene kan benyttes for å simulere andre løkketyper også. 10.1 Løkkebetingelse For å bestemme hvor mange ganger eller hvor lenge en løkke skal gjentas, er det til hver løkketype knyttet en løkkebetingelse. I C vil løkken fortsette å gjentas så lenge løkkebetingelses beregnes til et resultat som er forskjellig fra 0 - altså så lenge løkkebetingelsen er SANN, logisk sett. Løkkebetingelsen vil beregnes i starten eller slutten av løkken alt etter løkketype. Løkkebetingelsen har samme for som og tilsvarende betydning som testbetingelsen i if- setninger. 10.2 "for" - løkker Detter er den mest fleksible løkketypen og benyttes svært mye av C-programmerere. Grunnformen er som følger: for (ledd_A ; ledd_B ; ledd_C) { ; ; ... ... ; } Her er ledd_A et ledd som initialiserer løkken og utføres kun én gang - før selve løkken starter. Ledd_A utføres alltid og er et lovlig C-uttrykk. Ledd_B er løkkebetingelsen som beregnes i starten av hver runde i løkken. Løkken utføres så lenge løkkebetingelsen er SANN. Fordi løkkebetingelsen utføres i starten av løkken, kan man (hvis løkkebetingelsen beregnes til FEIL første gang) oppleve at en for- løkke ikke utføres 0 ganger. Ledd_C er et C-uttrykk som utføres i slutten av hver runde - før løkketesten beregnes på ny for å se om løkken skal gjennomløpes flere ganger. Disse 3 leddene skilles alltid med semikolon. Alle leddene kan om ønskelig utelates, men de runde parentesene og semikolonene må være med uansett. En løkkebetingelse som utelates teller som en SANN løkkebetingelse. I malen ovenfor er det antydet at hoveddelen av løkka består av en setningsblokk. En enkel C-setning kan også benyttes. Alternativt format med kun én setning i løkkekroppen: for (ledd_A ; ledd_B ; ledd_C) ; [pic] Et lite eksempel - skriv ut alle heltallene fra 0 til 9: for(i = 0; i < 10; i++) { printf("%d - ", i); } Ledd_A: i = 0. Dette er en typisk initialisering av en for-løkke som gir en variabel en startverdi. Ledd_B: i < 10. En like typisk løkkebetingelse i en for-løkke. Løkken skal først avsluttes når verdien til variabelen i har nådd verdien 10. Ledd_C. i++. Her økes verdien til i med en etter hver runde i løkka. Siden vi startet på 0 og løkken skal utføres for verdiene 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 vil altså løkken i dette tilfellet bli utføret 10 ganger. Verdiene som skrives ut er nettopp verdien til i for de forskjellige gjennomløpene av løkka. En noe mer komplisert eksempel: for(i=10, j=0 ; i != j ; i--, j++) { printf("%d - %d = %d\n", i, j, i-j); } I dette eksempelet er det nødvendig å initialisere 2 variable, dette kan gjøres ved å sette komma mellom de to initialiseringene. Beregningsrekkefølgen er ikke bestemt når vi benytter komma på denne måten. Riktigheten av programmet må derfor ikke avhenge av om i eller j får sin verdi først. Det spiller jo ingen rolle i dette tilfellet. Løkkebetingelsen er en test på om i er forkjellig fra j. Verdiene for de 2 variablene er jo til å begynne med henholdvis 10 og 0 - altså forskjellige. Løkken utføres derfor første gang. Etter at løkken er utføret en gang og verdiene til i og j og differensen i - j er skrevet ut, utføres ledd_C som i dette tilfellet er i--, j++. Det betyr at for hver runde i løkken skal verdien til i dekrementeres og verdien til j inkrementeres. Forhåpentligvis vil de møtes, slik at løkkebetingelsen feiler og løkken stoppes. Legg merke til at vi har benyttet komma også her for å adskille de 2 uttrykkene som skal utføres. La oss bygge et skikkelig program basert på eksempelet ovenfor: #include int main() { int i,j; for(i = 10, j = 0; i != j; i--, j++) { printf("%d - %d = %d\n", i, j, i-j); } return 0; } Dette programmet bør gi følgende utskrift når det kjøres: 10 - 0 = 10 9 - 1 = 8 8 - 2 = 6 7 - 3 = 4 6 - 4 = 2 Nedenfor er det vist en tabell der verdiene til variablene i og j og i != j er plottet inn for hvert omløp (iterasjon) av løkken: |Iterasjon|Før løkke |Etter | | | |løkke | |2 |9 |1 |SANN |8 |2 | |3 |8 |2 |SANN |7 |3 | |4 |7 |3 |SANN |6 |4 | |5 |6 |4 |SANN |5 |5 | |6 |5 |5 |FEIL |- |- | Som vi ser, inkrementeres j og dekrementeres i for hver iterasjon. Etter 5 iterasjoner er begge lik 5. Dette får altså løkkebetingelsen til å bli FEIL når denne regnes ut i starten på den 6. iterasjonen. Dette får løkka til å avslutte. Neste C-setning som skal utføres, vil bli den første etter løkken - i dette tilfellet return-setningen som avslutter programmet. Hvordan ville programmet forløpt hvis i ble initialisert til 9 i stedet for til 10? Vi setter opp tabellen på nytt: |Iterasjon|Før løkke |Etter | | | |løkke | |2 |8 |1 |SANN |7 |2 | |3 |7 |2 |SANN |6 |3 | |4 |6 |3 |SANN |5 |4 | |5 |5 |4 |SANN |4 |5 | |6 |4 |5 |SANN |3 |6 | |7 |3 |6 |SANN |2 |7 | |8 |2 |7 |SANN |1 |8 | Løkken kommer ikke til av kunne avsluttes fordi i og j aldri blir like. Hvordan skal da løkken kunne stanses? Vel, det finnes flere tenkelige alternativer, men her skal vi vise hvordan vi lett kan få kontroll over "vrange" løkker ved forandre litt på løkkebetingelsen. Følgende løkke vil stoppe av seg selv, hvis bare startverdien til i er større enn startverdien til j. for(i=10, j=0 ; i > j ; i--, j++) { --- --- } 10.2.1 Typiske anvendelsesområder Fordi for-løkken er så kraftig og fleksibel, har den mange anvendelsesområder i programmering. Her er noen eksempler: løkker som skal utføres et bestemt antall ganger løkker for å beregne elementene i en matematisk rekke løkker der variablene som inngår i løkkebetingelsen gis oppdaterte verdier i slutten av hver iterasjon. 10.2.2 Uendelige "for-løkker" Følgende for-setning gir en uendelig løkke: for ( ;; ) // Utføres alltid { --- --- --- } 10.3 "while" - løkker Rent strukturelt er while-løkken en forenklet form for for-løkke, der kun løkkebetingelsen er beholdt i selve løkkekontrollen: while () { ; ; ... ... ; } Alternativt format med kun én setning i løkkekroppen: while () ; [pic] Løkken utføres (som for-løkken) når løkkebetingelsen beregnes SANN. Fordi løkkebetingelsen må utføres for selve løkken kan gjennomløpes, kan det hende at løkken aldri vil gjennomløpes, som f. eks. i eksempelet nedenfor: x = 4; while ( x >= 5 ) { --- --- x = 7; } Ved hjelp av ekstra C-setninger kan nøyaktig samme oppgaver løses av while-løkker som av for-løkker. Eksemplet nedenfor viser hvordan programmet fra forrige delkapittel kan realiseres med en while-løkke: #include int main() { in t i, j; /* Følgende 2 initialiseringer kan gjøres i for-setningen */ i=10; j=0; while (i != j ) /* løkkebetingelse */ { printf("%d - %d = %d\n", i, j, i-j); /* Følgende 2 setninger kan utføres i selve for-setningen */ i--; j++; } return 0; } 10.3.1 Uendelige while-løkker. En uendelig while-løkke kan uttrykkes slik: while ( 1 ) // Utføres alltid { --- --- --- } 1 er jo alltid forskjellig fra 0 og derfor logisk sett SANN. På den andre side vil løkken nedenfor aldri bli utført, fordi 0 alltid logisk sett er FEIL. while ( 0 ) // Utføres aldri { --- --- --- } 10.3.2 Typiske anvendelsesområder While-løkker kan benyttes til alt. Imidlertid er det i noen tilfeller naturligere å ty til en while-løkke enn til f. eks. en for-løkke: løkker der det ikke er noen naturlig initialisering av variable som inngår i løkkebetingelsen. løkker der variablene i løkkebetingelsen får sine verdier utenfor selve programmet, f. eks. ved inntasting av tallverdier fra tastatur. venteløkker som kun har til hensikt å få programmet til å stoppe opp og vente til en eller annen betingelse er oppfylt. 10.4 "break" - setningen For å kunne bryte ut av ellers uendelige løkker er det en spesiell C-setning som er spesiallaget for formålet: break. Break-setningen består kun av nøkkelordet break etterfulgt av semikolon: break; Virkemåten til break er slik at nærmeste omsluttende løkke avsluttes umiddelbart, og programmet fortsetter med første instruksjon etter løkken. Virker for alle løkketyper og switch-setningen[12], men NB! IKKE FOR IF-SETNINGER. #include int main(void) { int i; for(i=0 ; /* no expression */ ; i++) { printf("%d\n", i); if(i == 1000) // Avslutt løkke når i blir 1000 { break; } } return 0; } Break er svært nyttig i enkelte tilfeller, men eksempelet ovenfor løses mer elegant med en løkkebetingelse slik: for(i=0 ; i <= 1000 ; i++) { printf("%d\n", i); } 10.5 "do while" - løkker Dette er kanskje den minst brukte løkke-setningen i C. Særegent for denne er at løkketesten utføres i slutten av løkken istedenfor i begynnelsen. Effekten av dette blir at løkken alltid utføres minst 1 gang. do { ; ; ... ... ; } while (); Alternativt format med kun én setning i løkkekroppen: do ; while (); [pic] Dette eksempelet skriver ut tallene 0 til 9: #include int main() { int i = 0; do { printf("%d", i); i++; } while (i < 10); // NB! Semikolon må være med her return 0; } Så lenge løkkebetingelsen beregnes SANN, gjentas løkken. Følgende while-løkke program utfører samme oppgave som do-while løkken ovenfor: #include int main() { int i = 0; printf("%d", i); /* første gangs itersasjon for do-while */ i++; while (i < 10) { printf("%d", i); i++; } return 0; } Det vil altså ofte være nødvendig å sette inn ekstra instruksjoner før while-løkken for å simulere en do-while løkke. 10.6 "continue" - setningen. Continue er et nøkkelord som kun benyttes i forbindelse med løkker. Hensikten er ikke å avslutte løkken, som for break, men å hoppe over resten av løkkekroppen for så å starte med løkketesten på nytt. For while- og do-while løkker hopper man direkte til løkketesten. I for-løkker hopper man til "ledd_C" der løkkevariablene typisk inkrementeres. Bruk av continue-setningen vises i følgende eksempel som gir en utskrift av alle tall mellom 0 og 100 som er delelig med 3 (uten rest): #include int main() { int tall = 0; for (tall = 0; tall <= 100; tall++) { if (tall % 3) // 3 går ikke opp i tall hvis uttrykket != 0 continue; // Dropp utskrift av tall printf("%d \n", tall); // Skriv tall } return 0; } Bruk av continue kan også i mange tilfeller erstattes med andre måter å konstruere programmet på som neste eksempel viser: #include int main() { int tall = 0; for (tall = 0; tall <= 100; tall++) { if ((tall % 3) == 0) // Går 3 opp i tall ? printf("%d \n", tall); // Skriv tall } return 0; } 10.7 Oppgaver Hva står det på skjermen etter at dette programmet er utført? #include int main(void) { int i; putchar('-'); for (i = 60; i <= 70; i++) { i++; if (i == 62) i -= 2; else if (i == 64) i += 2; else if (i > 66) break; printf("%d-", i); } return 0; } Skriv et program som skriver ut alle tall mellom 0 og 150 som er delelige med 7. Skriv et program som leser inn tall helt til summen av tallene overtiger 1000. Skriv et program som leser inn 10 tall (- ett tall for hver scanf()) og finner det minste av dem. Lag et program som finner arealet og omkretsen av en likebeint trekant, der brukeren skal taste inn høyde og lengde. Etter hver beregning skal brukeren spørres om han/hun ønsker å fortsette. Hvis brukeren svarer med 'J', skal en ny trekant beregnes. (Biblioteksfunksjonen sqrt(x) kan benyttes for å finne kvadratroten av et tall. Headerfilen må da inkluderes i programmet.) Skriv et program som finner og skriver ut alle primtall mellom 0 og 100. (Denne oppgaven krever bruk av 2 nestede løkker.) Lag et program som skriver ut den lille multiplikasjonstabellen på tabellform. (Hint: 2 nestede for-løkker.) Lag et program som skriver ut en multiplikasjonstabell fra A x A til B x B på tabellform. Ta gjerne med overskrifter etc. til tabellen, men få først programmet til å virke uten. (Kosmetikken til sist.) A og B skal leses fra tastaturet. Hint: Oppgaven vil kunne løses ved å benyttet en for-setning (for radene) om inneholder en ny for- setning (for kolonnene). Eksempel på resultat av programkjøring: Les inn A: 5 Les inn B: 9 25 30 35 40 45 30 36 42 48 54 35 42 49 56 63 40 48 56 64 72 45 54 63 72 81 Lag et program som genererer og skriver ut en sekvens av alle heltall som oppfyller alle kriteriene: 1. er mellom 0 og n 2. er delelig med 3 eller 7 er ikke delelig med 4 og 5 Programmet skal også skrive ut antall tall som er generert. Verdien til n skal leses fra tastaturet. Anta at du ikke har tilgjengelig operatorene * (multiplikasjon) og / (divisjon). Skriv et program som leser inn 2 heltall og multipliserer og dividerer disse tallene med hverandre ved bruk av løkker og + og - operatorene. Lag et program som skriver en liste (10 verdier per rad) over sinusverdiene til hver grad mellom 0 og 90 grader. Du kan benytte standardfunksjonen sin(x) som returnerer sinusverdien (float) til en vinkel x (float) som er angitt i radianer. Krever inkludering av headerfilen . Skriv et program som beregner og skriver ut parvise (x, y) verdier for ligningen y = Ax2 + Bx + C Programmet skal spørre brukeren om å oppgi konstantene A, B og C samt innenfor hvilket intervall (x1 - x2) som det skal beregnes y verdier og hvor stor avstand (d) det skal være mellom x-verdiene. Lag et program som aksepterer et binærtall som en sekvens av binære sifre '0' og '1' fra brukeren helt til bokstaven 'b' eller 'B' tastes inn. Det tilsvarende desimaltallet skal beregnes og skrives ut. Benytt getchar() i innlesningen av binærtallet. 11 Tabeller / Arrays Først noen ord om norske kontra engelske betegnelser som benyttes i dette kapittelet og ellers i kompendiet. Det engelske begrepet array oversettes til henholdsvis tabell, vektor og matrise. Begrepene vektor og tabell benyttes fortrinnsvis for en-dimensjonale tabeller, men også i enkelte tilfeller for flerdimensjonale tabeller. Matrisebegrepet benyttes for 2 og flerdimensjonale tabeller. Det er f. eks. ikke noen forskjell på en 2- dimesjonal tabell og en 2-dimensjonal matrise slik begrepene er benyttet her. I begge tilfeller er de oversatt fra det engelske "2 dimentional array". 11.1 Motivasjon. Det er tidligere vist hvordan man kan definere variable som kan holde informasjon om forskjellige ting; alder, vekt, pris, personnummer. Variablene kan være av forskjellig datatype; int, float etc. La oss anta at man har lagret personnummeret til 10 personer og ønsker å skrive ut disse. Informasjonen er lagret i variable av typen int. Deklarasjonen av variable og utskriftsetningene kan da se slik ut: --- --- int numA = - - - - -; int numB = - - - - -; int numC = - - - - -; int numD = - - - - -; int numE = - - - - -; int numF = - - - - -; int numG = - - - - -; int numH = - - - - -; int numI = - - - - -; int numJ = - - - - -; --- --- printf("%d\n", numA); printf("%d\n", numB); printf("%d\n", numC); printf("%d\n", numD); printf("%d\n", numE); printf("%d\n", numF); printf("%d\n", numG); printf("%d\n", numH); printf("%d\n", numI); printf("%d\n", numJ); --- --- Eksemplet er ganske klossete skrevet, men hovedhensikten er å vise at håndtering av et stort antall variable er ganske omstendelig, selv om det er den samme jobben som skal utføres for alle variablene - her utskrift av personnummer. Alternativet er å benytte tabeller. En tabell lar oss lagre sekvenser av variable av samme datatype med et felles navn. De enkelte variablene i denne sekvensen - kalt tabellelementer - identifiseres med navnet pluss en numerisk indeksverdi. 11.2 Deklarasjon av tabeller Tabeller må deklareres slik som alle andre variable. Vi angir at en variabel er en tabell (og ikke en enkeltvariabel) ved å legge til et sett av hakeparenteser [] etter variabelnavnet. Inne i parentesene angir man så hvor mange elementer tabellen skal ha. Antall elementer må være en konstantverdi - ikke en variabel. Eksempler: int personnummer[10]; // Tabell med 10 int-variable float vekt[235]; // Deklarasjon av tabell med 235 float-variable For å benytte en av tabellelementene angis tabellnavnet etterfulgt av hakeparenteser som omslutter indeksen til elementet man ønsker å nå. Tabellelementer kan benyttes på samme måte som enkle variable (i alle sammenhenger), f. eks. på begge sider av en tilordning. Tabellindeksen kan være en variabel eller en konstantverdi. Tabellindeksen må imidlertid alltid være et heltall (int, char). Eksempler: personnummer[3] = 13456; // Element 3 i tabellen gis verdi vekt[x] = vekt[200] + 2.5; // Element x og element 200 benyttes Merk at tallet inne i hakeparentesene [x] har forskjellig betydning i deklarasjonen av tabellen og ved bruk av tabellelementene. I første tilfellet angir tallet antall elementer som tabellen skal bestå av. I det andre tilfellet angir tallet x indeksen til det aktuelle tabellelementet. 11.2.1 Laveste og høyeste indeksverdi for en tabell. Anta at en tabell er definer med N elementer. Indeksene i tabellen løper alltid fra 0 som laveste indeks til N-1 som høyeste indeks. I en dal andre programmeringsspråk kan man selv velge startindeks, det kan man altså ikke når det gjelder C. Eksempler: Tabellen personnummer ovenfor har altså indekser fra 0 til 9. Tabellen vekt ovenfor har indekser fra 0 til 234. 11.2.2 Ingen indekssjekking. C har ingen sjekking for at programmereren benytter indekser som er innenfor det "lovlige" område. Man kunne tenke seg at kompilatoren sjekket dette der tabellindeksene er konstanter. Der tabellindeksen er variable måtte det legges inn tester i selve programmet som sjekket om indeksene holdt seg innenfor det lovlige området. Ingen av disse tingene sjekkes altså i C. Andre språk derimot har begge disse testene (PASCAL / ADA). For C er filosofien at programmereren vet best selv. Angir programmereren en indeksverdi utenfor det lovlige området, antar C-kompilatoren at det er det som ønskes. Eventuelle sjekker må programmeres inn i selve programmet. Enkelte programmerere benytter seg av den manglende sjekkingen til å lage "smarte" programmer som indekserer seg ut over det lovlige område for å oppnå spesielle effekter - farlig. 11.2.3 Initialisering av tabeller Tabeller kan som enkle variable gis initialverdier allerede når de deklareres. Alle tabellelementene kan gis verdi ved å liste opp alle elementverdiene adskilt med komma. Lista med initialverdier må omsluttes av klammeparenteser: int personnummer[10] = {5712, 2334, 35634, 453, 5656, 52, 2374, 245, 23423, 345}; Her er alle 10 elementene initialisert. Første element i lista tilsvarer indeks 0, neste indeks 1 etc. float vekt[235] = {36.12, 45.00, 123.90, 555.555, 0,3454}; Det er lite praktisk å initialisere 235 elementer på denne måten (i hvert fall i et kompendium som dette). Likevel er ovenstående initialisering riktig. Når antall elementer som listes opp er færre enn totalt antall elementer i tabellen, vil de første elementene i tabellen initialiseres som angitt - de resterende blir initialisert til 0. I forbindelse med initialisering, har man også muligheten til å gi C-kompilatoren ansvaret for å finne ut selv hvor mange tabellelementer det er behov for ut fra initialiseringslista: char tekst[] = {'a', 'b', 'c', 'd', 'e'}; Her vil det avsettes plass til en tabell med 5 elementer med indekser fra 0 til 4. Vi kan sjekke hvor mange bytes det er avsatt plass til i tabellen ved hjelp av sizeof() operatoren: printf("Antall elementer i tabellen -tekst- er %d\n", sizeof(tekst)); I dette tilfellet vil utskriften angi 5 elementer. 11.3 Tabeller "in action" La oss nå gå tilbake til eksemplet med å skrive ut 10 personnummer som ble vist i starten av kapittelet. Vi har nå et verktøy til å gjøre dette mye mer elegant ved bruk av tabeller enn det som ble vist i det første eksempelet: int personnummer[10] = {5712, 2334, 35634, 453, 5656, 52, 2374, 245, 23423, 345}; int i; for (i = 0; i < 10; i++) { printf("%d\n", personnummer[i]); } Utskriften i dette tilfellet blir: 5712 2334 35634 453 5656 52 2374 245 23423 345 Enkelt og greit? Noen kommentarer kan vel være nyttig likevel: Den første programlinje oppretter tabellen med personnummer og initialiserer denne med verdier. I for-løkka benyttes løkkevariabelen i som initialiseres til 0 før løkken starter. Siden i ikke tillates å bli større eller lik 10, og den inkrementeres med 1 for hver iterasjon, vil løkke gjennomløpes nøyaktig 10 ganger. For hver iterasjon i løkka skrives det ut et tabellelement. Sekvensen av tabellelementer som skrives ut blir derfor: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Hvis du nå likevel ikke innser fordelene ved bruk av en tabell i dette tilfellet, tenk deg da et register med tusenvis av personnumre og hvordan dette skal kunne håndteres ved bruk av enkle variable for hver post. En utvidelse av eksempelet ovenfor kan være å lese inn verdiene til tabellen fra tastaturet, isteden for å legge de inn i tabellen ved initialisering. Leses verdiene inn fra tastaturet, kan de forandres for hver gang programmet kjøres. Legges de derimot inn ved initialisering, medfører det at man tenger å rekompilere programmet hver gang verdiene skal forandres. int personnummer[10]; int i; --- --- for (i = 0; i < 10; i++) { scanf("%d", &personnummer[i]); } --- --- for (i = 0; i < 10; i++) { printf("%d\n", personnummer[i]); } Merk at akkurat som ved innesning til enkle variable vha. scanf(), må vi sette tegnet & foran et tabellelement. 11.4 Strenger og char-tabeller Tekststrenger har vi benyttet tidligere bl. a. ved utskrift av "Hello World\n" vha av printf(). En tekststreng i C er en sekvens av tegn omsluttet av anførselstegn. Teknisk sett er en tekststreng ikke noe annet en tabell av char-elementer. Vi kan altså deklarere en tekststreng-variabel på denne måten: char tekstreng[100]; char s[] = {'H', 'e ', 'l ', 'l ', 'o ', ' ', 'W ', 'o ', 'r ', 'l ', 'd ', '! '}; I tabellens er altså teksten "Hello World!" lagt inn ved initialisering. Da burde man kanskje få skrevet ut denne teksten ved hjelp av følgende kall til printf(): printf("%s", s); Her angir %s en formatspesifikator for utskrift av strengvariable. Dessverre ville ikke dette bli resultatet. Utskriften ville i beste tilfelle bli "Hello World!" etterfulgt av en rekke mystiske tegn og symboler som representerer det som tilfeldigvis måtte befinne seg i datamaskinens minne etter tabellen s. I verste tilfelle vil programmet krasje. 11.4.1 Null-avsluttet tekststreng. Årsak? En tekststreng, når den benyttes av standardfunksjonene i C, forutsettes å bestå av litt mer enn bare en sekvens av skrivbare tegn i en tabell. Sekvensen forutsettes alltid å være avsluttet av verdien 0 (ofte skrevet '\0') for at f. eks. printf() skal forstå når strengen er slutt. Merk at det er verdien 0 og ikke tegnet '0' som må avslutte strengen. Hvis s i eksemplet ovenfor hadde vært initialisert på følgende måte, hadde utskriften blitt som forventet: char s[] = {'H', 'e ', 'l ', 'l ', 'o ', ' ', 'W ', 'o ', 'r ', 'l ', 'd ', '! ', 0}; // eller char s[] = {'H', 'e ', 'l ', 'l ', 'o ', ' ', 'W ', 'o ', 'r ', 'l ', 'd ', '! ', '\0'}; // eller char s[] = "Hello World!"; // hmmm? Den siste initialisering som er vist, er den mest vanlige. For tekststrenger kan man altså bruke skrivemåten med anførselstegn som omslutter de tegnene som skal inngå i strengen. Når denne metoden benyttes, legges den avsluttende 0 inn automatisk (smart). 11.4.2 Strengkonstanter Når du angir en tekststreng som en sekvens av tegn omsluttet av anførselstegn ("tekst") lager man egentlig det som kalles en strengkonstant. Strengkonstanter og strengvariable kan i mange tilfeller benyttes om hverandre. 11.4.3 Utplukking av enkelttegn i strengvariable. Følgende lille program lar deg lese inn en tekst til en strengvariabel, for deretter å la deg velge ut hvilken del av denne teksten du vil skrive tilbake til skjermen. #include int main() { char charArray[81] ; // En hel linje pluss 0-avsluttning. int i, first, count; printf("Tekst: "); scanf("%s", &charArray); // %s er altså for strengvariable printf("Start utskrift fra indeks nummer: "); scanf("%d", &first"); printf("Antall tegn til utskriften: "); scanf("%d", &count); for(i = first ; i < (first + count) ; i++) { putchar(charArray[i]); } putchar('\n'); } Som det framgår av eksempelet ovenfor, kan utskrift fra strenger også foregå tegn for tegn vha. putchar() og andre funksjoner. I dette eksempelet burde man vel også ha testet på om de tegnene som man ønsker å skrive ut ligger innenfor det lovlige område for strengen før man starter utskriften. 11.5 Oppgaver Skriv et program der det deklareres en tabell (int) på 10 elementer som initialeres til følgende sekvens av verdier: 8, 7, 6, 4, 1, 2, 5, 10, 20, 3. Programmet skal skrive ut innholdet av tabellen til skjermen med tabellelement 0 først. Bruk en løkke for å skrive ut tallene. Som oppgave 1, men nå skal tabellen skrives ut i omvendt rekkefølge med siste element først. Som oppgave 1, men skriv ut annethvert tabellelement - 0 - 2 - 4 ... Skriv et program som leser inn 15 heltall fra tastaturet og lagrer dem i en tabell. Når alle tallene er lest inn skal den innleste sekvensen av tall skrives til skjermen. Som oppgave 4, men la programmet lese tall fra tastaturet helt til brukeren taster inn et negativt tall. Som oppgave 5, men skriv ut bare de tallene som ligger mellom 20 og 100. Innlesning av tall som oppgave 5. La så programmet finne antall forekomster av tall mellom 20 og 30 og skrive ut dette antallet og summen av disse tallene. Innlesning av tall som oppgave 5. La så programmet regne ut middelverdien av alle innleste tall og skrive den ut. Innlesning av tall som oppgave 5. La så programmet finne medianen i dette tallsettet. Skriv et program som deklarerer og initialiserer en tabell (char) med en tekst på 80 tegn eller mer. Når brukerer så trykker tasten 'p' (for print), skrives en linje med de 20 første tegnene fra teksten ut til skjermen. En ny 'p' skriver ut de neste 20 tegn osv. Skriv et program som lar brukeren taste inn to tekststrenger, hver på 10 tegn. La så programmet skrive ut resultatet av de to strengene flettet. En flettet streng får man ved å plukke tegn for tegn fra to strenger på en slik måte at første tegn fra begge strengene pukkes først, deretter neste tegn fra begge strengene osv. helt til siste tegn fra begge strengene er skrevet ut. 12 Funksjoner 12.1 Introduksjon En funksjon kan betraktes som et lite miniprogram du kan starte fra hovedprogrammet ditt - main()[13]. Funksjoner kan startes fra andre funksjoner også. Som beskrevet tidligere[14] sier vi at vil kaller en funksjon når vi mener å starte den. Derfor benyttes også betegnelsene kallende funksjon og kalt funksjon når vi referer til den funksjonen som henholdsvis startet den aktuelle funksjonen og denne funksjonen selv. Funksjoner kan lages av programmereren eller man kan benytte ferdiglagde funksjoner i såkalte "biblioteker". Språket C beskriver et sett av slike biblioteksfunksjoner som standard. Et vedlegg til dette kompendiet beskriver kort disse standard biblioteksfunksjonene. Vi har allerede sett på bruk av noen standardfunksjoner: printf(), scanf(), getchar(), putchar() osv. Å lage sine egne funksjoner kalles å definere funksjoner. Denne funksjonsdefinisjonen foregår etter bestemte regler som vi skal se nærmere på i dette kapittelet. Funksjoner i C tilsvarer subrutiner i assembly-programmering. Poenget er altså at man kan hoppe til et sted i programmet. Her utføres en sekvens av setninger (enkle og sammensatte) for så å returnere tilbake til det kallende programmet. Det som er fint med funksjoner er at man alltid vil returnere til det punktet i det kallende programmet der man kalte funksjonen. 12.2 Funksjonskall Et funksjonskall består av funksjonsnavnet etterfulgt av et sett av (runde) parenteser. Inne i disse parentesene angis det en liste med null, ett eller flere funksjonsargumenter. Disse argumentene kan være variable, konstanter eller uttrykk. Det er viktig å merke seg at resultatet av et funksjonskall i C også forbindes med en verdi - en funksjonsverdi - av en gitt datatype, f. eks: x = sqrt (4.0); // sqrt() finner kvadratroten av et tall Verdien til funksjonskallet sqrt(4.0) gir verdien 2.0. Denne verdien tilordnes så i dette tilfellet til variabelen x. c = getchar (); Her vil funksjonsverdien være ascii-verdien til et tegn som leses fra standard input. Et funksjonskall benyttes ofte i forbindelse med tilordningssetninger, men kan også benyttes i andre sammenhenger som f. eks. funksjonsargument til andre funksjoner: x = sqrt(sqrt(16.0)); // sqrt(16.0) ( 4.0, sqrt(4.0) ( 2.0 I dette eksempelet blir resultatet av rottrekkingen av 16.0 benyttet som argument til den ytre sqrt()-funksjonen. Resultatet blir da 4-roten av 16.0 som er 2.0. Funksjonskall som returnerer funksjonsverdier kan benytt i generelle uttrykk der variable og konstanter ellers kan inngå. Funksjonskall kan normalt ikke plasseres på venstre side i en tilordningssetning. Noen funksjoner har ikke noen verdi forbundet med funksjonskallet. Disse funksjonene sies å være av en spesiell datatype - void. Disse funksjonene benyttes der man ønsker at funksjonen skal utføre en spesiell oppgave der det ikke er naturlig å rapportere noen verdi tilbake til det kallende programmet. Et eksempel er funksjonen exit() som avslutter et C-program tvert. Siden man aldri vil returnere til det kallende programmet, er det derfor ikke hensiktsmessig å gi noen verdi til selve funksjonskallet. Funksjonsargumentet i dette tilfellet er en verdi som operativsystemet kan sjekke. exit(0); // normal avslutning En del ofte benyttede funksjoner som egentlig returnerer en funksjonsverdi, benyttes ofte uten å ta hensyn til denne. Et godt eksempel er printf() som lar oss konvertere tall til tekst og skrive ut teksten. Egenlig returnerer denne funksjonen en verdi som viser hvor mange tallformateringer som er utført, men dette benyttes sjelden: printf("X = %f\n", x); // 1 konvertering utføres Ønsker vi ikke å ta vare på funksjonsverdien, kan denne altså ignoreres som i eksempelet ovenfor. Ønsker vi å fortelle kompilatoren og andre lesere av programmet (og kanskje også oss selv) at vi er klar over at printf() egentlig returnerer en verdi som vi ønsker å ignorere, kan vi benytte typekonvertering på funksjonskallet: (void) printf("X = %f\n", x); // 1 konvertering utføres 12.3 Funksjonsdefinisjon Vi skal nå flytte fokus fra programmet som kaller funksjonen til funksjonen selv. Hva består denne av, og hvordan defineres den? En funksjon har 5 hovedegenskaper: Datatypen til funksjonsverdien (retur-datatypen). Funksjonsnavnet som benyttes ved kall av funksjonen. For at datamaskinen skal vite hvilken funksjon som skal kalles, må den referer til et funksjonsnavn. Reglene for valg av funksjonsnavn er de samme som for variable. Funksjonsargumentene er en liste med verdier som det kallende programmet benytter seg av for å overføre informasjon til selve funksjonen. Disse verdien kalles funksjonsargumenter. Funksjonsargumentene omsluttes av parenteser - (). Selve funksjonskroppen som inneholder de aktive setningene i programmet samt deklarasjon av eventuelle ekstra variable som trengs inne i selve funksjonen. Funksjonskroppen er avgrenset av et sett klammeparenteser - {}. Overføringen av funksjonsverdien fra funksjonen til det kallende programmet. Et eksempel på en funksjonsdefinisjon kan være: Her er altså kvadrat funksjonsnavnet. int er retur-datatypen. Funksjonsargumentet er a - også av typen int. Siden retur-datatypen er int, må også variabelen, b, som i funksjonen inneholder verdien som skal returneres som funksjonsverdi, være av typen int. Selve funksjonskroppen multipliserer verdien til variabelen som er funksjonsargument med seg selv. Dette gir kvadratet av a som resultat som igjen lagres i variabelen b. Det er returnsetningen som gir funksjonen sin funksjonsverdi (retur-verdi). Siden argumentet til return i dette tilfellet er b, blir altså funksjonverdien kvadratet til a. 12.3.1 Argumentverdier Hvordan får så funksjonsargumentet a sin verdi i eksempelet ovenfor? For å svare rimelig klart på dette må vi se på hvordan denne funksjonen kunne vært brukt i et tenkt program: i = 3; k = kvadrat(i); m = kvadrat(5); printf ("I = %d, K = %d, M = %d", i, k, m); Variabelen i gis her verdien 3. Variabelen i benyttes også på plassen til funksjonsargumentet a i det første kallet til kvadrat(). Funksjonen startes opp. Argumentet a får nå overført verdien til i i det kallende programmet (altså 3). Inne i funksjonen beregnes kvadratet av 3 (som blir 9) og tilordnes b. Verdien9 returneres derfor som funksjonsverdi. Funksjonsverdien tilordnes variabelen k i det kallede programmet. I neste funksjonskall overføres verdien av konstanten 5 fra det kallende programmet til funksjonsargumentet a inne i funksjonen. Funksjonverdien beregnes denne gangen til 25, som returneres og overføres til m i tilordningen. Verdiene som skrives ut blir altså 3, 9 og 25. 12.3.2 Call by value Prinsippet som alltid følges i et C-program ved argumentoverføring, er at verdiene til de variable, konstanter eller uttrykk som plasseres på plassen til et funksjonsargument i funksjonskallet, kopieres til de respektive funksjonsargumenter inne i funksjonen. På denne måten vil aldri funksjonen kunne forandre på de variable som benyttes i argumentlista til en funksjon i det kallende programmet. Se følgende eksempel: int halver (int x) { x = x / 2; return x; } I dette eksemplet halveres verdien til funksjonsargumentet x i selve funksjonen før verdien returneres som funksjonsverdi. Anta nå at denne funksjonen kalles et annet sted i programmet slik: n = 8; m = halver (n); printf ("%d %d", n, m); Spørsmålet er da, vil verdien til n halveres siden funksjonsargumentet x halveres inne i funksjonen? Svaret er nei! Siden funksjonsargumentet alltid kun får overført en kopi av verdien fra det kallende programmet er altså funksjonsargumentet x og n to atskilte variable i programmet. Verdiene 8 og 4 skrives derfor ut fra printf(). 12.3.3 Lokale variable Variable som funksjonen trenger, men som ikke resten av programmet må kjenne til, deklareres inne i selve funksjonen. Disse variablene kalles lokale variable og kan ikke brukes av andre funksjoner eller main(). Lokale variable er altså helt private for funksjonen. De kan derfor ha samme navn som variable i andre funksjoner uten at det oppstår konflikt av den grunn. Lokale variable i en funksjon må alltid deklareres øverst i funksjonskroppen, før noen aktive setninger. Lokale variable opprettes når funksjonen kalles og eksisterer bare når funksjonen utføreres. Etter at funksjonen er avsluttet, eksisterer ikke disse variablene lenger. Et annet navn på variable av denne typen er automatiske variable[15]. Funksjonsargumentene kan oppfattes som lokale variable i funksjonen. Når en funksjon kalles flere ganger i det samme programmet. Opprettes det et nytt sett av lokale variable hver gang. 12.3.4 Globale variable Globale variable er variable som ikke er deklarert som lokale variable i noen funksjon i programmet, men utenfor alle funksjoner. Typisk deklareres globale variable i starten av programmet - før main(). Globale variable kan benyttes og forandres av alle funksjonene som inngår i programmet og som er definert etter de globale variablene er definert. Det anses som dårlig programmeringspraksis å benytte mange globale variable i et program. Grunnen til dette er bl. a. at man lett mister oversikten ved at mange forskjellige funksjoner har anledning til å forandre på innholdet i disse variablene. Det anbefales derfor å benytte lokale variable og utveksle informasjone mellom de forskjellige funksjonene ved hjelp av funksjonsargumenter og funksjonsverdier (retur- verdier). I motsetning til lokale automatiske variable vil globale[16] variable eksistere så fra programmet startes til det avsluttes. 12.4 Funksjonsprototyp I eksemplet med funksjonen kvadrat() ser den første linja slik ut: int kvadrat (int a) Denne linja kalles funksjonshodet. Når man skriver et program som benytter funksjoner, vil kompilatoren trenge informasjonen fra funksjonshodet til de funksjonene som kalles for å kunne sjekke om antall argumenter er riktig, og om datatypen til funksjonsverdien er riktig brukt i funksjonskallet. Kompilatoren leser programfilen fra øverst til nederst. Det er derfor ikke noe problem med å få tilgang til funksjonshode-informasjonen for de funksjonene som er definert før de kalles. For de funksjonene som er definert etter at de er benyttet er det derimot verre. I C løses dette ved noe som betegnes funksjonsprototyper. Funksjonsprototypen til en funksjon er ikke noe annet enn funksjonshodet til funksjonen etterfulgt av semikolon slik: int kvadrat (int a); // Dette er funksjonsprototypen Funksjonsprototypene til alle funksjonene som inngår i programmet samles som regel i begynnelsen av programfilen. Alternativt plasseres prototypene i en headerfil som inkluderes i starten av programfilen. Funksjonsprototypene er altså kun til hjelp for at kompilatoren skal kunne sample viktig informasjon om de forskjellige funksjonene som benyttes i programmet. Denne informasjonen benyttes bl. a. til å gi feilmeldinger ved kompilering hvis funksjonen benyttes feil, f. eks. for mange funksjonsargumenter, eller feil datatype i funksjonsargumentene. Programeksempel: #include void printAverage(int x, int y, int z); // function prototype int main() { int a, b, c; // Local in main printf("Enter 3 integers separated by spaces: "); scanf("%d %d %d", &a, &b, &c); printAverage(a, b, c); // the function call return 0; // exit main function } void printAverage(int x, int y, int z) // function definition { float average = (float) (x + y + z) / 3; printf("The average value of %d, %d and %d is %f\n", x, y, z, average); } Det er alminnelig praksis å plassere funksjonsdefinisjonene under main() i C- programmet. Legg merke til at funksjonstypen til printAverage() er void, siden det ikke returneres noen verdi fra denne funksjonen. Fra en void-funksjon er det ikke nødvendig med noen returnsetning. Funksjonen vil avsluttes når siste setning i funksjonskroppen er utført. Hvis man ønsker av funksjonen skal returnere før dette, kan man sette inn en returnsetning uten returverdi. 12.5 Funksjoner som kaller andre funksjoner #include int triangular(int m); // funksjonsprototyp void print_triangular(int n); // funksjonsprototyp int get_number(void); // funksjonsprototyp int main() { int x; x = get_number(); // funksjonskall print_triangular(x); // funksjonskall return 0; } void print_triangular(int n) // funksjonsdefinisjon { int ones, t; ones = n % 10; t = triangular(n); // funksjonskall if(ones > 3 || n == 11 || n == 12 || n == 13) printf("\n%d is the %dth triangular number\n", t, n); else { if (ones == 1) printf("\n%d is the %dst triangular number\n", t, n); else if (ones == 2) printf("\n%d is the %dnd triangular number\n", t, n); else if (ones == 3) printf("\n%d is the %drd triangular number\n", t, n); } } int triangular(int m) // funksjonsdefinisjon { int x; x = (m * (m + 1)) / 2; // 1+2+3+ ... +m return x; } int get_number(void) // funksjonsdefinisjon { int numb; printf("Enter an integer: "); scanf("%d", &numb); return numb; } 12.6 Funksjoner som kaller seg selv - rekursive funksjoner #include int factorial(int x); int main() { int a, b, temp; // Compute the factorial with a recursive function printf("Enter an integer: "); scanf("%d", &a); printf("\n%d factorial is %d\n\n", a, factorial(a)); // Do the same with a loop printf("Enter another integer: "); scanf("%d", &a); temp = a; // remember original a for(b = 1; a > 0 ; a--) { b *= a; } printf("\n%d factorial is %d\n", temp, b); return 0; } int factorial(int x) // n factorial, (or n!) is 1*2*3* ... *n { if(x > 0) { return (x * factorial(x-1)); } return 1; } Rekursive funksjoner kan gi svært elegante løsninger på en del vanskelige algoritmer som ellers ville medført bruk av løkkekonstruksjoner. Dessverre har rekursive funksjoner en tendens til å gi lite effektive programmer ved at de både blir lagsommere og krever mer lagringsplass enn tilsvarende løkkeløsninger. Et annet mulig problem er at man kan komme ut i situasjoner der rekursjonen aldri stopper på grunn av programmeringsfeil. Rekursive funksjoner bør derfor brukes med varsomhet. 12.7 Oppgaver Skriv en C-funksjon med følgende funksjonsprototyp: float hypo(float kat1, float kat2); Funksjonen skal beregne og returnere lengden av hypotenusen i en trekant med katetene gitt som funksjonsargumenter. Bruk standardfunksjonen sqrt() i løsningen. Skriv en C-funksjon med følgende funksjonsprototyp: float areal_trekant(float kat1, float kat2); Funksjonen skal beregne og returnere arealet av en trekant med katetene gitt som funksjonsargumenter. Skriv en C-funksjon med følgende funksjonsprototyp: float radius(float areal); Funksjonen skal beregne radius i en sirkel basert på at arealet er gitt som funksjonsargument. Skriv en C-funksjon med følgende funksjonsprototyp: int posisjon(char c, char s[]); Funksjonen skal finne og returnere posisjonsnummeret til et vilkårlig tegn, c, i tekststrengen, s. C og s er funksjonsargumenter. Lag to varianter av funksjonen: Forutsett at tegnet c finnes i strengen s. Sjekk også om tegnet c inngår i strengen, hvis ikke skal verdien -1 returneres. NB! Husk at alle tekststrenger i C vil være avsluttet av et tegn med verdien 0 / '\0' (zero-terminated string). Skriv en C-funksjon med følgende funksjonsprototyp: void print_store(char s[]); Funksjonen skal skrive ut en tekststreng, s, til skjermen. Selv om s inneholder små bokstaver eller en blanding av små og store bokstaver, skal kun store bokstaver skrives til skjermen. Lag tre varianter av funksjonen: Forutsett at alle tegnene i strengen hører med til det engelske alfabetet a - z. Bokstavene har koder løper etter hverandre fra 'a' = 0x61 til 'z' = 0x7a og fra 'A' = 0x41 til 'Z' = 0x5a. Det er altså en konstant avstand = 0x20 mellom små og store bokstaver i det engelske alfabetet. Strengen kan også inneholde "norske" tegn (Æ = 0x92, Ø = 0x9d, Å = 0x8f, æ = 0x91, ø = 0x9b, å = 0x86). Strengen kan inneholde alle tegn, men bare bokstaver i alfabetet skal eventuelt konverteres til store bokstaver. Skriv en C-funksjon med følgende funksjonsprototyp: int mean(int tab[], int n); Funksjonen skal finne og returnere middelverdien til elementene i en tabell med n elementer. Skriv en C-funksjon med følgende funksjonsprototyp: int median(int tab[], int n); Funksjonen skal finne å returnere medianen til elementene i en tabell med n elementer. Medianen er verdien til det elementet som har like mange elementer som er større enn seg selv som elementer som er mindre enn seg selv. (For enkelhets skyld: Anta at alle elementene har forskjellig verdi og at tabellen inneholder et odde antall elementer). Lag et program som leser inn et areal på en sirkel fra tastaturet. Deretter skal programmet beregne radius i sirkelen og skrive ut denne til skjermen. Bruk blant annet funksjonen fra oppgave 3 i løsningen. Lag et program som beregner areal og hypotenus i en trekant. Lengden på de to katene leses inn fra tastaturet. Resultatene skrives til skjerm. Bruk blant annet funksjonene fra oppgavene 1 og 2 i løsningen. Les inn en streng fra tastaturet. La et program finne ut om denne strengen inneholder tegnet 'p' og i tilfellet på hvilken posisjon i strengen dette tegnet forekommer. Skriv passende tekst til skjermen som rapporterer resultatet. Bruk blant annet funksjonen fra oppgave 4 i løsningen. Hint: En tekststreng leses inn vha av formatspesifikatoren %s i scanf(). F. eks. slik: char streng[21]; scanf("%s", &streng); 13 Mer om funksjoner Funksjoner er lette å bruke; de gjør det mulig å bryte opp store kompliserte programmer i mindre og enklere moduler. Hver av disse modulene eller funksjonene vil være enklere å skrive, lese og vedlikeholde. Faktisk vil også totalprogrammet som er bygget opp ved hjelp av disse funksjonene, være vesentlig enklere å håndtere en et enhetlig, stort program. Vi har allerede støtt på funksjonen main() og benyttet I/O-funksjoner fra standardbiblioteket til C. I dette kapittelet skal vi studere hvordan vi skriver våre egne, hjemmelagde funksjoner. Obs! En del av stoffet i dette kapittelet overlapper med tilsvarende stoff i forrige kapittel. 13.1 Bruk av standard headerfiler. En funksjonskall i C består i all enkelhet i å skrive navnet til funksjonen etterfulgt av funksjonsargumentene i parenteser. C-kompilatoren sjekker at det er samsvar mellom argumentene i funksjonskallet og argumentene som er spesifisert i funksjonsdefinisjonen (og eventuelt funksjonsprototypen). Biblioteksfunksjoner er normalt ikke tilgjengelige på kildeform. Sjekkingen av argumenttypene og antallet argumenter baseres derfor utelukkende på funksjonsprototyper angitt i standard headerfiler (som stdio.h) som inneholder all nødvendig informasjon. For eksempel inneholder headerfilen math.h alle funksjonsprototypene for det matematiske standardbiblioteket. For å kunne funksjoner i dette biblioteket, må altså headerfilen inkluderes på følgende måte i starten av C-filen: #include < math.h> Det er viktig å merke seg (og dette kan ikke gjentas for ofte) at det ikke er selve funksjonsdefinisjonene som befinner seg i headerfilene, kun funksjonsprototypene samt noen andre tilsvarende definisjoner av symbolske konstanter etc. Selve funksjonen er ferdig kompilert og befinner seg i de binære biblioteksmodulene som maskininstruksjoner. De nødvendige funksjonene linkes inn i det endelige programmet ved behov for å danne en komplett, kjørbar programfil (.exe-fil). Selve linkprosessen foregår ganske automatisk i programutviklingssystemer som f. eks. Visual C++ når vi utsteder kommandoen Bygg Prosjekt. 13.1.1 De mest brukte headerfilene < stdio.h> ( defining I/O routines < ctype.h> ( defining character manipulation routines < string.h> ( defining string manipulation routines < math.h> ( defining mathematical routines < stdlib.h> ( defining number conversion, storage allocation and similar tasks < stdarg.h> ( defining libraries to handle routines with variable numbers of arguments < time.h> ( defining time-manipulation routines I tillegg finnes følgende headerfiler: < assert.h> ( defining diagnostic routines < setjmp.h> ( defining non-local function calls < signal.h> ( defining signal handlers < limits.h> ( definning constants of the int typ < float.h> ( defining constants of the float type Appendix B i Kernighan & Richie boken beskriver disse bibliotekene. Dette dokumentet er vedlegg til dette kompendiet. 13.2 Egendefinerte funksjoner. En funksjon har følgende oppsett: retur-datatype funksjonsnavn ( argumentliste-hvis-aktuelt ) { ...lokale variabeldeklarasjoner etc. ... ...C-setninger... return returverdi; } Hvis angivelse av retur-datatype utelates, antas int som standard datatype av C- kompilatoren. Returverdi må være av samme datatype som retur-datatype. En funksjon kan jo også ganske enkelt utføre en oppgave uten at det er aktuelt å returnere noen verdi. I dette tilfellet blir oppsettet til funksjonen: void funksjonsnavn ( argumentliste-hvis-aktuelt ) { ...lokale variabeldeklarasjoner etc. ... ...C-setninger... return; } eller void funksjonsnavn ( argumentliste-hvis-aktuelt ) { ...lokale variabeldeklarasjoner etc. ... ...C-setninger... } Se følgende eksempel: /* inkluder headerfiler for alle standard biblioteksfunksjoner som skal benyttes i programmet */ #include #include /* angi funksjonsprototyper for egendefinerte funksjoner */ int n_char(char string[]); void main() { int n; char string[50]; /* strcpy(a,b) kopierer streng b til streng a funksjonsprototype fra string.h headerfil */ strcpy(string, "Hello World"); /* kall av egendefinert funksjon */ n = n_char(string); printf("Length of string = %d\n", n); } /* definisjon av lokal funksjon n_char */ int n_char(char string[]) { int n; // Lokal variabel /* strlen(a) returns the length of string a */ n = strlen(string); // fra string.h if (n > 50) printf("String is longer than 50 characters\n"); /* returner lengden av strengen */ return n; } 13.3 Argumentoverføring Et C-program overfører alltid en kopi av de aktuelle argumentene i funksjonskallet over til de lokale argumentene i funksjonen. Dette kalles på fagspråket "call by value". Dette betyr at alle forandringer som gjøres med funksjonsargumenter utføres på kopier av argumentene som ble benyttet i funksjonskallet og vil derfor også bare få intern gyldighet. Dette er et greit og enkelt prinsipp: en funksjon kan ikke tøyse med variable i den kallende funksjonen. Men dette medfører også en begrensning, hvis det er nettopp det vi ønsker - å lage en funksjon som skal forandre på funksjonsargumentene. Noen språk som JAVA, C++ og PASCAL løser dette ved å ha en alternativ måte for å overføre argumenter som kalles "call by reference". I C må vi omgå begrensningen ved å overføre adressene til de argumentene vi eventuelt ønsker å forandre i stedet for variablene selv. La oss se på et eksempel der vi ønsker å lage en funksjon som bytter om verdiene til to variable. Studer eksempelet nedenfor som ikke vil gi ønsket resultat fordi argumentene overføres "by value": #include void exchange(int x, int y); // Function prototype void main() { /* WRONG CODE */ int a = 5, b = 7; printf("From main: a = %d, b = %d\n", a, b); exchange(a, b); // Function call printf("Back in main: a = %d, b = %d\n", a, b); } void exchange(int x, int y) // Function definition { int temp; temp = x; x = y; y = temp; printf(" From function exchange: x = %d, y = %d\n", x, y); } Kompiler og kjør dette eksempelet. Merk at a og b ikke har byttet verdi! Bare kopiene av argumentene lokalt i funksjonen er byttet om. Den riktige måten å løse dette problemet på er ved bruk av pekere. Selve temaet pekere skal diskuteres i et seinere kapittel, men vi skal som en forsmak se på et eksempel. 13.4 Grunnkurs pekere. Vanlige variable har sin bestemte adresse i datamaskinens hukommelse. Vi er som regel ikke interessert i å vite denne adressen. Det er bra nok for oss at kompilatoren holder styr på dette for oss. Dette er jo nettopp litt av hensikten med høynivåprogrammering relatert til assemblyprogrammering. Noen ganger ønsker vi likevel å hente ut adressen til en variabel. Dette gjøres i C ved å benytte operatoren & som benyttet i denne sammenhengen er en adresseoperator. En adresse til hukommelsen kan være så enkel som et 16 bits heltall (unsigned int). I andre tilfeller (andre CPU-er) kan adresse bestå av to eller flere elementer. Som regel tar C-kompilatoren seg også av dette, slik at programmereren slipper å tenke på hvordan CPU-en adresserer hukommelsen. For å skjule forskjeller i adresseringen, opereres det imidlertid med egne datatyper i C for å kunne angi adresseverdier. Disse datatypene kalles pekere. Regel: En peker er dataobjekt som angir en adresse til hukommelsen. En peker kan gjerne angi adressen til en variabel eller et annet dataobjekt. Vi har benyttet adresseoperatoren & tidligere, i forbindelse med argumentene som skal gis verdier i et kall til scanf(). Uttrykket: &x finner altså adressen til variabelen x som en peker. Pekere kan også lagres i spesielle pekervariable. For å definere en pekervariabel som kan lagre en peker, skal vi angi * foran variabelnavnet slik: int *p; // Definisjon av pekervariabel som // -- kan lagre en adresse til en int-variabel. Merk at pekerverdier ikke kan lagres i vanlige variable. Merk også at det finnes en peker-datatype for hver vanlig datatype. Vi kan altså ikke blande adressene til f. eks. int og char. Vi kan hente adressen til en variabel x og la en pekervariabel p peke til denne variabelen. int x, y; int *p; --- --- p = &x; --- Hva skal vi så med adressen til en variabel? Det kan diskuteres; språket JAVA benytter for eksempel ikke pekere, de anses for å være farlige å bruke. I C, derimot, benyttes pekere ganske ofte for å realisere en form for indirekte adressering. Vi kan nå nemlig manipulere på verdien til x gjennom pekeren p slik: --- *p = 4; // Variabelen p peker til gis verdien 4. y = *p + 1; // Variabelen y gis verdien summen av x og 1, // -- altså 5. --- Operatoren * brukes her for å angi indireksjon - som er å få tilgang til variabelen som pekeren peker til. Dette får være nok om pekere så langt. Så til eksempelet. #include void exchange ( int *x, int *y ); // Function prototype void main() { /* RIGHT CODE */ int a = 5, b = 7; printf("From main: a = %d, b = %d\n", a, b); exchange(&a, &b); // Function call printf("Back in main: a = %d, b = %d\n", a, b); } void exchange ( int *x, int *y ) // Function definition { int temp; temp = *x; *x = *y; *y = temp; printf(" From function exchange: x = %d, y = %d\n", *x, *y); } I funksjonsdefinisjonen benyttes pekere som argumenter i stedet for vanlige variable. Det medfører at vi inne i funksjonen må angi indireksjon når vi benytter disse variablene. Vi henter altså verdiene til de variablene som pekerne peker på og bytter om disse verdiene. Selve funksjonsargumentene forandrer ikke verdi, pekerne vil peke til de samme variablene gjennom hele funksjonen. Det er innholdet av disse indirekte adresserte variablene som forandres. I hovedprogrammet må vi også forandre funksjonskallet litt fra tidligere. Vi kan ikke benytte a og b som funksjonsargumenter fordi funksjonen forventer pekere som argumenter. Vi lager pekere til a og b ved å benytte adresseoperatoren &. Det er altså adressene til a og b som kopieres inn i funksjonsargumentene og ikke verdien til variablene selv. PÅ denne måten for funksjonen tilgang til adressene til variable som er lokale i main(). Tommelfingerregel: Benytt vanlige variable som funksjonsargumenter hvis du ikke ønsker at funksjonen skal forandre verdien til funksjonsargumentene. Benytt pekere når du ønsker at funksjonen skal forandre verdien til argumentene. 14 Flervalgssetningen I mange sammenhenger i programmering er det behov for å velge mellom mange forskjellige valgalternativer. I kapittelet om betingede valg studerte vi if-else setningen og så at denne kunne benyttes til dette formålet hvis vi innførte nestede if-else-if steninger. I dette kapittelet skal vi se på en ny kontrollsetning - switch - som er spesialkonstruert for å kunne velge mellom flere alternativer. Alternativene det testes på ved bruk av switch må være heltall. Dette er en vesentlig begrensning som gjør at hvis man f. eks. skal velge mellom flere verdiområder som en float-variabel kan innta, kan vi ikke benytte switch, men gå tilbake til if-else-if strukturer. If-else-if strukturer kan altså alltid benyttes der man ønsker flervalg (selv om det kan se litt uelegant ut), switch kan bare benyttes i forbindelse med heltall. Et flytskjema for switch-strukturen er vist nedenfor. Inne i romben står teksten valgvariabel for et heltalls variabelnavn eller et heltallsuttrykk som kan innta flere alternative verdier. Avhengig av disse alternativene utføres så et sett av setninger før strukturen avsluttes. Hvis ingen av de angitte alternativene er aktuelle, utføres ingenting. I et C-program ser switch-strukturen slik ut: switch () { case : break; case : break; case : break; .. .. case : break; } Legg merke til at valgvariabelen kommer i en parentes rett etter nøkkelordet switch, hvert alternativ skrives etter nøkkelordet case. Før neste valalternativ avsluttes setningene med en break setning. Switch - case - break hører altså sammen. Eksempel: Anta at vi leser informasjon fra en salgsautomat om hvilken mynt som er sluppet på. Det er myntene 1kr (avlesningsverdi 1), 5kr (avlesningsverdi 2) og 10kr (avlesningsverdi 2) som er aktuelle. En kodesnutt som leser inn myntverdien og legger riktig beløp til en sum er vist nedenfor: scanf("%i", &mynt); // les inn verdi til variabelen mynt switch (mynt) // test verdien av mynt { case 1: // hvis mynt == 1 sum = sum + 1; printf("Legg på neste mynt!"); break; case 2: // hvis mynt == 2 sum = sum + 5; printf("Legg på neste mynt!"); break; case 3: // hvis mynt == 3 sum = sum + 10; printf("Legg på neste mynt!"); break; } Hvis det slippes på en annen mynt en de tre alternativene skjer det ingenting. Det hadde vært bedre om det i dette tilfelle ble gitt en feilmelding. Dette får vi til ved å bruke et standardvalg som utføres hvis ingen av de andre opplistede valgene (etter case) slår til. Et slik standardvalg signaliseres ved bruk av nøkkelordet default. Standardvalget angis etter alle de endre valgene - slik: switch () { case : break; case : break; case : break; .. .. default: break; } En flytskjemafigur for dette ser slik ut: Eksempelet ovenfor kan modifiseres til å ta høyde for feil myntpåslipp: scanf("%i", &mynt); switch (mynt) { case 1: sum = sum + 1; printf("Legg på neste mynt!"); break; case 2: sum = sum + 5; printf("Legg på neste mynt!"); break; case 3: sum = sum + 10; printf("Legg på neste mynt!"); break; default: // hvis mynt ikke har verdien 1, 2 eller 3 printf("Feil mynt - forsøk igjen!"); } Fra eksempelet ser du at det ikke er nødvendig å ha med break etter default, men det ville ikke være feil om vi hadde gjort det heller. Det er mulig å teste på flere enn en valgmulighet om gangen ved å liste opp flere case- uttrykk etter hverandre. Et eksempel på dette får vi hvis vi har en variabel mnd som inneholder nummeret på en aktuell måned (1 - 12). Vi ønsker å kategorisere månedene i vår, høst, sommer og vinter: switch (mynt) { case 1: case 2: case 3: case 12: printf("Vinter"); break; case 4: case 5: printf("Vår"); break; case 6: case 7: printf("Sommer"); break; default: printf("Høst"); } Hvis det er mange alternativer er det i utgangspunktet tungvindt å benytte switch fordi hvert eneste alternativ må listes opp (bortsett fra standardalternativet). I noen tilfeller kan man imidlertid omforme mange alternativer til noen få på en enkel måte. Neste eksempel viser et tabell over hvordan rentene for et lån avhenger av lånebeløpet. Deretter vises et program som beregner lånerenten ut fra et vilkårlig beløp som skrives inn. 00000 <= lån < 10000 10% 10000 <= lån < 20000 10.5% 20000 <= lån < 30000 11% 30000 <= lån < 40000 11.5% 40000 <= lån 12% #include void main () { int sum, titusener; float rente; printf("Tast inn summen: "); scanf("%d", &sum); titusener = sum % 10000; switch (titusener) { case 0: rente = 10; break; case 1: rente = 10.5; break; case 2: rente = 11; break; case 3: rente = 11.5; break; default: rente = 12; break; } printf("Renten vil bli %.1f %%\n", rente); // %% ( % i utskrift } 15 Flerdimensjonale tabeller Til nå har vi kun sett på eksempler med endimensjonale tabeller - dvs. tabeller med én tabellindeks. En tabells dimensjon er definert som antall indekser som trengs for å referere til et spesielt element i tabellen. For eksempel vil man trenge to indekser (x og y) for å referere til en gitt koordinat i et x/y-koordinatsystem. I C er det muligheter det mulighet for tabeller med 2, 3 og flere dimensjoner. Ved å inspisere en tabellreferanse kan vi finne hvilken dimensjon tabellen har, ved å telle antall par med hakeparenteser med indekser som benyttes i referansen. I en endimensjonal tabell, begynner alltid indekseringen av første tabellelement med 0; arrayOfInts[0] er altså det første elementet i tabellen arrayOfInts. Indeksen er 0 - det er kun én indeks, slik at tabellen er endimensjonal. 15.1 Referanser til elementer i flerdimensjonale tabeller Anta at vi ønsker en todimensjonal tabell som inneholder karakterene for en klasse for en gitt prøve. Det er ikke bare totalkarakteren, men også delkarakterene for hver oppgave som skal lagres. For hver besvarelse er det altså flere resultater som må lagres. Tabellen navn er karakterer, og første indeks angir nummeret på besvarelsen. Indeks nummer to angir oppgavenummeret (indeks = 0 - betyr totalkarakter). For å referere til karakteren på oppgave 5 i besvarelse 3, kan vi skrive: karakterer[3][5]. På tilsvarende måte refererer karakterer[15][4] til oppgave 4 for besvarelse 15 og karakterer[7][0] til totalkarakteren for besvarelse 7. Første indeks plasseres altså i den første hakeparentesen, indeks nummer to plasseres i neste hakeparentes. Merk at vi ikke har lov til å plassere flere enn én indeks i hvert par med hakeparenteser. En tredimensjonal tabell, felt, kan f. eks. benyttes til å angi verdien til det elektriske feltet i et gitt tredimensjonalt (x / y / z) rom. Verdien til feltet ved koordinatene x = 3, y = 12 og z = 7 angis slik: felt[3][12][7]. I de fleste tilfellene greier man seg med opp til tredimensjonale tabeller. Det er imidlertid ingenting i veien for å operere med flere enn 3 dimensjoner. Det er bare å utvide med antall indekser. 15.2 Deklarasjoner av flerdimensjonale tabeller Det burde nå være kjent hvordan en endimensjonal tabell deklareres, men hva med todimensjonale og 3 dimensjonale tabeller? Det er ganske enkelt, det er bare å føre opp hvor mange elementer man ønsker å dimensjonere for i hver dimensjon. La oss se på hvordan tabellene karakterer og felt fra eksemplene ovenfor kan dimensjoneres, hvis det antas at begge tabellenes elementer er av typen float: float karakterer[30][7]; // 30 besvarelser, 6 oppgaver + totalkarakter Dette forteller datamaskinen å reservere plass til en tabell med 30 x 7 = 210 floatelementer. En måte å tenke på organiseringen av denne tabellen er at den består av en matrise med 30 rader (0 - 20) og 7 kolonner (0 - 6). I praktisk bruk kunne man tenke seg å betrakte siste indeks som radene i matrisen, men teknisk sett er det mest riktig å betrakte første indeks rom radindeksen. float felt[10][20][10]; // x < 10, y < 20, z < 10 Dette forteller datamaskinen å reservere plass til en tabell med 10 x 20 x 10 = 2000 floatelementer. 15.2.1 Flerdimensjonale tabeller som funksjonsargument Når en flerdimensjonal tabell er et funksjonsargument, er kompilatoren avhengig av å vite formatet på tabellen - dvs. hvor mange elementer den består av i hver dimensjon. Unntaket fra dette er første tabelldimensjon, som kan være tom. Det betyr at totalt elementer i en tabell som overføres til en funksjon kan variere. Årsaken til dette er at det ikke avsettes plass til hele tabellen når den overføres som et funksjonsargument, men bare plass til en adresse som peker til starten på tabellen. For at funksjonen skal kunne vite hvor mange elementer det totalt sett er i tabellen, må dette overføres som et eget funksjonsargument eller signaliseres på en annen måte - f. eks. med å sette inn et ugyldig tabellelement som angir slutten på tabellen. Jamfør null-terminerte tekststrenger. Eksempel: Det skal benyttes en todimensjonal tabell som funksjonsargument i funksjonen test_arr(). Dette kan uttrykkes slik: void test_arr(int array3D[][4][5], int max_x, int max_y, int max_z) { int x, y, z; int sum = 0; // finner summen av alle elementene for (x = 0; x < max_x; x++) for (y = 0; y < max_y; y++) for (z = 0; z < max_z; z++) sum = sum + array3D[x][y][z]; printf("Summen er %d\n", sum); } I eksemplet ovenfor kan ikke max_y og max_z gis større verdier enn henholdvis 3 og 4. Verdien til max_x er vilkårlig. I programmet som kaller denne funksjonen, kan f. eks. en tredimensjonal tabell defineres slik: int tab3D[10][4][5]; Når programmet kaller opp testfunksjonen, kan det se slik ut: tetst_arr(tab3D, 10, 4, 5); I funksjonshodet kan altså en tabell angis uten første dimensjon. Når man ønsker å få avsatt plass til en tabell i minnet, må man alltid angi alle dimensjoner. Dette kan gjøres direkte, som i eksemplet ovenfor, eller indirekte vi tabellinitialisering som vist i neste avsnitt. 15.3 Initialisering av todimensjonale tabeller Metodene som benyttes ligner de som benyttes for endimensjonale tabeller. Eksempler: int first[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; Her initialiseres tabellen som den skulle vært en endimensjonal tabell. Poenget med denne initialiseringen er at kolonneindeksen løper raskest under initialiseringen, slik at rad for rad initialiseres slik: 1. rad (0, 1, 2, 3), 2. rad (4, 5, 6, 7), 3. rad (8, 9, 10, 11). int second[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; Denne initialiseringen er identisk med foregående eksempel, men er ryddigere satt opp. Resultatet er altså det samme. Denne stilen anbefales. int third[3][4] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }}; Dette er også en variant av foregående eksempel. Her er det benyttet klammeparenteser for å skille radene. Resultatet er fortsatt det samme. int fourth [][5] = {0,1,2,3,4}; Her er ikke første dimensjon i deklarasjonen angitt. Det betyr at informasjon om størrelsen på tabellen må trekkes ut av antall tabellelementer i initialiseringen. Siden det kun er angitt 5 elementer, betyr det at første dimensjon kun kan ha lovlig indeks 0. int fifth[][6] = {0,1,2,3,4,5,6,7,8,9,10,11}; Variant av forrige eksempel, men her settes det av plass til 12 elementer. Siden hver rad er på 6 elementer, betyr det at det total blir 2 rader (med indekser 0 og 1). int sixth[2][3] = {0, 1, 2}; Her settes det av plass til 6 elementer, men det spesifiseres initialisering av bare 3 av dem. De resterende elementene initialiseres da automatisk til 0 - uavhengig om tabellen er deklarert som global evt. static variabel, eller som lokal variabel i en funksjon. 15.4 Tabellstørrelser Hvis du i programmet ønsker å finne ut hvor mye plass (antall bytes) som er avsatt til en tabell, kan du benytte sizeof-operatoren på denne måten: #include int main() { char arrayChar[] = {'A','r','r','a','y','\0'}; int arrayInt[5] = {1,2,4,8,16}; float arrayFloat[3] = { 1.24 , 2 , 4.68756 }; double arrayDouble[2]; int arrayInt2D[][4] = {1,6,3,7, 0,3,8,9, 2,5,2,3}; arrayDouble[0] = 23.23456532; arrayDouble[1] = 2.3422267; printf("The size of arrayChar is %d\n", sizeof(arrayChar)); printf("The size of arrayInt is %d\n", sizeof(arrayInt)); printf("The size of arrayFloat is %d\n", sizeof(arrayFloat)); /* Alternative way */ printf("The size of arrayDouble is %d\n", sizeof(double) * 2); printf("The size of arrayInt2D is %d\n", sizeof(arrayInt2D)); printf("The size of arrayInt2D[0] is %d\n", sizeof(arrayInt2D[0])); return 0; } Output: The size of arrayChar is 6 The size of arrayInt is 20 The size of arrayFloat is 12 The size of arrayDouble is 16 The size of arrayInt2D is 48 The size of arrayInt2D[0] is 16 Dy kan benytte sizeof for å finne ut hvor mange rader det er i en todimensjonal tabell. For eksempel, hvor mange rader er det i arrayInt2D[][4]? Dette er gitt av: sizeof(arrayInt2D) / sizeof(arrayInt2D[0]) som er 48/16 = 3. Tenk på beregningen som "størrelsen av hele tabellen" / "størrelsen av en rad". Det er altså 3 rader og 4 kolonner i denne tabellen. 15.4.1 Organisering av tabeller i minnet Dette snakket om rader og kolonner må ikke forvirre deg når det gjelder å tenke på hvordan data lagres fysisk i minnet: int arrayInt2D[][4] = {1,6,3,7, 0,3,8,9, 2,5,2,3}; Her benyttes 12 påfølgende minneceller med verdien 1 plassert i første celle, 6 i den neste osv. 7 lagres altså rett inntil 0 selv om de logisk sett er lagret i hver sin rad. Lagerplassen, pos, for et gitt tabellelement, tab[x][y], i en todimensjonal tabell er gitt av uttrykket: pos = x * ("antall elementer i rad") + y Hvis man er ut etter adressen (regnet i bytes) for tabellelementet, må man skalere med elementstørrelsen og legge til startadressen for tabellen: Adresse for elementet tab[x][y]: "startadresse for tab" + ("antall bytes per element") * pos Startadressen for en tabell i C-programmet fås ved å oppgi tabellnavnet uten indekser. Eksempel: Anta tabelldefinisjonen: int t[6][10]; Anta at sizeof(int) ( 4 Adressen til elementet t[2][5]: "startadresse til t" + 4(2*10 + 5) ( "startadresse til t" + 100 Ønsker vi å benytte adressen til et tabellelement i programmet holder C-kompilatoren rede på hvor menge bytes hvert tabellelement opptar. Følgende to uttrykk er derfor likeverdige når det gjelder å angi adressen til et gitt element: t + 25 (( &t[2][5]; Hvor 2*10 + 5 ( 25 16 Lesing og skriving av filer Det forutsettes at selve filbegrepet er kjent - det samme gjelder for de viktigste prinsippene når det gjelder fysisk lagring på disk. Til nå har vi studert hvordan data kan leses fra tastaturet og skrives til skjermen. Det er klart at om man har datamengder som er av noen størrelse, blir man snar lei av å taste de inn direkte som svar på ledetekster i og lignende i programmet. Et alternativ er da å forhåndslagre dataene på en fil og la programmet leses fra denne filen. Den samme filen kan da leses av programmet gang på gang. Dette er ikke minst nyttig under uttesting av programmer. Likeledes kan man skrive data til en fil som tillegg eller alternativ til skjermutskrift. Da kan man studere filen i en fileditor eller lignende i ro og mak etter at programmet er ferdig utført. Fra et C-program, kan man: Opprette filer Skrive til filer Lese fra filer Slette filer 16.1 Stream-begrepet Et C-program oppfatter en fil som en såkalt STREAM. Vi har tidligere også benyttet streams i forbindelse med standard input/output - stdio. Stdin og stdout er to forhåndsdefinerte streams som C-programmereren kan benytte (#include ). Streams går i korthet ut på at man bruker modell der en oppfatter dataene på en fil som en kontinuerlig strøm av tegn. Denne tegnstrømmen kan man hente inn i programmet eller man kan sende data fra programmet langs denne strømmen ned på disken. Når C-programmereren benytter denne modellen foregår det ganske mye aktivitet på lavere nivå i datamaskinen, men dette blir usynlig for programmereren. Denne lavnivåaktiviteten består bl. a. i å styre harddisken via operativsystemet. Fra harddisken hentes det opp blokker av tegn som "viderebefordres" til C-programmet via stream-modellen som en strøm av enkelttegn. C-inneholder en rekke funksjoner som kan manipulere på filer. De vi skal se på her, ligner svært på de som gjelder for lesing av data fra tastatur, og skriving av data til skjerm. For å benytte disse standard filfunksjonene må man først inkludere . For en detaljert beskrivelse av de enkelte funksjonene henvises det til eget vedlegg til kompendiet. Vi skal vise eksempler på bruken av følgende funksjoner fra standard IO-bibliotek, som alle baseres på bruk av streams: fopen - åpne en fil fclose - lukke en fil fscanf - lese formatert fra en stream fprintf - skrive formatert til en stream getc - lese et tegn fra en stream putc - skrive et tegn til en stream 16.1.1 Fil-peker For å få tilgang til dataene i en fil, benyttes en såkalt filpeker som referanse. En filpeker refererer til en stream og peker egentlig til et ganske komplisert dataobjekt, og ikke selve filen på harddisken. Detaljene her slipper vi heldigvis å bekymre oss noe særlig om, vi må bar vita at en filpeker deklareres på denne måten: FILE *fp; // navnet fp kan velges fritt En filpeker som opprettes på denne måten peker ikke til noen ting. For å kunne bruke denne filen må det først lages en kobling mellom filpekeren og den fysiske filen på disken. 16.2 Åpning av filer Forbindelsen mellom filpekeren og filen dannes ved en prosess som kalles å åpne filen. Funksjonen som benyttes til dette er fopen(). Denne funksjonen trenger å vite selve filnavnet og eventuelt filbanen til filen, samt om vi har tenkt å lese fra eller skrive til filen. Funksjonen returner en verdi som kan tilordnes en filpeker. Fopen() benyttes både for å åpne eksisterende og å opprette nye filer. 16.2.1 Å åpne en fil for lesing FILE *fa; fa = fopen("FIL_A", "r"); // Åpne for lesing I dette tilfellet er filen klar til lesing fra første posisjon i filen. Vi sier filpekeren peker til begynnelsen av filen. Lesing foregår deretter sekvensielt fra start til slutt av filen. Hvis ikke filen som forsøkes å åpnes for lesing eksisterer (det går ikke å lese noe som ikke finnes), returnerer fopen() en ugyldig filpeker - denne har verdien NULL. Vi kan teste om en fil finnes ved å forsøke å åpne den for lesing: fa = fopen("FIL_A", "r"); if (fa == NULL) exit(1); Å forsøke å benytte en ugyldig filpeker fører til at programmet krasjer. 16.2.2 Å åpne en fil for skriving FILE *fb; fb = fopen("FIL_B", "w"); // Åpne for overskriving Her er filen klare til skriving fra første posisjon i filen. Vi sier filpekeren peker til begynnelsen av filen. Skriving foregår deretter sekvensielt fra start. Forsøker man å åpne en fil som ikke eksisterer for skriving, blir en ny fil opprettet med angitt filnavn. Hvis derimot filen eksisterer fra før, blir det gamle innholdet slettet før skrivingen startes. Det er feil hvis vi forsøker å åpne en fil for skriving dersom vi ikke har tilgang til dette på grunn av brukerrettigheter eller at den er låst av andre årsaker. Man skal derfor også sjekke resultatet av en fopen() for skriveaksess også. 16.2.3 Å åpne en fil for tillegging av data FILE *fc; fc = fopen("FIL_B", "a"); // Åpne for skriving av nye data - append I dette tilfellet vil filen være klar til å legge inn nye data etter siste gyldige tegn i den eksisterende filen. Vi sier filpekeren peker til slutten av filen. Hvis ikke filen eksisterer fra før, blir en ny fil opprettet. Forskjellen på aksessmetodene "w" og "a" er altså at "w" skriver over en eksisterende fil (sletter gamle data), mens "a" tar vare på gamle data og legger til nye på slutten av filen. 16.3 Å lukke filer Når vi er ferdige med å bruke en fil skal vi lukke den ved funksjonen fclose(). Denne funksjonen tar bare et argument, og dette argumentet er en gyldig filpeker som refererer til filen som skal lukkes. fclose(fa); En lukket fil er utilgjengelig fra programmet. Når vi lukker en fil frigjøres systemresurser som går med til å administrere filen. 16.4 Formatert skriving. Til formatert skriving til en fil, kan man benytte funksjonen fprintf(). Bruken av denne tilsvarer bruken av printf(), men vi må i tillegg angi hvilen stream/filpeker vi vil benytte. Eksempel: int ivar = 3; float fvar = 3.0; fprintf(fb, "%d, %.2f\n", ivar, fvar); Dette vil føre til at ascii-tegn tilsvarende teksten "3, 3.00\n" skrives inn på filen. 16.5 Skrive et enkelt tegn Det finne en spesialfunksjon for å skrive et enkelt tegn til filen - putc(). Eksempel: putc (fb, 'x'); // Skriver tegnet 'x' 16.6 Formatert lesing. Her benyttes gjerne fscanf() som i bruk tilsvarer scanf(). Eksempel - lese inn fra en fil som er formatert med to heltall per linje: fscanf(fa, "%d "d", &x, &y); 16.7 Lesing av enkelttegn Her kan vi benytte en funksjon som tilsvarer putchar(), nemlig getc(). Eksempel: c = getc(fa); 16.8 Andre funksjoner. Det finnes flere funksjoner man kan benytte i forbindelse med filer, men vi begrenser oss til disse som er beskrevet ovenfor i denne omgang. Alle relevante funksjoner er beskrevet i vedlegg. (Foreløpig kke inkludert i dette dokumentet). 16.9 Eksempler på programmer som benytter filer. Et program som leser data fra en fil som inneholder avgangs tider og ankomsttider for en buss eller et tog. filen har dette formatet: 0700 0740 0730 0800 0910 0950 etc. Brukeren kan taste inn et klokkeslett. Programmet søker så gjennom filen og skriver ut avgangstid og ankomsttid som passer. #include void main(void) { int earliest, depart, arrive; FILE *timetable; printf("Earliest possible departure? "); scanf("%i", &earliest); timetable = fopen("TIMETABLE.DAT", "r"); do { fscanf(timetable, "%i %i", &depart, &arrive); } while (depart < earliest); printf("Your train leaves at %i ", depart); printf("and arrives at %i\n", arrive); fclose(timetable); } Neste eksempel viser hvordan man kan lage en fil fra scratch. Oppgaven er enkel. Programmet går i en løkke og genererer 10 flyttall som skrives til filen. Deretter leses det som programmet har skrevet til filen, tilbake i programmet og skrives ut. #include void main(void) { float f; int i; FILE *fi, *fo; fo = fopen("DATA.DAT", "w"); for (i = 0; i < 10; i++) { f = i; fprintf(fo, "%5.2f\n", f); } fclose(fo); fi = fopen("DATA.DAT", "r"); for (i = 0; i < 10; i++) { fscanf(fi, "%f", &f); printf("%5.2f\n", f); } fclose(fi); } Neste eksempel ligner på foregående. Forskjellen er i første rekke at når filen leses tilbake til programmet, leses tegn for tegn. Disse tegne skrives ut på to måter - som tegnsymboler og som hex-verdier for hvert ascii-tegn. #include void main(void) { float f; int i, c; FILE *fi, *fo; fo = fopen("DATA.DAT", "w"); for (i = 0; i < 10; i++) { f = i; fprintf(fo, "%5.2f\n", f); } fclose(fo); fi = fopen("DATA.DAT", "r"); while (1) { c = getc(fi); if (c == EOF) break; printf("%c", c); } fclose(fi); fi = fopen("DATA.DAT", "r"); while (1) { c = getc(fi); if (c == EOF) break; printf("%02x ", c); } fclose(fi); } Legg merke til hvordan man tester om det er mer å lese fra filen. Verdien som leses fra stream-en når det ikke er flere tegn å lese er en spesialverdi EOF. Ved å teste på denne verdien, kan man finne ut om det er slutt på filen eller ikke. En annen mulighet er å benytte feof() funksjonen for å sjekke dette. 16.10 Oppgaver Lag et program som skriver heltallene fra 1000 til 2000 til en fil, med to tall per linje. Skriv en C-funksjon med følgende funksjonsprototyp: FILE *open_ok(char filename[], char mode[]); Funksjonen skal åpne en fil med angitt filnavn og i en aksessmodus som er gitt av funksjons argumentet, mode ("r", "w" eller "a"). Dersom filen ble åpnet på normal måte, skal funksjonen returnere den aktuelle filpekeren. Hvis derimot en eller annen feil oppstod ved åpningen, skal funksjonen skrive ut teksten "Feil i åpning av fil ..." og avslutte programmet. Bruk standardfunksjonen exit(). Hensikten med funksjonen er å slippe å teste i hovedprogrammet hver gang man skal åpne en fil. Forutsett at det finnes en tekstfil som inneholder noen linjer med vanlig norsk tekst (ascii-kodet). Lag et program som åpner denne filen og skriver innholdet til skjermen linje for linje. Imidlertid skal all utskrift kun benytte store bokstaver (versaler). Se oppgave under kapittelet om funksjoner vedrørende utskrift av store bokstaver. Les inn filnavnet fra tastaturet, og sjekk at filen faktisk eksisterer før du åpner den. Hint: Hele linjer med tekst fra en fil kan leses med fscanf() slik: char line[81]; fscanf(fp, "%[^\n]", line); // fp = gyldig filpeker Lag et program som lager en fil som inneholder 10000 "tilfeldige" verdier generert av rand()-funksjonen. (#include ) Lagre et tall per linje. srand ((unsigned int) time(NULL)/2); // initialisering x = rand(); // for hver nye verdi Lag et program som sorterer innholdet i filen som ble opprettet i forrige oppgave, og skriver ut tallene i sortert rekkefølge. Forutsett at det finnes en tekstfil som inneholder heltall - ett per linje. Lag et program som finner middelverdien og medianen til disse tallene. Resultatet skrives til skjermen. Hint: Les tallene inn i en tabell før du starter å behandle dem. 17 Strukturer Vi har i et tidligere kapittel sett at datasamlinger der alle elementene er av samme type kan samles i arrays (tabeller). Språket C tilbyr også muligheten for å samle datasett av forskjellige typer. En slik samling kalles en struktur (struct) og er et nyttig hjelpemiddel for å systematisere datasett. Anta f. eks. at visse egenskaper ved en bil skal beskrives med et sett av prametre: Motor (antall hestekrefter) - som en float. Antall dører - som et heltall. Merke - som et heltall. Vekt - som en float. Registreringsår - som et heltall. Vi kan konstruere en ny datatype der alle disse parametrene inngår. La oss kalle datatypen bil parametere: struct bilparametere { float motor; int regaar; int dorer; float vekt; int merke; } Denne nye datatypen kan nå benyttes til å deklarere variable og tabeller som skal inneholde bildata. Det kan gjøres på denne måten: struct bilparametre bil_A; struct bilparametre bil_B; struct bilparametre bilsamling[20]; Initialisering av variablene er også mulig: struct bilparametre bil_A = {100.0, 1995, 4, 1157.0, 25 }; Merk at nøkkelordet struct må angis i variabeldeklarasjonene. Disse nye variablene kan ikke benyttes i aritmetiske og logiske operasjoner som variable med standard datatyper kan. Derimot kan variablene benyttes i tilordninger og som funksjonsargumenter og returverdier. De enkelte elementene i strukturvariablene kalles struturmedlemmer. Medlemmene kan hentes fram og benyttes som tradisjonelle variable av den datatypen de er deklarert som. Strukturmedlemmene spesifiseres ved å sette et punktum '.' mellom variabelnavnet og medlemsnavnet: bil_A.motor = 200.5; bil_B.dorer = 5; if (bil_B.motor > 150.0) printf("OK - motor\n"); 18 Bit-for-bit operatorer En viktig gruppe operatorer arbeider direkte på de enkelte bitene i dataordene. Dataordene må være av heltallstypen (char, int, long int etc.). Det er viktig å skille disse operatorene fra de logiske operatorene som er beskrevet tidligere, og som tolker innholdet av hele odet som en logisk størrelse (0 ( '0' og alt annet ( '1'). Vi har 6 bit-for-bit logiske operatorer: And, or, exclusive-or, komplement, venstreskift, høyreskift. |Operatorens navn |Symbol | |AND (Bit-for-bit) |& | |OR (Bit-for-bit) || | |EXCLUSIVE OR (Bit-for-bit) |^ | |COMPLEMENT (Bit-for-bit) |~ | |VENSTRESKIFT (Bit-for-bit) |<< | |HØYRESKIFT (Bit-for-bit) |>> | 18.1 Komplement Resultat ( ~op1 Den enkleste av disse er komplement-operatoren som bitinverterer alle bitene i et heltalls dataord (variabel eller konstant). (Resultat - bit 0) ( NOT (op1 - bit 0) (Resultat - bit 1) ( NOT (op1 - bit 1) etc. Anta at et dataord, xy, har verdien 0x3A. Dette blir på binærform: 0011 1010. ~xy skal komplementere (invertere) alle bit slik: 1100 0101 som gir 0xC5. ~0x3A ( 0xC5. 18.2 Bit-for-bit AND Resultat ( op1 & op2 Denne operatoren trenger 2 operander. Hvis man tenker seg disse operandene på binærform, skal det foretas en parvis AND-operasjon mellom de samsvarende bitene i disse operandene: (Resultat - bit 0) ( (op1 - bit 0) AND (op2 - bit 0) (Resultat - bit 1) ( (op1 - bit 1) AND (op2 - bit 1) etc. Eksempler: 30 & 45 ( 0x1E & 0x2D ( 0001 1110 AND 0010 1101 ( 0000 1100 ( 0x0C ( 12 0xAA & 0x7C ( 1010 1010 AND 1110 1100 ( 1010 1000 ( 0xA8 Bit-for-bit AND kan benyttes hvis man ønsker å nulle ut et bestemt bit i et ord. Man setter da opp en AND-ing mellom ordet og en bitmaske som inneholder bare 1-ere bortsett fra en 0 på den posisjonen man ønsker å nulle ut. Eksempel: Nuller ut bitposisjon 3 i variabelen test (antar 8 bits ord): test = test & 0xF7; Alternativt kan man benytte komplement-operatoren for å angi den aktuelle bitmasken for å oppnå det samme: test = test & ~0x08; 18.3 Bit-for-bit OR Resultat ( op1 << op2 Denne operatoren virker tilsvarende som bit-for-bit AND, men OR-er sammen bitene i to ord isteden. (Resultat - bit 0) ( (op1 - bit 0) OR (op2 - bit 0) (Resultat - bit 1) ( (op1 - bit 1) OR (op2 - bit 1) etc. Eksempler: 0xF0 | 0x0F ( 1111 0000 OR 0000 1111 ( 1111 1111 ( 0xFF 0xAA | 0x50 ( 1010 1010 OR 0101 0000 ( 1111 1010 ( 0xF5 Bit-for-bit OR kan benyttes for å sette spesifiserte bit til 1 i et dataord. Følgende lille eksempel vil sørge for at bit 0 i variabelen test settes til 1: test = test | 0x01; 18.4 Bit-for-bit exclusive OR Resultat ( op1 << op2 Exclusive OR- eller XOR-operatoren har ikke noe motstykke i de rene logiske operatorene i C-språket. Funksjonene er imidlertid kjent fra tradisjonell Boolsk algebra og digitalteknikk. Funksjonen til operatoren er som følger: |XOR-funksjon | |a XOR b |Resultat | |'0' XOR '0' |'0' | |'0' XOR '1' |'1' | |'1' XOR '0' |'1' | |'1' XOR '1' |'0' | Funksjonen ligner på OR, men hvis begge operander er '1', blir resultatet '0'. Hvis altså operatorene er ulike, blir resultatet '1'; hvis de er like blir resultatet '0'. (Resultat - bit 0) ( (op1 - bit 0) XOR (op2 - bit 0) (Resultat - bit 1) ( (op1 - bit 1) XOR (op2 - bit 1) etc. Eksempler: 0xFF ^ 0x55 ( 1111 1111 XOR 0101 0101 ( 1010 1010 ( 0xAA 0xAA ^ 0xAA ( 1010 1010 XOR 1010 1010 ( 0000 0000 ( 0x00 Den viktigste bruksområdet til XOR er å kunne snu polariteten til enkeltbit i et ord uten å kjenn til om biten på forhånd var 0 eller 1. At dette blir riktig ser man fra tabellen; Hvis b = '0' ( resultat = a, og hvis b = '1' ( resultat = ~a. test = test ^ 0x01; // Snu bit nummer 0 test = test ^ 0x81; // Snu bitene 7 og 0 18.5 Venstreskift Resultat ( op1 << op2 Denne operatoren utfører ingen logisk funksjon, men skifter alle bitene i et ord et visst antall posisjoner mot mest signifikante bit. Dette kalles venstreskift fordi vi når vi skriver et bitmønster, som oftest tenker oss et binærtall med mest signifikante bit til venstre - på samme måten som et desimaltall har mest signifikante siffer til venstre. Vi fyller inn med '0'-er fra høyre (minst signifikante ende) når vi venstreskifter. Første operand i et skifteuttrykk angir ordet som skal skiftes. Andre operand angir hvor mange posisjoner det skal skiftes. Anta at operand 1 er på 8 bit, og at operand 2 angir at det skal skiftes 1 posisjon til venstre. Følgende figur illustrerer hva som skjer: [pic] Situasjonen hvis operand 2 angir 3 bits skift mot venstre blir: [pic] Eksempler: 0x0F << 3 ( 0000 1111 << 3 ( 0111 1000 ( 0x78 0x37 << 2 ( 0011 0111 << 2 ( 1101 1100 ( 0xDC Siden å multiplisere et ord med 2n tilsvarer å skifte innholdet i et ord n plasser til venstre, benyttes venstreskiftoperatoren ofte for å multiplisere med tall som er potenser av 2. Datamaskinen utfører nemlig venstreskift mye hurtigere en multiplikasjon. test = test << 2; // Tilsvarer å multiplesre test med 4 18.6 Høyreskift Resultat ( op1 >> op2 Denne operatoren skifter alle bitene i et ord et visst antall posisjoner mot minst signifikante bit. Dette kalles høyreskift fordi vi når vi skriver et bitmønster, som oftest tenker oss et binærtall med minst signifikante bit til høyre - på samme måten som et desimaltall har minst signifikante siffer til høyre. Første operand i et skifteuttrykk angir ordet som skal skiftes. Andre operand angir hvor mange posisjoner det skal skiftes. Når det gjelder høyreskift må vi skille mellom å skifte et ord der innholdet er med eller uten fortegn. Om ordet er med eller uten fortegn kan angis i variabeldeklarasjonen eller ved å sette en 'U' etter en konstant eller ved å benytte cast-mekanismen. 18.6.1 Unsigned høyreskift Vi fyller inn med '0'-er fra venstre (mest signifikante ende) når vi høyreskifter et tall uten fortegn. Anta at operand 1 er på 8 bit, og at operand 2 angir at det skal skiftes 1 posisjon til høyre. Følgende figur illustrerer hva som skjer: [pic] Situasjonen hvis operand 2 angir 2 bits skift mot høyre blir: [pic] Eksempler: 0x5FU >> 3 ( 0101 1111 >> 3 ( 0000 1011 ( 0x0BU 0xF4U >> 2 ( 1111 0100 >> 2 ( 0011 1101 ( 0x3DU 18.6.2 Signed høyreskift Negative tall beskrives vha. 2's komplementkode[17]. Dette medfører blant annet at i positive tall, er det mest signifikante bit ''0'. For negative tall er mest signifikante bit '1'. Ved signed høyreskift beholdes fortegnet gjennom skifteprosessen. Det betyr at om tallet som skiftes er negativt, blir en '1' fylt inn i ordet fra venstre for hver skiftet posisjon. For positive tall fylles det inn '0'-er fra venstre. Anta at operand 1 er på 8 bit, og at operand 2 angir at det skal skiftes 1 posisjon til høyre. Følgende figur illustrerer hva som skjer: [pic] Situasjonen hvis operand 2 angir 2 bits skift mot høyre blir: [pic] Eksempler: 0x5F >> 3 ( 0101 1111 >> 3 ( 0000 1011 ( 0x0B 0xF4 >> 2 ( 1111 0100 >> 2 ( 1111 1101 ( 0xFD Siden å dividere et ord med 2n tilsvarer å skifte innholdet i et ord n plasser til høyre, benyttes venstreskiftoperatoren ofte for å dividere med tall som er potenser av 2. Datamaskinen utfører nemlig høyreskift mye hurtigere en divisjon. test = test >> 2; // Tilsvarer å dividere test med 4 Fortegnet beholdes i divisjonen hvis operanden som skal skiftes er deklarert som et objekt med fortegn. Konstanter og heltallsdatatypene int, short int og long int er i utgangspunktet med fortegn. 18.7 Bit-for-bit logiske tilordningsoperatorer På tilsvarende måte som vi kan kombinere de aritmetiske operatorene med tilordning, kan også de bit-for-bit logiske operatorene kombineres med tilordning. Disse kombinerte operatorene innfører ikke noe som ikke kan utføres med de tidligere beskrevne operatorene sammen med tilordningsoperatoren '=', men kan betraktes som kortformer: |Fullt utskrevet |Kortform | |x = x & y; |x &= y; | |x = x | y; |x |= y; | |x = x ^ y; |x ^= y; | |x = x << y; |x <<= y; | |x = x >> y; |x >>= y; | C-programmerere benytter seg ofte av disse kortformsnotasjonene. Pass på at alle tegnene i den kombinerte operatoren må skrives tett inntill hverandre uten mellomrom. Eksempler: x &= 0xF0; // Nullstill bit 3 - 0 i x, resultat i x y |= 0x55; // Sett bit 6, 4, 2 og 0 i y til 1, resultat i y z ^= 0xAA; // Snu bit 7, 5, 3, 1 i z, resultat i z v <<= 3; // Skift innholdet i v 3 plasser til venstre, resultat i v w >>= 2; // Skift innholdet i w 2 plasser til høyre, resultat i w Når bit-for-bit logiske tilordningsoperatorer benyttes, modifiseres altså den venstre operanden som altså må være en variabel. 19 Appendix A - Nøkkelord i C Følgende ord har definert betydning i C og kan derfor ikke benyttes til navn på variable, funksjoner eller kontanter. Nye ord hvor nøkkelordene inngår som en del av navnet kan benyttes; f. eks. newchar, der char er et nøkkelord. |auto |double |int |struct | |break |else |long |switch | |case |enum |register |typedef | |char |extern |return |union | |const |float |short |unsigned | |continue |for |signed |void | |default |goto |sizeof |volatile | |do |if |static |while | Ikke alle av disse nøkkelordene er benyttet i dette kompendiet. 20 Appendix B - Operatorer Nedenfor presenteres enenkel liste med de operatorsymbolene finnes i C: |Operator |Presedensnivå/|Definisjon | | | | | | |Prioritetsnivå| | |! |1 |Logisk 'not' . | |& |1 |Ta adressen til . (brukes ved | | | |adressering) | |* |1 |Ta innholdet av . (brukes ved | | | |pekere) | |* |2 |Multiplikasjon | |/ |2 |Divisjon | |% |2 |Finn rest etter heltallsdivisjon | |+ |3 |Addisjon | |- |3 |Subtraksjon | |< |4 |. Mindre enn . | |> |4 |. Større enn . | |<= |4 |. Mindre eller lik . | |>= |4 |. Større eller lik . | |== |5 |. lik med . | |!= |5 |. forskjellig fra . | |&& |6 |Logisk 'and' | ||| |7 |Logisk 'or' | | | | | | | | | | | | | I et uttrykk der flere operatorer inngår, utføres operatorene med lavest prioritetsnivå (presedens) først. Parenteser overstyrer presedensen. I tillegg betraktes også følgende konstruksjoner som operatorer: f( ): ( ) angir funksjonskall når de benyttes etter et funksjonsnavn. a[ ]: [ ] angir indekser i en tabell. s.m: . velger et medlem fra en datastruktur. 21 Løsningsforslag på noen oppgaver 21.1 Innledning 21.2 Hello World 21.3 Konstanter og variable 21.4 De 4 grunnleggende datatypene 21.5 Typemodifikatorer 21.6 Aritmetiske operatorer 21.7 Standard IO 21.8 Relasjonsoperatorer og Logiske operatorer 21.9 Betingede valg 21.10 Løkker 21.11 Tabeller / Arrays 21.12 Funksjoner Skriv en C-funksjon med følgende funksjonsprototyp: float hypo(float kat1, float kat2); Funksjonen skal beregne og returnere lengden av hypotenusen i en trekant med katetene gitt som funksjonsargumenter. Bruk standardfunksjonen sqrt() i løsningn. float hypo(float kat1, float kat2) { float sum_sqr, h; sum_sqr = kat1 * kat1 + kat2 * kat2; h = sqrt(sum_sqr); return h; } Skriv en C-funksjon med følgende funksjonsprototyp: float areal_trekant(float kat1, float kat2); Funksjonen skal beregne og returnere arealet av en trekant med katetene gitt som funksjonsargumenter. float areal_trekant(float kat1, float kat2) { float a; a = kat1 * kat2 / 2; return a; } Skriv en C-funksjon med følgende funksjonsprototyp: float radius(float areal); Funksjonen skal beregne radius i en sirkel basert på at arealet er gitt som funksjonsargument. #define PI 3.14 float radius(float areal) { float r; r = sqrt(aral / PI); return r; } Skriv en C-funksjon med følgende funksjonsprototyp: int posisjon(char c, char s[]); Funksjonen skal finne og returnere posisjonsnummeret til et vilkårlig tegn, c, i tekststrengen, s. C og s er funksjonsargumenter. Lag to varianter av funksjonen: Forutsett at tegnet c finnes i strengen s. Sjekk også om tegnet c inngår i strengen, hvis ikke skal verdien -1 returneres. NB! Husk at alle tekststrenger i C vil være avsluttet av et tegn med verdien 0 / '\0' (zero-terminated string). a) int posisjon(char c, char s[]) { int p; for (p = 0; s[p] != c; p++) { } // Scan string return p; } b) int posisjon(char c, char s[]) { int p, found = 0; for (p = 0; s[p] != 0; p++) { // Sacan string if (s[p] == c) { found = 1; break; // Stop search } } if (found) return p; else return -1; } Skriv en C-funksjon med følgende funksjonsprototyp: void print_store(char s[]); Funksjonen skal skrive ut en tekststreng, s, til skjermen. Selv om s inneholder små bokstaver eller en blanding av små og store bokstaver, skal kun store bokstaver skrives til skjermen. Lag tre varianter av funksjonen: Forutsett at alle tegnene i strengen hører med til det engelske alfabetet a - z. Bokstavene har koder løper etter hverandre fra 'a' = 0x61 til 'z' = 0x7a og fra 'A' = 0x41 til 'Z' = 0x5a. Det er altså en konstant avstand = 0x20 mellom små og store bokstaver i det engelske alfabetet. Strengen kan også inneholde "norske" tegn (Æ = 0x92, Ø = 0x9d, Å = 0x8f, æ = 0x91, ø = 0x9b, å = 0x86). Strengen kan inneholde alle tegn, men bare bokstaver i alfabetet skal eventuelt konverteres til store bokstaver. a) void print_store(char s[]) { char c; c = s[0]; for (p = 0; c != 0; p++) { // Scan string if (c >= 0x61) // lowercase c -= 0x20; // subtract constant offset putchar(c); // print c = s[p]; } } b) void print_store(char s[]) { char c; c = s[0]; for (p = 0; c != 0; p++) { // Scan string if (c == 0x91) // æ c = 0x92; // Æ else if (c == 0x9b) // ø c = 0x9d; // Ø else if (c == 0x86) // å c = 0x8f; // Å else if (c >= 0x61) // lowercase c -= 0x20; // subtract constant offset putchar(c); // print c = s[p]; } } c) void print_store(char s[]) { char c; c = s[0]; for (p = 0; c != 0; p++) { // Scan string if (c == 0x91) // æ c = 0x92; // Æ else if (c == 0x9b) // ø c = 0x9d; // Ø else if (c == 0x86) // å c = 0x8f; // Å else if (c >= 0x61 && c <= 0x7a) // lowercase c -= 0x20; // subtract constant offset putchar(c); // print c = s[p]; } } Skriv en C-funksjon med følgende funksjonsprototyp: int mean(int tab[], int n); Funksjonen skal finne og returnere middelverdien til elementene i en tabell med n elementer. int mean(int tab[], int n) { int i, m, sum = 0; // compute sum of all elements for (i = 0; i < n; i++) { sum = sum + tab[i]; // or sum += tab[i]; } // mean is sum divided by the number of elements m = sum / n; return m; } Skriv en C-funksjon med følgende funksjonsprototyp: int median(int tab[], int n); Funksjonen skal finne å returnere medianen til elementene i en tabell med n elementer. Medianen er verdien til det elementet som har like mange elementer som er større enn seg selv som elementer som er mindre enn seg selv. (For enkelhets skyld: Anta at alle elementene har forskjellig verdi og at tabellen inneholder et odde antall elementer). // Strategi for løsning: // Går gjennom tabellen og tester elementer. // For hvert element som testes, telles hvor mange // elementer som er større og mindre enn dette elementet. // Når disse to antallene er like, er medianen funnet. int median(int tab[], int n) { int i, j, greater, less, med; // compute sum of all elements for (i = 0; i < n; i++) { // Testing all elements greater = 0; less = 0; for (j = 0; j < n; j++) { if (j == i) // element in test continue; // Skip - test next if (tab[j] < tab[i]) less++; else if (tab[j] > tab[i]) greater++; } // end inner for-loop if (greater == less) break; // found; index = i } // end outer for-loop return tab[i]; } Bruk funksjonene fra oppgave 1 til 7 i løsningene av følgende oppgaver. Lag et program som leser inn et areal på en sirkel fra tastaturet. Deretter skal programmet beregne radius i sirkelen og skrive ut denne til skjermen. #include #include #define PI 3.14 float radius(float areal); // Funksjonsprot. må deklareres int main(void) { float ar, r; printf("Skriv inn arealet til en sirkel: "); scanf("%f", &ar); // NB - husk peker r = radius(ar); printf("Radius i sirkelen er: %f\n", r); } Lag et program som beregner areal og hypotenus i en trekant. Lengden på de to katene leses inn fra tastaturet. Resultatene skrives til skjerm. #include #include float areal_trekant(float kat1, float kat2); float hypo(float kat1, float kat2); int main(void) { float k1, k2, ar, h; printf("TREKANTBEREGNING 1.0\n"); printf("Skriv inn lengden til 1. katet: "); scanf("%f", &k1); // NB - husk peker printf("Skriv inn lengden til 2. katet: "); scanf("%f", &k2); // NB - husk peker h = hypo(k1, k2); ar = areal_trekant(k1, k2); printf("Hypotenus er: %f, arealet er %f\n", h, ar); } Les inn en streng fra tastaturet. La et program finne ut om denne strengen inneholder tegnet 'p' og i tilfellet på hvilken posisjon i strengen dette tegnet forekommer. Skriv passende tekst til skjermen som rapporterer resultatet. Hint: En tekststreng leses inn vha av formatspesifikatoren %s i scanf(). F. eks. slik: char streng[21]; scanf("%s", &streng); #include int posisjon(char c, char s[]); int main(void) { char str[81]; // Reserverer plass til en linje tekst int pos; printf("Skriv inn en tekststreng: "); scanf("%s", &str); // NB - husk peker pos = posisjon('p', str); if (pos != -1) printf("\'p\' befinner seg på posisjon %d\n", pos); else printf("\'p\' finnes ikke i strengen\n"); } 21.13 Mer om funksjoner 21.14 Flervalgsetningen 21.15 Flerdimensjonale tabeller 21.16 Lesing og skriving av filer Lag et program som skriver heltallene fra 1000 til 2000 til en fil, med to tall per linje. Skriv en C-funksjon med følgende funksjonsprototyp: FILE *open_ok(char filename[], char mode[]); Funksjonen skal åpne en fil med angitt filnavn og i en aksessmodus som er gitt av funksjons argumentet, mode ("r", "w" eller "a"). Dersom filen ble åpnet på normal måte, skal funksjonen returnere den aktuelle filpekeren. Hvis derimot en eller annen feil oppstod ved åpningen, skal funksjonen skrive ut teksten "Feil i åpning av fil ..." og avslutte programmet. Bruk standardfunksjonen exit(). Hensikten med funksjonen er å slippe å teste i hovedprogrammet hver gang man skal åpne en fil. FILE *open_ok(char filename[], char mode[]) { FILE *fp; fp = fopen(filename, mode); if (result == NULL) { printf("Feil i \x86pning av fil %s\n", filename); exit(1); } return fp; } Forutsett at det finnes en tekstfil som inneholder noen linjer med vanlig norsk tekst (ascii-kodet). Lag et program som åpner denne filen og skriver innholdet til skjermen linje for linje. Imidlertid skal all utskrift kun benytte store bokstaver (versaler). Se oppgave under kapittelet om funksjoner vedrørende utskrift av store bokstaver. Les inn filnavnet fra tastaturet, og sjekk at filen faktisk eksisterer før du åpner den. Hint: Hele linjer med tekst fra en fil kan leses med fscanf() slik: char line[81]; fscanf(fp, "%[^\n]", line); // fp = gyldig filpeker #include FILE *open_ok(char filename[], char mode[]); void print_store(char s[]); int main(void) { char fname[81], line[81]; FILE *f; printf("Skriv inn filnvnet: "); scanf("%s", &fname); f = open_ok(fname, "r"); while (!feof(f)) { fscanf(f, "%[^\n]", line); print_store(line); printf("\n"); } fclose (f); } Lag et program som lager en fil som inneholder 10000 "tilfeldige" verdier generert av rand()-funksjonen. (#include ) Lagre et tall per linje. srand ((unsigned int) time(NULL)/2); // initialisering x = rand(); // for hver nye verdi Lag et program som sorterer innholdet i filen som ble opprettet i forrige oppgave, og skriver ut tallene i sortert rekkefølge. Forutsett at det finnes en tekstfil som inneholder heltall - ett per linje. Lag et program som finner middelverdien og medianen til disse tallene. Resultatet skrives til skjermen. Hint: Les tallene inn i en tabell før du starter å behandle dem. #include int mean(int tab[], int n); int median(int tab[], int n); FILE *open_ok(char filename[], char mode[]); int main(void) { char fname[] = "TABELL.TXT"; FILE *f; int meanval, medval, element = 0; int table[1000]; // No more than 1000 elements f = open_ok(fname, "r"); // Read table and count elemnts while (!feof(f)) { // while there are more lines in file fscanf(f, "%d", &table[element]); element++; } fclose (f); meanval = mean(table, element); medval = median(table, element); printf("Middelverdien = %d, medianen = %d\n", meanval, medval); } 22 Diverse oppgaver uten løsningsforslag Hvilke syntaksfeil og eventuelle andre feil finner du i programmet nedenfor? #include #define PI 3.14; int i = 3, j = 4, k float f; int main(void) { if (k < 5) printf("K mindre enn 5\n", k); if (f < PI) printf("F mindre en PI\n); scanf["%d %d", i, j]; if i > j { printf("I er større enn J\n"); } return 0; } ----------------------- [1] Faktisk blir tegnet '\0' plassert på den 18. posisjonen for å markere avslutningen av strengen. Dette kan vi foreløpig la ligge. [2] Venstre side kan generelt være en adresse til en plass i hukommelsen der det går an å lagre verdier. Dette innbefatter også det som i C heter pekere og som vil bli behandlet senere. [3] Mer om tester siden [4] Mer om funksjonsprototyper siden. [5] Det finnes unntak fra denne hovedregelen. [6] EOF - End Of File [7] Dette er ikke en 100% korrekt måte å betrakte strengvariable på i C, men mer om temaet siden. [8] Symbolet & kan også benyttes i andre sammenhenger. [9] I alle fall nesten alltid. Se nedenfor når det gjelder tekststrengvariable. [10] Mer om dette i kapittelet om tabeller/arrays. [11] C har også operatorer som utfører logiske operasjoner på de enkelte bit i en variabel eller konstant. Dette forklares seinere. [12] Se eget kapittel om flervalgssetningen. [13] I C er egentlig også main() en funksjon - den funksjonen som angir starten for hele programmet. [14] Kapittel 3 [15] Det finnes også en type lokale variable som kan deklareres som static. Denne variabeltypen har helt andre egenskaper enn det som er beskrevet her. [16] Begrepet "global variabel" brukes egentlig sjelden i C. I stedet snakker man om "eksterne variable" som for praktiske formål her er det samme. [17] 2's komplement er forklart i de fleste lærebøker som omhandler Boolsk algebra og logiske kretser.