previous next Title Contents Index

19. Kerho- ohjelman talletukset


Mitä tässä luvussa käsitellään?

* kerhon talletus

* kerhon lukeminen

* template- funktiot

* muunnos: numeerinen tieto <-> merkkitieto Olemme nyt testanneet kerho- ohjelmamme tietorakenteen käyttämällä syöttönä "arvottuja" henkilöitä. Tämä on nopeampaa kuin syöttää käsin henkilön vaatimat tiedot usealle henkilölle ja todeta sitten, ettei tietorakenne toimikaan. Uudelleen yrityksessä sitten helposti testi jää vajavaiseksi jos syöttöjä pitää tehdä paljon.

Vastaavasta syystä kannattanee vielä hetken malttaa mieli ja tehdä jopa tiedostojen käsittely ennen päätesyöttöä. Nimittäin olisi aika masentavaa syöttää 10 ihmisen tiedot ja huomata sitten, ettei talletus toimikaan.

Jatkossa aliohjelmista esitetään vain osia ja nekin usein ilman kommentteja, koska ohjelmasta on täydellinen listaus monisteen liitteenä.

19.1 Talletus

Siispä ryhdymme ensin miettimään miten ohjelman talletuksen tulisi toimia:
	- jos ei muutoksia, turha tallettaa
	-  tee (mahdollisesta) vanhasta tiedostosta.BAK 
	-  avaa tiedosto kirjoittamista varten
	-  talleta kokonimi ja maksimikoko (otsikkotiedot)
	-  talleta kukin tietue
Seuraavaksi pitää miettiä mikä toimenpide suunnitelluista kuuluu millekin luokalle. Luokkinahan olivat:
	cNaytto
	cKerho
	cJasenet
	cJasen

19.1.1 Näytön osuus talletuksesta

Koska näytön tehtävä on huolehtia kaikesta käyttöliittymään liittyvästä, voisi .bak- tiedoston tekeminen kuulua osittain näytölle, varsinainen talletus menköön kerhon tehtäviin. Toisaalta jos kerhoon lisätään harrastukset, niin myös harrastusten .bak- tiedoston tekeminen jäisi näytön huoleksi. Siispä sitten huolehtikoon kerho myös .bak- tiedostojen tekemisestä. Laiskana kerho tietysti delegoi tämänkin homman eteenpäin. Näytön tehtäviin jää siis vain delegoida tehtäviä kerholle:

talletus.2\naytto.cpp - kerhon talletus

	int cNaytto::talleta()
	{
	  logo();
	  if ( !kerho->Muutettu() ) return 0;
	  int vanhat_pilalla = kerho->TeeBak(VANHATARK);
	
	  if ( ilmoitus(kerho->talleta()) ) return 1;
	
	  cout << endl;
	  cout << "Tiedot talletettu tiedostoon "
	       << kerho->Jasenet().Tiedoston_nimi() << endl;
	  if ( !vanhat_pilalla )
	    cout << "Vanhat tiedot tiedostossa    "
	         << kerho->Jasenet().Bak_nimi() << endl;
	
	  return 0;
	}

19.1.2 Kerhon osuus talletuksesta

Kerhon tehtävä on lähinnä vain delegoida tehtäviä "alamaisilleen", esim. jäsenistölle ja harrastuksille:

talletus.2\kerho.h - kerho talletukset

	class cKerho {
	...
	  const char *talleta(const string &tied="");
	...
	  int TeeBak(const string &bak_tark)     { return jasenet.TeeBak(bak_tark);
	};

talletus.2\kerho.cpp - talleta

	const char *cKerho::talleta(const string &tied)
	{
	  return jasenet.talleta(tied);
	}

19.1.3 Jäsenistön osuus talletuksesta

Jäsenistö ei enää paljoa pysty tehtäviään välttelemään:

talletus.2\jasenet.cpp - talletus

	//-------------------------------------------------------------------------
	int cJasenet::TeeBak(const string &bak_tark)
	{
	  bak_nimi = Tiedoston_nimi();
	  vaihda_tarkennin(bak_nimi,bak_tark);
	
	  remove(bak_nimi); /* Vanha .BAK täytyy poistaa jotta rename toimii */
	  return rename(Tiedoston_nimi(),bak_nimi);
	}
	...
	//-------------------------------------------------------------------------
	const char *cJasenet::talleta(const string &tied)
	{
	  if ( !muutettu ) return NULL;
	  string tiedosto(tied); if ( tied == "" ) tiedosto = tiedoston_nimi;
	  ofstream f(tiedosto.c_str());
	
	  if ( !f ) return TIED_EI_AUKEA;
	
	  f << koko_nimi << endl;
	  f << max_lkm << endl;
	
	  if ( !f ) return OTS_EI_KIRJ;
	
	  for (int i=0; i<lkm; i++) {
	    f << *alkiot[i] << endl;
	    if ( !f ) return ALKIO_EI_KIRJ;
	  }
	
	  muutettu = 0;
	  tiedoston_nimi = tiedosto;
	  return NULL;
	}
Myös kommentit kannattaisi tavalla tai toisella siirtää vanhasta alkuperäisestä tiedostosta. Tätä varten voisimme kirjoittaa aliohjelman
	kopioi_kommentit(f,bak_nimi) 
Myöhemmin ehkä käytännössä huomataan, että olisi siistimpää tulostaa kenttiä hieman täsmällisemmin allekkain - kuten alkuperäisessä suunnitelmassa oli. Tämä voitaisiin toteuttaa mallirivin avulla, missä mallirivi on alkuperäisen tiedoston kommenteista luettu rivi. Tästä rivistä tutkitaan erotinmerkkien paikkoja ja pyritään saamaan erotinmerkit vastaaviin paikkoihin myös tulosjonossa.

19.1.4 Jäsenen tehtävät talletuksessa

Jäsenellekin on jäänyt tehtäviä. Jäsenen tulostus tietovirtaan voitaisiin tehdä esimerkiksi:
	ostream &operator<<(ostream &os,const cJasen &jasen)
	{
	  char erotin = jasen.erotin;
	  os << jasen.tunnus_nro     << erotin
	     << jasen.nimi           << erotin
	     << jasen.hetu           << erotin
	     << jasen.katuosoite     << erotin
	...
	     << jasen.jmaksu         << erotin
	     << jasen.maksu          << erotin
	     << jasen.lisatietoja    << erotin;
	}
Nyt maksut tulostuisivat desimaaleiltaan varsin mielivaltaisesti. Jos tämä tyydyttää, niin em. tapa on aivan hyvä. Toisaalta voitaisiin tehdä apufunktio jonoksi, jonka tehtävänä on muotoilla reaaliluku merkkijonoksi siististi, esimerkiksi kahdella desimaalilla:
	string jonoksi(double d)
	{
	  char st[40];
	  double_jonoksi(N_S(st),d,"%4.2lf");
	  return string(st);
	}
Nyt reaalilukukenttien tulostus voitaisiin tehdä
	...
	     << jonoksi(jasen.jmaksu)         << erotin
	     << jonoksi(jasen.maksu)          << erotin
	...
Symmetriasyistä kaikille muillekin tietotyypeille voitaisiin tehdä vastaava funktio C++:an kuormitusmahdollisuuden ansiosta. Näin jäsenen tietovirtaan tulostaminen voisi olla myös:

talletus.2\jasen.cpp - talletus

	ostream &operator<<(ostream &os,const cJasen &jasen)
	{
	  char erotin = jasen.erotin;
	  os << jonoksi(jasen.tunnus_nro)     << erotin
	     << jonoksi(jasen.nimi)           << erotin
	     << jonoksi(jasen.hetu)           << erotin
	     << jonoksi(jasen.katuosoite)     << erotin
	     << jonoksi(jasen.postinumero)    << erotin
	     << jonoksi(jasen.postiosoite)    << erotin
	     << jonoksi(jasen.kotipuhelin)    << erotin
	     << jonoksi(jasen.tyopuhelin)     << erotin
	     << jonoksi(jasen.autopuhelin)    << erotin
	     << jonoksi(jasen.liittymisvuosi) << erotin
	     << jonoksi(jasen.jmaksu)         << erotin
	     << jonoksi(jasen.maksu)          << erotin
	     << jonoksi(jasen.lisatietoja)    << erotin;
	  return os;
	}
Tästä on vielä se lisäetu, että voidaan esimerkiksi tehdä jonoksi- funktiosta sellainen, että tietty kokonaislukuarvo tai reaalilukuarvo (esim. -1) tallettuu tyhjänä merkkijonona, tarkoittaen ettei arvoa ole syötetty. 0:han ei yleensä voi tällainen arvo olla, koska 0 on usein aivan järkevä syöttö.

Tehtävä 19.165 Ystävyys pois

Toteuta jäsenen operator<< siten, ettei ystäväfunktiota tarvita.

19.2 Lukeminen

Lähdemme hahmottelemaan lue_tiedosto - metodia:
	-  selvitä kerhon nimi
	-  avaa vastaava tiedosto
	-  mikäli ei aukea, niin kysy tuleeko uusi ja aloita alusta
	-  mikäli aukesi, niin lue alkutiedot, eli koko nimi ja
	  kerhon maksimijäsenmäärä
	-  luo jäsenistö
	-  lue jäsenet tiedostosta

19.2.1 Näytön tehtävät lukemisessa

Tässä pitää taas valita mikä tehtävä kuuluu millekin luokalle. Selvästi nimen kysyminen ja muiden tietojen uteleminen on käyttöliittymäluokan cNaytto tehtäviä:

talletus.2\naytto.cpp - kerhon lukeminen

	int cNaytto::lue_tiedosto()
	/*
	** Luetaan kerho levyltä.
	** Ensin kysytään kerhon nimi.  Jos kerhoa ei ole, utellaan
	** lisätietoja ja luodaan se.
	----------------------------------------------------------------------------*/
	{
	  string tied,nimi; int maksimi;
	
	  do { // Kysellään kunnes tiedosto aukeaa tai luodaan uusi
	    cout << endl;
	    cout << "Anna kerhon nimi>";  lue_rivi(cin,tied);
	    if ( tied == "" ) return ilmoitus("Tiedoston nimeä ei annettu");
	
	    laita_tarkennin(tied,TARKENNIN);
	    if ( onko_tiedostoa(tied) ) return ilmoitus(kerho->lue_tiedostosta(tied));
	
	    cout << "Tiedostoa " << tied << " ei ole!" << endl;
	  } while ( kylla_kysymys("Luodaanko uusi tiedosto?") == 0 );
	
	  cout << endl;
	  cout << "Anna kerhon koko nimi    >";  lue_rivi(cin,nimi);
	  cout << "Anna kerhon maksimi koko >";  lue_rivi(cin,maksimi);
	
	  return ilmoitus(kerho->luo(tied,nimi,maksimi));
	
	}

19.2.2 Kerhon tehtävät lukemisessa

Kerho vaan taas jakaa kutsuja eteenpäin jäsenistölle. Jos lisätään harrastukset, kerho jakaa kutsuja myös harrastuksille.

19.2.3 Jäsenistön tehtävät lukemisessa

Lopulta jäsenistö on taas se joka joutuu hommiin:

talletus.2\jasenet.cpp - lukeminen

	const char *cJasenet::luo(const string &tied,const string &nimi,int max_koko)
	{
	  IF_ERR_RETURN(luo_taulukko(max_koko));
	
	  tiedoston_nimi = tied;
	  koko_nimi      = nimi;
	
	  muutettu = 1;
	  return NULL;
	}
	...
	const char *cJasenet::lue_tiedostosta(const string &tied)
	{
	  ifstream f(tied.c_str());
	  if ( !f ) return TIED_EI_AUKEA;
	
	  string nimi;  lue_rivi(f,nimi);       if ( !f ) return EI_NIMEA;
	  int max_koko; lue_rivi(f,max_koko);   if ( !f ) return EI_MAXKOKOA;
	
	  IF_ERR_RETURN(luo_taulukko(max_koko));
	
	  tiedoston_nimi = tied;
	  koko_nimi      = nimi;
	
	  char rivi[400];
	  cJasen uusi;
	  while ( f ) {
	    lue_rivi(f,rivi,sizeof(rivi)); 
	    if ( rivi[0] == 0 || rivi[0] == ';' ) continue;
	    uusi.alusta(rivi);
	//    f >> uusi;  // vaatisi kunkin rivin olemisen täydellisenä, muuten OK!
	    IF_ERR_RETURN(lisaa(uusi));
	  }
	
	  muutettu = 0;
	  return NULL;
	}

19.2.4 Jäsenen tehtävät lukemisessa

Jälleen tehtäviä riitti vähän myös itse jäsenelle. Jäsen pitäisi saada selvitettyä merkkijonosta:

talletus.2\jasen.cpp - alusta

	int cJasen::alusta(const char *rivi) 
	{
	  char *urivi = tee_jono(rivi); // Kopio, koska ei ole const char * alustajaa
	  istrstream sstr(urivi);       // merkkivirralle
	  sstr >> *this;
	  free(urivi);
	  return 0;
	}
Seuraavaksi pitäisi jäsen lukea tietovirrasta:
	istream &operator>>(istream &is,cJasen &jasen)
	{
	  char jono[50]; 
	  is.getline(N_S(jono),'|'); sscanf(jono,"%d", &jasen.tunnus_nro);
	  is.getline(N_S(jono),'|'); nimi = poista_tyhjat(jono);
	  is.getline(N_S(jono),'|'); sotu = poista_tyhjat(jono);
	  ...
	  is.getline(N_S(jono),'|'); sscanf(jono,"%lf", &jasen.maksu);
	  ...
	}
Tämä on taas muuten hyvä, mutta ratkaisua vaivaa tietty epäsymmetria eri tietotyyppien välillä. Lisäksi jos talletuksessa on sovittu, että esimerkiksi -1 tarkoittaa syöttämätöntä arvoa ja talletetaan tyhjänä, pitäisi lukemisessa tämä käsitellä kääntäen. Voitaisiin yrittää myös seuraavaa:

talletus.2\jasen.cpp - lukeminen

	istream &operator>>(istream &is,cJasen &jasen)
	{
	  char erotin = jasen.erotin;
	  ota_seuraava(is,jasen.tunnus_nro      ,erotin);
	  ota_seuraava(is,jasen.nimi            ,erotin);
	  ota_seuraava(is,jasen.hetu            ,erotin);
	  ota_seuraava(is,jasen.katuosoite      ,erotin);
	  ota_seuraava(is,jasen.postinumero     ,erotin);
	  ota_seuraava(is,jasen.postiosoite     ,erotin);
	  ota_seuraava(is,jasen.kotipuhelin     ,erotin);
	  ota_seuraava(is,jasen.tyopuhelin      ,erotin);
	  ota_seuraava(is,jasen.autopuhelin     ,erotin);
	  ota_seuraava(is,jasen.liittymisvuosi  ,erotin);
	  ota_seuraava(is,jasen.jmaksu          ,erotin);
	  ota_seuraava(is,jasen.maksu           ,erotin);
	  ota_seuraava(is,jasen.lisatietoja     ,erotin);
	  if ( jasen.tunnus_nro >= jasen.seuraava_nro )
	    jasen.seuraava_nro = jasen.tunnus_nro + 1;
	  return is;
	}
Funktio ota_seuraava on polymorfinen eri tietotyypeille ja sen tehtävänä on päästä eroon turhista välilyönneistä ja lukea tietovirtaa seuraavaan erotinmerkkiin saakka. ota_seuraava huolehtii myös tyhjän arvon käsittelystä (-1 <=> "").

19.3 Funktiomallit (template- funktiot)

Funktio ota_seuraava C- merkkijonoa varten voidaan tehdä seuraavasti:

talletus.2\jasen.cpp - ota_seuraava

	static int ota_seuraava(istream &is, char *s, int max_koko, char erotin)
	{
	  if ( max_koko <= 0 ) return 0;
	  s[0] = 0;
	  if ( !is ) return 1;
	  is.getline(s,max_koko,erotin);
	  poista_tyhjat(s);
	  return ( is == 0 );
	}
Tätä käyttäen voimme tehdä saman funktion kuormitettuja versioita muille tietotyypeille:
	int ota_seuraava(istream &is, double &d,  char erotin)
	{
	  char s[100];
	  int err = ota_seuraava(is,s,sizeof(s),erotin);
	  d = jono_doubleksi(s,"%lf");
	  return err;
	}
	int ota_seuraava(istream &is, int &i,  char erotin)
	{
	  char s[100];
	  int err = ota_seuraava(is,s,sizeof(s),erotin);
	  i = jono_intiksi(s,"%d");
	  return err;
	}
Ja vielä tarvittaisiin lisää! Kuitenkin nämä näyttävät keskenään yllättävän samanlaisilta. Kirjoittamalla pari apualiohjelmaa ja vaihtamalla nimiä saadaankin niistä täsmälleen samanlaisia:
	int ota_seuraava(istream &is, double &a,  char erotin)
	{
	  char s[100];
	  int err = ota_seuraava(is,s,sizeof(s),erotin);
	  muunna_jono(s,a);
	  return err;
	}
	
	int ota_seuraava(istream &is, int &a,  char erotin)
	{ ... ihan sama ... }
	
	inline void muunna_jono(const char *s,int &i)
	{
	  i = jono_intiksi(s,"%d");
	}
	
	inline void muunna_jono(const char *s,double &d)
	{
	  d = jono_doubleksi(s,"%lf");
	}
Seuraavaksi herääkin kysymys: Kannattaako ota_seuraava- funktiota kirjoittaa erikseen useita eri versioita yhden sanan muutoksella. Kyllähän näitä leikkaa- liimaa - menetelmällä syntyy kuin sieniä sateella, mutta entäpä ylläpito, jos yleiseen kaavaan tuleekin pieni muutos. Kokemus osoittaa että jostakin "samanlaisesta" funktiosta muutos unohtuu ja virhe paljastuu vasta aikojen kuluttua!

Onneksi C++:ssa on apu tätä varten: funktiomallit (function template). Idea on siinä, että jokin yleinen funktio kirjoitetaan tyyppiä vaille valmiiksi ja kääntäjä generoi sitten tästä mallista (muotista) tarvittavan määrän vastaavia todellisia funktioita niille tyypeille, joilla funktiota kutsutaan:

	template <class TYPE>
	int ota_seuraava(istream &is, TYPE &a,  char erotin)
	{
	  char s[100];
	  int err = ota_seuraava(is,s,sizeof(s),erotin);
	  muunna_jono(s,a);
	  return err;
	}
Jos nyt on esimerkiksi kutsu:
	int i;
	ota_seuraava(is,i,'|');
generoituu tästä funktio jossa TYPE on korvattu tyypillä int:
	int ota_seuraava(istream &is, int &a,  char erotin);
Nyt joudumme kirjoittamaan vaan muunna_jono- funktion kutakin eri tietotyyppiä varten. Tämä ei ole kohtuuton vaatimus, sillä kullekin tietotyypille olisi hyvä olla tapa muuntaa se merkkijonoksi (meillä jonoksi) ja päinvastoin.

Siis jos lisätään uusi tietotyyppi, kirjoitetaan funktiot:

	void muunna_jono(const char *s,uusi_tyyppi &u);
	string jonoksi(const uusi_tyyppi &u);

19.4 Virheilmoitukset, static

Talletukseen liittyvät aliohjelmat on suunniteltu const char * - tyyppisiksi. Mikäli jokin menee pieleen, palauttavat ne nimessään osoittimen itse virheilmoitukseen, jolloin kutsunut ohjelma voi halutessaan tulostaa virheilmoituksen.

Mikäli aliohjelma päättyy onnellisesti, voidaan palauttaa NULL- osoitin, eli ei virheilmoitusta.

	jasenet.cpp:
	static const char *TIED_EI_AUKEA  = "Tiedosto ei aukea!";
	...
	  if ( !f ) return TIED_EI_AUKEA;
	  ...
	  return NULL;
	...
	  if ( (virhe=talleta_kerho(&kerho))!=NULL ) {
	  printf("%s",virhe);
	
	naytto.cpp:
	  const char *virhe;
	  virhe = kerho->lue_tiedostosta(tied);
	  if ( virhe ) {
	    cout << virhe << endl;
	    return 1;
	  }
	  return 0;
Tässä static tarkoittaa, että muuttuja on vain tämän tiedoston (jasenet.cpp) sisäinen eikä näin ollen nimenä näy tiedoston ulkopuolelle. Kuitenkin muuttuja säilyttää arvonsa koko ohjelman suorituksen ajan, joten ulospäin voimme välittää osoitteita tällaisiin muuttujiin, ja näin jokin toinenkin ohjelman osa pääsee niihin käsiksi (tosin tässä ei ole suotavaa, että joku niitä muuttaisi, siis osoitteet on käsitettävä "read only"). Tähän käyttöön merkkijonot olisi voitu esitellä myös aliohjelman sisäisinä staticeina.

static- muuttujat varataan eri alueesta kuin aliohjelman lokaalit (automaattiset) muuttujat. Lokaalit muuttujat otetaan yleensä ajonaikana pinosta, joka saattaa myös loppua. Näin ollen ei- rekursiivisissa aliohjelmissa usein lokaaleillekin isoille muuttujille annetaan static- määritys.

C- kielen static - sanalla on siis kaksi merkitystä, ja sen tilalla pitäisikin olla oikeastaan kaksi eri sanaa: PRIVATE ja SAVE. Joku saattaisikin määritellä

	#define PRIVATE static
	#define SAVE    static

Tehtävä 19.166 static

Mikä oli static - sanan toinen (SAVE) merkitys?

19.4.1 Aliohjelma käsittelemään virheilmoitus

Jos usein tarvitaan käsittelyä
	  virhe = joku_jomma_josta_mahdollisesti_virheilmoitus_tai_NULL(...)
	  if ( virhe ) {
	    cout << virhe << endl;
	    return 1;
	  }
	  return 0;
voidaan tehdä ehkä mieluummin aliohjelma ilmoitus, jota voidaan käyttää:
	return ilmoitus(kerho->lue_tiedostosta(tied));
Eli aliohjelma tulostaa mahdollisen virheilmoituksen ja palauttaa 0 jos virheilmoitusta ei ollut ja 1 jos virheilmoitus oli. Lyhentää kirjoittamista kivasti, joten virhekäsittely tulee helpommin tehtyä.

19.4.2 Makro virhekäsittelyn avuksi

Toisaalta usein voi tulla tilanne, jossa aliohjelman jatkaminen on kiinni siitä, tuleeko jostakin apualiohjelmasta virhe vai ei:
	...
	const char *virhe;
	virhe = luo_taulukko(max_koko);
	if ( virhe ) return virhe;
	...
Tätä käsittelyä varten ei voida helpolla tehdä aliohjelmaa, mutta voidaan tehdä kyllä makro, jota voitaisiin kutsua:
	IF_ERR_RETURN(luo_taulukko(max_koko));
Makron toteutus olisi vaikkapa seuraavanlainen:
	#define IF_ERR_RETURN(v) { const char *virhe=(v); if ( virhe ) return virhe; }

Tehtävä 19.167 IF_ERR_RETURN

Pura auki makrokutsu IF_ERR_RETURN(luo_taulukko(max_koko));

19.5 Muunnokset numeerinen <- > merkkitieto

Numeeristen kenttien käsittelyä varten pitää voida muuttaa numeerista tietoa merkkijonoksi ja päinvastoin:
	1.25 <- - > "1.25"
Muunnoksia varten on valmiina mm. funktiota
	atoi
	atof
	sscanf
	sprintf
Usein nämä funktiot kelpaavatkin sellaisenaan. Meidän tarkoituksessamme tarvitsemme myös tyhjän arvon; kentän johon ei ole vielä syötetty arvoa. Numerona tyhjää ei sellaisenaan voi esittää, joten valitsemme vaikkapa - 1:en esittämään tyhjää arvoa (koska emme tarvitse negatiivisia lukuja). Kuitenkin tulosteissa - 1 näyttäisi hassulta ja sitä pitäisi selitellä käyttäjille. Siis on helpompi tulostaa tyhjä arvo - 1:en tilalle.

Näitä muunnoksi varten kirjoitamme avuksi aliohjelmat, joita voidaan kutsua esim:

talletus.2\muunnos.c - luvut <-> merkkijonot

	int_jonoksi (N_S(st),i,"%d");
	double_jonoksi (N_S(st),d,"%4.2lf");
	i = jono_intiksi (st,"%d");
	d = jono_doubleksi (st,"%lf");

Tehtävä 19.168 int_jonoksi, jono_doubleksi

Kirjoita alkeelliset versiot yllä mainituista funktioista, siten että -1 <=> "".

19.5.1 Varoitus

Vaikka jonoksi muuttavat funktiot onkin kirjoitettu siten, että ne palauttavat nimessään osoittimen muunnettuun jonoon, on esimerkiksi seuraavan kaltaisten kutsujen kanssa oltava tarkkana:
	char st[80];
	...
	printf("%s %s",double_jonoksi(N_S(st),d1,"%4.2lf"),
	               double_jonoksi(N_S(st),d2,"%4.2lf"));

Tehtävä 19.169 Vaarallinen ??_jonoksi

Miksi edellä mainittu kutsu on vaarallinen?

19.6 Talletuksen testaus

Testaamme ohjelmaamme lisättynä tiedoston lukemisella ja talletuksella. Aivan aluksi ohjelmaa ajetaan, lisätään muutama henkilö ja lopetetaan ohjelma. Katsotaan tekstieditorilla onko syntynyt tiedosto sellainen kuin sen pitäisi. Vasta kun on siirrytään testeissä eteenpäin.

Seuraavana kannattaa ehkä käyttää syöttönä tekstieditorilla tehtyä tiedostoa. Mikäli (aikanaan) kaikki menee halutulla tavalla, voidaan lisätä edellä huomatut puutteet muotoilussa ja kommenttien kopioinnissa ja/tai siirtyä eteenpäin suunnittelemaan päätesyöttöä.


previous next Title Contents Index