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