8. Lektion

Denne lektion omhandler filer og filhåndtering

Emnerne behandles i lærebogen på siderne 415 til 445

De fleste programmer har brug for at skrive til og læse fra filer. I denne lektion skal vi se på, hvorledes vi får data overført til en fil på disken. At få data sendt til disk, printer, skærm osv. betegnes i C; input og output (I/O). Disk I/O operationer udføres på objekter, som betegnes filer. En fil er en samling af bytes, som har fået et navn. På de fleste computere gemmes filer på enten floppydiske eller en fast installeret disk, som kaldes en harddisk. Desuden kan filer skrives til CD-ROM's, tapestreamere, serverdiske osv. Stort set al kommunikation med lagermedierne foregår via computerens operativsystem, dvs. DOS, Windows, Linux osv.

Data, som skal sendes til filer, kan kategoriseres i forskellige grupper, til trods for en sådan kategorisering vil data fra de forskellige kategorier ofte overlappe hinanden mere eller mindre.

Standard I/O versus System I/O
Den groveste opdeling man kan foretage mellem C's fil systemer er ved at sondere mellem Standard I/O (ofte kaldt stream I/O) og System I/O (ofte kaldet low-level I/O). Hvert af disse, er mere eller mindre komplette systemer til at læse fra og skrive til diske. Begge systemerne har funktioner, som kan læse og skrive. På mange måde ligner de to systemer hinanden, en del af funktioner kan erstattes af det andet system. Trods det, er der meget vigtige forskelle på de to systemer.
Standard I/O er det system man anvender oftest, når man vil udføre I/O-operationer i C. Standard I/O har flest funktioner til håndtering af data, derfor kan det på den ene side virke noget uoverskueligt, på den anden side kan man gemme data med forskellige formater, således at det ikke er nødvendigt at programmere ekstra for at håndtere data, når man henter dem tilbage fra disken, til gengæld skal man normalt læse med præcis det samme format, som man gemte sine data med.
System I/O giver færre muligheder til at håndtere data, man kan betragte systemet, som et mere primitivt system. Systemet minder en hel del om den metode operativsystemerne håndtere data på. Man skal selv programmere sig til styring af buffer, format osv. under håndteringen af data. Fordi systemet ligger tættere op af operativsystemet, er System I/O også lidt mere effektivt end Standard I/O.

Tegn, strenge, formateret og poster behandlet af I/O
Standard I/O giver mulighed for fire forskellige måde at udskrive og indlæse data. Dette er lidt i modsætning til System I/O, som kun har en metode til ind- og udlæsning. Ved første øjekast kan det se lidt uoverskueligt ud med alle de mange muligheder for I/O-operationer, men det er ikke så slemt, som det ser ud. Tre af metoderne kender vi allerede, de virker fuldstændig på samme måde, som de funktioner, vi tidligere har anvendt til ind- og udlæsning via tastatur og skærm.
På figuren herunder ses en oversigt over de forskellige metoder.

Fra højre er den første metode indlæsning at tegn, metoden virker fuldstændig som getch(). Den næste metode fra højre læser strenge og fungerer fuldstændig, som den tilsvarende skærmkommando. Den tredie fra højre er den formaterede, der ligeledes virker som skærmkommandoen. Den fjerde er forskellig fra noget, vi tidligere har arbejdet med, den anvendes til ind- og udlæsning af poster (Records), i noget litteratur ses metoden også benævnt block-I/O.

Tekst versus binær
En anden måde at karakterisere I/O-operationer på er ved at se på om filerne åbnes i tekst-mode eller binær-mode. De to metoder bestemmer, hvordan forskellige detaljer vedrørende filhåndteringen bliver udført.
F.eks. hvordan Return (Ny linie) gemmes, eller hvordan enden af filen markeres (EOF) osv. Grunden til, at der er flere metoder også på dette plan er, at UNIX, hvorpå C oprindeligt blev opfundet, gør ting på én måde, DOS på en anden måde og Windows på en tredie måde.
For at gøre forvirringen total er der også en anden måde at skelne mellem tekst og binær på. Det er tekst-format og binær-format. Format anvendes til at gemme tal på disken. I tekst-format er tallene gemt som strenge af tegn. Binær-format gemmer tegnene nøjagtig, som tegne forefindes i maskinens hukommelse. Dvs. 2 bytes for heltal, 4 bytes for decimaltal osv.

Tekst-mode versus binær-mode beskæftiger sig meget med, hvorledes "ny linie" og EOF, bliver oversat og gemt. Tekst-format versus binær-format beskæftiger sig med, hvorledes tal bliver gemt. 

Disse to formater skyldes ikke forskellige operativsystemer, men fordi det kan være mere praktisk at anvende det ene eller det andet system i en given situation.

Standard I/O
I det følgende vil jeg vise nogle eksempler på anvendelse af de forskellige I/O-metoder.

Eksempel 1
void main(void)
{
   FILE *fptr;
   char ch;

   fptr = fopen("textfil.txt", "w");

   while( (ch = getche() ) !='\r')
      putc(ch, fptr);

   fclose(fptr);
}

Programmet starter med at erklære en pointer til en fil. Pointeren bliver den "pegepind som dirigere data ned i filen. Den næste variabel som erklæres er en variabel til et tegn, som skal modtage indtastningerne og aflevere tegnene til filpointeren.
Derpå åbnes en fil med filnavnet "textfil.txt", det lille w fortæller compileren at filen skal åbnes for skrivning. Hvis filen ikke eksistere oprettes den, hvis den findes overskrives den. Der kommer ikke nogen advarsel før overskrivningen, hvis der skal advares før overskrivningen, skal programmøren selv finde ud af om filen eksistere og derpå advare brugeren.
Herunder følger de forskellige bogstaver som anvendes i forbindelse med åbning af filer.
"r"      Åben en fil for læsning. Filen skal eksisterer.
"w"     Åben en fil for skrivning. Filen vil blive oprettet eller overskrevet.
"a"      Filen åbnes for tilføjning. Hvis den ikke eksisterer vil den blive oprettet.
"r+"    Åben filen for både læsning og skrivning. Filen skal eksisterer.
"w+"   Åben filen for læsning og skrivning. Hvis den eksisterer vil den blive overskrevet.
"a+"    Åbnes for tilføjelse og læsning. Hvis filen ikke eksisterer vil den blive oprettet.
"rb"     Åbner en fil for binær læsning.

Sætningen: putc(ch, fptr); skriver et tegn på adressen, som peges på med fptr.
Det er meget vigtigt at man husker at lukke filkanalen, når man er færdig med at skrive til sin fil. Hvis der ikke udføres et fclose(fptr); vil filen vedblive med at være åben. Dette er noget man skal være meget opmærksom på under udviklingen af et program. Hvis programmet går ned efter det har åbnet en fil og inden det igen har fået lukket filen, vil der være efterladt en logisk fejl på disken. En sådan fejl kan enten rettes ved at man har et lille program, som lukker åbne filer i det program man er ved at udvikle, eller man kan køre programmet ScanDisk, som kom sammen med Windows og DOS. Det er en af de mest almindelige fejl, som ScanDisk udfører hver gang en maskine er gået ned uden at de forskellige filer er blevet ordentligt lukkede.

Læsning fra en fil
Nu vil vi prøve at konstruere et program som kan læse den fil vi oprettede med programmet i eksempel 1.

Eksempel 2
void main(void)
{
   FILE *fptr;
   char ch;

   fptr = fopen("textfil.txt", "r");
   while( (ch = getc(fptr) ) != EOF)
      printf("%c", ch);

   fclose(fptr);
}

Den nye i dette eksempel er egentlig blot det reserverede ord EOF, bemærk det skrives med store bogstaver. Dette er en markering, som programmet i eksempel 1 satte, da det nåede til fclose(). Det er et enkelt tegn, hvis man udskriver værdien, opdager man at værdien er: -1.

Eksempel 3
DOS-kald.c
int main( int  argc, char *argv[])
{
   FILE *fptr;
   int tal =0;
   if(argc != 2)
{
   printf("\n Format: C:\Sti> count filename");
   exit(1);
}
   if( (fptr=fopen(argv[1], "r")) == NULL)
   {
       printf("\nKan ikke åbne filen %s.", argv[1]);
       exit(1);
   }
   while ( getc(fptr) != EOF )
      tal++;
   fclose(fptr);
   printf("\n Filen %s indeholder %d tegn.", argv[1], tal);
   return(0);
}
Eksempel 3 giver flere nye muligheder. Først bemærker man at main() er blevet til en funktion i stedet for en procedure. main() returnerer nu en heltalsværdi. Endvidere tager funktionen to argumenter, men det er ikke argumenter i normal forstand. Dette vender jeg tilbage til.
Man har normalt det problem at når man arbejder med filer kan der ske det at man f.eks. forsøger at åbne et filnavn som ikke eksistere eller man forsøger at skrive til en disk, hvor der ikke er mere plads. Hvis man ikke i sin programmering har taget højde for den type fejl, vil brugeren af et program blive sur på programmøren når programmet tilsyneladende uden grund pludselig "går ned". Det er så heldigt at funktionen fopen() returnerer værdien NULL, hvis den ikke kan udføre det stykke arbejde den er sat til. Det benytter eksempel 3 sig af idet der er indføjet en if()-sætning som spørger om det lykkes at åbne filen, hvis fopen() returnerer værdien NULL udskrives en melding til brugeren på skærmen om at filen med det anførte filnavn ikke eksistere. Derpå anvendes funktionen exit(1), som returnerer værdien 1. Ved at ændre main() til at returnere et heltal kan vi få denne værdi ud at main(). Gennemløbes programmet som det skal returneres 0 (nul).
Når der er argumenter i kaldet til main(), betyder det, at man kan starte programmet fra en DOS-prompt. I kaldet af funktionen, kan man indsætter et eller flere argumenter. I dette tilfælde vil man starte programmet med: C:>dos-kald filnavn.txt
De to argumenter i kaldet til main() opfattes således. Den første argument optæller, hvor mange ord kaldet af funktionen består af. I dette tilfælde består kaldet af to ord. Det første er programmets navn og det andet er filnavnet på den fil, som ønskes åbnet. Dvs. at argc får værdien 2. *argv[] er en pointer til det array af strenge, som indtastes. Værdien i et array starter altid med 0 så programnavnet vil lagres i arrayet som argv[0], mens filnavnet, som skal åbnes, lagres i argv[1]. Dvs. at der er anvendt to værdier. Hvis man ikke vil indtaste programnavn , filnavn osv. når man køre programmer fra en DOS-prompt, kan man oprette en BAT-fil, hvori man indskriver de nødvendige kommandoer til kørsel af programmet.

Hvis du undersøger en filstørrelse, dvs. hvis du bruger DOS eller Windows til at undersøge, hvor stor en fil er, vi du få et andet svar, end hvis du undersøger filstørrelsen med programmet i eksempel 3. Problemet ligger i, at C sætter ét kombinationstegn, som "ny linie", mens DOS og Windows anvender to tegn nemlig RETURN og LINEFEED (CR/LF). Når C gemmer filen, konverteres C's tegn til CR/LF, dvs. der ender to tegn på disken, men når man derpå læser filen igen med C, konverteres CR/LF igen til ét tegn, som derfor optælles af vores C-program, som et enkelt tegn. Derfor bliver antallet af tegn tilsyneladende mindre ved optælling med C-programmet, end hvis man skriver DIR ved en DOS-prompt. Filen til øvelse 1 har størrelsen 211 i DOS. Optælling med programmet fra eksempel 3 giver 203 tegn.

Skrivning og læsning af strenge til en fil
Skrivning og læsning af tekster fra en fil er stort set lige så let, som at skrive og læse enkelt karakterer eksempel 4 demonstrerer, hvordan det går for sig.

Eksempel 4
void main(void)
{
   FILE *fptr;
   char text[81];
   fptr = fopen ("bruger.txt", "w");

   while( strlen( gets(text) ) > 0)
   {
      fputs(text, fptr);
      fputs("\n", fptr);
   }
   fclose(fptr);
}

I eksemplet forsætter indtastningerne indtil der indtastes en tom streng, hvis der eneste, der indtastes er ¿, vil længden af strengen bliver 0 (nul). Funktionen der anvendes er den samme som den strengfunktion puts(), der er anvendt i en tidligere lektion, forskellen er blot at funktionen som anvendes til skrivning til filer har et f foran funktionsnavnet.

Formateret skrivning til en fil
Indtil nu har vi kun beskæftiget os med skrivning af tegn til disken. Hvad med tal? Jeg vender nu tilbage til den sidste udgave vi havde med vores agentprogram, men lader som om vi ikke ved noget om poster. Dvs. vi konstruere et program som kan gemme et agentnavn, et nummer og en højde.

Eksempel 5
void main(void)
{
   FILE *fptr;
   char navn[40];
   int kode;
   float hojde;
   fptr = fopen ("agent.txt", "w");
   
   do
   {
      printf("Indtast navn, kode nummer og højde: ");
      scanf("%s %d %f", navn, &kode, &hojde);
      fprintf(fptr, "%s %d %f", navn, kode, hojde);
   }
   while( strlen(navn) > 1);
   fclose(fptr);
}

I eksemplet ses det at det eneste der sker her er at de indtastede data skrives til en fil med en fprinf()-funktion. Når man udskriver til en fil skal der i dette tilfælde ikke være nogen "hjælpetekst" det er de "nøgne" data som indskrives i filen.

Binær-tilstand og tekst-tilstand
Som nævnt i begyndelsen af denne lektion, kunne man også dele fil-tilstande (fil modes) i om filerne er skrevet i tekst-mode eller binær-mode. At der findes to forskellige former skyldes at C blive udviklet på UNIX, på UNIX skrives der normalt i tekst-mode, da man så skulle tilpasse C til PC-miljøet valgte Borland at arbejde med to forskellige tilstande, idet den normale tilstand for en fil på en PC er binært. Ikke alle compilerfabrikanter har begge tilstande. Man kan således sige at Tekst-mode imitere UNIX-filer og Binær-mode imiterer MS-DOS-filer.

Når vi har med tekstfiler at gøre oversættes CR/LF, når man udskriver henholdsvis læser fra en fil. Denne konvertering finder ikke sted ved binære-filer.

Det følgende eksempel viser lidt om hvad man kan udrette med et program som læser i binær-mode kontra tekst-mode. Programmet tager hver byte i en fil, hvis det filer et tegn vises det i hex-værdi samtidig med at det udskrives med sin normalt tekst-værdi på skærmen. Programmet foretager en "røngentundersøgelse" af en fil. Man vil kunne genkende måden at vise et filindhold på fra mange andre programmer til undersøgelse af filer på en disk.

Eksempel 6
int main( int argc, char *argv[])
{
   FILE *fileptr;
   int ch;
   int tal, not_eof;
   unsigned char string[LANG + 1];

   if(argc != 2)
   {
      printf("\n Format: C:\Sti> bindump filename");
      exit(1);
   }
   if( (fileptr=fopen(argv[1], "rb")) == NULL)
   {
      printf("\nKan ikke åbne filen %s.", argv[1]);
      exit(1);
   }
   not_eof = TRUE;
   do
   {
      for(tal = 0; tal < LANG; tal++)
      {
         if ( (ch=getc(fileptr)) == EOF)
         not_eof = FALSE;
         printf("%3x ", ch);
         if (ch > 31)
             *(string+tal) = ch;
         else
             *(string+tal) = '.';
      }
      *(string+tal) = '\0';
      printf(" %s\n", string);
   }
   while ( not_eof == TRUE );
   fclose(fptr);
   return(0);
}

Forskellen ved åbning af filen er at der skrives "rb", dette får programmet til at læse binært i stedet for tekst. Egentlig skulle man have skrevet "rt" ved tekst, men man har vedtaget, at når der ikke skrives noget, er det altid tekst-mode.

Binærudskrivning af databaseposter til en fil
Sidste eksempel handler om at udskrive poster til en fil, det er valgt at udskrive disse poster binært til filen.

Eksempel 7
int main(void)
{
   struct
   {  char navn[40];
      int agentnum;
      double hojde;
   } agent;
   FILE *fptr;
   char talstr[81];

   if( (fptr=fopen("agent.rec", "wb")) == NULL)
   {
      printf("\nKan ikke åbne filen agent.rec");
      exit(1);
   }
   do
   {
       printf("\nIndtast første agents navn : ");
       gets(agent.navn);
       printf("\nIndtast første agents nummer max. 3 tal: ");
       gets(talstr);
       agent.agentnum = atoi(talstr);
       printf("\nIndtast agentens højde : ");
       gets(talstr);
       agent.hojde = atof(talstr);
       fwrite(&agent, sizeof(agent), 1, fptr);
       printf("Vil du indtaste flere agenter (y/n)? ");
   }
   while ( getche() == 'y' );
   fclose(fptr);
   return(0);
}

Der er ikke så meget nyt i dette eksempel, eksemplet er mest en kombination af alt hvad der er gennemgået tidligere. Bemærk at filen åbnes i tilstanden "wb", altså skrivning i binær-tilstand.
Indtastningerne udskrives til filen med funktionen:

fwrite(&agent, sizeof(agent), 1, fptr);

Funktionen udskriver alle felter i posten på en gang og sizeof(agent) sørger for at der sættes den fornødne plads af til hele posten. Tallet 1 fortæller hvor mange poster vi vil skrive med funktionen. Hvis vi havde haft et array af poster kunne vi have skrevet alle posterne ind i et huk, ved at bytte 1-tallet ud med det antal poster som fandtes i array'et. Prøv at anvende programmet fra eksempel 6 til at læse den fil der oprettes med programmet her fra eksempel 7.

Alt hvad der har været talt om i denne lektion har været om filer, som blev læst eller skrevet i fra den ene ende til den anden. Dette kaldes sequentiel tilgang til filerne. En anden metode er at man kan læse og skrive i filerne på tilfældige steder, dette kaldes Random access. Jeg vil ikke her gennemgå det men henvise til at man læser om dette i lærebogen hvis man får brug for denne type manipulering med filer. System I/O vil jeg også springe over i denne lektion.  

Øvelse 1
Konstruer et program som kan optælle hvor mange ord der findes i en tekst i en fil.
Du kan hente en tekstfil til at øve dig på her. Resultatet skal være 41 ord.

Øvelse 2
Konstruer et program, som kan læse den tekstfil, der bliver oprettet med eksempel 4.

Øvelse 3
Konstruer et program som kan læse data fra filen, der blev oprettet med eksempel 5.

Eksempler og løsningerne kan hentes her
Eksempel 1
Eksempel 2
Eksempel 3
Eksempel 4
Eksempel 5
Eksempel 6
Eksempel 7
Øvelse 1
Øvelse 2
Øvelse 3