previous next Title Contents Index

17. 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);
	                      tai: ifstream fi; ... fi.open(nimi);
	Lukeminen       C:    fscanf(f,format,osoite,...);
	                      fgets(mjono,max_pit,f);
	                C++:  fi >> muuttuja;
	                      fi.getline(mjono,max_pit);
	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 >>... - fi.getline(...) - 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.

Jos lukija haluaa keskittyä pelkästään C++:aan, hän voi hypätä C- ongelmia käsittelevien kappaleiden ylitse. Jos luetaan molemmat osat, on kuitenkin muistettava käyttää pareina C- funktioita ja C++- funktioita (ei siis esim. ifstream fi("oma.txt") ja fclose(fi) vaan fi.close() ).

17.1 Tiedostojen käsittely C- funktioilla

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 ennenkuin ohjelmassa edes on päätesyöttöä lukevaa osaa.

17.1.1 Avaaminen, fopen

Tiedosto (esimerkissä "readst.txt" - niminen, nimi voi tietysti olla muukin) avataan komennolla fopen:
	  FILE *f;
	...
	  f = fopen("readst.txt","rt");
	  if ( f == NULL ) ... toimenpiteet jos tiedosto ei aukea
	... 
Mikäli avaus epäonnistuu, palautetaan NULL. Ensimmäinen parametri on tiedoston nimi levyllä ja toinen sen tyyppi. Tässä tapauksessa tyyppinä on "rt" eli avaus lukemista (read) varten ja tekstitiedosto (text). 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.

17.1.2 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 1. rivi on siis kerhon koko nimi. AVATUSTA tiedostosta (jos avattu kahvaan nimellä f) tämä voitaisiin lukea seuraavasti:

	fgets(jono,80,f); 

Toisaalta meillä on valmis funktio f_lue_jono, joka suorittaa tarvittavia korjailuja, mikäli jono ei mahdu kokonaisuudessaan luettavaan muuttujaan:

	if ( f_lue_jono(f,kerho- >kerhon_nimi,
	                  sizeof(kerho- >nimi)) <OLETUS ) return TIEDOSTO_VAARIN; 

Seuraava rivi voitaisiin lukea
	fscanf(f,"%d",&kerho- >max_jasenia); 
Tässä ainoa riski on siinä, että riville jäisi vielä jotakin, jolloin joutuisimme poistamaan loppurivin. Siksi voitaisiin myös ensin lukea rivi merkkijonona ja tämän jälkeen sscanf- funktiolla ottaa jonosta kokonaisluku.
	if ( f_lue_jono(f,jono,sizeof(jono))<OLETUS ) return TIEDOSTO_VAARIN;
	if ( sscanf(jono,"%d",&kerho- >max_jasenia)<1 ) return TIEDOSTO_VAARIN; 

17.1.3 Tiedoston lopun testaaminen, feof

Tiedoston loppumista voidaan testata funktiolla feof. Usein tiedostoa luetaan silmukalla
	  while ( !feof(f) ) {
	    if ( f_lue_jono(f,jono,sizeof(jono)) ...
	    ...
	  }

17.1.4 Tiedoston sulkeminen, fclose

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.

Tiedosto suljetaan funktiokutsulla

	fclose(f); 

17.1.5 Tiedostoon tulostaminen, fprintf

Kirjoittamista varten avattuun tiedostoon voidaan tulostaa printf- funktiota vastaavalla funktiolla fpritnf. Esimerkiksi:
	f = fopen("valit.dat","wt");
	...
	fprintf(f,"%03d - %03d\n",alku,loppu ); /* tallettaa esim rivin:  001 -  007 */

17.1.6 Esimerkki tiedoston käsittelystä

Olkoon meillä tiedosto nimeltä LUVUT.DAT:
	13.4
	23.6
	kissa
	1.9
	<EOF>     <-   ei aina välttämättä mikään merkki
Tiedostossa olisi yksi luku rivillä. Tehtävä ohjelma joka tulostaa lukujen summan ja keskiarvon, mikäli tiedostossa on vääriä merkkejä, ne jätetään huomiotta:

tiedosto\tied_ka.c - esimerkki tiedoston lukemisesta

	#include <stdio.h>
	
	int main(void)
	{
	  FILE *f;
	  double luku,summa,ka;
	  int n;
	
	  f = fopen("luvut.dat","rt");
	
	  if (!f) {
	    printf("Tiedosto ei aukea!\n");
	    return 1;
	  }
	
	  summa = 0.0;  n = 0;  ka = 0.0;
	
	  while ( !feof(f) ) {
	    if ( fscanf(f,"%lf",&luku)<=0 ) {
	      fgetc(f);
	      continue;
	    }
	    summa += luku;
	    n++;
	  }
	
	  fclose(f);
	
	  if ( n > 0 ) ka = summa/n;
	
	  printf("Lukuja oli %d kappaletta.\n",n);
	  printf("Niiden summa oli %5.2lf\n",summa);
	  printf("ja keskiarvo oli %5.2lf.\n",ka);
	
	  return 0;
	}

Tehtävä 17.150 Tiedoston lukujen summa

1.
Muuta tiedoston tied_ka.c - ohjelmaa siten, että väärän merkin kohdalla tulostetaan väärä merkki ja lopetetaan koko ohjelma.
2.
Muuta edelleen ohjelmaa siten, että väärät merkit tulostetaan näyttöön:
	Tiedostossa oli seuraavat laittomat merkit:
	kissa
	Lukuja oli... 
3.
Ilmoitusta ei tietenkään tule, mikäli tiedostossa ei ole laittomia merkkejä.

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.

17.2 Tiedostojen käsittely C++:n tietovirroilla

Vastaavasti kuin C- funktiolla on tiedostojen käsittely C++:ssa vain cin ja cout - tietovirtoja vastaavien tietovirtojen käsittelyä.

Kirjoitetaanpa vastaava tiedoston luvut lukeva ohjelma C++:n tietovirroilla:

tiedosto\tied_ka.cpp - Lukujen lukeminen tiedostosta

	// Ohjelma lukee tiedostosta LUVUT.DAT lukuja ja tulostaa niiden
	// summan ja keskiarvon. 
	#include <iostream.h>
	#include <fstream.h>
	
	int main(void)
	{
	  double luku,summa,ka;
	  int n;
	
	  ifstream fi("luvut.dat");
	  if ( !fi ) {
	    cout << "Tiedosto ei aukea!" << endl;
	    return 1;
	  }
	
	  summa = 0.0;  n = 0;  ka = 0.0;
	
	  while ( !fi.eof() ) {
	    fi >> luku;
	    if ( fi.fail() ) {
	      fi.clear();
	      char ch;  fi.get(ch);
	      cout << ch;
	      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;
	}

17.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
Tiedosto voidaan myös jättää avaamatta esittelyn yhteydessä ja avata sitten myöhemmin open- metodilla:
	  ifstream f; 
	...
	  f.open("luvut.dat");
Tiedoston aukeamisen tila voidaan testata tietovirtaolion arvosta esimerkiksi
	if ( !f ) { ...

17.2.2 Tiedostosta lukeminen >> ja tiedostoon kirjoittaminen <<

Tiedostosta lukeminen on jälleen analogista päätesyötön kanssa:
	fi >> luku;
Vastaavasti kirjoittamista varten avattuun tiedostoon kirjoitettaisiin
	ofstream fo("tulos.dat");
	...
	fo << luku;
Myös kaikki muut cin ja cout - olioista tutut metodit ovat käytössä, esim:
	fi.getline(s,80);

17.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<EOF>
saataisiin kaksi reaalilukua silmukalla:
	while ( !fi.eof() ) { 	
	  fi >> luku;
	  summa += luku;
	  n++;
	}

Tehtävä 17.152 Ylimääräinen lukeminen

Miten voitaisiin ilman if- lauseita järjestää niin, ettei ylimääräinen lukeminen haittaisi summan laskua jos lukumäärää ei tarvittaisi.
Seuraava auttaisi jos oltaisiin varmoja ettei tiedostossa ole muuta kuin lukuja:
	while ( 1 ) { 	// Jos muuta kuin lukuja 
	  f >> luku;
	  if ( f.eof() ) break;
	  summa += luku;
	  n++;
	}
Jos silmukka saadaan pysäyttää ensimmäiseen virheeseen, olisi seuraava varmin tapa lukea:
	while ( f >> luku ) {
	  summa += luku;
	  n++;
	}

17.2.4 Tiedoston sulkeminen close

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();

17.3 sizeof

Useille C- kirjaston valmiille aliohjelmille sekä myös monille itse kirjoittamillemme aliohjelmille täytyy viedä parametrina käsiteltävän 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);
	... 

17.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 parametrina 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 toteutuksesta riippuen */
	... sizeof(int)   ...   /*   -  " -                        */
	... sizeof(char)  ...   /*  Aina 1                       */
	... sizeof(Pvm_tyyppi)  /* Esim. 2+20+2 == 24 tot. riip. */
	... sizeof(pvm.kk_nimi) /*  20                           */
	... 

17.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 */

17.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)!

17.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");

17.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.

17.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 parametrillisia 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.153 Makron yllätykset

Keksi muita esimerkkejä missä K_2 - makron käyttö tuottaisi yllätyksiä.

17.5 Tiedoston yhdellä rivillä monta kenttää, lukeminen C:llä

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 fscanf - lauseella.

17.5.1 fscanf

Olkoon meillä vaikkapa seuraavanlainen tiedosto:

tiedosto\tuotteet.dat - esimerkkitiedosto

	    Volvo |  12300 | 1
	    Audi  |  55700 | 2
	    Saab  |   1500 | 4
	    Volvo | 123400 | 1<EOF>
Tiedosto voitaisiin lukea ja vaikkapa tulostaa näytölle seuraavalla ohjelmalla:

tiedosto\tuotteet.c - tuotetiedoston lukeminen

	#include <stdio.h>
	#include <stdlib.h>
	
	typedef struct{
	  char   nimike[20];
	  double hinta;
	  int    kpl;
	} Tuote_tyyppi;
	
	
	int tulosta_tuotteet(void)
	{
	  FILE *f; Tuote_tyyppi tuote;
	
	  f = fopen("TUOTTEET.DAT","rt");
	  if (!f) return 1;
	
	  printf("\n\n\n");
	  printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n");
		
	  while ( !feof(f) ) {
	    fscanf(f,"%s |%lf |%d",&tuote.nimike,&tuote.hinta,&tuote.kpl);
	    printf("%- 20s %7.0lf %4d\n",tuote.nimike,tuote.hinta,tuote.kpl);
	  }
	
	  printf("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n");
	  printf("\n\n\n");
	
	  fclose(f);
	
	  return 0;
	}
	
	
	int main(void)
	{
	  if (tulosta_tuotteet()) {
	    printf("Tuotteita ei saada luetuksi!\n");
	    return 1;
	  }
	  return 0;
	}
Ohjelmassa on kuitenkin seuraavia huonoja puolia:
*
mikäli tiedoston loppu ei olekaan viimeisen rivin lopussa, tulostaa ohjelma viimeisen rivin 2 kertaa
*
mikäli jokin rivi on väärää muotoa, menee ohjelma varsin sekaisin

Tehtävä 17.154 Tiedoston lopun testaus

Miten edellisessä ohjelmassa estettäisiin viimeisen rivin tulostuminen kahdesti?

Tehtävä 17.155 Ohjelman "sekoaminen"

Mitä edellinen ohjelma tulostaisi seuraavasta tiedostosta:
	    Volvo |  12300 | 1
	    Audi     55700 | 2
	    Saab  |   1500 | 4
	    Volvo | 123400 | 1
	<EOF>

17.5.2 Rivi kerrallaan lukeminen

Ongelmaa voidaan osittain ratkaista lukemalla tiedostoa merkkijonoon aina rivi kerrallaan:
	  FILE *f; Tuote_tyyppi tuote; char rivi[80];
	...
	  while ( !feof(f) ) {
	    if ( f_lue_jono(f,N_S(rivi)) <= OLETUS ) continue;
	    sscanf(rivi,"%s |%lf |%d",&tuote.nimike,&tuote.hinta,&tuote.kpl);
	    printf("%- 20s %7.0lf %4d\n",tuote.nimike,tuote.hinta,tuote.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

17.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| | | | | | |
 +--------------------------------------0----------+ 
	                                        NUL- merkki
Jos saisimme erotettua tästä 3 merkkijonoa:
 pala1               pala2           pala3
 +--------------     -------------   -----  
 | |V|o|l|v|o| |     |1|2|3|0|0| |   |1| | 
 +------------0-     -----------0-   ---0-
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,"%lf",&tuote.hinta); 

17.6.1 Osoitintaulukko

Siis tarvitsemme aliohjelman, joka paloittelee sille annetun merkkijonon valittujen merkkien kohdalta. Miten tulos palautetaan. Yksi mahdollisuus olisi palauttaa osoitintaulukko

          0   1   2   3   4
        +-------------------+ 
palaset | o | o | o | o | o |
        +-+---+---+---------+
  +-------+   |   +-----------------+ 
  |           +---+                 |
  v               v                 v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| | | |1| | | | | | |
 +--------------0-----------------0-----0----------+ 
	                                        NUL- merkki
Tässä olisi muutamia ongelmia:
*
kuinka pitkä osoitintaulukko varattaisiin ja minne
*
alkuperäinen jono täytyy pilata

17.6.2 "Liikkuva" osoitin

Kun hieman tutkimme ongelmaa, toteamme, ettei alkuperäistä jonoa ehkä tarvitakaan, siis se voidaan pilata. Toisaalta emme tarvitse pätkiä yhtäaikaa, riittää kun saamme ensin ensimmäisen ja otamme siitä nimen, sitten toisen, josta otamme hinnan jne...

17.6.3 strtok

Erään mahdollisuuden tällaisen palanen kerrallaan ottamiseen tarjoaa string.h:n funktio strtok joka toimii seuraavasti:
Ennen kutsua:

  +---rivi
  v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| ||| | |1|2|3|0|0| ||| |1| | | | | | |
 +--------------------------------------0----------+ 
	
	1. Kutsu:  p = strtok(rivi,"|");
  +---rivi
  v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| ||| |1| | | | | | |
 +--------------0-----------------------0----------+ 
  ^
  +--- p
	
	2. Kutsu:  p = strtok(NULL,"|");
  +---rivi
  v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| | | |1| | | | | | |
 +--------------0-----------------0-----0----------+ 
                  ^
       p ---------+ 
	
	3. Kutsu:  p = strtok(NULL,"|");
  +---rivi
  v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| | | |1| | | | | | |
 +--------------0-----------------0-----0----------+ 
                                    ^
       p ---------------------------+ 
	   
	4. Kutsu:  p = strtok(NULL,"|");
  +---rivi
  v
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| | | |1| | | | | | |
 +--------------0-----------------0-----0----------+ 
              
       p ----> NULL
Näyttäisi siis siltä, että strtok sopii hyvin tarkoituksiimme:

tiedosto\tuot_sto.c - rivin pätkiminen strtok:n avulla

	int rivi_tuotteeksi(char *rivi, Tuote_tyyppi *tuote) 
	{
	  char *p; 
	  p = strtok(rivi,"|"); if (!p) return 1;
	  poista_tyhjat(p); kopioi_jono(N_S(tuote- >nimike),p);
	
	  p = strtok(NULL,"|"); if (!p) return 1;
	  if ( sscanf(p,"%lf",&tuote- >hinta) != 1 ) return 1;
	
	  p = strtok(NULL,"|"); if (!p) return 1;
	  if ( sscanf(p,"%d",&tuote- >kpl) != 1 ) return 1;
	
	  return 0;
	}
	
	int tulosta_tuotteet(void)
	{
	...
	  while ( !feof(f) ) {
	    if ( f_lue_jono(f,N_S(rivi)) <= OLETUS ) continue;
	    if ( rivi_tuotteeksi(rivi,&tuote) ) continue;
	    printf("%- 20s %7.0lf %4d\n",tuote.nimike,tuote.hinta,tuote.kpl);
	  }
	...
	}

17.6.4 palanen ja static

Vaikka edellä strtok näyttikin hyvältä, on siinä muutamia puutteita:
*
mikäli erotinmerkit ovat peräkkäin, esimerkiksi
	Audi||20
tulee 1. palaseksi "Audi" ja toiseksi "20" eikä kolmatta enää saada (tosin tämä saattaa olla toivottavakin ominaisuus katkottaessa välilyönnein erotettuja jonoja!)
*
erotinmerkkien loputtua funktio palauttaa NULL- osoittimen, käytännössä voisi olla parempi palauttaa osoitin tyhjään merkkijonoon, jolloin tulosta voitaisiin käyttää ilman ylimääräistä if- lausetta
Teemme strtok funktiota vastaavan funktion nimeltä palanen (mjonot.h), jossa meidän kannaltamme puutteelliset kohdat on korjattu. Aluksi tulee ongelma siitä, miten ensimmäisen kutsukerran jälkeen osataan jatkaa seuraavan erotinmerkin etsimistä. Aliohjelman täytyy "muistaa" mitä on tehty viimeksi. Normaalisti aliohjelmasta poistuttaessa aliohjelman sisäiset muuttujat häviävät. Mikäli muuttuja esitellään static etuliitteellä, säilyy muuttuja myös aliohjelmasta poistumisen jälkeenkin (eikä ole enää automaattinen muuttuja):

ali\mjonot.c - merkkijonon paloittelu

	/****************************************************************************/
	char                      /*                                                */
	*palanen(                 /* Osoitin merkkijonon palaseen.                  */
	  char *jono             ,/* s   Pätkittävä jono, turmeltuu!                */
	  char *erottimet        ,/* s   Merkit joiden kohdalta katkaistaan.        */
	  int  *jaljella          /* t   Paljonko jonoa on vielä jäljellä (- 1 loppu)*/
	) 
	/* 
	** Funktiolla pätkitään merkkijonoa osiin.  1. kutsukerralla välitetään
	** tutkittava jono ja tämän jälkeen seuraavilla NULL osoitin.
	** Funktio vaihtaa löytämänsä erotinmerkit null- merkeiksi!
	**
	** Muuttuu:   jono
	** Algoritmi:       
	** Esimerkki:       012345678901234
	**            jono="Aku|Ankka||12" erottimet="|"
	**            1. kutsu palanen(jono,"|",&j) - > "Aku"  , j=10
	**            2. kutsu palanen(NULL,"|",&j) - > "Ankka", j=4
	**            3. kutsu palanen(NULL,"|",&j) - > ""     , j=3
	**            4. kutsu palanen(NULL,"|",&j) - > "12"   , j=0
	**            5. kutsu palanen(NULL,"|",&j) - > ""     , j=- 1  
	- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	{
	  static char *st="";
	  static int p1=0,p2=0,pit=0;
	
	  if (jono) {      /* 1. kutsukerta, alustetaan apumuuttujat */
	    st  = jono;
	    pit = strlen(jono);
	    p1  = 0;
	  }
	  else
	    p1  = p2+1;    /* Muilla kerroilla jatketaan siitä mihin viim. jäätiin. */
	
	  if ( p1 > pit ) {
	    *jaljella = - 1;
	    return st+pit; /* Tyhjä merkkijono, kun osoitetaan jonon null- tavuun.   */
	  }
	
	  p2 = p1+strcspn(st+p1,erottimet);
	  st[p2] = 0;
	  *jaljella = pit- p2;
	
	  return st+p1;
	
	}

Funktio strcspn palauttaa ensimmäisen indeksin, josta löytyy erottimet merkkijonon jokin merkki. Mikäli merkkiä ei löydy, palautetaan jonon pituus.

Funktion toimintaidea on seuraava:


	2. Kutsu:  p = palanen(NULL,"|",&j);
	 Kutsuun tultaessa: (pit == 19, p1 == 0, p2 == 7)
  +---st          +---st+p2+1
  v               v
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| ||| |1| | | | | | |
 +--------------0-----------------------0----------+ 
                  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
	                    
	Etsitään |- merkkiä alkaen paikasta 8.  Löytyy jonoon st+p2+1
	nähden paikasta 8.  Siis alkuperäiseen jonoon nähden
	paikasta 16.  
	
	Muutetaan arvot: (pit == 19, p1 == 8, p2 = 8+8)
  +---st          +---st+p1       +----st+p2
  v               v               v
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
 +-------------------------------------------------+ 
 | |V|o|l|v|o| | | | |1|2|3|0|0| | | |1| | | | | | |
 +--------------0-----------------0-----0----------+ 
                  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6

	palautetaan st+p1 ja *jaljella = 19- 16 = 3                
Näin olemme saaneet funktion, jota voidaan huoletta kutsua myös merkkijonon loppumisen jälkeenkin:

tiedosto\tuot_pal.c - rivin pillkominen palanen- aliohjelman avulla

	int rivi_tuotteeksi(char *rivi, Tuote_tyyppi *tuote)
	{
	  char *p; int j;
	  p = palanen(rivi,"|",&j); 
	  poista_tyhjat(p); kopioi_jono(N_S(tuote- >nimike),p);
	
	  p = palanen(NULL,"|",&j); 
	  if ( sscanf(p,"%lf",&tuote- >hinta) != 1 ) return 1;
	
	  p = palanen(NULL,"|",&j);
	  if ( sscanf(p,"%d",&tuote- >kpl) != 1 ) return 1;
	
	  return 0;
	}

Tehtävä 17.156 Parempi palanen

Kirjoita palanen - aliohjelmasta versio, joka ei pilaa alkuperäistä merkkijonoa.

Tehtävä 17.157 Vielä parempi palanen

Voiko palanen - aliohjelma olla yhtäaikaa kahdessa eri käytössä, eli voiko sillä esimerkiksi paloitella rinnakkain kahta eri merkkijonoa?
Mikäli ei voi, niin esitä ratkaisu miten voitaisiin. Mikäli voi, niin perustele miksi.

17.7 Tiedoston yhdellä rivillä monta kenttää, lukeminen C++:lla

Toteutetaan tuot_pal.c:tä vastaava ohjelma C++:n tietovirroilla:

tiedosto\tuot_pal.cpp - esimerkki tiedoston lukemisesta

	// Projektiin +ALI\mjonot.c 
	#include <iostream.h>
	#include <fstream.h>
	#include <strstrea.h>
	#include <stdio.h>
	#include <string>
	using namespace std;
	#include "mjonotpp.h"
	
	
	struct tTuote {
	  string nimike;
	  double hinta;
	  int    kpl;
	};
	
	void alusta(tTuote &tuote)
	{
	  tuote.nimike = "";
	  tuote.hinta = 0.0;
	  tuote.kpl   = 0;
	}
	
	int lue(istream &is,tTuote &tuote) {
	  char jono[50];
	  alusta(tuote);
	  is.getline(N_S(jono),'|');   tuote.nimike = poista_tyhjat(jono);
	  is.getline(N_S(jono),'|');
	  if ( sscanf(jono,"%lf",&tuote.hinta) < 1 ) return 1;
	  is.getline(N_S(jono),'\n');
	  if ( sscanf(jono,"%d",&tuote.kpl) < 1 ) return 1;
	  return 0;
	}
	
	int rivi_tuotteeksi(char *rivi, tTuote &tuote)
	{
	  istrstream sstr(rivi);
	  return lue(sstr,tuote);
	}
	
	int tulosta_tuotteet(void)
	{
	  tTuote tuote;
	  char rivi[80];
	
	  ifstream fi("TUOTTEET.DAT");
	  if ( !fi ) return 1;
	
	  cout << "\n\n\n";
	  cout << "-------------------------------------------\n";
	
	  while ( 1 ) {
	    if ( !fi.getline(rivi,sizeof(rivi))) break;
	    if ( rivi_tuotteeksi(rivi,tuote) ) continue;
	    printf("%-20s %7.0lf %4d\n",tuote.nimike.c_str(),tuote.hinta,tuote.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;
	}
	

17.7.1 Merkkijonosta tietovirta

Ohjelmassa on pystytty välttämään palanen- tyylisen aliohjelman käyttö luomalla merkkijonosta tietovirta:
	int rivi_tuotteeksi(char *rivi, tTuote &tuote)
	{
	  istrstream sstr(rivi);
	  return lue(sstr,tuote);
	}
Koska tämä tietovirta käyttäytyy aivan samoin kuin päätesyöttökin, voidaan tehdä aliohjelma lue, joka lukee tuotteen tiedot ikäänkuin tiedostosta. Itse asiassa ohjelman lukusilmukassa voitaisiin aivan hyvin kutsua myös
	while ( 1 ) {
	  if ( lue(fi,tuote) ) break;
	  printf("%-20s %7.0lf %4d\n",tuote.nimike.c_str(),tuote.hinta,tuote.kpl);
	}
mutta tällöin lukeminen loppuisi ensimmäiseen virheeseen tai sopivalla tiedostolla rivijako menisi sekaisin. Tämän vuoksi luettaessa tiedostoja joissa rivi on "yksi tietue", kannattaa lukea ensin rivin sisältö merkkijonoon ja sitten muuttaa tämä merkkijono tietueeksi.

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 strstream 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.

17.7.2 Olio joka lukee itsensä

Muutetaan vielä edellistä oliomaisemmaksi, eli annetaan tuotteelle kuuluvat tehtävät kokonaan tuote- luokan vastuulle, samalla lisätään tuotteet- luokka.

tiedosto\tuot_pao.cpp - esimerkki oliosta joka käsittelee tiedostoaa

	// Projektiin +ALI\mjonot.c
	#include <iostream.h>
	#include <iomanip.h>
	#include <fstream.h>
	#include <strstrea.h>
	#include <stdio.h>
	#include <string>
	using namespace std;
	#include "mjonotpp.h"
	
	
	//----------------------------------------------------------------------------
	class cTuote {
	  string nimike;
	  double hinta;
	  int    kpl;
	  void alusta() { nimike = "";  hinta = 0.0;  kpl = 0; }
	public:
	  int alusta(const char *rivi) {  // Alustaa jonosta Volvo|4500|3
	    istrstream sstr((char *)rivi);
	    return lue(sstr);
	  }
	  cTuote() { alusta(); }
	  ostream &tulosta(ostream &os) const {
	    long oldf = os.setf(ios::left);
	    os << setw(20) << nimike << " " << setiosflags(ios::right)
	       << setw(7)  << hinta  << " "
	       << setw(4)  << kpl;
	    os.flags(oldf);
	    return os;
	  }
	  int lue(istream &is) {
	    char jono[50];
	    alusta();
	    is.getline(N_S(jono),'|');   nimike = poista_tyhjat(jono);
	    is.getline(N_S(jono),'|');
	    if ( sscanf(jono,"%lf",&hinta) < 1 ) return 1;
	    is.getline(N_S(jono),'\n');
	    if ( sscanf(jono,"%d",&kpl) < 1 ) return 1;
	    return 0;
	  }
	};
	
	
	ostream &operator<<(ostream &os,const cTuote &tuote) 
	  { return tuote.tulosta(os); }
	istream &operator>>(istream &is,cTuote &tuote) { tuote.lue(is); return is; }
	
	//----------------------------------------------------------------------------
	class cTuotteet {
	  string nimi;
	public:
	  cTuotteet(const char *n) { nimi = n; }
	  int tulosta(ostream &os) const;
	};
	
	
	int cTuotteet::tulosta(ostream &os) const
	{
	  cTuote tuote;
	  char rivi[80];
	
	  ifstream f(nimi.c_str());  if ( !f ) return 1;
	
	  cout << "\n\n\n";
	  cout << "-------------------------------------------" << endl;
	
	  while ( !f.eof() ) {
	    if ( !f.getline(N_S(rivi)) ) continue;
	    if ( tuote.alusta(rivi) ) continue;
	    os << tuote << 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;
	}

17.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\tuoterek.cpp - esimerkki tiedoston lukemisesta C-funktiolla

	// tuoterek.cpp 
	/*
	  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
	  Projektiin: tuoterek.cpp,ALI\mjonot.c
	*/
	
	#include <iostream.h>
	#include <iomanip.h>
	#include <strstream.h>
	#include <fstream.h>
	#include <stdio.h>
	#include <string.h>
	#include <string>
	using namespace std;
	#include <mjonot.h>
	#include <mjonotpp.h>
	
	//---------------------------------------------------------------------------
	class cTuote {
	  string nimike;
	  double hinta;
	  int    kpl;
	public:
	  int alusta(const char *n,double h,int k=0) {
	    nimike = n; hinta = h; kpl = k; return 0;
	  }
	  int alusta(const char *rivi) {  // Alustaa jonosta Volvo|4500|3
	    istrstream sstr((char *)rivi);
	    return lue(sstr);
	  }
	  int alusta()                            { return alusta("",0,0);           }
	  cTuote()                                { alusta();                        }
	  cTuote(const char *rivi)                { alusta(rivi);                    }
	  int lue(istream &is) {...}
	  ostream &tulosta(ostream &os) const { ... }
	
	  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); }
	istream &operator>>(istream &is,cTuote &tuote) { tuote.lue(is); return is; }
	
	
	//---------------------------------------------------------------------------
	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 char *s=NULL);
	};
	
	int cTuotteet::etsi(const string &tnimi) const
	{
	  int i;
	  for (i=0; i<tuotteita; i++)
	    if ( tuotteet[i].Nimike() == tnimi ) return i;
	  return -1;
	}
	
	int cTuotteet::lisaa(const string &tnimi)
	{
	  if ( tuotteita >= MAX_TUOTTEITA ) return -1;
	  tuotteet[tuotteita].alusta(tnimi.c_str(),0.0,0);
	  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 char *s)
	{
	  char rivi[80];
	
	  if ( s ) nimi = s;
	  ifstream f(nimi.c_str());  if ( !f ) return 1;
	
	  while ( f.getline(N_S(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; i<tuotteita; i++)
	    os << tuotteet[i] << "\n";
	  os << "-------------------------------------------\n";
	  os << yhteensa << "\n";
	  os << "-------------------------------------------\n";
	  os << "\n\n" << endl;
	  return os;
	}
	
	
	int main(void)
	{
	  cTuotteet varasto("tuotteet.dat");
	
	  if ( varasto.lue() ) {
	    cout << "Tuotteita ei saada luetuksi!" << endl;;
	    return 1;
	  }
	
	  varasto.tulosta(cout);
	
	  return 0;
	}

Tehtävä 17.158 Tietorakenne

Piirrä kuva cTuotteet - luokan tietorakenteesta.

Tehtävä 17.159 Perintä

Miten voisit perinnän avulla saada tiedoston tuote_pao.cpp luokasta cTuote tiedoston tuoterek.cpp vastaavan luokan (tietysti eri nimelle, esim. cRekTuote). Mitä muutoksia olisi hyvä tehdä alkuperäisessä cTuote- luokassa.

Tehtävä 17.160 Tunnistenumero

Lisää tuoterek.cpp- ohjelmaan tunnistenumeron käsittely mahdollista tulevaa relaatiokäyttöä varten.

Tehtävä 17.161 Mittakaava

Kirjoita mittakaavaohjelma, jossa on vakiotaulukko
y
ks

mm

mm
cm
dm
m
inch

1.0
10.0
100.0
1000.0
25.4

ja jonka toiminta näyttäisi seuraavalta:
	...
	Mittakaava ja matka>1: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! 


previous next Title Contents Index