Extra informatie over het gebruik van structs.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de THRijswijk groepen EPm en EPv die de module SOPX1 volgen.

In C kan een aantal variabelen die bij elkaar horen gegroepeerd worden in een struct of in een array. In een array moeten alle elementen van hetzelfde type zijn (omdat het anders niet mogelijk is om het adres van het nde element te berekenen). Een element uit de array wordt geselecteerd met behulp van een index. Het eerste element heeft index 0, het tweede element index 1 enz. In een array genaamd rij kunnen we het derde element selecteren met de expressie rij[2]. In een struct kunnen de elementen van verschillende typen zijn. Een element uit de struct wordt geselecteerd met behulp van een naam (veldnaam). In een struct genaamd s1 kunnen we het element genaamd nummer selecteren met de expressie s1.nummer.

Definiëren van een struct.

Een struct kan als volgt gedefinieerd worden:

struct student {
int nummer;
char naam[20]; float cijfer; };

Vervolgens kunnen variabelen van dit struct type worden aangemaakt:

int main() {
   struct student s1, s2;
   s1.nummer=80234;
   strcpy(s1.naam, "S.I.N. Terklaas");
   s1.cijfer=8.4;
   s2.nummer=80323;
   strcpy(s2.naam, "K. Erstman");
   s2.cijfer=3.4;

Als je de struct alleen in main gebruikt kun je de struct ook lokaal definiëren:

int main() {
   struct student {
int nummer;
char naam[20]; float cijfer; }; struct student s1, s2;

In dit geval mag je de declaratie van de struct en de definitie van de variabelen combineren:

int main() {
   struct student {
int nummer;
char naam[20]; float cijfer; } s1, s2;

Omdat je nu de naam van de struct niet meer nodig hebt kun je die ook weglaten:

int main() {
   struct {
int nummer;
char naam[20]; float cijfer; } s1, s2;

Je kunt een struct variabele (net zoals een array variabele) bij het aanmaken meteen vullen:

int main() {
   struct student s1={80234, "S.I.N. Terklaas", 8.4}, s2={80323, "K. Erstman", 3.4};

In de praktijk wordt een struct vaak globaal geclareerd en zijn er veel functies waarbij dit struct type als parameter wordt gebruikt:

struct student {
int nummer;
char naam[20]; float cijfer; };
void printStudent(struct student s);
struct student leesStudent(void); void geefCijfer(struct student *ps, float c);

In dit geval is het handig om een typedef te gebruiken. Met een typedef kunnen we een alias voor een bestaand type definiëren:

struct student {
int nummer;
char naam[20]; float cijfer; }; typedef struct student Student;
void printStudent(Student s);
Student leesStudent(void); void geefCijfer(Student *ps, float c);

In dit geval kan de declaratie van de struct en de definitie van de typenaam worden gecombineerd:

typedef struct student {
int nummer;
char naam[20]; float cijfer; } Student; void printStudent(Student s); Student leesStudent(void); void geefCijfer(Student *ps, float c);

Omdat je nu de naam van de struct niet meer nodig hebt kun je die ook weglaten:

typedef struct {
int nummer;
char naam[20]; float cijfer; } Student;

Gebruiken van een struct.

De hierboven gedeclareerde functie printstudent kan als volgt gedefinieerd worden:

void printStudent(Student s) {
printf(
"%5d %-20s %4.1f\n", s.nummer, s.naam, s.cijfer); }

De hierboven gedeclareerde functie geefCijfer kan als volgt gedefinieerd worden:

void geefCijfer(Student *ps, float c) {
/* hoogste cijfer telt: */
   if (c > (*ps).cijfer) {
      (*ps).cijfer=c;
   }	
}

Omdat de variabele ps een Student* is moet het element cijfer van de struct waar deze pointer naar wijst geselecteerd worden met de expressie (*ps).cijfer. De haakjes zijn nodig omdat de operator . een hogere prioriteit heeft dan de operator *. Deze vervelende notatie mag je ook afkorten met ps->cijfer.

void geefCijfer(Student *ps, float c) {
/* hoogste cijfer telt: */
   if (c > ps->cijfer) {
      ps->cijfer=c;
   }	
}

Opslaan van een struct.

Een struct variabele kan eenvoudig worden opgeslagen (en weer worden teruggelezen uit) een binairefile:

void writeStudent(FILE* fp, Student s) {
fwrite(&s, sizeof s,
1, fp); } void readStudent(FILE* fp, Student *ps) {
fread(ps, sizeof *ps,
1, fp);
}

Meer informatie over het gebruik van binairefiles kun hier vinden.

Voorbeeld.

Bij het gebruik van binarefiles kun je met fseek heel snel een bepaalde struct vinden. Als je bijvoorbeeld de volgende struct gebruikt:

struct Persoon {
   int sleutel;
   char naam[80];
};

Dan kun je in een file waarin een aantal van deze structs zijn opgeslagen heel snel de persoon met een bepaalde sleutel vinden en uitlezen. Dit gaat als volgt:

   FILE* fp;
   struct Persoon p;
   fp=fopen("persoon.geg", "rb");
   fseek(fp, sleutel * sizeof p, SEEK_SET);
   fread(&p, sizeof p, 1, fp);

Dit werkt echter alleen maar goed als de structs netjes op volgorde in de file zijn opgeslagen. Dus als eerste de struct met sleutel 0, daarna de struct met sleutel 1, daarna de struct met sleutel 2 enz. Bij het aanmaken van personen moet het programma dus zelf de sleutels toekennen.

Er ontstaat nu een probleem als je een persoon uit de file wilt verwijderen. Alle volgende structs moeten dan een plaatsje opschuiven en ook hun sleutels moeten worden aangepast. Als er nog andere bestanden zijn waarin naar personen wordt verwezen door middel van sleutels dan moeten ook in deze bestanden de sleutels worden aangepast! Het op deze manier verwijderen van een persoon uit de file is heel ingewikkeld en duurt bij grote files erg lang. Je kunt dit ook veel eenvoudiger oplossen door een struct niet echt te verwijderen maar alleen maar als leeg te markeren. In de struct moet je dan een extra veld toevoegen:

struct Persoon {
   int sleutel;
   int leeg;
   char naam[80];
};

Het veld leeg geeft aan of de struct "leeg" is. Bij het aanmaken van personen worden de sleutels netjes op volgorde toegekend en krijgt het veld leeg steeds de waarde 0. Als nu een persoon verwijderd moet worden dan krijgt het veld leeg van deze struct de waarde 1. Alle volgende structs blijven dus gewoon op dezelfde plaats staan en de sleutels wijzigen niet.

Bij het uitlezen van de file moet nu natuurlijk wel gecontroleerd worden of een struct niet leeg is. Bij het toevoegen van een nieuwe persoon moet je nu eerst kijken of er geen lege struct in de file staat. Deze lege struct met de bijbehorende sleutel kun je dan hergebruiken. Als er geen lege struct gevonden is dan moet de nieuwe persoon aan het einde van de file met de volgende sleutel worden toegevoegd.

In het onderstaande programma kun je zien hoe je op deze manier personen kunt:

#include <stdio.h>

struct Persoon {
   int sleutel;
   int leeg;
   char naam[80];
};

void voegtoe(void) {
   FILE* fp;
   struct Persoon p;
   int gevonden=0;
   fp=fopen("persoon.geg", "rb+");
   if (fp==NULL) {
      char antw;
      do {
         printf("Kan persoon.geg niet vinden. Nieuw bestand openen? (J/N): ");
         fflush(stdin);
      } while (scanf("%c", &antw)!=1 || antw!='J' && antw!='j' && antw!='N' && antw!='n');
      if (antw=='J' || antw=='j') {
         fp=fopen("persoon.geg", "wb+");
         if (fp==NULL) {
            printf("Error: Kan persoon.geg niet aanmaken.\n");
         }
      }
   }
   if (fp!=NULL) {
      p.sleutel=-1;
      while (gevonden==0 && fread(&p, sizeof p, 1, fp)==1) {
         if (p.leeg==1) {
            gevonden=1;
         }
      }
      if (gevonden==1) {
         fseek(fp, -1 * sizeof p, SEEK_CUR);
      }
      else {
         ++p.sleutel;
      }
      p.leeg=0;
      do {
         printf("Geef naam voor persoon %d:\n", p.sleutel);
         fflush(stdin);
      } while(scanf("%79[ a-zA-Z]", p.naam)!=1);
      fwrite(&p, sizeof p, 1, fp);
      fclose(fp);
   }
}

void verwijder(void) {
   FILE* fp;
   fp=fopen("persoon.geg", "rb+");
   if (fp==NULL) {
      printf("Error: de file persoon.geg kan niet geopend worden.\n");
   }
   else {
      int sleutel;
      struct Persoon p;
      do {
         printf("Geef nummer: ");
         fflush(stdin);
      }
      while(scanf("%d", &sleutel)!=1);
      fseek(fp, sleutel * sizeof p, SEEK_SET);
      if (fread(&p, sizeof p, 1, fp)!=1) {
         printf("Error: de persoon kan niet gevonden worden.\n");
      }
      else {
         if (p.leeg==1) {
            printf("Error: de persoon kan niet gevonden worden.\n");
         }
         else {
            p.leeg=1;
            fseek(fp, -1 * sizeof p, SEEK_CUR);
            if (fwrite(&p, sizeof p, 1, fp)!=1) {
               printf("Error: de persoon kan niet verwijderd worden.\n");
            }
         }
      }
      fclose(fp);
   }
}

void zoek(void) {
   FILE* fp;
   fp=fopen("persoon.geg", "rb");
   if (fp==NULL) {
      printf("Error: de file persoon.geg kan niet geopend worden.\n");
   }
   else {
      int sleutel;
      struct Persoon p;
      do {
         printf("Geef nummer: ");
         fflush(stdin);
      }
      while(scanf("%d", &sleutel)!=1);
      fseek(fp, sleutel * sizeof p, SEEK_SET);
      if (fread(&p, sizeof p, 1, fp)!=1) {
         printf("Error: de persoon kan niet gevonden worden.\n");
      }
      else {
         if (p.leeg==1) {
            printf("Error: de persoon kan niet gevonden worden.\n");
         }
         else {
            printf("%6d %s\n", p.sleutel, p.naam);
         }
      }
      fclose(fp);
   }
}

void drukaf(void) {
   FILE* fp;
   fp=fopen("persoon.geg", "rb");
   if (fp==NULL) {
      printf("Error: de file persoon.geg kan niet geopend worden.\n");
   }
   else {
      struct Persoon p;
      while (fread(&p, sizeof p, 1, fp)==1) {
         if (p.leeg==0) {
            printf("%4d %s\n", p.sleutel, p.naam);
         }
      }
      fclose(fp);
   }
}

void main() {
   int antw;
   do {
      printf("0. Stoppen.\n"
             "1. Afdrukken.\n"
             "2. Toevoegen.\n"
             "3. Verwijderen.\n"
             "4. Zoek op nummer.\n"
             "\n"
             "Geef keuze: ");
      fflush(stdin);
      if (scanf("%d", &antw)!=1) {
         printf("Error: invoer is geen getal.\n");
      }
      else {
         switch (antw) {
            case 0:
               break;
            case 1:
               drukaf(); break;
            case 2:
               voegtoe(); break;
            case 3:
               verwijder(); break;
            case 4:
               zoek(); break;
            default:
               printf("Error: verkeerde invoer.\n");
         }
      }
   }
   while (antw!=0);
}