C/Funksjoner
I dette kapittelet skal vi se på et nøkkelverktøy når det kommer til abstraksjon av kode, nemlig funksjoner. En funksjon er kort sagt et subprogram i selve programmet vårt, som kan kalles dvs. kjøres hvor som helst i koden. En av hovedgrunnene til å bruke funksjoner er ryddighet og forenkling av koden. Hvis vi tenker oss et spill, vil det antageligvis ha mange, mange funksjoner som hver har sin oppgave: En funksjon sjekker kanskje om spilleren er borti gulvet, en annen flytter kanskje fiender, og en annen renderer (tegner) skjermbildet. Denne fordelingen av oppgaver ut over flere funksjoner gjør koden mye mer oversiktlig og gjør det også dermed lettere å ordne bugs og feil.
Dersom du er kjent med funksjonsbegrepet i matematikk vil du nok kjenne deg igjen her etterhvert, for funksjoner i C er ikke helt ulike de vi finner i matematikken.
En funksjon i C har tre hovedpunkt: Ta noen (valgfrie) argumenter, gjøre noe (evt. med argumentene) og så evt. returnere en returverdi.
Definering av funksjoner, argumenter, returverdi
redigerVi definerer en funksjon slik:
returtype navn (argumentliste) { funksjonskropp }
Returtypen er den typen den eventuelle returverdien skal være av. Navnet kan være hva som helst, men må ikke være det samme som navnet på en variabel, og selvsagt ingen reserverte navn. Argumentlista er argumentene, eller innverdiene til funksjonen. Vi skal se på argumentlista om litt. Slik kan vi definere en enkel funksjon som printer en tekst:
#include <stdio.h> void PrintTekst(void) { printf("Jeg er en funksjon.\n"); } int main(void) { printf("La oss kalle samme funksjon tre ganger ...\n"); PrintTekst(); PrintTekst(); PrintTekst(); return 0; }
Merk hvordan vi kaller (kjører) funksjoner ved å oppgi navnet etterfulgt av argumentlista (som her er tom). Legg også merke til at vi i definisjonen av PrintTekst skriver void. Dette er for å fortelle kompilatoren at funksjonen ikke tar noen argumenter.
I eksempelet over vil vi få teksten "La oss kalle samme funksjon tre ganger..." på skjermen én gang, og "Jeg er en funksjon." på skjermen tre ganger. Dette er fordi vi kaller funksjonen PrintTekst tre ganger.
I definisjonen av PrintTekst introduserte jeg en ny variabeltype, nemlig void. void er faktisk en type vi bruker når vi ikke vet hvilken type verdi det er snakk om, men i funksjonssammenheng bruker vi det til å fortelle at funksjonen ikke returnerer noen verdi. I argumentlista bruker vi den til å fortelle at funksjonen ikke tar noen argumenter.
La oss se på en funksjon som tar et nummer som argument og printer dette på skjermen for å bekrefte at den fikk et nummer:
#include <stdio.h> void PrintTall(int tall) { printf("Jeg fikk %i som argument!", tall); } int main(void) { PrintTall(5); PrintTall(1234); PrintTall(99999999); PrintTall(1337); return 0; }
Her definerer vi funksjonen PrintTall til å ta i mot et argument av typen int som vil få navnet tall i funksjonskroppen. Som du ser er det bare i definisjonen av PrintTall vi oppgir hvilken type argumentet er av. Når vi kaller funksjonen, trenger vi ikke oppgi dette -- vi trenger bare å oppgi verdien vi vil gi til funksjonen.
En funksjon kan også ha flere argumenter. Da skiller vi dem fra hverandre med komma i både definisjonen og når vi kaller den. Se på denne totalt unyttige funksjonen som legger sammen to tall:
void LeggSammen(int a, int b) { printf("%i + %i = %i", a, b, a + b); }
Denne kan vi da kalle slik: LeggSammen(3, 8); og få "3 + 8 = 11" på skjermen når vi kaller funksjonen.
Funksjoner kan også returnere en returverdi. For å gjøre dette, legger vi til en return der hvor vi vil returnere fra funksjonen. Vi må også oppgi en returtype, annet enn void. Vi kan f.eks. modifisere LeggSammen litt:
#include <stdio.h> int LeggSammen(int a, int b) { return a + b; } int main(void) { printf("5 + 5 = %i", LeggSammen(5, 5)); printf("3 + 6 = %i", LeggSammen(3, LeggSammen(3, 3))); return 0; }
Her ser vi at LeggSammen tar inn to tall, a og b og returnerer summen av dem. Merk hvordan vi kaller LeggSammen direkte inne i selve printf. Dette går an de innerste uttrykkene alltid evalueres først. Legg også merke til hvordan det samme blir gjort på den siste printf-en: Vi kaller LeggSammen med 3 og returverdien vi får når vi kaller LeggSammen med 3 og 3. Poenget er at returverdien av en funksjon kan brukes i stedet for funksjonen. Jeg kan f.eks. gjøre følgende:
int sum = LeggSammen(3, 3);
Da vil sum få verdien 6.
Vi kan også bruke return flere steder i koden. Ta f.eks. følgende versjon av den matematiske funksjonen abs som returnerer den absolutte (ikke-negative) verdien av et tall. I matematikken er abs definert slik:
abs(x) = x hvis x > 0 0 hvis x = 0 -x hvis x < 0
Vi kan enkelt lage vår egen C-versjon av abs (selv om den finnes fra før i math.h):
int abs(int x) { if (x > 0) return x; if (x == 0) return 0; if (x < 0) return -x; }
Vi kan selvsagt definere funksjoner som tar andre elementtyper enn bare int. Vi kan også definere funksjoner som tar hele tabeller og strenger. Vi kan f.eks. ha følgende funksjon som setter et element i en tabell av typen int til en ny verdi og returnerer den gamle:
int SettTabell(int tabell[], int indeks, int nyverdi) { int gammelverdi = tabell[indeks]; tabell[indeks] = nyverdi; return gammelverdi; }
Merk at vi ikke oppgir hvor stor tabell er i definisjonen til SettTabell. Dette er fordi C ikke holder oversikt over hvor store tabellene er. Det har både gode og dårlige følger: Det positive er at SettTabell kan ta inn tabeller av forskjellige størrelser som argument, men på den andre siden blir det vanskelig å sjekke om indeks er større enn størrelsen på tabellen, noe som selvsagt vil føre til feil.
Argumentvariabler, lokale og globale variabler, scope
redigerLa oss se litt nærmere på hva som skjer når vi kaller abs med f.eks. tallet 3. Det som skjer da, er at tallet 3 kopieres over i variabelen x til abs. Dette gjelder også når abs kalles med en variabel som argument:
int n = 3; abs(n);
Verdien av n blir da kopiert over i x'en til abs. Dersom vi gjør følgende inne i abs
x = 99;
... vil ikke n i funksjonen den ble kalt fra bli 99, bare x innenfor abs. De eneste argumenttypene som er unntak fra dette, er tabeller, som du så et eksempel på i forrige del av dette kapittelet (funksjonen som erstattet et element i en tabell med en ny verdi og returnerte den som var der før).
Det siste vi skal innom i dette kapittelet er globale og lokale variabler, og scopet (virkeområdet) de har. En variabel som defineres innenfor en funksjon, om det så er main eller en annen funksjon, er definert lokalt for den funksjonen. Det vil si at den variabelen ikke gjelder innenfor andre funksjoner. Om vi referer til navnet på en variabel som er definert i funksjon a1 fra funksjon a2, vil vi få en feilmelding om at variabelen ikke er definert. Vi sier at scopet til den lokale variabelen bare er innenfor funksjonen dens. Dette betyr også at flere funksjoner kan ha egne variabler med samme navn som andres; a1 kan ha en variabel kalt x, og a2 kan ha en som heter x, uten at de er i konflikt med hverandre.
Variabler som defineres utenfor noen funksjon blir automatisk definert som globale variabler. Globale variabler har en rekkevidde som går over hele programmet, dvs. alle funksjonene. Det betyr at vi kan referere til den fra alle funksjoner, uten at den nødvendigvis er definert der. Men, vi kan også definere lokale variabler med samme navn som den globale. Det som skjer da, er at den lokale variabelen overskygger den globale variabelen helt til rekkevidden til dens tar slutt.
Vi kan oppsummere dette i følgende program:
#include <stdio.h> int y = 1001; void Funksjon1(void) { int x = 1234; printf("X er %i og Y er %i.\n", x, y); } void Funksjon2(void) { int x = 4321, y = 999; printf("X er %i og Y er %i.\n", x, y); } int main(void) { printf("Y er nå %i.\n", y); Funksjon1(); Funksjon2(); printf("Y er nå %i.\n", y); return 0; }
Her vil vi få følgende utskrift:
Y er nå 1001. X er 1234 og Y er 1001. X er 4321 og Y er 999. Y er nå 1001.
Dette er riktignok ikke alt om scope. Det gjelder nemlig ikke bare funksjoner, men programblokker generelt. Vi kan f.eks. ha følgende kode:
int main(void) { int x = 10; printf("X er %i\n", x); { int x = 20; printf("X er %i.\n", x); } printf("X er %i\n", x); return 0; }
Denne koden gir følgende utskrift:
X er 10 X er 20 X er 10
Denne overskyggingen fungerer akkurat slik som med funksjoner: For hver programblokk med en variabeldefinisjon blir det laget en ny variabel av samme navn, og den gamle verdien blir "tatt vare på" når scopet til den programblokken slutter.
Rekkefølge på funksjoner i koden, deklarasjon
redigerI C må også funksjoner, som variabler, være definert over uttrykkene der de tas i bruk. Noe annet vil føre til en feilmelding:
int main(void) { printf("Det dobbelte av 2 er %i\n", dobbel(2)); return 0; } int dobbel(x) { return x * 2; }
Her er double definert etter der den brukes (i main.) Dette vil som sagt mest sannsynlig føre til en feilmelding, og det er ikke C-standard å gjøre det. For å løse problemet kan vi selvsagt bare definere dobbel over main, men dette kan i lengden bli slitsomt, spesielt når flere funksjoner bruker den. Da kan lett rekkefølgen på funksjonene blir et problem. Heldigvis er det en løsning på dette; vi kan deklarere funksjonen øverst i programmet vårt. Deklarasjonen forteller kompilatoren at navnet som blir deklarert refererer til en funksjon som blir definert senere, som tar de og de argumentene og returnerer en verdi av typen x. Dermed kan vi skrive selve koden til funksjonen akkurat hvor vi vil (så lenge det ikke er i en annen funksjon da.)
En funksjonsdeklarasjon består essensielt bare av den første delen av funksjonen: returtypen, navnet og argumentlista. Selve koden kommer i definisjonen, som er akkurat som før. En deklarasjon av SettTabell ser da slik ut:
int SettTabell(int tabell[], int indeks, int nyverdi);
Faktisk må vi ikke skrive navnene på argumentene til funksjonen heller, vi kan gjerne utelate dem, men datatypen må fortsatt være med. Vi kan altså deklarere SettTabell slik om vi vil:
int SettTabell(int [], int, int);
Det er opp til hver enkelt hvordan man vil gjøre det, men det er mest oversiktlig å ha med variabelnavnene -- da blir deklarasjonene som en innholdsliste over programmet.
Vi kan nå skrive om eksempelet over (som gir feil) slik at det fungerer, ved å deklarere dobbel først:
#include <stdio.h>
/* Deklarasjon */
int dobbel(int x);
int main(void)
{
printf("Det dobbelte av 2 er %i\n", dobbel(2)); /* Merk: bruk av dobbel før den er definert */
return 0;
}
/* Definisjon */
int dobbel(x)
{
return x * 2;
}