1. Tiedostot ja makrot Mitä tässä luvussa käsitellään? ? Tiedostojen käsittely C–funktiolla ? Tiedostojen käsittely C++ –tietovirroilla ? sizeof ? strlen ? Parametrilliset makrot ? Tiedostot joissa rivillä monta kenttää Syntaksi: Tied. avaaminen C: FILE *f = fopen(nimi,tyyppi); C++: ifstream fi(nimi); ofstream fo(nimi); ofstream fo(nimi,ios::app); tai: ifstream fi; … fi.open(nimi); Lukeminen C: fscanf(f,format,osoite,...); fgets(mjono,max_pit,f); C++: fi >> muuttuja; getline(fi,mjono); Kirjoittaminen C: fprintf(f,format,lauseke,...); C++: fo << lauseke; Sulkeminen C: fclose(f); C++: fi.close(); tai automaattisesti hajottajan ansiosta Muuttujan koko sizeof(muuttuja) // tavuina Tyypin viemä tila sizeof(tyyppi) // tavuina Yhdessä käytettävät funktiot tai operaattorit: C: FILE *fi = fopen(nimi,"rt") - fscanf(fi,...) - fgets(...,fi) - feof(fi) - fclose(fi) FILE *fo = fopen(nimi,"wt") - fprintf(fo,...) - fclose(fo) C++: ifstream fi(nimi) - fi >>... - getline(fi,...) - fi.eof() - fi.close() ofstream fo(nimi) - fo <<... - fo.close() Pyrimme seuraavaksi lisäämään kerho–ohjelmaamme tiedostosta lukemisen ja tiedostoon tallettamisen. Tätä varten tutustumme ensin lukemiseen mahdollisesti liittyviin ongelmiin. Perehdymme tässä luvussa tarkemmin vain C++:n tiedostonkäsittelyyn. 1.1 Tiedostojen käsittely Tiedostojen käsittely ei eroa päätesyötöstä ja tulostuksesta, siis tiedostojen käyttöä ei ole mitenkään syytä vierastaa! Itse asiassa päätesyöttö ja tulostus ovat vain stdin ja stdout –nimisten tiedostojen käsittelyä. Tiedostoja on kahta päätyyppiä: tekstitiedostot ja binääritiedostot. Tekstitiedostojen etu on siinä, että ne käyttäytyvät täsmälleen samoin kuin päätesyöttökin. Binääritiedoston etu on taas siinä, että talletus saattaa viedä vähemmän tilaa (erityisesti numeerista muotoa olevat tyypit) ja suorasaannin käyttö on järkevämpää. Keskitymme aluksi tekstitiedostoihin, koska niitä voidaan tarvittaessa editoida tavallisella editorilla. Näin ohjelman testaaminen helpottuu, kun tiedosto voidaan rakentaa valmiiksi ennen kuin ohjelmassa edes on päätesyöttöä lukevaa osaa. 1.1.1 Lukeminen Muutamme hieman alkuperäistä suunnitelmaamme jäsenrekisteritiedoston sisällöstä: Kelmien kerho ry 100 ; Kenttien järjestys tiedostossa on seuraava: id| nimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin... 1|Ankka Aku |010245–123U|Ankkakuja 6 |12345 |ANKKALINNA |12–12324 ... 2|Susi Sepe |020347–123T| |12555 |Perämetsä | ... 3|Ponteva Veli |030455–3333| |12555 |Perämetsä | ... Olemme lisänneet rivin, jossa kerrotaan tiedoston maksimikoko. Tätähän tarvittiin jäsenlistan luomisessa. Nyt kokoa voidaan tarvittaessa muuttaa tiedostosta tekstieditorilla tarvitsematta tietää ohjelmoinnista mitään. Tiedoston sisällössä on kuitenkin pieni ongelma: siinä on sekaisin sekä puhtaita merkkijonoja, numeroita että tietuetyyppisiä rivejä. Vaikka kielessä onkin työkalut sekä numeeristen tietojen lukemiseksi tiedostosta, että merkkijonojen lukemiseen, nämä työkalut eivät välttämättä toimi yksiin. Siksi usein kannattaa käyttää lukemiseen vain yhtä työkalua, joka useimmiten on kokonaisen tiedoston rivin lukeminen. 1.2 Tiedostojen käsittely C++:n tietovirroilla Tiedostojen käsittely C++:ssa on vain cin ja cout –tietovirtoja vastaavien tietovirtojen käsittelyä. Olkoon meillä tiedosto nimeltä luvut.dat: 13.4 23.6 kissa 1.9 <– ei aina välttämättä mikään merkki Kirjoitetaan esimerkkitiedoston luvut lukeva ohjelma C++:n tietovirroilla. Tarkoitus on hylätä ne rivit, joiden alussa ei ole reaalilukua: tiedosto\Tied_kaG.cpp - Lukujen lukeminen tiedostosta // Ohjelma lukee tiedostosta luvut.dat lukuja ja tulostaa niiden // summan ja keskiarvon. Jos tiedostossa on virheellisiä // rivejä, tulostetaan ne. #include #include #include // File stream #include using namespace std; int main(void) { double luku,summa,ka; string s; int n; ifstream fi("luvut.dat"); if ( !fi ) { cout << "Tiedosto ei aukea!" << endl; return 1; } summa = 0.0; n = 0; ka = 0.0; while ( getline(fi,s) ) { if ( sscanf(s.c_str(),"%lf",&luku) <= 0 ) { cout << s << "\n"; continue; } summa += luku; n++; } fi.close(); if ( n > 0 ) ka = summa/n; cout.precision(2); cout.setf(ios::showpoint | ios::fixed); cout << "\n"; cout << "Lukuja oli " << n << " kappaletta\n"; cout << "Niiden summa oli " << summa << "\n"; cout << "ja keskiarvo oli " << ka << endl; return 0; } Tehtävä 17.150 Tiedoston lukujen summa 1. Muuta tiedoston Tied_kaG.cpp –ohjelmaa siten, että väärän rivin kohdalla tulostetaan väärä rivi ja lopetetaan koko ohjelma. 2. Muuta edelleen ohjelmaa siten, että väärät rivit tulostetaan näyttöön: Tiedostossa oli seuraavat laittomat rivit: kissa Lukuja oli... Ilmoitusta ei tietenkään tule, mikäli tiedostossa ei ole laittomia merkkejä. Tyhjää riviä ei tulkita vääräksi riviksi. 1.2.1 Tiedoston avaaminen muodostajassa tai open Tiedosto voidaan siis avata heti kun tiedostoa vastaava tietovirta esitellään: ifstream f("luvut.dat"); // Input File STREAM Parametri "luvut.dat" on tiedoston nimi levyllä. Nimi voi sisältää myös hakemistopolun, mutta tätä kannattaa välttää, koska hakemistot eivät välttämättä ole samanlaisia kaikkien käyttäjien koneissa. Jos hakemistopolkuja käyttää, niin erottimena kannattaa käyttää /-merkkiä. Samoin kannattaa olla tarkkana isojen ja pienien kirjainten kanssa, sillä useissa käyttöjärjestelmissä kirjainten koolla on väliä. Tiedoston nimiparametri on useimmiten tietysti muuttuja, valitettavasti kuitenkin vain C-merkkijono. Mikäli tiedoston nimi on C++-merkkijonossa, on se siis muutettava C- merkkijonoksi: string s = "luvut.dat"; ifstream f(s.c_str()); // vain C-merkkijonot kelpaavat Tiedosto voidaan myös jättää avaamatta esittelyn yhteydessä ja avata sitten myöhemmin open–metodilla: ifstream f; ... f.open("luvut.dat"); Lukemista varten avattaessa tiedoston täytyy olla olemassa tai avaus epäonnistuu. Tätä voidaan tietenkin käyttää hyväksi esimerkiksi tutkittaessa onko tiedostoa lainkaan olemassa. Tiedoston aukeamisen tila voidaan testata tietovirtaolion arvosta esimerkiksi if ( !f ) { ... 1.2.2 Tiedostosta lukeminen >> ja tiedostoon kirjoittaminen << Tiedostosta lukeminen on jälleen analogista päätesyötön kanssa: fi >> luku; Kuitenkin jos tiedostosta ei olekaan lukua, on virheen käsittely kohtuullisen työlästä. Siksi mieluummin kannattaa aina lukea tiedostosta rivi merkkijonoon ja sitten käsitellä tämä merkkijono tarvittavalla tavalla. Lisäksi >>-operaattorilla luettaessa lukupuskuri jää rivin "loppumerkin" kohdalle. Tällöin seuraava getline saa vain tyhjän rivin. Tämänkin vuoksi on helpompaa lukea aina kokonainen rivi. Vastaavasti kirjoittamista varten avattuun tiedostoon kirjoitettaisiin ofstream fo("tulos.dat"); // avataan tiedosto kirjoittamista varten ... // avauksessa vanha tiedosto tuhoutuu fo << luku; Mikäli avattaessa tiedostoa kirjoittamista varten, ei haluta tuhota vanhaa sisältöä, vaan kirjoittaa vanhan perään, käytetään avauksessa openmode parametriä ios::app (append): ofstream fo("virheet.txt",ios::app); // avataan perään kirjoittamista varten Tiedoston jatkaminen on erittäin kätevä esimerkiksi virhelogitiedostoja kirjoitettaessa. Tiedoston lukemisessa ja kirjoittamisessa myös kaikki muut cin ja cout –olioista tutut metodit ja funktiot ovat käytössä, esimerkiksi: char s[80]; string st; fi.getline(s,sizeof(s)); getline(fi,st); Useimmiten kannattaa kaikki näyttöön tulostavat aliohjelmat/metodit kirjoittaa sellaiseksi, että niille viedään parametrinä se tietovirta, johon tulostetaan. Näin samalla aliohjelmalla voidaan helposti tulostaa sitten näyttöön tai tiedostoon tai jopa kirjoittimelle (joka on vain yksi tietovirta muiden joukossa, esim. Windowsissa PRN- niminen tiedosto). 1.2.3 Tiedoston lopun testaaminen eof Totuusarvotyyppinen metodi eof palauttaa tosi, kun ollaan tiedoston lopun kohdalla. while ( !fi.eof() ) { Valitettavasti arvo on tosi vasta kun kuvitteellisen loppumerkin kohdalle on saavuttu, EI silloin kun se on seuraavana. Esimerkiksi tiedostosta 13.4 saataisiin kaksi reaalilukua silmukalla: while ( !fi.eof() ) { ? fi >> luku; summa += luku; n++; } Jälleen helpompi ratkaisu on perustaa lukusilmukka siihen, että yritetään lukea kokonainen tiedoston rivi ja jos tämä epäonnistuu, on tiedostokin todennäköisesti loppu. 1.2.4 Tiedoston sulkeminen close Avattu tiedosto on aina lukemisen tai kirjoittamisen lopuksi syytä sulkea. Tiedoston käsittely on usein puskuroitua, eli esimerkiksi kirjoittaminen tapahtuu ensin apupuskuriin, josta se kirjoittuu fyysisesti levylle vain puskurin täyttyessä tai tiedoston sulkemisen yhteydessä. Käyttöjärjestelmä päivittää tiedoston koon levylle usein vasta sulkemisen jälkeen. Sulkemattoman tiedoston koko saattaa näyttää 0 tavua. C++:ssa tiedosto voidaan joskus jopa jättää sulkematta, koska tietovirtaolion hajottaja sulkee tiedoston joka tapauksessa. Tästä huolimatta tiedosto kannattaa sulkea, jos sen käyttö on loppu ja ohjelmalohkon lopussa on vielä jonkin aikaa kestäviä operaatioita: fi.close(); Tehtävä 17.151 Kommentit näytölle Kirjoita ohjelma, joka kysyy tiedoston nimen ja tämän jälkeen tulostaa tiedostosta rivien /******* ja––––––*/välisen osan näytölle. 1.3 sizeof Useille C–kirjaston valmiille aliohjelmille sekä myös monille itse kirjoittamillemme aliohjelmille täytyy viedä parametrinä käsiteltävän C-merkkijonon maksimikoko: char jono[80]; ... lue_jono(jono,80); ... f_lue_jono(f,jono,80); ... kopioi_jono(jono,80,"Kissa"); ... cin.getline(jono,80); Edellisessä on vielä vaarana se, että muutettaisiin jonon maksimikokoa 80, mutta samalla unohdettaisiin päivittää aliohjelmien kutsut. Yksi ratkaisu on määritellä vakio: #define MAX_JONO 80 char jono[MAX_JONO]; ... cin.getline(jono,MAX_JONO); ... 1.3.1 sizeof palauttaa muuttujaan varaaman tilan Toinen usein kätevämpi tapa on käyttää käännösaikaista sizeof –operaattoria, joka palauttaa muuttujan tai tyypin tarvitseman muistitilan. Esimerkiksi char jono[80]; int koko; ... koko = sizeof(jono); sijoittaisi muuttujalle koko arvon 80. Voisimme siis kirjoittaa kutsuja: char jono[80]; ... lue_jono(jono,sizeof(jono)); ... f_lue_jono(f,jono,sizeof(jono)); ... kopioi_jono(jono,sizeof(jono),"Kissa"); cin.getline(jono,sizeof(jono)); sizeof –operaattorille voidaan antaa parametrinä myös tyypin nimi: typedef struct { int pv; char kk_nimi[20]; int vv; } Pvm_tyyppi; ... int vuosi; Pvm_tyyppi pvm; ... ... sizeof(vuosi) ... /* Esim 2 tai nykyisin 4 toteutuksesta riippuen */ ... sizeof(int) ... /* – " – */ ... sizeof(char) ... /* Aina 1 */ ... sizeof(Pvm_tyyppi) /* Esim. 4+20+4 == 28 tot. riip. */ ... sizeof(pvm.kk_nimi) /* 20 */ ... 1.3.2 sizeof ei ole strlen sizeof –operaattoria EI PIDÄ sotkea strlen –funktioon: char jono[80]; int koko,pituus; ... koko = sizeof(jono); /* 80 */ kopioi_jono(jono,koko,"Kissa"); pituus = strlen(jono); /* 5 */ 1.3.3 sizeof:in vaarat sizeof –operaattoria ei pidä käyttää huolimattomasti. Esimerkiksi: char *viesti ="Kissat uimaan!"; char vastaus[40] ="Ei kissat ui!"; ==> sizeof(viesti) == 2 tai 4 tai vastaavaa muistimallista riippuen sizeof(vastaus) == 40 !!! Helposti tulee käytettyä osoitinta ja saadaan osoittimen koko, kun itse asiassa tarvittaisiin itse muistipaikan koko. Tällaisista virheistä kääntäjä ei varoita mitään (koska mitään syntaksivirhettä ei ole)! 1.4 Parametrilliset makrot Esimerkiksi edellä jouduimme käyttämään muotoja char jono[80]; ... cin.getline(jono,sizeof(jono)); ... kopioi_jono(jono,sizeof(jono),"Kissa"); Voisimme tietysti määritellä makron #define JONOS jono,sizeof(jono) jolloin kutsut supistuisivat muotoon cin.getline(JONOS); ... kopioi_jono(JONOS,"Kissa"); 1.4.1 Helpottaa kirjoittamista Mutta entäpä jos haluaisimme käyttää toisen nimistä muuttujaa. Määriteltäisiinkö uusi makro? Onneksi C–kielen esiprosessori sallii parametrin käytön makroissa. Siispä voimme rakentaa vaikkapa makron (N_S, name and size) #define N_S(nimi) nimi,sizeof(nimi) jonka esiintymän esiprosessori muuttaa vastaavasti char jono[80],elain[20]; f_lue_jono(f,N_S(jono)); kopioi_jono(N_S(elain),"Kissa"); cin.getline(N_S(jono)); esiprosessori ==> f_lue_jono(f,jono,sizeof(jono)); kopioi_jono(elain,sizeof(elain),"Kissa"); cin.getline(jono,sizeof(jono)); kääntäjä ==> f_lue_jono(f,jono,80); kopioi_jono(elain,20,"Kissa"); cin.getline(jono,80); Siis makron parametrilistassa olevat sanat korvataan ensin niillä sanoilla jotka ovat makron esiintymässä. Tämän jälkeen esiintymä korvataan tällä uudella merkkijonolla ja lopulta korvattu merkkijono annetaan kääntäjän käsiteltäväksi. 1.4.2 Ole kuitenkin varovainen Makrot ovat kuitenkin hyvin vaarallisia huolimattomasti käytettynä: #define K_2(i) (i + i) ... a = K_2(4); –> a = (4 + 4); a = K_2(n++); –> a = (n++ + n++); /* !!!!!!!!!! */ Onneksi C++:ssa ei enää tarvita niin paljon parametrillisiä makroja kuin puhtaassa C:ssä. Sama asia voidaan useimmiten hoitaa inline–funktiolla: inline int k_2(int i) { return (i + i); } ... a = k_2(4); –> a = 8; a = k_2(n++); –> a = 2*n; n++; // Toimii oikein! Tehtävä 17.152 Makron yllätykset Keksi muita esimerkkejä missä K_2 –makron käyttö tuottaisi yllätyksiä. 1.5 Tiedoston yhdellä rivillä monta kenttää Jäsenrekisterissä on tiedoston yhdellä rivillä useita kenttiä. Kentät saattavat olla myös eri tyyppisiä. Miten lukeminen hoidetaan varmimmin? Usein lukeminen voitaisiin hoitaa sopivasti muotoillulla >> –operaattorilla. 1.5.1 Ongelma Olkoon meillä vaikkapa seuraavanlainen tiedosto: tiedosto\tuotteet.dat - esimerkkitiedosto Volvo | 12300 | 1 Audi | 55700 | 2 Saab | 1500 | 4 Volvo | 123400 | 1 Tiedostoa voitaisiin periaatteessa lukea vaikkapa seuraavasti: string tuote; char valimerkki; double hinta, int kpl; ... fi >> tuote >> valimerkki >> hinta >> valimerkki >> kpl; Ratkaisussa on kuitenkin seuraavia huonoja puolia: ? mikäli tiedoston loppu ei olekaan viimeisen rivin lopussa, tulee "ylimääräisen" rivin käsittelystä ongelmia ? mikäli jokin rivi on väärää muotoa, menee ohjelma varsin sekaisin Tehtävä 17.153 Ohjelman "sekoaminen" Jos esimerkin ratkaisussa olisi silmukka, joka tulostaa tiedot kunkin lukemisen jälkeen, niin mitä tulostuisi seuraavasta tiedostosta: Volvo | 12300 | 1 Audi 55700 | 2 Saab | 1500 | 4 Volvo | 123400 | 1 1.5.2 Rivi kerrallaan lukeminen Ongelmaa voidaan osittain ratkaista lukemalla tiedostoa merkkijonoon aina rivi kerrallaan: ifstream(fi); string rivi; char nimike[20]; double hinta; int kpl; ... while ( getline(fi,rivi) ) { if ( rivi <= "" ) continue; sscanf(rivi.c_str(),"%s |%lf |%d",nimike,&hinta,& kpl); ... } ... Tässäkin vaihtoehdossa on vielä muutamia huonoja puolia: ? virheellinen rivi hyväksytään ja lopuille arvoille jää edellisen kerran arvot ? sscanf käytetyllä formaatilla on vaarallinen, mikäli nimikekenttään sattuisi tulemaan nimike, jossa on yli 20 merkkiä ? mikäli nimikekentässä tarvittaisiin nimi, jossa on välilyöntejä, menisi syöttö jälleen sekaisin ? mikäli rivillä olisi ennalta tuntematon määrä kenttiä, ei tämä formaatti toimisi 1.6 Merkkijonon paloittelu Tutkitaanpa ongelmaa tarkemmin. Tiedostosta on siis luettu rivi, joka on muotoa +-------------------------------------+ ¦ ¦V¦o¦l¦v¦o¦ ¦|¦ ¦ ¦1¦2¦3¦0¦0¦ ¦|¦ ¦1¦ +-------------------------------------+ Jos saisimme erotettua tästä 3 merkkijonoa: pala1 pala2 pala3 +-------------- ----------------- ----- ¦ ¦V¦o¦l¦v¦o¦ ¦ ¦ ¦ ¦1¦2¦3¦0¦0¦ ¦ ¦ ¦1¦ +-------------- ----------------- ----- voisimme kustakin palasesta erikseen ottaa haluamme tiedot. Esimerkiksi 1. palasesta saadaan tuotteen nimi, kun siitä poistetaan turhat välilyönnit. Hinta saataisiin 2. palasesta kutsulla sscanf(pala2.c_str(),"%lf",&hinta); 1.6.1 luvuksi Merkkijono pitää varsin usein muuttaa reaaliluvuksi tai kokonaisluvuksi. Siksi kirjoitammekin tiedostoon mjonotpp.h kaksi funktiota luvuksi: inline bool luvuksi(vstring &jono, double &d, double def=0.0) { d = def; return std::sscanf(jono.c_str(),"%lf",&d) == 1; } inline bool luvuksi(vstring &jono, int &i, int def=0) { i = def; return std::sscanf(jono.c_str(),"%d",&i) == 1; } Funktion avulla voimme kirjoittaa muunnoksen lyhyemmin ja tuvallisemmin, sillä C++:n funktion kuormitus pitää huolen siitä että luvun tyypin mukaan valitaan oikea funktio käytettäväksi: luvuksi(pala2,hinta); 1.6.2 erota Tehdään yleiskäyttöinen funktio erota, jonka tehtävä on ottaa merkkijonon alkuosa valittuun merkkiin saakka, poistaa valittu merkki ja palauttaa sitten funktion tuloksena tämä alkuosa. Itse merkkijonoon jää jäljelle ensimmäisen merkin jälkeinen osa. Funktio on kirjoitettu tiedostoon mjonotpp.h: inline string erota(string &jono, char merkki=' ', bool etsi_takaperin=false) { size_t p; if ( !etsi_takaperin ) p = jono.find(merkki); else p = jono.rfind(merkki); string alku = jono.substr(0,p); if ( p == string::npos ) jono = ""; else jono.erase(0,p+1); return alku; } 1.6.3 Esimerkki erota-funktion käytöstä Kirjoitetaan lyhyt esimerkki, jolla demonstroidaan funktion käyttöä: tiedosto\erotaesim.cpp - esimerkki erota-funktion käytöstä // Vesa Lappalainen 29.12.2001 #include #include #include using namespace std; #include "mjonotpp.h" void tulosta(int n,const string &pala, const string &jono) { int valeja = 10-pala.length(); cout << n << ": pala = '" << pala <<"'" << setw(valeja) << ' ' << "jono = '" << jono << "'\n"; } int main(void) { string jono = " Volvo | 12300 | 1"; string pala; tulosta(0,pala,jono); pala = erota(jono,'|'); tulosta(1,pala,jono); pala = erota(jono,'|'); tulosta(2,pala,jono); pala = erota(jono,'|'); tulosta(3,pala,jono); pala = erota(jono,'|'); tulosta(4,pala,jono); return 0; } Ohjelma tulostaa: 0: pala = '' jono = ' Volvo | 12300 | 1' 1: pala = ' Volvo ' jono = ' 12300 | 1' 2: pala = ' 12300 ' jono = ' 1' 3: pala = ' 1' jono = '' 4: pala = '' jono = '' 1.6.4 Erota funktion toiminta vaihe vaiheelta Ennen ensimmäistä kutsua tilanne on seuraava: pala jono ++ +-------------------------------------+ ¦¦ ¦ ¦V¦o¦l¦v¦o¦ ¦|¦ ¦ ¦1¦2¦3¦0¦0¦ ¦|¦ ¦1¦ ++ +-------------------------------------+ 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 Ensimmäisessä kutsussa erota-funktio löytää etsittävän | -merkin paikasta 7. Merkit 0-6 kopioidaan funktion paluuarvoksi ja sitten jonosta tuhotaan merkit 0-7. Funktion paluuarvo sijoitetaan muuttujaan pala: pala jono +-------------+ +---------------------+ ¦ ¦V¦o¦l¦v¦o¦ ¦ ¦ ¦ ¦1¦2¦3¦0¦0¦ ¦|¦ ¦1¦ +-------------+ +---------------------+ 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 Seuraavalla kutsulla (kerta 2) |-merkki löytyy jonosta paikasta 8. Nyt merkit jonon merkit 0-7 kopioidaan funktion paluuarvoon ja merkki 8 tuhotaan. Kutsun jälkeen tilanne on: pala jono +---------------+ +---+ ¦ ¦ ¦1¦2¦3¦0¦0¦ ¦ ¦ ¦1¦ +---------------+ +---+ 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 Kolmannessa kutsussa merkkiä | ei enää löydy jonosta. Tämä ilmenee siitä, että find- metodi palauttaa arvon string::npos (no position), eli ei paikkaa. Näin koko jono kopioidaan funktion paluuarvoksi ja kutsun jälkeen tilanne on: pala jono +---+ ++ ¦ ¦1¦ ¦¦ +---+ ++ 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 Vastaava toistuu neljännessä kutsussa, eli koko jono sitten kopioidaan paluuarvoksi ja tilanne on neljännen kutsun jälkeen: pala jono ++ ++ ¦¦ ¦¦ ++ ++ 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 Tämän jälkeen tilanne pysyy samana vaikka erota-funktiota kutsuttaisiin kuinka monta kertaa tahansa. Tästä saadaan se etu, että erota-funktiota voidaan turvallisesti kutsua kuinka monta kertaa tahansa, vaikkei jonosta enää palasia saataisikaan. Jos kutsua tehdään silmukassa, voidaan silmukan lopetusehdoksi kirjoittaa while ( jono != "" ) { pala = erota(jono,'|'); cout << pala << "\n"; } 1.7 Lukeminen ja paloittelu Nyt voimme toteuttaa "tuotetiedoston" lukevan ohjelman C++:n tietovirroilla ja funktioiden erota ja luvuksi avulla: tiedosto\luetuote.cpp - esimerkki tiedoston lukemisesta // Projektiin +ALI\mjonot.c #include #include #include #include using namespace std; #include "mjonotpp.h" int tulosta_tuotteet(void) { string rivi,pala; string nimike; double hinta; int kpl; ifstream fi("tuotteet.dat"); if ( !fi ) return 1; cout << "\n\n\n"; cout << "-------------------------------------------\n"; while ( getline(fi,rivi) ) { nimike = erota(rivi,'|'); poista_tyhjat(nimike); pala = erota(rivi,'|'); if ( !luvuksi(pala,hinta) ) continue; pala = erota(rivi,'|'); if ( !luvuksi(pala,kpl) ) continue; printf("%-20s %7.0lf %4d\n",nimike.c_str(),hinta,kpl); } cout << "-------------------------------------------\n"; cout << "\n\n\n"; return 0; } int main(void) { if ( tulosta_tuotteet() ) { cout << "Tuotteita ei saada luetuksi!\n"; return 1; } return 0; } Ohjelma tulostaa: ------------------------------------------- Volvo 12300 1 Audi 55700 2 Saab 1500 4 Volvo 123400 1 ------------------------------------------- 1.7.1 Merkkijonosta tietovirta C:ssä merkkijonoa yritettiin purkaa sscanf–funktiolla, joka onkin erittäin helppokäyttöinen niin kauan kuin merkkijono sisältää vain lukuja, jotka voidaan kaikki erottaa kerralla. Jos kuitenkin erotettavien osien joukossa on merkkijonoja, menee ongelma tunnetusti hankalammaksi (tosin voidaan hoitaa erikoistapauksissa). Jos kuitenkin merkkijonosta pitää saada 0-n erillistä osaa (merkkijonoa tai lukua), ei sscanf:ää voida käyttää! C++:n stringstream on taas siitä mukava, että se tekee merkkijonoon samanlaisen "lukuosoittimien" kuin muutkin tietovirrat tiedostoon. Näin voidaan palasia erotella yksi kerrallaan ja "lukemista" voidaan jatkaa siitä, mihin edellinen lukeminen jäi. Edellisessä esimerkissä olisi voitu erota-funktion tilalla käyttää merkkijonovirtojakin: #include ... while ( getline(fi,rivi) ) { istringstream is(rivi); getline(is,nimike,'|'); poista_tyhjat(nimike); getline(is,pala,'|'); if ( !luvuksi(pala,hinta) ) continue; getline(is,pala,'|'); if ( !luvuksi(pala,kpl) ) continue; printf("%-20s %7.0lf %4d\n",nimike.c_str(),hinta,kpl); } Merkkijonotietovirrasta voitaisiin ottaa suoraan numeerinenkin arvo: is >> hinta; Esimerkissä kuitenkin tulisi ongelmaksi tällöin tolppien poisto, jonka getline ja erota osaavat hoitaa. Merkkijonovirtojen suurin ongelma on siinä, että ne tulivat vasta uusimman standardin mukana ja voi vielä löytyä kääntäjiä, joissa merkkijonovirtoja ei ole toteutettu Voidaan tehdä myös merkkijonotietovirta tulostamista varten: ostringstream os; os << "Volvo " << "|"; os << 12300.00 << "|"; os << 1; string s = os.str(); // s = "Volvo |12300|1" Tällä tavalla voidaan numeerisia arvoja muuttaa varsin mukavasti merkkijonoiksi. 1.7.2 Olio joka lukee itsensä Muutetaan vielä tuotteiden lukua oliomaisemmaksi, eli annetaan tuotteelle kuuluvat tehtävät kokonaan tuote–luokan vastuulle, samalla lisätään tuotteet–luokka. tiedosto\luerek.cpp - esimerkki oliosta joka käsittelee tiedostoaa // luerek.cpp - esimerkki oliosta joka käsittelee tiedostoa // Vesa Lappalainen -96,-01 // Projektiin +ALI\mjonot.c #include #include #include #include #include "mjonotpp.h" #include "dosout.h" using namespace std; //--------------------------------------------------------------------------- class cTuote { string nimike; double hinta; int kpl; void alusta() { nimike=""; hinta=0.0; kpl=0; } public: cTuote() { alusta(); } int setAsString(string &st) { string pala; alusta(); nimike = erota(st,'|'); poista_tyhjat(nimike); if ( nimike == "" ) return 1; pala = erota(st,'|'); if ( !luvuksi(pala,hinta) ) return 1; pala = erota(st,'|'); if ( !luvuksi(pala,kpl) ) return 1; return 0; } ostream &tulosta(ostream &os) const { ios::fmtflags oldf = os.setf(ios::left); os << setw(20) << nimike << " " << setiosflags(ios::right) << setw(7) << hinta << " " << setw(4) << kpl; os.flags(oldf); return os; } }; //---------------------------------------------------------------------------- class cTuotteet { string nimi; public: cTuotteet(const char *n) { nimi = n; } int tulosta(ostream &os) const; }; int cTuotteet::tulosta(ostream &os) const { cTuote tuote; string rivi; ifstream f(nimi.c_str()); if ( !f ) return 1; cout << "\n\n\n"; cout << "-------------------------------------------" << endl; while ( getline(f,rivi) ) { if ( tuote.setAsString(rivi) ) continue; tuote.tulosta(os); os << endl; } cout << "-------------------------------------------\n"; cout << "\n\n\n" << endl;; return 0; } //---------------------------------------------------------------------------- int main(void) { cTuotteet tuotteet("tuotteet.dat"); if ( tuotteet.tulosta(cout) ) { cout << "Tuotteita ei saada luetuksi!" << endl; return 1; } return 0; } 1.8 Esimerkki tiedoston lukemisesta Seuraavaksi kirjoitamme ohjelman, jossa tulee esiin varsin yleinen ongelma: tietueen etsiminen joukosta. Kirjoitamme edellisiä esimerkkejä vastaavan ohjelman, jossa tavallisen tulostuksen sijasta tulostetaan kunkin tuoteluokan yhteistilanne. tiedosto\luetrek.cpp - esimerkki tiedoston lukemisesta // luetrek.cpp c /* Ohjelma lukee tiedostoa tuotteet.dat, joka on muotoa: Volvo | 12300 | 1 Audi | 55700 | 2 Saab | 1500 | 4 Volvo | 123400 | 1 Ohjelma tulostaa kuhunkin tuoteluokkaan kuuluvien tuotteiden yhteishinnat ja kappalemäärät sekä koko varaston yhteishinnan ja kappalemäärän. Eli em. tiedostosta tulostetaan: ------------------------------------------- Volvo 135700 2 Audi 111400 2 Saab 6000 4 ------------------------------------------- Yhteensä 253100 8 ------------------------------------------- Vesa Lappalainen 15.3.1996 Muutettu 30.12.2001/vl : enemmän C++-maiseksi Projektiin: luetrek.cpp,ALI\mjonot.c */ #include #include #include #include #include "mjonotpp.h" #include "dosout.h" using namespace std; //--------------------------------------------------------------------------- class cTuote { string nimike; double hinta; int kpl; void alusta() { nimike=""; hinta=0.0; kpl=0; } public: cTuote() { alusta(); } cTuote(string &st) { setAsString(st); } cTuote(const string &s) { setAsString(s); } cTuote(const char *s) { setAsString(s); } int setAsString(string &st) { string pala; alusta(); nimike = erota(st,'|'); poista_tyhjat(nimike); if ( nimike == "" ) return 1; pala = erota(st,'|'); if ( !luvuksi(pala,hinta) ) return 1; pala = erota(st,'|'); if ( !luvuksi(pala,kpl) ) return 1; return 0; } int setAsString(const string &s) { string st(s); return setAsString(st); } int setAsString(const char *s) { string st(s); return setAsString(st); } ostream &tulosta(ostream &os) const { ios::fmtflags oldf = os.setf(ios::left); os << setw(20) << nimike << " " << setiosflags(ios::right) << setw(7) << hinta << " " << setw(4) << kpl; os.flags(oldf); return os; } void ynnaa(const cTuote &tuote) { hinta += tuote.hinta * tuote.kpl; kpl += tuote.kpl; } cTuote &operator+=(const cTuote &tuote) { ynnaa(tuote); return *this; } const string &Nimike() const { return nimike; } }; ostream &operator<<(ostream &os,const cTuote &tuote) { return tuote.tulosta(os); } //--------------------------------------------------------------------------- const int MAX_TUOTTEITA = 10; class cTuotteet { string nimi; int tuotteita; cTuote tuotteet[MAX_TUOTTEITA]; cTuote yhteensa; public: cTuotteet(const char *n) : nimi(n), yhteensa("Yhteensä") { tuotteita = 0; } ostream &tulosta(ostream &os) const; int etsi(const string &tnimi) const; int lisaa(const string &tnimi); int ynnaa(const cTuote &tuote); int lue(const string &s); int lue() { return lue(""); } }; int cTuotteet::etsi(const string &tnimi) const { for (int i=0; i= MAX_TUOTTEITA ) return -1; tuotteet[tuotteita].setAsString(tnimi); return tuotteita++; } int cTuotteet::ynnaa(const cTuote &tuote) { if ( tuote.Nimike() == "" ) return 1; int i = etsi(tuote.Nimike()); if ( i < 0 ) i = lisaa(tuote.Nimike()); if ( i < 0 ) return 1; tuotteet[i] += tuote; yhteensa += tuote; return 0; } int cTuotteet::lue(const string &s) { string rivi; if ( s != "" ) nimi = s; ifstream f(nimi.c_str()); if ( !f ) return 1; while ( getline(f,rivi) ) { cTuote tuote(rivi); if ( ynnaa(tuote) ) cout << "Rivillä \"" << rivi << "\" jotain pielessä!" << endl; } return 0; } ostream &cTuotteet::tulosta(ostream &os) const { int i; os << "\n\n\n"; os << "-------------------------------------------\n"; for (i=0; i1:10000 10 cm[RET] Matka maastossa on 1.00 km. Mittakaava ja matka>1:200000 20[RET] Matka maastossa on 4.00 km. Mittakaava ja matka>loppu[RET] Kiitos! Muuta ohjelmaa siten, että yksiköiden muunnostaulukko luetaan ohjelman aluksi tiedostosta muunnos.dat. Muuta ohjelmaa vielä siten, että mikäli mittakaava jätetään antamatta, käytetään edellisellä kerralla annettua mittakaavaa ja ensimmäinen luku onkin matka. ... Mittakaava ja matka>1:10000 10 cm[RET] Matka maastossa on 1.00 km. Mittakaava ja matka>0.20 dm[RET] Matka maastossa on 0.20 km. Mittakaava ja matka>loppu[RET] Kiitos! 20 20