10. Använda filer

 

1.       Datatypen FILE.

2.       Öppna och stänga filer.

3.       Läsa filer.

4.       Skriva filer.

5.       Radvis läsning och skrivning.

 

1. Datatypen FILE.    

 

När man vill arbeta med en fil låter man operativsystemet sköta själva filhan­ter­ingen. API innehåller funktioner som 'pratar' med operativsystemet för vårt pro­grams räkning. Man får tillgång till detta genom att ta med filen stdio.h i  pro­grammet:

 

#include <stdio.h>

 

Den konversation man för med operativsystemet utnyttjar en speciell struktur, vilken vi själva inte behöver deklarera. Den finns deklarerad i stdio.h. Däremot behöver vi en pekare som kan hålla reda på var strukturen finns i minnet. Innan vi deklarerar den ska vi ta och titta litet i stdio.h, för att se hur strukturen är de­k­larerad:

 

struct _iobuf {

    char *_ptr;

    int   _cnt;

    char *_base;

    char  _flag;

    char  _file;

    };

 

typedef struct _iobuf FILE;

 

Här har man skapat en ny typ kallad struct _iobuf. Det kan vara litet obekvämt att alltid behöva skriva nyckelordet struct, så här har man utnyttjat en möjlighet att skapa ett nytt namn. Man använder nyckelordet typedef, skriver det gamla typ­namnet, i detta fall struct _iobuf, och sedan det nya namnet. I detta fall är det nya namnet FILE och det är det vi ska använda i våra program. Vi ska alltså ska­pa en pekare av typen FILE. Om vi avser att läsa en fil kan det vara lämpligt att kalla pekaren 'pInFil':

 

FILE *pInFil;

 

Strukturen innehåller ett antal för oss ganska kryptiska variabeldeklarationer. Den första är uppenbarligen en pekare till en textbuffert, och den andra en räk­nare som verkar vara till för att hålla reda på hur många tecken som lästs eller skrivits. Vi behöver faktiskt inte bry oss om vad de olika elementen är avsedda för, eftersom vi bara kommer att använda diverse funktioner i stdio.h som i sin tur använder de olika elementen i strukturen.

 

Definition: Filer manipuleras av operativsystemet. Vi har tillgång till funk­tioner som konverserar med operativsystemet via en struktur. Vi skapar en pekare av typen FILE* vilken funktionerna använder mot strukturen.

 

Det finns mycket annat i stdio.h. Om vi bortser från att denna fil innehåller stöd för text in och utmatning till bildskärm, ungefär som cin och cout, så finns div­er­se andra deklarationer som har med filhantering att göra. En del av dessa ut­gör de funktioner vi kan använda för att skriva respektive läsa i en fil, och vi ska testa ett par av dem strax. Det finns även en del konstanter, och en av dem ser ut så här:

 

#define FILENAME_MAX 128

 

Detta är alltså ett exempel på konstanter som deklarerats i denna fil. Just denna konstant är avsedd att ange maximala längden på ett filnamn, inklusive sökväg. Om vi vet att det är 128 tecken kan vi till exempel deklarera en teckenlista av­sedd att innehålla ett filnamn så här:

 

char cFilNamn[128];

 

...men det är bättre att göra så här:

 

char cFilNamn[FILENAME_MAX];

 

Om vi använder konstanten skulle det underlätta om vi flyttar programmet till en annan typ av dator eller till och med bara byter operativsystem på den dator vi har. Med C++ kompilatorn följer en ny version av stdio.h vilken har deklar­erat FILENAME_MAX på ett annat sätt. En UNIX-maskin kanske har möjlig­het till längre filnamn. När vi kompilerar programmet blir det i alla fall rätt.

 

En annan konstant, EOF kommer vi att ha mycket nytta av. Anledningen till att EOF definieras så här är densamma som när det gäller FILENAME_MAX: oli­ka datorsystem kan ha olika värden för just EOF, men vi behöver inte veta vil­ket, här definieras den, så vi kan bara skriva EOF. Om du inte redan gissa det så står EOF för End Of File, d.v.s. filslut. Om du har gissat det så står det i alla fall för filslut. Man kan t.ex. testa i en skriva en whileslinga för läsning som läser ett tecken i taget på följande sätt:

 

while ((cTecken = getc(Infil)) != EOF)

 

Vi kommer att titta på getc() och andra funktioner strax, så det du ska titta på ov­an är bara att while gör en jämförelse på om funktionen returnerar ett tecken till cTecken, eller om den returnerar EOF. Returnerar den inte EOF så fortsätter slingan.

 

Om man söker i filen stdio.h efter 'function prototypes' finner man en kommen­tar, vilken alltid föregår de funktionsprototyper headerfilen innehåller. Detta innebär att man här kan se vilka funktioner vi kan ha nytta av. Observera att det finns funktioner som hanterar in och utmatning av texter till bildskärm också i denna fil, och dem ska vi inte titta på i denna kursen. De flesta av de funktioner vi nu är intresserade av börjar med bokstaven f, för file. Här nedan har jag tryckt ut några av dem, nämligen de vi ska titta på i detta kapitlet:

 

/* function prototypes */

 

int __cdecl fclose(FILE *);

int __cdecl fgetc(FILE *);

char * __cdecl fgets(char *, int, FILE *);

FILE * __cdecl fopen(const char *, const char *);

int __cdecl fputc(int, FILE *);

int __cdecl fputs(const char *, FILE *);

 

Det går lätt att härifrån ta reda på vad dessa kryptiska funktionsnamn står för. Placera bara markören på ett funktionsnamn och tryck på F1, så öppnas hjälpen med rätt avsnitt aktuellt, som vanligt.

 

2. Öppna och stänga filer.

 

Man måste be operativsystemet att etablera kontakt med en fil, tala om vad man vill göra med den (läsa/skriva/förlänga), och tala om var i minnet data ska lag­ras etc. Operativsystemet kontrollerar även att man inte låter flera program sam­tidigt manipulerar samma fil. Därför är det viktigt att man meddelar operativ­sys­temet när man är klar med filen. Dessa två processer kallas att öppna respek­tive stänga filen.

 

Man öppnar en fil med följande syntax:

 

FILE-pekare = fopen("filnamn","arbetsläge");

 

Funktionen fopen() skapar en strukturvariabel av typen _iobuf i minnet, fyller i den med vissa data och tar sedan kontakt med operativsystemet för att etablera kontakt med filen. Om allt fungerar som det ska kommer fopen() att svara med adressen till _iobuf-strukturen, vilken vi lagra i vår FILE-pekare.

 

Om operativsystemet av någon anledning inte kan öppna filen kommer vi att få värdet noll i pekaren. Detta måste kollas innan man försöker gå vidare. Gick det inte att öppna filen, ska man naturligtvis inte försöka läsa eller skriva i den.

 

Funktionen fopen() tar två argument av typen char*, det vill säga vi ska ange två texter. Enligt syntaxen ovan är dessa filnamn respektive arbetsläge.

 

Filnamn är ett vanligt filnamn med eller utan sökväg. Man kan ange såväl enhet som sökväg och relativ sökväg. Om filen finns i samma katalog som program­met behöver man bara ange filnamnet. Om man anger en sökväg måste man tän­ka på att tecknet '\' är ett styrtecken. Kompilatorn tolkar detta tecken på ett spe­ci­ellt sätt. Om man skriver ett 'n' efter detta styrtecken kommer kompilatorn att tolka det som 'ny rad'. Detta har vi ju använt tidigare (\n). Det finns flera styr­tecken, men vi bryr oss inte om dem här. Viktigt just nu är att veta att man kan komma runt problemet genom att skriva '\\'. Detta uppfattar kompilatorn som att vi vill ha med ett '\' i texten. (Samma sak gäller om man behöver ha ett dubbelt citattecken i en text. Då skriver man '\"'.)

 

Om vi nu skulle skriva till exempel följande filnamn som en textsträng:

 

"C:\CCPP\LASFIL\TEXTFIL.TXT"

 

...skulle kompilatorn klaga över styrtecknen 'C', 'L' respektive 'T'. I stället skriver man:

 

"C:\\CCPP\\LASFIL\\TEXTFIL.TXT"

 

Funktionen fopen() skulle ju ha två textsträngar som argument. Den andra text­strängen står för arbetsläge. Här talar man om huruvida man avser att läsa eller att skriva i filen. Om man skriver i en fil som redan innehåller data kommer man att skriva över det data som fanns från början, varvid det går förlorat. Det går att i stället skriva till data på slutet av filen, och detta kallas för append.

 

Arbetsläge kan vara:    r   = read, läs fil.

                                  w =  write, skriv fil.

                                  a  =  append, förläng fil.

 

Operativsystemet håller reda på var i filen man läser eller skriver. Man kallar den variabel som håller reda på detta för filpekaren. Öppnar man en fil för att läsa kommer filpekaren att stå i början av filen. När du ber om nästa tecken kommer filpekaren att flyttas ett steg framåt i filen. Läser man vidare stegas filpekaren framåt ett tecken i taget tills du hittar EOF.

 

Öppnar man en fil för att skriva kommer filpekaren också att stå i början av filen. När man sedan stänger filen kommer EOF att skrivas där filpekaren står. Då har man antingen skrivit över filens innehåll, eller flyttat EOF till filens början, och DOS kommer att säga att den är tom. Hur du än gör så förlorar du innehållet i filen. Finns inte filen när du vill öppna den, kommer DOS att skapa den, om du öppnar med arbetsläget "w".

 

Vill du öppna en fil för skrivning utan att förlora innehållet ska du använda för­längning (append). Då sätts filpekaren vid EOF. Du kan då lägga till data i slut­et av filen, det vill säga förlänga den.

 

Om man har en fil öppen för skrivning eller append kommer en filslutsmarker­ing att läggas till efter sista skrivna tecknet när man stänger filen. Vi ska strax titta på hur man gör detta.

 

Olika exempel på filöppning:

 

// Öppna filen c:\ccpp\lasfil\text.txt för läsning:

pInFil = fopen("c:\\ccpp\\lasfil\\text.txt","r");

 

// Öppna filen text.txt i aktuell katalog för skrivning:

pInFil = fopen("text.txt","w");

 

// Öppna filen text.txt i underkatalogen texter för append:

pInFil = fopen("texter\\text.txt","a");

 

Här vore det kanske naturligt att börja använda filen, men alla filer som öppnats måste så småningom stängas. Funktionerna fopen() och fclose() och uppträder där­för i 'par', så vi tar och tittar på stängning redan nu:

 

Syntaxen för att stänga en fil:

 

fclose(FILE-pekare);

 

Man behöver alltså bara ange pekaren, inte filnamnet, för att stänga filen. Till exempel:

 

fclose(pInFil);

 

Det är faktiskt en sak till vi bör titta på innan vi börjar använda filen. Observera att fopen() levererade en adress i variabeln infil. Skulle DOS ha något att invän­da mot att vi öppnar filen, så får vi ingen adress, vi får värdet noll i stället. Vi mås­te alltså testa pekaren innan man försöker läsa eller skriva:

 

if (infil == 0)

{

    cout << "Det gick inte att öppna filen!\n";

}

else

{

   // Använd filen.

}

 

Man kan kombinera ihop funktionsanropet och testen (sånt här älskar äkta C++ programmerare, koden blir ju ännu svårare att förstå):

 

if ((pInFil = fopen("text.txt", "r")) == 0)

{

    cout << "Det gick inte att öppna filen!\n";

}

else

{

    // Använd filen.

}

 

Du kan naturligtvis vända på logiken:

 

if ((pInFil = fopen("text.txt","r")) != 0)

{

    // Använd filen.

}

else

{

    cout << "Det gick inte att öppna filen!\n";

}

 

Om man inte kunde öppna filen, så ska man naturligtvis inte stänga den efter sig. Så här kan vi placera in stängningen på rätt ställe i ett av de ovanstående exemplen:

 

if ((pInFil = fopen("text.txt","r")) == 0)

{

    cout << "Det gick inte att öppna filen!\n";

}

else

{

    // Använd filen.

    fclose(pInFil);

}

 

Definition: Funktionen fopen() används för att etablera kontakt med en fil via operativsystemet. Man anger då filens namn samt huruvida man vill läsa, skriva eller förlänga filen. Det går inte att läsa eller skriva utan att använda fopen() först. Funktionen fclose() avslutar kontakten.

 

Övningsexempel 10.2.1:

·      Skapa ett nytt projekt OpenClose.

·      Skriv in ovanstående program.

·      Vid kommentaren '// Använd filen.' skriver du "Använder filen..." på bildskärmen.

·      Prova att köra programmet, så här ska det bli:

 

                

 

Det gick inte att öppna filen, för den finns ju inte.

 

Övningsuppgift 10.2.2:

·      Öppna projektet OpenClose om det inte redan är öppet.

·      Använd developer studio till att skapa en ny fil som heter text.txt. Du gör på samma sätt som när du skapade källkoden till programmet. Filen ska alltså hamna i samma katalog som källkoden. Du behöver inte skriva något i filen.

·      Testa nu programmet igen (utan att kompilera om det).

·      Så här ska det bli:

 

                

 

3. Läsa filer.

 

Vi kan läsa en fil med hjälp av funktionen fgetc(). Som du kan se ovan finns den deklarerad i stdio.h, och den vill ha en pekarvariabel av type FILE som argument. Funktionen fgetc() vill ha en FILE-pekare som argument och retur­nerar ett tecken ur filen. Denna funktion läser ett tecken ur filen.

 

Exempel, öppna en fil och läs första bokstaven, visa bokstaven på skärmen och stäng filen:

 

#include <iostream.h>

#include <stdio.h>

 

void main()

{

   FILE * pInFil;

   char cBokstav;

   if((pInFil= fopen("test.txt","r")) != 0)

   {

      cBokstav = fgetc(pInFil);

      cout << "Första bokstaven i filen är: "

           << cBokstav << "\n\n";

      fclose(pInFil);

   }

   else cout "Det gick inte att öppna filen!\n\n";

}

 

Övningsuppgift 10.3.1:

·      Öppna projektet OpenClose om det inte redan är öppet.

·      Ändra i programmet så att det skriver ut första bokstaven i filen.

·      Öppna filen text.txt och skriv in ditt förnamn. Var noga med att börja längst upp till vänster.

·      Stäng filen text.txt.

·      Kör programmet. Så här kan det se ut:

 

 

 

När fgetc() läser ett tecken ur filen kommer den så kallade filpekaren att flyttas fram ett steg. Vi behöver inte själva manipulera den, även om det finns funk­ti­oner för just detta. Upprepade anrop till fgetc() hämtar ett tecken i taget ur filen. När filen är slut returneras det tecken som motsvarar EOF. Om man lägger in läsningen i en programslinga, kan man låta pro­gram­slingan avbrytas när det lästa tecknet är lika med EOF.

 

Övningsuppgift 10.3.2:

·      Gör ett program LasFil som läser en fil som heter test.txt och skriver ut texten i filen på skärmen på skärmen.

·      Gör en textfil som heter test.txt  och skriv in texten:
”Denna fil har jag skrivit själv.
Nu ska jag göra ett program som kan läsa den och visa på skärmen.”

·      Rätta svenska tecken med hjälp av DOS-Edit.

·      Spara och stäng filen test.txt.

·      När man kör programmet ska det se ut så här:

 

 

Övningsuppgift 10.3.3:

·      Gör ett program som räknar antalet tecken i textfilen från exemplet ovan.

·      Kalla programmet CharCnt.

 

Övningsuppgift 10.3.4:

·      Gör ett program som räknar orden i samma textfil.

·      Kalla programmet WordCnt.

·      Tips: efter varje ord finns ett tecken som inte är en bokstav. Tecknet är vanligen ett mellanslag, men kan även vara ett skiljetecken. Dessutom kan det vara ett nyradstecken eller, om ordet står sist i filen, ett EOF-tecken.

 

4. Skriva filer.

 

Vill man skriva i en fil måste man öppna den med ‘w’ eller ‘a’ enligt vad som sagts ovan. Själ­va skrivningen kan man göra med funktionen fputc(). Funk­ti­on­en vill ha en var­i­abel av typen char eller int som första argument, och till FILE-pekaren som an­dra argument, t.ex:

 

fputc(cBokstav, pUtFil);

 

Vill man t.ex. kopiera innehållet i en fil till en annan, tecken för tecken, kan man klara det med en enda liten loop:

 

while((cBokstav = fgetc(pInFil)) != EOF)

{

    fputc(cBokstav, pUtFil);

}

 

Övningsuppgift 10.4.1:

·      Skriv ett program FileCopy som kopierar filer enligt ovanstående.

·      Först ska det fråga efter infilens namn, sedan efter utfilens.

·      Därefter sker kopiering, om såväl infil som utfil gick att öppna.

·      Antal kopierade tecken ska slutligen redovisas.

·      Så här kan det se ut om man till exempel kopierar autoexec.bat från rotkatalogen på C: till aktuell katalog:

 

 

 

·      Kontrollera att kopian är korrekt genom att öppna filen för redigering i Developer Studio.

·      Antalet kopierade tecken kan vara annorlunda på din dator, beroende på hur många tecken du har i autoexec.bat.

 

5. Radvis läsning och skrivning.

 

Än så länge har vi nöjt oss med att läsa och skriva ett tecken i taget. I övnings­uppgift 10.3.2, LasFil, läste vi en fil som innehöll nyradstecken. När vi redi­ger­ar en text och trycker på 'Enter' så lagras det ett tecken för ny rad i filen. Det­ta tecken lästes och tolkades av cout på så sätt att vi fick en ny rad i vår utskrift. Man kan också använda fgets() och fputs() för att läsa/skriva hela rader. Här är syntaxen för fgets() och fputs():

 

fgets(textpekare, max antal tecken, FILE-pekare);

fputs(textpekare, FILE-pekare);

 

Funktionen fgets() kopierar en rad från filen och lägger in den där textpekaren pekar. Om raden är längre än max antal tecken kommer läsningen att avbrytas där. Resten av raden läses i så fall vid nästa fgets(). Om filen är slut kommer fgets att returnera värdet noll, vilket man alltså bör testa vid varje läsning.

 

När fgets() läst in en rad i den charlista vi deklarerat måste vi tänka på att raden inte avslutas med en nolla. En textfil har ju normalt radslutstecken, '\n', i slutet av varje rad. Vill vi använda den inlästa texten i någon funktion som är bero­en­de av en slutnolla, till exempel strcmp,  måste vi lägga till en sådan. Gör i så fall  charlistan ett extra tecken längre, och lägg själv till slutnollan efter läsning. Så här kan man göra:

 

// Läs en rad och kontrollera att det gick att läsa:

if(fgets(cRad,80,pInFil)

{

    // Leta upp positionen efter radslutstecknet:

    i = 0;

    while(cRad1[i++] != '\n');

 

    // Tilldela positionen efter radslutstecknet värdet noll:

    cRad1[i] = '\0';

}

 

Nu har vi en text som avslutas med först ett radslutstecken och sedan med en slutnolla.

 

Funktionen fputs() skriver en rad i filen. Om Texten inte innehåller radsluts­teck­en kommer nästa utskrift på samma rad, det vill säga funktionen lägger inte själv till radslutstecken. Vill man ha ett sådant måste man lägga till det själv, antingen genom att lägga det till texten eller genom ett extra anrop till fputs() en extra gång, och ange "\n" som argument:

 

fputs("\n", pUtFil);

 

Observera att nyradstecknet levereras till funktiopnen som en nollterminerad textsträng, inte som en enda char. Funktionen fputs() tar inte en char som första argument. Vad vi egentligen levererat ovan är två tecken, ett nyradstecken och en slutnolla.

 

Om texten innehåller en slutnolla kommer denna inte att kopieras in i filen.

 

Övningsuppgift 10.5.1:

·      Skriv ett program FileCompare som jämför två filer.

·      Använd radvis läsning, det vill säga fgets().

·      Läs en rad ur varje fil samtidigt, och avbryt om den ena eller båda filerna är slut, det vill säga om fgets() returnerar värdet noll.

·      Lägg till slutnollor efter radslutstecknen i de inlästa texterna.

·      Jämför raderna med funktionen strcmp().

·      Skriv en resultatfil som innehåller de rader som skiljer sig: första filens rad, andra filens rad och en blankrad (så att men ser vilka rader som hör ihop).

·      Man ser resultatet genom att öppna resultatfilen i textredigeraren.

·      Skapa textfiler att testa med. Kontrollera resultatet i resultatfilen. Du kan till exempel kopiera din egen källkod och ändra litet i kopian. Så här blev det när jag gjorde det:

 

                

 

Resultatet finns i filen logg.cpp:

 

 

Det kan vara intressant att se hur radslut hanterars i filen. När man öppnar en textfil i Developer Studio finns det en listväljare längs ner i filvals­dia­logrutan, där man kan ange hur filen ska redigeras. Den är förinställd på 'Au­to' vilket innebär att redigeraren känner av filtyp och öppnar den med rätt typ av redigerare. Är det en textfil, *.txt, *.cpp, *.h etcetera, kommer den att öppnas av den vanliga textredigeraren, men vi kan även öppna bilder och andra resurser, typ dialogrutor och menyer, och dessa redigeras på andra sätt. Vi kan styra öppnandet genom att ange en annan typ.

 

Om vi då tittar på en textfil som redigerats med den vanliga textredigeraren, men öppnar den som 'binary', kommer vi att se såväl texten som de hexadeci­mala koder som motsvarar ansikoderna för vår text. På så sätt kan man även se styrtecken i filen. Ett radslut borde ha koden 0D i filen, men vi finner två koder i stället, nämligen 0D och 0A. Dessa representerar faktiskt ny rad och vagnretur, det vill säga börja från början på raden. En skrivare behöver till exempel båda dessa koder.

 

Övningsuppgift 10.5.2:

·      Öppna den fil du skapade i föregående övningsuppgift. Den som du angav som utfil. I mitt fall hette den logg.cpp.

·      När du öppnar den ska du välja 'Binary' i listväljaren nederst i filvalsdialog­rutan.

·      Maximera fönstret så att du kan se hela innehållet.

·      Lägg märke till hur texten, som står till höger, representeras av hexadecimala ansikoder.

·      De koder som inte kan visas på skärmen ersätts till höger av punkter.

·      Leta upp ett ställe där ett radslut borde finnas och markera de två punkter som följer, det vill säga de som motsvarar radslut. Lägg märke till att 0D och 0A markeras i hexkodslistan.

·      Så här kan det se ut: