C/Kontrollstrukturer

< C

I dette kapittelet skal vi se på kontrollstrukturene C tilbyr. I et hvert programmeringsspråk har man behov for å styre programflyten slik at man kan få programmet til å gjøre ting basert på visse tilfeller, eller kjøre en rekke uttrykk om igjen flere ganger. Vi deler kontrollstrukturene inn i to typer: betingede kontrollstrukturer og løkkestrukturer. Vi starter med førstnevnte.

Noen ganger har vi behov for å gjøre noe, men bare i et visst tilfelle. For eksempel må en spillmotor flytte en fiende, men bare dersom fienden ikke allerede er i en vegg, eller et passordbeskyttet program må ha en måte å unngå at programmet blir kjørt dersom feil passord tastes. Hvordan kan man bestemme når kode skal kjøres og når den ikke skal kjøres? Det er her man bruker kontrollstrukturene for å styre programflyten basert på reglene vi setter opp i programmet.


Betinget valg: if

rediger

Den enkleste og mest brukte kontrollstrukturen er if ("hvis" på norsk). if har følgende "skjelettform":

if (testuttrykk) gjør dette

I if-uttrykket testes først testuttrykket. Dette kan være en funksjon, flere funksjoner, et operatøruttrykk, eller mer generelt: et uttrykk som returnerer sant eller usant. Dersom testuttrykket returnerer sant, vil "gjør dette"-delen av if-uttrykket kjøres. Hvis ikke, blir den hoppet over. Dette kan illustreres ved å redigere det forrige eksempelet i kapittelet om scanf:

#include <stdio.h>

int main(void)
{
	char navn[20];
	int alder;

	printf("Navnet ditt: ");
	scanf("%s", navn);
	printf("Alderen din: ";
	scanf("%i", &alder);
	printf("Hei, du heter %s og er %i år gammel.\n", navn, alder);
	if (alder > 120)
		printf("(Burde ikke du vært død?\n)");
	return 0;
}

Her ser vi at det er lagt til et if-uttrykk. I dette er det introdusert en ny operatør; "større enn"-operatøren, >. Denne betyr det samme som i matematikken. I C vil den returnere sant dersom det til venstre for den er større enn det til høyre, og usant dersom det er mindre. Her ser vi den i bruk i testuttrykket til if:

if (alder > 120)
    printf("(Burde ikke du være død?)");

Her sammenlignes innholdet i alder med 120, for å se om alder er større. Dersom innholdet er større, blir uttrykket til if (printf-uttrykket i dette tilfellet) kjørt. Hvis ikke, blir det hoppet over. Dermed vil vi få omtrent følgende når vi kjører programmet:

Navnet ditt: Johan
Alderen din: 17
Hei, du heter Johan og er 17 år gammel.

--- neste kjøring av programmet: ---

Navnet ditt: Johan
Alderen din: 123
Hei, du heter Johan og er 123 år gammel.
(Burde ikke du være død?)

Merk at if-uttrykket ikke har to semikolon, men bare ett. Det er altså ikke noe semikolon etter test-delen av if selv om "gjør dette"-delen kommer på neste linje (noe den strengt tatt ikke må, men bør for lettleselighetens skyld). Hvis du tenker deg om er dette egentlig logisk: både testuttrykket og det som skal kjøres om det er sant hører jo sammen -- med andre ord er de ett uttrykk.

Et problem oppstår derimot når man vil utføre flere linjer med kode når testuttrykket er sant. Dersom man legger til en ekstra linje, f.eks. slik:

if (alder > 120)
	printf("(Burde ikke du være død?)\n");
	printf("Bare tuller.\n");

Her antar du kanskje at vi får "Bare tuller." når alderen er større enn 120, men det skjer ikke: "Bare tuller." vises selv om alder ikke er større enn 120. Dette er fordi if bare tar ett uttrykk som skal kjøres når testuttrykket er sant. Løsningen på problemet er å legge alt vi vil skal skje når testuttrykket er sant i en programblokk, som du har sett eksempler på tidligere. Programblokken binder uttrykkene slik at de sammen kan brukes som uttrykket som skal kjøres når testuttrykket i if er sant. Den rette koden er altså:

if (alder > 120) 
{
	printf("(Burde ikke du være død?)\n");
	printf("Bare tuller.\n");
}

(Merk: noen foretrekker å ha startklammen på samme linje som testuttrykket til if. Det er ingen standard på dette, så her må du finne ut hva du liker best.)

Når if ikke er nok: else og else if

rediger

I noen tilfeller vil du at kode skal kjøre dersom et uttrykk ikke er sant, men at den koden ikke skal kunne kjøres dersom testuttrykket var sant. I dette tifellet må du bruke else som fungerer slik:

else gjør-dette

Som if kan "gjør-dette"-uttrykket være ett uttrykk, eller flere uttrykk samlet i en programblokk. Et eksempel på else kan være følgende:

#include <stdio.h>

int main(void)
{
	char navn[20];
	int alder;

	printf("Navnet ditt: ");
	scanf("%s", navn);
	printf("Alderen din: ");
	scanf("%i", &alder);
	printf("Hei, du heter %s og er %d år gammel.\n", navn, alder);
	if (alder > 120)
		printf("(Burde ikke du vært død?)\n");
	else
		printf("(Du har sikkert lenge igjen å leve.)\n");
	return 0;
}

Her får vi da utskriften "(Du har sikkert lenge igjen å leve.)" når alder er mindre enn 120 (med andre ord når testuttrykket til if returnerte usant og else-koden ble kjørt). Dersom vi ikke hadde hatt else her, ville "(Du har sikkert lenge igjen å leve.)" blitt vist uansett om alder var større enn 120 eller ikke.

En siste form av if er else if som fungerer slik:

else if (uttrykk) gjør-dette

Testuttrykket i else if evalueres dersom if-uttrykket over resulterte i usant. Dersom testuttrykket til else if også returnerer usant, vil eventuelle andre else if-uttrykk eller else-uttrykk kjøres. Med else if kan vi modifisere alderprogrammet vårt ytterligere:

#include <stdio.h>

int main(void)
{
	char navn[20];
	int alder;

	printf("Navnet ditt: ");
	scanf("%s", navn);
	printf("Alderen din: ");
	scanf("%i", &alder);
	printf("Hei, du heter %s og er %d år gammel.\n", navn, alder);
	if (alder > 120)
		printf("(Burde ikke du være død?)\n");
	else if (alder > 80)
		printf("(Gamleheimen neste?)\n");
	else if (alder > 60)
		printf("(Din pensjonist ...)\n");
	else if (alder == 14)
		printf("(Fjortis!)\n");
	else
		printf("(Du har sikkert lenge igjen å leve.)\n");
	return 0;
}

Her ser vi at alder blir sammenlignet med 120, så 80, 60, og 14. I det siste else if-uttrykket ser vi enda en ny operatør, nemlig likhetsoperatøren, ==. Den sammenligner to verdier og returnerer sant dersom de er like. Merk at det er forskjell mellom tilordning (=) og likhetsoperatøren (==)! Senere i dette kapittelet kommer en gjennomgang av de forskjellige relasjonsoperatørene.

Dersom ingen av verdiene passer alder, det vil si når den inneholder et tall mindre eller lik 60, og som ikker er 14, printes "(Du har sikkert lenge igjen å leve.)", fordi den er i else-uttrykket som kjøres når alle andre if og else if-uttrykk feiler (ikke evaluerer til sant).

Når noe skal sammenlignes med mange verdier: switch

rediger

I forrige eksempel sammenlignet vi alder med flere forskjellige tall og printet forskjellige setninger basert på resultatet (f.eks. om alder var større enn 60). Dette førte til en repetert bruk av else if og til slutt en else. Slike former for sammenligninger er veldig vanlige, og enda vanligere er tilfeller der man sammenligner en variabel med mange forskjellige testverdier. Disse sammenligningene er såpass vanlige at C har en annen måte å gjøre de på, som på mange måter er mer effektiv, nemlig switch. Den har følgende basis eller skjelettform:

switch (testverdi) 
{
	case 1: gjør noe dersom 1
	case 2: gjør noe dersom 2
	...
	case X: gjør noe dersom X
	
	default: gjør noe dersom ingen av delene over
}

Testverdien, som selvsagt ikke er en konstantverdi, men en variabel eller noe vi ikke vet hva er før programmet kjøres, sammenlignes med alle case-verdiene. Om de er like, blir koden i den aktuelle case-en kjørt. Noe som er litt uvanlig med switch er at den ikke behøver noen programblokk med { og } selv om man vil kjøre mer enn ett uttrykk. I stedet må man huske å sette inn et break-uttrykk i stedet, som forteller at case-blokken slutter der. Et eksempel på switch er dette totalt ubrukelige programmet:

#include <stdio.h>

int main(void)
{
	int tall = 0;

	printf("Skriv et tall: ");
	scanf("%i", &tall);

	switch(tall)
	{
		case 1:
			printf("1 er et bra tall.\n");
			break;
		case 2:
			printf("2 rimer på sko.\n");
			break;
		case 5:
			printf("5 er slem.\n");
			break;
		case 1234:
			printf("1234 er et magisk tall.\n");
		case 1235:
			printf("1235 er ikke et magisk tall.\n");
			break;
		default:
			printf("Jeg kan ikke noe om det tallet, jeg.\n");
	}

	return 0;
} 

Her spør vi enkelt og greit om et tall, og så blir dette tallet sammenlignet med en rekke verdier i switch-uttrykket. Der hvor det ikke finner en match, blir koden under default: kjørt.

Legg merke til at alle case-blokkene med kode avsluttes med et break-uttrykk. Hvis det ikke hadde vært der, ville programflyten fortsatt ned i neste case og kjørt koden der også! Dette er selvsagt ingen bug -- det kan ha fordeler i enkelte applikasjoner. Du ser et eksempel på hva som skjer om vi unnlater å skrive break i case 1234:

Skriv et tall: 1234
1234 er et magisk tall.
1235 er ikke et magisk tall.


Relasjonsoperatører og logiske operatører

rediger

C har en håndfull operatører som er essensielle å kunne:

>    - større enn
<    - mindre enn
==   - likhet
!=   - ulikhet
>=   - større eller lik
<=   - mindre eller lik

Noen ganger vil vi at noe skal skje dersom ikke bare ett, men flere uttrykk er sanne. I et slikt tilfelle er det lett å tenke seg noe slikt:

#include <stdio.h>

int main(void)
{
	int alder = 0;

	printf("Alderen din: ");
	scanf("%i", &alder);
	if (alder > 12)
		if (alder < 20)
			printf("Du er en tenåring.\n");
	return 0;
}

Her sjekker vi først om den inntastede alderen er større enn 12, og så om den er mindre enn 20. Hvis begge testuttrykkene evaluerer til sant, vil "Du er en tenåring." vises. Men det er en bedre og enklere måte å gjøre dette på: Man kan kombinere flere testuttrykk v.h.a. operatører som kombinerer sant/usant-verdier til et samlet resultat. Disse er:

&&  - AND (og)
||  - OR (eller)
!   - NOT (ikke)

&& bruker du når du vil at if-uttrykkets kode skal kjøre bare dersom alle testuttrykkene evaluerer til sant. || bruker du når det er nok at bare ett av uttrykkene evaluerer til sant. Kort sagt:

AND:		OR:             NOT:

S && S = S	S || S = S      !S = U
S && U = U	S || U = S      !U = S
U && S = U	U || S = S
U && U = U	U || U = U

Hvor S står for sant og U for usant. ! kan virke ulogisk til å begynne med -- det eneste den gjør er å "snu" på sannhetsverdien, slik at om den er sann blir den usann, og omvendt. ! brukes når du vil at noe skal skje dersom et uttrykk ikke er sant. Når et uttrykk ikke er sant, vil ! snu det til sant, og f.eks. et if-uttrykk vil kjøre likevel.

&& kan brukes slik, som en forenkling av programmet vi skrev tidligere:

#include <stdio.h>

int main(void)
{
	int alder = 0;

	printf("Alderen din: ");
	scanf("%i", &alder);
	if (alder > 12 && alder < 20) {
		printf("Du er en tenåring.\n");
	}
	return 0;
} 

Her bruker vi && fordi alderen må være både større enn 12 og mindre enn 20 for at personen er en tenåring.

(Merk: Dersom du skriver & i stedet for && vil du ikke få noen feilmelding, for & er også en operatør, men du vil ikke få den effekten du ønsket. Pass derfor på å skrive to &-er.)

Løkkestrukturer

rediger

(Merk: Det jeg kaller "løkke" her, er loop på engelsk.)

En annen kontrollstrukturtype, er løkkestrukturene. De brukes når vi vil kjøre kode om igjen flere ganger. En del av programmet som kjøres om igjen og om igjen, kaller vi en løkke. Om vi lager en IRC-klient, har vi gjerne en løkke som henter data fra IRC-serveren, skriver til skjermen, henter data fra tastaturet, og sender tilbake til IRC-serveren. Denne prosessen gjentar seg hele tiden, til IRC-klienten logger av, eller når det skjer en feil o.l. Vi sier da at løkken avslutter eller avbrytes.


Enkel løkke: while

rediger

Den enkleste løkketypen er while. Den evaluerer et testuttrykk, i likhet med if og kjører ett eller flere uttrykk om igjen så lenge det er sant. Skjelettet til while er slik:

while (testuttrykk) gjør dette

Testuttrykket evalueres og så lenge det evaluerer til sant, vil "gjør dette"-uttrykkene kjøres.

Tenk deg at du vil skrive "Geiter er rare dyr." på skjermen 10 ganger nedover. Det første du tenker er kanskje å sette opp en stor printf med 10 like linjer. Dette lukter det forenkling av, og det er akkurat det vi skal gjøre: Vi lager en løkke:

#include <stdio.h>

int main(void)
{
	int linjer = 0;

	while (linjer < 10) {
		printf("Geiter er rare dyr.\n");
		linjer++;
	}

	return 0;
} 

Det første vi gjør er å opprette en variabel, linjer. Denne skal holde hvor mange linjer vi har skrevet til skjermen. Deretter entrer vi while-løkka. Testuttrykket er linjer < 10 som vil evaluere til sant så lenge linjerer mindre enn 10. I kroppen (uttrykkene som skal kjøres) til while printer vi teksten på skjermen, og deretter øker vi linjer med 1. Dermed vil løkka kjøres om og om igjen, til linjer har økt til 10. Da avsluttes løkka, og teksten er printet 10 ganger til skjermen.

En annen form av while er en do-while-løkke. Den har testuttrykket til slutt i stedet for i starten av løkka. Skjelettformen er slik:

do dette while (testuttrykk)

Du lurer kanskje på hva som er forskjellen på å teste i starten og slutten av løkka. Det er i grunn noe man får bruk for i større og mer komplekse løkker, noe du sikkert vil oppdage. Uansett kan vi skrive eksempelprogrammet vårt slik i do-while-form:

#include <stdio.h>

int main(void)
{
	int linjer = 0;

	do {
		printf("Geiter er rare dyr.\n");
		linjer++;
	} while (linjer < 10);

	return 0;
}


Sannhet, uendelige løkker og break

rediger

Det er noen viktige ting man må passe seg for når man lager løkker. Det er viktig å unngå uendelige løkker, det vil si løkker som aldri slutter fordi testuttrykket aldri blir usant. Hadde vi f.eks. glemt linjer++; i forrige eksempel, ville linjer aldri nådd 10, og løkka vil dermed aldri sluttet. Resultatet hadde blitt at "Geiter er rare dyr." hadde blitt printet igjen og igjen nedover, til det ble avsluttet ved at konsollvinduet ble lukket.

Når denne advarselen er sagt, skal det også sies at uendelige løkker kan avbrytes og at uendelige løkker kan være nyttige i enkelte tilfeller. For å lage en uendelig løkke, må vi altså ha et testuttrykk som evaluerer til sant hele tiden. Dette kan vi selvsagt gjøre på mange måter, men for å lage et testuttrykk som [u]alltid[/u] er sant, må vi finne en måte å gi while sant hele tiden. For å få til det, må vi se litt på hvordan sannhet fungerer i C.

I C representeres sant av tallet 1 og usant av 0. Det betyr at alle utttrykk som evaluerer til sant eller usant, egentlig evaluerer til 1 eller 0. Hvis vi ser på følgende uttrykk, der vi antar at x er lik 5:

if (x == 5) printf("Lik 5.");

Her vil x == 5 evalueres, og likhetsoperatøren (==) vil returnere 1 fordi x og 5 er like. Vi får da følgende if-uttrykk:

if (1) printf("Lik 5.");

Dette er noe som skjer når programmet kjøres, siden verken vi eller kompilatoren kan vite hva xer på forhånd (f.eks. om den blir gitt verdi av brukeren vha. scanf).

Med denne kunnskapen inne, bør det være ganske klart hvordan vi skaper en uendelig løkke uten noe testuttrykk:

while (1) gjør dette

Her er "testuttrykket" 1, altså sant, og kan aldri endres av uttrykkene i kroppen til while. Hadde vi brukt 0 som testuttrykk, ville løkka aldri blitt kjørt fordi den da var usann med en gang.

Men hvordan skal vi avbryte løkka, når vi ikke kan gjøre det ved hjelp av testuttrykket? Da bruker vi break, som vil avslutte løkken den er inni når den kjøres. Eksempelprogrammet der vi printet 10 linjer vil da se slik ut i en uendelig løkke med break:

#include <stdio.h>
 
int main(void)
{
	int linjer = 0;

	while (1) {
		printf("Geiter er rare dyr.\n");
		linjer++;
		if (linjer == 10) 
			break;
	}

	return 0;
} 

Her vil løkka i utgangspunktet kjøre evig, siden testuttrykket alltid er sant. Vi printer teksten og øker linjer. Men når linjer er lik 10, er if-uttrykket sant, og løkken avbrytes med break.

Systematisk løkke: for

rediger

I mange tilfeller lager vi løkker som gjør noe et visst antall ganger, som teller seg oppover eller nedover, eller lignende operasjoner. Slike løkker kan skrives enklere med for. for har en slik skjelettform:

for (startuttrykk; testuttrykk; økningsuttrykk)

for er med andre ord litt mer komplisert enn while. Startuttrykket evalueres kun én gang, nemlig når løkka startes. Der initialiserer man som regel variabler man skal bruke i løkka. Testuttrykket fungerer på samme måte som i while -- det testes hver gang en runde i løkka har kjørt, for å sjekke om den skal ta en runde til. Det siste er økningsuttrykket, som utføres på slutten av hver runde. Dette er ganske mye å få med seg. Kanskje denne omskrivingen av eksempelprogrammet hjelper:

#include <stdio.h>

int main(void)
{
	int linjer;

	for (linjer = 0; linjer < 10; linjer++)
		printf("Geiter er rare dyr.\n");

	return 0;
}

Her ser vi at for tar seg av veldig mye for oss. Det første vi gjør er selvsagt å definere linjer. Når forstarter, setter vi linjer til 0. Husk at startuttrykket bare kjøres en gang, når løkka starter. Testuttrykket setter vi til det samme som da vi brukte while. Økningsuttrykket setter vi til linjer++;. Det eneste vi da trenger å gjøre i kroppen til for, er å printe teksten. Oppsett og økning av linjer og testing av løkkeslutt tar forseg av.

Nå kan for kanskje virke som en overflødig og komplisert løkkestruktur. Men tenk oss at vi har to variabler der den ene skal telle til 10 og den andre fra 10 til 0. Dette kan vi gjøre slik med for:

#include <stdio.h>

int main(void)
{
	int teller1, teller2;

	for (teller1 = 0, teller2 = 10; teller1 < 10; teller1++, teller2--)
		printf("Teller 1: %i | Teller 2: %i\n", teller1, teller2);

	return 0;
}

Dette kunne vi selvsagt gjort med en while hvor vi manuelt økte teller1 og senket teller2, men det kan gjøres såpass enkelt med for. Merk at vi her skiller flere uttrykk fra hverandre med komma. For eksempel ser økningsuttrykket slik ut: teller1++, teller2--.