Terug naar de inhoudsopgave

Les 12 – Projecten

Tot nu toe hebben we al onze code in één bestand gezet. Dit wordt al snel onoverzichtelijk en als je met meerdere mensen aan een programma werkt is het zelfs onmogelijk vol te houden. Deze les kijken we naar een manier om programmacode te verspreiden over meerdere bestanden.

Functieprotoypes

Voordat we met bestanden aan de slag gaan, eerst nog even iets over functies. C++ kan een functie pas aanroepen, als hij weet dat die functie bestaat (typisch geval van 'duh', dacht ik zo). Omdat C++ bronbestanden van boven naar beneden verwerkt, moet de functie bovendien beschreven staan, voordat hij wordt aangeroepen.

#include <iostream>

using namespace std;

// schrijft Hello, world! naar het scherm
void HelloWorld()
{
	cout << "Hello, world!";
}

// het programma start hier
void main()
{
	// dit kan, want de functie HelloWorld staat boven main
	HelloWorld();
	
	// dit kan niet, want de functie ByeByeBirdie staat onder main
	ByeByeBirdie();
}

// schrijft Bye bye, birdie! naar het scherm
void ByeByeBirdie()
{
	cout << "Bye bye, birdie!";
}
	

In het bovenstaande voorbeeld kun je de functie ByeByeBirdie niet aanroepen vanuit main, omdat C++ op dat moment nog niet weet dat de functie bestaat. In dit geval kun je natuurlijk de functie ByeByeBirdie naar boven verplaatsen, maar dat is niet altijd wenselijk en vaak ook niet mogelijk. Er is dus nog een andere oplossing. We kunnen C++ namelijk vertellen dat de functie bestaat, zonder te vertellen wat de functie doet. Dat gaat met het prototype van de functie. Vergelijk onderstaand programma met het bovenstaande programma.

#include <iostream>

using namespace std;

void ByeByeBirdie();

// schrijft Hello, world! naar het scherm
void HelloWorld()
{
	cout << "Hello, world!";
}

// het programma start hier
void main()
{
	// dit kan, want de functie HelloWorld staat boven main
	HelloWorld();
	
	// dit kan niet, want de functie ByeByeBirdie staat onder main
	ByeByeBirdie();
}

// schrijft Bye bye, birdie! naar het scherm
void ByeByeBirdie()
{
	cout << "Bye bye, birdie!";
}
	

Er is maar één regel verschil tussen beide programma's: void ByeByeBirdie();. Dit is het prototype van de functie. Hiermee vertellen we C++: 'Er bestaat een functie ByeByeBirdie, die geen return value heeft en geen parameters accepteert, dus nou niet zeuren als ik straks die functie aanroep.' En C++ doet braaf wat 'm verteld wordt.

Dat wil zeggen, de C++-compiler doet braaf wat hem verteld wordt. De linker is minder snel tevreden. Het is namelijk de taak van de linker om te zorgen dat de functieaanroep ook echt wat uitvoert. De linker gaat dus op zoek naar de implementatie (ook wel de definitie genoemd) van de functie ByeByeBirdie. Als hij die functie niet vindt, begint hij te klagen (terecht, overigens). In ons voorbeeld is de functie keurig aanwezig.

Meerdere bronbestanden

Wat heeft dit nou allemaal te maken met code verspreiden over meerdere bestanden? Nou, heel veel. We schrijven een nieuw programma en dat begint als volgt. Merk op dat ik de bestandsnaam boven de code heb gezet. Dat wordt van belang als we straks andere bestanden aan dit project gaan toevoegen.

Main.cpp
// het programma start hier
void main()
{
	// roep functie Hello aan
	Hello();
}
	

Het moge duidelijk zijn dat dit programma niet door de compiler geaccepteerd zal worden: we roepen een functie Hello aan, maar we hebben de functie nergens gedeclareerd.

O, even tussendoor: wat jargon. Het prototype van de functie heet ook wel de declaratie. Als we een functie declareren, vertellen we de compiler dus hoe de functie heet, wat zijn return value is en wat zijn parameters zijn. De code die bij de functie hoort, noemen we de definitie van de functie of de implementatie van de functie. Ja, lees deze alinea nog maar een keer door, want je hebt de termen nodig.

Terug naar het voorbeeld. We moeten een functie Hello schrijven, maar we (nou, ik, in ieder geval) willen die functie in een apart bronbestand zetten. Zo gezegd, zo gedaan.

Hello.cpp
#include <iostream>

using namespace std;

// schrijft Hello naar het scherm
void Hello()
{
	cout << "Hello";
}
	

De compiler is nog niet tevreden, want in het bestand Main.cpp is hem nog steeds niet verteld dat de functie Hello bestaat. We passen Main.cpp dus aan.

Main.cpp
void Hello();

// het programma start hier
void main()
{
	// roep functie Hello aan
	Hello();
}
	

O joy. Compiler tevreden, linker tevreden, programmeur tevreden. Alles werkt zoals verwacht. Zijn we klaar? Nee.

Headerbestanden

Ik pas de vorige bestanden even aan en ik voeg nog een bestand toe.

Main.cpp
// het programma start hier
void main()
{
	// roep Hello aan
	Hello("smurf");
	
	// roep HelloWorld aan
	HelloWorld();
}
	
Hello.cpp
#include <iostream>
#include <string>

using namespace std;

// groet een persoon
// naam: de naam van de persoon om te groeten
void Hello(string naam)
{
	cout << "Hello, " << naam;
}
	
HelloWorld.cpp
// schrijft Hello, world naar het scherm
void HelloWorld()
{
	Hello("world");
}
	

De functie Hello wordt nu niet meer alleen vanuit main aangeroepen, maar ook vanuit HelloWorld. Bovendien is het prototype veranderd. We kunnen natuurlijk zowel in Main.cpp als in HelloWorld.cpp het prototype van Hello opnemen. Maar wat nou als we wel twintig functies gebruiken, moeten we dan van elk van die functies het prototype opnemen? Dan moeten we dat prototype wel precies weten. En als - zoals in het voorbeeld - het prototype een keer verandert, dan moeten we dat overal aanpassen. Niet handig.

Vandaar headerbestanden. Een headerbestand is een bestand met declaraties. We kunnen een headerbestand in een ander bestand plakken en daarmee plakken we automatisch alle declaraties in dat bestand. Zie hier, het headerbestand voor de functie Hello.

Hello.h
#include <string>

using namespace std;

void Hello(string name);
	

Dat is alles. En we gebruiken het als volgt.

Main.cpp
#include "Hello.h"

// het programma start hier
void main()
{
	// roep Hello aan
	Hello("smurf");
	
	// roep HelloWorld aan
	HelloWorld();
}
	

Nu zeurt de compiler niet meer over de aanroep van Hello, want daar heb ik het prototype van opgegeven. Hij zeurt nog wel over HelloWorld, dus laten we dat ook maar oplossen. Bovendien zorgen we ervoor dat de functie HelloWorld de functie Hello kan aanroepen.

HelloWorld.cpp
#include "Hello.h"

void HelloWorld()
{
	// schrijf Hello, world naar het scherm
	Hello("world");
}
	
HelloWorld.h
void HelloWorld();
	
Main.cpp
#include "Hello.h"
#include "HelloWorld.h"

// het programma start hier
void main()
{
	// roep Hello aan
	Hello("smurf");
	
	// roep HelloWorld aan
	HelloWorld();
}
	

Smurf!

Goede gewoonte

Hoewel alles nu prima werkt, moeten we toch een kleine wijiziging aanbrengen aan de headerbestanden. Het kan namelijk problemen opleveren als de compiler een headerbestand meer dan één keer doorloopt. Hoewel we dat probleem voorlopig niet zullen tegenkomen, is het toch verstandig om nu al de gewoonte aan te nemen om het te voorkomen.

HelloWorld.h
#ifndef __HELLOWORLD_H__
#define __HELLOWORLD_H__

void HelloWorld();

#endif
	
Hello.h
#ifndef __HELLO_H__
#define __HELLO_H__

#include <string>

using namespace std;

void Hello(string name);

#endif
	

Door deze regels toe te voegen, onthoudt de compiler dat hij een headerbestand heeft verwerkt. De volgende keer dat hij het headerbestand tegenkomt, slaat hij het over. De namen __HELLO_H__ en __HELLOWORLD_H__ komen overeen met de namen van de headerbestanden, met wat underscores toegevoegd. We zijn vrij om deze namen zelf te kiezen, maar ze moeten uniek zijn; vandaar deze conventie.

Externe variabelen

Een globale variabele kunnen we gebruiken in alle functies in ons programma, maar als we werken met meerdere bronbestanden, dan hebben we hetzelfde probleem als met functies: we moeten C++ eerst vertellen dat er een globale variabele bestaat.

Om C++ te vertellen dat we een globale variabele gebruiken die ergens anders wordt gedefinieerd, zetten we het woord extern voor de declaratie. Uiteraard moeten we de variabele wel ergens in ons project definiëren. De voorbeeldcode laat zien hoe we omgaan met globale variabelen als er meerdere bestanden in het spel zijn.

Hello.cpp
#include <iostream>

using namespace std;

// variabele om naam van gebruiker op te slaan
string Gebruikersnaam;

// groet de gebruiker
void Hello()
{
	cout << "Hello, " << Gebruikersnaam;
}
	
Hello.h
#ifndef __HELLO_H__
#define __HELLO_H__

extern string Gebruikersnaam;

void Hello();

#endif
	
Main.cpp
#include "Hello.h"
#include <iostream>

using namespace std;

// het programma start hier
void main()
{
	// vraag naam aan gebruiker en sla op in globale variabele
	cin >> Gebruikersnaam;
	
	// groet gebruiker
	Hello();
}
	

Bij de les