|
Structuren en Unions
Een beschrijving van de structuur
Een structuur is een door de gebruiker zelf gedefinieerd datatype. Je
hebt als programmeur de mogelijkheid een datatype te definiëren dat
aanmerkelijk complexer is als de datatypes die we tot nu toe in de
cursus gebruikt hebben. Een structuur is opgebouwd uit variabelen van
datatypes die eerder werden gedefinieerd. Dit mogen ook weer structuren
zijn. Anders gezegd, een structuur is een groepering van aan elkaar
gerelateerde gegevens. Structuren kunnen het leven van de programmeur
bijzonder veraangenamen. Laten we eens naar een voorbeeld gaan kijken.
Bestudeer daarom het programma STRUCT1.C.
main ()
{
struct _field {
int sRow; /* field row */
int sColumn; /* field column */
int sLength; /* field length */
} veld1, veld2;
veld1.sRow = 6;
veld1.sColumn = 12;
veld1.sLength = 20;
veld2.sLength = 32;
veld2.sColumn = 12;
veld2.sRow = veld1.sRow + 2;
printf ("veld1 staat op rij %d, kolom %d en is %d tekens lang\n",
veld1.sRow, veld1.sColumn, veld1.sLength);
printf ("veld2 staat op rij %d, kolom %d en is %d tekens lang\n",
veld2.sRow, veld2.sColumn, veld2.sLength);
}
Het programma begint met de definitie van een structuur. Het
gereserveerde woord struct wordt gevolgd door een structuur
naam en een aantal variabele declaraties tussen accolades, die de
componenten van de structuur vormen. Na de sluitaccolade vindt je de
namen van twee variabelen, veld1 en veld2. Zoals
vastgelegd in de definitie van een structuur is veld1 nu een
variabele bestaande uit drie componenten, sRow,
sColumn en sLength. Elk van deze drie componenten
zijn gerelateerd aan veld1 en kunnen gegevens opslaan
afhankelijk van hun type. De variabele veld2 bevat ook drie
componenten, die wel dezelfde naam hebben, maar verder volledig andere
geheugenlokaties beslaan. In feite hebben we hier 6 integers
gedeclareerd.
Laten we eens wat nauwkeuriger naar de variabele veld1
kijken. De drie componenten waaruit deze structuur is opgebouwd, zijn
alle integers en kunnen in principe overal toegepast worden in een C
programma waar een gewone integer gebruikt mag worden. Je
kunt er dus mee rekenen, afdrukken, in- of uitvoeren, enz. Het probleem
is nu hoe een component van een structuur (ook wel structuur member
genoemd) aan te wijzen in een programma. We doen dit door beide namen
te gebruiken, dat wil zeggen structuur naam plus component naam met een
puntje ertussen. Dus veld1.sLength is de naam van de
variabele die de lengte component van het eerste veld aanwijst. Het is
niet toegestaan aan de component naam alleen te refereren, omdat de
compiler dan niet kan bepalen om welke component het gaat. Het opnemen
van de variabele sLength in een programma is dus zinloos en
leidt tot een foutboodschap van de compiler.
Het programma kent vervolgens aan elk van de drie componenten van
veld1 een waarde toe. De componenten van veld2
krijgen in omgekeerde volgorde een waarde, om aan te tonen dat de
volgorde waarin structuur members een waarde krijgen niet relevant is.
Nu alle variabelen een waarde hebben, kunnen we er iets mee doen. Om
het eerste voorbeeld eenvoudig te houden, werd alleen met de rij van
het tweede veld een berekening uitgevoerd en worden de variabelen
slechts gebruikt om af te drukken. Het printf() statement
vertoont geen echte bijzonderheden. De samengestelde naam van de
variabelen wordt gebruikt, omdat dit de enige geldige manier is om ze
aan te wijzen.
Structuren zijn bijzonder handig om gegevens te structureren en maken
het je makkelijker om programma's te schrijven en te begrijpen. Dit
eerste voorbeeld is te eenvoudig om je de kracht ervan te laten zien.
De volgende voorbeelden zullen je al wat meer inzicht geven en zijn een
goede basis om het Totaal Programma te doorgronden.
Compileer en test het programma.
Een tabel van structuren
Bestudeer het programma STRUCT2.C. Dit programma bevat dezelfde
structuur als het vorige, echter deze keer declareren we 8 velden in de
vorm van een tabel. Dit programma bevat dus 8 maal 3 is 24 variabelen,
die alle een integer waarde kunnen bevatten. Tevens wordt een lus
variabele gedeclareerd, om via een iteratie de structuur te vullen.
main ()
{
int index;
struct _field {
int sRow; /* field row */
int sColumn; /* field column */
int sLength; /* field length */
} veld[8];
for (index = 0; index < 8; index++) {
veld[index].sRow = index + 4;
veld[index].sColumn = 12;
veld[index].sLength = 20;
} /* endfor */
veld[1].sLength = 32; /* verander veld 2 */
veld[3].sLength = 32; /* verander veld 4 */
veld[5].sLength = 32; /* verander veld 6 */
for (index = 0; index < 8; index++) {
printf ("veld %d: rij %2d, kolom %2d, %2d tekens\n", index+1,
veld[index].sRow,
veld[index].sColumn,
veld[index].sLength);
} /* endfor */
}
De for lus wordt 8 keer doorlopen en in elke stap van de iteratie wordt
aan de drie componenten van een veld een waarde toegekend. Op deze
manier kan de interne administratie van het Yahtzee spel georganiseerd
worden. Hier wordt geadministreerd op welke regel en welke kolom van
het scherm een veld begint en wat de maximale lengte is.
In de volgende drie statements wordt aan sommige lengte componenten een
andere waarde gegeven. Dit is om te laten zien hoe dat moet. In C is
het niet mogelijk een structuur via een toekenning te vullen vanuit een
andere structuur. Sommige moderne compilers ondersteunen dit echter
wel. Kijk daarom altijd in de documentatie van de compiler hoe met
structuren kan worden omgegaan.
Tenslotte worden alle gegenereerde waarden middels een iteratie
afgedrukt. Compileer en test dit programma.
Pointers en structuren
Bestudeer het programma STRUCT3.C voor een voorbeeld van een programma
waarin structuren en pointers in combinatie worden toegepast. Ook dit
programma lijkt op de vorige twee en is er een uitbreiding op.
main ()
{
int index;
struct _field {
int sRow; /* field row */
int sColumn; /* field column */
int sLength; /* field length */
} veld[8], *ptr;
for (index = 0; index < 8; index++) {
ptr = veld + index;
ptr->sRow = index + 4;
ptr->sColumn = 12;
ptr->sLength = 20;
} /* endfor */
veld[1].sLength = 32; /* verander veld 2 */
veld[3].sLength = 32; /* verander veld 4 */
veld[5].sLength = 32; /* verander veld 6 */
for (ptr = veld; ptr < veld+8; ptr++)
printf ("veld %d: rij %2d, kolom %2d, %2d tekens\n",
(ptr-veld)+1, ptr->sRow, ptr->sColumn, ptr->sLength);
}
Naast de bekende declaraties vinden we nu ook de declaratie van een
pointer met de naam ptr, hetgeen een pointer is naar de
structuur _field. Het is niet toegestaan deze pointer te
gebruiken als wijzend naar een ander datatype. Daar is een duidelijke
reden voor, zoals we aanstonds zullen zien.
In de iteratie wordt nu de pointer gebruikt om de component variabelen
aan te wijzen. Omdat zowel veld als ptr pointers
zijn naar dezelfde structuur, kunnen we de structuur members benaderen
middels de pointer. De variabele veld is een constante, dus
die kunnen we niet variëren, maar ptr is een pointer en kan
dus andere waarden aannemen.
Als we aan ptr de waarde toekennen van veld, zal
het duidelijk zijn dat de pointer naar het eerste element van de tabel
met velden wijst.
Als we de waarde 1 bij de pointer optellen, zal deze naar het tweede
element van de tabel gaan wijzen. Dit heeft te maken met hoe C met
pointers omgaat. De compiler weet dat de structuur uit drie integers
bestaat. Wanneer dus 1 moet worden opgeteld bij de pointer, zal in
werkelijkheid 3 maal de lengte van een integer worden opgeteld,
waardoor de pointer precies naar het begin van het volgende element
wijst. Dit is dus de reden dat een pointer niet gebruikt kan worden
om naar een variabele van een ander type te wijzen.
In iedere slag van de iteratie zal de pointer dus naar het begin van
een tabel element wijzen. We kunnen de pointer dus gebruiken om toegang
te krijgen tot de gegevens die in de tabel zijn opgeslagen. Het refereren
aan gegevens middels een pointer wordt zo vaak toegepast, dat de
programmeertaal C er een apart symbool voor heeft. Met behulp van de
-> operator kan een structuur member worden benaderd. De
variabele veld.sLength kan dus benaderd worden met
ptr->sLength, wanneer ptr naar veld
wijst.
Omdat de pointer naar de structuur wijst, moeten de componenten dus
individueel aangewezen worden. Het voorbeeld programma laat in de twee
iteraties verschillende manieren zien hoe dat kan. In de eerste
iteratie wordt de waarde van de pointer samengesteld uit het begin
adres van de tabel plus een afstand. In de tweede iteratie wordt de
pointer zelf gebruikt en telkens een element verschoven. De tweede
iteratie laat ook nog eens duidelijk het rekenen met pointers zien.
Bestudeer het programma aandachtig en met name de uitdrukking
(ptr-veld)+1. Compileer en test het programma, bestudeer de
uitvoer en kijk of je begrijpt wat er wordt afgedrukt.
Geneste structuren
Bestudeer het programma GENEST.C. Dit programma bevat een geneste
structuur, dat wil zeggen een structuur met daarin een structuur.
De structuren die we tot nu toe zagen waren enkelvoudig. Structuren
mogen tot in oneindige hiërarchie worden opgebouwd. Het is een groot
voordeel structuren uit deel structuren op te mogen bouwen. Het
opbouwen van een complexe structuur in een keer zou tot een zeer
onoverzichtelijk programma leiden.
main ()
{
int index;
struct _field {
int sRow; /* field row */
int sColumn; /* field column */
int sLength; /* field length */
};
struct _screen {
int sRows; /* number of rows on screen */
int sColumns; /* number of cols on screen */
struct _field veld[8];
} scherm;
scherm.sRows = 24;
scherm.sColumns = 80;
for (index = 0; index < 8; index++) {
scherm.veld[index].sRow = index + 4;
scherm.veld[index].sColumn = 12;
scherm.veld[index].sLength = 20;
} /* endfor */
printf ("Het scherm heeft %d regels van %d tekens\n\n",
scherm.sRows, scherm.sColumns);
for (index = 0; index < 8; index++) {
printf ("veld %d: rij %2d, kolom %2d, %2d tekens\n", index+1,
scherm.veld[index].sRow,
scherm.veld[index].sColumn,
scherm.veld[index].sLength);
} /* endfor */
}
De eerste structuur is weer onze veld structuur en bevat drie integers.
De structuur heeft wel een naam, maar er zijn geen variabelen van dit
structuur type gedeclareerd. We hebben hier dus te maken met de
definitie van een structuur, er is dus geen geheugen ruimte
gereserveerd. De naam _field kan worden gebruikt om aan de
structuur te refereren. Eigenlijk hebben we hier een nieuw type
gedefinieerd. Dit type kunnen we in verdere declaraties gebruiken, net
als we dat deden met de typen int en char. Als we
dat doen, zijn we verplicht het gereserveerde woord struct
bij de declaratie er voor te zetten.
Bij de declaratie van de tweede structuur kunnen we dit zien. Eerst
worden twee integers als componenten gedefinieerd, gevolgd door een
variabele van het type struct _field. De variabele van dit
structuur type heeft de naam veld en is tevens een tabel van
8 elementen. Omdat een veld weer uit 3 componenten bestaat, bevat de
scherm structuur dus 2 plus 8 maal 3 is 26 componenten, alle integers.
De variabele die bij deze structuur hoort, heeft de naam
scherm.
Het programma kent eerst waarden toe aan de enkelvoudige componenten
van de scherm structuur. Om deze te benaderen is het toevoegen van de
structuur naam scherm voldoende. Vervolgens krijgen de
integers van de tabel een waarde. Omdat dit een tabel is van
structuren, deel uitmakend van een structuur, zijn drie namen nodig. Om
bijvoorbeeld het lengte veld van het derde element te benaderen, is de
variabele naam scherm.veld[2].sLength vereist.
Om te laten zien hoe structuur variabelen op een tweede en derde niveau
van hierarchie worden benoemd in de printf() funktie, worden
alle variabelen afgedrukt. Compileer en test dit programma nu.
In principe kun je doorgaan met het nesten van structuren totdat je er
zelf van in de war raakt. Als je het op de juiste wijze doet zal de
compiler er nooit van in de war raken, omdat er geen formeel
gedefinieerde limiet bestaat. Wel is er een praktische limiet. Wanneer
je meer dan drie niveaus diep gaat, zul je er al moeite mee hebben.
Structuren kunnen tabellen bevatten bestaande uit weer andere
structuren die als zodanig ook weer uit tabellen of structuren bestaan.
Ook recursieve structuren zijn mogelijk. De strekking van deze uitleg
is dat je verstandig met structuren moet omgaan, zeker in het begin.
Begin eerst voorzichtig. Krijg je wat meer ervaring, dan kun je met
complexer structuren gaan werken. Houdt het altijd overzichtelijk.
Erg ingewikkelde structuren worden hier verder niet behandeld. Het
Totaal Programma bevat wel een aantal mooie voorbeelden. Bij de
bestudering daarvan zul je niet al te veel problemen meer ondervinden,
wanneer je dit hoofdstuk hebt begrepen.
Een beschrijving van de union
Bestudeer het programma UNION1.C. Een union geeft je de
mogelijkheid om gegevens in het computer geheugen als zijnde van
verschillend datatype te beschouwen, of om dezelfde gegevens met
verschillende namen te benaderen.
main ()
{
union {
int waarde;
struct {
char eerste;
char tweede;
char derde;
char vierde;
} deel;
} nummer;
long index;
for (index = 753; index < 0x0FFFFFFF; index *= 4) {
nummer.waarde = index;
printf ("%08X - %02X %02X %02X %02X\n",
nummer.waarde,
nummer.deel.eerste, nummer.deel.tweede,
nummer.deel.derde, nummer.deel.vierde);
} /* endfor */
}
In het voorbeeld programma staat de declaratie van een union
die uit vier onderdelen is opgebouwd. Allereerst is er de integer
variabele met de naam waarde. Voor het gemak gaan we er even
van uit dat deze is opgeslagen als 32-bits getal, 4 bytes. Dan is er
een structuur variabele van 4 characters met de naam deel.
Deze bevat de componenten eerste, tweede,
derde en vierde. Deze vier character variabelen
zijn 8 bits groot, 1 byte, en beslaan hetzelfde geheugengebied als de
integer. Ze liggen als het ware over elkaar heen. Dat is nu precies wat
een union doet. De union maakt het je mogelijk
gegevens van verschillend type in dezelfde geheugen lokaties op te
slaan. Tenslotte is er de union variabele nummer.
In het voorbeeld programma wordt een getal opgeslagen in de component
waarde. Dit getal wordt hexadecimaal afgedrukt, waardoor
goed de samenstelling van de bits te controleren valt. Vervolgens
worden de 32 bits waaruit het getal bestaat in vieren geknipt en elk
deel afzonderlijk afgedrukt middels een character variabele van 8 bits.
Door het getal met 4 te vermenigvuldigen schuiven alle bits 3 op naar
links. Compileer en test dit programma, zodat je kunt zien hoe dat in
zijn werk gaat.
Het benaderen van een union member gaat op exact dezelfde wijze als
het benaderen van een structuur member. Bestudeer het voorbeeld, zodat
verdere uitleg daaromtrent niet nodig is.
Unions worden niet zo vaak toegepast en zeker niet door beginnend
programmeurs. Je zult ze zo hier en daar wel eens tegenkomen. Dat is de
reden dat er enkele aandacht aan wordt besteed. Je hoeft er niet alles
van te weten op dit moment. Ook het Totaal Programma bevat geen unions.
Besteed er dus niet te veel tijd aan. Structuren zijn veel
belangrijker. Probeer die wel goed te begrijpen.
Een voorbeeld programma
Bestudeer nu het programma ZEGTYD.C. Dit programma bevat heel veel
elementen van hetgeen je tot op heden hebt bestudeerd. Alle onderwerpen
in een klein programma. De bedoeling van het programma is dat het de
systeemtijd afdrukt als ware het gesproken tekst. Een conversie van
numeriek naar uitgesproken tekst als het ware. Laten we er maar eens
naar kijken.
#include /* standard Invoer/Uitvoer header file */
#include /* datum/tijd funkties header file */
char wrd[15][10] = {
"twaalf", "een", "twee", "drie", "vier", "vijf", "zes", "zeven",
"acht", "negen", "tien", "elf", "twaalf", "dertien", "veertien"
};
main ()
{
int hh, mm;
time_t tijd;
struct tm *t;
tijd = time (NULL);
t = localtime (&tijd);
hh = t->tm_hour;
mm = t->tm_min;
printf ("Het is ");
if (mm < 1) printf ("precies %s uur ", wrd[hh%12]);
else if (mm < 2) printf ("een minuut over %s ", wrd[hh%12]);
else if (mm < 15) printf ("%s minuten over %s ",
wrd[mm], wrd[hh%12]);
else if (mm < 16) printf ("kwart over %s ", wrd[hh%12]);
else if (mm < 29) printf ("%s minuten voor half %s ",
wrd[30-mm], wrd[hh%12+1]);
else if (mm < 30) printf ("een minuut voor half %s ",
wrd[hh%12+1]);
else if (mm < 31) printf ("half %s ", wrd[hh%12+1]);
else if (mm < 32) printf ("een minuut over half %s ",
wrd[hh%12+1]);
else if (mm < 45) printf ("%s minuten over half %s ",
wrd[mm-30], wrd[hh%12+1]);
else if (mm < 46) printf ("kwart voor %s ", wrd[hh%12+1]);
else if (mm < 59) printf ("%s minuten voor %s ",
wrd[60-mm], wrd[hh%12+1]);
else printf ("een minuut voor %s ", wrd[hh%12+1]);
printf ("... \n");
}
Het programma begint met twee toevoegingen. Het standaard
Invoer/Uitvoer Bestand en een bestand met de definities van diverse
tijd funkties en structuren.
Er wordt een tabel van strings gedeclareerd. In C is dat een
tweedimensionale tabel van variabelen van het type char. De
strings zijn de woorden van de uren. De subscript op de tabel komt
overeen met het uur of de minuut; het element komt overeen met het uur
of de minuut plus 1. Uur nul is uur twaalf is element 1 is subscript 0.
Minuut veertien is element 15 is subscript 14.
Alle element zijn te lang, maar doordat strings eindigen bij het NULL
karakter, zal toch alles correct worden afgedrukt.
In het hoofdprogramma volgt de declaratie van twee integer
hulpvariabelen hh en mm, voor respectievelijk de
uren en minuten. De variabele tijd is van het type
time_t, een type dat vooraf gedefinieerd is in het
time.h bestand.
Tenslotte is er de variabele t, hetgeen een pointer is naar
een variabele van het structuur type tm. Ook dit laatste
type staat vooraf gedefinieerd is in het time.h bestand.
Middels funktie time() wordt de systeemtijd opgehaald en
toegekend aan variabele tijd. Met deze variabele als parameter van de
funktie localtime, krijgt de pointer zijn correcte waarde en
zal dus naar een structuur wijzen waarvan de componenten de systeemtijd
bevatten. Van deze componenten hebben we alleen uur en minuut nodig. De
namen daarvan zijn hour en min.
De wijzerplaat wordt nu in een aantal specifieke delen geknipt en van
elk deel de uitspraak bepaald. Een switch statement zou hier
op zijn plaats zijn, echter het aantal case's wordt wat
groot. Het voorbeeld programma toont een aardig alternatief
hiervoor&colon. een boom van if/else combinaties.
Hoewel wrd een tweedimensionale tabel is van characters,
wordt hier gebruik gemaakt van het feit dat de variabele wrd
een eendimensionale tabel is van strings. Met behulp van de
%s specificatie in de printf() parameter wordt
een van toepassing zijnde tekst afgedrukt. Het programma sluit af met
het afdrukken van drie puntjes en een NewLine karakter.
Compileer en test dit programma.
|