* muita vaihtoehtoja tarkistusten tekemiseen
* funktio- osoittimet
Hyvä ohjelma olisi muutettavissa käyttäjän mukaan. Aloittelijalle enemmän ohjeita ja varmistuskysymyksiä ja kokenut käyttäjä saa itse vastata tekosistaan.
Eri kielten toiminta eri tietotyyppejä luettaessa vaihtelee. Pascal - ohjelma kaatuu mikäli numeeriseen tietoon vastataan merkkitietoa. Mikä olisikaan harmillisempaa kuin se, että sihteeri on naputellut tietoja koko päivän (EIKÄ OLE TALLETTANUT NIITÄ VÄLILLÄ!???) ja iltapäivän väsymyksessä vastaa kysymykseen
Jäsenmaksu>kymppija koko ohjelman suoritus loppuu!
C- kieli on huomattavasti siedettävämpi numeerisen tiedon luvussa. scanf ei vain muuta merkkitietoa numeeriseksi ja näin tavallisen scanf- funktion käyttö on aivan suotavaa numeerisen tiedon lukemiseen. Rivin lukematta jäänyt (mahdollisesti virheellinen) osa on sitten syytä poistaa (fflush).
C- kielen huono puoli on siinä, että vastaavasti merkkijonojen lukeminen standardifunktioilla on vaarallista. Näiden lukemiseen pitääkin lähes poikkeuksetta tehdä oma aliohjelma (tai käyttää sopivaa kirjastoa). Aina on ihmisiä, jotka sanovat: "Laita jonon pituudeksi 200, niin ei kukaan jaksa kirjoittaa niin pitkää vastausta!". Näinhän se on, mutta monestiko itselläsi on manuaali tai jokin muu esine jäänyt nojaamaan näppäimistöön ja näin auto- repeat - toiminto työntää määrättömästi merkkejä?
Todellisissa ohjelmissa voidaan näppäimistön lukua tehdä merkki kerrallaan aliohjelmilla, joille on tarkkaan kerrottu syötön muoto ja sen aikana sallitut näppäimet. Tällöin mahdollisista virhepainalluksista voidaan piipata tai niihin voidaan reagoida muuten. Samoin kentälle varatun koon ylittyessä voidaan heti reagoida. Tämä kuitenkin vaatii puskuroimatonta syötön käsittelyä (ohjelmien siirrettävyys kärsii) ja hieman lisää vaivaa.
jono <= "52 mk" if ( sscanf(jono,"%d",&hinta)<1 ) /* virhe.... */;
string hetu; ... do { printf("Sosiaaliturvatunnus>"); getline(cin,hetu,'\n'); } while ( tarkista_hetu(hetu) ) ... Henkilötunnus>1234[RET] Hetu väärin! Anna uudelleen! Henkilötunnus>020347- 123T[RET] ...Tässä tapauksessa aliohjelma voi myös tulostaa virheilmoituksen, jolloin kutsuvan ohjelman ainoa tehtävä on lukea kunnes tulee oikea vastaus.
Mutta entäpä jos tiettyjä tarkistuksia haluttaisiin kuitenkin tehdä. Pitääkö koko ohjelma kirjoittaa uusiksi?
Ei! Kriittinen on tietenkin vain kysy_tiedot. Mikäli aliohjelma on toteutettu ilman silmukkaa, kysymällä kukin tieto omalla lauseellaan, niin siitä sitten vaan lisäämään tarkistus kunkin lukemisen ympärille. Tämä on aivan hyvä ratkaisu, mikäli kenttiä on vähän tai kentät ovat eri tietotyyppiä.
int cNaytto::kysy_tiedot(cJasen &jasen) { . . . do { cout << "Hetu >"; getline(cin,apujasen.hetu,'\n'); poista_tyhjat(apujasen.hetu); if ( apujasen.hetu == "q" ) return 1; } while ( tarkista_hetu(apujasen.hetu) ); . . . do { cout << "Jäsenmaksu mk >"; cin.getline(N_S(jono),'\n'); if ( strcmp(jono,"q") == 0 ) return 1; apujasen.jmaksu = jono_doubleksi(jono,"%lf"); } while ( (apujasen.jmaksu < 0) || ( 500 < apujasen.jmaksu ) );Voiko tarkista_hetu edes selvitä tarkistuksista? Osasta voi, mutta entäpä jos esimerkiksi hetu pitää olla erisuuri kullekin jäsenelle. Aliohjelma ei tiedä mihin hetuja verrattaisiin! Siis aliohjelmalle pitäisi parametrina viedä tietysti myös käsiteltävä kerho tai ainakin jäsenistö.
Entä kuka kutsui aliohjelmaa. Jokaiseen vastaavaan paikkaan täytyy myös tietysti lisätä kutsuparametri.
Ohjelmointiympäristöjen mukana tulee usein apuohjelma nimeltä grep. Ohjelmalla voidaan etsiä sanoja (tai tiettyä hakuehtoa) valitusta joukosta tiedostoja. Esimerkiksi
E:\KURSSIT\CPP\KERHO\TARKISTU.4>grep - n+ kysy_tiedot *.cpp *.h File NAYTTO.CPP: 359 int cNaytto::kysy_tiedot(cJasen &jasen) 426 if ( kysy_tiedot(jasen) != 0 ) return; File NAYTTO.H: 32 int kysy_tiedot(cJasen &jasen); E:\KURSSIT\CPP\KERHO\TARKISTU.4>Kun saamme listan kaikista esiintymistä, muutetaan tarvittavat kohdat. Päivitetään myös kommentoinnin muutos- osaan, että metodin parametrien määrä on muuttunut, jottei joku muu saman kirjaston käyttäjä sitten ihmettele liian kauan sitä, miksi aliohjelma ei enää toimi. Voidaan myös kirjoittaa aivan uusi aliohjelma eri nimelle (esim. kysy_ja_tark_tiedot).
cNaytto::kysy_kentta(int k,cJasen &jasen) . . . virhe = jasen.sijoita(k,jono); switch ( virhe ) { case KENTTA_OK: return TOIM_SEURAAVA; case KENTTA_VAROITUS: cout << virhe.Virhe() << endl; return TOIM_SEURAAVA; case KENTTA_UUDELLEEN: case KENTTA_MUUTETTU_KYSY: cout << "Tarkista ja anna tieto uudelleen" << endl; return TOIM_KYSY_UUDELLEEN; case KENTTA_OK_ONKO_AINOA: lkm = kerho- >Jasenet().laske_montako_muuta(jasen,k,kuka); if ( lkm == 0 ) return TOIM_SEURAAVA; kerho- >Jasenet().anna(kuka).tulosta(cout); cout << "On jo ennestään!" << endl; jasen.sijoita(k,edell); return TOIM_KYSY_UUDELLEEN; case KENTTA_OK_VAROITA_MUUT: lkm = kerho- >Jasenet().laske_montako_muuta(jasen,k,kuka); if ( lkm == 0 ) return TOIM_SEURAAVA; kerho- >Jasenet().anna(kuka).tulosta(cout); if ( kylla_kysymys("On jo, lisätäänkö silti uusi!") ) return TOIM_SEURAAVA; jasen.sijoita(k,edell); return TOIM_KYSY_UUDELLEEN; default: ; } // virhe . . . int cNaytto::kysy_tiedot(cJasen &jasen) { . . . kaille kentille do { virhe = kysy_kentta(int k,cJasen &jasen) } while ( virhe != TOIM_SEURAAVA );
int cJasen::sijoita(int k,const string &st) { switch ( k ) { case 0: ... return KENTTA_OK; ... case 2: // Hetu if ( tarkista_hetu(st) ) return KENTTA_UUDELLEEN; hetu = st; return KENTTA_OK_ONKO_AINOA; ... case 9: if ( sscanf(st.c_str(),"%d"&liittymisvuosi) != 1 ) return KENTTA_UUDELLEEN; if ( liittymisvuosi < 1950 ) return KENTTA_UUDELLEEN; if ( liittymisvuosi > 1998 ) return KENTTA_UUDELLEEN; return KENTTA_OK; ... default: return KENTTA_OK; // Vääriä kenttiä ei sijoiteta mutta ne kelp. } }
Esimerkiksi funktion integrointi suorakaidesäännöllä voitaisiin hoitaa seuraavasti:
#include <stdlib.h> #include <stdio.h> #include <math.h> double integroi(double x1, double x2, int tiheys) { double x,dx,summa=0; dx = (x2- x1)/tiheys; for (x=x1+dx/2 ; x<x2; x+=dx) summa += sin(x)*dx; return summa; } int main(void) { printf("Integraali sin(x) väliltä [0,pi] on noin %7.5lf\n", integroi(0,M_PI,100)); return 0; }Entäpä mikäli haluaisimme integroida vaikka ex. No vaihdetaan sin(x) tilalle exp(x)! Entäpä jos tarvitsemme samassa ohjelmassa sekä sin(x) että exp(x) integraalit? Kirjoitammeko integroi_sin ja integroi_exp. Ei kuulosta järkevältä!
Otamme käyttöön funktio- osoittimet. Määritellään aluksi funktio_tyyppi
typedef double (*funktio_tyyppi)(double x);Sitten voimme esitellä tarpeellisen määrän vastaavaa tyyppiä olevia funktiota. Itse integrointi muutetaan käyttämään yhtä "ylimääräistä" parametria; funktiota jota integroidaan:
double oma_funktio(double x) { return 2*x- 5; } double integroi(funktio_tyyppi f,double x1, double x2, int tiheys) { double x,dx,summa=0; dx = (x2- x1)/tiheys; for (x=x1+dx/2 ; x<x2; x+=dx) summa += f(x)*dx; return summa; }Nyt voimme kutsua esimerkiksi:
double ifx; ... ifx = integroi(oma_funktio,0,5,100); ... ifx = integroi(sin,0,M_PI,1000); ... ifx = integroi(exp,0,1,500); ...Huomautus! Hienot adaptiiviset integrointimenetelmät muuttavat itse välin tiheyttä funktion käyttäytymisen mukaan.
/* Funktio- osoitin tarkistusfunktioon: */ typedef int (*Tarkistus_funktio)(Tarkistus_tyyppi *, string &); . . .
int cNaytto::kysy_tiedot(cJasen &jasen) { Tarkistus_funktio tark; . . . kaikille kentille do { . . . kysy kenttä => jono tark = jasen.Tarkistus(k); virhe = 0; if ( tark != NULL ) virhe = tark(jono); } while ( virhe ); . . . }
Edellä totesimme, että tarkista_hetu(kerho,jono) ei ehkä riitäkään. Miksi? Jos lisäämme uutta nimeä, niin asia on aivan oikein. Mutta entäpä mikäli korjaamme sotua ja kirjoitamme vastaukseksi täsmälleen saman tekstin kuin alkuperäinenkin. Eikö alkuperäinen hetu tällöin löydy? Löytyypä hyvinkin. Siis tarkista_hetu "hermostuu" ja tulostaa virheilmoituksen:
Sotu esiintyy ennestäänja vaatii uutta syöttöä!
Siis parametrit eivät riitä! Mitä pitää lisätä? Tästä ajattelutavasta saattaa seurata loputon kasa uusia parametreja. Kasataan kaikki tarkistusten tarvitsemat parametrit yhteen ainoaan tietueeseen Tarkistus_tyyppi ja kutsutaan tarkistusfunktioita muodossa:
t_sotu(tarkistus_tiedot,jono);
Vastaavasti tietysti tarkistusfunktio-osoittimien kanssa
typedef int (*Tarkistus_funktio)(Tarkistus_tyyppi &, string &); . . . int cNaytto::kysy_tiedot(cJasen &jasen) { Tarkistus_funktio tark; Tarkistus_tyyppi tiedot(... . . . if ( tark != NULL ) virhe = tark(tiedot,jono); . . .
Kun uusi tarkistusfunktio kirjoitetaan, pitää nyt * kirjoittaa se tiedostoon kerhotar.cpp
* osoite lisätä tarvittavaan kohtaan metodissa cJasen::Tarkistus(int k)
* lisätä funktion prototyyppi tiedostoon kerhotar.h
int cJasenet::laske_montako_muuta(const cJasen &jasen, int k,int &kuka) const // Lasketaan monnellako muulla kerholaisella on sama tieto kentässä k // kuin jasenella. Palautetaan muiden maara ja sijoitetaan muuttujaan // kuka viimeinen sellainen jolla oli sama tieto { int i,samoja=0; string kentta = jasen.kentta_jonoksi(k); for (i=0; i<lkm; i++) { if ( alkiot[i]- >kentta_jonoksi(k) == kentta && alkiot[i]- >sama_rekisteri(jasen) == 0 ) { samoja++; kuka = i; } } return samoja; }Kohdalla olevan jäsenen tiedot voidaan välttää tarkistamalla ettei rekisterinumero ole sama (alkiot[i]- >sama_rekisteri(jasen)).
Tekniikan etuna on se, että ajan oloon kertyy kattava määrä erilaisia kenttä- luokkia ja seuraava ohjelma voidaan kasata vain valitsemalla mitä luokkia tarvitaan:
class cJasen { cIntKentta tunnus_nro; cNimi1Kentta nimi; cHetu1Kentta hetu; cJono1isoksiKentta katuosoite; cPostinumeroKentta postinumero; cJonoIsoksiKentta postiosoite; cPuhKentta kotipuhelin; cPuhKentta tyopuhelin; cPuhKentta autopuhelin; cIntKentta liittymisvuosi; cDoubleKentta jmaksu; cDoubleKentta maksu; cJonoKentta lisatietoja; ...
1. Yleistä avustusta, jossa mielellään voidaan selata ohjelman eri kohtien toimintaa. Tämä on suhteellisen helppo toteuttaa yleisesti.
2. Erityistä avustusta kutakin toimenpidettä kohti. Tällaisen sisältöriippuvan avustuksen (context sensitive help) tekeminen vaatii ohjelmaan lisäyksiä mm. kunkin eri kentän lukurutiinin toimintoihin.
Kirjoitamme yleiskäyttöisen aliohjelmakirjaston help.c, jolla molemmat edellä mainitut ominaisuudet voidaan toteuttaa. Koska käytännössä avustustietoutta on aina liian vähän ja siinä on kirjoitusvirheitä, pyrimme sijoittamaan avustuksen omaksi tiedostokseen, josta help.c- kirjaston on sitä helppo lukea. Olkoon avustustiedoston muoto vaikkapa seuraava:
[?] ? = Avustuksen avustus ====================== ?- merkillä saa yleensä joka paikassa avustusta! Jos avustuksessa kysytään aihetta, josta avustusta halutaan, voidaan vastata esimerkiksi: .... [Lisäys] Lisäys ====== Lisäystoiminolla lisätään uusia henkilöitä. Lisättävä henkilö ... Katso myös: Tietojen syöttö, Asetukset [Tietojen syöttö] Tietojen syöttö =============== Tietoja syötettäessä näytössä näkyy suluissa arvo, joka tulee kentän ... [t_sotu]# Sotuksi kelpaa... ... [SISÄLLYS] Sisällysluettelo: ================= ? Lisäys Etsiminen ...Nyt esimerkiksi kutsulla
help(NULL);päästäisiin avustuksen sisällysluetteloon, josta sitten käyttäjä voi tarvittaessa siirtyä haluamaansa kohtaan vaikka kirjoittamalla Tie*.
Sisältöriippuvassa avustuksessa kutsuttaisiin sitten suoraan haluttua kohtaa, esimerkiksi:
help("[Lisäys]");Helpoimmin tämä kävisi esimerkiksi lisäämällä kysy_kentta - aliohjelmaan kutsu funktioon: char *avustus(int nro), jonka mukaan avustusta pyydettäisiin halutusta kentästä jos käyttäjä painaisi?.
Toisaalta avustusta tarvitaan ehkä mieluumminkin kentän tyypin mukaan, ei niinkään itse kentän mukaan (koska "Jäsenen nimi>" tai "Sotu>" sinänsä ovat jo itse selittäviä). Tällöin teemme tarkistusfunktion perusteella löytyvän avustuksen tarkistus_nimi(t_funk), joka palauttaa vastaavan nimen (esim. t_sotu =>"t_sotu"). Näin kysy_kentta voi funktion perusteella saada selville tarkistusfunktiota vastaavan nimen ja nimen perusteella voi kutsua avustusta. Avustukseen on kirjoitettu valmis funktio help_aihe(nimi), joka lisää sulut []nimen ympärille ("t_sotu" =>"[t_sotu]").
Toteutamme tämän vasta ohjelman viimeisessä versiossa.