* 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ä.
- jos ei muutoksia, turha tallettaa - tee (mahdollisesta) vanhasta tiedostosta.BAK - avaa tiedosto kirjoittamista varten - talleta kokonimi ja maksimikoko (otsikkotiedot) - talleta kukin tietueSeuraavaksi pitää miettiä mikä toimenpide suunnitelluista kuuluu millekin luokalle. Luokkinahan olivat:
cNaytto cKerho cJasenet cJasen
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; }
class cKerho { ... const char *talleta(const string &tied=""); ... int TeeBak(const string &bak_tark) { return jasenet.TeeBak(bak_tark); };
const char *cKerho::talleta(const string &tied) { return jasenet.talleta(tied); }
//------------------------------------------------------------------------- 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.
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:
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ö.
- 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
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)); }
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; }
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:
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 <=> "").
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);
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
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));
... 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; }
1.25 <- - > "1.25"Muunnoksia varten on valmiina mm. funktiota
atoi atof sscanf sprintfUsein 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:
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");
char st[80]; ... printf("%s %s",double_jonoksi(N_S(st),d1,"%4.2lf"), double_jonoksi(N_S(st),d2,"%4.2lf"));
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öä.