Föreläsning 4: Filer och strömmar PROGRAM OPERATIV- SYSTEM EXTERNA (IO-)ENHETER cout << Hej... ; STANDARD UTSTRÖM Hej... cin >> x; STANDARD INSTRÖM ifstream infil A.TXT infil >> str; INGÅENDE FILSTRÖM ofstream utfil B.TXT utfil << Svej << endl << Gurka ; UTGÅENDE FILSTRÖM A.TXT hej! HÅRDDISK B.TXT Svej Gurka Idag skall vi prata om filer och strömmar. Vi har tidigare nämnt att filer kan vara bra att ha för att backa upp data. Om man t.ex. har väldigt stora data som inte får plats i internminnet (tänk dig en jättehögupplöst film) så kanske man måste ha data på fil temporärt också. Vi börjar med hur man får åtkomst till filer. För att göra detta behöver man filströmmar, dessa finns i biblioteket fstream. Operativsystemet har inte "öppna filströmmar" till vart enda fil på hårddisken, så om man vill skriva något, eller läsa något från en fil så måste man explicit begära detta från OSet. I samband med detta måste man också bestämma om man vill läsa eller skriva till filen. Vi kan börja med att läsa från en fil som vi antar finnas "A.TXT": #include <fstream> using namespace std; ifstream infil"a.txt"; return 0; Ett annat sätt att öppna filen är att skriva: ifstream infil; infil.open("a.txt"); Nu har vi tillgång till filen och kan läsa från den. T.ex. så här:
string str; infil >> str; Känns det igen? Jovisst, precis som från cin. Dessa strömma fungerar alltså precis som vi är vana vid. Nu bör det direkt nämnas att om man öppnar en fil som skall behandlas så bör man också stänga filen när man är klar med den. Det gör man med close: En annan sak man bör tänka på är att filen kanske faktiskt inte existerar. I detta fall så kommer inläsningen inte att gå vägen, men man får inget felmeddelande. Därför är det bra att i förväg göra en kontroll huruvida filen faktiskt existerade. Det kan man göra genom att kolla strömmens fail-flagga: if (infil.fail()) cerr << "Filen existerade inte!" << endl; return 1; Man kan även använda (kanske mer vanligt) själva strömmen som ett boolskt värde: if (!infil)... samma som ovan Nu kan vi applicera allt vi har lärt oss om inläsning m.a.p. cin på detta! Vi kan tänka oss att istället för att det ligger saker i tangentbordsbufferten ligger det nu saker i en inläsningsbuffert, och dessa saker har hämtats från filen. Vi kan läsa saker från bufferten/strömmen, och då försvinner de där ifrån, men de kommer inte försvinna från filen. Vi går nu över till att skriva till filer. För att göra detta behöver vi istället en ofstream, som också finns i samma bibliotek fstream. eller ofstream utfil"b.txt"; ofstream utfil; utfil.open("b.txt"); Nu finns två möjligheter. Om filen inte existerade så kommer den nu skapas och vi kan då börja skriva saker till den. Om filen existerade sedan tidigare så kommer filen öppnas och tömmas så att när vi börjar skriva saker till den så kommer detta vara det enda som finns där när vi är klara. D.v.s. om vi nu gör: utfil << "Hejsan" << endl << "Gurka" << endl; Så kommer filen efteråt (oavsett om den fanns sedan tidigare med innehåll, eller om vi nu skapade den) se ut så här: Hejsan Gurka Det bör också nämnas att en textfil bör avslutas med ett newline innan filslutet, annars är det per definiton inte en textfil.
Nu kanske man tycker det är synd att filen töms helt och hållet. Man kanske nu skulle vilja lägga till raden "Potatis" till filen B.TXT, men hur? Trevligt nog finns det en filmod man kan skicka med till open för att tala om att man vill lägga till (append på engelska): eller ofstream utfil2; utfil2.open("b.txt", ofstream::app); ofstream utfil2"b.txt", ofstream::app); om vi nu gör följande: utfil2 << "Potatis" << endl; Så får vi det önskade resultatet: Hejsan Gurka Potatis Nu vill vi göra något så häftigt som att kopiera en hel fil till en annan! Hur gör vi detta? Vi kan börja med att formulera ett litet huvudprogram: ifstream infil"a.txt"; ofstream utfil"b.txt"; copy(infil, utfil); utfil.close(); Nu formulerar vi underprogrammet. Vi kan ju läsa en hel rad med getline, men tänk om det är en jäääääättelång rad som tar många mbytes! Det känns lite onödigt att kopiera in allt det i minnet samtidigt, egentligen kan vi ju bara överföra ett tecken i taget! void copy(ifstream& infil, ofstream& utfil) char c; while (infil.get(c)) utfil.put(c); Nu har vi ett program som kan kopiera filer! Om man nu ser detta lite mer generellt så kopierar vi faktiskt från en inström till en utström, skulle vi då kunna anropa vårt program så här: ifstream infil"a.txt"; copy(infil, cout); // funkar detta??
Besväret med detta är att vi i vårt underprogram copy har sagt att utströmmen skall vara just en ofstream, d.v.s att det har med filer att göra. Om vi istället ändrar här och skriver att parametern är en "ostream" så fungerar detta galant! Vi kan göra samma sak med den andra parametern: void copy(istream& infil, ostream& utfil) char c; while (infil.get(c)) utfil.put(c); Att kunna hantera data med strömmar och formaterat in/utmatning på detta vis är ibland mycket praktiskt. Vi tänker oss nu att vi har följande uppgift framför oss: Vi har i en godtykligt lång sträng ett heltal, t.ex. "31874823". Vi skulle nu vilja konvertera detta till datatypen int (vi utgår ifrån att det får plats i en int). Ett sätt att göra detta är att gå igenom strängen och helt enkelt räkna ihop summan, men då kommer vi ändå att behöva konvertera de enskilda tecknen i något läge. Ett annat sätt är att skriva strängen till en fil och sedan läsa in från filen igen, fast då läsa en int. Ungefär såhär: string number = "31874823"; int x; // i denna vill vi ha talet 31874823 ofstream utdata"tmpfil.txt"; ofstream << number; utdata.close(); instream indata"tmpfil.txt"; indata >> x; utdata.close(); Nu känns det ju lite smålöjligt att behöva använda en fil till detta. Det är ju egentligen inget som behöver sparas eller på något sätt hanteras utanför vårt program. Det vore därför bra att ha strömmar som man helt enkelt bara kunde dra till med när man behöver dem. Just detta är strängströmmar (biblioteket sstream). Vi kan tänka oss följande bild nu: stringstream strs; strs << number; int x; strs >> x; STRÄNGSTRÖM Jag har nu ritat denna som del av vårt program, eftersom vi inte behöver blanda in operativsystemet eller någon annan del av datorn för att sköta just det här. Strängströmmar är alltså praktiska i de lägen då vill vi "skriva lite data" för att sedan läsa tillbaka det. Det som är praktiskt med stringstream är att det både är en inström och en utström. Vi kan t.ex. nu göra detta för att fylla på vår strängström: copy(cin, strs); Eller för att skriva ut vår strängström till cout: copy(strs, cout);
Övning på lektion Detta lektionsmaterial är omfattande. Räkna inte med att hinna med allt som står utan låt det ta den tid det tar. Man bör hinna med två uppgifter. Uppgift 1: Antag att vi inte har tillgång till "ios::app" (eller "ofstream::app") i den kompilator som finns. Hur gör vi om vi vill utföra en "Append" iallafall? Vi har tillgång till allt utom just "::app". Detta problem är bra till att diskutera hur man löser problem (d.v.s. det som kursen går ut på). Man kan här ta upp saker som att man kan "använda brain storming", "tänka på svenska", "post-itlappar", "stegvis förfining", "skjuta problem framför sig" m.m. Vi kan börja med att skriva ett huvudprogram som visar vad det är vi vill komma fram till. #include <ofstream> using namespace std; ofstream utfil; // vi borde skriva // utfil.open("a.txt", ofstream::app); // men vi har ju kompilatorbugg för att få fram // något bra exempel att arbeta med på lektion : ) // OBS! ofstream::app fungerar utmärkt med g++ append(utfil, "A.TXT"); utfil << "Lite extra text..." << endl; utfil.close(); Studenterna kan ju få hjälpa till med vilken datatyp som skall användas m.m., men det är nog bra att få fram budskapet vad uppgiften går ut på (det brukar finnas en del funderingar på vad det är annars). Vi skall alltså skapa oss en 'Append' som ser till att filen kan skrivas på och att de data som redan låg på filen inte försvinner. Bra att veta att standardbeteende (då man inte anger ofstream::app) innebär att filens innehåll "raderas". Det går inte att öppna för läsning och läsa från början till slut och sen tro att det bara är att fortsätta skriva. Det brukar bli en del diskussion om hur man skall göra och det får det väl bli. Försök att vara öppen och skriv upp det de säger så kanske man till slut kan få en lista på bra saker som skall göras. Då någon kommit på att det går att kopiera filen till en ny fil och sen kopiera tillbaka den för att sen lämna den öppen för vidare skrivning har vi kommit fram till lösningen. Det är inte tillåtet att spara alla data i ett fält eller annat (filer kan vara alldeles för stora). Någon kanske ger förslag på en strängström, men det är egentligen samma problem där, STORA filer.
Vi antecknar vad som måste göras (inte alls säkert att förslagen kommer i den rätta ordningen, men vi styr lite så blir det bra): 1) Öppna originalfilen (vi kallar den A) för läsning. 2) Skapa en ny fil (vi kallar den B) för skrivning. 3) Kopiera A -> B. 4) Stäng A och B. 5) Öppna B för läsning. 6) Öppna A för skrivning. 7) Kopiera B -> A. 8) Ta bort B (inte helt nödvändigt). 9) KLART! Översätt detta till kod! Det borde inte vara så svårt. :-) void append(ofstream& utfil, string filnamn) ifstream infilfilnamn; ofstream tmpfil"tmp.tmp"; copy(infil, tmpfil); tmpfil.close(); infil.open("tmp.tmp"); utfil.open(filnamn); copy(infil, utfil); Vi behöver en extra procedur som kopierar (om vi inte var klantiga när vi skrev "Append"). void copy(istream& instrom, ostream& utstrom) char c; while (infil.get(c)) utfil.put(c); Denna borde de klara som en dans då vi redan haft den på FÖ.
Uppgift 2: Läs in de två sista talen från en fil, summera dessa och skriv sedan ut summan i slutet av samma fil. Om det inte existerar några tal eller bara finns ett tal på filen ska programmet se till att filen innehåller de två talen 1 och 1 istället. Vi kan anta att det alltid finns en fil i första problemet. ifstream infil"tal.txt"; int a; int b; int x; int no_of_numbers = 0; while (infil >> x) if (no_of_numbers < 2) ++no_of_numbers; b = a; a = x; ofstream utfil; if (no_of_numbers == 1) utfil.open("tal.txt"); utfil << 1 << endl << 1 << endl; else utfil.open("tal.txt", ofstream::app); utfil << a + b << endl; utfil.close(); return 0; Uppgift 3: Samma problem som i uppgift 2, men om filen inte existerar ska programmet skapa filen och skriva ut de två talen (1 och 1) på den. Ja, nu behövs det felhantering om man skall lösa detta problem. Enda ändringen blir i detta fall att vi vill fånga felet som kan uppstå för att filen inte existerar och detta kan vi göra i början av programmet.
ifstream infil"tal.txt"; if (!infil) ofstream utfil"tal.txt"; utfil << 1 << endl << 1 << endl; return 1; int a; int b; int x; int no_of_numbers = 0; while (infil >> x) if (no_of_numbers < 2) ++no_of_numbers; b = a; a = x; ofstream utfil; if (no_of_numbers == 1) utfil.open("tal.txt"); utfil << 1 << endl << 1 << endl; else utfil.open("tal.txt", ofstream::app); utfil << a + b << endl; utfil.close(); return 0;