Terug naar de inhoudsopgave

Les 6 – Functies

In deze les bespreken we de volgende onderwerpen:

Programma's splitsen

Tot nu toe hebben we al onze programma code geplaatst binnen de accolades die volgen op de regel void main(). Zodra je programma een uit enkele tientallen regels code bestaat, wordt het al snel onoverzichtelijk. Daarom gaan we in deze les kijken hoe we programma's kunnen splitsen in een aantal kleine programmaatjes.

Invoer, verwerking, uitvoer

Zoals reeds verteld werken alle computers en ook alle computerprogramma's volgens het principe invoer, verwerking, uitvoer. Dus als we ons programma gaan splitsen in kleine programma's, moeten ook al die kleine programma's voldoen aan dit principe.

Zo'n klein programma noemen we een functie. Een functie in C++ is net als een functie in de wiskunde: je stopt er iets in (invoer), de functie bewerkt dat (verwerking) en je krijgt iets terug (uitvoer). Laten we aan de hand van een voorbeeld eens bekijken hoe dat in zijn werk gaat. We schrijven een functie die de oppervlakte van een vierkant uitrekent (uitvoer). Daarvoor moet je de functie de hoogte en de breedte van het vierkant vertellen (invoer).

// rekent de oppervlakte van een vierkant uit
// hoogte:  de hoogte van het vierkant
// breedte: de breedte van het vierkant
// returns: de oppervlakte van het vierkant
int Oppervlakte(int hoogte, int breedte)
{
	// bereken oppervlakte
	int myOppervlakte = hoogte * breedte;

	// geef oppervlakte terug
	return myOppervlakte;
}	

De eerste regel (commentaar niet meegerekend) zegt meer over de manier waarop we de functie in onze code moeten gebruiken. We noemen dit het prototype van de functie. Straks zullen we uitgebreid naar het prototype kijken. De regels tussen de accolades voeren de daadwerkelijk berekening van de oppervlakt uit. Met de accolades geef je dus aan waar de functie begint en waar hij ophoudt.

Elke functie heeft een naam. In ons voorbeeld is dat Oppervlakte. Deze naam mag bestaan uit cijfers, letters en een underscore en hij mag niet beginnen met een cijfer. Dat zijn dus dezelfde regels als bij een variabele. We zijn verder vrij om een naam te kiezen, maar het is verstandig om de functie een naam te geven die duidelijk maakt wat de functie doet. Verder moet je onthouden dat je niet twee verschillende functies met dezelfde naam mag hebben.

Parameters

De functie Oppervlakte verwacht twee getallen als invoer, namelijk de hoogte en de breedte. Dit kun je zien aan de tekst die in het prototype achter de functienaam tussen accolades staat: (int hoogte, int breedte). Tussen haakjes staan de parameters. Parameters worden gescheiden door een komma. Als je een functie schrijft moet je aangeven hoe je de parameter noemt en van welke datatype de parameter is. In ons voorbeeld is er een parameter hoogte van het type int en een parameter breedte van het type int.

Binnen de functie gebruik je de parameters als variabelen. Je kunt er op dezelfde manier mee rekenen. De waarden van de parameters weet je niet als je de functie schrijft; die worden pas bepaald als de functie wordt aangeroepen.

Voor de naam van een parameter gelden in C++ dezelfde regels als voor de naam van een variabele. Wij spreken af dat we de naam van de parameter altijd beginnen met een kleine letter zonder het voorvoegsel my. Op die manier kunnen we parameters makkelijk van variabelen onderscheiden.

Return value

Een functie kan hooguit één waarde als uitvoer hebben. Deze waarde noemen we de return value. Ook van de return value moet je aangeven van welk datatype het is. Dit doe je door het datatype in het prototype voor de naam van de functie te zetten. In ons voorbeeld is de return value dus van het type int.

Uiteraard moet je in de code van de functie aangeven welke waarde de return value moet krijgen. Dit doe je door de opdracht return te geven met daarachter de waarde die de functie als uitvoer heeft. Dit kan een variabele of een letterlijke waarde zijn.

Commentaar

Bij elke functie moet je commentaar schrijven. De eerste regel vertelt wat de functie doet. Daarna geef je van elke parameter een korte omschrijving. Ten slotte geef je aan wat de functie teruggeeft.

Functies aanroepen

Nu we de functie Oppervlak geschreven hebben, kunnen we hem gaan gebruiken. Onderstaand voorbeeld rekent de oppervlakte uit van een vierkant van vijf hoog en drie breed.

/*
	Programmeur:  dhr. Ronkes Agerbeek
	Programma:    Oppervlakte

	Omschrijving: Berekent de oppervlakte van een vierkant.
*/

#include <iostream>

using namespace std;

// rekent de oppervlakte van een vierkant uit
// hoogte:  de hoogte van het vierkant
// breedte: de breedte van het vierkant
// returns: de oppervlakte van het vierkant
int Oppervlakte(int hoogte, int breedte)
{
	// bereken oppervlakte
	int myOppervlakte = hoogte * breedte;

	// geef oppervlakte terug
	return myOppervlakte;
}

// het programma start hier
void main()
{
	// schrijf oppervlakte van vierkant naar het scherm
	cout << "Een vierkant van 5 hoog en 3 breed heeft "
	cout << "een oppervlakte van " << Oppervlakte(5, 3);
}	

Je roept een functie aan door de naam van de functie op te geven, gevolgd door de waarden van de parameters. Om een functie aan te roepen hoef je dus niet te weten hoe de parameters heten, als je maar weet hoeveel parameters je op moet geven.

De waarde die een functie uitvoert kun je opslaan in een variabele, gebruiken in een berekening of - zoals hierboven - direct naar het scherm schrijven. Je gebruikt hem dus net als een variabele.

Functies testen

Vaak roep je een functie aan op vele verschillende plaatsen in je programma. Het is dus van groot belang dat je functie geen fouten bevat. Dit betekent dat je je functies goed moet testen.

Unit testing

Unit testing is een manier om te zorgen dat de functie die je schrijft correct is en correct blijft. Een unit test is zelf ook een functie. Binnen een unit test, roep je een aantal keer de functie aan die je wilt testen en je kijkt of de uitkomst klopt. Aan de hand van een voorbeeld zal duidelijker worden hoe unit testing werkt. Eén ding moet je goed onthouden: je schrijf eerst je test en daarna pas de functie.

In ons voorbeeld zullen we een functie schrijven die rente berekent. De functie verwacht het startbedrag en de rente per maand als invoer en geeft als uitvoer hoeveel geld er op de rekening staat als de rente erbij opgeteld is. Het prototype van de functie ziet er dus als volgt uit.

float Rente(float startbedrag, float percentage)	

We beginnen met het schrijven van een hele simpele unit test.

// unit test voor de functie Rente
// returns: true als de test slaag, anders false
bool RenteTest()
{
	// test met een startbedrag van EUR 0
	if (Rente(0.0, 1.0) != 0.0)
	{
		// test niet geslaagd
		return false;
	}

	// alle testen geslaagd
	return true;
}	

Bovenstaande functie voert één unit test uit. Voordat we gaan kijken welke test dat is, even iets over de functie zelf. De naam van de functie is gelijk aan de naam van de functie die getest wordt plus het woord Test. Dit is een afspraak die we maken: zo kunnen we makkelijk zien dat het om een unit test gaat. Aan het prototype kun je zien dat de functie geen parameters verwacht en dat hij een waarde teruggeeft van het type bool. De functie geeft true terug als alle unit tests geslaagd zijn en false als één van de testen mislukt is.

Nu iets over de bovenstaande test. We weten dat de functie Rente altijd 0 terug moet geven als het startbedrag 0 is. Immers, je kunt nog zoveel rente krijgen, als je geen geld inlegt, krijgt je ook niets terug. De test roept dus de functie Rente aan met een startbedrag van 0 en een willekeurige waarde voor het percentage en het antwoord moet 0 zijn. Als dat niet zo is, dan geeft RenteTest de waarde false terug. Als alle testen geslaagd zijn, dan geeft RenteTest de waarde true terug.

Om de test uit te voeren schrijven we een functie UnitTest die alle testfuncties aanroept en het resultaat weergeeft. Deze functie roepen we aan vanuit main.

/*
	Programmeur:  dhr. Ronkes Agerbeek
	Programma:    Rente

	Omschrijving: Berekent het saldo na het toevoegen van
	              rente
*/

#include <iostream>

using namespace std;

// unit test voor de functie Rente
// returns: true als de test slaagt, anders false
bool RenteTest()
{
	// test met een startbedrag van EUR 0
	if (Rente(0.0, 1.0) != 0.0)
	{
		// test niet geslaagd
		return false;
	}

	// alle testen geslaagd
	return true;
}

// voert alle unit tests uit
void UnitTest()
{
	// declareer variabele om uitkomst op te slaan
	bool mySuccesful = true;

	// voer tests uit
	mySuccesful &= RenteTest();

	// zijn de tests succesvol afgerond?
	if (mySuccesful == true)
	{
		// ja, schrijf resultaat naar scherm
		cout << "Unit tests succesvol afgerond.";
	}
	else
	{
		// nee, schrijf resultaat naar scherm
		cout << "Unit tests niet geslaagd!";
	}
}

// het programma start hier
void main()
{
	// voer unit tests uit
	UnitTest();
}	

Als je bovenstaand programma compileert, krijg je een foutmelding. De functie Rente bestaat namelijk nog niet. We voegen de functie toe boven de functie RenteTest. We schrijven echter niet in één keer de hele functie: we zorgen er alleen maar voor dat de eerste unit test succesvol wordt doorlopen.

// berekent de rente als startbedrag en percentage gegeven
// zijn
// startbedrag: het bedrag waar rente bij geteld wordt
// percentage:  het rentepercentage
// returns:     het bedrag na optelling van rente
float Rente(float startbedrag, float percentage)
{
	// geef rente terug
	return 0;
}	

Om te controleren of de functie tot nu toe goed werkt, compileren we het programma en starten we het. Als alles goed is, slaagt de test en kunnen we een test toevoegen. We breiden RenteTest uit met een nieuwe test: bij een rentepercentage van 0 procent, is het resultaat gelijk aan het startbedrag.

// unit test voor de functie Rente
// returns: true als de test slaagt, anders false
bool RenteTest()
{
	// test met een startbedrag van EUR 0
	if (Rente(0.0, 1.0) != 0.0)
	{
		// test niet geslaagd
		return false;
	}

	// test met een percentage van 0
	if (Rente(100.0, 0.0) != 100.0)
	{
		return false;
	}

	// alle testen geslaagd
	return true;
}	

We compileren het programma en starten het. De test moet nu fout gaan. Het is altijd goed om te test één keer te zien mislukken. Als de test meteen lukt kan het zijn dat de test niet klopt of dat hij overbodig is. Om aan de nieuwe test te voldoen, veranderen we de functie Rente. Uiteraard moeten we ook nog steeds voldoen aan de vorige test.

// berekent de rente als startbedrag, percentage en looptijd
// gegeven zijn
// startbedrag: het bedrag waar rente bij geteld wordt
// percentage:  het rentepercentage
// returns:     het bedrag na optelling van rente
float Rente(float startbedrag, float percentage,  int
	looptijd)
{
	// is percentage 0?
	if (percentage == 0.0)
	{
		// ja, geef rente terug
		return startbedrag;
	}

	// geef rente terug
	return 0;
}	

Compileren en starten: de tests slagen. Volgens dezelfde methode voegen we één voor één tests toe, totdat de functie af is. In dit geval hebben we nog één test nodig van een specifiek geval waarvan we de uitkomst van tevoren berekenen.

// unit test voor de functie Rente
// returns: true als de test slaagt, anders false
bool RenteTest()
{
	// test met een start bedrag van EUR 0
	if (Rente(0.0, 1.0) != 0.0)
	{
		// test niet geslaagd
		return false;
	}

	// test met een percentage van 0
	if (Rente(100.0, 0.0) != 100.0)
	{
		return false;
	}

	// test voorberekende geval
	if (Rente(100.0, 0.5) != 50.0)
	{
		return false;
	}

	// alle testen geslaagd
	return true;
}	

Meteen een keer uitvoeren om de test te zien mislukken en daarna de code van de functie Rente aanpassen.

// berekent de rente als startbedrag en percentage
// startbedrag: het bedrag waar rente bij geteld wordt
// percentage:  het rentepercentage
// returns:     het bedrag na optelling van rente
float Rente(float startbedrag, float percentage)
{
	// geef rente terug
	return startbedrag * percentage;
}	

Niet alleen hebben we nu een functie geschreven, we weten ook dat de functie goed werkt. Nu kun je de aanroep van UnitTest weghalen uit main en de functie gebruiken voor een echt programma. De testfuncties zelf moet je laten staan. Als je de functie later uitbreidt of aanpast, kun je door de unit tests uit te voeren heel snel zien of je niet per ongeluk een fout hebt geïntroduceerd. Je functie werkt dus niet alleen nu goed, hij blijft goed werken. Dat scheelt later een hele hoop werk.

Bij de les