Monisteen korjaus
Auta korjaamaan tämä moniste kuntoon!
Katso karkeasti miltä pitäisi näyttää monisteesta:
http://users.jyu.fi/~vesal/kurssit/cpp/moniste/html/m.htm
Korjattavaa:
- tehtävät (osa puuttuu, mikään ei ole vastattavissa)
- ohjelmat ajettaviksi mikäli mahdollista
- ohjelmalistaukset tiiviiksi
- kappaleenjakomerkkejä
Ota yhteyttä jos haluat auttaa vesal@jyu.fi, niin annan tarkempia ohjeita.
Lukijalle
Tämä moniste on tarkoitettu oheislukemistoksi Ohjelmointi-kurssille. Vaikka monisteen yksi teema onkin C++–kieli, ei C++–kieli ole monisteen päätarkoitus. Päätarkoituksena on esitellä ohjelmointia. Esitystavaksi on valittu yhden ohjelman suunnitteleminen ja toteuttaminen alusta lähes loppuun saakka. Tämä Top-Down –metodi tuottaa varsin suuren kokonaisuuden, jonka hahmottaminen saattaa aluksi tuntua vaikealta.
Kunhan oppii kuitenkin katsomaan kokonaisuuksiin yksityiskohtien sijasta, asia helpottuu. Yksityiskohtia harjoitellaan monisteen esimerkeissä (Bottom-Up), joista suuri osa liittyy monisteen malliohjelmaan, mutta jotka silti voidaan käsittää mallista riippumattomina palasina.
Monisteen ohjelmat on saatavissa myös elektronisesti, jotta niiden toimintaa voidaan kokeilla kunkin vaiheen jälkeen.
C++–kieltä ja sen ominaisuuksia on monisteessa sen verran, että lukijalla on juuri ja juuri erittäin pienet mahdollisuudet selvitä ilman muuta kirjallisuutta.
Lukijan kannattaakin ilman muuta hankkia ja seurata tämän monisteen rinnalla jotakin varsinaista C++–ohjelmointikirjaa. Esimerkkinä mainittakoon Päivi Hietasen C++ ja Olio–ohjelmointi, Teknolit, 1996 tai itse kielen kehittäjän teos:. Bjarne Stroustrup: The C++ Programming Language, Second Edition - Addison-Wesley, 1991. Olio-ohjelmoinnista on kansantajuinen teos: Timothy A. Budd: An Introduction to Object-Oriented Programming, Second Edition - Addison Wesley, 1997. Hietasen teoksen joihinkin esimerkkeihin kannattaa suhtautua kuitenkin varauksellisesti. Usein myös ohjelmointiympäristön mukana olevasta OnLine–avustuksesta (Help) saa tarvittavaa lisätietoa.
Monisteen esimerkkiohjelmat löytyvät elektronisessa muodossa:
Mikroluokka: hakemisto: n:\kurssit\cpp\moniste\esim
WWW: URL: http://www.math.jyu.fi/~vesal/kurssit/cpp/moniste/esim
Edellä mainittuun polkuun lisätään vielä ohjelman yhteydessä mainittu polku.
Monisteessa on lukuisia esimerkkitehtäviä, joiden tekeminen on oppimisen kannalta lähes välttämätöntä. Vaikka lukija saattaa muuta kuvitellakin, on monisteen vaikeimmat tehtävät monisteen alussa. Mikäli loppupuolen tehtävät tuntuvat vaikeilta, ei monisteen alkuosa olekaan hallinnassa. Siksi kehotankin lukijaa aina vaikeuksia kohdatessaan palaamaan monisteen alkuosaan; siitä ei monikaan voi sanoa, ettei asioita ymmärtäisi.
Lopuksi kiitokset kaikille työtovereilleni monisteen kriittisestä lukemisesta. Erityisesti Tapani Tarvainen on auttanut suunnattomasti C–kieleen tutustumistani ja Jonne Itkonen vastaavasti tutustumista C++–kieleen ja olio–ohjelmointiin.
Alkuperäinen versio allekirjoitettu Palokassa 28.12.1991
Monisteen 3. korjattuun painokseen on korjattu edellisissä monisteissa olleita painovirheitä sekä lisätty lyhyt C–kielen "referenssi". Lisäksi kunkin esimerkkiohjelman alkuun on laitettu kommentti siitä, mistä tiedostosta lukija löytää esimerkin. Myös hakemistoa on parannettu vahventamalla määrittelysivun sivunumero.
Palokassa 28.12.1992
Monisteen 4. korjattuun painokseen on jopa vaihdettu monisteen nimi: Ohjelmointi++, kuvaamaan paremmin olio–ohjelmoinnin ja C++:n saamaa asemaa. Tätä kirjoittaessani moniste ei ole vielä kokonaan valmis ja kaikkia siihen tulevia muutoksia en vielä tässä pysty luettelemaan.
Joka tapauksessa olen monisteeseen lisännyt tekstiä – valitettavasti nimenomaan ohjelmointikieleen liittyvää – jota vuosien varrella olen huomannut opiskelijoiden jäävän kaipaamaan. Lisäksi kunkin luvun alkuun on lisätty suppea luettelo luvun pääteemoista ja luvussa esiintyvistä kielen piirteistä sekä niiden syntaksista. Tämä syntaksilista on helppolukuinen "lasten" syntaksi, varsinainen tarkka ja virallinen syntaksi pitää katsoa kielen määrityksistä.
Ohjelmalistauksiin on lisätty syntax-highlight, eli kielen sanat on korostettu ja näin lukijan toivottavasti on helpompi löytää mitkä termit on itse valittavissa ja mitkä täytyy kirjoittaa juuri kuvatulla tavalla. Myös joitakin vinkkejä on lisätty. Pedagogisesti on vaikea päättää saako esittää virheellisiä tai huonoja ohjelmia lainkaan, mutta vanha viidakon sananlasku sanoo että "Viisas oppii virheistä, tavallinen kansa omista virheistä ja tyhmä ei niistäkään". Siis mahdollisuus "viisaillekin" ja nämä virheelliset ohjelmat on merkitty surullisella naamalla: . Näin aivan jokaisen ei tarvitse rämpiä jokaista sudenkuoppaa pohjia myöten ominpäin.
Olio-ohjelmointi on kuvattu esimerkkien avulla ja varsinainen oliosuunnittelu - joka on erittäin tärkeää – on jätetty erittäin vähälle. Suosittelenkin lukijalle jonkin oliosuunnitteluun liittyvän kurssin käymistä tai kirjan lukemista.
Myös tätä kurssia edeltävä kurssi Ohjelmoinnin alkeet on kokenut muutoksia ja vaikka kurssi meneekin nykyisin entistä pitemmälle, ei tästä monisteesta ole kuitenkaan poistettu kaikkea päällekkäisyyttä Ohjelmoinnin alkeet –monisteen kanssa. Toimikoon nämä päällekkäisyyden kertauksena ja kohtina, joissa luennoilla voidaan asia sivuttaa nopeammin. Joka tapauksessa lukijan kannattanee pitää myös Ohjelmoinnin alkeet –moniste tämän monisteen rinnalla.
Palokassa 5.1.1997
Monisteen 5. korjattuun painokseen on korjattu pieniä kirjoitusvirheitä ja epätäsmällisyyksiä. Samalla on poistettu hieman C-esimerkkejä ja yritetty enemmän jättää jäljelle esimerkkejä siitä, kuinka käytännössä kannattaa tehdä. Nyt erityisesti kurssia myös pitänyt Antti-Juhani Kaijanaho on antanut merkittävästi palautetta monisteesta.
Palokassa 30.12.2001
Vesa Lappalainen
1. Johdanto
Tämän monisteen tarkoituksena on toimia tukimateriaalina opeteltaessa sekä algoritmisen että olio–ohjelmoinnin alkeita. Aluksi meidän tulee ymmärtää mitä kaikkea ohjelmointi pitää sisällään. Aivan liian usein ohjelmointi yhdistetään päätteen äärellä tapahtuvaan jonkin tietyn ohjelmointikielen koodin naputtamiseen. Tämä on ehkä ohjelmoinnin näkyvin osa, mutta myös toisaalta mekaanisin ja helpoin osa.
Ohjelmointi voidaan jakaa esimerkiksi seuraaviin vaiheisiin:
- tehtävän saaminen
- tehtävän tarkentaminen ja tarvittavien toimintojen hahmottaminen
- ohjelman toimintojen ja tietorakenteiden suunnittelu, > oliosuunnittelu
- yksityiskohtaisten olioiden ja algoritmien suunnittelu
- OHJELMOINTITYÖKALUN VALINTA
- algoritmien/luokkien metodien tarkentaminen valitulle työkalulle
- ohjelmakoodin kirjoittaminen
- ohjelman testaus
- ohjelman käyttöönotto
- ohjelman ylläpito
Huomattakoon, että edellisessä listassa varsinaisesti tietokoneella tehtävä työ on vain aivan listan viimeisissä kohdissa. Tietenkin nykyisin suunnittelun alkuvaiheessakin tarvittava dokumentointi ja ideoiden sekä vaihtoehtojen kirjaaminen tehdään käyttäen tekstinkäsittelyohjelmia. Varsinaisesta koodauksesta ei kuitenkaan alkuvaiheessa ole kysymys.
Ohjelman kehityksen eri vaiheissa saatetaan tarvittaessa palata takaisin alkumäärityksiin. Kuitenkin ohjelman valittujen toimintojen muuttaminen oman laiskuuden tai osaamattomuuden takia ei ole suotavaa. Ei saa lähteä ompelemaan kissalle takkia ja huomata, että kangas riittikin lopulta vain rahapussiin.
1.1 Ohjelman suunnittelu
Usein ohjelmointikursseilla unohdetaan itse ohjelmointi ja keskitytään valitun työkalun – ohjelmointikielen – esittelyyn. Ajanpuutteen takia tämä onkin osin ymmärrettävää. Kuulijat kuitenkin hämääntyvät eivätkä ymmärrä luennoitsijan tekevän edellä kuvatun listan kaltaista suunnittelutyötä myös kunkin pienen malliesimerkin kohdalla. Kokenut ohjelmoija saattaa pystyä hahmottamaan ongelman ratkaisun ja tarvittavat erikoistapaukset päässään silloin, kun on kysy erittäin lyhyistä malliesimerkeistä. Jossain vaiheessa ohjelmoinnin oppimista suunnittelu ja koodin kirjoittaminen tuntuvat sulautuvan yhteen.
Opiskelun alkuvaiheessa on kuitenkin syytä keskittyä nimenomaan ongelman analysointiin ja ohjelman suunnitteluun. Tässä paras apu on usein terve maalaisjärki. Mitä vähemmän ymmärtää itse ohjelmointikielistä, sitä vähemmän kielet rajoittavat luovaa ajattelua.
Usein ohjelman suunnittelu voidaan aloittaa jopa käyttöohjeen kirjoittamisella! Tällöin tulee tutkituksi ohjelmalta vaaditut ominaisuudet ja toimintojen loogisuus sekä helppokäyttöisyys! Nykytyökaluilla voidaan myös rakentaa suhteellisen helposti ensin ohjelman käyttöliittymä ilman oikeita toimintoja. Tätä "protoa" voidaan sitten tutkia yhdessä asiakkaan kanssa ja päättää toimintojen loogisuudesta ja riittävyydestä.
1.2 Työkalun valinta
Kun ohjelmaan on suunniteltu halutut toimenpiteet ja päätetty mitä tietorakenteita tarvitaan, on edessä työkalun valinta. Nykypäivänä ei ole itsestään selvää, että valitaan työkaluksi jokin perinteinen ohjelmointikieli. Vastakkain pitää asettaa erilaiset sovelluskehittimet, valmisohjelmat kuten tietokannat ja taulukkolaskennat, ehkä jopa tavallinen tekstinkäsittely sekä ohjelmointikielet. Matemaattisissa ongelmissa jokin symbolisen tai numeerisen laskennan paketti saattaa olla soveltuva.
Ratkaisu voi koostua myös useiden eri ohjelmien toimintojen yhdistelemisestä: CAD –ohjelmalla piirretään/digitoidaan kartan pohjakuva, tietokantaohjelmalla pidetään kirjaa paikoista ja pienellä C-kielisellä ohjelmalla suoritetaan ne osat, joita CAD-ohjelmalla tai tietokantaohjelmalla ei voida suorittaa.
Joskus työkaluksi valitaan prototyyppiä varten jokin sovelluskehitin tai tietokantaohjelmisto. Kun halutut toiminnot on perusteellisesti testattu ja tuotetta tarvitsee edelleen kehittää, voidaan ohjelmointi toteuttaa uudelleen vaikkapa C–kielellä. Prototyyppi on rinnalla toimivana ja uudessa ohjelmassa käytetään samoja tietoja ja toimintoja.
1.3 Koodaus
Mikäli työkalun valinnassa päädytään olio/lausekieleen (esim. C++), ei pyörää kannata keksiä uudelleen. Nelikulmioon nähden kolmikulmiossa on yksi poksaus vähemmän kierroksella, mutta kyllä silti ympyrä on paras. Siis käytetään toisten kirjoittamia valmiita olioita ja/tai aliohjelmapaketteja "likaisessa" työssä.
Aina tietenkin puuttuu joitakin alemman tason palasia. Nämä tietysti koodataan JA TESTATAAN ERILLISINÄ ennen varsinaiseen ohjelmaan liittämistä.
Siis itse koodaus on pienten aputyökalujen etsimistä, tekemistä, testaamista ja dokumentointia. Lopullinen koodaus on näiden aputyökaluista muodostuvan palapelin yhteen liittäminen.
Jo koodausvaiheessa kannattaa miettiä ongelman yleisiä ominaisuuksia. Jos ollaan kirjoittamassa telinevoimistelun pistelaskua naisten sarjaan, niin koodissa ei mitenkään tulisi estää ohjelman käyttöä myös miesten sarjassa. Siis telineiden nimet ja määrät pitäisi olla helposti muutettavissa.
Koodausta voidaan tehdä joko BOTTOM–UP periaatteella, jolloin ensin rakennetaan työkalut (=olioluokat/aliohjelmat) jotka sitten kasataan yhteen. Toinen mahdollisuus on koodaus TOP–DOWN periaatteella, jolloin päätoiminnat kirjoitetaan ensin ja alatoiminnoista tehdään aluksi tyhjiä laatikoita. Myöhemmin valmiita ja testattuja alitoimintoja liitetään tähän runkoon. Valitulla menetelmällä ei ole vaikutusta lopputulokseen ja joskus voikin olla hyvää vaihtelua siirtyä näpertelemään pikkuasioiden kimpussa isojen kokonaisuuksien sijasta tai päinvastoin.
Missään tapauksessa ohjelma ei synny siten kuin se kirjallisuudessa näyttää olevan: alkumäärittelyt, aliohjelmat ja päämoduuli.
Koodaajan on osattava hyvin käytettävä työkalu, esim. ohjelmointikieli. Kuitenkin jonkin ohjelmointikielen hyvän osaamisen avulla on suhteellisen helppo kirjoittaa myös muunkielisiä ohjelmia.
Koodaus on pääosin tekstinkäsittelyä ja 10–sormijärjestelmä nopeuttaa koodin syntymistä oleellisesti. Myös hyvä tekstinkäsittelytaito valmiiden palasten siirtelemisineen ja kopioimisineen helpottaa tehtävää.
1.4 Testaus
Ohjelman testaus alkaa jo suunnitteluvaiheessa. Valitut algoritmit ja toiminnot pitää pöytätestata teoriassa ennen niiden koodaamista. Suunnitteluvaiheessa täytyy miettiä kaikki mahdolliset erikoistapaukset ja todeta algoritmin selviävän niistäkin tai ainakin määritellä miten erikoistapauksissa menetellään. Testitapaukset kirjataan ylös myöhempää käyttöä varten.
Koodausvaiheessa kukin yksittäinen aliohjelma testataan kaikkine mahdollisine syötteineen pienellä testiohjelmalla. Aliohjelman kommentteihin voidaan kirjata suunnitteluvaiheessa todettu testiaineisto ja testausvaiheessa ruksataan testatut toiminnot ja erikoistapaukset.
Lopullisen ohjelman toimivuus riippuu hyvin paljon siitä, miten hyvistä palasista se on kasattu.
Testauksessa apuna on aluksi pöytätestit. Sitten käytetään pieniä testiohjelmia. Ennen testiohjelmiin lisättiin tulostuslauseita. Nykyisin tehokkaat debuggerit helpottavat testausta huomattavasti: ohjelman toimintaa voidaan seurata askel kerrallaan ja epäilyttävien muuttujien arvoja voidaan tarkistaa kesken suorituksen. Voidaan myös laittaa ohjelma pysähtymään jonkin muuttujan saadessa virheellisen arvon.
Testaus on vaihe, missä hyvä koneenkäyttörutiini on suureksi avuksi.
1.5 Käyttöönotto
Valmiin ohjelman käyttöönotto tapahtuu yleensä aina liian aikaisin. Paineet keskeneräisesti testatun ohjelman käyttämiseksi ovat suuret. Lisäksi testaajat ja erityisesti ohjelman koodaajat ovat sokeita tavallisen käyttäjän (usein myös omilleen) virheille.
Käyttöohjeen olisi syytä olla valmis viimeistään tässä vaiheessa.
1.6 Ylläpito
Käyttöönotetusta ohjelmasta paljastuu aina virheitä tai puuttuvia toimintoja. Virheet pitää korjata ja puuttuvat toiminnot mahdollisesti lisätä ja ollaan jälleen ohjelmansuunnittelun alkuvaiheessa. Hyvin suunniteltuun ohjelmaan saattaa olla helppo lisätä uusia toimintoja ja vastaavasti huonosti suunnitellussa saattaa jopa tietorakenteet mennä uusiksi.
Myös ohjelman alkuperäiset kirjoittajat ovat saattaneet häipyä ja kesätyöntekijä joutuu ensitöikseen paikkaamaan toisten huonosti dokumentoitua sotkua.
1.7 Yhteenveto
Ohjelmointi ei yleensä ole yhden henkilön työtä. Eri henkilöt voivat tehdä eri vaiheita ohjelmoinnissa. Lähes aina tulee tilanne, missä jonkin toisen kirjoittamaa koodia joudutaan korjailemaan.
Oli ohjelmaa tekemässä kuinka monta henkilöä tahansa (vaikka vain yksi), pitää ohjelmointi jakaa vaiheisiin. Oikeaa ohjelmaa on mahdoton "nähdä" valmiina C-kielisinä lauseina heti tehtävän määrityksen antamisen jälkeen. Aloitteleva ohjelmoija kuitenkin haluaisi pystyä tähän (koska hän "näkee" määrityksestä: Kirjoita ohjelma joka tulostaa "Hello world", heti myös C-kielisen toteutuksen). Tämän takia ohjelmoinnin helpoin osa, eli koodaus koetaan ohjelmoinnin vaikeimmaksi osaksi – suunnittelu on unohtunut!
Valitulla ohjelmointikielellä ei ole suurtakaan merkitystä ohjelmoinnin toteuttamiseen. Jokin kieli saattaa soveltua paremmin johonkin tehtävään, mutta pääosin BASIC, Fortran, Pascal, C, LISP, Modula–2, ADA jne. ovat samantyylisiä lausekieliä. Samoin oliokielistä esimerkiksi C++, Java ja Delphi (Pascal) ovat hyvin lähellä toisiaan. Kun yhden osaa, on toiseen siirtyminen jo helpompaa.
Jos joku kuvittelee ettei hänen tarvitse koskaan ohjelmoida C/C++–kielellä, voi hän olla aivan oikeassakin. Nykyisin kuitenkin jokaisessa tietokantaohjelmassa, taulukkolaskentaohjelmassa ja jopa tekstinkäsittelyohjelmissakin (vrt. esim. TEX, joka on tosin ladontaohjelma) on omat ohjelmointikielensä. Osaamalla jonkin ohjelmointikielen perusteet, voi saada paljon enemmän hyötyä käyttämästään valmisohjelmasta. Ja joka väittää selviävänsä nykymaailmassa (ja sattuu lukemaan tätä monistetta) esimerkiksi ilman tekstinkäsittelyohjelmaa on suuri valehtelija!
2. Kerhon jäsenrekisteri
Mitä tässä luvussa käsitellään?
- tehtävän "analysointi"
- ohjelman vaatimien aputiedostojen sisällön suunnittelu
- ohjelman suunnittelu ohjelman tulosteiden avulla
- suunnitelman korjaus
- tarvittavien algoritmien hahmottaminen
- relaatiotietomalli
2.1 Tehtävän tarkennus
Ohjelman suunnittelu aloitetaan aina tehtävän tarkastelulla. Annettua tehtävää joudutaan usein huomattavasti tarkentamaan.
Olkoon tehtävänä suunnitella kerhon jäsenrekisteri. Onko kerho iso vai pieni? Mitä tietoja jäsenistä talletetaan? Mitä ominaisuuksia rekisteriltä halutaan?
Mikäli sovitaan, että kerho on kohtuullisen pieni (esim. alle 500 jäsentä), ei meidän heti alkuun tarvitse miettiä parhaita mahdollisia hakualgoritmeja eikä tiedon tiivistämistä.
Mitä tietoja jäsenistä tarvitaan?
- nimi
- sotu
- katuosoite
- postinumero
- postiosoite
- kotipuhelin
- työpuhelin
- autopuhelin
- liittymisvuosi
- tämän vuoden maksetun jäsenmaksun suuruus
- lisätietoja
jne...
Mitä ominaisuuksia rekisteriltä halutaan?
- kerholaisten lisääminen
- kerholaisten poistaminen
- tietyn kerholaisen tietojen hakeminen
- tietyn kerholaisen tietojen muuttaminen
- postitustarrat postinumerojärjestyksessä
- nimilista nimen mukaisessa järjestyksessä
- lista jäsenmaksua maksamattomista jäsenistä
jne...
2.2 Työkalun valinta
On varsin selvää, ettei tätä nimenomaista tehtävää kannattaisi nykypäivänä lähteä itse ohjelmoimaan, vaan turvauduttaisiin tietokantaohjelmaan. Joissakin erikoistapauksissa saatetaan vaatia ominaisuuksia, joita tietokantaohjelmasta ei saada. Tällöin työkaluksi valittaisiin lausekieli ja tietokantaohjelmiston aliohjelmakirjasto, joka hoitelee varsinaiset tietokannan ylläpitoon yms. liittyvät toimenpiteet.
Edellinen analyysi on kuitenkin tehtävä työkalusta riippumatta! Esimerkin vuoksi jatkamme tehtävän tutkimista hieman pidemmälle tavoitteena ohjelmoida jäsenrekisteri jollakin lausekielellä.
2.3 Tietorakenteet ja tiedostot
Mikäli työkalun valinnassa on päädytty johonkin lausekieleen, on jossain vaiheessa päätettävä käytettävistä tietorakenteista. Esimerkin tapauksessa meillä on selvästikin joukko yhden henkilön tietoja. Mikäli yhden henkilön tietoa pidetään yhtenä yksikkönä (tietueena), on koko tietorakenne taulukko henkilöiden tiedoista. Taulukko voidaan tarvittaessa toteuttaa myös lineaarisena listana tai jopa puurakenteena. Mikäli kyseessä on pieni rekisteri, mahtuu koko tietorakenne ohjelman ajon aikana muistiin.
Missä tiedot talletetaan kun ohjelma ei ole käynnissä? Tietenkin levyllä tiedostona. Minkä tyyppisenä tiedostona? Tiedoston tyyppinä voisi olla binäärinen tiedosto alkioina henkilötietueet. Tällaisen tiedoston käsittely hätätapauksessa on kuitenkin vaikeata. Varmempi tapa on tallettaa tiedot tekstitiedostoksi, jota tarvittaessa voidaan käsitellä millä tahansa tekstinkäsittelyohjelmalla. Tällöin on lisäksi usein mahdollista käsitellä tiedostoa taulukkolaskentaohjelmalla tai tietokantaohjelmalla ja näin joitakin harvinaisia toimintoja voidaan suorittaa rekisterille vaikkei niitä olisi alunperin edes älytty laittaa ohjelmaan mukaan.
Minkälainen tekstitiedosto? Ehkäpä yhden henkilön tiedot yhdellä rivillä? Miten yhden henkilön eri tiedot erotetaan toisistaan? Mahdollisuuksia on lähinnä kaksi: erotinmerkki tai tietty sarake. Valitaan erotinmerkki. Usein on mukavaa lisäksi laittaa joitakin huomautuksia eli kommentteja tiedostoon. Siis talletustiedoston muoto voisi olla vaikkapa seuraava:
kelmit.dat - ensimmäinen ehdotus tiedostoksi
; Tässä tiedostossa on KELMIT Ry:n jäsentiedot
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi|sotu|katuosoite|postinumero|postiosoite
;|kotipuhelin|työpuhelin|autopuhelin|liittymisvuosi|jmaksu|lisätietoja
Ankka Aku|010245-123U|Ankkakuja 6|12345|ANKKALINNA|12-12324|||1991|50|velkaa Roopelle
Susi Sepe|020347-123T||12555|Takametsä||||1990|50|jäsen myös kelmien kerhossa
Ponteva Veli|030455-3333||12555|Takametsä||||50|1989|
Tällaisenaan tiedosto on varsin suttuinen luettavaksi. Vaikka
valitsimmekin erotinmerkin erottamaan tietoja toisistaan, voimme silti kirjoittaa vastaavat tiedot allekkain sopimalla, ettei loppuvälilyönneillä ole merkitystä.
kelmit.dat - sarakkeet linjaan
; Tässä tiedostossa on KELMIT Ry:n jäsentiedot
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |sotu |katuosoite|postinumero|postiosoite|kotipuhelin|työpuhelin|.
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
Susi Sepe |020347-123T| |12555 |Takametsä | | |
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
Nyt tiedostoa on helpompi lukea ja tyhjien kenttien jättäminen ei ole vaikeaa. Tiedosto vie kuitenkin levyltä enemmän tilaa kuin ensimmäinen versio. Lisäksi yhden henkilön tiedot eivät mahdu kerralla näyttöön. Onneksi kuitenkin lähes kaikki nykyiset tekstieditorit suostuvat rullaamaan näyttöä myös sivusuunnassa. Mikäli saman henkilön tietoja jaettaisiin eri riveille, tarvitsisi meidän valita vielä tietueen loppumerkki (nytkin se on valittu: rivinvaihto).
2.4 Käyttöohje tai käyttöliittymä
Jatkosuunnittelu on ehkä helpointa tehdä suunnittelemalla ohjelman toimintaa käyttöohjeen tai käyttöliittymän tavoin. Erityisesti graafisella käyttöliittymällä varustettuja ohjelmia suunnitellaan nykytyökaluilla nimenomaan "piirtämällä" käyttäjälle näkyvä käyttöliittymän osa. Tähän osaan sitten lisätään heti tai jälkeenpäin itse toiminnallisuus. Tällaisia työkaluja on esimerkiksi Delphi, Visual Basic ja myös muiden ohjelmointikielten resurssityökalut.
Graafisen käyttöliittymän suunnittelu ja toteutus on kuitenkin jo edistyneempää - ja erityisesti laiteriippuvaa - puuhaa, joten tällä kurssilla täytyy tyytyä "vanhanaikaiseen" keskustelevaan tekstikäyttöliittymään. Jos koodausvaiheessa riittävästi erotetaan käyttöliittymään liittyvä koodi tietorakennetta ylläpitävästä koodista, voidaan ohjelma kohtuullisella työllä muuttaa myös graafisessa käyttöliittymässä toimivaksi.
Suunnittelussa toimitaan käyttäjän ja helppokäyttöisyyden (= myös nopea käyttö, ei aina välttämättä hiiri) ehdoilla.
Ohjelmassa on kahdenlaisia vastauksia. Toisiin riittää painaa pelkkä yksi kirjain tai numero (kuten menut ja K/e tyyppiset valinnat). Mikäli vastaukseen on mahdollista kirjoittaa enemmän kuin yksi merkki, pitää vastaus lopettaa [RET]–näppäimen painamisella (Return, Enter). Jatkossa hoputteilla (prompt) on seuraavat merkitykset
: odotetaan pelkkää yhtä merkkiä. Mikäli vaihtoehdot on lueteltu ja jokin niistä on isolla kirjaimella, valitaan tämä painettaessa [RET]–näppäintä.
> odotetaan 0 – useata merkkiä ja [RET]–näppäintä. Mikäli hoputteen edessä on suluissa jokin arvo, tulee tämä arvo vastauksen arvoksi painettaessa pelkkää [RET]–näppäintä. Mikäli oletusvastaus on epätyhjä ja halutaan antaa vastaukseksi tyhjä merkkijono, painetaan välilyönti ja [RET].
Ohjelman päävalintaan päästään usein vastaamalla pelkkä [RET] uuden kierroksen alussa tai painamalla q[RET] missä tahansa ohjelman kohdassa.
Kun ohjelma käynnistyy, tulostuu näyttöön:
###################################
# J Ä S E N R E K I S T E R I #
# versio 9.95 #
# Hannu Hanhi #
###################################
Tällä ohjelmalla ylläpidetään kerhon jäsenrekisteriä.
Anna kerhon nimi >_
Kerhon tiedot on talletettu vaikkapa tiedostoon nimi.DAT. Näin voimme ylläpitää samalla ohjelmalla useiden eri kerhojen tietoja. Mitäpä jos tiedostoa ei ole? Tällöin voi syynä olla kirjoitusvirhe tai se, ettei rekisteriä ole vielä edes aloitettu! Miten ohjelman tulee tällöin menetellä?
Tällä ohjelmalla ylläpidetään kerhon jäsenrekisteriä.
Anna kerhon nimi>KERMIT[RET]
Kerhon KERMIT tietoja ei ole!
Luodaanko tiedot (K/e):e
Anna kerhon nimi>KELMIT[RET]
Odota hetki, luetaan tietoja...
Jäsenrekisteri
==============
Kerhossa KELMIT on 52 jäsentä.
Valitse:
0 = lopetus
1 = lisää uusi jäsen
2 = etsi jäsenen tiedot
3 = tulosteet
4 = tietojen korjailu
:_```
Edellä on edetty siihen saakka, kunnes ohjelmassa on päädytty päävalikkoon (main menu). Seuraavaksi voimme lähteä tarkastelemaan eri alakohtien toimintaa. Näissä eri erikoistapaukset on otettava huomioon:
2.4.1 Lisää uusi jäsen
1. Uuden jäsenen lisäys
=======================
Jäseniä on nyt 52.
Anna uusi nimi muodossa sukunimi etunimi etunimi
Jäsenen nimi () >Ankka Aku[RET]
Rekisterissä on jo jäsen Aku Ankka! Mikäli haluat muuttaa
hänen tietojaan, valitse tietojen korjailu päävalikosta!
Jäsenen nimi () >ANKKA TUPU[RET]
Sotu () >012356-1257[RET]
Sotu mieletön tai tarkistusmerkki väärin (N)!
Sotu (012356-125N) >010356-125J[RET]
Katuosoite () >Ankkakuja 6[RET]
Postinumero (12345) >[RET]
Postiosoite (ANKKALINNA) >[RET]
Kotipuhelin () >12-12324[RET]
Työpuhelin () >[RET]
Autopuhelin () >[RET]
Liittymisvuosi (91) >91[RET]
Jäsenmaksu mk (50) >10[RET]
Lisätietoja () >Aku Ankan veljenpoika[RET]
Lisätäänkö
Ankka Tupu 010356-125J
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk.
Aku Ankan veljenpoika
:k
Jäseniä on nyt 53.
Anna uusi nimi muodossa sukunimi etunimi etunimi
Jäsenen nimi () >[RET]
Jäsenrekisteri
==============
Kerhossa KELMIT on 53 jäsentä.
Valitse:
0 = lopetus
1 = lisää uusi jäsen
2 = etsi jäsenen tiedot
...
Edellä on suluissa esitetty oletusarvo, joka tulee kentän arvoksi, mikäli painetaan pelkkä [RET]. Mikäli oletusarvon tilalle halutaan vaikkapa tyhjä merkkijono, vastataan välilyönti ja [RET]. Näin voidaan nopeuttaa tiettyjen samojen asioiden syöttöä. Oletusarvo voidaan antaa joko ohjelmasta väkisin (kuten tyhjä sotuksi) tai se voidaan ottaa edellisen syötön perusteella.
2.4.2 Etsi jäsenen tiedot
Tietoa voidaan hakea usealla eri tavalla. Voidaan haluta etsiä nimellä, osoitteella tai jopa puhelinnumerolla. Myös lisätietokentästä voidaan haluta etsiä tiettyä sanaa.
Siis aluksi pitää valita minkä kentän mukaan haetaan. Kun kenttä on selvitetty, voi olla mielekästä voida käyttää myös jokerimerkkejä (esim. ? ja *, vrt. MS-DOS.
2. Etsi jäsenen tiedot
======================
Nykyinen henkilö:
Ankka Lupu 010356-127L
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk.
Aku Ankan veljenpoika, sudenpentu
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.) :?
1 = nimi
2 = sotu
3 = katuosoite
4 = postinumero
5 = postiosoite
6 = kotipuhelin
7 = työpuhelin
8 = autopuhelin
9 = liittymisvuosi
A = jäsenmaksu mk
B = lisätietoja
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.) :1
Jäsenen nimi (Ankka Lupu) >*ANKKA*[RET]
Tähän täsmää 3 jäsentä:
Ankka Aku 010245-123U
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 50 mk.
velkaa Roopelle
Lisää (K/e):[RET]
Ankka Lupu 010356-127L
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk.
Aku Ankan veljenpoika, sudenpentu
Lisää: (K/e):e
2. Etsi jäsenen tiedot
======================
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.) :[RET]
Korjailua ja muuta varten on mukavaa, että viimeksi löydetyn henkilön tiedot jäävät muistiin. Näin esimerkiksi korjailu tai poisto voidaan tehdä suoraan tälle henkilölle.
2.4.3 Tulosteet
Tulosteita varten voisi tulla menu siitä, mitä tulostetaan. Lisäksi voisi olla kysymys siitä, millä ehdoilla tulostus tehdään (jäsenmaksu maksamatta jne.) sekä tulostusjärjestys. Tulostusjärjestys on tärkeä, sillä esimerkiksi postitusta varten lehdet yms. pitää lajitella postin antamien ohjeiden mukaisesti postinumeroittain nippuihin, jotka sitten menevät tietylle postialueelle. Jäsenrekisteriä varten taas aakkosjärjestetty lista on kätevin.
Tulostuskohdan suunnittelu jätetään lukijalle harjoitustehtäväksi. Tulosteiden ulkonäkö kannattaa kuitenkin suunnitella tarkasti, koska tämä on muille kerholaisille näkyvin osa ohjelmastamme!
Tehtävä 2.1 Tulosteet
Suunnittele tulostusmenu ja kunkin kohdan alta mahdollisesti saatavat kysymykset sekä tulosteiden ulkonäkö.
2.4.4 Tietojen korjailu
4. Tietojen korjailu
=====================
Korjailtava henkilö:
Ankka Lupu 010356-127L
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk.
Aku Ankan veljenpoika, sudenpentu
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.)
tai poisto (P) tai korjailu (K) :1
Jäsenen nimi (Ankka Lupu) >*aku*[RET]
Tähän täsmää 1 jäsentä:
Ankka Aku 010245-123U
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 50 mk.
velkaa Roopelle
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.)
tai poisto (P) tai korjailu (K) :p
Haluatko todellakin poistaa jäsenen Ankka Aku (k/E):e
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.)
tai poisto (P) tai korjailu (K) :k
Jäsenen nimi (Ankka Aku) >[RET]
Sotu (010245-123U) >[RET]
Katuosoite (Ankkakuja 6) >Ankkakuja 13[RET]
Postinumero (12345) >[RET]
Postiosoite (ANKKALINNA) >[RET]
Kotipuhelin (12-12324) >12-12325[RET]
Työpuhelin () >12-33333[RET]
Autopuhelin () >[RET]
Liittymisvuosi (91) >[RET]
Jäsenmaksu mk (50) >[RET]
Lisätietoja (velkaa Roopelle) >[RET]
Ankka Aku 010245-123U
Ankkakuja 13 12345 ANKKALINNA
k: 12-12325 t: 12-33333 a:
Liittynyt -91. Jäsenmaksu 50 mk.
velkaa Roopelle
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.)
tai poisto (P) tai korjailu (K) :[RET]
Huomattakoon, että edellä LUPU ANKKA jäi vielä asumaan osoitteeseen Ankkakuja 6. Tietysti ohjelma voitaisiin tehdä myös siten, että joidenkin tiettyjen arvojen muuttaminen muuttaisi haluttaessa myös muita (esim. kirjoitettu sama osoite).
2.4.5 Lopetus
Ohjelman lopetuksessa tulee huolehtia siitä, että ohjelman aikana mahdollisesti rekisteriin tehdyt muutokset tulevat talletetuksi. Tämä voidaan tehdä automaattisesti tai talletus voidaan varmistaa käyttäjältä. Automaattisen talletuksen tapauksessa alkuperäinen tiedosto on ehkä syytä tallettaa eri nimelle.
###################################
# J Ä S E N R E K I S T E R I #
# versio 9.95 #
# Hannu Hanhi #
###################################
Tiedot talletettu tiedostoon KELMIT.DAT
Vanhat tiedot tiedostossa KELMIT.BAK
KIITOS KÄYTÖSTÄ JA TERVETULOA UUDELLEEN
2.5 Käyttöohjeen testaus
Kirjoitettu käyttöohje kannattaa antaa jollekin luotettavalle henkilölle "apinatestiin". Onko toimintoja riittävästi? Keksitäänkö erikoistapauksia, joista saattaisi seurata hankaluuksia?
Miksi jäsenen tietojen etsintä ja korjailu ovat eri paikoissa? Turvallisuussyistä!
2.5.1 Testaus
Annetaanpa käyttöohje vaikkapa kerhon sihteerin testattavaksi. Hänelle on juuri vuodenvaihteessa tullut iso kasa pankkisiirtokuitteja uuden vuoden jäsenmaksujen maksamisesta. Niinpä seuraakin ehkä keskustelu:
Sihteeri: Miten jäsenmaksut korjataan. Ensin kaikki viimevuotiset pitää poistaa. Jaa ehkä jos joku on ainaisjäsen, niin hänen merkintäänsä ei poisteta. No muilta kuitenkin. Sitten pitäisi nämä kuitit saada naputeltua sinne!
Ohjelmoija: No poisto ensiksi. Valitse 4 eli korjailu. Sitten valitse 1 Jäsenen nimi ja vastaa ensimmäisen jäsenen nimi. Kun se löytyy, niin paina K niin kuin korjailu ja sitten vain RET kunnes olet jäsenmaksun kohdalla ja sitten välilyönti ja RET jäsenmaksuun. Sitten taas 1 Jäsenen nimi jne. Helppoa!
Sihteeri: Tuohan vie AINAKIN vuoden!!! MINÄ KÄYTÄN EDELLEEN VANHAA käsikirjanpitoani. Tästä vaan kumilla vanha pois ja uusi tieto tilalle...
Ohjelmoija: Öh, tuota mutta kun minä...
2.5.2 Korjaus
Siis parasta mennä takaisin miettimään. Onneksi itse ohjelmaa ei ole ehditty kirjoittamaan. Jäsenmaksut?
Miten jäsenmaksukentän käyttö? Ehkä tämä kenttä pitää säilyttää tiedoksi siitä, paljonko henkilön pitäisi maksaa jäsenmaksua. Tarvitsemme siis uuden kentän:
Maksettu maksu (10) >
Ainais-– ja kunniajäsenet hoidetaan siten, että heidän jäsenmaksukseen, joka pitäisi maksaa, annetaan vaikkapa 0 mk.
Lisäksi meille ilmiselvät asiat eivät aina olleetkaan sihteerille selviä. Ohjelmaamme täytynee lisätä myös avustus.
Lisätään kaksi uutta valintaa päävalikkoon:
...
Valitse:
? = avustus
0 = lopetus
1 = lisää uusi jäsen
2 = etsi jäsenen tiedot
3 = tulosteet
4 = tietojen korjailu
5 = päivitä jäsenmaksuja
:5
5. Päivitä jäsenmaksuja
=======================
Valitse:
? = avustus
0 = takaisin päävalintaan
1 = poista kaikki edellisen vuoden maksut
2 = kysy maksettu maksu nimen mukaan
:1
Poistetaan kaikki edellisen vuoden jäsenmaksut!
Poistetaan siis kaikki maksetut jäsenmaksut (K/e):K
Odota hetki... Jäsenmaksut poistettu!
Valitse:
? = avustus
0 = takaisin päävalintaan
1 = poista kaikki edellisen vuoden maksut
2 = kysy maksettu maksu nimen mukaan
:2
Muutetaan maksettuja jäsenmaksuja!
==================================
Jos haluat kaikki jäsenet, anna *
Kysely loppuu, mikäli annat maksuksi q
Jäsenen nimi () >*[RET]
Ankka Aku 010245-123U
Ankkakuja 13 12345 ANKKALINNA
k: 12-12325 t: 12-33333 a:
Liittynyt -91. Jäsenmaksu 50 mk. Maksettu mk.
velkaa Roopelle
Maksettu maksu mk () >[RET]
Ankka Lupu 010356-127L
c/o Aku Ankka (Ankkakuja 13 12345 ANKKALINNA)
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk. Maksettu mk.
Aku Ankan veljenpoika, sudenpentu
Maksettu maksu () >10[RET]
... Jatkuu näin kunnes kaikki käyty läpi ...
... Tai vastataan ...
Maksettu maksu mk () >q[RET]
Jäsenen nimi () >*aku*[RET]
Ankka Aku 010245-123U
Ankkakuja 13 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 50 mk. Maksettu mk.
velkaa Roopelle
Maksettu maksu () >40[RET]
Pitäisi maksaa 50 mk ja on maksanut 40 mk.
Onko oikein (K/e):e
Maksettu maksu mk () >50[RET]
Jäsenen nimi () >[RET]
Valitse:
? = avustus
0 = takaisin päävalintaan
1 = poista kaikki edellisen vuoden maksut
2 = kysy maksettu maksu nimen mukaan
:0
...
2.5.3 Muita korjauksia
Käyttäjällä saattaa tulla vaikeuksia myös kesken jonkin kysymyksen. Tällöin voisi olla hyvä, että käyttäjä voi painaa ?–merkkiä ja saada avustusta siitä, mitä tähän kohti pitäisi syöttää (sisältöriippuva avustus, context sensitive help).
Jäsenmaksun päivittämisessä jokerimerkin käyttö tuntui varsin kätevältä tavalta saada joko yksi tai useampi henkilö (tai vaikkapa kaikki) päivitettäväksi.
Samaa ajatusta voitaisiin täydentää myös muuhun päivittämiseen. Muutetaankin korjailussa esiintynyt kysymys
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.)
tai poisto (P) tai korjailu (K) :
muotoon
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.),
poisto (P), korjailu (K), seuraava (+), edellinen (-):
silloin, kun hakuehtoon täsmääviä on löytynyt useita. Tekstit seuraava ja edellinen voidaan varmaankin jättää pois, jos seuraavaa tai edellistä ei ole.
Kenttälistaan hakuehdossa voitaisiin lisätä lisäkohta, jossa kaikille kentille voidaan antaa ehto (ja/tai):
1 = nimi
...
9 = liittymisvuosi
A = jäsenmaksu mk
B = maksettu maksu mk
C = lisätietoja
& = JA ehto kaikille kentille
| = TAI ehto kaikille kentille
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.):&
Kirjoita niihin kenttiin ehto, joiden mukaan haluat etsiä.
== tarkoittaa, että kentän TÄYTYY olla tyhjä.
Jäsenen nimi () >*ankka*[RET]
Sotu () >[RET]
Katuosoite () >[RET]
Postinumero () >[RET]
Postiosoite () >[RET]
Kotipuhelin () >[RET]
Työpuhelin () >[RET]
Autopuhelin () >[RET]
Liittymisvuosi () >[RET]
Jäsenmaksu mk () >[RET]
Maksettu maksu mk () >==[RET]
Lisätietoja () >[RET]
Tähän täsmää 1 jäsentä:
Ankka Tupu 010356-125J
Ankkakuja 6 12345 ANKKALINNA
k: 12-12324 t: a:
Liittynyt -91. Jäsenmaksu 10 mk. Maksettu mk.
Aku Ankan veljenpoika
...
Edellä siis etsittiin kaikkia niitä Ankkoja, joilla maksettu maksu on tyhjä. Näin sihteeri voisi aina tutkia kenellä maksut on maksamatta (tässä tapauksessa erityisesti Ankoista). Hakuehtoihin voitaisiin vielä liittää epäyhtälöt:
< <= > >= == !=
Siis hakuehto voisi olla esimerkiksi
Jäsenen nimi () >!=*ankka*[RET]
Sotu () >[RET]
...
Jäsenmaksu mk () ><30[RET]
Maksettu maksu mk () >==[RET]
Lisätietoja () >[RET]
Eli etsitään niitä jäseniä, joiden nimi EI OLE *ankka* ja joiden jäsenmaksu on alle 30 sekä maksettu maksu on tyhjä.
Samalla tietojen etsimisessä kysymys
Lisää (K/e):[RET]
voitaisiin muuttaa selaukseksi:
Valitse kenttä jonka mukaan etsitään (?=kenttälista uudel.),
seuraava (+), edellinen (-):
Kerhon nimi saattaa olla varsin pitkä. Sen antaminen aina ohjelman käynnistämisen yhteydessä voi olla työlästä. Siksi käynnistämisessä kannattaakin antaa vain lyhenne, jolla tiedosto on talletettu. Varsinainen nimi täytyy tallettaa jonnekin muualle. Minne?
Nimi voitaisiin tallettaa vaikkapa jäsenrekisteritiedoston ensimmäiselle riville:
kelmit.dat - kerhon nimikin talteen
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
Susi Sepe |020347-123T| |12555 |Takametsä | | |
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
Jos halutaan vielä suurempaa yhteensopivuutta valmiiden tietokantaohjelmien kanssa, voidaan kerhon nimi tallettaa erilliseen tiedostoon muiden kerhoon liittyvien lisätietojen kanssa. Esimerkiksi jos jäsenet on tiedostossa kelmit.dat, voisi lisätiedot olla tiedostossa kelmit.opt. Tällöin myös kommentit (;) kannattaa jättää pois tiedostosta ja tiedoston ensimmäinen rivi on kenttien nimiä kuvaava rivi.
kelmit.dat - yhteensopivuus muihin ohjelmiin
nimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
Susi Sepe |020347-123T| |12555 |Takametsä | | |
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
kelmit.opt - yleiset tiedot tänne
[Tiedot]
nimi=Kelmien kerho ry
maxjaseniä=100
2.5.4 Uusi testaus
Koska käyttöohjeemme on tietenkin kirjoitettu tekstinkäsittelyohjelmalla (tai ohjelmaeditorilla), on muutokset helppo tehdä. Menemme uudelleen sihteerin luokse uudistetun käyttöohjeen kanssa ja pyydämme jos hän armeliaasti vielä kerran suostuisi katsomaan sitä.
Paranneltavaa saattaa löytyä lisää nyt kun kiinnostus ehkä herää. Korjaamme selvät kohdat mutta joistakin asioista täytyy neuvotella ja ainakin tinkiä ettei niitä ehkä toteuteta ohjelman ensimmäiseen versioon.
Yksi selvä ongelma ilmenee. Entä jos lisäyksessä todella on kaksi samannimistä jäsentä? Homma täytynee korjata seuraavasti:
Jäsenen nimi () >Ankka Aku[RET]
Rekisterissä on jo jäsen Aku Ankka! Mikäli haluat muuttaa
hänen tietojaan, valitse tietojen korjailu päävalikosta!
Lisätäänkö sama nimi (l) vai kysytäänkö toinen nimi (T):T
Jäsenen nimi () >ANKKA TUPU[RET]
Jatkamme testauskierrosta kunnes potentiaaliset käyttäjät on saatu tyytyväiseksi.
2.6 Tarvittavien algoritmien hahmottaminen
Nyt olemme selvillä ohjelman toiminnasta. Edellisestä käyttöohjeesta voimme etsiä mitä työkaluja (aliohjelmia) tarvitsemme ohjelman toteutuksessa. Ainakin seuraavat tulevat helposti mieleen:
2.6.1 Ylemmän tason aliohjelmat
- tiedoston lukeminen
- tiedoston tallettaminen
- henkilön tietojen kysyminen päätteeltä
- tiedoston lajittelu haluttuun järjestykseen
- tiedon etsiminen tiedostosta tietyllä hakuehdolla
- uuden henkilön lisääminen tiedostoon
- henkilön poistaminen tiedostosta
2.6.2 Alemman tason aliohjelmat
Mikäli tutkimme yo. palasia tarkemmin, tarvitsemme ehkä seuraavia pienempiä ohjelman palasia (apualiohjelmia):
- yhden merkin vastauksen lukeminen mahdollisen oletusarvon kanssa
- merkkijonon lukeminen päätteeltä siten, että sille voidaan jättää oletusarvo
- pitkän merkkijonon pilkkominen osamerkkijonoihin annetun merkin kohdalta
- loppuvälilyöntien poistaminen merkkijonosta
- isojen ja pienien kirjainten muuttaminen merkkijonossa esimerkiksi:
- sotun oikeellisuuden tarkistus
- ovatko merkkijonot "*aku*" ja "AKU ANKKA" samoja?
2.6.3 Ohjelman yhteiset osat
Kannattaa myös etsiä onko ohjelmassa samanlaisina toistuvia osia. Edellä meillä selvästikin tiedon haku on samanlainen sekä etsimis- että korjailukohdassa. Samoin henkilön tietojen luku on samanlainen sekä lisäämisessä että korjailussa. Yhden henkilön tietojen tulostaminen näytölle esiintyy useammassa kohdassa.
Mikäli löytyy likipitäen samanlaisuuksia, kannattaa harkita voidaanko ne käyttämisen yksinkertaistamiseksi ja/tai ohjelmoinnin helpottamiseksi muuttaa samanlaisiksi. Mikäli voidaan, korjataan äkkiä käyttöohjetta tältä osin.
2.7 Ikkunoinnit ja muut hienostelut
Ohjelma voitaisiin suunnitella myös nykyaikaisen ikkunoidusti toimivan käyttöliittymän mukaiseksi. Kuten edellä todettiin, tämä on kuitenkin ohjelmoinnin oppimisen tässä vaiheessa liian työlästä ja tähän paneudutaan vasta myöhemmillä kursseilla.
Tässäkin ohjelmassa korjailua voitaisiin parantaa siten, että meillä olisi käytössä aliohjelma, jolle korjailtava merkkijono vietäisiin parametrinä. Palautuksena tulisi korjattu merkkijono ja korjailun aikana toimisivat nuolinäppäimet yms. hienoudet. Tosin suurtakaan muutosta ohjelmaan ei tarvitse tehdä mikäli em. aliohjelman tilalla käytämme aluksi vain merkkijonon lukemiseen kykenevää aliohjelmaa. Myöhemmin tätä voitaisiin parantaa.
Menut voisivat olla nykytyyliin alasvetovalikoita, mutta aluksi meille riittää vallan hyvin käyttöohjeessa esitetyn kaltainen "näyttö tyhjäksi ja uusi menu ruutuun" –tyyli. Myös hiirtä voitaisiin käyttää, mutta jälleen ohjelmointityö kasvaisi vastaavasti.
Hakuehdot voisivat olla monipuolisempia ja niille voitaisiin yrittää keksiä jokin hienompi menetelmä. Esimerkiksi tarvitsisi hakea seuraavilla ehdoilla
nimi on "*ankka*" tai jäsenmaksu on "<50"
postiosoite "ankka*" ja lisätiedoissa "*sudenpentu*"
Tätä varten hakemiseen voitaisiin kehitellä vaikkapa seuraavanlainen kieli:
Hakuehto >(nimi=*ankka*) || (jmaksu<50)[RET]
...
Hakuehto >(postiosoite=ankka*) && (lisätiedot=*sudenpentu*)[RET]
Saattaa tulla myös tarve lisätä uusia kenttiä henkilön tietoihin. Tämä on hallittavissa huolellisella ohjelman suunnittelulla, jossa käytettyjen kenttien määrä ja nimet esiintyisivät vain yhdessä paikassa ohjelmaa.
Kun näitä haluttuja lisäominaisuuksia silmäillään, ei ole ihme että on kehitetty tietokantaohjelmia; eli halutut ominaisuudet tarvitaan lähes jokaisessa vastaavassa sovelluksessa. Hieman muuttamalla oman ohjelmamme toimintaa, voisimme saada siitäkin yleiskäyttöisen tietokantaohjelman, mutta jätettäköön tämäkin lukijalle harjoitustehtäväksi.
Etsimisissä voisi olla oletuksena lisätä * kummallekin puolelle etsittävää jonoa, jolloin kun nimeen vastataan
aku
täydennetään tämä muotoon
*AKU*
ja näin löydetään Ankka Aku.
2.8 Koodaus ohjelmointikielelle
Seuraava vaihe olisi suunnitelman koodaaminen valitulle ohjelmointikielelle. Voisimme kirjoittaa aluksi löytämiämme alimman tason aliohjelmia (BOTTOM-UP–suunnittelu) ja testata ne toimiviksi. Voisimme myös kirjoittaa pääohjelman ja tyhjiä aliohjelmia testataksemme ohjelman rungon (TOP-DOWN). Menujen alavalinnat voitaisiin laittaa vain sanomaan:
TOIMINTAA EI OLE VIELÄ TOTEUTETTU!
Emme kuitenkaan osaa vielä riittävästi ohjelmointikieltä, jotta voisimme aloittaa koodauksen. Huomattakoon, ettei yllä olevassa suunnitelmassa ole missään kohti vedottu käytettävään ohjelmointikieleen. Palaamme myöhemmin takaisin ohjelman osien koodaamiseen.
2.9 Varautuminen tulevaan, eli relaatiotietomalli
Vaikka sihteerimme ei juuri nyt huomannutkaan, saattaa hän tulevaisuudessa esimerkiksi kysyä miten rekisterillä pidettäisiin yllä tietoja jäsenten harrastuksista. Mietitäänpä?
Ensin miten harrastukset muuttaisivat tiedostomuotoamme?
2.9.1 Kaikki samassa tietueessa
Eräs mahdollisuus olisi lisätä kunkin rivin loppuun jollakin erotinmerkillä harrastukset:
kelmit.dat - harrasteet samalle riville
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |sotu |...|harrastukset
Ankka Aku |010245-123U|...|kalastus,laiskottelu,työn pakoilu
Susi Sepe |020347-123T|...|possujen jahtaaminen,kelmien kerho
Ponteva Veli |030455-3333|...|susiansojen rakentaminen
Ratkaisu toimisi tietyissä erityistapauksissa. Ongelmia tulisi esimerkiksi jos pitäisi kuhunkin harrastukseen liittää esimerkiksi harrastuksen aloitusvuosi, viikoittain harrastukseen käytetty tuntimäärä jne.
2.9.2 Erimalliset tietueet
Edellinen ongelma ratkeaisi esimerkiksi laittamalla henkilön tietojen rivin perään jollakin tavalla eroavia rivejä, joilla harrastuksen on lueteltu:
kelmit.dat - harrasteet omalle riville
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
- kalastus | 1955 | 20
- laiskottelu | 1950 | 20
- työn pakoilu | 1952 | 40
Susi Sepe |020347-123T| |12555 |Takametsä | | |
- possujen jahtaaminen | 1954 | 20
- kelmien kerho | 1962 | 2
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
- susiansojen rakentaminen | 1956 | 15
Ratkaisu olisi aivan hyvä ja tämän ratkaisun valitsemiseksi meidän ei tarvitsisi tehdä mitään muutoksia tiedostomuotoomme vielä tässä vaiheessa.
Huono puoli on kuitenkin se, että tämän muotoisen tiedoston siirrettävyys muihin järjestelmiin on varsin huono.
2.9.3 Relaatiomalli
Suurin osa tämän hetken valmiista järjestelmistä käyttää relaatiotietokantamallia. Tämä tarkoittaa sitä, että koko tietokanta koostuu pienistä tauluista, jossa kukin rivi (=tietue) on samaa muotoa. Eri taulujen välillä tiedot yhdistetään yksikäsitteisten avainkenttien avulla. Meidän esimerkissämme kelmit.dat olisi yksi tällaisen taulu ja sosiaaliturvatunnus kelpaisi yhdistäväksi avaimeksi (relaatioksi).
Kuitenkin sosiaaliturvatunnus on varsin pitkä kirjoittaa ja välttämättä sitä ei saada kaikilta. Jos tällainen pelko on olemassa, täytyy avain luoda itse. Itse ohjelman käyttäjän ei tarvitse tietää mitään tästä uudesta muutoksesta, vaan ohjelma voi itse generoida avaimet ja käyttää niitä sisäisesti.
Valitaan vaikkapa juoksevasti generoituva numero. Jos jäseniä poistetaan jää ko. jäsenen numero vapaaksi eikä sitä yritetäkään enää käyttää. Uuden jäsenen numero olisi sitten aina suurin jäsenen numero +1.
kelmit.dat - relaatiokannan päätaulu
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
;id|sukunimi etunimi |sotu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
1 |Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
2 |Susi Sepe |020347-123T| |12555 |Takametsä | | |
4 |Ponteva Veli |030455-3333| |12555 |Takametsä | | |
Harrastukset kirjoitetaan toiseen tiedostoon, jossa tunnusnumerolla ilmaistaan kuka harrastaa mitäkin harrastusta.
harrastu.dat - harrasteet relaation avulla
id|harrastus |aloit |viikossa
1 |kalastus | 1955 | 20
1 |laiskottelu | 1950 | 20
1 |työn pakoilu | 1952 | 40
2 |possujen jahtaaminen | 1954 | 20
2 |kelmien kerho | 1962 | 2
4 |susiansojen rakentaminen | 1956 | 15
Nyt esimerkiksi kysymykseen "Mitä Sepe Susi harrastaa" saataisiin vastus etsimällä ensin Sepe Suden tunnus (2) tiedostosta kelmit.dat. Sitten etsittäisiin ja tulostettaisiin kaikki rivit joissa tunnus on 2 tiedostosta harrastu.dat.
Myös vastaus kysymykseen "Ketkä harrastavat laiskottelua" löytyisi suhteellisen helposti.
Tämä ratkaisu vaatii muutoksen tiedostomuotoomme jo suunnitelman tässä vaiheessa, mutta toisaalta mikäli ratkaisu valitaan, voidaan sen ansiosta lisätä jatkossa vastaavia "monimutkaisia" kenttiä rajattomasti tekemällä kullekin oma "taulu".
Valitsemmekin siis tämän ratkaisun, eli annamme kullekin jäsenelle tunnusnumeron heti alusta pitäen. Itse ohjelman käyttösuunnitelmaan ei tässä vaiheessa tarvita muutoksia.
Tehtävä 2.2 Ketkä harrastavat?
Kirjoita algoritmi joka relaatiomallin tapauksessa vastaa kysymykseen "Ketkä harrastavat harrastusta X".
Tehtävä 2.3 Mikä on tilaa säästävin talletusmuoto
Laske mikä edellä esitetyistä kolmesta vaihtoehdosta on tilaa säästävin kun rivinvaihtomerkin lasketaan vievän yhden merkin verran tilaa ja välilyönnit "unohdetaan".
3. Yksinkertainen tulkkiohjelma
3.1 Tehtävän tarkennus
Olkoon meillä tehtävänä tehdä satunnaisen matkaajan käyttöön soveltuva tulkkiohjelma, joka kääntää sana kerrallaan. Mitään valmiita lauseita ja taivutusmuotoja hallitsevaa ohjelmaa emme edes yritä tehdä. Tavoitteena on siis sanakirjan yksinkertaistettu elektroninen versio.
Ohjelman yksi tavoite täytyisi olla sanaston helppo täydentäminen. Sanasto on jälleen luonnollisinta säilyttää tiedostossa. Mikäli sanasto on normaali tekstitiedosto, on sen täydentäminen tekstinkäsittelyohjelmalla helppoa. Nykyisin JOKAISEN on osattava käyttää jotakin tekstinkäsittelyohjelmaa!
Tehtäväksemme jää siis sanastotiedoston muodon suunnittelu sekä itse ohjelman toimintojen suunnittelu.
3.2 Tietorakenteet ja tiedostot
Sanasto voisi näyttää vaikkapa seuraavalta:
Suomi Ruotsi Englanti
;––––––––––––––––––––––––––––––––––––––––––
minä jag i
sinä du you
hän han he
Tässä tuleekin nyt ongelmia. Tarvitaan synonyymejä. Esimerkiksi "hän" täytyisi voida sanoa "he" tai "she" suvun mukaan. Miten esittäisimme synonyymit? Tarvitaan ehkä myös kommentteja! Ratkaisu voisi olla seuraava:
Suomi | Ruotsi | Englanti
;-----------------------------------------------------------
minä | jag | i
sinä | du | you (yks.)
hän | han (mask.) | he (mask.)
hän | hon (fem.) | she (fem.)
me | vi | we
te | ni | you (mon.)
he | dom | they
hernekeitto | ärtsoppa | pea soup
sukellusvene | ubåt | submarine
suklaa | chokolade | chocolate
sukka | socka (lyhyt) | sock (lyhyt)
sukka | strumpa (pitkä)| stocking (pitkä)
Itse ohjelman on sitten huolehdittava siitä, että synonyymien tapauksessa myös synonyymit tulostetaan.
Ohjelma lukee käynnistyessään koko tiedoston. Tiedoston 1. riviltä selviävät käytettyjen kielten nimet. Nimessä iso kirjain tarkoittaa kirjainta, jolla kielen nimi voidaan lyhentää.
Tehtävä 3.4 Kielen lisääminen
Lisää sanastoon Savon kieli!
3.3 Käyttöohje
Ohjelman suunnittelu on tässäkin tapauksessa parasta tehdä käyttöohjeen avulla. Suunnittelemme aluksi kuitenkin ohjelmalta halutut toiminnot.
Satunnaisen matkaajan tarpeet ovat varmaankin seuraavat:
- Hän istuu ruokapöydässä Ruotsissa ja lukee menua. Pitäisi saada selville mitä "ärtsoppa" tarkoittaa.
- Hän haluaa "hernekeittoa". Mitä se on ruotsiksi?
- Hän näkee sukellusveneen Tukholman rannikolla ja haluaa kertoa tästä vieressään seisovalle merisotilaalle.
- Ruotsalainen merisotilas ei hetkauta korvaansa "UBÅT–UBÅT" –huudoille. Takana seisoo englantilainen kenraali ja satunnainen matkaaja haluaisi tiedottaa näkemästään myös hänelle.
Suomalaisen ollessa Ruotsissa tarvitaan käännöksiä ruotsi–suomi ja suomi–ruotsi. Siis molemmat kielet tulee voida valita.
Hätätilassa pitää suhteellisen helposti päästä käsiksi myös muunkielisiin sanoihin.
Periaatteessa, mikäli kielet on jo valittu, riittäisi käyttö tyyliin
Sana >ärtsoppa[RET]
ruotsi: ärtsoppa = suomi: hernekeitto
Entäpä mikäli kielinä olisi suomi ja englanti:
Sana >he[RET]
suomi : he = englanti: they
englanti: he (mask.) = suomi : hän
Tämä voisi olla aivan hyvä ratkaisu. Miten kielen vaihto kävisi kätevästi (nopeasti)?
Sana >sukellusvene/s- e[RET]
suomi: sukellusvene = englanti: submarine
Voisi olla tarpeen myös saada käännös samanaikaisesti usealla eri kielellä:
Sana >sukellusvene/s- re[RET]
suomi: sukellusvene = ruotsi : ubåt
= englanti: submarine
Sovimme siis, että sanan perään kirjoitetaan kauttaviivan jälkeen mistä kielestä käännetään ja mihin. Mikäli kumpiakin kieliä on vain yksi, käännös voi olla kumpaankin suuntaan. Mikäli toista kieltä on useita kappaleita, tehdään käännös vain kuten on pyydetty. Mikäli toista kieltä ei anneta lainkaan, tarkoitetaan kaikkia mahdollisia kieliä.
Kerran annetut käännöskielet säilyvät, kunnes käännöskielet annetaan uudelleen.
Siis ohjelman toiminta voisi olla vaikkapa seuraavanlainen:
Terve! Olen Satunnaiselle matkaajalle suunniteltu Tulkki!
Sanastoni on tiedostossa SANASTO.DAT. Mikäli haluat lisätä
sanastoa, käytä tekstinkäsittelyohjelmaa sanaston
päivittämiseksi.
Avustusta saat vastaamalla ? kun sinulta kysytään sanaa.
Tulkattavina kielinä on nyt suomi- ruotsi.
Sana>?[RET]
------------------------------------------------------------------
Avustus:
Kirjoita sanakysymyksen perään sana jonka haluat kääntää.
Mikäli painat pelkän [RET], ohjelman toiminta loppuu.
Mikäli haluat vaihtaa kieliä, kirjoita sanan perään
kauttaviiva ja kielten nimien lyhennekirjaimet, joita
haluat kääntää. Mikäli haluat käännöksen samalla
usealle eri kielelle/kieleltä, kirjoita kaikki haluamasi
kielet. Mikäli haluat käännöksen kaikille mahdollisille
kielille, kirjoita vain lähtökieli.
Tunnen kielet Suomi Ruotsi Englanti.
Esimerkki:
Sana>kissa/s- e
suomi: kissa = englanti: cat
Sana>car
englanti: car = suomi: auto
Sanassa voi käyttää myös jokerimerkkejä * ja ?
Esimerkki:
Sana>suk*
------------------------------------------------------------------
Sana>hän/s- e[RET]
suomi: hän = englanti: he (mask.)
= englanti: she (fem.)
Sana>he[RET]
suomi : he = englanti: they
englanti: he (mask.) = suomi : hän
Sana>hän/s- [RET]
suomi: hän = englanti: he (mask.)
= ruotsi : han (mask.)
= englanti: she (fem.)
= ruotsi : hon (fem.)
Sana>suk*/s- r[RET]
suomi: sukellusvene = ruotsi : ubåt
sukka = ruotsi : strumpa (pitkä)
sukka = ruotsi : socka (lyhyt)
suklaa = ruotsi : chokolade
Sana>käyrätorviorkesteri[RET]
Ei löydy sanastosta!
Sana>[RET]
Kiitos käytöstä!
Satunnainen matkaaja voi olla huono/hidas konekirjoittaja. Siksi valitsimme hänelle lisäksi mahdollisuuden antaa sanoja jokerimerkkien avulla. Näin hänen ei välttämättä tarvitse osata kirjoittaa koko sanaa vaan hän voi lyhentää sen haluamastaan kohdasta. Tätä sanotaan käyttäjäystävällisyydeksi
Mikäli kieliä ei anneta lainkaan, sanaa etsittäisiin kaikista mahdollisista kielistä. Tällaisen option tarve olisi silloin, kun ei ole harmaintakaan aavistusta siitä, millä kielellä sana on kirjoitettu.
Sana >h*/- [RET]
suomi : hän = ruotsi : han (mask.)
= englanti: he (mask.)
ruotsi : han (mask.) = suomi : hän
= englanti: he (mask.)
englanti: he (mask.) = suomi : hän
= ruotsi : han (mask.)
suomi : hän = ruotsi : hon (fem.)
= englanti: she (fem.)
ruotsi : hon (fem.) = suomi : hän
= englanti: she (fem.)
suomi : he = ruotsi : dom
= englanti: they
suomi : hernekeitto = ruotsi : ärtsoppa
= englanti: pea soup
Sana >h*/- s[RET]
ruotsi : han (mask.) = suomi : hän
englanti: he (mask.) = suomi : hän
ruotsi : hon (fem.) = suomi : hän
3.4 Algoritmien hahmottaminen
3.4.1 Ylemmän tason aliohjelmat
Voimme jälleen havaita seuraavat suuremmat ohjelman kokonaisuudet:
- tiedoston lukeminen
- sanan lukeminen ja kielten erottaminen
- sanan etsiminen sanastosta sekä synonyymien tarkistus
Miten sanaa etsitään sanastosta? Mikäli sanasto ei ole järjestetty (voidaan myöhemmin järjestää mikäli sanasto kasvaa) riittää varmaankin raaka peräkkäishaku.
Miten löydetään sanaa vastaavat muunkieliset tulkinnat? Ne ovat löytyneen sanan kanssa samalla rivillä.
Käännöksenä on esim. "s–e" (suomi–englanti) ja annetaan sana he. Mitä tehdään? Etsitään suomi –sarakkeesta sanan "he" –rivi ja tulostetaan rivin englanti –sarakkeessa oleva sana. Etsitään englanti –sarakkeesta "he" –rivi ja tulostetaan rivin suomi –sarakkeessa oleva sana.
3.4.2 Alemman tason aliohjelmat
Yllättäen alemman tason aliohjelmissa tarvitaan samoja ominaisuuksia kuin jäsenrekisteriäkin tehtäessä:
- merkkijonon lukeminen päätteeltä siten, että sille voidaan jättää oletusarvo (laitetaan oletusarvoksi tyhjä)
- pitkän merkkijonon pilkkominen osamerkkijonoihin annetun merkin kohdalta
- loppuvälilyöntien poistaminen merkkijonosta
- isojen ja pienien kirjainten muuttaminen merkkijonossa. Esimerkiksi:
- ovatko merkkijonot "*aku*" ja "AKU ANKKA" samoja?
Tehtävä 3.5 Aliohjelmien käyttö
- Mikä edellisistä sopii sanan perässä olevan kommentin poistoon (suluissa oleva sana)?
- Voidaanko jotain edellistä soveltaa sanan ja kielten erottamiseen?
- Entä kun käännös (esim. s- e) on löytynyt, niin voidaanko jotain edellä olevista soveltaa mistä- ja mihin- kielten erottamiseen?
Mikäli käännösetsiminen osataan tehdä esim. s–sarakkeesta ja tulos ottaa e–sarakkeesta ohjeella "s–e", niin miten sama saataisiin tehtyä päinvastoin? Ohjeella "e–s"?. Siis mikäli käännösohjeeksi valittaisiin "s–e", niin se ehkä kannattaisi muuttaa muotoon "se–es". Näin kahden kielen tapausta ei tarvitsisi käsitellä minään erikoistapauksena. Samalla ajatuksella ohje "–" muutettaisiin muotoon "sre–sre" (eli kaikkiin tunnettuihin kieliin). Siis koko kääntäminen muodostuisi siten, että annettua sanaa etsitään ohjeessa vasemmalla annetuista sarakkeista ja mikäli se jostakin niistä löytyy, tulostettaisiin oikealla annetut sarakkeet paitsi se sarake josta sana löytyi!
Tehtävä 3.6 Erikoistapauksia
Miten muutetaan seuraavat käännöskielet:
- s
r-
Tehtävä 3.7 Algoritmi muuttamiselle
Kirjoita selkeät säännöt (=taulukko), miten käännösohje muutetaan kussakin erikoistapauksessa.
syöttömuoto muunnettu muoto
---------------------------------
s- e se- es
e- s es- se
s- r sr- rs
s- s- sre
...
- sre- sre
...
Eräs ongelma on se, miten tulos muotoillaan siistiksi, eli löydetään pisimmät mahdolliset sanat ja kielet jotta vastinsanat saadaan tulostettua siististi allekkain.
3.5 Koodaus ohjelmointikielelle
Koodaus jätetään jälleen oppimisen myöhempään vaiheeseen. Taaskaan suunnittelun alkuvaihe ei sisältänyt (eikä tarvinnut sisältää) mitään tietoa siitä, millä ohjelmointikielellä ohjelma toteutettaisiin.
4. Algoritmin suunnittelu
Mitä tässä luvussa käsitellään?
- mikä on algoritmi
- vertailu ja lajittelu
- algoritmin kompleksisuus
- alkion etsiminen joukosta
Kun ohjelman suunnittelu on edennyt siihen pisteeseen, että tarvitaan yksityiskohtaisia algoritmeja, meneekin jo monella sormi suuhun.
Vaikeudet johtuvat taas liian hankalasta ajattelutavasta ja siitä, että algoritmi yritetään nähdä osana koko ohjelmaa. Tästä ajattelutavasta on luovuttava ja osattava määritellä tarvittava algoritmi omana kokonaisuutenaan, jota suunniteltaessa sitten unohdetaan kaikki muu.
4.1 Algoritmi
Algoritmi on se joukko toimenpiteitä, joilla annettu tehtävä saadaan suoritettua. Mieti esimerkiksi miten selostat kaverillesi ohjeet juna–asemalta opiskelu–boxiisi.
Voit tietysti antaa ohjeet myös muodossa "Tule osoitteeseen Ohjelmoijankuja 17 B 5". Tämäkin on varsin hyvä algoritmi. Kaverin vain oletetaan nyt osaavan enemmän. Kaverin oletetaan osaavan etsiä katuluettelosta kadun paikka ja keksivän itse menetelmän tulla asemalta sinne.
Toisaalta kaverisi saattaa hypätä taksiin ja sanoa kuskille osoitteen. Tämä on hyvä ja helppo algoritmi, mutta ehkä liian kallis opintovelkaiselle opiskelijalle. Mikäli algoritmia tarvitaan useasti, voidaan sitä myöhemmin parantaa tyyliin:
- kävele asemalta sinne ja sinne
- hyppää bussiin se ja se
- jne
Tarkennettu algoritmi voisi olla myös seuraavanlainen:
Valitse seuraavista:
1. Kello 7- 20:
- kävele kirkkopuistoon
- nouse bussiin no 3 joka lähtee 15 yli ja 15 vaille
2. Sinulla on rahaa tai saat kimpan:
- ota taksi
3. Ei rahaa tai haluat ulkoilla:
- kävele
Edellä eri kohdat eivät ole toisiaan poissulkevia. Kello voi olla 9 ja rahaakin voi olla, mutta siitä huolimatta halutaan kävellä. Hyvässä algoritmissa ei saa olla tällaisia epätäsmällisyyksiä, vaan ohjelmoijan tulee etukäteen jo päättää mitä missäkin tapauksessa tehdään. Esimerkiksi:
1. Jos haluat ulkoilla, niin
- kävele.
2. Muuten jos kello 7- 20:
- kävele kirkkopuistoon
- nouse bussiin no 3 joka lähtee 15 yli ja 15 vaille
3. Muuten jos sinulla on rahaa tai saat kimpan:
- ota taksi
4. Muuten
- kävele
Tässäkin algoritmissa jää vielä kaverillekin tehtävää: Miten kävellään? Miten astutaan bussiin jne..
No tätä ei kaverille ehkä enää selostetakaan. Lapsille nämä asiat on aikanaan opetettu ja myöhemmin ne kuitataan yhdellä tai kahdella sanalla. Sama pätee ohjelmoinnissakin. Kerran tehtyä ei joka kerran pureksita uudelleen (vrt. aliohjelma)!
Tehtävä 4.8 Kävelyohjeet
Yritä kirjoittaa ohjeet siitä miten kävellään. Kirjoita kaverillesi kävelyohjeet (missä käännytään, ei miten kävellään) rautatieasemalta asunnollesi.
4.2 Lajittelu
Kerhon jäsenrekisteriä suunniteltaessa tulee jossakin kohtaa vastaan tilanne, jossa nimet tai osoitteet pitää pystyä lajittelemaan jollakin tavalla.
4.2.1 Nimien ja numeroiden vertaus
Jos osaamme lajitella numeroita, niin osaammeko lajitella nimiä? Vastaus on KYLLÄ. Mikä numeroiden lajittelussa on oleellista? Oleellista on tietää onko numero A pienempi kuin numero B. Miten tämä sitten soveltuu nimille? Jos osaamme päättää onko nimi A aakkosissa ennen kuin nimi B, on ongelma ratkaistu.
Verrataanpa erilaisia nimiä:
A: Kassinen Katto
B: Ankka Aku
B on ensin aakkosissa. Miksi? Koska B:n ensimmäinen kirjain (A) on ennen nimen A ensimmäistä kirjainta (K).
A: Kassinen Katto
B: Karhukopla 701107
Nytkin B on ensin. Siis miten vertaamme kahta nimeä?
Vertaamme nimiä merkki kerrallaan kunnes vastaan tulee eri–
suuret kirjaimet. Kumpi erisuurista kirjaimista on aakko–
sissa ennen, määrää sen kumpi nimistä on aakkosissa ennen.
Siinä meillä on algoritmi joka on varsin selvä. Jos algoritmi haluttaisiin vielä kirjoittaa "lausekieliseen" muotoon, niin se olisi suurin piirtein seuraavanlainen:
- siirry kummankin nimen ensimmäiseen kirjaimeen
- jos kummankin nimen viimeinen merkki on ohitettu, niin nimet ovat samat
- jos toisessa nimessä viimeinen merkki on ohitettu, niin se on ennen aakkosissa
- verrataan vuorossa olevia kirjaimia kummastakin nimestä
– jos samat, niin siirrytään seuraaviin kirjaimiin ja jatketaan kohdasta 2.
– jos erisuuret, niin se ensin aakkosissa, jonka kirjain on ensin
Tähän vielä pieni "viilaus enemmän strukturoidummaksi", niin meillä olisikin valmis (ali)ohjelma nimien vertaamiseksi.
4.2.2 Algoritmin sanallinen versio on kuvaavampi!
Vaikka esitimmekin algoritmin "lausekielisenä" kohdittain numeroituna, ei koskaan pidä unohtaa sitä ennen ollutta sanallista versiota, joka on selkeämpi kuvaus siitä ideasta, mitä tehdään!
Siis kirjoita aina ensin sanallinen kuvaava kuvaus algoritmista ja vasta sitten sen yksityiskohtainen "lausekielinen" versio!
4.2.3 Numeroiden järjestäminen
Näin ollen on aivan yksi lysti opettelemmeko järjestämään nimiä vai numeroita. Siksi paneudummekin seuraavassa numeroiden järjestämiseen. Kuulostaako vaikealta?
Otapa käteesi korttipakka ja ota sieltä esiin vaikkapa vain kaikki padat. Nyt sinulla on joukko "numeroita" (A=14, K=13, Q=12, J=11), yhteensä 13 kappaletta. Sekoita kortit! Yritä järjestää kortit suuruusjärjestykseen siten, ettet tarvitse pöytätilaa kuin yhden kortin verran, loput kortit pidät kädessäsi.
Millaisen algoritmin saat? Ehkäpä seuraavan (insertion sort):
Pöydällä on lajiteltujen kasa. Aluksi tietysti tyhjä. Ota
kädestäsi seuraava kortti ja laita pöydällä olevaan kasaan
omalle paikalleen. Jatka kunnes kädessä ei enää kortteja.
"Lausekielisenä":
- ota kädessä olevan kasan päällimmäinen kortti
- sijoita se pöydällä olevaan kasaan paikalleen
- mikäli kortteja vielä jäljellä, niin jatka kohdasta 1.
Algoritmisi voi olla myös seuraava (selection sort):
Etsitään aina pienin kortti ja laitetaan se pöydälle olevan
kasan päällimmäiseksi. Jatketaan kunnes kädessä olevat
kortit on loppu.
Eli "lausekielisenä":
- etsi kädessäsi olevista korteista pienin
- laita se pöydällä olevan pinon päällimmäiseksi
- mikäli vielä kortteja jäljellä, niin jatka kohdasta 1.
Tehtävä 4.9 Muita lajittelualgoritmeja
Mitä muita mahdollisia "lajittelumenetelmiä" keksit?
Siinä eräitä ratkaisuja tähän "hirveän vaikeaan" ongelmaan. Ratkaisuissa on tiettyjä huonoja puolia, mutta ratkaisut ovat todella yksinkertaisia ja jokaisen itse keksittävissä.
Tehtävä 4.10 Algoritmin kompleksisuus
Mikäli kahden kortin vertaaminen lasketaan yhdeksi "operaatioksi", niin kuinka monta "operaatiota" joudumme tekemään, jotta pakka on lajiteltu Selection Sortilla ?
Edellisen tehtävän vastausta sanotaan algoritmin kompleksisuudeksi.
Tehtävä 4.11 Lajittelujärjestys
Edellinen algoritmi ( selection sort ) toimi siten, että kortit jäivät pöydälle suurin päällimmäiseksi. Miten algoritmia pitää muuttaa, jotta pienin saataisiin päällimmäiseksi?
Ei siis ole suurtakaan väliä pitääkö lajitella nouseva vai laskeva järjestys!
4.2.4 Kuplalajittelu
Kokeillaanpa vielä erästä algoritmia: Sotke kortit kädessäsi uudelleen.
Bubble sort:
Vertaa aina kahta peräkkäistä korttia keskenään. Mikäli ne
ovat väärässä järjestyksessä, vaihda ne keskenään. Kun koko
pakka on käyty lävitse, aloita alusta ja jatka kunnes yhtään
kertaa ei tarvitse vaihtaa peräkkäisiä kortteja.
Tehtävä 4.12 Kuplalajittelu
Tuleeko pakka järjestykseen tällä algoritmilla? Voidaanko algoritmia nopeuttaa mitenkään? Kirjoita algoritmista "lausekielinen" versio.
4.2.5 Lajittelu avaimen mukaan
Kirjoita nyt joukko pahvilappuja, joissa kussakin on henkilön nimi, osoite ja puhelinnumero.
Sekoita laput ja kokeile toimiiko edelliset algoritmit mikäli laput järjestetään nimien mukaan. Ai tyhmä ehdotus! Tässä se onkin ohjelmoinnin vaikeus. Asiat ovat yksinkertaisia! Eiväthän ne osoitteet siellä lajittelua sotke.
Mikäli laput järjestetään nimen mukaan, sanotaan nimen olevan lajitteluavaimena. Lajitteluavaimeksi voitaisiin valita myös osoite tai puhelinnumero. Mikäli kahdella henkilöllä olisi sama nimi, voitaisiin nämä kaksi järjestää osoitteen perusteella. Tällöin lajitteluavain muodostuisi merkkijonosta johon olisi yhdistettynä nimi ja osoite.
4.2.6 Algoritmin parantaminen
Kaikki edelliset algoritmit ovat kompleksisuudeltaan normaalitapauksessa samanlaisia.
Ohjelman toimintaan saattamisen kannalta olisi riittävää löytää jokin toimiva algoritmi. Myöhemmin, mikäli ohjelman toiminta todetaan hitaaksi ko. algoritmin kohdalta, voidaan algoritmia yrittää tehostaa. Lajittelussa tehostus saattaisi olla vaikkapa QuickSort (mukana mm. C–kielen standardikirjastossa).
Tehtävä 4.13 Loppuminen erikoistapauksessa
Mikä edellisistä algoritmeista loppuu nopeasti, mikäli kortit jo olivat järjestyksessä? Ohjelman toimintaan saattamisen kannalta olisi riittävää löytää jokin toimiva algoritmi. Myöhemmin, mikäli ohjelman toiminta todetaan hitaaksi ko. algoritmin kohdalta, voidaan algoritmia yrittää tehostaa. Lajittelussa tehostus saattaisi olla vaikkapa QuickSort (mukana mm. C- kielen standardikirjastossa).
Tehtävä 4.14 QuickSortin kompleksisuus
Jos algoritmin kompleksisuus on esimerkiksi 2n2+n, sanotaan että kompleksisuus on O(n2), eli usein kiinnostaa vain kompleksisuuden suurin "potenssi". QuickSortin keskimääräinen kompleksisuus on O(n log2n). On olemassa myös erikoistapauksissa toimivia lajitteluja, joissa kompleksisuus on O(n). Piirrä kuva jossa on Selection Sortin , QuickSortin ja lineaarisen lajittelun käyttämä "aika" piirrettynä lajiteltavien alkioiden (n=10,100,1000,10000,1000000) funktiona.
Tehtävä 4.15 Lisäys oikealle paikalleen vaiko lisäys loppuun ja lajittelu?
Tutki kumpiko on työmäärältään edullisempaa jos järjestettyyn taulukkoon tulee lisättäväksi suuri määrä uusia alkiota
- lisätä alkio aina taulukkoon oikealle paikalleen
- lisätä alkio aina taulukon loppuun ja kun kaikki alkiot on lisätty, niin lajitella taulukko
4.3 Algoritmin tarkentaminen
Edellisissä lajittelualgoritmeissa oli vielä muutamia aukkopaikkoja! Etsi pienin? Laita oikealle paikalleen?
4.3.1 Pienimmän etsiminen
Miten kädessä olevista korteista voidaan etsiä pienin. Yksi mahdollisuus on kuljettaa "pienin ehdokasta" läpi koko pakan. Mikäli matkan varrelta löytyy parempi ehdokas, otetaan tämä tilalle. Edellä mainittu kuplalajittelu korjattuna perustuu nimenomaan tähän ideaan.
Entä jos kädessä olevien korttien järjestystä ei haluta muuttaa? Voisimme menetellä esimerkiksi seuraavasti (alkuarvaus ja arvauksen korjaaminen):
- vedä kädessä olevan pakan ylin kortti hieman esille ota ensimmäinen kortti tutkittavaksi
- vertaa tutkittavaa korttia ja esiinvedettyä korttia
- mikäli tutkittava on pienempi, vedä se esiin ja työnnä edellinen takaisin
- siirry tutkimaan seuraavaa korttia ja jatka kohdasta 1. kunnes olet tutkinut koko pakan
4.3.2 Paikalleen sijoittaminen
Miten kortti sijoitetaan paikalleen jo lajiteltuun kasaan? Esimerkiksi seuraavasti:
- laita uusi kortti päällimmäiseksi lajiteltuun kasaan
- vertaa uutta ja seuraavaa
- mikäli väärässä järjestyksessä, niin vaihda ne keskenään ja jatka kohdasta 1.
4.4 Haku järjestetystä joukosta
Usein tulee vastaan myös tilanne, jossa tietyn henkilön tiedot pitäisi hakea esimerkiksi nimen mukaan. Mikäli valittu tietorakenne on järjestetty nimen mukaan, voidaan hakemisessa käyttää vaikkapa puolitushakua.
Nimen hakeminen ei taas poikenne kortin etsimisestä järjestetystä korttipakasta vai mitä?
4.4.1 Suora haku
Kun kortit ovat järjestämättä, niin miten löydät haluamasi kortin?
Ota seuraava kortti. Mikäli etsittävä niin lopeta, muuten ota taas
seuraava.
Algoritmi on OK 13 kortille, mutta kokeilepa Äystön etsimistä puhelinluettelosta tällä algoritmilla (muista lukea jokainen nimi ennen Äystöä)!
4.4.2 Puolitushaku
Mikäli 13 korttiasi on järjestyksessä ja sinun pitäisi mahdollisimman vähällä pläräämisellä löytää vaikkapa pata 4, niin miten voisit menetellä?
- laita pakka pöydälle kuvapuolet ylöspäin
- laita pakka puoliksi
- laita molemmat pakat pöydälle kuvapuolet ylöspäin
- kummassako kasassa etsittävä on?
- heitä se pakka pois jossa etsittävä ei ole
- jos etsittävä ei päällimmäinen, niin jatka kohdasta 1.
Vaikuttaa tyhmältä 13 kortille, mutta kokeilepa 1000 kortilla! Tai kokeile nyt etsiä ÄYSTÖÄ puhelinluettelosta tällä algoritmilla.
Tehtävä 4.16 Puolitushaku
Kirjoita puolitushausta kunnon "lausekielinen versio" kun meillä on sivunumeroitu kirja, jonka kullakin sivulla on täsmälleen yhden henkilön tiedot. Sivunumeroita kirjassa on N-kappaletta. Aloitat sivuista S1=0 ja S2=N+1. Miten jatkat mikäli pitää etsiä nimi NIMI?
Tehtävä 4.17 Puolitushaun kompleksisuus
Mikä on puolitushaun kompleksisuus?
4.5 Yhteenveto
Tätä on ohjelmointi! Kykyä (ja rohkeutta) sanoa selvät asiat täsmällisesti. Jossain vaiheessa vaihdamme vain täsmällisyyden astetta ja "lausekielen" sijasta siirrymme käyttämään oikeata lausekieltä, esim. C–kieltä. Nämä omatekoiset algoritmit kannattaa kuitenkin säilyttää ja kirjata näkyviin todellisen ohjelman kommentteihin. Arviot algoritmin nopeudesta kannattaa myös laittaa kommentteihin, jotta jälkeenpäin on helpompi etsiä jo tekovaiheessa hitaaksi epäiltyjä kohtia. Miksi jättää seuraavalle lukijalle sama tehtävä ihmeteltäväksi, jos olemme sen toteutuksen jo jonnekin kirjanneet.
Algoritmit kannattaa testata huolellisesti jossain tutussa ympäristössä. Hyvin moni ohjelmointiongelma vektoreiden (=taulukko, =kasa kortteja, =ruutupaperi, =sivunumeroitu kirja jne.) kanssa samaistuu johonkin jokapäiväiseen ilmiöön. Kuten etsiminen puhelinluettelosta, korttipakan järjestäminen jne. Yritä etsiä näitä yhteyksiä ja kokeile ensin ratkaista ongelma tällä tavoin. Siirrä ratkaisu sitten "lausekielelle" ja lopulta ohjelmointikielelle.
Äläkä yritä liikaa, vaan jaa aina ongelma pienempiin osiin, kunnes tulee vastaan sen kokoisia osaongelmia, jotka osataan ratkaista! Tällaista osaongelman ratkaisijaa sanotaan ohjelmointikielessä aliohjelmaksi.
Kun osaongelma on ratkaistu, unohda se miten sen ratkaisija toimii ja käsittele ratkaisijaa vain yhtenä yksinkertaisena toimenpiteenä (vrt. aikaisempi kävelyesimerkki). Tämä on myös eräs ohjelmoinnin "vaikeus". Kirjoittaja haluaa nähdä kaikkien osien toiminnan yhtäaikaisesti. Tämä on kuitenkin mahdotonta. Siis kun jokin osa tekee hommansa, niin tehköön se sen miten tahansa.
Huono on johtaja joka kyttää koko ajan alaisiaan, eikä luota siihen, että nämä tekevät heille annetun tehtävän. Tässä mielessä ohjelmointia voisi verrata yrityksen johtamiseen: Johtaja jakaa koko yrityksen pyörittämisessä tarvittavia tehtäviä alaisilleen (aliohjelmille). Nämä saattavat edelleen jakaa joitakin osatehtäviä omille alaisilleen (aliohjelma kutsuu toista aliohjelmaa) jne. Johtaja (=ohjelmoija ja pääohjelma) kokoaa alaisten tekemän työn toimivaksi kokonaisuudeksi ja firma tuottaa.
Tehtävä 4.18 Kumin paikkaus
Kirjoita algoritmi polkupyörän kumin paikkaamiseksi.
Tehtävä 4.19 Sunnuntai- ilta
Kirjoita algoritmi sunnuntai- illan viettoa varten (muista että ohjelmoinnin demot on maanantaina).
Tehtävä 4.20 Onkiminen
Kirjoita algoritmi 10 ei- alimittaisen kalan onkimiseksi mato- ongella.
Tehtävä 4.21 Järjestyksen kääntäminen päinvastaiseksi
Kirjoita algoritmi pöydälle levitetyn 13 kortin kääntämiseksi päinvastaiseen järjestykseen.
5. Algoritmeissa tarvittavia rakenteita
Mitä tässä luvussa käsitellään?
- silmukat ja valintalauseet
- totuustaulut
- pöytätesti
- muuttujat
- taulukot
- osoittimet
Vaikka jatkossa keskitymmekin oliopohjaiseen ohjelmointiin, tarvitaan yksittäisen olion metodin toteutuksessa algoritmeja. Riippumatta käytettävästä ohjelmointikielestä, tarvitaan algoritmeissa aina tiettyjä samantyyppisiä rakenteita.
Käsittelemme seuraavassa tyypilliset rakenteet nopeasti lävitse. Tarvitsisimme asioille enemmänkin aikaa, mutta otamme asiat tarkemmin esille käyttämämme ohjelmointikielen opiskelun yhteydessä. Lukijan on kuitenkin asioita tarkennettaessa syytä muistaa, ettei rakenteet ole mitenkään sidottu ohjelmointikieleen. Vaikka ne näyttäisivät kielestä täysin puuttuvankin (esim. assembler), voidaan ne kuitenkin lähes aina toteuttaa.
5.1 Ehtolauseet
Triviaaleja algoritmeja lukuun ottamatta algoritminen suoritus tarvitsee ehdollisia toteutuksia:
Jos kello yli puolenyön ota taksi
muuten mene linja- autolla
Ehtolauseita voi ehtoon tulla useampiakin ja tällöin on syytä olla tarkkana sen kanssa, mihin ehtoon mahdollinen muuten–osa liittyy:
Jos kello 00.00- 07.00
Jos sinulla on rahaa niin ota taksi
muuten kävele
muuten mene linja- autolla
Tehtävä 5.22 Ajanlisäys
Jos sinulla on muuttujassa t tunnit ja muuttujassa m minuutit, niin kirjoita algoritmi miten lisäät n minuuttia kellonaikaan t:m.
Tehtävä 5.22 Ajanlisäys
Tehtävä 5.23 Postimaksu
Kirjoita algoritmi g- painoisen kirjeen postimaksun määräämiseksi (saat keksiä hinnaston itse).
5.2 Valintalauseet
Usein ehtoja kasaantuu niin paljon, että peräkkäiset ja sisäkkäiset ehtolauseet muodostavat varsin sekavan kokonaisuuden. Tällöin voi olla helpompi käyttää valintalausetta:
Auto on/oli rekisterinumeron 1. kirjaimen mukaan rekisteröity seuraavassa läänissä:
Auto on/oli rekisterinumeron 1. kirjaimen mukaan rekisteröity seuraavassa läänissä:
X Keski- Suomen lääni
K Kuopion lääni
M Mikkelin lääni
A,U Uudenmaan lääni
Tehtävä 5.24 Korvaaminen ehtolauseilla
Esitä auton rekisteröintipaikan riippuvuus rekisterin ensimmäisestä kirjaimesta sisäkkäisten ehtolauseiden avulla.
5.3 Silmukat
Hyvin usein algoritmi tarvitsee toistoa: Esimerkiksi ohjeet (vuokaavio) hiekkarannalla toimimiseksi jos nenä näyttää merelle päin:
Ehtolause voi olla silmukan alussa, tällöin on mahdollista ettei silmukan runkoa tehdä yhtään kertaa. Ehto voi olla myös silmukan jälkeen, jolloin silmukan runko tehdään vähintään yhden kerran. Joissakin kielissä on lisäksi mahdollisuus silmukan rungon keskeltä poistuminen.
Silmukoihin liittyy aina ohjelmoinnin eräs klassisimmista vaaroista: päättymätön silmukka! Tämän takia silmukoita tulee käsitellä todella huolella. Eräs oleellinen asia on aina muistaa suorittaa silmukan rungossa jokin silmukan lopetusehtoon vaikuttava toimenpide. Mitä tapahtuu muuten?
Myös silmukan suorituskertojen lukumäärän kanssa tulee olla tarkkana. Silmukka tulee helposti suoritettua yhden kerran liikaa tai yhden kerran liian vähän.
Tehtävä 5.25 Uiminen
Mitä eroa on kahdella edellä esitetyllä "uimaan- meno" - algoritmilla? Mitä ehtoja algoritmiin voisi vielä lisätä?
Tehtävä 5.26 Ynnää luvut 1- 100
Kirjoita algoritmi lukujen 1- 100 yhteenlaskemiseksi sekä do-while
- että while
- silmukan avulla.
5.4 Muuttujat
Algoritmeissa tarvitaan usein muuttujia.
kellonaika
rahan määrä
5.4.1 Yksinkertaiset muuttujat
Yksinkertaisessa tapauksessa muuttuja voi olla yksinkertaista tyyppiä kuten kellonaika (jos ilmaistu minuutteina), rahasumma jne.
Yksinkertainen luvun jaollisuuden testausalgoritmi voisi olla vaikkapa seuraavanlainen:
Jaetaan tutkittavaa lukua jakajilla 2,3,5,7...luku/2.
Jos jokin jako menee tasan, niin ei alkuluku:
0. Laita jakaja:=2, kasvatus:=1,
Jos luku=2 lopeta, alkuluku
1. Jaa luku jakajalla. Meneekö jako tasan?
- jos menee, on luku jaollinen jakajalla, lopeta
2. Kasvata jakajaa kasvatus arvolla (jakaja:=jakaja+kasvatus)
3. Kasvatus:=2; (koska parillisilla ei kannata enää jakaa)
4. Onko jakaja<luku/2?
- jos on, niin jatka kohdasta 1
- muuten lopeta, luku on alkuluku
Tehtävä 5.27 Vuokaavio
Piirrä jaollisuuden testausalgoritmista vuokaavio.
5.4.2 Pöytätesti
Hyvin usein algoritmi kannattaa pöytätestata. Pöytätesti alkaa kirjoittamalla sarakkeiksi kaikki algoritmissa esiintyvät muuttujat. Muuttujiksi voidaan kirjoittaa myös algoritmissa esiintyviä ehtoja. Tällainen muuttuja voi saada arvon kyllä tai ei. Pöytätestin riveiksi kirjoitetaan algoritmin eteneminen vaiheittain. Sarakkeisiin muuttujille kirjoitetaan uusia arvoja vain niiden muuttuessa.
Testataan esimerkiksi edellisen esimerkin algoritmi:
askel | Luku | Jakaja | Kasvatus | Luku/Jakaja | Jako tasan? | Jakaja<Luku/2? | Tulostus |
---|---|---|---|---|---|---|---|
0 | 25 | 2 | 1 | ||||
1 | 12.500 | ei | |||||
2 | 3 | ||||||
3 | 2 | ||||||
4 | 3<12.5 | ||||||
1 | 8.333 | ei | |||||
2 | 5 | ||||||
3 | 2 | ||||||
4 | 5<12.5 | ||||||
1 | 5.000 | kyllä | Jaollinen 5:llä |
askel | Luku | Jakaja | Kasvatus | Luku/Jakaja | Jako tasan? | Jakaja<Luku/2? | Tulostus |
---|---|---|---|---|---|---|---|
0 | 23 | 2 | 1 | ||||
1 | 11.500 | ei | |||||
2 | 3 | ||||||
3 | 2 | ||||||
4 | 3<11.5 | ||||||
1 | 7.667 | ei | |||||
2 | 5 | ||||||
3 | 2 | ||||||
4 | 5<11.5 | ||||||
1 | 4.600 | ei | |||||
2 | 7 | ||||||
3 | 2 | ||||||
4 | 7<11.5 | ||||||
1 | 3.286 | ei | |||||
2 | 9 | ||||||
3 | 2 | ||||||
4 | 9<11.5 | ||||||
1 | 2.556 | ei | |||||
2 | 11 | ||||||
3 | 2 | ||||||
4 | 11<11.5 | ||||||
1 | 2.091 | ei | |||||
2 | 13 | ||||||
3 | 2 | ||||||
4 | 13>11.5 | Alkuluku |
Usein pöytätesti antaa hyviä vinkkejä myös algoritmin jatkokehittelylle.
5.4.3 Yksiulotteiset taulukot
Tutkikaamme aikaisempia korttipakkaesimerkkejämme! Nyt tietorakenteeksi ei enää riitäkään pelkkä yksi muuttuja. Mikäli pakasta on otettu esiin pelkät padat, tarvitsisimme 13 muuttujaa. Näiden kunkin nimeäminen erikseen olisi varsin työlästä.
Tarvitsemme siis jonkin muun tietorakenteen. Mahdollisuuksia on useita: listat, jonot, pinot ja taulukot. Ohjelmoinnin alkuvaiheessa taulukot ovat tietorakenteista helpoimpia, joten keskitymme niihin aluksi.
Varataan pöydältä tilaa leveyssuunnassa 13 kortille. Varattua tilaa voimme nimittää taulukoksi tai vektoriksi. Taulukon yksi alkio on yhdelle kortille varattu paikka. Taulukon yhden alkion sisältö on se kortti, joka on siinä paikassa.
Mikäli numeroimme varatut paikat vaikkapa 0:sta alkaen vasemmalta oikealle, on meidän korteillamme osoitteet 0–12:
Nyt voimme käsitellä yksittäisiä kortteja aivan kuin ne olisivat yksittäisiä muuttujia. Viittaamme tiettyyn korttipaikkaan (taulukon alkioon) sen indeksillä (olkoon taulukon nimi kortit):
paikassa kortit[5] meillä on pata 9
paikassa kortit[8] meillä on pata akka
Minkälaisia algoritmeja tulee vastaan taulukoita käsiteltäessä? Esim. ♠9:n siirtäminen taulukon viimeiseksi vaatisi ♠4:en siirtämistä paikkaan 5. ♠6:en siirtämistä paikkaan 6, ♠Q:n siirtämistä paikkaan 7 jne. Näin loppuun saataisiin raivatuksi paikka ♠9:lle.
Lajittelun ilman valtaisaa korttien siirtelyä voisimme hoitaa seuraavasti:
0. laita alku paikkaan 0
1. etsi alku paikasta lähtien pienin kortti
2. vaihda pienin ja paikassa alku oleva kortti
3. alku:=alku+1
4. mikäli alku<suurin indeksi, niin jatka 1
Sovitaan, että ässä=1. Nyt pienimmän kortin etsimisalgoritmi voisi olla seuraava:
0. Alkuarvaus: pien.paikka:=alku, tutki:=alku
1. Jos kortit[tutki] < kortit[pien.paikka]
niin pien.paikka:=tutki
2. tutki:=tutki+1
3. Jos tutki<=suurin indeksi, niin jatka 1.
Voisimme vielä pöytätestata algoritmin:
5.4.4 Osoittimet
Edellisessä pöytätestissä merkitsimme pienen merkin niiden korttien kohdalle, joita kunakin hetkenä tutkimme. Kun tutkimme esimerkiksi paikoissa 3 ja 10 olevia kortteja (P2 ja PJ) voisimme sanoa, että muuttuja pien.paikka osoitti korttiin pata 2 ja muuttuja tutki korttiin pata jätkä. Näin ollen voisimme oikeastaan sanoa, että (indeksi)muuttujat pien.paikka ja tutki ovat osoittimia korttipakkaan. Niiden osoittamassa paikassa (indeksit 3 ja 10) on tietyt kortit (P2 ja PJ).
Lajittelualgoritmi voitaisiin lausua esimerkiksi:
0. levitä kortit rinnakkain pöydälle
osoita vasemman käden etusormella ensimmäiseen korttiin
1. etsi vasemman käden osoittamasta kohdasta alkaen oikealle
pienin kortti ja osoita sitä oikean käden etusormella
2. vaihda etusormien kohdalla olevat kortit keskenään
3. siirrä vasemman käden etusormea yksi kortti oikealle päin
4. mikäli vasen sormi vielä kortin päällä, jatka kohdasta 1.
Osoittimen ja indeksin ero on siinä, että osoittimen tapauksessa emme yleensä koskaan ole kiinnostuneita itse osoittimen arvosta (osoitteesta, siitä indeksistä missä kohdin olemme menossa), vaan osoittimen osoittaman paikan sisällöstä (sormen kohdalla olevan kortin arvo tai ei korttia). Indeksejä käsitellessämme tutkimme monesti myös itse indeksin arvoa (tutki=3 –>kortit[tutki]=P2).
Osoitin voi tarvittaessa osoittaa myös itse taulukon ulkopuolelle. Mikäli kirjoittaisimme pöydälle numeroita, voisimme osoittaa sormella yhtä hyvin pöydälle kirjoitettuja numeroita (älkää hyvät ihmiset nyt töhrätkö pöytää!) kuin pöydälle levitettyjä kortteja (taulukon alkioita).
Siis indeksit liittyvät kiinteästi taulukoihin ja osoittimet voivat liittyä mihin tahansa tietorakenteisiin alkaen yksinkertaisesta muuttujasta päätyen monimutkaisen lista– tai puurakenteen alkioon.
5.4.5 Moniulotteiset taulukot
Yksiulotteista taulukkoa voidaan verrata rivitaloon tai ruutupaperin yhteen riviin. Kaksiulotteinen taulukko on vastaavasti kuten kapea kerrostalo tai koko ruutupaperin yksi sivu. Tarvitsemme vastaavasti useampia osoitteita (indeksejä) osoittamaan millä rivillä ja missä sarakkeessa liikumme.
Alla on esimerkki 5x7 taulukosta (♠=pata, ♣=Risti, ♦=ruutu, ♥=hertta):
Jos taulukon nimi on peli, niin paikassa 3,1 on kortti pata 2:
peli[3][1] = ♠2
Tehtävä 5.33 Kaksiulotteisen taulukon indeksit
Kirjoita kaikkien esimerkissä olevien korttien osoitteet em. muodossa.
Kaksiulotteista taulukkoa nimitetään usein matriisiksi.
Usein taulukoiden indeksit ilmoitetaan eri järjestyksessä kuin koordinaatiston (x,y)–koordinaatit. Tämä johtuu siitä ajattelutavasta, että taulukon rivi sinänsä voidaan kuvitella yhdeksi alkioksi (rivityypiksi) ja tällöin ilmaisu
peli[3]
tarkoittaa koko riviä (♥7, ♠2, ♦2, ♠9, ♥6, ♥3, ♦7), jonka indeksi on kolme. Mikäli tämän perään laitetaan vielä [1], niin tarkoitetaan ko. tietorakenteen alkiota jonka indeksi on yksi (♠2).
Tarvittaessa moniulotteiset taulukot voidaan muodostaa yksiulotteisenkin taulukon avulla. Esimerkin taulukko voitaisiin muodostaa yksiulotteisesta taulukosta siten, että yksiulotteisen taulukon 7 ensimmäistä alkiota kuvaisivat matriisin 0:ta riviä, 7 seuraavaa matriisin ensimmäistä riviä jne.
Siis mikäli yksiulotteisen taulukon nimi olisi pakka, niin voisimme käyttää samaistuksia:
peli[3][1] = pakka[7*3+1]
peli[j][i] = pakka[7*j+i]
Olemme siis numeroineet kaksiulotteisen taulukon alkiot juoksevasti. Voimmehan tehdä näin myös kerrostalon huoneistoille tai teatterin istumapaikoille.
Taulukot voivat olla myös useampiulotteisia, esimerkiksi 3x4x5 taulukko:
0 1 2 3 4
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
0 │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐0
└─┤ │ └─┤PJ│ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
1 │ ┌┴─┐ │ ┌┴─┐ │R5┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤HA┴─┐└─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
2 │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
3 │P7┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
isopeli[0][0][1] = PJ
isopeli[2][1][2] = R5
isopeli[1][1][3] = HA
isopeli[2][3][0] = P7
Tehtävä 5.34 Sijoitus 3- ulotteiseen taulukkoon
Esitä 5 muuta sijoitusta taulukkoon.
Tehtävä 5.35 3- ulotteinen taulukko 1- ulotteiseksi
Esitä kaava miten edellä oleva 3- ulotteinen taulukko voitaisiin esittää yksiulotteisella taulukolla.
Aikaisempi satunnaisen matkaajan sanastomme on oikeastaan myös kolmiulotteinen taulukko:
0 1 2
0 minä jag i
1 sinä du you
2 hän han he
Se on kaksiulotteinen taulukko sanoista. Mitä sitten yksi sana on? Se on yksiulotteinen taulukko kirjaimista!
0 1 2 3 4
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
0 │h┌┴─┐ │ä┌┴─┐ │n┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤s┌┴─┐ └─┤i┌┴─┐ └─┤n┌┴─┐ └─┤ä┌┴─┐└─┤ ┌┴─┐0
└─┤m │ └─┤i │ └─┤n │ └─┤ä │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
1 │h┌┴─┐ │a┌┴─┐ │n┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤d┌┴─┐ └─┤u┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐
└─┤j │ └─┤a │ └─┤g │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
2 │h┌┴─┐ │e┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤y┌┴─┐ └─┤o┌┴─┐ └─┤u┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐
└─┤i │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
Siis "you" sanan indeksi on [1][2] ja sen kirjaimen "y" indeksi on [0]. Siis kaiken kaikkiaan "you"–sanan "y"-kirjaimen indeksi olisi [1][2][0].
Taulukko voitaisiin järjestää 3–ulotteiseksi myös toisinkin. Esimerkiksi yhdessä "tasossa" olisi yksi kieli jne.
Tehtävä 5.36 3- ulotteinen taulukko
Esitä edellisessä esimerkissä kaikkien kirjainten indeksit. Millaisella yhden kirjaimen sijoituksella muuttaisit sanan " han" sanaksi " hon"?
Tehtävä 5.37 4- ulotteinen taulukko
Mitenkä tavallinen kirja voitaisiin kuvitella 3- ulotteiseksi taulukoksi? Miten kirja voitaisiin kuvitella 4- ulotteiseksi taulukoksi? Piirrä edellisiin perustuen esimerkki 4- ulotteisesta taulukosta ja anna muutama esimerkkisijoitus.
Osoitinmuuttuja osoittaisi myös moniulotteisessa taulukossa yhteen alkioon kerrallaan. Esimerkiksi osoittamalla "you"–sanan "y"-kirjaimeen.
Moniulotteisen ja yksiulotteisen taulukon väliset muunnokset ovat tärkeitä, koska tietokoneen muisti (1997) on lähes aina yksiulotteinen. Siis loogisesti moniulotteiset taulukot joudutaan lopulta aina toteuttamaan yksiulotteisina. Onneksi useat kielet sisältävät moniulotteiset taulukot tietotyyppinä ja kääntäjät tekevät sitten muunnoksen. Tästä huolimatta esimerkiksi C–kielessä joudutaan usein muuttamaan moniulotteisia taulukoita yksiulotteisiksi.
5.4.6 Sekarakenteet
Taulukko voi olla myös taulukko osoittimista. Esimerkiksi sanastomme tapauksessa kaikki sanat voisivat olla yhdessä "möykyssä":
0 1 2 3
0123456789012345678901234567890123
minä jag i sinä du you hän han he
Itse sanasto voisi sitten olla taulukko osoittimia sanojen alkupaikkoihin:
0 | 1 | 2 | |
0 | 00 | 05 | 09 |
1 | 11 | 16 | 19 |
2 | 23 | 27 | 31 |
Siis taulukon paikasta sanasto[1][0] löytyy osoitin. Tämän osoittimen arvo on tässä esimerkissä 11. Siis osoitin viittaa sanan "sinä" alkuun. Tässä 2-ulotteinen taulukko osoittimista 1-ulotteiseen merkkitaulukkoon
// C++:lla
char *sanasto[3][3];
Tehtävä 5.38 Sanojen muuttaminen
Mitä ongelmia edellä olisi, mikäli yhdenkin sanan pituutta kasvatettaisiin? Voitaisiinko edellä käyttää samoja sanoja uudestaan ja jos niin miten?
5.5 Osoittimista ja indekseistä
Osoitinmuuttujaa voitaisiin kuvitella myös seuraavasti: Olkoon meillä osoitekirja (osoitteet) jossa on sivuja:
Meidän osoitekirjamme on tavallaan taulukko osoittimista (tässä tapauksessa osoitteita, älä sotke termejä!). Taulukon osoitteet paikassa 1 (sivu 1) on osoite "Sepe Sudelle". Mitä tapahtuu mikäli kirjoitamme kokonaan uuden henkilön osoitteen sivulla 1 olevan osoitteen päälle (sijoitetaan uusi arvo osoitinmuuttujalle sivu[1])?
Mitä tapahtuu "Sepe Sudelle"? Tuskinpa sijoitus osoitekirjassamme siirtää "Sepe Sutta" yhtään mihinkään "Perämetsästä", tai tekee edes häntä murheelliseksi! Tämä on eräs tyypillinen virhekäsitys osoitinmuuttujia käytettäessä. Osoitinmuuttujaan sijoittaminen ei muuta tietenkään itse tiedon sisältöä. Mutta sijoittaminen siihen paikkaan johon osoitinmuuttuja osoittaa (esimerkissämme "Sepe Suden" asuntoon) muuttaa tietenkin myös itse tiedon sisältöä.
// C++:lla
sivu[1] = uusi_osoite; // ei vaikuta Sepe Suteen
*sivu[1] = uusi_henkilo; // laittaa uuden henkilön Sepen osoitteeseen
// = "tähdätään osoitetta pitkin"
Vastaavasti jos meillä on indeksimuuttuja nimeltä snro, niin sijoitus muuttujalle
snro=2
ei muuta mitenkään itse sivun sisältöä. Vasta sijoitus
sivu[snro]=
muuttaisi sivun 2 sisältöä.
5.6 Aliohjelmat
Aliohjelmat on tarkempi kuvaus tietylle asialle. Tämä kuvaus esitetään jossakin toisessa kohdassa ja sitä ei suinkaan tarvitse joka kerta lukea uudelleen.
Keittokirjassa lukee:
pyöritä lihapullataikinasta pyöreitä pullia
paista pullat kauniin ruskeiksi
Miten pullat paistetaan kauniin ruskeiksi. Tämä olisi edellisen algoritmin aliohjelma. Kokenut kokki ei välitä aina (eikä edes suostuisi) joka reseptin kanssa lukea itse paistamisohjetta. Aloittelija tätä saattaisi tarvita, jottei naapuri hälyttäisi palokuntaa paikalle liian savunmuodostuksen takia. Siis toivottavasti keittokirjasta jostakin kohti löytyisi myös itse paistamisohje.
Aliohjelmille on tyypillistä, että ne saattavat suoriutua vastaavista tehtävistä eri muuttujillekin. Näitä kutsukerroista riippuvia muuttujia sanotaan parametreiksi.
Esim. lihapullan paisto–ohje saattaa semmoisenaan kelvata myös tavalliselle pullalle:
paista("paistettava","c")
Korvaa seuraavassa sana "paistettava" sillä mitä olet
paistamassa ja "c" sillä lämpötilalla, joka keittokirjan
ohjeessa ko. paistettavan kohdalla on:
0. laita "paistettavat" pellille
1. lämmitä uuni "c"- asteeseen
2. laita pelti uuniin
...
9. mikäli "paistettavat" ovat mustia mene ostamaan
kaupasta valmiita
...
Tehtävä 5.39 Lihapullan paistaminen
Täydennä edellinen paistamisalgoritmi. Onko parametrejä tarpeeksi?
5.7 Vaihtoehtojen tutkiminen totuustaulun avulla
Usein helpostakin tehtävästä seuraa monia eri vaihtoehtoja, joiden täydellinen hallitseminen pelkästään päässä saattaa olla ylivoimaista. Tällöin avuksi tulee totuus– ja päätöstaulut. Päätöstaulu on totuustaulun pidemmälle viety versio.
Tutkikaamme aluksi muutamaa esimerkkiä jotka eivät suoranaisesti liity ohjelmointiin, mutta joissa esiintyy täsmälleen vastaava tilanne:
5.7.1 Kaikkien vaihtoehtojen kirjaaminen
Uusien opiskelijoiden lähtötasotesti 1991 (n. 20% oli osannut vastata oikein tähän tehtävään):
Tehtävä 3.4: Seisot tienhaarassa tuntemattomassa maassa. Toinen teistä vie pääkaupunkiin. Maan asukkaat joko valehtelevat aina tai puhuvat aina totta. Toisen tien alussa seisoskelee yksi heistä. Esität hänelle kyllä vai ei –kysymyksen ja saat vastauksen. Mitä kahta seuraavista neljästä kysymyksestä olisit voinut käyttää ollaksesi varma siitä, kumpi tie vie pääkaupunkiin – riippumatta siitä valehteleeko asukas vai ei?
- Viekö tämä tie pääkaupunkiin?
- Olisitko sanonut, että tämä tie vie pääkaupunkiin, jos olisin kysynyt sinulta sitä?
- Onko niin, että tämä joko on tie pääkaupunkiin, tai sitten sinä puhut totta (mutta ei molemmin tavoin)?
- Onko totta, että tämä tie vie pääkaupunkiin ja sen lisäksi sinä puhut totta?
Erotelkaamme eri kysymykset:
1 = A Viekö pääkaupunkiin?
B Puhutko totta? (mielenkiinnon vuoksi)
2 = C Olisitko sanonut että vie jos A?
3 <– D Tie pääk. XOR puhut totta?
4 <– E Tie pääk. AND puhut totta?
Nyt voimme kirjoittaa eri vaihtoehtojen mukaan seuraavan totuustaulun (V=vastaus kun valehteleminen otetaan huomioon):
Tie vie pää- | Puhuu | 1 | 2 | 3 | 4 | |||||
kaupunkiin | totta | A | B | C | D | V | E | V | ||
E | E | K | K | E | E | K | E | K | ||
E | K | E | K | E | K | K | E | E | ||
K | E | E | K | K | K | E | E | K | ||
K | K | K | K | K | E | E | K | K |
Siis 2 ja 3 vastauksissa on järkevä korrelaatio siihen viekö tie pääkaupunkiin vai ei.
5.7.2 Vaihtoehtojen lukumäärä
Mistä tiedämme milloin kaikki vaihtoehdot on kirjoitettu? Mikäli systeemiin vaikuttavia asioita on n kappaletta ja kukin on kyllä/ei tyyppinen (0/1), niin vaihtoehdot on helpointa saada aikaan kirjoittamalla kaikki n–bittiset binääriluvut järjestyksessä (esimerkissämme n=2) ja suorittamalla sitten tarvittavat samaistukset (esim. E=0 ja K=1). Vaihtoehtoja on tällöin 2n.
00 - > E E
01 - > E K
10 - > K E
11 - > K K
Tehtävä 5.40 Kombinaatioiden lukumäärä
Olkoon meillä tehtävä, jossa yksi muuttuja voi saada arvot K
,E
,tyhjä
ja toinen muuttuja arvot 5
ja 10
. Kirjoita kaikki ko. muuttujien kombinaatiot. Mikäli meillä on vaihtoehtoja n
kappaletta ja kukin voi saada k(i)
eri arvoa, niin montako eri kombinaatiota saamme aikaiseksi?
5.7.3 Useita vaihtoehtoja samalla muuttujalla
Ottakaamme toinen vastaava tehtävä:
Tehtävä 4.3: Pekka valehtelee maanantaisin, tiistaisin ja keskiviikkoisin; muina viikonpäivinä hän puhuu totta. Paavo valehtelee torstaisin, perjantaisin ja lauantaisin; muina viikonpäivinä hän puhuu totta. Eräänä päivänä Pekka sanoi: "Eilen valehtelin!" Paavo vastasi: "Niin minäkin!" Mikä viikonpäivä oli?
Minä päivinä kaverukset saattaisivat sanoa ko. lausuman? Näitä päiviä on tietysti ne, jolloin joko eilen valehdeltiin ja tänään puhutaan totta TAI eilen puhuttiin totta ja tänään valehdellaan. (XOR)
päivä | Pekka | valehteli eilen | Paavo | valehteli eilen |
|
sunnuntai | k sanoo | ||||
maanantai | V | sanoo | |||
tiistai | V | k | |||
keskiviikko | V | k | |||
torstai | k sanoo | V | sanoo | <== | |
perjantai | V | k | |||
lauantai | V | k | |||
sunnuntai | k sanoo |
Totuustaulun tavoitteena on siis kerätä kaikki mahdolliset vaihtoehdot ohjelmoijan silmien eteen, ja näin kaikki mahdollisuudet voidaan analysoida ja käsitellä.
Tehtävä 5.41 BAL=kyllä?
Eräällä saarella asuu luonnonkansa. Puolet kansan asukkaista aina valehtelevat ja toinen puoli puhuu aina totta. Lisäksi heidän kielensä on tuntematon. On saatu selville, että "BAL" ja "DA" tarkoittavat "kyllä" ja "ei", muttei sitä kumpiko tarkoittaa kumpaa. He ymmärtävät suomea mutta vastaavat aina omalla kielellään. Vastaasi tulee yksi saaren asukas.
- Mitä saat selville kysymyksellä "Tarkoittaako BAL KYLLÄ"?
- Millä kysymyksellä saat selville mikä sana on kyllä?
Tehtävä 5.42 Kuka valehtelee?
Jälleen maahan jossa asukkaat joko valehtelevat tai puhuvat aina totta. Tapaat kolme asukasta A:n, B:n ja C:n. He sanovat sinulla
A: Me kaikki kolme valehtelemme! B: Tasan yksi meistä puhuu totta!
Mitä ovat A, B ja C?
Entä jos asukkaat sanovat:
A: Me kaikki kolme valehtelemme! B: Tasan yksi meistä valehtelee!
Entä jos:
A: Minä valehtelen mutta B ei!
Entä jos:
A: B valehtelee! B: A ja C ovat samaa tyyppiä!
Vielä yksi:
A sanoo: B ja C ovat samaa tyyppiä. C:ltä kysytään: Ovatko A ja B samaa tyyppiä?
Mitä C vastaa?
5.7.4 Loogiset operaatiot
Ehtoja usein yhdistellään loogisten operaatioiden avulla:
Mikäli kello 7- 20 ja et halua ulkoilla
- mene bussilla
...
Mikäli sinulla on rahaa tai saat kimpan
- ota taksi
Yksittäinen ehto antaa tulokseksi tosi (T=true) tai epätosi (F=false). Ehtojen tulosta voidaan usein myös kuvata 1 tai 0. Ehtojen yhdistämistä loogisilla operaatioilla kuvaa seuraava totuustaulu (myös C++:n loogiset operaattorit merkitty):
ja | tai | ehd. tai | ei | |||||||||||||||
AND | OR | XOR | NOT | |||||||||||||||
p | q | p | && | q | p | || | q | p | ^ | q | ! | |||||||
F | 0 | F | 0 | F | 0 | F | 0 | F | 0 | T | ||||||||
F | 0 | T | 1 | F | 0 | T | 1 | T | 1 | T | ||||||||
T | 1 | F | 0 | F | 0 | T | 1 | T | 1 | F | ||||||||
T | 1 | T | 1 | T | 1 | T | 1 | F | 0 | F |
Huomattakoon edellä, että AND operaatio toimii kuten kertolasku ja OR operaatio kuten yhteenlasku (mikäli määritellään 1+1=1). Siis loogisia operaattoreita voidaan käyttää kuten normaaleja algebrallisia operaattoreita ja niillä operoiminen vastaa tavallista algebraa. Loogisten operaatioiden algebraa nimitetään Boolen –algebraksi.
Ehtojen sieventämisessä käytettäviä kaavoja voidaan todistaa oikeaksi totuustaulujen avulla. Todistetaan esimerkiksi de Morganin kaava (vrt. joukko–oppi, 1=true, 0=false):
NOT (p AND q) = (NOT p) OR (NOT q)
Jaetaan ensin väittämä pienempiin osiin:
NOT e1 = e2 OR e3
e1 | e2 | e3 | |||||
p | q | p AND q | NOT p | NOT q | NOT e1 | e2 OR e3 | |
0 | 0 | 0 | 1 | 1 | 1 | 1 | |
0 | 1 | 0 | 1 | 0 | 1 | 1 | |
1 | 0 | 0 | 0 | 1 | 1 | 1 | |
1 | 1 | 1 | 0 | 0 | 0 | 0 |
Koska kaksi viimeistä saraketta ovat samat ja kaikki muuttujien p ja q arvot on käsitelty, on laki todistettu!
Tehtävä 5.43 de Morganin kaava
Todista oikeaksi myös toinen de Morganin kaava:
NOT (p OR q) = (NOT p) AND (NOT q)
Tehtävä 5.44 Osittelulaki
Yhteenlaskun ja kertolaskun välillähän pätee osittelulaki:
p * (q + r) = (p * q) + (p * r)
Samaistamalla * <=> AND ja + <=> OR todetaan loogisille operaatioillekin osittelulaki:
p AND (q OR r) = (p AND q) OR (p AND r)
Todista oikeaksi toinen osittelulaki (toimiiko vast. yhteenlaskulla ja kertolaskulla):
p OR (q AND r) = (p OR q) AND (p OR r)
e1 | e2 | e3 | ||||||
p | q | r | q AND r | p OR q | p OR r | p OR e1 | e2 AND e3 | |
0 | 0 | 0 | ||||||
0 | 0 | 1 | ||||||
0 | 1 | 0 | ||||||
0 | 1 | 1 | ||||||
1 | 0 | 0 | ||||||
1 | 0 | 1 | ||||||
1 | 1 | 0 | ||||||
1 | 1 | 1 |
5.8 Muistele tätä
Mikäli edellä esitetyt asiat tuntuvat ymmärrettäviltä, niin ohjelmoinnissa ei tule olemaan mitään vaikeuksia. Jos vastaavat asiat tuntuvat vaikeilta ohjelmoinnin kohdalla, kannattaa palata takaisin tähän lukuun ja yrittää samaistaa asioita ohjelmointikieleen.
Taulukoiden samaistaminen ruutupaperiin, korttipakkaan tai muuhun tuttuun asiaan auttaa asian käsittelyä. Osoitinmuuttuja on yksinkertaisesti jokin (vaikkapa sormi) joka osoittaa johonkin (vaikkapa yhteen kirjaimeen).
Silmukat ja ehtolauseet ovat hyvin luonnollisia asioita.
Aliohjelmat ovat vain tietyn asian tarkempi kuvaus. Tarvittaessa tiettyä asiaa ei ongelmaa tarvitse heti ratkaista, vaan voidaan määritellä aliohjelma, joka hoitaa homman ja kirjoitetaan itse aliohjelman määrittely joskus myöhemmin.
0 | 1 | 2 | 3 | 4 | 5 | |
K | i | s | s | a | NUL |
Tehtävä: Välilyöntien poistaminen jonon alusta.
- Välilyöntien poistaminen jonon lopusta.
- Ylimääräisten (2 tai useampia) välilyöntien poistaminen jonosta.
- Kaikkien ylimääräisten (alku-, loppu- ja monikertaiset) välilyöntien poistaminen.
- Jonon muuttaminen siten, että kunkin sanan 1. kirjain on iso kirjain.
- Tietyn merkin esiintymien laskeminen jonosta.
- Esiintyykö merkkijono toisessa merkkijonossa (kissatarha, sata –>; esiintyy; kissatarha, satu –>; ei esiinny).
Tehtävä: Onko vuosi karkausvuosi vai ei. (Huom! 1900 ei, 2000 on)
- Montako karkausvuotta on kahden vuosiluvun välillä.
- Jos 1.1 vuonna 1 oli maanantai, niin mikä viikonpäivä on 1.1 vuonna x? (Oletetaan että kalenteri olisi ollut aina samanlainen kuin nytkin. Vihje! Tutki almanakkaa peräkkäisiltä vuosilta.)
- Onko päivämäärä pp.kk.vvvv oikeata muotoa?
6. Esimerkkejä eri kielistä
Mitä tässä luvussa käsitellään?
- katsotaan mitä syntaktista eroa on eri ohjelmointikielillä yksinkertaisessa esimerkissä
Jossakin vaiheessa ohjelmoinnin opiskelua tullaan siihen, että ohjelma pitäisi toteuttaa jollakin olemassa olevalla ohjelmointikielellä (on tosin ohjelmointikursseja, joilla käytetään keksittyä ohjelmointikieltä).
Seuraavassa esitämme ohjelman jonka ainoa tehtävä on tulostaa teksti:
Terve! Olen ??–kielellä kirjoitettu ohjelma.
Itse ohjelman suunnittelu on tällä kertaa varsin triviaali ja tehtävä ei tarvitse varsinaista tarkennustakaan, kaikki on sanottu tehtävän määrityksessä. Siis valitaan vain käytetyn kielen tulostuslause.
6.1 Esimerkkiohjelmat
Jotta lukija ymmärtäisi, ettei eri kielten välillä ole kuin pieni ero, esitämme ohjelman useilla eri kielillä. Ohjelman lopun jälkeen olevan poikkiviivan alapuolella on mahdollisesti esitetty miten ohjelmaa voitaisiin kokeilla MS–DOS–koneessa:
6.1.1 Pascal
{ Pascal-kieli }
PROGRAM olen(OUTPUT);
BEGIN
WRITELN('Terve! Olen Pascal-kielellä kirjoitettu ohjelma.');
END.
-------------------------------------------------------------------
- käynnistä vaikkapa Turbo Pascal
TP OLEN.PAS
- kirjoita ohjelma
- paina [Ctrl-F9]
6.1.2 C
Ajaminen:
- käynnistä vaikkapa Linuxin komentorivi
nano olen.c
- kirjoita ohjelma
- paina [Ctrl-X]
- käännä kirjoittamalla: gcc olen.c
- aja ohjelma kirjoittamalla:
./a.out
6.1.3 C++
Ajaminen
- käynnistä vaikkapa Linuxin komentorivi
nano olen.cpp
- kirjoita ohjelma
- paina [Ctrl-X]
- käännä kirjoittamalla: g++ olen.cpp
- aja ohjelma kirjoittamalla:
./a.out
6.1.4 Java
Ajaminen:
- Kirjoita esimerkki/Olen.java jollakin editorilla
- siis nykyhakemiston alihakemistoon koska package esimerkki
- käännä: javac esimerkki/Olen.java
- aja: java esimerkki.Olen
6.1.5 Fortran
C Fortran-kieli
PRINT*,'Terve! Olen Fortran-kielellä kirjoitettu ohjelma.'
STOP
END
6.1.6 ADA
-- ADA-kieli
with TEXT_IO; use TEXT_IO;
procedure ADA_MALLI is
pragma MAIN;
begin
PUT("Terve! Olen ADA-kielellä kirjoitettu ohjelma."); NEW_LINE;
end ADA_MALLI;
6.1.7 BASIC
REM BASIC-kieli
PRINT "Terve! Olen BASIC-kielellä kirjoitettu ohjelma."
END
-------------------------------------------------------------------
- käynnistä vaikkapa Quick Basic
QBASIC OLEN.BAS
- kirjoita ohjelma
- paina [Alt-R][Return]
6.1.8 APL
₳ APL-kieli
□ ←'Terve! Olen APL-kielellä kirjoitettu ohjelma.'
6.1.9 Modula–2
(* Modula-2 -kieli *)
MODULE olen;
FROM InOut IMPORT WriteString, WriteLn;
BEGIN
WriteString("Terve! Olen Modula-2 -kielellä kirjoitettu ohjelma.");
WriteLn;
END olen.
6.1.10 Common Lisp
Ajaminen:
- Käynnistä jokin Common Lisp -toteus, kuten SBCL
- kirjoita koodi kehoitteeseen, rivinvaihto ajaa sen
- editorilla voi tehdä myös tiedoston, joka sisältää halutun ohjelman
- Common Lisp -toteutukselle annetaan ko. tiedosto argumenttina:
esim. sbcl ohjelmani.lisp
6.1.11 FORTH
( FORTH-kieli )
: OLEN ( -- )
S" Terve! Olen FORTH-kielellä kirjoitettu ohjelma." TYPE CR
;
OLEN
6.1.12 Assembler
; 8086 assembler
DOSSEG
.MODEL TINY
.STACK
.DATA
viesti DB 'Terve! Olen 8086-assemblerilla kirjoitettu ohjelma.',0DH,0AH,'$'
.code
olen PROC NEAR
MOV AX,@@DATA
MOV DS,AX ; Viestin segmentti DS:ään
MOV DX,OFFSET viesti ; Viestin offset osoite DX:ään
MOV AH,09H ; Funktiokutsu 9 = tulosta merkkijono DS:DX
INT 21H ; Käyttöjärjestelmän kutsu
MOV AX,4C00H ; Funktiokutsu 4C = ohjelman lopetus
INT 21H ; Käyttöjärjestelmän kutsu
olen ENDP
END olen
-------------------------------------------------------------------
- kirjoita ohjelma jollakin ASCII-editorilla nimelle OLEN.ASM
- anna käyttöjärjestelmässä komennot (oletetaan että TASM on polussa):
TASM OLEN
TLINK OLEN
OLEN
Siis erot eri kielten välillä ovat hyvin kosmeettisia (Pascalin BEGIN on C:ssä { jne.). Jollakin kielellä asia pystyttiin esittämään hyvin lyhyesti ja jossakin tarvitaan enemmän määrittelyjä. Ainoastaan assembler–versio on sellaisenaan epäselvä, suoritettavia lauseita on täytynyt kommentoida enemmän.
6.2 Käytettävän kielen valinta
Kullakin ohjelmointikielellä on omat etunsa. Pascal on hyvin tyypitetty kieli ja sillä ei ole aloittelijankaan niin helppo tehdä eräitä tyypillisiä ohjelmointivirheitä kuin muilla kielillä. Standardi Pascal on kuitenkin hyvin suppea ja siitä puuttuu esim. merkkijonojen käsittely.
Turbo Pascalin laajennukset tekevät kielestä erinomaisen ja nopean kääntäjän ja UNIT–kirjastojen ansiosta se on todella miellyttävä käyttää. Nykyisin lisäksi Delphi–sovelluskehittimen kielenä on juuri Turbo Pascalista laajennettu Object Pascal. Delphi on eräs merkittävimmistä ja helppokäyttöisimmistä työkaluista Windows–ohjelmointiin. Borlandin julkaistua Kylix sovelluskehittimen, tarjotaan myös Linux ohjelmoijille samaa helppoutta..
BASIC–kieli on yleensä suppea kieli ja siksi helppo oppia. Lukuisten eri murteiden takia ohjelmien siirtäminen ympäristöstä toiseen on lähes mahdotonta. Microsoftin Visual Basic on kuitenkin Windows–ympäristössä nostanut Basicin jopa ohjelmankehittäjien työkaluksi. Alkuperäiset Basic-murteet olivat huonoja opiskelukielijä rajoittuneiden rakenteiden ja automaattisen muuttujanluomisen vuoksi. Visual Basicin uusimmissa versioissa on jo mukana yleisimmin tarvittavat rakenteet.
Fortran on luonnontieteellisissä sovelluksissa eniten käytetty kieli ja siihen on saatavissa laajat aliohjelmakirjastot useisiin numeriikan ongelmiin. Mikroissa kääntäjät ovat kuitenkin hitaita. Fortran–77 standardi on eräs parhaista standardeista, jota seuraamalla ohjelma toimii lähes koneessa kuin koneessa. Fortranin uusin standardi –90 tarjoaa ennen kielestä puuttuneet rakenteet.
ADA on Pascalin kaltainen vielä tarkemmin tyypitetty kieli, jossa on joitakin olio–ominaisuuksia. Se on Yhdysvaltain puolustusministeriön tukema kieli, joten se lienee tulevaisuuden kieliä. Sopii erityisesti raskaiden reaaliaikatoteutusten kirjoittamiseen (esim. ohjusjärjestelmät). GNU ADA tuo kääntäjän käyttämisen mahdolliseksi jokaiselle. Lisäksi ADA-95 tuo kieleen olio-ominaisuudet.
C–kieli on välimuoto Pascalista ja konekielestä. Ohjelmoijalle sallitaan hyvin suuria vapauksia, mutta toisaalta käytössä on hyvät tietotyypit ja rakenteet. Hyvä kieli osaavan ohjelmoijan käsissä, mutta aloittelija saattaa saada aikaan katastrofin. ANSI–C on suhteellisen hyvin standardoitu ja sitä seuraamalla on mahdollista saada ohjelma toimimaan pienin muutoksin myös toisessakin laiteympäristössä. Lisäksi ANSI–C:n tuoma funktioiden prototyypitys ja muutkin tyyppitarkistukset poistavat suuren osan ohjelmointivirheiden mahdollisuuksista, eivät kuitenkaan kaikkia. UNIX –käyttöjärjestelmän leviämisen myötä C on kohonnut erääksi kaikkein käytetyimmistä kielistä.
C++ on C–kielen päälle kehitetty oliopohjainen ohjelmointikieli. Aikaisemmin C–kielellä oli niin suuri merkitys, että se kannatti ehkä opetella aluksi. Nykyisin jokainen merkittävä C–kääntäjä on myös C++–kääntäjä. Oliopohjaisen ohjelmoinnin kannalta on parempi mitä aikaisemmin olio–ohjelmointi opetellaan. Valitettavasti C++ ei ole hypridikielenä (multi paradigm) paras mahdollinen ohjelmoinnin opetteluun. Kuitenkin paremmin opetteluun soveltuvat kielet ovat usein "leikkikieliä", kuten alkuperäinen Pascalkin oli. Delphi olisi mahtavan graafisen kirjastonsa ja kehitysympäristönsä ansiosta loistava opettelutyökalu, valitettavasti vaan lehti–ilmoituksissa harvoin haetaan Delphi–osaajia! Kohtuullisena kompromissina C++:kin voidaan valita opettelukieleksi, kunhan ei heti yritetä opetella kaikkia kielen kommervenkkeja. Jokin työkalu olio–ohjelmoinnin opiskeluun tarvitaan, sillä erityisesti tapahtumaohjattujen järjestelmien (kuten graafiset käyttöliittymät) ohjelmoinnissa oliopohjaisilla kielillä on hyvin merkittävä asema.
Monisteen loppuosassa käytämme C++–kieltä esimerkkikielenä sen siirrettävyyden takia.
Java on verkkoympäristössä tapahtuvaan ohjelmointiin kehitetty oliokieli. Javan erikoisuus on se, että se käännetään siirrettävään Java-tavukoodi muotoon. Tätä tavukoodia voidaan sitten suorittaa lähes missä tahansa ympäristössä Java-virtuaalikoneen avulla. Javaa on sanottu C++:aa yksinkertaisemmaksi, mutta kuitenkin Java kirjat ovat yhtä paksuja kuin C++ kirjatkin. Nykyisin onkin monesti niin, ettei itse kieli ole ongelma, vaan sille kehitettyjen aliohjelmakirjastojen opettelu ja käyttö. Javan suurimpana etuna on sen lähes kaikissa koneissa toimiva graafinen kirjasto. Java työkalut kehittyvät kovaa vauhtia ja saattaakin olla että jo seuraava ohjelmointikurssi pidetäänkin Javan pohjalta. Tällä kertaa vielä emme Javaa valitse, koska emme halua olla juoksemassa pelkkien muotioikkujen perässä. Tosin C++ opiskelu ei mitenkään haittaa mahdollista myöhempää Javaan siirtyvää ohjelmoijaa, pikemminkin päinvastoin.
Vaikkei valittu ohjelmointikieli olekaan kurssin tärkein asia, lukijan on kuitenkin hyvin tarkkaan ymmärrettävä kielestä esitettävät seikat, koska liika omapäinen "harhailu" saattaa kielen puuttuvien tarkistusten takia käydä kohtalokkaaksi. Joudumme ottamaan käyttöön esimerkiksi osoittimet jo heti opiskelun alkuvaiheessa.
7. C ja C++ –kielten alkeita
Mitä tässä luvussa käsitellään?
- C-kielisen ohjelman peruskäsitteet
- C++-kielisen ohjelman peruskäsitteet
- kääntämisen ja linkittämisen merkitys
- vakiotyyliset makrot (#define)
- C++:n vakiot (const)
Syntaksi:
Seuraavassa lauseke on mikä tahansa jonkin tyypin tuottava ohjelman osa, esim: 1+2 sin(x)+9 sormia henkilon_nimi
kommentti, C /* vapaata tekstiä, vaikka monta riviäkin */
kommentti, C++ // loppurivi vapaata tesktiä
sisällyttäminen: #include <tiedoston_nimi> tai #include "tiedoston_nimi"
makro: #define tunnisteXkorvattava_teksti // X mikä tahansa
// ei A-Z,a-z,_,0-9
// X tulee mukaan korv.tekst
vakio, C++: const tyyppi nimi = arvo;
tulostus, C: printf(format,lauseke, lauseke); // 0-n x lauseke
tulostus, C++: cout << lauseke << lauseke; // 1-n x <<lauseke
Ohjelman toteuttamista varten täytyy valita jokin todellinen ohjelmointikieli. Lopullisesta ohjelmasta ei valintaa toivottavasti huomaa. Valitsemme käyttökielen tällä kurssilla puhtaasti "markkinaperustein": käytetyin ja työelämässä vielä tällä hetkellä kysytyin - C++.
Tässä luvussa käsittelemme rinnakkain C ja C++ –kielistä ohjelmaa. Seuraavissa luvuissa käsitellään pelkästään C++:aa, kuitenkin siten että jos ohjelman tarkeninen on .C, niin ohjelma toimii samalla myös C-kielisenä ohjelmana.
7.1 C/C++
Mikä ero on C ja C++ –kielillä? Hyvin pieniä poikkeuksia lukuun ottamatta jokainen C–kielinen ohjelma on myös C++ –kielinen ohjelma. Voitaisiin sanoa että C++ on C:n oliopohjainen laajennus. Tosin C++:ssa on myös lisäyksiä, joilla ei sinänsä ole mitään tekemistä olio–ohjelmoinnin kanssa. Koska C++ ei ole "puhdas" oliokieli, sanotaan sitä hybridikieleksi (multi paradigm). Sitä että C++:lla voi kirjoittaa myös ei–olio–ohjelmia, pitävät monet olio–ohjelmoinnin asiantuntijat pahana. Toisaalta maailmassa on valtava määrä C–kielen osaajia, joille on näin luotu "pehmeä" lasku olio–ohjelmointiin ilman että heidän aikaisemmin kirjoittamansa koodi tulisi kerralla arvottomaksi. Ilman muuta C–kielen "painolasti" haittaa tietyllä tavalla C++–kielen kehittymistä, mutta reaalimaailmassa on tyydyttävä kompromisseihin.
Kumpi sitten kannattaa opetella ensiksi? Puristit sanovat että jokin "leikkikieli", jotkut että C ja "oliogurut" sanovat että ilman muuta jokin oliokieli. Otamme siis tällä kurssilla kultaisen(?) keskitien ja opettelemme C++:sta eräänlaisen "lasten" version, jossa jatkossa käytämme hyväksi myös kielen olio–ominaisuuksia. Loppumonisteessa merkintä C tarkoittaa ominaisuutta, joka toimii sekä C-kielessä, että C++-kielessä. Merkintä C++ tarkoittaa ominaisuutta, joka toimii vain C++-kielessä.
7.1.1 Hello World! C–kielellä
Aloitamme C–kielen opiskelun klassisella esimerkillä: Tulostetaan teksti "Hello world!" näyttöön:
c-alk\hello.c -ensimmäinen C-ohjelma
7.1.2 Hello World! C++ –kielellä
Huomattakoon että edellinen hello.c on sellaisenaan aivan hyvä C++ –ohjelma, mikäli nimi muutettaisiin hello.cpp:ksi. Kirjoitamme kuitenkin uuden version, jossa on käytetty hyväksi C++:an uusia ominaisuuksia:
c-alk\hello.cpp -ensimmäinen C++ ohjelma
Tehtävä: Nimi ja osoite
Kirjoita puhdas C- ohjelma ja iostreamia käyttävä C++- ohjelma, joka tulostaa:
Terve!
Olen Matti Meikäläinen 25 vuotta.
Asun Kortepohjassa.
Puhelinnumeroni on 603333.
7.2 Tekstitiedostosta toimivaksi konekieliseksi versioksi
7.2.1 Kirjoittaminen
Ohjelmakoodi kirjoitetaan millä tahansa tekstieditorilla tekstitiedostoon vaikkapa nimelle HELLO.CPP. Yleensä tiedoston tarkennin määrää ohjelman tyypin.
7.2.2 Kääntäminen
Valmis tekstitiedosto käännetään ko. kielen kääntäjällä. Käännöksestä muodostuu objektitiedosto, joka on jo lähellä lopullisen ohjelman konekielistä versiota. Objektitiedostosta puuttuu kuitenkin mm. kirjastorutiinit. Kirjastorutiinien kutsujen kohdalla on "tyhjät" kutsut.
7.2.3 Linkittäminen
Linkittäjällä (kielestä riippumaton ohjelma) liitetään kirjastorutiinit käännettyyn objektitiedostoon. Linkittäjä korvaa tyhjät kutsut varsinaisilla kirjastorutiinien osoitteilla kunhan saa selville mihin kohti muistia kirjastorutiinit sijoittuvat. Näin saadaan valmis ajokelpoinen konekielinen versio alkuperäisestä ohjelmasta.
7.2.4 Ohjelman ajaminen
Käännetty ohjelma ajetaan käyttöjärjestelmästä riippuen yleensä kirjoittamalla ohjelman alkuperäinen nimi. Tällöin käyttöjärjestelmän lataaja–ohjelma lataa ohjelman konekielisen version muistiin ja siirtää prosessorin ohjelmalaskurin ohjelman ensimmäisenä suoritettavaksi tarkoitettuun käskyyn. Vielä tässäkin vaiheessa osa aliohjelmakutsujen osoitteista voidaan muuttaa vastaamaan sitä todellista osoitetta, johon aliohjelma muistiin ladattaessa sijoittui. Tämän jälkeen vastuu koneen käyttäytymisestä on ohjelmalla. Onnistunut ohjelma päättyy aina ennemmin tai myöhemmin käyttöjärjestelmän kutsuun, jossa ohjelma pyydetään poistamaan muistista.
7.2.5 Varoitus
Alkuperäisellä editorilla kirjoitetulla ohjelmakoodilla ei ole tavallista kirjettä kummempaa virkaa ennen kuin teksti annetaan kääntäjä–ohjelman tutkittavaksi. Käännöksen jälkeen alkuperäinen teksti voitaisiin periaatteessa vaikka hävittää - käytössä tietysti alkuperäinen teksti säilytetään ylläpidon takia! Siis me kirjoitamme tekstiä, joka ehkä (toivottavasti) muistuttaa C++–kielen syntaksin mukaista ohjelmaa. Vasta käännös ja linkkaus tekevät todella toimivan ohjelman.
7.2.6 Integroitu ympäristö
On olemassa ohjelmankehitysympäristöjä, joissa editori, kääntäjä ja linkkeri (sekä mahdollisesti debuggeri, virheenjäljitin) on yhdistetty käyttäjän kannalta yhdeksi toimivaksi kokonaisuudeksi. Esimerkkinä Microsoftin Visual–C++ ja Borlandin Turbo–C++, Borland–C++ tai Borland–C++ Builder .
Esimerkiksi Borlandin ympäristöissä ohjelma kirjoitetaan tekstinä ja kun ohjelmakoodi on valmis, saadaan koodi käännettyä, linkitettyä ja ladattua ajoa varten vain painamalla [F9](tai [Ctrl-F9] versiosta riippuen) .
7.3 Ohjelman yksityiskohtainen tarkastelu
Seuraavaksi tutkimme ohjelmaa lause kerrallaan:
7.3.1 Kommentti
/* Ohjelma tulostaa tekstin Hello world! */
Ohjelman alussa on kommentoitu mitä ohjelma tekee. Yleensä ohjelmakoodit on hyvä varustaa kuvauksella siitä, mitä ohjelma tekee, kuka ohjelman on tehnyt, milloin ja miksi. Milloin ohjelmaa on viimeksi muutettu, kuka ja miten.
Lisäksi jokainen vähänkin ei–triviaali lause tai lauseryhmä kommentoidaan. Kommenttien tarkoituksena on kuvata ohjelmakoodia lukevalle lukijalle se mistä on kyse.
Kommentti alkaa /* –merkkiyhdistelmällä ja päättyy */ –merkkiyhdistelmään. Kommentteja voidaan sijoittaa C–koodissa mihin tahansa mihin voitaisiin pistää myös välilyönti. Rivin loppuminen ei sinänsä lopeta kommenttia. Kommentin sisällä SAA esiintyä / ja * –merkkejä yhdessä tai erikseen, muttei lopettavaa yhdistelmää */.
Yleinen virhe on unohtaa kommentin loppusulku pois. Mikäli esimerkissämme puuttuisi kommentin loppusulku, olisi koko loppuohjelma kommenttia ja mitään ohjelmaa ei siis olisikaan. Mikäli kääntäjä antaa vyöryn ihmeellisiä virheilmoituksia, kannattaa aina ensin tarkistaa kommenttisulkujen täsmäävyys. Tosin tähän auttaa nykyisten ohjelmointiympäristöjen värikoodien käyttö eri ohjelman osille, eli esimerkiksi kommentit näkyvät eri värisinä ja puuttuva komenttisulku paljastuu välittömästi.
// Ohjelma tulostaa tekstin Hello world!
C++:ssa kommentti voidaan ilmaista myös // –merkkiyhdistelmällä, jolloin rivinloppu lopettaa kommentin. Siirrettävyyden takia niissä ohjelman osissa, jotka ovat puhtaasti C–kieltä, voitaisiin käyttää /* */ –kommentointia ja muualla // –kommentointia.
7.3.2 Kirjastofunktioiden esittely
#include <stdio.h>
#include <iostream.h> // tai #include <iostream>
Tarvitsemme ohjelmassamme tulostusfunktiota printf. Tämä funktio löytyy C–kielen kirjastosta ja se on esitelty otsikkotiedostossa stdio.h. Kääntäjää varten meidän täytyy esitellä millaisia parametrejä funktiolle voidaan välittää. Kutakin kirjastoa varten on esittelytiedostot ("header"–tiedostot, yleensä nimetään .h), joissa kirjastofunktioiden parametrilistat on esitelty.. Tässä ohjelmassa stdio.h –tiedostosta käytetään vain printf:n esittelevää riviä ja voitaisiin myös kirjoittaa #include –rivin tilalle printf:n esittely:
int printf(const char __format, ...);
mutta oikean muodon muistaminen voisi olla vaikeampaa. (Huom! Edellä sanottua on hieman yksinkertaistettu, sillä periaatteessa <> voidaan toteuttaa myös muulla tavalla)
C++:an tulostusvirta cout löytyy kirjastosta iostream.h. cout –olion määrittely iostream.h –tiedostossa on niin monimutkainen ettei sitä käytännössä voisi itse edes kirjoittaa!
#include
on C–kielen esikääntäjän (pre–prosessor) käsky, joka ilmoittaa että perässä olevan niminen tiedosto on luettava ja käsiteltävä koodin sekaan tässä kohti käännöstä.
< >
–merkit tiedoston nimen ympärillä ilmoittavat, että ko. tiedostoa etsitään C:n systeemin mukaisesta INCLUDE-hakemistosta. Mikäli nimi suljettaisiin "–merkeillä, etsittäisiin tiedostoa myös käyttäjän kotihakemistosta. Näin voidaan tehdä omia tehtäväkohtaisia kirjastoja.
7.3.3 Päämodulin esittely
int main(void)
Seuraavaksi esitellään ohjelman pääohjelma ("oikea" ohjelma koostuu isosta kasasta aliohjelmia ja yhdestä pääohjelmasta, jonka nimi on main).
int tarkoittaa, että pääohjelmamme palauttaa kokonaisluvun kutsuvalle ohjelmalle - käyttöjärjestelmälle. Palautettavan luvun arvo on tyypillisesti 0 mikäli kaikki menee ohjelman aikana niin kuin pitääkin ja muilla numeroilla ilmaistaan erilaisia virhetilanteita. MS–DOS:ssa tätä arvoa voidaan tutkia ERRORLEVEL–muuttujalla.
main tarkoittaa pääohjelman nimeä. Tämä TÄYTYY aina olla main.
(void) ilmoittaa, että funktio jota kirjoitamme ei tarvitse yhtään parametriä (eng. void = mitätön). Myöhemmin huomaamme että myös pääohjelmalla voi olla parametrejä. Huom: C++:ssa void voidaan myös jättää pois sulkujen sisältä.
int main() // Myös oikea muoto C++:ssa, ei C:ssä
void main() // Useissa kirjoissa esiintyvä VÄÄRÄ muoto :-(
7.3.4
{ } C:ssä isompi joukko lauseita kootaan yhdeksi lauseeksi sulkemalla lauseet aaltosulkuihin. Funktion täytyy aina sisältää aaltosulkupari, vaikka siinä olisi vain 0 tai 1 suoritettavaa lausetta.
7.3.5 Tulostuslause
printf("Hello world!\n")
printf("?") tulostaa ajonaikana sen tekstin, joka on lainausmerkkien välissä. Myöhemmin opimme, että funktion kutsussa voi olla myös useampia parametrejä ja voidaan käyttää myös muuttujia.
\n on C:n erikoismerkki, joka kääntyy merkkijonon sisällä käyttöjärjestelmän rivinvaihtomerkiksi. Tällaisen esiintyminen merkkijonossa aiheuttaa tulostuksen siirtymisen uuden rivin alkuun. Muista käyttää!
cout << "Hello world!" << endl;
cout on C++:n yksi tulostustietovirtaluokan (output stream class) ostream esiintymä, eli olio jolle (coutin tapauksessa) siirretyt merkit tulostuvat näyttöön (Console OUTput) .
<<
on operaattori, jolla C++:ssa on useita merkityksiä. Tässä tapauksessa kun operaattorin vasempana operandina on tietovirtaolio, on kyseessä tietovirtaan siirtämisoperattoori (inserter). Käytämme tästä jatkossa nimitystä tulostusoperaattori. Koska operaattorikin on vain aliohjelma, voisi <<–operaattorin varsinainen kutsumuoto olla esimerkiksi
operator<<(cout, "Hello world!");
operator<<(cout, endl);
Koska operaattori palauttaa cout–olion, voidaan kutsu kirjoittaa myös (samoin kuin 1+2 palauttaa kokonaisluvun) ketjumuotoon
operator<<( operator<<(cout, "Hello world!"), endl);
// lyhennetty muoto
(cout << "Hello world!") << endl;
joka ilman sulkuja on ohjelmassa hello.cpp esitetty muoto. Operaattorin muoto voi olla myös:
cout.operator<<("Hello world!");
cout.operator<<(endl);
// josta ketjutettuna:
(cout.operator<<("Hello world!")).operator<<(endl);
// lyhennetty muoto:
(cout << "Hello world!") << endl;
Vertaa vastaavaan ketjuttamiseen + –operaattorin kanssa:
int i,j;
i = 1 + 2; j = i + 4;
// voidaan kirjoittaa myös:
j = ( 1 + 2 ) + 4;
endl
on tietovirran muotoilija (manipulator), joka vaihtaa tulostuksen uudelle riville ja tyhjentää tulostuspuskurin. Rivinvaihto voitaisiin tehdä myös jonolla "\n", mutta tulostuspuskuri on muistettava myös tyhjentää. endl
olisi hyvä olla vähintään viimeisessä tulostettavassa lauseessa ennen pysähtymistä. Joissakin koneissa ohjelmat voivat toimia myös ilman endl
:ää, mutta jos halutaan standardin mukaista koodia, joka toimii KAIKISSA koneissa, on sitä syytä käyttää.
Kumpiko tulostuslause sitten on parempi? Jos kirjoitetaan C–koodia, on tietysti käytettävä printf:ää, mutta C++:n tapauksessa molemmat käyvät. cout
on parempi sen vuoksi, että siihen liittyvään tulostusoperaattorin voidaan lisätä uusia tietotyyppejä tulostettavaksi (kuormittaa, operator overloading). Toisaalta tulostuksen muotoilu on helpompaa printf
:n kanssa. Määrityksissä sanotaan että molempia voi käyttää, muttei samalla tulostusrivillä. Laajennettavuuden takia valitaan cout aina kuin se vain on mahdollista.
7.3.6 Lauseen loppumerkki ;
; puolipiste lopettaa lauseen. Puolipiste voidaan sijoittaa mihin tahansa lopetettavaan lauseeseen nähden. Sen eteen voidaan jättää välilyöntejä tai jopa tyhjiä rivejä. Sen pitää kuitenkin esiintyä ennen uuden lauseen alkua. Näin C–kieli ei ole rivisidonnainen, vaan C–kielinen lause voi jakaantua usealle eri riville tai samalla rivillä voi olla useita C-kielisiä lauseita.
Puolipisteen unohtaminen on tyypillinen syntaksivirhe. Ylimääräiset puolipisteet aiheuttavat tyhjiä lauseita, joista tosin ei ole mitään haittaa: “Tyhjän tekemiseen ei kauan mene” – sanoo tyhjän toimittaja.
7.3.7 Funktion arvon palautus
return 0;
return jokainen C–funktio tulisi lopettaa return–lauseeseen. Mikäli funktio on esitelty muun kuin void–tyyppiseksi, pitää kertoa myös arvo, joka palautetaan. Funktio voi tarvittaessa sisältää myös useita eri return–lauseita. Heti kun kohdataan ensimmäinen return–lause, poistutaan funktiosta. void–tyyppisessä funktiossa return–lause ei ole pakollinen.
7.3.8 Isot ja pienet kirjaimet
Isoilla ja pienillä kirjaimilla on C–kielessä eri merkitys. Siis EI VOIDA KIRJOITTAA:
Int MAIN(Void) /* VÄÄRIN! */ :-(
7.3.9 White spaces, tyhjä
Välilyöntejä, tabulointimerkkejä, rivinvaihtoja ja sivunvaihtoja nimitetään yleisesti yhteisellä nimellä "white space". Käännettäessä kommentit muutetaan yhdeksi välilyönniksi, joten myös kommenteista voitaisiin käyttää nimitystä "white space". Jatkossa käytämme nimitystä tyhjä tai tyhjä merkki, kun tarkoitamme "white space".
C–koodi voi sisältää tyhjiä merkkejä missä tahansa, kunhan niitä ei kirjoiteta keskelle sanaa tai tekstiä määrittelevän ""–parin ollessa auki. ""–parin sisällä tyhjätkin merkit ovat merkityksellisiä. Tyhjillä merkeillä ei saa myöskään sotkea esikääntäjälle tarkoitettuja #–direktiivi –rivejä, näiden pitää muodostaa täsmälleen yksi rivi.
Lainausmerkkeihin suljettu jono voidaan tarvittaessa katkaista tyhjillä merkillä sulkemalla ja avaamalla lainausmerkit. Esimerkiksi
"Kissa"
"istuu"
- >
"Kissaistuu"
Tarvittaessa C–ohjelman riviä voidaan jatkaa uudelle riville kirjoittamalla \–merkki edellisen rivin loppuun ja sen jälkeen välittömästi rivinvaihto.
Siis kääntäjän kannalta malliohjelmamme voitaisiin kirjoittaa myös seuraavillakin tavoilla:
#include\ :-(
<stdio.h>
int
main
(
void
)
{
printf
(
"Hel\
lo "
"w" /* kommentti keskellä jonoa */ "or"
"ld!"
"\n"
)
;
return
0
;
}
#include <stdio.h> :-(
int main(void){
printf("Hello "
"world!\n"
);return 0;}
#include <stdio.h> :-(
int main(void){printf("Hello world!\n");return 0;}
Yleinen tyyli on kuitenkin jakaa koodia riveihin ja sisentää lohkoja muutamalla pykälällä. Kunnes lukija on varma omasta tyylistään, kannattaa matkia tässä monisteessa (ei kuitenkaan edellisiä esimerkkejä) esitettyä kirjoitustapaa ohjelmille.
7.4 Makro-direktiivi ja vakiot
#include oli tiedote esikääntäjälle siitä, että koodin sekaan täytyy lukea välillä jokin toinen teksti.
Makro-direktiivi #define on tiedote siitä, että tällä määreellä myöhemmin tekstissä esiintyvät sanat täytyy korvata toisella sanalla/sanoilla/merkeillä.
Yksinkertaisten makrojen käytön hyöty on siinä, että usein ohjelmassa esiintyvät sanat/vakionumerot voidaan koota helposti hallittavaksi ohjelman alkuun tai jopa omaan määrittelytiedostoonsa.
Siis #define–direktiivi on puhdas tekstinkäsittelyotus, joka toimii suurin piirtein seuraavasti:
c-alk\makrot.c - esimerkki mitä #define korvaa
#define TERVE "Hello "
tarkoittaisi esikääntäjälle:
vaihda kaikki TERVE–sanat merkkijonoiksi "Hello "
eli seuraavat vaihdettaisiin:
p=TERVE+1 /* => p="Hello "+1 */
"Kissa"TERVE"istuu" /* => "Kissa""Hello ""istuu" */
TERVE MIEHEEN /* => "Hello " MIEHEEN */
muttei seuraavia
TERVEYDEKSI /* Ei pelkkä TERVE-sana */
TERVE_MIEHEEN /* Ei pelkkä sana */
"OLEN OLLUT TERVE 2 PÄIVÄÄ" /* Lainausmerkeissä */
Terve MIEHEEN /* Eri tavalla kirjoitettu */
Huomattakoon, ettei lainausmerkkien sisällä oleviin sanoihin kajota lainkaan!
Määriteltävä sana voidaan kirjoittaa isoja ja/tai pieniä kirjaimia käyttäen, mutta yleiseen C–tyyliin kuuluu kirjoittaa #define –määritellyt sanat isoilla kirjaimilla.
Vaihdettava sana loppuu ja korvaava jono alkaa ensimmäisestä ei–kirjaimesta tai numerosta, eli
#define X=2; /* VAARALLINEN! X => =2; */ :-(
int i=X; /* => int i==2;; */
7.4.1 Vakiomerkkijonot
Esikääntäjän makro-ominaisuutta hyväksikäyttäen voimme määritellä ohjelmaamme vakioita; arvoja jotka esiintyvät ohjelmassa täsmälleen yhden kerran. Näin ohjelmastamme saadaan helpommin muuteltava. Esimerkiksi seuraava ohjelma tulostaisi myös tekstin "Hello world!":
c-alk\hello2.c - tervehdys vakioksi
Miksikö? Koska esikääntäjä muuttaisi lauseen
printf(TERVE MAAILMA"!\n");
muotoon
printf("Hello " "world""!\n");
Kun tästä lisäksi poistetaan ylimääräiset "white space"–merkit saadaan:
printf("Hello world!\n");
7.4.2 Vakiolukuarvot
Vakiomäärittelyä voitaisiin käyttää esimerkiksi kokonaislukuvakioiden määrittelemiseen:
c-alk\kuutio.c - monikulmion tiedot vakioksi
/* Ohjelma tulostaa tietoja monitahokkaasta */
#include <stdio.h>
#define TAHOKAS "Kuutiossa"
#define KARKIA 8
#define SIVUTASOJA 6
#define SARMIA 12
int main(void)
{
printf("%20s on %2d kärkeä,\n" ,TAHOKAS,KARKIA);
printf("%20s %2d sivutasoa ja\n"," " ,SIVUTASOJA);
printf("%20s %2d särmää.\n" ," " ,SARMIA);
return 0;
}
Tehtävä 7.51 Tetraedri
Muuta edellistä ohjelmaa siten, että tulostetaan samat asiat tetraedristä.
Tehtävä 7.52 printf ja %
Mitä arvelet %2d:n ja %20s:n merkitsevän edellisessä esimerkissä, kun ohjelma tulostaa seuraavan tekstin:
0 1 2 3 4
1234567890123456789012345678901234567890
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Kuutiossa on 8 kärkeä,
6 sivutasoa ja
12 särmää.
7.4.3 Muita makro-"temppuja"
Koska C–kielen makro on todellakin vain tekstinkorvaus, muuttaa esikääntäjä seuraavan tekstin
c-alk\makroja.c - "turhia" makrotemppuja
#include <stdio.h>
#define ALKU int main(void) {
#define LOPPU return 0; }
#define mk *100
#define km *1000
ALKU
double hinta_penneina,matka_m;
hinta_penneina = 5 mk;
matka_m = 3.5 km;
LOPPU
muotoon
... kaikki stdio.h:ssa oleva koodi makrot käsiteltynä ...
int main(void) {
double hinta_penneina,matka_m;
hinta_penneina = 5 *100;
matka_m = 3.5 *1000;
return 0; }
Tosin tällaisia makrotemppuiluja kannattaa välttää ellei siitä saa suunnatonta ohjelman ylläpidollista hyötyä. Tarvittaessa pelkkä esikäännös voidaan tehdä vaikka Borland C++:lla:
C:\OMAT\OHJELMOI\VESA>CPP makroja.c[RET]
tulee tiedosto makroja.i
7.4.4 Esikääntäjän toiminnasta
Esiprosessori kerää jokaisen löytämänsä #define -alkuisen rivin omaan sisäiseen listaansa. Jos tulee vastaan #undef jollekin listassa mainitulle sanalle, poistetaan sana listasta. Esikääntäjä lukee varsinaisia rivejä (ei #-alkuisia) token ("sananen") kerrallaan. Tämä token on pienin esikääntäjän ymmärtämä yksikkö (joka on erotettu ympäristöstä ei–muuttujaan sallituilla kirjaimilla). Esimerkiksi rivillä
a;bd(kissa)
on 3 tokenia: a, bd ja kissa
.
Kun esikääntäjä käsittelee yhtä tokenia, etsii se sitä sisäisestä listastaan. Jos token löytyy listasta, korvataan se vastaavalla merkkijoukolla ja listan ko. token merkitään käytetyksi. Mahdollinen korvaus muodostaa 0 – n uutta tokenia. Nämä kaikki käsitellään rekursiivisesti esikääntäjän listan kanssa kunnes yhtään korvausta ei voida tehdä (token ei löydy listasta tai kaikki listan alkiot on merkitty käytetyiksi). Näin tämä yksi token on saatu muutetuksi. Tämän jälkeen listan kaikki alkiot vapautetaan ja siirrytään rivin seuraavaan mahdolliseen tokeniin.
Miksi listaan merkitään sanoja käytetyksi? Miten kävisi muuten (kävi vanhoilla C–esikääntäjillä) seuraavan ohjelmanpätkän kanssa:
#define K T T
#define T K K
K
T
Lyhyesti: esikääntäjä ei korvaa sisäkkäisiä #definejä heti, vaan sitten kun ne esiintyvät. Näin useissa normaalitapauksissa (kuten ei edelläkään) ei ole väliä sillä, missä järjestyksessä #define–rivit kirjoitetaan.
Tehtävä 7.53 Sisäkkäiset makrot
Miten edellisestä K T T , esimerkissä korvautuu K? Entä miten T?
7.5 C++:n vakiot
Koska makrojen käyttöön liittyy tiettyjä tyyppitarkistuksiin/oikeellisuuskirjoitukseen liittyviä riskejä, kannattaa C++:ssa mieluummin käyttää tyypitettyjä vakioita (seuraavan esimerkin const–rivit toimivat tosin C–kielessäkin):
c-alk\hello2.cpp - tervehdys "vakioksi"
// Ohjelma tulostaa tekstin Hello world!
#include <iostream.h>
const char *TERVE = "Hello ";
const char *MAAILMA = "world";
int main(void)
{
cout << TERVE << MAAILMA << "!" << endl;
return 0;
}
Valitettavasti on tilanteita, joissa makroja on edelleen lähes pakko käyttää. Esimerkiksi merkkijonojen yhdistäminen TERVE MAAILMA tyyliin ei onnistu. Myöhemmin opimme joitakin muitakin tilanteita, joissa makrot ovat "välttämättömiä". Erityisesti lukuvakioiden määrittelyssä const–vakiot ovat omimmillaan:
c-alk\kuutio.cpp - monikulmion tiedot vakioksi
// Ohjelma tulostaa tietoja monitahokkaasta
#include <iostream.h>
#include <iomanip.h>
const char *TAHOKAS = "Kuutiossa";
const int KARKIA = 8;
const int SIVUTASOJA = 6;
const int SARMIA = 12;
int main(void)
{
cout<<setw(20)<<TAHOKAS<<" on "<<setw(2)<<KARKIA <<" kärkeä,\n";
cout<<setw(20)<<" " <<" "<<setw(2)<<SIVUTASOJA<<" sivutasoa,\n";
cout<<setw(20)<<" " <<" "<<setw(2)<<SARMIA <<" särmää" << endl;
return 0;
}
Tehtävä 7.54 Vakiot vai #define
Päättele seuraavasta esimerkistä miksi const- vakiot ovat parempia kuin #define - makrot.
c-alk\const.cpp - vakion ja #definen erot
// Ohjelmalla tutkitaan mikä ero on const ja #define -vakioiden välillä
#include <iostream.h>
const int sormia = 1 + 4;
#define VARPAITA 1 + 4 :-(
int main(void)
{
int syht = 2 * sormia;
int vyht = 2 * VARPAITA;
cout << "Sormia = " << syht << " ja varpaita = " << vyht << endl;
return 0;
}
Selitä mistä virhe johtuu? Miten virhe saadaan tässä tapauksessa poistettua?
8. C++–kielen muuttujista ja aliohjelmista
Mitä tässä luvussa käsitellään?
- muuttujat
- malliohjelma jossa tarvitaan välttämättä muuttujia
- osoiteoperaattori, &
- osoitin muuttujat ja epäsuora osoitus, *
- aliohjelmat, eli funktiot
- erilaiset aliohjelmien kutsumekanismit
- referenssimuuttujat eli viitemuuttujat, &
Syntaksi:
Seuraavassa muut = muuttujan nimi, koostuu A-Z,a-z,0-9,_, ei ala 0-9
muut.esittely: tyyppi muut = alkuarvo; // 0-1 x =alkuarvo
sijoitus: muut = lauseke;
lukeminen, C: scanf(format, osoite, osoite, ...) // 0-n x osoite
lukeminen, C++ cin >> olio >> olio; // 1-n x olio
aliohj.esittely: tyyppi aliohj_nimi(tyypi muut, tyyppi muut); // 0-n x muut
aliohj.kutsu muut = aliohj_nimi(arvo, arvo); // 0-1 x muut=, 0-n x arvo
muut.osoite: &muut
osoitinmuut.esit: tyyppi *pMuut_nimi
epäsuora osoitus: *osoite
viitemuut.esit. tyyppi &rMuut_nimi
Jatkossa puhumme C–kielestä kun tarkoitamme ominaisuuksia, jotka ovat sekä varsinaisessa C-kielessä että C++–kielessä. Jos puhumme C++–kielen ominaisuuksista, tarkoitamme ominaisuuksia joita ei ole C–kielessä. Aloittelevan lukijan kannalta tällä jaottelulla ei niinkään ole merkitystä. Merkitystä on ainoastaan silloin, jos joutuu kirjoittamaan ohjelmaa, jonka on mentävä läpi puhtaasta C–kääntäjästä. Tällainen tilanne voi tulla vastaan esimerkiksi sulautettuja järjestelmiä ohjelmoitaessa (=järjestelmät joissa tietokone on "näkymättömässä" osassa, kodinelektroniikka, "kännykät", ohjauslaitteet). Näihin ei vielä aina ole saatavilla C++–kääntäjää.
8.1 Mittakaavaohjelman suunnittelu
Satunnainen matkaaja ajelee tällä kertaa kotimaassa. Autoillessa hänellä on käytössä Suomen tiekartan GT –karttalehtiä, joiden mittakaava on 1:200000. Viivoittimella hän mittaa kartalta milleinä matkan, jonka hän aikoo ajaa. Ilman matikkapäätä laskut eivät kuitenkaan suju. Siis hän tarvitsee ohjelman, jolla matkat saadaan muutettua kilometreiksi.
Millainen ohjelman toiminta voisi olla? Vaikkapa seuraavanlainen:
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>35[RET]
Matka on luonnossa 7.0 km.
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>352[RET]
Matka on luonnossa 70.4 km.
C:\OMA\MATKAAJA>
Edellisessä toteutuksessa on vielä runsaasti huonoja puolia. Mikäli samalla haluttaisiin laskea useita matkoja, niin olisi kätevämpää kysellä matkoja kunnes kyllästytään laskemaan. Lisäksi olisi ehkä kiva käyttää muitakin mittakaavoja kuin 1:200000. Muutettava matka voitaisiin tarvittaessa antaa jopa ohjelman kutsussa. Voimme lisätä nämä asiat ohjelmaan myöhemmin, kunhan kykymme siihen riittävät. Toteutamme nyt kuitenkin ensin mainitun ohjelman.
8.2 Muuttujat
Ohjelmamme poikkeaa aikaisemmista esimerkeistä siinä, että nyt ohjelman sisällä tarvitaan muuttuvaa tietoa: matka millimetreinä. Tällaiset muuttuvat tiedot talletetaan ohjelmointikielissä muuttujiin. Muuttuja on koneen muistialueesta varattu tarvittavan kokoinen "muistimöhkäle", johon viitataan käytännössä muuttujan nimellä.
Kone viittaa muistipaikkaan muistipaikan osoitteella. Kääntäjä-ohjelman tehtävä on muuttaa muuttujien nimiä muistipaikkojen osoitteiksi. Kääntäjälle täytyy kuitenkin kertoa aluksi minkä kokoisia 'möhkäleitä' halutaan käyttää. Esimerkiksi kokonaisluku voidaan tallettaa pienempään tilaan kuin reaaliluku. Mikäli haluaisimme varata vaikkapa muuttujan, jonka nimi olisi matka_mm kokonaisluvuksi, kirjoittaisimme seuraavan C-kielisen lauseen (muuttujan esittely):
int matka_mm; /* yksinkertaisen tarkkuuden kokonaisluku */
Pascal –kielen osaajille huomautettakoon, että Pascalissahan esittely oli päinvastoin:
VAR matka_mm: INTEGER;
Tulos, eli matka kilometreinä voitaisiin laskea muuttujaan matka_km. Tämän muuttujan on kuitenkin oltava reaalilukutyyppinen (ks. esimerkkiajo), koska tulos voi sisältää myös desimaaliosan:
double matka_km; /* kaksinkertaisen tarkkuuden reaaliluku */
On olemassa myös yksinkertaisen tarkkuuden reaaliluku float, mutta emme tarvitse sitä tällä kurssilla. Samoin kokonaisluvusta voidaan tehdä etumerkillinen, etumerkitön, "lyhyt" tai "kaksi kertaa isompi":
signed int matka_km; /* Sama kuin int matka_km */
unsigned int sormia; /* Aina positiivinen */
short int varpaita; /* Ei koskaan kovin montaa */
long int valtion_velka_Mmk;/* Tarvitaan ISO arvoalue */
int–tyyppiä ei edellä olisi pakko kirjoittaa:
signed matka_km; /* Sama kuin int matka_km */
unsigned sormia; /* Aina positiivinen */
short varpaita; /* Ei koskaan kovin montaa */
long valtion_velka_Mmk;/* Tarvitaan ISO arvoalue */
Muuttujan määritys voisi olla myös
const volatile unsigned long int sadasosia;
Tulemme kuitenkin aluksi varsin pitkään toimeen pelkästään seuraavilla tyypeillä ja niiden osoittimilla (Turbo C++:n arvoalueet):
int - kokonaisluvut - 32 768 - 32 767, 16-bit, tai
- -2 147 483 648 - 2 147 483 647, 32-bit systeemit
double - reaaliluvut n. 15 desim. - > 1.7e308
char - kirjaimet, kokonaislukuina - 128 - 127 (tai 0-255)
8.2.1 Matkan laskeminen
Ohjelman käyttämä mittakaava kannattaa sijoittaa ehkä vakioksi, tällöin ainakin ohjelman muuttaminen on helpompaa. Samoin vakioksi kannattaa sijoittaa tieto siitä, paljonko yksi km on millimetreinä (1 km = 1000 m, 1 m = 1000 mm). Ohjelmastamme tulee tällöin esimerkiksi seuraavan näköinen:
c-muut\matka.cpp - mittakaavamuunnos 1:200000 kartalta
// matka.cpp
// Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
// Vesa Lappalainen 18.9.1991
#include <iostream>
using namespace std;
const double MITTAKAAVA = 200000.0;
const double MM_KM = 1000.0*1000.0;
int main(void)
{
int matka_mm;
double matka_km;
// Ohjeet
cout << "Lasken 1:" << MITTAKAAVA
<< " kartalta millimetreinä mitatun matkan\n";
cout << "kilometreinä luonnossa.\n";
// Syöttöpyyntö ja vastauksen lukeminen
cout << "Anna matka millimetreinä>";
cin >> matka_mm;
// Datan käsittely
matka_km = matka_mm*MITTAKAAVA/MM_KM;
// Tulostus
cout << "Matka on luonnossa "<< matka_km << " km." << endl;
return 0;
}
Lukija huomatkoon, että muuttujien ja vakioiden nimet on pyritty valitsemaan siten, ettei niitä tarvitse paljoa selitellä. Tästä huolimatta isommissa ohjelmissa on tapana kommentoida muuttujan esittelyn viereen muuttujan käyttötarkoitus. Mekin pyrimme tähän myöhemmin.
Tehtävä 8.55 Vakion korvaaminen
Korvaa vakioiden esittely # define makroilla. Mitä vaaroja liittyy makrojen käyttöön tässä tapauksessa.
8.2.2 Muuttujan nimeäminen
Muuttujien nimissä on sallittuja kaikki kirjaimet (a–z, A–Z) sekä numerot (0–9) sekä alleviivausviiva (_). Muuttujan nimi ei kuitenkaan saa alkaa numerolla. Muuttujia saa esitellä (declare) useita samalla kertaa, kunhan muuttujien nimet erotetaan toisistaan pilkulla.
Muuttujan nimi ei myöskään saa olla mikään varatuista sanoista (reserved word):
C-kielen varatut sanat:
C-kielen varatut sanat:
auto break case char
const continue default do
double else enum extern
float for goto if
int long register return
short signed sizeof static
struct switch typedef union
unsigned void volatile while
C++:ssa on lisäksi varattuja sanoja:
asm bool catch class
const_cast delete dynamic_cast explicit
false friend inline mutable
namespace new operator private
protected public __rtti static_cast
template this throw true
try typeid typename reinterpret_cast
using virtual wchar_t
Tehtävä 8.56 Varatut sanat
Merkitse edelliseen taulukkoon kunkin varatun sanan viereen se, missä kohti monistetta ko. sana on selitetty.
Tehtävä 8.57 Muuttujan nimeäminen
Mitkä seuraavista ovat oikeita muuttujan esittelyjä ja mitkä niistä ovat hyviä:
int o;
int 9_kissaa;
int _9_kissaa;
double pitkä_matka;
int i, j, kissojen_maara;
int auto, pyora, juna;
8.2.3 Muuttujalle sijoittaminen =
Muuttujalle voidaan antaa ohjelman aikana uusia arvoja käyttäen joko sijoitusoperaattoria = tai aliohjelmakutsua, joka muuttaa muuttujan arvoa osoittimen välityksellä (tai ++,––,+=,–=,*= jne. –operaattoreilla).
Sijoitusmerkin = vasemmalle puolelle tulee muuttujan nimi ja oikealle puolelle mikä tahansa lauseke, joka tuottaa halutun tyyppisen tuloksen (arvon). Lausekkeessa voidaan käyttää mm. operaattoreita +,–,*,/ ja funktiokutsuja. Lausekkeen suoritusjärjestykseen voidaan vaikuttaa suluilla (ja):
kengan_koko = 42;
pi = 3.14159265358979323846;
// usein käytetään math.h:n M_PI vakiota
pi = M_PI;
pinta_ala = leveys * pituus;
ympyran_ala = pi*r*r;
hypotenuusa = vastainen_kateetti/sin(kulma);
matka_km = matka_mm*MITTAKAAVA/MM_KM;
Seuraava sijoitus on tietenkin mieletön:
r*r = 5.0; /* MIELETÖN USEIMMISSA OHJELMOINTI KIELISSA! */ :-(
Eli sijoituksessa tulee vasemmalla olla sen muistipaikan nimi, johon sijoitetaan ja oikealla arvo joka sijoitetaan.
Huom! C–kielessä = merkki EI ole yhtäsuuruusmerkki, vaan nimenomaan sijoitusmerkki. Yhtäsuuruusmerkki on ==.
Tehtävä 8.58 Muuttujien esittely
Esittele edellisissä sijoitus - esimerkeissä tarvittavat muuttujat.
8.2.4 Muuttujan esittely ja alkuarvon sijoittaminen
Muuttujan esittelyn (declaration) yhteydessä muuttujalle voidaan antaa myös alkuarvo (alustus, definition). Muuttujien alustaminen tietyllä arvolla on tärkeää, koska alustamattoman muuttujan arvo saattaa olla hyvinkin satunnainen. Alustamattoman muuttujan käyttö onkin jälleen eräs tyypillinen ohjelmointivirhe.
int kengan_koko = 32, takin_koko = 52;
double pi = 3.14159265358979323846, r = 5.0;
8.3 Muuttujan arvon lukeminen päätteeltä
8.3.1 scanf ja muuttujan osoite &
Muuttujalle voidaan sijoittaa uusi arvo myös C–kielen scanf–funktiolla. Funktion kutsussa on kaksi osaa: ohje– eli format –osa ja lista muuttujien osoitteista, joihin arvot sijoitetaan. Matka olisi voitu lukea myös kutsulla:
scanf("%d",&matka_mm);
Tässä "%d" tarkoittaa, että kutsun listaosassa vastaavassa paikassa oleva muuttuja on tyyppiä d eli desimaalinen (annetaan 10–järjestelmässä) kokonaisluku.
&matka_mm tarkoittaa muuttujan matka_mm osoitetta. Funktiolle ei voida viedä parametrinä itse muuttujan arvoa, koska funktio ei tämän perusteella tiedä mihin se luetun arvon sijoittaisi. Tämän takia välitämmekin muuttujan osoitteen, eli sen paikan muistissa, missä muuttujan arvo sijaitsee. Näin aliohjelma tietää sen mihin paikkaan arvo tulee sijoittaa.
Varoitus! Yleinen virhe on unohtaa &–merkki pois scanf–funktion kutsusta.
Joissakin ohjelmointikielissä (kuten Pascal tai Fortran ja osittain myös C++:ssa) ei osoitemerkkiä kirjoiteta, koska kääntäjä kääntää vastaavat kutsut siten, että parametrinä viedäänkin osoite. C–kielessä on mahdollista välittää vain arvoja parametrinä.
int pituus,leveys;
printf("Anna kentän pituus ja leveys metreinä >");
scanf("%d %d",&pituus,&leveys);
Koska kyseessä on funktio, niin se myös palauttaa jonkin arvon. Tässä tapauksessa palautetaan kokonaislukuarvo, joka kertoo montako onnistunutta sijoitusta pystyttiin tekemään.
c-muut\ala.c - tietojen kysyminen kunnes oikein
/* ala.c */
/* Ohjelmalla luetaan kentän leveys ja pituus sekä tulostetaan
näiden perusteella kentän ala. Tietoja kysytään kunnes
molemmat tulevat oikein annetuksi.
Vesa Lappalainen 18.9.1991
*/
#include <stdio.h>
int main(void)
{
int pituus,leveys;
do {
printf("Anna kentän pituus ja leveys metreinä >");
fflush(stdin); /* Poistetaan mahd. ed. kier. ylim. merkit, epästd. */
} while ( scanf("%d %d",&pituus,&leveys) < 2 );
printf("Ala on %d m2.\n",pituus*leveys);
return 0;
}
format–osassa voidaan pakottaa syöttämään tiettyjä merkkejä syöttötekstin väliin:
scanf("%d,%d",&pituus,&leveys)
Tällöin syöttöjä saataisiin seuraavasti:
Syöttö: syöttöjen lkm pituus leveys
--------- --------------- -------- -------- --
23,34 2 23 34
23, 45 2 23 45
23 34 1 23 alkup.
23 ,34 1 23 alkup.
,34 0 alkup. alkup.
odottaa lisää
34 1 34 alkup.
kissa 0 alkup. alkup.
Näin voitaisiin lukea esimerkiksi mittakaava:
printf("Anna mittakaava muodossa 1:2000 >");
scanf("1:%lf",&mittakaava);
tai jopa:
printf("Anna mittakaava muodossa 1:2000 >");
scanf("%lf:%lf",&kerroin,&mittakaava);
Mikäli syöttö on formaattiin nähden väärää muotoa, jäävät ne muuttujat alkuperäiseen arvoonsa, jotka ovat virheellisen syöttökohdan jälkeen. Esimerkiksi
scanf("1:%lf",&mittakaava);
palauttaa 0 ja jättää mittakaava –muuttujan arvon koskemattomaksi, mikäli syöttö alkaa millä tahansa muulla kuin 1:.
8.3.2 Tiedon lukeminen C++:ssa, cin
C++:ssa lukeminen voidaan suorittaa C:n scanf:n lisäksi tietovirtoja käyttäen:
cin >> matka_mm;
Valitettavasti samanlaista syötön pakottamista kuin scanf:llä ala.c –ohjelmassa oli, ei ole helppo saavuttaa. Käytännössä tällä ei ole merkitystä, koska "oikeassa" ohjelmassa syöttö tulee lähes aina ensin merkkijonoon, josta syöttötietoa sitten aletaan käsittelemään. Pilkun syöttäminen lukujen väliin (jos se on oleellista) voitaisiin tutkia seuraavasti:
c-muut\ala.cpp - tiedon kysyminen kunnes pilkku väliin
#include <iostream>
using namespace std;
int main(void)
{
int pituus,leveys;
char c;
do { // Ohjelma on helppo saada ikuiseen silmukkaan!
cout << "Anna kentän pituus ja leveys metreinä >";
cin >> pituus >> c >> leveys;
} while ( c != ',' );
cout << "Ala on " << pituus*leveys << " m2." << endl;
return 0;
}
Hyvä puoli tietovirtojen käytössä on se, ettei helposti unohtuvia osoitemerkkejä (&) tarvita. Ohjelma ei kuitenkaan toimi idioottivarmasti, mutta kuten edellä todettiin, niin lukemalla syöttörivi ensin merkkijonoon saataisiin parempi lopputulos. Tähän tekniikkaan paneudutaan vähän myöhemmin.
8.4 Osoittimet
8.4.1 Miksi osoittimet?
C–kielessä osoittimet piti opetella heti ohjelmoinnin alussa, jos halusi tehdä minkäänlaisia järkeviä aliohjelmia. C++:ssa ongelmaa voidaan kiertää viitemuuttujien (references) avulla. Mutta vaikka emme osoittimia ihan välttämättä parametrin välityksen takia enää tarvitsisikaan, opettelemme ne seuraavaksi. Käytännössä niitä kuitenkin tarvitaan, koska maailmassa on valtavasti valmista C–koodia, jonka hyödyntämiseksi osoittimia joutuu joka tapauksessa käyttämään. Lisäksi olio–ohjelmointi pääsee C++:ssa täysiin oikeuksiinsa vasta olio–osoittimien ja polymorfismin myötä. Ja kuten aiemmin todettiin, myös taulukoiden läpikäynnissä osoittimet ovat käteviä.
Edellä sanottiin, että &matka_mm
on osoite muuttujaan matka_mm
. Tätä voitaisiin kuvata seuraavasti:
Reaali– ja kokonaislukujen lisäksi voidaan määritellä myös muuttujia, jotka voivat saada osoite–arvoja. Tällaisia muuttujia nimitetään osoittimiksi (eng. pointer, vrt. sormet ja korttien lajittelu). Osoitearvon saava muuttuja siis osoittaa johonkin muistipaikkaan, eli muuttujaan. Muuttujiin voidaan sijoittaa arvoja paitsi suoraan, niin myös epäsuorasti osoittimien avulla.
8.4.2 Muuttujan arvon epäsuora muuttaminen
Tutkitaanpa vaikkapa seuraavaa ohjelmaa:
Osoitinmuuttuja määritellään laittamalla * muuttujan nimen eteen esiteltäessä muuttujaa. Sijoituksessa
osoitin = // tässä tapauksessa pElaimia =
pitää oikealla puolella olla osoite–tyyppiä oleva lauseke (esim. &kissoja).
Vastaavasti muoto *osoitin ("tähdätään osoitinta pitkin") tarkoittaa sen muistipaikan sisältöä, johon muuttuja osoitin osoittaa. Olkoon meillä muisti jakaantunut käännöksen jälkeen seuraavasti:
Osoitinmuuttujia tarvitaan erityisesti aliohjelmien ja taulukoiden yhteydessä. Tämä esimerkki on itse asiassa varsin huono! Nimenomaan tällaista moninimisyyttä (aliasing), eli sama muuttuja voi muuttua useata eri kautta, tulisi välttää, koska se on omiaan tekemään ohjelmista epäluotettavia ja vaikeasti ylläpidettäviä. Järkevämmän osoite–esimerkin otamme heti kun saamme aliohjelmat kunnolla käyttöön.
Varoitus! Edellisestä tehtävästä huolimatta ÄLÄ KOSKAAN mene itse keksimään arvoja, joita sijoitat osoitinmuuttujalle. Tämä on 99.999% varma tapa saada kone kaatumaan. Käytännössä osoittimien arvot (osoitteet) ovat edellä kuvattua esimerkkiä monimutkaisempia.
8.4.3 * ja &
Osoittimen sisällön ottaminen
*p
on sallittu silloin, kun p on esitelty osoittimeksi johonkin muuhun tyyppiin kuin void tyyppiin. Siis
c-muut\ososij.c - miten osoitinta saa käyttää
/* ososij.c */
int *p,i; ... i = *p; /* OK jos on ollut sijoitus p = */
double *p,d; ... d = *p; /* OK jos on ollut sijoitus p = */
void *p; int i; ... i = *p; /* VÄÄRIN */'
Jos otetaan muuttujan osoite
&i
saadaan osoitin, joka on samaa tyyppiä kuin muuttuja. Siis
int i,*p; p = &i; /* OK */
double *p,d; p = &d; /* OK */
void *p; int i; p = &i; /* OK, koska void osoittimelle saa
sijoittaa minkä tahansa osoittimen */
Seuraavat ovat oikein, mutta niitä täytyy varoa:
double *p; int i;
p = &i; /* VAARALLINEN */
i = *p; /* OK, paikassa p oleva double muuttuu int */
Jos i ja j on esitelty vaikkapa int i,j;, ei seuraavassa ole mieltä:
int i,j;
j = *i; /* VÄÄRIN, koska i ei ole osoitin! */
Tehtävä 8.61 *&
Tästä tehtävästä huolimatta ei alla olevan kaltaisia sotkuja tule käyttää! Olkoon muuttujien esittelyt kuten ohjelmassa kissaoso.c. Missä seuraavista tapauksissa tulee sijoitetuksi 5 muuttujalle kissa ( &i tarkoittaa (&i)):
/* ososotku.c */
/* a */ *&kissoja = 5;
/* b */ &*kissoja = 5;
/* c */ *&*&*&*&kissoja = 5;
/* d */ &*pElaimia = &kissoja; *&*pElaimia = 5;
/* e */ *&pElaimia = kissoja; *pElaimia = 5;
8.4.4 NULL-osoitin
Yleensä osoittimelle ei saa sijoittaa mitään vakioarvoa. Kuitenkin eräs vakioarvo, NULL, muodostaa poikkeuksen. Jokaisen kunnollisen ohjelman tulisi aina ennen osoittimen käyttöä tarkistaa ettei osoittimen arvo ole NULL.
NULL on varattu tarkoittamaan, ettei osoittimella ole laillista osoitteena toimivaa arvoa. Yleensä vakio NULL on 0, mutta tähän ei saa liiaksi luottaa. Kuitenkin taataan, että jos p on NULL–osoitin, niin ehtolause
if ( p ) ...
/* on sama kuin */
if ( p != NULL ) ...
Erityisesti monet C–kirjaston funktioista palauttavat NULL arvoja, mikäli hommat eivät menneet niin kuin piti (vrt. malloc, fopen jne.).
8.5 Aliohjelmat (funktiot)
Eräs ohjelmoinnin tärkeimmistä rakenteista on aliohjelma. C–kielessä kaikkia eri tyyppisiä aliohjelmia nimitetään funktioiksi; joissakin muissa kielissä eri tyyppejä erotetaan eri nimille. Aliohjelmaa käytetään seuraavissa tapauksissa:
- Haluttu tehtävä on valmiiksi jonkun toisen kirjoittamana aliohjelmana esimerkiksi standardikirjastossa.
Haluttua tehtävää suoritetaan usein liki samanlaisena joko samassa ohjelmassa tai jossain toisessa ohjelmassa.
Haluttu tehtävä muodostaa selvän kokonaisuuden, jonka toiminta on ilmaistavissa muutamalla sanalla riittävän selkeästi (= aliohjelman nimi).
Haluttua tehtävää ei juuri sillä hetkellä osata tai viitsitä ohjelmoida. Tällöin määritellään millainen aliohjelma tarvitaan ja kirjoitetaan tarvittavaan kohtaan pelkkä aliohjelman kutsu. Itse aliohjelma voidaan aluksi toteuttaa varsin triviaalina ja korjata myöhemmin tekemään sen varsinainen tehtävä.
Rakenne saadaan selkeämmän näköiseksi.
8.5.1 Parametritön aliohjelma
Aliohjelma esitellään vastaavasti kuin pääohjelmakin. Esimerkiksi satunnaisen matkaajan mittakaavaohjelmassa (tässä puhdas C–kielinen versio) voisimme kirjoittaa käyttöohjeet omaksi aliohjelmakseen:
c-muut\matka_a1.c - ohjeet aliohjelmaksi
#include <stdio.h>
#define MITTAKAAVA 200000.0
#define MM_KM (1000.0*1000.0)
void ohjeet(void)
{
printf("Lasken 1:%3.0lf kartalta millimetreinä mitatun matkan\n",MITTAKAAVA);
printf("kilometreinä luonnossa.\n");
}
int main(void)
{
int matka_mm;
double matka_km;
ohjeet();
printf("Anna matka millimetreinä>");
scanf("%d",&matka_mm);
matka_km = matka_mm*MITTAKAAVA/MM_KM;
printf("Matka on luonnossa %1.1lf km.\n",matka_km);
return 0;
}
Tämän etu on siinä, että saimme pääohjelman selkeämmän näköiseksi.
8.5.2 Funktiot ja parametrit
Voisimme jatkaa pääohjelman selkeyttämistä. Tavoite voisi olla aluksi vaikkapa kirjoittaa pääohjelma muotoon:
ohjeet();
kysy_matka(&matka_mm);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
return 0;
Tällainen pääohjelma tuskin tarvitsisi paljoakaan kommentteja.
Edellä on käytetty neljän eri tyypin aliohjelmia (funktioita)
ohjeet();
– parametriton aliohjelmakysy_matka(&matka_mm);
– aliohjelma joka palauttaa tuloksen parametrissään (vrt. esim. scanf).mittakaava_muunnos(matka_mm);
– funktio, joka palauttaa tuloksen nimessääntulosta_matka(matka_km);
– aliohjelma, jolle vain viedään arvo, mutta mitään arvoa ei palauteta
Valmis ohjelma, jossa myös aliohjelmat on esitelty, näyttäisi seuraavalta (rivien numerointi on myöhemmin esitettävää pöytätestiä varten):
c-muut\matka_a2.cpp - erilaisia funktioita
// matka_a2.cpp
// Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
// Vesa Lappalainen 4.1.1997
#include <iostream.h>
const double MITTAKAAVA = 200000.0;
const double MM_KM = 1000.0*1000.0;
void ohjeet(void)
{
cout << "Lasken 1:" << MITTAKAAVA
<< " kartalta millimetreinä mitatun matkan\n";
cout << "kilometreinä luonnossa.\n";
}
void kysy_matka(int *pMatka_mm)
{
int mm;
cout << "Anna matka millimetreinä>";
cin >> mm;
*pMatka_mm = mm;
}
double mittakaava_muunnos(int matka_mm)
{
return matka_mm*MITTAKAAVA/MM_KM;
}
void tulosta_matka(double matka_km)
{
cout << "Matka on luonnossa "<< matka_km << " km." << endl;
}
int main(void)
{
int matka_mm;
double matka_km;
ohjeet();
kysy_matka(&matka_mm);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
return 0;
}
c++
Edellä olevasta huomataan, että aliohjelmat jotka eivät palauta mitään arvoa nimessään, esitellään void–tyyppisiksi.
mittakaava_muunnos on reaaliluvun palauttava funktio, joten se esitellään double –tyyppiseksi.
Seuraavaksi pöytätestaamme ohjelmamme toiminnan:
Mikäli kukin aliohjelma olisi testattu erikseen, riittäisi meille pelkkä pääohjelman testi:
Tämä on testaustapa, johon tulisi pyrkiä. Isossa ohjelmassa ei ole enää mitään järkeä testata sitä jokainen aliohjelma kerrallaan. Koodiin liitettyjen aliohjelmien tulee olla testattuja kukin erillisinä ja lopullinen testi on vain viimeisimmän mallin mukainen!
Tehtävä 8.62 matka_a2.c
Kirjoita matka_a2.cpp:stä C- versio (tietovirrat => printf/scanf)
8.5.3 Parametrin nimi kutsussa ja esittelyssä
Huomattakoon, ettei parametrien nimillä aliohjelmien esittelyissä ja kutsuissa ole mitään tekemistä keskenään. Nimi voi olla joko sama tai eri nimi. Parametrien idea on nimenomaan siinä, että samaa aliohjelmaa voidaan kutsua eri muuttujien tai mahdollisesti vakioiden tai lausekkeiden arvoilla. Esimerkiksi nyt kirjoitettua tulosta_matka aliohjelmaa voitaisiin kutsua myös seuraavasti:
c-muut\matka_a3.cpp - erilaisia tapoja kutsua funktiota
// matka_a3.cpp
#include <iostream.h>
void tulosta_matka(double matka_km)
{
cout << "Matka on luonnossa "<< matka_km << " km." << endl;
}
int main(void)
{
double d = 50.2;
tulosta_matka(d); // eri niminen muuttuja
tulosta_matka(30.7); // vakio
tulosta_matka(d+20.8); // lauseke
tulosta_matka(2*d-30.0); // lauseke
return 0;
}
Edellä aliohjelman kutsut voidaan tulkita seuraaviksi sijoituksiksi aliohjelman tulosta_matka muuttujaan matka_km:
matka_km = d;
matka_km = 30.7;
matka_km = d+20.8;
matka_km = 2*d- 30.0
Aliohjelma jouduttiin edellä vielä kirjoittamaan uudestaan (käytännössä kopioimaan edellisestä ohjelmasta), mutta myöhemmin opimme miten aliohjelmia voidaan kirjastoida standardikirjastojen tapaan (ks. moduuleihin jako), jolloin kerran kirjoitettua aliohjelmaa ei enää koskaan tarvitse kirjoittaa uudestaan (eikä kopioida).
8.5.4 Osoitteen välittäminen
Aliohjelma kysy_matka joutuu palauttamaan kysytyn matkan arvon kutsuvalle ohjelmalle. Tämän takia aliohjelmalle ei viedä matkan arvoa parametrinä, vaan sen paikan osoite, johon aliohjelman tulos täytyy laittaa:
kysy_matka(&matka_mm)
Vastaavasti parametri esitellään osoitin–tyyppiseksi aliohjelman esittelyssä:
void kysy_matka(int *pMatka_mm)
Jatkossa pitää muistaa, että tämän aliohjelman pMatka_mm on osoite sinne, minne tulos pitää laittaa. Nimeämmekin tässä opiskelun alkuvaiheessa osoitinmuuttujat aina alkamaan pienellä p–kirjaimella (p=pointer). Tällaista nimeämistapaa, jossa muuttujan alkukirjaimilla kuvataan muuttujan tyyppi, sanotaan unkarilaiseksi nimeämistavaksi. Jotkut ohjelmoinnin opettajat vastustavat nimeämistapaa, mutta suuria Windows–ohjelmia lukiessa täytyy todeta että on todella mukava tietää mitä tyyppiä mikäkin muuttuja on.
Aliohjelmassa on aluksi yksinkertaisuuden vuoksi esitelty aliohjelman paikallinen muuttuja mm, johon päätteeltä saatava arvo ensin luetaan. Jotta myös pääohjelma saisi tietää tästä, pitää suorittaa epäsuora osoitus ("tähdätä osoitinta pitkin"):
*pMatka_mm = mm;
"Tosiohjelmoija" ei tällaista apumuuttujaa kirjoittaisi, vaan lyhentäisi aliohjelman suoraan muotoon:
void kysy_matka(int *pMatka_mm)
{
cout << "Anna matka millimetreinä>";
cin >> *pMatka_mm;
}
Vinkki
"Tähtäämisoperaation" muistaa ehkä helpoimmin, jos ajattelee että muuttujan nimi onkin *pMatka_mm
eikä pMatka_mm
.
Lyhennetty C–versio samasta aliohjelmasta olisi:
void kysy_matka(int *pMatka_mm)
{
printf("Anna matka millimetreinä>");
scanf("%d",pMatka_mm);
}
Aikaisemmin olemme tottuneet kirjoittamaan scanf–funktioon
scanf("%d",&matka_mm);
Nyt kuitenkin pMatka_mm on valmiiksi osoite, jolloin &–merkki täytyy jättää pois kutsusta. Toisaalta voisimme ajatella, että muuttujan nimi onkin *pMatka_mm (koska se on niin esitelty) ja tällöin scanf:n kutsu olisi
scanf("%d",&*pMatka_mm); /* ==scanf("%d",pMatka_mm); */
On siis erittäin tärkeää ymmärtää milloin on kyse osoitteesta ja milloin arvosta.
8.5.5 Viitemuuttujat (referenssimuuttujat, &)
C++ tarjoaa vielä oman uuden tavan välittää muuttuvia parametrejä: viitemuuttujat, eli referenssimuuttujat (reference). Varsinainen mekanismi on täsmälleen sama kuin parametrin välittäminen osoitteiden avullakin. Vain syntaksi on erilainen - aloittelijalle jopa helpompi käyttää:
Aliohjelma olisi muotoa:
c-muut\matka_ar.cpp - parametri referenssinä
void kysy_matka(int &rMatka_mm)
{
int mm;
cout << "Anna matka millimetreinä>";
cin >> mm;
rMatka_mm = mm;
}
tai lyhyemmässä muodossa:
void kysy_matka(int &rMatka_mm)
{
cout << "Anna matka millimetreinä>";
cin >> rMatka_mm;
}
Tässä esittelyllä
int &rMatka_mm
esitellään referenssimuuttuja rMatka_mm, eli muuttuja joka referoi johonkin toiseen muuttujaan aina kun siihen viitataan. Näin sijoitus
rMatka_mm = mm;
tarkoittaakin, että tulos sijoitetaan kutsuneen ohjelman vastaavalle muuttujalle.
Huonona (tai jonkin mielestä hyvänä) puolena viitemuuttujista voitaisiin pitää sitä, että itse kutsu pääohjelmasta täytyy nyt olla muodossa:
kysy_matka(matka_mm); // HUOM!
Miksi pitäisin tätä huonona? Siksi, ettei kutsusta nyt näe suoran aikooko aliohjelma muuttaa muuttujan matka_mm arvoa vaiko ei.
Hyvänä puolena on taas se, että aliohjelma voidaan muuttaa viitteitä käyttäväksi muuttamatta itse kutsuvaa ohjelmaa. Tämä tulee kyseeseen sitten kun välitämme parametreinä "isoja" oliota, jolloin on edullisempaa vain viitata olioon, kuin kuljettaa mukana koko olio.
Vielä yksi huono (tai joidenkin mielestä hyvä) puoli viitemuuttujissa on se, ettei niiden viittaamaa paikkaa voida muuttaa muuta kuin alustuksessa (esim. aliohjelman kutsun yhteydessä). Näin osoittimia tarvitaan vielä tilanteissa, joissa tietorakenteita pitää käydä läpi.
8.5.6 & –merkillä monta eri merkitystä
Vielä eräs inhottava puoli on se, että samalle merkille & (eikä tämä ole edes ainoa kuormitettu symboli) on otettu useita eri merkityksiä riippuen siitä missä yhteydessä merkki esiintyy:
Osoiteoperaattori: Jos & on jo olemassa olevan muuttujan edessä, otetaan muuttujan osoite. - scanf("%d",&matka_mm);
Referenssin esittely: Jos & on muuttujan esittelyn yhteydessä, esitellään viitemuuttuja (referenssi) - int &rMatka_mm
Bittitason AND-operaattori: Jos & esiintyy kahden lausekkeen välissä, on kyseessä bittitason JA-operaattori: parillinen = luku & 0xfffe
Looginen AND-operaattori: Jos & esiintyy 2-kertaisena kahden lausekkeen välissä, on kyseessä looginen JA-operaattori:
if ( kello < 23 && 0 < rahaa ) ...
Jos selviämme tästä &–sekamelskasta, selviämme lopuistakin ongelmista C++:n kanssa.
Yleensä AND operaatiot eivät aiheuta sekaannusta, mutta jokin muistisääntö tarvitaan siihen milloin kirjoitetaan * ja milloin &. Olkoon se vaikka:
Vinkki: Tähdet taivaalla
Jos kyseessä on osoiteparametrin välitys tulee * ylös, koska tähdet ovat taivaalla ja niitä osoitetaan, tällöin &–merkit tulevat alas!
Jos kyseessä on parametrin välitys referenssin avulla, ei tarvitse tähtäillä, joten tähtiä ei tule ja jokainen merkki siirtyy pykälän ylöspäin, eli & merkit ylös ja alhaalle ei jää mitään!
Tavallisessa arvoparametrin välityksessä ei tarvita mitään ihmemerkkejä, mutta ei saada ihmeitä aikaankaan!
Kertaamme "problematiikkaa" vähän myöhemmin, kun katsomme tarkemmin mitä aliohjelmakutsu oikein tarkoittaa.
8.5.7 Nimessään arvon palauttavat funktiot
Funktion arvo palautetaan return –lauseessa. Jokaisessa ei-void –tyyppiseksi esitellyssä funktiossa tulee olla vähintään yksi return –lause. void–tyyppisessäkin voi olla return–lause. Tarvittaessa return–lauseita voi olla useampiakin:
int suurempi(int a, int b)
{
if ( a >= b ) return a;
return b;
}
Kun return –lause tulee vastaan, lopetetaan HETI funktion suoritus. Tällöin myöhemmin olevilla lauseilla ei ole mitään merkitystä. Näin ollen useat return–lauseet ovat mielekkäitä vain ehdollisissa rakenteissa. Siis seuraavassa ei olisi mitään mieltä:
int hopo(int a) :-(
{
int i;
return 5; /* Palauttaa aina 5!!! */
i = 3 + a;
return i+2;
}
return–lausetta ei saa sotkea arvojen palauttamiseen osoitteen avulla. Esimerkiksi:
c-muut\funjaoso.c - sivuvaikutuksellinen funktio
/* funjaoso.c */
#include <stdio.h>
int tupla_plus_yksi(int *pArvo)
{
int vanha=*pArvo; /* Alkuperäinen arvo talteen */
*pArvo = *pArvo + 1; /* Kutsun muuttuja muuttui nyt! eli j = 4 */
return 2*vanha; /* Arvo palautui nimessä nyt! eli i = 6 */
}
int main(void)
{
int i,j=3;
i = tupla_plus_yksi(&j);
printf("i = %d, j = %d\n",i,j);
return 0;
}
Tehtävä 8.63 Funktio ja osoitin
Mitä pääohjelma funjaoso tulostaisi jos aliohjelma olisikin ollut:
int tupla_plus_yksi(int *pArvo)
{
*pArvo = *pArvo + 1;
return 2**pArvo;
}
Tehtävä 8.64 Funktio ja referenssi
Kirjoita edellisestä tehtävästä molemmista tupla_plus_yksi - versioista viitemuuttujia käyttävä versio!
8.5.8 Ketjutettu kutsu
Koska funktio–aliohjelma palauttaa valmiiksi arvon, voitaisiin matka_a2.cpp:n pääohjelma kirjoittaa myös muodossa:
ohjeet();
kysy_matka(&matka_mm);
tulosta_matka(mittakaava_muunnos(matka_mm));
return 0;
Funktioita käytetään silloin, kun aliohjelman tehtävänä on palauttaa vain yksi täsmällinen arvo. Tyypillisiä math.h–kirjaston funktioita on esim. (suluissa olevat eivät ole standardin funktioita):
(abs) acos asin atan atan2 atof (cabs) ceil
cos cosh exp fabs floor fmod frexp (hypot)
(labs) ldexp log log10 (matherr) modf (poly) pow
(pow10) sin sinh sqrt tan tanh
Funktioita käytetään kuten matematiikassa on totuttu:
c = sqrt(a*a+b*b) + asin((sin(alpha)+0.2)/2.0);
kysy_matka ja kysy_mittakaava voitaisiin kirjoittaa myös funktioiksi, ja tällöin niitä voitaisiin kutsua esim. seuraavasti:
matka_km = kysy_matka()*kysy_mittakaava()/MM_KM;
Vaarana olisi kuitenkin se, ettei voida olla aivan varmoja kumpiko funktiosta kysy_matka vai kysy_mittakaava suoritettaisiin ensin ja tämä saattaisi aiheuttaa joissakin tilanteissa yllätyksiä.
Tämän vuoksi pyrimmekin kirjoittamaan funktioiksi vain sellaiset aliohjelmat, jotka palauttavat täsmälleen yhden arvon ja jotka eivät ota muuta informaatiota ympäristöstä kuin sen mitä niille parametrinä välitetään. Eli tavoitteena on se, että funktioiden kutsuminen lausekkeen osana olisi turvallista.
Muut aliohjelmat kirjoitamme siten, että arvot palautetaan osoitteen avulla. Hyvin yleinen C–tapa on kuitenkin palauttaa tällaisenkin aliohjelman onnistumista kuvaava arvo funktion nimessä (vrt. esim. scanf).
Tehtävä 8.65 math.h
Katso Turbo- C :n Help- toiminnon avulla kunkin math.h- kirjaston funktion parametrien määrä ja tyyppi sekä se mitä kukin todella tekee.
Tehtävä 8.66 Funktiot
Kirjoita edellä mainitut kysy_matka ja kysy_mittakaava nimessään arvon palauttavina funktioina.
Tehtävä 8.67 Ympyrän ala ja pallon tilavuus
Kirjoita funktiot, jotka palauttavat r- säteisen ympyrän pinta- alan ja r- säteisen pallon tilavuuden. Kirjoita pääohjelma, jossa pinta- ala ja tilavuus - funktiot testataan.
Tehtävä 8.68 Pääohjelma yhtenä funktiokutsuna
Jatka edellä mainittua ketjuttamista siten, että koko pääohjelma on vain yksi lauseke ( ohjeet- kutsu saa olla oma rivinsä jos haluat). Tosin tämä on C-hakkerismia eikä mikään tavoite helposti luettavalta ohjelmalta. Itse asiassa hyvä kääntäjä tekee automaattisesti tämän kaltaista optimointia (mitä muka voitiin säästää?).
8.5.9 Aliohjelmien testaaminen
Kun uusi aliohjelma kirjoitetaan, kannattaa sen testaamista varten kirjoittaa hyvin lyhyt testi–pääohjelma.
Esimerkiksi kerhon jäsenrekisterin päämenun tulostamista varten voisimme kirjoittaa aliohjelman nimeltä paamenu. Tämä päämenu voitaisiin sitten testata vaikkapa seuraavalla testipääohjelmalla:
c-muut\menutest.cpp - päämenun testaus
// menutest.cpp
#include "paamenu.cpp" // HUOM! "Oikeasti" aliohjemia ei INCLUDEta
// vaan paamenu.h ja tehdään projekti tai MAKEFILE!
int main(void)
{
paamenu(10);
return 0;
}
Tiedostot paamenu.h
ja paamenu.cpp
, joissa aliohjelman prototyyppi ja itse päämenu esiteltäisiin, voisivat olla esimerkiksi:
c-muut\paamenu.h - päämenun otsikkotiedosto
/* paamenu.h */
#ifndef PAAMENU_H
#define PAAMENU_H
void paamenu(int jasenia);
#endif /* PAAMENU_H */
c-muut\paamenu.cpp - päämenun totetutus
// paamenu.cpp
#include <iostream.h>
#include "paamenu.h"
void paamenu(int jasenia)
{
cout << "\n\n\n\n\n";
cout << "Jäsenrekisteri\n";
cout << "==============\n";
cout << "\n";
cout << "Kerhossa on " << jasenia << " jäsentä.\n";
cout << "\n";
cout << "Valitse:\n";
cout << " ? = avustus\n";
cout << " 0 = lopetus\n";
cout << " 1 = lisää uusi jäsen\n";
cout << " 2 = etsi jäsenen tiedot\n";
cout << " 3 = tulosteet\n";
cout << " 4 = tietojen korjailu\n";
cout << " 5 = päivitä jäsenmaksuja" << endl;
cout << " :";
Huomattakoon, että aliohjelma voitaisiin kirjoittaa myös seuraavasti (miksi?):
c-muut\paamenu2.cpp - toteutus vähillä cout-kutsuilla
// paamenu2.cpp
#include <iostream.h>
#include "paamenu.h"
void paamenu(int jasenia)
{
cout << "\n\n\n\n\n"
"Jäsenrekisteri\n"
"==============\n"
"\n"
"Kerhossa on " << jasenia << " jäsentä.\n"
"\n"
"Valitse:\n"
" ? = avustus\n"
" 0 = lopetus\n"
" 1 = lisää uusi jäsen\n"
" 2 = etsi jäsenen tiedot\n"
" 3 = tulosteet\n"
" 4 = tietojen korjailu\n"
" 5 = päivitä jäsenmaksuja" << endl <<
" :";
}
Voidaan kirjoittaa jopa (miksi):
c-muut\paamenu3.cpp - rivin jatkaminen
#include <iostream.h>
#include "paamenu.h"
void paamenu(int jasenia)
{
cout << "\n\n\n\n\n\
Jäsenrekisteri\n\
==============\n\
\n\
Kerhossa on " << jasenia << " jäsentä.\n\
\n\
Valitse:\n\
? = avustus\n\
0 = lopetus\n\
1 = lisää uusi jäsen\n\
2 = etsi jäsenen tiedot\n\
3 = tulosteet\n\
4 = tietojen korjailu\n\
5 = päivitä jäsenmaksuja" << endl << "\
:";
}
Jatkossa kommentoimme aliohjelmia enemmän, mutta nyt olemme jättäneet kommentit pois, jotta ohjelma olisi mahdollisimman lyhyt.
Huomattakoon, että aliohjelma on saatu kopioiduksi suoraan aikaisemmasta ohjelman suunnitelmasta lisäämällä vain kunkin rivin alkuun cout <<"ja loppuun \n";. Tällaiset toimenpiteet voidaan automatisoida tekstinkäsittelyn avulla.
8.6 Lisää aliohjelmista
8.6.1 Useita parametrejä
Kaikissa edellisissä esimerkeissämme meillä on ollut vain 0 tai yksi parametriä välitettävänä aliohjelmaan. Käytännössä usein tarvitsemme useampia parametrejä. Esimerkiksi edellisessä paamenu–aliohjelmassa pitäisi oikeastaan tulostaa myös kerhon nimi. Emme vielä kuitenkaan osaa käsitellä merkkijonoja, joten palaamme tähän ongelmaan myöhemmin.
Ottakaamme esimerkiksi mittakaava_muunnos –funktio. Mikäli ohjelma haluttaisiin muuttaa siten, että myös mittakaavaa olisi mahdollista muuttaa, pitäisi myös mittakaava voida välittää muunnos–aliohjelmalle parametrinä. Kutsussa tämä voisi näyttää esim. tältä:
matka_km = mittakaava_muunnos(10000.0,32);
Vastaavasti funktio–esittelyssä täytyisi olla kaksi parametriä:
double mittakaava_muunnos(double mittakaava,int matka_mm)
{
return matka_mm*mittakaava/MM_KM;
}
Kun kutsu suoritetaan, välitetään aliohjelmalle parametrit siinä järjestyksessä, missä ne on esitelty. Voitaisiin siis kuvitella aliohjelmakutsun aiheuttavan sijoitukset aliohjelman parametrimuuttujiin (tosin sijoitusjärjestystä ei taata, eli ei tiedetä kumpi sijoitus suoritetaan ensin):
mittakaava = 10000.0;
matka_mm = 32;
Jos kutsu on muotoa
matka_km = mittakaava_muunnos(MITTAKAAVA,matka_mm);
kuvitellaan sijoitukset:
mittakaava = MITTAKAAVA; /* Ohjelman vakio */
matka_mm = matka_mm; /* Pääohjelman muuttuja matka_mm */
Siis vaikka kutsussa ja esittelyssä esiintyykin sama nimi, ei nimien samuudella ole muuta tekemistä kuin mahdollisesti se, että turha on väkisinkään keksiä lyhennettyjä huonoja nimiä, jos kerran on hyvä nimi keksitty kuvaamaan jotakin asiaa.
Parametreistä osa, ei yhtään tai kaikki voivat olla myös osoitteita tai referenssejä.
Huom! Vaikka kaikilla aliohjelman parametreille olisikin sama tyyppi, täytyy jokaisen parametrin tyyppi mainita silti erikseen:
double nelion_ala(double korkeus, double leveys)
Tehtävä 8.69 Toisen asteen yhtälön juuri
Kirjoita funktio root_1(a,b,c), joka palauttaa jomman kumman toisen asteen yhtälön ax2+bx+c=0 juurista (oletetaan tällä kertaa, että a<>0 ja D = b 2- 4ac >= 0. Miksi oletetaan?).
Tehtävä 8.70 Toisen asteen polynomi, root_1
Kirjoita funktio root_1 joka palauttaa toisen asteen polynomin P(x) = ax 2+bx+c arvon (muista viedä parametrinä myös a,b ja c).
Tehtävä 8.71 root_1 testaus
Kirjoita pääohjelma, jolla voidaan testata root_1 - aliohjelma (jotenkin myös se, että tulos toteuttaa yhtälön).
8.6.2 Muuttujien lokaalisuus
Kukin aliohjelma muodostaa oman kokonaisuutensa. Edellä olleissa esimerkeissä aliohjelmat eivät tiedä ulkomaailmasta mitään muuta, kuin sen, mitä niille tuodaan parametreinä kutsun yhteydessä.
Vastaavasti ulkomaailma ei tiedä mitään aliohjelman omista muuttujista. Näitä aliohjelman lokaaleja muuttujia on esim. seuraavassa:
void kysy_matka(int *pMatka_mm)
{
int mm;
printf("Anna matka millimetreinä>");
scanf("%d",&mm);
*pMatka_mm = mm;
}
pMatka_mm - aliohjelman parametrimuuttuja (tässä tapauksessa osoitinmuuttuja).
mm - aliohjelman lokaali apumuuttuja matkan lukemiseksi.
Yleensäkin C–kielessä lausesulut { ja } muodostavat lohkon, jonka ulkopuolelle mikään lohkon sisällä määritelty muuttuja tai tyyppimääritys ei näy. Näkyvyysalueesta käytetään englanninkielisessä kirjallisuudessa nimitystä scope. Lokaaleilla muuttujilla voi olla vastaava nimi, joka on jo aiemmin esiintynyt jossakin toisessa yhteydessä. Lohkon sisällä käytetään sitä määrittelyä, joka esiintyy lohkossa:
c-muut\lokaali.c - lokaalien muuttujien näkyvyys
c-muut.c - lokaalien muuttujien näkyvyys
#include <stdio.h> :-(
int main(void)
{
char ch='A';
printf("Kirjain %c",ch);
{
int ch = 5;
printf(" kokonaisluku %d",ch);
{
double ch = 4.5;
printf(" reaaliluku %5.2lf\n",ch);
}
}
return 0;
}
Saman tunnuksen käyttäminen eri tarkoituksissa on kuitenkin kaikkea muuta kuin hyvää ohjelmointia.
Tehtävä 8.72 Eri nimet
Korjaa edellinen ohjelma siten, että kullakin erityyppisellä muuttujalla on eri nimi.
8.6.3 auto ja register
Varattu sana auto tarkoittaa, että muuttujasta tehdään automaattinen muuttuja. Oletuksena jokainen lokaali muuttuja, joka ei ole esitelty static –määrityksellä, on automaattinen muuttuja. Tämän vuoksi auto–sanaa harvoin käytetään. Muuttujan automaattisuus tarkoittaa sitä, että kun muuttujan määrittelylohkosta poistutaan, tuhotaan muuttuja samalla.
{
int mm; /* sama kirjoitetaanko näin */
auto int mm; /* vai näin */
Joskus kääntäjän työn helpottamiseksi voidaan kääntäjälle ehdottaa jonkin lokaalin muuttujan sijoittamista prosessorin (CPU) rekisteriin ja näin saadaan muuttujan käyttö nopeammaksi. Tämä tehdään varatulla sanalla register.
c-muut\register.c – muuttujat prosessorin rekistereihin
#include <stdio.h>
int main(void)
{
register int i;
for (i=0; i<4; i++) printf("i=%d\n",i);
/* Seuraava ei toimi koska rekisteristä ei saada osoitetta */
/* printf("i:n osoite on %p\n",&i); */
return 0;
}
8.6.4 Parametrinvälitysmekanismi
Ainoa C–kielen tuntema parametrinvälitysmekanismi on parametrien välittäminen arvoina. Tämä tarkoittaa sitä, että aliohjelma saa käyttöönsä vain (luku)arvoja, ei muuta. Olkoon meillä esimerkiksi ongelmana tehdä aliohjelma, jolle viedään parametreinä tunnit ja minuutit sekä niihin lisättävä minuuttimäärä. Jos ensimmäinen yritys olisi seuraava:
c-muut\aikalisa.cpp - yritys lisätä arvoja
#include <iostream.h>
void lisaa(int h, int m, int lisa_min) :-(
{
int yht_min = h*60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
void tulosta(int h, int m)
{
cout << h << ":" << m << endl;
}
int main(void)
{
int h=12,m=15;
lisaa(h,m,55);
tulosta(h,m);
return 0;
}
Tämä ei tietenkään toimisi! Hyvä kääntäjä jopa varoittaisi että:
Warn : aikalisa.cpp(8,2):'m' is assigned a value that is never used
Warn : aikalisa.cpp(7,2):'h' is assigned a value that is never used
Mutta miksi ohjelma ei toimisi? Seuraavan selityksen voi ehkä ohittaa ensimmäisellä lukukerralla. Tutkitaanpa tarkemmin mitä aliohjelmakutsussa oikein tapahtuu. Oikaisemme seuraavassa hieman joissakin kohdissa liian tekniikan kiertämiseksi, mutta emme kovin paljoa. Katsotaanpa ensin miten kääntäjä kääntäisi aliohjelmakutsun (Borland C++ 5.1, 32-bittinen käännös, rekisterimuuttujat kielletty jottei optimointi tekisi konekielisestä ohjelmasta liian monimutkaista):
lisaa(h,m,55);
muistiosoite assembler selitys
-------------------------------------------------------------------------
004010F9 push 0x37 pinoon 55
004010FB push [ebp-0x08] pinoon m:n arvo
004010FE push [ebp-0x04] pinoon h:n arvo
00401101 call lisaa mennään aliohjelmaan lisää
00401106 add esp,0x0c poistetaan pinosta 12 tavua (3 x int)
Kun saavutaan aliohjelmaan lisaa, on pino siis seuraavan näköinen:
muistiosoite sisältö selitys
------------------------------------------------------------------------
064FDEC 00401106 <-ESP paluuosoite kun aliohjelma on suoritettu
064FDF0 0000000C h:n arvo, eli 12
064FDF4 0000000F m:n arvo, eli 15
064FDF8 00000037 lisa_min, eli 55
Eli aliohjelmaan saavuttaessa aliohjelmalla on käytössään vain arvot 12,15 ja 55. Näitä se käyttää tässä järjestyksessä omien parametriensä arvoina, eli m,h,lisa_min.
Muutetaanpa ohjelmaan parametrin välitys osoitteiden avulla:
c-muut\aikalis2.cpp - parametrin välitys osoittimilla
#include <iostream.h>
void lisaa(int *ph, int *pm, int lisa_min)
{
int yht_min = *ph * 60 + *pm + lisa_min;
*ph = yht_min / 60;
*pm = yht_min % 60;
}
void tulosta(int h, int m)
{
cout << h << ":" << m << endl;
}
int main(void)
{
int h=12,m=15;
lisaa(&h,&m,55);
tulosta(h,m);
return 0;
}
Nyt ohjelma toimii ja tulostaa 13:10 kuten pitääkin. Eikä kääntäjäkään anna varoituksia. Mitä nyt tapahtuu ohjelman sisällä? Aliohjelmakutsusta seuraa:
lisaa(&h,&m,55);
muistiosoite assembler selitys
-------------------------------------------------------------------------
0040111D push 0x37 pinoon 55
0040111F lea eax,[ebp-0x08] m:osoite rekisteriin eax
00401122 push eax ja tämä pinoon (0064FDFC)
00401123 lea edx,[ebp-0x04] h:n osoite rekisteriin edx
00401126 push edx ja tämä pinoon (0064FE00)
00401127 call lisaa mennään aliohjelmaan lisää
0040112C add esp,0x0c poistetaan pinosta 12 tavua (3 x int)
Pino on aliohjelmaan saavuttaessa seuraavan näköinen
muistiosoite assembler selitys
------------------------------------------------------------------------
0064FDEC 0040112C <-ESP paluuosoite kun aliohjelma on suoritettu
0064FDF0 0064FE00 h:n osoite, eli ph:n arvo
0064FDF4 0064FDFC m:n osoite, eli pm:n arvo
0064FDF8 00000037 lisa_min, eli 55
Aliohjelman alussa olevien lauseiden
muistiosoite assembler selitys
-------------------------------------------------------------------------
0040107C push ebp pinoon talteen rekisterin ebp arvo
0040107D mov ebp,esp ebp:hn pinon pinnan osoite
0040107F push ecx pinoon tilaa yhdelle kokonaisluvulle (min)
suorittamisen jälkeen pino näyttää seuraavalta
muistiosoite sisältö selitys
------------------------------------------------------------------------
0064FDE4 -04 00000000 <-ESP aliohjelman oma tila, eli min-muuttuja
0064FDE8 +00 0064FE04 <-EBP vanha EBP:n arvo, johon EBP nyt osoittaa
0064FDEC +04 0040112C paluuosoite kun aliohjelma on suoritettu
0064FDF0 +08 0064FE00 h:n osoite, eli ph:n arvo
0064FDF4 +0C 0064FDFC m:n osoite, eli pm:n arvo
0064FDF8 +10 00000037 lisa_min, eli 55
Esimerkiksi minuuteille sijoitus kääntyisi seuraavasti:
*pm = yht_min % 60;
muistiosoite assembler selitys
muistiosoite assembler selitys
-------------------------------------------------------------------------
004010A1 mov eax,[ebp-0x04] eax:ään yht_min muuttujan arvo
004010A4 mov ecx,0x0000003c ecx:ään 60 jakajaksi
004010A9 cdq konvertoidaan eax 64 bitiksi edx:eax
004010AA idiv ecx jaetaan edx:eax/ecx:llä tulos eax, jakoj. edx
004010AC mov eax,[ebp+0x0c] eax:ään m:n osoite (eli pm:n arvo)
004010AF mov [eax],edx sinne muistipaikkaan, jonne eax asoittaa,
eli 0064FDFC, eli pääohjelman m:ään kopioidaan
edx, eli 10. HUOM! Pääohjelman m:n arvo
muuttui juuri tällä hetkellä!
} aliohjelmasta poistuminen
004010B1 pop ecx pinosta pois aliohjelman omat muuttujat
004010B2 pop ebp alkuperäinen ebp:n arvo talteen
004010B3 ret ja paluu osoitteeseen joka on pinon päällä nyt
eli 0040112C, eli pääohjelmaan call-lauseen
jälkeen
HUOM! Kääntäjä ei ole optimoivillakaan asetuksilla huomannut, että sekä h:n että m:n sijoitus saataisiin samasta jakolaskusta, koska toisessa tarvitaan kokonaisosaa ja toisessa jakojäännöstä, jotka molemmat saadaan samalla kertaa idiv operaatiossa. Huonoa kääntäjän kannalta, mutta ilahduttavaa että hyvälle assembler–ohjelmoijallekin jää vielä käyttöä.
Takaisin asiaan. Nyt siis aliohjelmalla oli pelkkien 12,15,55 arvojen sijasta käytössä osoitteet pääohjelman h:hon ja m:ään sekä arvo 55. Näin aliohjelma pystyi muuttamaan kutsuneen ohjelman muuttujien arvoja. Sama kuvana ennen *pm–sijoitusta:
Tehtävä 8.73 aikalis3.cpp
Kirjoita aikalis2.cpp:stä viitemuuttujia käyttävä versio. Sisäisesti ohjelma kääntyy täsmälleen samanlaiseksi kuin osoittimilla, eli osoittimet ja viitemuuttujat ovat sisäisesti todellakin sama asia.
Tehtävä 8.74 Muotoilu?
Kokeilepa lisätä aikaan esimerkiksi 50 min. Mitä tulostuu? Miten vian voisi korjata?
Tehtävä 8.75 Tiedon lukeminen
Kirjoita aliohjelma lue, joka kysyy ja lukee arvon kellonajalle, syöttö muodossa 12:15.
8.6.5 Aliohjelmien kirjoittaminen
Aliohjelmien kirjoittaminen kannattaa aina aloittaa aliohjelmakutsun kirjoittamisesta. Näin voidaan suunnitella mitä parametrejä ja missä järjestyksessä aliohjelmalle viedään. Näinhän teimme mittakaava–ohjelmassakin.
8.6.6 Globaalit muuttujat
Muuttujat voidaan esitellä myös globaaleiksi. Mikäli muuttujat esitellään kaikkien ohjelman lausesulkujen ulkopuolella, näkyvät muuttujat kaikille niille lohkoille, jotka on esitelty muuttujan esittelyn jälkeen. Seuraava ohjelma on kaikkea muuta kuin hyvän ohjelmointitavan mukainen, mutta pöytätestaamme sen siitä huolimatta: "- c-muut\alisotku.c - parametrin välitystä
/* 01 */ /* alisotku.c */
/* 02 */ /* Mitä ohjelma tulostaa?? */
/* 03 */ #include <stdio.h>
/* 04 */
/* 05 */ int a,b,c;
/* 06 */
/* 07 */ void ali_1(int *a, int b)
/* 08 */ {
/* 09 */ int d;
/* 10 */ d = *a;
/* 11 */ c = b + 3;
/* 12 */ b = d - 1;
/* 13 */ *a = c - 5;
/* 14 */ }
/* 15 */
/* 16 */ void ali_2(int *a, int *b)
/* 17 */ {
/* 18 */ int c;
/* 19 */ c = *a + *b;
/* 20 */ *a = 9 - c;
/* 21 */ *b = 32;
/* 22 */ }
/* 23 */
/* 24 */ int main(void)
/* 25 */ {
/* 26 */ int d;
/* 27 */ a=1; b=2; c=3; d=4;
/* 28 */ ali_1(&d,c);
/* 29 */ ali_2(&b,&a);
/* 30 */ ali_1(&c,3+d);
/* 31 */ printf("%d %d %d %d\n",a,b,c,d);
/* 32 */ return 0;
/* 33 */ }
Seuraavassa g.c täytyy tulkita: globaali muuttuja c ja m.d: main–funktion muuttuja d.
kesken
Globaaleiden muuttujien käyttöä tulee ohjelmoinnissa välttää. Tuskin mistään on tullut yhtä paljon ohjelmointivirheitä, kuin vahingossa muutetuista globaaleista muuttujista!
Tehtävä 8.76 Muuttujien näkyvyys
Pöytätestaa seuraava ohjelma:
c-muut2.cpp - parametrin välitystä
/* 01 */ /* alisotk2.c */ /* Mitä ohjelma tulostaa?? */
/* 02 */ #include <stdio.h>
/* 03 */
/* 04 */ int b,c;
/* 05 */
/* 06 */ void s_1(int *a, int b)
/* 07 */ {
/* 08 */ int d;
/* 09 */ d = *a;
/* 10 */ c = b + 3;
/* 11 */ b = d - 1;
/* 12 */ *a = c - 5;
/* 13 */ }
/* 14 */
/* 15 */ void a_2(int *a, int &b)
/* 16 */ {
/* 17 */ c = *a + b;
/* 18 */ { int c; c = b;
/* 19 */ *a = 8 * c; }
/* 20 */ b = 175;
/* 21 */ }
/* 22 */
/* 23 */ int main(void)
/* 24 */ {
/* 25 */ int a,d;
/* 26 */ a=4; b=3; c=2; d=1;
/* 27 */ s_1(&b,c);
/* 28 */ a_2(&d,a);
/* 29 */ s_1(&d,3+d);
/* 30 */ printf("%d %d %d %d\n",a,b,c,d);
/* 31 */ return 0;
/* 31 */ }
9. Kohti olio–ohjelmointia
Mitä tässä luvussa käsitellään?
- tietueet
- yksinkertaiset luokat
- olioiden perusteet
- olioterminologia
- koostaminen
- perintä
- polymorfismi
Syntaksi:
luokan esittely: class cNimi : public cIsa, public cIsa { // 0-n x public cIsa
private: // päällä oletuksena
yksityiset_attribuutit // vain itse näkee
yksityiset_metodit
protected:
suojatut_attribuutit // perilliset näkee
suojatut_metodit
public:
julkiset_attribuutit // kaikki näkee
julkiset_metodit
}; // HUOM! puolipiste
attr kuten muuttuja
attrib.esitt. tyyppi attr;
metodin esitt. kuten aliohjelman esittely
metodin esitt.
luokan ulkop. tyyppi cNimi::metodin_nimi(param_lista)
viit.olion metod: olio.metodin_nimi(param,param) // 0-n x param
jos osoitin: pOlio->metodin_nimi(param,param)
yliluokan metodiin viit. yliluokka::metodin_nimi(param,param)
muodostaja cNimi(param_lista) // voi olla monta eri param_listoilla
hajoittaja ~cNimi
Tähän lukuun on kasattu suuri osa olioihin liittyvää asiaa yhden esimerkin valossa. Esimerkin yksinkertaisuuden takia se ei anna joka tilanteessa täyttä hyötyä esitetyistä ominaisuuksista. Lisäksi asiaa voi olla yhdelle lukukerralla liikaa ja esimerkiksi perintä ja polymorfismi kannattaa ehkä jättää myöhemmälle lukukerralle.
9.1 Miksi olioita tarvitaan
Emme tässä ryhdy pohtimaan kovin syvällisiä siitä, miten olio–ohjelmointiin on päädytty. Todettakoon kuitenkin että olio–ohjelmointi on hyvin luonnollinen jatke rakenteelliselle ohjelmoinnille heti, kun huomataan siirtää käsiteltävä data ja dataa käsittelevä koodi samaan ohjelman osaan. Tämä toimenpide voidaan tehdä tietenkin myös perinteisillä ohjelmointikielilläkin. Puhtaat oliokielet eivät vaan jätä edes muuta mahdollisuutta. Lähestymme asiaa evoluutiomaisesti – niin kuin kehitys on olioihin johtanut. Loput ylilaulut olioista kannattaa lukea jostakin hyvästä kirjasta.
Aloitetaanpa tutkimalla aikalisa esimerkkiämme. Pääohjelmassa esiteltiin muuttuja tunteja varten ja muuttuja minuutteja varten. Aluksi tämä saattaa tuntua hyvin luonnolliselta ja niin se onkin, niin kauan kuin ohjelman koko pysyy pienenä. Entäpä ohjelma jossa tarvitaan paljon kellonaikoja?
olioalk\aikalis4.cpp - useita aika "muuttujia"
... alku kuten aikalis3.cpp
int main(void) L?
{
int h1=12,m1=15;
int h2=13,m2=16;
int h3=14,m3=25;
lisaa(h1,m1,55);
tulosta(h1,m1);
lisaa(h2,m2,27);
tulosta(h2,m2);
lisaa(h3,m3,39);
tulosta(h3,m3);
return 0;
}
Hyvinhän tuo vielä toimii? Ja jos otettaisiin taulukot käyttöön, ei tarvitsisi edes numeroida muuttujia. Entäpä jos joku tulee ja sanoo, että sekunnitkin mukaan! Tulee paljon työtä jos on paljon aikoja.
Tehtävä 9.77 Tulostus
Mitä ohjelma aikalis4.cpp tulostaa?
9.2 Tietueet, välivaihe kohti olioita
Jo pitkään ovat ohjelmointikielet tarjonneet erään apuvälineen tietojen yhdistämiseen: tietueet. Kasataan asiaan liittyvät muuttujat yhteen "joukkoon" ja annetaan tälle "joukolle" uusi nimi, meidän esimerkissämme vaikkapa unkarilaisella nimeämisellä tAika – aika tyyppi:
olioalk\aikalist.cpp - yhteiset asiat samaan tietueeseen
#include <iostream.h>
#include <iomanip.h>
struct tAika {
int h,m;
};
void lisaa(tAika &rAika, int lisa_min)
{
int yht_min = rAika.h * 60 + rAika.m + lisa_min;
rAika.h = yht_min / 60;
rAika.m = yht_min % 60;
}
void tulosta(tAika Aika)
{ // Muotoilu jotta 15:04 tulostuisi oikein eikä 15:4
cout << setfill('0') << setw(2) << Aika.h << ":" << setw(2) << Aika.m << endl;
}
int main(void)
{
tAika a1={12,15}, a2={13,16}, a3={14,25};
lisaa(a1,55); tulosta(a1);
lisaa(a2,27); tulosta(a2);
lisaa(a3,39); tulosta(a3);
return 0;
}
9.2.1 Tietuemuuttujan esittely
Eli muutos ohjelmassa on varsin syntaktinen. Siinä missä ennen esittelimme 2 muuttuja, h1 ja m1, esitellään nyt vain yksi muuttaja a1 joka onkin aika–tyyppiä.
tAika a1;
9.2.2 Tietueen alustaminen esittelyn yhteydessä
Jos tietuetyyppinen muuttuja halutaan alustaa esittelyn yhteydessä, voidaan se tehdä (ja vain esittelyn yhteydessä):
tAika a1={12,15};
9.2.3 Uuden tietuetyypin määrittely
Uusi tietuetyyppinen tyyppi määritellään
struct tAika { // struct ja tyypin nimi
int h,m; // alkioiden tyypit ja nimet
};
// voi olla myös
struct tAika {
int h;
int m;
};
Huom! C–kielessä määrittely täytyy tehdä typedef –lauseella:
typedef struct {
int h,m;
} tAika;
Muuten kaikki tässä sanottu pätee myös C–kieleen. C++:ssakin voitaisiin tietue määritellä em. tavalla, yhteensopivuussyistä ehkä jopa kannattaisi?
9.2.4 Viittaus tietueen alkioon
Jos ohjelmassa tarvitsee viitata yksittäiseen tietueen alkioon, voidaan tämä tehdä ilmoittamalla tietuen nimi, piste ja tietueen alkio:
minuutit = a1.m;
9.2.5 Viittaus osoitteen avulla
Mikäli lisaa–aliohjelma olisi kirjoitettu C:mäisesti, pitäisi muistaa "tähdätä" osoitinta pitkin:
olioalk\aikalisp.cpp – osoittimen avulla viittaaminen tietueeseen
void lisaa(tAika *pAika, int lisa_min)
{
int yht_min = (*pAika).h * 60 + (*pAika).m + lisa_min;
(*pAika).h = yht_min / 60;
(*pAika).m = yht_min % 60;
}
...
lisaa(&a1,55);
9.2.6 Lyhennetty viittaus osoitteen avulla (->)
Muoto (*pAika).m
on aika pitkähkö kirjoittaa. Siksi sille onkin synonyymi pAika‑>m
, joka on lisäksi varsin havainnollinen; tähdätään osoitinta pitkin tietueen tiettyyn alkioon :
void lisaa(tAika *pAika, int lisa_min)
{
int yht_min = pAika->h * 60 + pAika->m + lisa_min;
pAika->h = yht_min / 60;
pAika->m = yht_min % 60;
}
Muista että tämä osoitemerkintä toimii VAIN silloin kun kyseessä on osoitin muuttuja. Tavallisen tietuemuuttujan tapauksessa viitataan alkioon pisteellä.
9.2.7 Tietueen muuttaminen?
Joko nyt on helpompi lisätä sekunnit, niin ettei esimerkiksi pääohjelmassa tarvitse tehdä muutoksia? Osittain, sillä jos tietueen viimeiseksi lisättäisiin int s, niin alustus
tAika a1={12,15};
jättäisi sekunnit alkuarvoonsa, joka tässä tapauksessa on valitettavasti tuntematon! Muuten riittää muuttaa pelkkiä aliohjelmia!
Suurin hyöty tietueesta olikin lähinnä kasata yhteen kuuluvat asiat saman nimikkeen alle. Tämä on kapselointia (encapsulation) väljässä mielessä.
Tehtävä 9.78 Päivämäärätyyppi
Esittele tietue, jolla kuvataan päivämäärä. Kirjoita aliohjelma tulosta, joka tulostaa päivämäärän.
9.2.8 Funktioiden kuormittaminen (lisämäärittely, overloading)
Edellisessä tehtävässä pyydettiin kirjoittamaan aliohjelma tulosta, joka tulostaa päivämäärätyyppisen muuttujan arvon. Onko tämä järkevää, koska meillä jo oli aliohjelma tulosta, joka tulostaa kellonajan? Eräs C++:n uusia ominaisuuksia on mahdollisuus kuormittaa, eli määritellä lisää merkityksiä (eng. overloading) funktion nimelle. Varsinainen kutsuttava funktio tunnistetaan nimen ja parametrilistassa olevien lausekkeiden avulla. Funktion nimi koostuukin tavallaan nimen ja parametrilistan yhdisteestä. Siten jos on esitelty
tAika aika={12,30};
tPvm pvm={14,1,1997};
tulosta(aika);
tulosta(pvm);
niin kumpikin tulosta–kutsu kutsuu eri aliohjelmaa. Funktioiden kuormitus onkin varsin mukava lisä ohjelmointiin, se ei kuitenkaan ole varsinaisia olio–ohjelmoinnin piirteitä.
9.3 Hynttyyt yhteen, eli muututaan olioksi
Itse asiassa vanhalla C–kielelläkin pystyi kirjoittamaan "olioita", kirjoittamalla tietuetyypin esittely ja sitä käyttävät aliohjelmat yhdeksi aliohjelmakirjastoksi. Näin data ja sitä käsittelevät aliohjelmat on kapseloitu yhdeksi paketiksi.
9.3.1 Terminologiaa
Asia voidaan viedä vielä hieman pitemmälle. Kasataankin käsittelevät aliohjelmat suoraan tietueen sisälle. Nyt astuu kuvan mukaan olio–ohjelmoijat ja he nimittävät sitten näin syntyneitä aliohjelmia metodeiksi (method), tai C++–kirjallisuudessa jäsenfunktioiksi (member function). Tietueen alkioita, kenttiä nimitetään sitten attribuuteiksi tai jäsenmuuttujiksi (member variable).
Itse tietue saakin nimen luokka (class) ja tietuetta vastaava muuttuja - luokan ilmentymä - on sitten se kuuluisa olio (object)
9.3.2 Ensimmäinen olio–esimerkki
Muutetaanpa aikalisa luokaksi ja olioksi:
olioalk\aikaolio.cpp – tietueesta olioksi
#include <iostream.h>
#include <iomanip.h>
struct cAika {
int h,m;
void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
void tulosta() const {
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
}
};
int main(void)
{
cAika a1={12,15}, a2={13,16}, a3={14,25};
a1.lisaa(55); a1.tulosta();
a2.lisaa(27); a2.tulosta();
a3.lisaa(39); a3.tulosta();
return 0;
}
Siinäpä se! Onko muutokset edelliseen nähden suuria? Siis iso osa koko olio–ohjelmoinnista (ja tietotekniikasta muutenkin) on markkinahenkilöiden huuhaata ja yleistä hysteriaa "kaiken ratkaisevan" teknologian ympärillä. No, tosin olio-ohjelmoinnissa on puolia, joita emme vielä ole nähneetkään, joiden ansiosta olio–ohjelmointia voidaan pitää uutena ja ohjelmointia ja ylläpitoa helpottavana teknologiana. Näitä ovat mm. perintä ja polymorfismi (monimuotoisuus), joihin emme valitettavasti tällä kurssilla ehdi perehtyä kovinkaan syvällisesti.
No takaisin esimerkkiimme. Uutta on lähinnä se, että metodien (no sanotaan tästä lähtien funktioita metodeiksi) parametrilistat ovat lyhentyneet. Itse oliota ei tarvitse enää viedä parametrina, koska metodit ovat luokan sisäisiä ja tällöin luokkaa edustava olio kyllä tuntee itse itsensä. Samoin on jäänyt pois metodien sisältä viite olioon - samasta syystä:
...
void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
...
Metodia kutsutaan analogisesti tietueen kenttään viittaamisen kanssa, eli
a1.lisaa(55); a1.tulosta();
Tällekin on keksitty oma nimi: välitetään oliolle viesti "tulosta" (message passing). Tässä kuitenkin jatkossa voi vielä lipsahtaa ja vahingossa sanomme kuitenkin, että kutsutaan metodia tulosta, vaikka ehkä pitäisi puhua viestin välittämisestä.
Metodin tulosta esittelyssä
void tulosta() const {
oleva const tarkoittaa että metodi ei muuta itse olion tilaa (sen attribuutteja). Metodi lisaa taas vastaavasti muuttaa olion tilaa hyvinkin paljon.
Tehtävä 9.80 const-metodi
Kokeile mitä kääntäjä ilmoittaa, jos muutat myös lisaa- metodin const- tyyppiseksi.
9.3.3 Taas terminologiaa
Kerrataanpa vielä termit edellisen esimerkin avulla:
Vinkki
Älä hämäänny termeistä
oliotermi perinteinen termi
-----------------------------------------------------------------------------
cAika - aika-luokka tietuetyyppi
h,m - aika-luokan attribuutteja tietueen alkio
lisaa,tulosta - aika-luokan metodeja funktio,aliohj.
a1,a2,a3 - olioita, jotka ovat aika-luokan ilmentymiä muuttuja
a1.lisaa(55) - viesti olioille a1: lisää 55 minuuttia aliohjelman kutsu
9.3.4 Luokka (class) ja kapselointi
Virallisesti luokat esitellään C++:ssa class–avainsanalla struct–avainsanan sijasta. Mikä ero sitten class–avainsanalla on? Kumpikin käy, mutta attribuuteille ja metodeille on suojaustasot, jotka oletuksena struct–määritellyssä luokassa kaikki ovat julkisia, eli metodeja voi kutsua kuka tahansa ja erityisesti kuka tahansa voi muuttaa attribuuttien arvoja ilman että olio tätä itse huomaa.
Kuva 9.1 Suojaustasot
Jos esimerkkimme luokka esiteltäisiin:
class cAika {
int h,m;
void lisaa(int lisa_min) {...}
void tulosta() const {...}
};
lakkaisi ohjelmamme toimimasta, koska esimerkiksi pääohjelman kutsu
a1.lisaa(55)
tulisi laittomaksi kaikkien luokan jäsenten ollessa yksityisiä (private). Ongelmaa voisi yrittää korjata esittelemällä metodit julkisiksi:
class cAika {
int h,m;
public:
void lisaa(int lisa_min) {...}
void tulosta() const {...}
};
Nyt kääntäjä valittaisi esimerkiksi:
Error: aikacla.cpp(21,13):Objects of type 'cAika' cannot be initialized with { }
rivistä
cAika a1={12,15},...
Nyt vasta alkaakin olio–ohjelmoinnin hienoudet! Aloittelijasta saattaa tuntua että mitä turhaa tehdään asioista monimutkaisempaa kun se onkaan! Nimittäin aikaolio.cpp saattoi tuntua kohtuullisen ymmärrettävältä. Mutta todellakin jos olisi ilman muuta sallittua sijoittaa oliolle mitä tahansa arvoja, niin mitä mieltä oltaisiin seuraavasta:
cAika a1={42,175};
Väärinkäytetyt ja virheelliset arvot muuttujilla on ollut ohjelmoinnin kiusa alusta alkaen. Nyt meillä on mahdollisuus päästä niistä eroon kapseloinnin (jotkut sanovat kotelointi, encapsulation) ansiosta. Eli kaikki arvojen muutokset (eli olio tapauksessa olion tilojen muutokset) voidaan suorittaa kontrolloidusti, vain olion itse siihen suostuessa. Mutta miten sitten alustuksen tapauksessa?
9.3.5 Muodostajat (constructor)
C++:ssa on yksi erityinen metodi: muodostaja (konstruktori, rakentaja, constructor), jota kutsutaan muuttujan syntyessä. Muodostajan tehtävä on alustaa olion tila ja luoda mahdollisesti tarvittavat dynaamiset muistialueet. Näin voidaan järjestää se, että olion tila on aina tunnettu olion syntyessä.
Joissakin oliokielissä konstruktori ilmoitetaan omalla avainsanallaan. C++:ssa muodostaja on metodi, jolla on sama nimi kuin luokalla. Muodostajia voi olla useitakin. Muodostaja on aina tyypitön, siis ei edes void–tyyppiä.
olioalk\aikacla.cpp - muodostaja alustamaan tiedot
#include <iostream.h>
#include <iomanip.h>
class cAika {
int h,m;
public:
cAika(int ih, int im) { h = ih; m = im; }
void lisaa(int lisa_min) {
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
void tulosta() const {
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
}
};
int main(void)
{
cAika a1(12,15), a2(13,16), a3(14,25);
a1.lisaa(55); a1.tulosta();
a2.lisaa(27); a2.tulosta();
a3.lisaa(39); a3.tulosta();
return 0;
}
Esimerkissämme muodostaja on esitelty 2-parametriseksi
cAika(int ih, int im) { h = ih; m = im; }
ja sitä ”kutsutaan” olion esittelyn yhteydessä
cAika a1(12,15);
9.3.6 Oletusmuodostaja (default constructor)
Nyt ei kuitenkaan voida esitellä oliota ilman alkuarvoa
cAika aika;
Kääntäjä antaisi esimerkiksi virheilmoituksen:
Error: aikacla.cpp(26,14):Could not find a match **for** 'cAika::cAika()'
Parametritöntä muodostajaa sanotaan oletusmuodostajaksi (default constructor). Sellainen on luokalla aina ilman muuta, jos luokalle ei ole esitelty yhtään muodostajaa. Jos luokalle esitellään muodostaja, ei oletusmuodostaja enää tulekaan automaattisesti.
Meidän pitäisi päättää nyt paljonko kellomme on, jos sitä ei erikseen ilmoiteta. Olkoon kello vaikka 0:0, eli keskiyö. Esittelemme oletusmuodostajan
olioalk\aikacla2.cpp - lisätään oletusmuodostaja
...
class cAika {
int h,m;
public:
cAika() { h = 0; m = 0; }
cAika(int ih, int im) { h = ih; m = im; }
void lisaa(int lisa_min) { ... }
void tulosta() const { ... }
};
int main(void)
{
cAika a1(12,15), a2(13,16), a3(14,25);
a1.lisaa(55); a1.tulosta();
...
cAika aika;
aika.tulosta();
return 0;
}
Oletusmuodostajaa ”kutsutaan” hieman epäloogisesti ilman sulkuja
cAika aika;
eikä
cAika aika();
kuten muiden parametrittömien funktioiden kutsusta saattaisi päätellä! Mainittu rivi nimittäin tarkoittaisi parametrittömän cAika tyyppisen funktion prototyyppiä.
9.3.7 Oletusparametrit
C++:ssa on myös ominaisuus antaa funktioiden ja metodien parametreille oletusarvoja oikealta vasemmalle päin. Koska oletusmuodostajaksi riittää se, että muodostajaa voi ”kutsua” ilman parametrejä, voitaisiin esitellä myös:
olioalk\aikacla3.cpp - oletusparametrit
...
public:
cAika(int ih=0, int im=0) { h = ih; m = im; }
void lisaa(int lisa_min) {
...
};
Nyt oliot voitaisiin esitellä millä tahansa seuraavista tavoista; ilman parametrejä, yhdellä tai kahdella parametrillä:
cAika a1, a2(13), a3(14,25);
Tulostus olisi vastaavasti:
a1.tulosta(); a2.tulosta(); a3.tulosta();
=>
00:00
13:00
14:25
Oletusparametri tarkoittaa siis sitä, että mikäli parametrillä ei ole kutsussa arvoa, ”sijoitetaan” parametrille funktion/metodin esittelyssä ollut oletusarvo.
9.3.8 Sisäinen tilan valvonta
Emme edelleenkään ole ottaneet kantaa siihen, mitä tapahtuu, jos joku yrittää alustaa oliomme mielettömillä arvoilla, esimerkiksi:
cAika a1(42,175);
Toisaalta miten joku voisi muuttaa ajan arvoa muutenkin kuin lisaa–metodilla? Teemmekin aluksi metodin aseta, jota kutsuttaisiin
a1.aseta(12,15); a2.aseta(16);
Nyt pitää kuitenkin päättää mitä tarkoittaa laiton asetus! Jos sovimme että minuuteissa yli 59 arvot ovat aina 59 ja alle 0:n arvot ovat aina 0, voisi aseta–metodi olla kirjoitettu seuraavasti:
void aseta(int ih,int im=0) {
h = ih; m = im;
if ( m > 59 ) m = 59;
if ( m < 0 ) m = 0;
}
Jos taas haluaisimme, että ylimääräinen osuus siirtyisi tunteihin, voitaisiinkin tämä tehdä "kierosti" lisaa–metodin avulla
olioalk\aikacla4.cpp - sisäinen tilan valvonta asetuksessa
#include <iostream.h>
#include <iomanip.h>
class cAika {
int h,m;
public:
void aseta(int ih,int im=0) {
h = ih; m = im; lisaa(0);
}
cAika(int ih=0, int im=0) { aseta(ih,im); }
void lisaa(int lisa_min) { ... }
void tulosta() const { ... }
};
int main(void)
{
cAika a1, a2(13), a3(14,25);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
return 0;
}
Huomattakoon, että samalla kun tehdään aseta–metodi, kannattaa muodostajassakin kutsua sitä. Näin olion tilaa muutetaan vain muutamassa metodissa, jotka voidaan ohjelmoida niin huolella, ettei mitään yllätyksiä pääse koskaan tapahtumaan. Tämä rupeaa jo muistuttamaan olio–ohjelmointia!
Tehtävä 9.81 Negatiivinen minuuttiasetus
Mitä ohjelma aikacla4.cpp tulostaisi? Miksi ohjelma toimisi halutulla tavalla?
Tehtävä 9.81 Negatiivinen minuuttiasetus
Tehtävä 9.82 Tuntien tarkistus
Ohjelmoi myös tunneille tarkistus, missä pidetään huoli siitä, että tunnit ovat aina välillä 0-23.
9.3.9 inline–metodit ja tavalliset metodit
Olemme kaikissa edeltävissä olioesimerkeissä kirjoittaneet metodien toteutuksen myös luokan esittelyn sisään. Javassa näin tehdään aina, mutta C++:ssa oikeastaan varsin harvoin. Tyypillisempi tapa kirjoittaa on sellainen, missä ensin esitellään luokka ilman metodien toteutusta ja sitten metodit esitellään luokan ulkopuolella:
olioalk\aikacla5.cpp - metodit luokan ulkopuolella
#include <iostream.h>
#include <iomanip.h>
class cAika {
int h,m;
public:
cAika(int ih=0, int im=0);
void aseta(int ih,int im=0);
void lisaa(int lisa_min);
void tulosta() const;
};
void cAika::aseta(int ih,int im)
{
h = ih; m = im; lisaa(0);
}
cAika::cAika(int ih, int im)
{
aseta(ih,im);
}
void cAika::lisaa(int lisa_min)
{
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
void cAika::tulosta() const
{
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
}
int main(void)
{
cAika a1, a2(13), a3(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
return 0;
}
Jos metodin toteutus on luokan ulkopuolella, pitää metodin nimen eteen liittää tieto siitä, minkä luokan metodista on kyse. Esimerkiksi:
void cAika::lisaa(int lisa_min)
Nyt metodit ovat todellisia kutsuttavia aliohjelmia. Tähän saakka metodit ovat olleet inline–funktioita, eli niihin ei ole tehty aliohjelmakutsua, vaan niiden koodi on sijoitettu kutsun tilalle. Etuna on nopeampi suoritus kun aliohjelmaan siirtyminen ja paluu jäävät pois, mutta haittana suurempi ohjelman koko, erityisesti jos kutsuja on paljon ja metodit ovat suuria.
Jos nopeussyistä jälkeenpäin jokin luokan ulkopuolelle kirjoitettu metodi haluttaisiinkin takaisin inline–metodiksi, voidaan tämä tehdä myös inline–avainsanalla:
inline void cAika::aseta(int ih,int im)
{
h = ih; m = im; lisaa(0);
}
Siis inline–metodeja voidaan tehdä kahdella eri tavalla:
Kirjoitetaan metodi luokan sisälle. Tässä monisteessa käytetään jatkossa tätä tapaa lähinnä siitä syystä, että ohjelmalistaukset tulevat näin hieman lyhyemmiksi.
Kirjoitetaan metodi luokan ulkopuolella ja kirjoitetaan metodin toteutuksen eteen inline.
Sopivalla inline–määreen käytöllä mahdollinen olio–ohjelmoinnin byrokratiasta aiheutuva hitaus voidaan poistaa. Tosin inline kannattaa laittaa vain usein kutsuttavien lyhyiden metodien eteen.
Esimerkiksi seuraavasta ohjelmasta:
olioalk\inline.cpp - esimerkki optimoinnista
#include <iostream.h>
inline void ynnaa_yksi(int &ri)
{
ri = ri + 1;
}
inline void ynnaa_kaksi(int &ri)
{
ynnaa_yksi(ri);
ynnaa_yksi(ri);
}
int main (void)
{
int i = 10;
ynnaa_kaksi(i);
cout << i << endl;
return 0;
}
riveistä
int i = 10;
ynnaa_kaksi(i);
hyvä kääntäjä kääntää koodin:
mov eax,10 // Suoraan kääntäjä ei välttämättä uskalla laittaa mov eax,12
inc eax // koska se ei voi tietää tarvitaanko inc lauseen ominaisuutta
inc eax // Oikein hyvä kääntäjä ehkä laittaisikin mov eax,12
joka on täsmälleen sama koodi, joka generoituu lauseista
i = 10; ++i; ++i;
Jos inline–määreet jätettäisiin ynnaa–aliohjelmista pois, olisi ynnaa_kaksi kutsu yli 30 kertaa hitaampi! (vanhoilla prosessooreilla oli).
9.3.10 Jako useampaan tiedostoon
Kaiken tämän jälkeen lukija varmaan taas kysyy: Miksi vielä tuplata koodin pituus, kun aikacla4.cpp oli vielä jotenkin siedettävä alkuperäisen aikalis4.cpp:n rinnalla! Vastaus on uudelleen käytettävyys. Eli kun lopulta aika–luokkamme on valmis, paketoimme sen sellaiseksi, että se on helppo liittää myöhempiin omiin ohjelmiimme tai jopa luovuttaa (mahdollisesti korvausta vastaan :-) muille erillisenä komponenttina. Tuplatyö nyt saattaa olla 1/100 työ tulevaisuudessa.
Oikeasti siis jaamme koko ohjelman 3 eri tiedostoon:
aika.h
- luokan esittely
aika.cpp
- luokan toteutus
aikatest.cpp
- luokan testaava pääohjelma
olioalk\aika.h - luokan esittely omaan tiedostoon
#ifndef AIKA_H // Suoja, jottei samaa koodia "includata" kahta kertaa!
#define AIKA_H
class cAika {
int h,m;
public:
cAika(int ih=0, int im=0);
void aseta(int ih,int im=0);
void lisaa(int lisa_min);
void tulosta() const;
};
#endif // AIKA_H
olioalk\aika.cpp - luokan metodit kirjoitettu omaan tiedostoon
#include <iostream.h>
#include <iomanip.h>
#include "aika.h"
void cAika::aseta(int ih,int im)
{
h = ih; m = im; lisaa(0);
}
cAika::cAika(int ih, int im)
{
aseta(ih,im);
}
void cAika::lisaa(int lisa_min)
{
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
void cAika::tulosta() const
{
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
}
olioalk\aikatest.cpp - aika.cpp:n testi
#include "aika.h"
// Projektiin aika.cpp ja aikatest.cpp
int main(void)
{
cAika a1, a2(13), a3(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
return 0;
}
Lopuksi tehtäisiin vielä projekti tai makefile johon kuuluvat aika.cpp ja aikatest.cpp.
9.3.11 this–osoitin
Jos verrataan funktiota
void lisaa(tAika *pAika, int lisa_min)
{
int yht_min = pAika->h * 60 + pAika->m + lisa_min;
pAika->h = yht_min / 60;
pAika->m = yht_min % 60;
}
...
lisaa(&a1,55);
ja metodia
void cAika::lisaa(int lisa_min)
{
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
...
a1.lisaa(55);
niin helposti näyttää, että ensin mainitussa funktiossa on enemmän parametrejä. Tosiasiassa kummassakin niitä on täsmälleen sama määrä. Nimittäin jokaisen metodin ensimmäisenä näkymättömänä parametrinä tulee aina itse luokan osoite, this. Voitaisiinkin kuvitella, että metodi onkin toteutettu:
"void cAika::lisaa(cAika *this,int lisa_min)" // Näin EI SAA KIRJOITTAA!!!
{
int yht_min = this->h * 60 + this->m + lisa_min;
this->h = yht_min / 60;
this->m = yht_min % 60;
}
...
"a1.lisaa(a1,55)";
Oikeasti this
–osoitinta ei saa esitellä, vaan se on ilman muuta mukana parametreissä sekä esittelyssä että kutsussa. Mutta voimme todellakin kirjoittaa:
void cAika::lisaa(int lisa_min)
{
int yht_min = this->h * 60 + this->m + lisa_min;
this->h = yht_min / 60;
this->m = yht_min % 60;
}
Jonkun mielestä voi jopa olla selvempi käyttää this–osoitinta luokan attribuutteihin viitattaessa, näinhän korostuu, että käsitellään nimenomaan tämän luokan attribuuttia h, eikä mitään globaalia muuttujaa h. Joskus this–osoitinta tarvitaan välttämättä palautettaessa oliotyyppisellä metodilla olion koko tila (esim. viite olioon). Lisäksi joissakin kielissä this–osoittimen vastinetta (usein self) on aina käytettävä.
Usein this-osoitinta käytetään, jos ei haluta antaa metodin parametrilistan muuttujille eri nimiä kuin vastaavilla attribuuteilla:
void cAika::aseta(int h, int m) {
this->h = h; this->m = m; lisaa(0);
}
9.4 Perintä
9.4.1 Luokan ominaisuuksien laajentaminen
Pidimme jo aikaisemmin toiveena sitä, että voisimme laajentaa luokkaamme käsittelemään myös sekunteja. Miksi emme tehneet tätä heti? No tietysti olisi heti pitänyt älytä laittaa mukaan myös sekunnit, mutta tosielämässäkin käy usein näin, eli hyvästäkin suunnittelusta huolimatta toteutuksen loppuvaiheessa tulee vastaan tilanteita, jossa alkuperäiset luokat todetaan riittämättömiksi.
Tämän laajennuksen tekemiseen on olio–ohjelmoinnissa kolme mahdollisuutta: Joko muuttaa alkuperäistä luokkaa, periä alkuperäisestä luokasta laajempi versio tai tehdä uusi luokka, jossa on alkuperäinen luokka yhtenä attribuuttina.
Tutustumme seuraavassa kaikkiin kolmeen eri mahdollisuuteen.
9.4.2 Alkuperäisen luokan muuttaminen
Läheskään aina ei voi täysin välttää sitäkään, etteikö alkuperäistä luokkaa joutuisi muuttamaan. Jos näin joudutaan tekemään, pitäisi tämä pystyä tekemään siten, että jo kirjoitettu luokkaa käyttävä koodi säilyisi täysin muuttumattomana (tai ainakin voitaisiin päivittää minimaalisilla muutoksilla) ja vasta uudessa koodissa käytettäisiin hyväksi luokan uusia ominaisuuksia.
Jos luokka on saatu joltakin kolmannelta osapuolelta, ei luokan päivittäminen edes ole mahdollista, vaan silloin täytyy turvautua muihin (parempiin) tapoihin.
Päivitämme nyt kuitenkin alkuperäistä luokkaa:
olioalk\aikacla6.cpp - laajentaminen muuttamalla kantaluokkaa
#include <iostream.h>
#include <iomanip.h>
class cAika {
int h,m,s; // lisätty s
public:
cAika(int ih=0, int im=0, int is=0); // lisätty is=0
void aseta(int ih,int im=0, int is=0); // lisätty is=0
void lisaa(int lisa_min, int lisa_sek=0); // lisätty lisa_sek=0
void tulosta(int tulsek=0) const; // lisätty tulsek=0
};
void cAika::aseta(int ih,int im, int is) // lisätty is
{
h = ih; m = im; s = is; lisaa(0,0); // lisätty s=is ja ,0
}
cAika::cAika(int ih, int im, int is) // lisätty int is
{
aseta(ih,im,is);
}
void cAika::lisaa(int lisa_min, int lisa_sek) // lisätty int lisa_sek
{
s += lisa_sek; // lisätty
int yht_min = h * 60 + m + lisa_min + s / 60; // lisätty s / 60
s = s % 60; // lisätty
h = yht_min / 60;
m = yht_min % 60;
}
void cAika::tulosta(int tulsek) const // lisätty int tulsek
{
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
if ( tulsek ) cout << ":" << setw(2) << s; // lisätty
cout << endl;
}
int main(void)
{
cAika a1, a2(13), a3(14,175); // ei muutoksia!
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
cAika a4(12,55,45); a4.tulosta(1); // lisätty uusi
a4.lisaa(3,30); a4.tulosta(1);
return 0;
}
Huh huh! Tulipa paljon muutoksia, mutta onnistuimme kuitenkin pitämään alkuperäisen pääohjelman koodin muuttumattomana. Tulostuksessa olisi tietysti voitu valita sekin linja, että aina tulostetaan sekunnit tai sekuntien tulostus on oletuksena. Kumpikin valinta olisi aiheuttanut olemassa olevan koodin toiminnan muuttumisen. Jos näin olisi haluttu, niin sitten olisi valittu niin.
Tehtävä 9.83 Sekuntien tulostus aina tai oletuksena
Muuta ohjelmaa aikacla6.cpp siten, että sekunnit tulostetaan aina. Muuta ohjelmaa aikacla6.cpp siten, että sekunnit tulostetaan oletuksena jos ne on != 0.
9.4.3 Koostaminen
Seuraava mahdollisuus olisi uuden luokan koostaminen (aggregation) vanhasta aikaluokasta ja sekunneista. Tämä mahdollisuus meillä on aina käytössä vaikkei alkuperäistä lähdekoodia olisikaan käytössä. Tätä vaihtoehtoa pitää aina vakavasti harkita. Tehdään koodi sellaiseksi, että uusi luokka tulostaa aina sekunnit (inline–muotoa käytetään tilan säästämiseksi):
olioalk\aikacla7.cpp - laajentaminen koostamalla
#include <iostream.h>
#include <iomanip.h>
class cAika {
... kuten aikacla5.cpp, mutta seuraava muutos:
void tulosta(int lf=1) const {
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
if ( lf ) cout << endl;
}
};
class cAikaSek {
cAika hm;
int s;
public:
void aseta(int ih=0, int im=0, int is=0) {
s = is; hm.aseta(ih,im); lisaa(0);
}
cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
void lisaa(int lisa_min, int lisa_sek=0) {
s += lisa_sek;
hm.lisaa(lisa_min+s/60);
s %= 60;
}
void tulosta(int lf=1) const {
hm.tulosta(0);
cout << ":" << setw(2) << s;
if ( lf ) cout << endl;
}
};
int main(void)
{
cAika a1, a2(13), a3(14,175);
a1.tulosta(); a2.tulosta(); a3.tulosta();
a1.aseta(12,15); a2.aseta(16,-15);
a1.tulosta(); a2.tulosta();
cAikaSek a4(12,55,45); a4.tulosta(1); // lisätty uusi
a4.lisaa(3,30); a4.tulosta(1);
return 0;
}
Valitettavasti emme aivan täysin onnistuneet. Nimittäin alkuperäisen luokan tulosta oli niin tyhmästi toteutettu, että se tulosti aina rivinvaihdon. Tämmöistä hölmöyttä ei pitäisi mennä koskaan tekemään ja siksipä alkuperäinen luokka pitää joka tapauksessa palauttaa valmistajalle remonttiin. No luokan valmistaja muutti tulosta–metodia siten, että se oletuksena tulostaa rivinvaihdon (merk. lf = line feed), mutta pyydettäessä jättää sen tekemättä. Näin vanha koodi voidaan käyttää muuttamattomana.
Palaamme tulostusongelmaan myöhemmin ja keksimme silloin paremman ratkaisun, jota olisi pitänyt käyttää jo alunperin.
Luokassa on niin vähän ominaisuuksia, että uudessa luokassamme olemme joutunee itse asiassa tekemään kaiken uudelleen ja on kyseenalaista olemmeko hyötyneet vanhasta luokasta lainkaan. Tämä on onneksi lyhyen esimerkkimme vika, todellisilla luokilla säästö kokonaan uudestaan kirjoitettuun verrattuna olisi moninkertainen.
9.4.4 Perintä
Viimeisenä vaihtoehtona tarkastelemme perintää (inheritance). Valinta koostamisen ja perinnän välillä on vaikea. Aina edes olioasiantuntijat eivät osaa sanoa yleispätevästi kumpiko on parempi. Nyrkkisääntönä voisi pitää seuraavaa is-a –sääntöä:
- Jos voi sanoa että lapsiluokka on (is-a) isäluokka, niin peritään.
- Jos sanotaan että lapsiluokassa on (has-a) isäluokka, niin koostetaan
Kokeillaanpa: "luokka jossa on aika sekunteina" on "aika–luokka". Kuulostaa hyvältä. Siis perimään (taas inline–muoto tilan säästämiseksi):
olioalk\aikacla8.cpp - laajentaminen perimällä
... kuten aikacla7.cpp
... kuten aikacla7.cpp
class cAikaSek : public cAika {
int s;
public:
void aseta(int ih=0, int im=0, int is=0) {
s = is; cAika::aseta(ih,im); lisaa(0);
}
cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
void lisaa(int lisa_min, int lisa_sek=0) {
s += lisa_sek;
cAika::lisaa(lisa_min+s/60);
s %= 60;
}
void tulosta(int lf=1) const {
cAika::tulosta(0);
cout << ":" << setw(2) << s;
if ( lf ) cout << endl;
}
};
int main(void)
... kuten aikacla7.cpp
Tässä tapauksessa kirjoittamisen vaiva oli tasan sama kuin koostamisessakin. Joissakin tapauksissa perimällä pääsee todella vähällä. Otamme tästä myöhemmin esimerkkejä, kunhan pääsemme eroon syntaksin esittelystä.
Lapsiluokka, aliluokka (child class, subclass) on se joka perii ja isäluokka, yliluokka (parent class, superclass) se joka peritään. Käytetään myös nimitystä välitön ali/yliluokka, kun on kyseessä perintä suoraan luokalta toiselle, kuten meidän esimerkissämme.
C++:ssa välitön yliluokka ilmoitetaan aliluokan esittelyssä:
class cAikaSek : public cAika {
Jos täytyy viitata yliluokan metodeihin, joille on kirjoitettu aliluokassa oma määrittely, käytetään näkyvyysoperaattoria :: (scope resolution operator):
cAika::aseta(ih,im);
Näkyvyysoperaattoria EI käytetä, mikäli samannimistä metodia ei ole aliluokassa.
Perintää kuvataan piirroksessa:
9.4.5 Polymorfismi, eli monimuotoisuus
Edellisestä esimerkistä ei oikeastaan paljastunut vielä mitään, mikä olisi puoltanut perintää. Korkeintaan snobbailu uudella syntaksilla. Mutta tosiasiassa pääsemme tästä kiinni olio–ohjelmoinnin tärkeimpään ominaisuuteen, jota on vaikea saavuttaa perinteisellä ohjelmoinnilla: polymorfismi (polymorphism) eli monimuotoisuus.
Lisätäänpä vielä testiohjelman loppuun:
cAika a1; ... a1.aseta(12,15);
cAikaSek a4(12,55,45); a4.lisaa(3,30);
cAika *pAika;
pAika = &a1; pAika->tulosta();
pAika = &a4; pAika->tulosta();
Tulostus:
12:15
12:59
Mistä tässä oli kyse? Osoitin pAika oli monimuotoinen, eli sama osoitin osoitti kahteen eri tyyppiseen luokkaan. Tämä on mahdollista, jos luokat ovat samasta perimähierarkiasta kuten tässä tapauksessa ja osoitin on tyypiltään näiden yhteisen kantaluokan olion osoitin.
Mutta hei! Eikös jälkimmäinen tulostus olisi pitänyt olla sekuntien kanssa, olihan jälkimmäinen tulostettava luokka "aikaluokka jossa sekunnit"? Totta! Taas on alkuperäisen luokan tekijä möhlinyt eikä ole lainkaan ajatellut että joku voisi periä hänen luokkaansa. Tässä syy, miksi otamme perinnän mukaan tässä vaiheessa opintoja. Vaikka jatkossa emme hirveästi perinnän varaan rakennakaan, emme myöskään saa tehdä luokkia, jotka olisivat "väärin" tehtyjä!
9.4.6 Myöhäinen sidonta
Ongelma täytyy ratkaista siten, että jo alkuperäisessä luokassa kerrotaan että vasta ohjelman suoritusaikana selvitetään mistä luokasta todella on kyse, kun metodia kutsutaan. Tällaista ominaisuutta sanotaan myöhäiseksi sidonnaksi (late binding) (tälle monisteellekin tulee kyllä myöhäinen sidonta), vastakohtana sille, jota olemme edellä käyttäneet, eli aikainen sidonta (early binding). Sidonnan sisäisen mekanismin opettelun jätämme jollekin toiselle kurssille (ks. vaikkapa Olio–ohjelmointi ja C++/VL).
Myöhäinen sidonta saadaan C++:ssa aikaan liittämällä virtual–avainsana metodien eteen. Kaikki ne metodit täytyy ilmoittaa myöhäiseen sidontaan, joita mahdolliset perilliset tulevat muuttamaan. Siis luokat vielä kerran remonttiin:
olioalk\aikacla9.cpp - myöhäinen sidonta
#include <iostream.h>
#include <iomanip.h>
class cAika {
int h,m;
public:
virtual void aseta(int ih,int im=0) { // lisätty virtual
h = ih; m = im; lisaa(0);
}
cAika(int ih=0, int im=0) { aseta(ih,im); }
virtual void lisaa(int lisa_min) { // lisätty virtual
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
virtual void tulosta(int lf=1) const { // lisätty virtual
cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
if ( lf ) cout << endl;
}
};
class cAikaSek : public cAika {
int s;
public:
virtual void aseta(int ih=0, int im=0) { cAika::aseta(ih,im); } // lisätty
virtual void aseta(int ih, int im, int is) { // lisätty virtual
s = is; aseta(ih,im); lisaa(0,0); // pois cAika::
}
cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
virtual void lisaa(int lisa_min) { cAika::lisaa(lisa_min); } // lisätty
virtual void lisaa(int lisa_min, int lisa_sek) { // lisätty virtual
s += lisa_sek; lisaa(lisa_min+s/60); s %= 60; // pois cAika::
}
virtual void tulosta(int lf=1) const { // lisätty virtual
cAika::tulosta(0);
cout << ":" << setw(2) << s;
if ( lf ) cout << endl;
}
};
int main(void)
.. kuten aikacla8.cpp + cAika *pAika; ...
Virtual sanat on lisätty kaikkien metodien eteen. Aliluokassa virtual on tarpeeton uudelleenmääriteltävien (korvaaminen, syrjäyttäminen, overriding) metodien kohdalle, mutta se on hyvä pitää siellä kommenttimielessä.
Sitten tuleekin hankalammin selitettävä asia. Miksi yksi-parametrinen lisaa ja kaksi-parametrinen aseta on täytynyt kirjoittaa uudelleen? Tämä johtuu kielen ominaisuuksista, sillä muuten vastaavat useampi-parametriset metodit syrjäyttäisivät alkuperäiset metodit ja alkuperäisiä ei olisi lainkaan käytössä. Tässä tapauksessa olisimmekin voineet antaa niiden syrjäytyä, mutta koska tulevaisuudesta ei koskaan tiedä, on alkuperäiselläkin parametrimäärällä olevien metodien ehkä hyvä säilyä.
Parempi ratkaisu olisi ehkä kuitenkin ollut, jos jo alkuperäisessä luokassa olisi varauduttu sekuntien tuloon, vaikka niitä ei olisi mitenkään otettukaan huomioon:
olioalk\aikaclaA.cpp - myöhäinen sidonta, sekunnit jo kantaluokassa
...
class cAika { // Muutokset aikacla8.cpp:hen verrattuna
int h,m;
public:
virtual void aseta(int ih=0,int im=0, int lisa_sek=0) { // virtual, lisa_sek
h = ih; m = im; lisaa(0);
}
cAika(int ih=0, int im=0) { aseta(ih,im); }
virtual void lisaa(int lisa_min, int lisa_sek=0) { // virtual, lisa_sek
int yht_min = h * 60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
virtual void tulosta(int lf=1) const { ... } // virtual
};
class cAikaSek : public cAika {
int s;
public:
virtual void aseta(int ih=0, int im=0, int is=0) { // virtual
s = is; cAika::aseta(ih,im); lisaa(0); // järj. vaihd.
}
cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
virtual void lisaa(int lisa_min, int lisa_sek=0) { // virtual
s += lisa_sek; cAika::lisaa(lisa_min+s/60); s %= 60;
}
virtual void tulosta(int lf=1) const { // virtual
};
...
int main(void)
{
...
cAika *pAika;
pAika = &a1; pAika->tulosta();
pAika = &a4; pAika->tulosta();
return 0;
}
Varoitus: Borlandin C++5.1:en optimoivalla kääntäjällä käännettynä em. koodi kaatoi kääntäjän ympäristöineen jos cAikaSek::aseta oli muodossa:
cAika::aseta(ih,im); s = is; lisaa(0);
Tehtävä 9.84 Miksi ensin sekuntien alustus?
Osaatko selittää miksi sekunnit pitää alustaa ensin metodissa: cAikaSek::aseta? Jos osaat, olet jo melkein valmis C++- ohjelmoija!
Tehtävä 9.85 2-3 - parametrinen aseta
Onko nyt olemassa 2 ja 3 - parametrinen aseta- metodi?
9.4.7 Yliluokan muodostajan kutsuminen ennen muodostajaa
Joskus kirjoittamalla vähän enemmän voi saada aikaan nopeampaa ja turvallisempaa koodia. Niin tässäkin tapauksessa. Nimittäin säätäminen siinä, ettemme vaivautuneet kirjoittamaan kumpaankin luokkaan erikseen oletusmuodostajaa, johtaa siihen että esimerkiksi alustus
cAikaSek a4(12,55,45);
kutsuu kaikkiaan seuraavia metodeja:
cAikaSek::cAikaSek(12,55,45);
cAika::cAika(0,0); // Oletusalustus yliluokalle
cAika::aseta(0,0,0);
cAika::lisaa(0,0); // Tässä cAika, koska ei vielä "kasvanut" cAikaSek
cAikaSek::aseta(12,55,45);
cAika::aseta(12,55);
cAikaSek::lisaa(0,0); // HUOM! Todellakin cAikaSek::lisaa virt.takia
cAika::lisaa(0,0);
cAikaSek::lisaa(0,0);
cAika::lisaa(0,0);
Olisitko arvannut! Enää ei tarvitse ihmetellä miksi olio–ohjelmat ovat isoja ja hitaita. Totta kai voitaisiin sanoa, että hyvän kääntäjän olisi pitänyt huomata tuosta optimoida päällekkäisyydet pois. Mutta tämä on vielä tänä päivänä kova vaatimus kääntäjälle. Mutta ehkäpä voisimme ohjelmoijina vähän auttaa:
olioalk\aikaclaB.cpp - oletusmuodostajat
...
class cAika {
int h,m;
public:
virtual void aseta(int ih=0,int im=0, int is=0) { ... }
cAika() { h = 0; m = 0; } // tai cAika() : h(0), m(0) {} // Oletusmuodostaja
cAika(int ih, int im=0) { aseta(ih,im); } // ih=0 oletus pois
virtual void lisaa(int lisa_min, int lisa_sek=0) { ... }
virtual void tulosta(int lf=1) const { ... }
};
class cAikaSek : public cAika {
int s;
public:
virtual void aseta(int ih=0, int im=0, int is=0) {...}
cAikaSek() : s(0), cAika() {} // Oletusmuodostaja
cAikaSek(int ih, int im=0, int is=0) : cAika(ih,im), s(is) { lisaa(0); }
};
...
Nyt on saatu ainakin seuraavat edut: oletustapauksessa kumpikin luokka alustuu pelkillä 0:ien sijoituksilla, ilman yhtään ylimääräistä kutsua. Oletusmuodostaja luokalla cAika voidaan esitellä esim. seuraavilla tavoilla, joista keskimmäistä voidaan pitää parhaana:
cAika() { h = 0; m = 0; } // Normaalit sijoitukset
cAika() : h(0), m(0) {} // h:n muodostaja arvolla 0, eli h=0 kokonaisluvulle
// m:n muodostaja arvolla 0, eli m=0
cAika() : h(0) { m=0; }
Vastaavasti luokalle cAikaSek pitää oletusmuodostaja tehdä seuraavanlaiseksi:
cAikaSek() : s(0), cAika() { } // s alusetetaan 0:ksi ja peritty yliluokka
// alustetaan muodostajalla cAika()
cAikaSek() { s = 0; } // TÄMÄ on TÄSSÄ tapauksessa sama kuin
// edellinen, koska jos yliluokan muodostajaa
// ei itse kutsuta, kutsutaan
// oletusmuodostajaa
ELI! Perityn yliluokan muodostajaa kutsutaan automaattisesti, jollei sitä itse tehdä. Nyt parametrillisessä muodostajassa kutsutaan yliluokan muodostajaa, ja näin voidaan välttää ainakin oletusmuodostajan kutsu:
cAikaSek(int ih, int im=0, int is=0) : cAika(ih,im), s(is) { lisaa(0); }
Nyt alustuksesta
cAikaSek a4(12,55,45);
seuraa seuraavat metodikutsut
cAikaSek::cAikaSek(12,55,45);
cAika::cAika(12,55); // NYT EI oletusalustusta yliluokalle
cAika::aseta(12,55,0);
cAika::lisaa(0,0);
cAikaSek::lisaa(0,0);
cAika::lisaa(0,0)
Turhaahan tuossa on vieläkin, mutta tippuihan kuitenkin noin puolet pois! Joka tapauksessa periminen tuottaa jonkin verran koodia, aina kun yliluokan metodeja käytetään hyväksi. Ja jollei käytettäisi, kirjoitettaisiin samaa koodia uudelleen, eli palattaisiin 70–luvulle. Nykyisin kasvanut koneteho kompensoi kyllä tehottomamman oliokoodin ja olio–ohjelmoinnin ansiosta pystytään kirjoittamaan luotettavampia (?) ja monimutkaisempia ohjelmia.
9.5 Saantimetodit
Osa aikaisemmista ongelmista olisi voitu kiertää, mikäli olisimme päässeet käsiksi luokan yksityisiin tietoihin. Esimerkiksi alkuperäisen luokan tulosta olisi voitu jättää koskemattomaksi vaikka se olikin väärin tehty (paitsi se virtual sinne kyllä olisi pitänyt lisätä joka tapauksessa). Tätä olisi voitu helpottaa sillä, että kantaluokassa cAika olisi julistettu h ja m protected suojauksella. Tosin vain perityssä versiossa tästä olisi ollut apua.
9.5.1 Miksi ja miten
Muutenkin saattaa tulla tilanteita, joissa luokan ulkopuolinen haluaa päästä käsiksi sisäisiin tietoihin. Ainakin lukemana niitä. Eihän ole ollenkaan tavatonta ajankaan kanssa, että joku haluaisi tietää tunnit, muttei tulostaa? Mikä ratkaisuksi? Julistetaanko kaikki attribuutit julkisiksi (public)? No ei sentään! Kirjoitetaan saantimetodi kullekin attribuutille, jonka perustellusti voidaan katsoa tarpeelliseksi jollekin ulkopuoliselle voitavan julkaista:
"Lopullinen" versio aikaluokastamme voisikin siis olla seuraava:
olioalk\aikaclaC.cpp - saantimetodit
class cAika {
int h,m;
public:
virtual void aseta(int ih=0,int im=0, int is=0) {...}
...
virtual void tulosta(int lf=1) const { ... }
int getH() const { return h; } // saantimetodi
int getM() const { return m; } // saantimetodi
};
Huomattakoon nyt, että perinnässä ei tarvitse määritellä uudestaan saantifunktioita getH() ja getM(), ainoastaan uudet, eli esimerkissämme getS().
Nyt voitaisiin esimerkiksi kutsua:
cout << a1.getH() << ":" << pAika->getM() << ":" << a4.getS() << endl;
Mikä tässä sitten on erona attribuuttien julkaisemiseen verrattuna? Se että attribuutit ovat nyt tietyssä mielessä vain luettavissa (read-only), eli niitä voi lukea saantimetodien avuilla, mutta niitä voi asettaa vain aseta–metodin avulla, joka taas pystyy suorittamaan oikeellisuustarkistukset ja näin olion tila ei koskaan pääse muuttumaan olion itsensä siitä tietämättä.
Saantimetodit kannattaa ilman muuta kirjoittaa inline, sillä niistä kääntäjä voi sitten kääntää nopeaa koodia, eikä saantibyrokratiasta tule yhtään kustannuksia suoraan attribuuttiin viittaamiseen verrattuna.
Kannattaakin harkita, nimeäisikö attribuutit uudelleen: ah,am,as, jotta nimet h,m,s jäisi vapaaksi saantimetodeille! Toisaalta tärkeimmistä attribuuteista voisi tehdä myös pitkillä nimillä varustetut saantimetodit: esimerkiksi tunnit(), minuutit() ja sekunnit(). Rakkaalla lapsella voi olla montakin nimeä ja inlinen ansiosta tämähän ei "maksa mitään".
Tehtävä 9.87 Saantimetodi sekunneille
Täydennä aikaclaB.cpp:hen em. saantimetodit ja lisäksi getS() aliluokkaan cAikaSek. Kirjoita myös pitemmillä nimillä varustetut saantimetodit.
Tehtävä 9.88 Saantimetodien käyttäminen
Muuta vielä edellisessä tehtävässä jokainen mahdollinen viittaus luokan sisälläkin saantimetodeja käyttäväksi suoran attribuuttiviittauksen sijasta. Käännä alkuperäinen koodi ja uusi koodi, sekä vertaa .exe tiedostoja keskenään jollakin vertailuohjelmalla.
9.5.2 Rajapinta ja sisäinen esitys
Kapseloinnin ansiosta luokan käyttämiseksi on tullut selvä rajapinta (interface): metodit, joilla olion tilaa muutetaan. Tämän rajapinnan ansiosta luokka muuttuu "mustaksi laatikoksi", jonka sisällöstä ulkomaailma ei tiedä mitään, mutta jonka kanssa voi kommunikoida metodien avulla.
Tämä luokan sisustan piilottaminen antaa meille mahdollisuuden toteuttaa luokka oleellisesti eri tavalla. Voimme esimerkiksi toteuttaa ajan minuutteina vuorokauden alusta laskien:
olioalk\aikaclaD.cpp - sisäinen toteutus minuutteina
...
class cAika {
int yht_min;
public:
virtual void aseta(int ih=0,int im=0, int is=0) {
yht_min = 0; lisaa(60*ih+im);
}
cAika(int ih=0, int im=0) { aseta(ih,im); }
virtual void lisaa(int lisa_min, int lisa_sek=0) { yht_min += lisa_min; }
virtual void tulosta(int lf=1) const {
cout << setfill('0') << setw(2) << getH() << ":" << setw(2) << getM();
if ( lf ) cout << endl;
}
int getH() const { return yht_min / 60; }
int getM() const { return yht_min % 60; }
};
...kaikki muu kuten aikaclab.cpp ...
Tehtävä 9.89 minuutteina()
Lisää aikaclaB:hen ja aikaclaD:hen, eli molempiin sisäisiin toteutustapoihin saantimetodi getMinuutteina, joka palauttaa kellonajan vuorokauden alusta minuutteina laskettuna. Lisää vielä saantimetodi getSekunteina.
9.6 Mistä hyviä luokkia
Alunperin kirjoittamamme luokka cAika kokikin varsin kovia tarkemmassa tarkastelussa. Näistä muutoksista osa oli vielä aivan perusasioita; läheskään kaikkea emme vieläkään ole ottaneet huomioon (vertailu, syöttö päätteeltä, muuntaminen merkkijonoksi ja takaisin, lisäyksessä tapahtuvan ylivuodon luovuttaminen päivämäärälle, jne.). Miten sitten monimutkaisempien luokkien kanssa? Niin kauan pärjää, kun luokat on omaan käyttöön. Heti kun yritetään tehdä yleiskäyttöisiä luokkia (joka on yksi olio–ohjelmoinnin tavoite), tuleekin ongelmia vastaan.
Paremmalla suunnittelulla luokasta olisi heti voinut tulla yleiskäyttöisempi. Usein jopa joudutaan tekemään kahden luokan yläpuolelle abstrakti, tai muuten vaan yhteinen yliluokka, josta hieman toisistaan poikkeavat luokat peritään. Kuljimme tämän pitkän tien sen vuoksi, että lukija oppisi ymmärtämään miksi valmiit luokat eivät ole parin rivin koodinpätkiä.
Tulevaisuudessa ohjelmoijat jakaantunevatkin selvästi kahteen ryhmään: toiset käyttävät valmiita luokkia (mikä on helppoa, jos luokat ovat kunnossa, vrt. Delphi tai Visual Basic, ehkä myös osittain Java ja Jonnen pyynnöstä mainitaan tietysti Python). Ammattitaitoisempi ryhmä sitten suunnittelee ja tekee näitä yleiskäyttöisiä luokkia. Sitä mukaa kun luokkia saadaan valmiiksi eri "elämän aloille", siirtyy ammattilaiset yhä spesifimmille aloille.
Esimerkiksi M$:in Windowsin ohjelmointiin tarkoitetut luokkakirjastot ovat paisuneet niin suuriksi, että niiden käyttämistä tuskin kukaan enää hallitsee, ja ennen kuin entisen kirjaston on ehtinyt edes auttavasti oppia, tuleekin uusi versio. Tästä tulee oravanpyörä, jossa voi olla kova homma pysyä mukana, jollei ohjelmateollisuus keksi jotakin uutta ja mullistavaa avuksi.
Joka tapauksessa haave siitä, että näkee näyn ja keksii hyvän luokan, jota muut sitten yhtään muuttamatta voivat käyttää hyväkseen, kannattaa heittää Ylistönrinteen sillan alle. Mieluummin kannattaa alistua siihen, että opettelee käyttämään hyviä luokkia ja imee niitä käyttäessään ideoita siitä, miten parantaa omia luokkiaan seuraavalla kerralla.
9.7 Valmiita luokkia
Olio-ohjelmoinnin eräs tavoite on tuottaa ohjelmoijien käyttöön yleiskäyttöisiä komponentteja, jotta jokainen ei keksisi samaa pyörää uudelleen. Erityisesti graafisen ohjelmoinnin puolella ja sekä myös tietokantaohjelmoinnin puolella näitä komponentteja onkin varsin mukavasti. Borlandin Delphillä syntyy melkein Kerho–ohjelmaamme vastaava Windows–ohjelma lähes koodaamatta, pelkästään pudottelemalla komponentteja lomakkeelle.
9.7.1 Merkkijonot
Jos kerrankin pääsisin vastakkain nykykielten kehittäjien kanssa, niin tekisi kovasti mieli kysyä ovatko he koskaan tehneet oikeaa ohjelmaa. Nimittäin lähes kielestä riippumatta kunnolliset merkkijonot loistavat poissaolollaan. Ja ohjelmoijat ovat käyttäneet äärettömästi työtunteja tehdessään itselleen aluksi edes auttavaa merkkijonokirjastoa. Ainoastaan "lelukielissä" – Basicissä ja Turbo Pascalissa on ollut hyvät ja turvalliset merkkijonot.
C–kielen char jono[10] on todellinen aikapommi, jonka aukkoisuuteen perustuu vielä tänäkin päivänä useat hakkereiden kikat murtautua vieraisiin tietojärjestelmiin. Katsotaanpa ensin mitä C–merkkijonoille voi/ei voi tehdä:
char s1[10],s2[5],*p; :-(
p = "Kana" // Toimii!
p[0] = 'S'; // Toimii! Mutta jatkossa käy huonosti...
s1 = "Kissa"; // ei toimi!
strcpy(s2,"Koira"); // Huonosti käy! Miksi? Älä käytä koskaan...
if ( s1 < s2 ) ... // Sallittu, mutta tekee eri asian kuin lukija arvaakaan...
gets(s1); // Itsemurha, tämä on eräs kaikkein hirveimmistä funktioista
// lukee päätteeltä rajattomasti merkkejä ...
fgets(s1,sizeof(s1),stdin); // Oikein! Tosin rivinvaihto jää jonoon jos syöte
// on lyhyempi kuin 9 merkkiä
printf(s1); // Ohohoh! Tämä jopa toimii!!!
cout << s1; // Ja jopa tämäkin!!!
cin >> s1; // Taas itsemurha ....
Palaamme myöhemmin monisteessa C:n merkkijonoihin, ja siihen miten niitä voidaan kohtuullisen turvallisesti käyttää.
Onneksi C++:assa on kohtuullinen merkkijonoluokka. Nyt jo! Yli 10 vuotta kielen kehittämisen jälkeen...
Katso esimerkiksi: Merkkijonot ja C++ / Antti-Juhani Kaijanaho
Merkkijonoluokalla voi tehdä mm. seuraavia:
\kurssit\cpp\ali\strtest.cpp - merkkijonojen testaus
#include <iostream.h>
#include <string>
using namespace std;
int main(void)
{
string mjono1 = "Ensimmainen";
string mjono2 = "Toinen";
string mjono;
// syotto ja tulostus
cout << "Anna merkkijono > ";
getline(cin,mjono,'\n');
cout << "Annoit merkkijonon : " << mjono << endl;
// kasittely merkeittain
mjono[0] = mjono1[4];
mjono[1] = mjono2[1];
mjono[2] = 't';
mjono[3] = '\0'; // Laitonta jos merkkijono ei ole nain iso!
// Ei vaikuta!
cout << mjono << endl; // tulostaa: mot ...+jotakin
// sijoitukset
mjono = mjono1;
cout << mjono << endl; // Ensimmainen
mjono = "Eka";
cout << mjono << endl; // Eka
// katenointi
mjono = mjono1 + mjono2;
cout << mjono << endl; // EnsimmainenToinen
mjono = mjono1 + "Toka";
cout << mjono << endl; // EnsimmainenToka
mjono = "Eka" + mjono2;
cout << mjono << endl; // EkaToinen
// vertailut
if (mjono1 == mjono2) cout << "1 on 2" << endl; // ei tulosta
if (mjono1 == "Apua") cout << "1 on Apua" << endl; // ei tulosta
if ("Apua" == mjono2) cout << "Apua on 2" << endl; // ei tulosta
if (mjono1 != mjono2) cout << "1 ei ole 2" << endl; // 1 ei ole 2
if (mjono1 != "Apua") cout << "1 ei ole Apua" << endl; // 1 ei ole Apua
if ("Apua" != mjono2) cout << "Apua ei ole 2" << endl; // Apua ei ole 2
if (mjono1 < mjono2) cout << "1 pienempi kuin 2" << endl; // 1 pienempi ku
if (mjono1 < "Apua") cout << "1 pienempi kuin Apua" << endl; // ei tulosta
if ("Apua" < mjono2) cout << "Apua pienempi kuin 2" << endl; // Apua pienempi
if (mjono1 > mjono2) cout << "1 suurempi kuin 2" << endl; // ei tulosta
if (mjono1 > "Apua") cout << "1 suurempi kuin Apua" << endl; // 1 suurempi ku
if ("Apua" > mjono2) cout << "Apua suurempi kuin 2" << endl; // ei tulosta
if (mjono1 <= mjono2) cout << "1 pienempi tai yhtasuuri kuin 2" << endl;
if (mjono1 >= mjono2) cout << "1 suurempi tai yhtasuuri kuin 2" << endl;
// Vastaavat vakiomerkkijonoilla EI onnistu, koska vakiomerkkijonot ovat
// OSOITTIMIA!
mjono1.erase(4,2);
cout << mjono1 << endl; // Ensiainen
mjono1.insert(4,"mm");
cout << mjono1 << endl; // Ensimmainen
return 0;
}
Luokan string pitäisi tulla nykyisten C++–kääntäjien mukana. Se saadaan käyttöön lisäämällä koodin alkuun:
#include <string> // Otetaan käyttöön standardin merkkijonoluokka
using namespace std; // nyt ei tarvitse kirjoittaa std::string
Jos koneessa on niin vanha kääntäjä, ettei uudet luokat käänny sillä (ei esim. poikkeutusten käsittelyä), voi käyttää \kurssit\c\ali hakemistosta löytyvää "lasten" vastinetta, string, eli em. testiohjelma toimii jos otsikkotiedostojen hakupolkuun lisätään mainittu ali–hakemisto.
Tehtävä 9.90 Ensimäinen melkein järkevä olio
Täydennä seuraava ohjelma
olioalk\oppilas.cpp - 1. järkevä olio
// Taydenna ja korjaile. Mista puuttuu virtual? Mista const?
// Mita metodeja viela puuttuu?
#include <iostream.h>
#include <string>
using namespace std;
class cHenkilo {
string nimi;
int ika;
double pituus_m;
public:
cHenkilo(string inimi="???",int iika=0,int ipituus=0) {}
void tulosta() {}
void kasvata(double cm) {}
};
class cOpiskelija : public cHenkilo {
double keskiarvo;
public:
cOpiskelija(string inimi="???",int iika=0, int ipituus=0, int ikarvo=0.0) :
cHenkilo(inimi,iika,ipituus), keskiarvo(ikarvo) {}
};
int main(void)
{
cHenkilo Kalle("Kalle",35,1.75);
Kalle.tulosta();
Kalle.kasvata(2.3);
Kalle.tulosta();
cOpiskelija Ville("Ville",21,1.80,9.9);
Ville.tulosta();
return 0;
}
10. C–kielen ohjausrakenteista ja operaattoreista
Mitä tässä luvussa käsitellään?
- if-else –lause
- loogiset operaattorit: &&, || ja !
- bittitason operaattorit: &,|,^ ja ~
- silmukat while, do-while ja for
- silmukan "katkaisu" break, continue, goto
- sijoituslauseet: = += -= jne.
- valintalause switch
Syntaksi:
lause joko ylause; // HUOM! Puolipiste
tai lohko // eli koottu lause
ylause yksinkertainen lause
esim a = b + 4
vaihda(a,b)
lohko { lause1 lause2 lause3 } // lauseita 0-n
esim { a = 5; b = 7; }
ehto lauseke joka tuottaa 0 tai != 0
esim a < 5
( 5 < a ) && ( a < 10 )
!a // jos a=0 => 1, muuten 0
HUOM! Vertailu a == 5
if-else if ( ehto ) lause1
else lause2 // ei pakollinen
while while ( ehto ) lause;
do-while do lause while ( ehto );
for for ( ylause1a,ylause2a; ehto ; ylause1k,ylause2k ) lause
esim for ( i=0,s=0; i<10; i++ ) s += i; // ylause1a
swicth switch ( lauseke ) {
case arvo1: lause1 break; // valintoja 0-n
case arvo2: // arvolla 2 ja 3 sama
case arvo3: lause2 break;
default: laused break; // ei pakollinen
}
Ohjelma jossa ei ole minkäänlaista valinnaisuutta tai silmukoita on varsin harvinainen. Kertaamme seuraavassa C/C++–kielen tarjoamat mahdollisuudet suoritusjärjestyksen ohjaamiseen. Samalla näemme kuinka suomenkielisen algoritmin kääntäminen ohjelmointikielelle on varsin mekaanista puuhaa.
10.1 if–lause
Mikäli meillä on kaksi lukua, jotka pitäisi olla suuruusjärjestyksessä, voisimme hoitaa järjestämisen seuraavalla algoritmilla:
1. Jos luvut väärässä järjestyksessä,
niin vaihda ne keskenään
Tämän kirjoittamiseksi ohjelmaksi tarvitsemme ehto–lausetta:
if ( ehto ) ylause1;
lause2;
Huomattakoon, että tässä sulut ehdon ympärillä ovat pakolliset. lause1 suoritetaan vain kun ehto on voimassa. lause2 suoritetaan aina. Lause voitaisiin kirjoittaa myös muodossa
if(ehto) ylause1;
lause2;
muttei näin tehdä, jotta erottaisimme paremmin funktion ja if–lauseen toisistaan. Sama tulee koskemaan myös for, while ja muita vastaavia rakenteita.
10.1.1 Ehdolla suoritettava yksi lause
Olkoon meillä aliohjelma nimeltään vaihda, joka suorittaa itse vaihtamisen:
if ( a > b ) vaihda(&a,&b);
10.1.2 Ehdolla suoritettava useita lauseita
Mikäli aliohjelmaa ei ole käytössä, täytyisi meidän voida suorittaa useita lauseita muuttujien vaihtamiseksi. C–kielessä voidaan lausesuluilla kasata joukko lauseita yhdeksi lauseeksi (lohko, koottu lause, block):
Vinkki
Sisennä kauniisti
if ( a > b ) {
t = a;
a = b;
b = t;
}
Huomautus! Lauseiden kirjoittaminen samalle riville ei auttaisi mitään, sillä
if ( a > b ) t = a; a = b; b = t;
/* vastaisi loogisesti rakennetta: */
if ( a > b ) t = a;
a = b;
b = t;
Koodia voidaan kuitenkin usein lyhentää kirjoittamalla asioita samalle riville:
if ( a > b ) {
t = a; a = b; b = t;
}
/* tai joskus jopa */
if ( a > b ) { t = a; a = b; b = t; }
Niin kauan kuin todella hallitsee asian, voi olla helpointa laittaa aina if–lauseen ainoakin suoritettava lause lausesulkuihin
if ( a > b ) {
vaihda(&a,&b);
}
Tästä on se etu, että myöhemmin monimutkaisten makrojen kanssa ei tule ongelmia, sekä se, että nyt if–lauseen suoritettaviksi lauseiksi on helppo lisätä uusia lauseita. Mikäli sulkuja ei olisi, täytyisi toisen lauseen lisäyksen yhteydessä muistaa lisätä myös sulut (tosin eihän hyvin suunniteltua ohjelmaa tarvinnut enää jälkeenpäin paikata?).
10.2 Loogiset lausekkeet
C–kielessä mikä tahansa lauseke voidaan kuvitella loogiseksi lausekkeeksi. Arvo 0 on epätosi ja kaikki muut arvot ovat tosia.
a = 4;
if ( a ) ...
10.2.1 Vertailuoperaattorit
Vertailuoperaattorin käyttö muodostaa loogisen lausekkeen, jonka arvo on 0 tai 1. Vertailuoperaattoreita ovat:
== yhtäsuuruus
!= erisuuruus
< pienempi kuin
<= pienempi tai yhtä kuin
> suurempi kuin
>= suurempi tai yhtä kuin
Esimerkkejä vertailuoperaattoreiden käytöstä:
if ( a < 5 ) printf("a alle viisi!\n);
if ( a > 5 ) printf("a yli viisi!\n);
if ( a == 5 ) printf("a tasan viisi!\n);
if ( a != 5 ) printf("a ei ole viisi!\n);
10.2.2 Sijoitus palauttaa arvon!
Yhtäsuuruutta verrataan == operaattorilla, EI sijoituksella =. Tämä on eräs tavallisimpia aloittelevan (ja kokeneenkin) C–ohjelmoijan virheitä:
c-silm\ifsij.c - esimerkki sijoituksesta ehdossa
/* Seuraava tulostaa vain jos a == 5 */
if ( a == 5 ) printf("a on viisi!\n");
/* Seuraava sijoittaa aina a = 5 ja tulostaa AINA! */
if ( a = 5 ) printf("a:ksi tulee AINA 5!\n");
Sijoitus a=5 on myös lauseke, joka palauttaa arvon 5. Siis sijoitus kelpaa tästä syystä vallan hyvin loogiseksi lausekkeeksi. Onneksi useat C–kääntäjät osaavat varoittaa tästä katalasta virhemahdollisuudesta.
Joskus ominaisuutta voidaan tarkoituksella käyttää hyväksikin. Esimerkiksi halutaan sijoittaa AINA a=b ja sitten suorittaa jokin lause, mikäli b!=0. Tämä voitaisiin kirjoittaa useilla eri tavoilla:
c-silm\ifsij2.c - esimerkki tahallisesta sijoituksesta ehdossa
/*1*/ a = b; if ( b ) printf("b ei ole nolla!\n");
/*2*/ a = b; if ( b != 0 ) printf("b ei ole nolla!\n");
/*3*/ if ( a = b ) printf("b ei ole nolla!\n");
/*4*/ if ( (a=b) != 0 ) printf("b ei ole nolla!\n");
Edellisistä tapa 3 on C–mäisin, mutta kääntäjä saattaa varoittaa siitä (ja tämä varoitus kannattaa ottaa todesta). Jotta C–mäinen tapa voitaisiin säilyttää, voidaan käyttää tapaa 4 jolloin varoitusta ei tule, mutta generoidaan aivan vastaava koodi. Oleellista on, että sijoitus on suluissa (muuten tulisi sijoitus a =(b!=0) ). Mikäli asian toimimisesta on pieninkin epäilys kannattaa käyttää tapaa 1 tai 2!
10.3 Loogisten lausekkeiden yhdistäminen
Loogisia lauseita voidaan yhdistää loogisten operaatioiden avulla. Tietysti lauseita voidaan yhdistää myös normaaleilla operaatioilla (+,–,*,/), mutta tämä ei ole oikein hyvien tapojen mukaista.
10.3.1 Loogiset operaattorit &&, || ja !
&& ja
|| tai
! muuttaa ehdon arvon päinvastaiseksi (eli 0–>1, <>0 –>0)
Mikäli yhdistettävät ehdot koostuvat esimerkiksi vertailuoperaattoreiden käytöstä, kannattaa ehtoja sulkea sulkuihin, jottei seuraa turhia epäselvyyksiä.
if ( ( rahaa > 50 ) && ( kello < 19 ) ) printf("Mennään elokuviin!\n);
if ( ( rahaa < 50 ) || ( kello >3 ) ) printf("Ei kannata mennä kapakkaan!\n");
if ( ( 8 <= kello ) && ( kello <= 16 ) ) printf("Pitäisi olla töissä!\n");
if ( ( !rahaa ) || ( sademaara < 10 ) ) printf("Kävele!\n");
Usein tulee vastaan tilanne, jossa pitäisi testata on luku jollakin tietyllä välillä. Esimerkiksi onko
1900 <= vuosi <= 1999
palauttaisi C-kielisenä lauseena aina 1. Miksikö? Koska lause jäsentyy
( 1900 <= vuosi ) <= 1999
0 tai 1 <= 1999 eli aina 1
Oikea tapa kirjoittaa väli olisi:
if ( ( 1900 <= vuosi ) && ( vuosi <= 1999 ) ) ...
Huomattakoon edellä miten väliä korostettiin kirjoittamalla välin päätepisteet lauseen laidoille.
C–kielen sidontajärjestyksen ansiosta lause toimisi myös ilman sisimpiä sulkuja, mutta ne kannattaa pitää mukana varmuuden vuoksi. Vertailtavat kannattaa kirjoittaa nimenomaan tähän järjestykseen, koska tällöin vertailu muistuttaa eniten alkuperäistä väliämme!
Vastaavasti jos arvon halutaan olevan välin ulkopuolella, kannattaa kirjoittaa:
if ( ( vuosi < 1900 ) || ( 1999 < vuosi ) ) ...
Tällöin epäyhtälöiden suuntaa ei joudu koskaan miettimään, vaan arvot ovat aina siinä järjestyksessä kuin lukusuorallakin:
1900 vuosi 1999 1900<=vuosi && vuosi <=1999
-----------o==============o--------------------
vuosi 1900 1999 vuosi vuosi<1900 || 1999 <vuosi
===========o--------------o====================
10.3.2 Loogisen lausekkeen suoritusjärjestys
Loogiset lausekkeet suoritetaan AINA vasemmalta oikealle, kunnes ehdon arvo on selvinnyt.
Siis: Loogisen lausekkeen evaluoiminen lopetetaan heti kun ehdon arvo selviää (boolean expression shortcut).
Esimerkiksi:
if ( a || ( (b=c)==0 ) ) printf("Kukkuu\n");
Tai-operaattorin (||) oikealla puolella oleva sijoitus suoritetaan vain mikäli a==0:
a b c sij.suor tulostetaan | ||||||
---|---|---|---|---|---|---|
0 | ? | 0 | kyllä | kyllä | ||
0 | ? | 3 | kyllä | ei | ||
5 | ? | 0 | ei | kyllä | ||
5 | ? | 3 | ei | kyllä |
Tätä ominaisuutta voidaan käyttää hyväksi esimerkiksi tiedostoja luettaessa:
while ( f && f >> luku )
summa += luku;
Tällöin lopussa olevaa tiedostoa ei enää lueta, koska AND–operaation (&&) arvo voidaan päättää epätodeksi heti ensimmäisestä osalauseesta. Tosin edellinen silmukka toimii myös muodossa
while ( f >> luku ) summa += luku;
10.4 Bittitason operaattorit
Yksi C–kielen vahvoista piirteistä erityisesti alemman tason ohjelmoinnissa on mahdollisuus käyttää bittitason operaattoreita.
Loogisia operaattoreita &&, || ja ! ei pidä sotkea vastaaviin bittitason operaattoreihin:
& bittitason AND
| bittitason OR
^ bittitason XOR
~ bittitason NOT
<< rullaus vasemmalle, 0 sisään oikealta
>> rullaus oikealle, 0 sisään vasemmalta (unsigned int ja int >=0)
, voi tulla 0 tai 1 sisään vasemmalta (int joka <0)
(laiteriippuva, esim. Turbo C:ssä tulee 1).
Bittitason operaattoreita voidaan käyttää vain kokonaisluvuiksi muuttuviin operandeihin.
Operaattoreiden toimintaa voidaan kuvata seuraavasti. Olkoon meillä sijoitukset a=5; b=14;. Kuvitellaan kokonaisluvut tilapäisesti 8 bitin mittaisiksi (oikeasti yleensä 16 tai 32 bittiä):
Binäärisenä | desim. | ||
---|---|---|---|
a | 0000 0101 | 5 | |
b | 0000 1110 | 14 | |
a & b | 0000 0100 | 4 | |
a | b | 0000 1111 | 15 | |
a ^ b | 0000 1011 | 11 | |
~a | 1111 1010 | -6 | |
a<<2 | 0001 0100 | 20 | |
b>>3 | 0000 0001 | 1 | |
a && b | 0000 0001 | 1 | |
a || b | 0000 0001 | 1 | |
!a | 0000 0000 | 0 |
Huomautus! Tyypillinen ohjelmointivirhe on sotkea keskenään loogiset ja bittitason operaattorit:
{ /* binoper.c */
int a=5; b=2;
if ( a&&b ) printf("On ne!\n");
if ( a&b ) printf("Ei ne ookkaan!\n");
if ( a ) printf("a on!\n");
if ( ~b ) printf("b ehkä on!\n");
if ( !b ) printf("b ei ole!\n");
}
10.5 if – else –rakenne
if –lauseesta on myös versio, jossa jotakin voidaan tehdä ehdon ollessa epätosi:
if ( ehto ) ylause1;
else ylause2;
Jälleen, mikäli jommassa kummassa osassa tarvitaan useampia lauseita, suljetaan lausejoukko lausesuluilla. Tosin kannattaa taas harkita lausesulkujen käyttöä aina myös yhdenkin lauseen tapauksessa.
/* Samalle riville: */
if ( a < 5 ) printf("a alle viisi!\n");
else printf("a vähintään viisi!\n");
/* Eri riville: */
if ( a < 5 )
printf("a alle viisi!\n");
else
printf("a vähintään viisi!\n");
/* Lausesulkujen käyttö: */
if ( a < 5 ) {
printf("a alle viisi!\n");
}
else {
printf("a vähintään viisi!\n");
}
/* Seuraavaa tyyliä käytetään myös usein: */
if ( a < 5 ) {
printf("a alle viisi!\n");
} else {
printf("a vähintään viisi!\n");
}
10.5.1 Sisäkkäiset if–lauseet
Meillä oli aikaisemmin tehtävänä kirjoittaa funktio, joka palauttaa toisen asteen yhtälön ax2+bx+c=0 toisen juuren. Tällöin oletuksena oli, että a<>0 ja D>=0. Mikäli ratkaisukaavaa sovelletaan sellaisenaan ja a=0 tai D<0, niin tällöin ohjelman suoritus päättyy ajonaikaiseen virheeseen.
Voisimme muuttaa tehtävän määrittelyä siten, että kumpikin juuri pitää palauttaa ja funktion nimessä palautetaan tieto siitä, tuliko ratkaisussa virhe, eli jollei juuret olekaan reaalisia.
if ( a != 0 ) {
D = b*b – 4*a*c;
if ( D > 0 ) {
...
}
else {
...
}
}
else {
...
}
Tosin yhtälö pystytään mahdollisesti ratkaisemaan myös kun a==0. Tällöin tehtävä jakautuu useisiin eri tilanteisiin kertoimien a,b ja c eri kombinaatioiden mukaan:
juuret | |||||||||
---|---|---|---|---|---|---|---|---|---|
a | b | c | D | yhtälön muoto | reaalisia | x1 | x2 | ||
0 | 0 | 0 | ? | 0 = 0 | juu | 0 | 0 | ||
0 | 0 | c | ? | c = 0 | ei | 0 | 0 | ||
0 | b | ? | ? | bx - c = 0 | juu | -c/b | -c/b | ||
a | ? | ? | >=0 | ax2 + bx + c = 0 | juu | (-b-SD)/2a | (-b+SD)/2a | ||
a | ? | ? | <0 | - " - | ei |
Algoritmiksi kirjoitettuna tästä seuraisi:
1. Jos a=0, niin
Jos b=0
Jos c=0 yhtälö on muotoa 0=0 joka on aina tosi
palautetaan vaikkapa x1=x2 =0
muuten (eli c<>0) yhtälö on muotoa c=0 joka on
aina epätosi, palautetaan virhe
muuten (eli b<>0) yhtälö on muotoa bx=c
joten voidaan palauttaa vaikkapa x1=x1=–c/b
2. Jos a<>0, niin
Jos D>=0 kyseessä aito 2. asteen yhtälö ja käytetään
ratkaisukaavaa
muuten (eli D<0) ovat juuret imaginaarisia
Funktio ja sen testiohjelma voisi olla esimerkiksi seuraavanlainen:
c-silm\p2_2.cpp - esimerkki 2. asteen yhtälön ratkaisemiseta
#include <iostream.h>
#include <math.h>
int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
double a, double b, double c)
{
double D,SD;
x1 = x2 = 0;
if ( a == 0 ) { /* bx + c = 0 */
if ( b == 0 ) { /* c = 0 */
if ( c == 0 ) { /* 0 = 0 */
return 0; /* id. tosi */
} /* c==0 */
else { /* c!=0 */ /* 0 != c = 0 */
return 1; /* Aina epät. */
} /* c!=0 */
} /* b==0 */
else { /* b!=0 */ /* bx + c = 0 */
x1 = x2 = –c/b;
return 0;
} /* b!=0 */
} /* a==0 */
else { /* a!= 0 */ /* axx + bx + c = 0 */
D = b*b – 4*a*c;
if ( D >= 0 ) { /* Reaaliset juuret */
SD = sqrt(D);
x1 = (–b–SD)/(2*a);
x2 = (–b+SD)/(2*a);
return 0;
} /* D>=0 */
else { /* Imag. juuret */
return 1;
} /* D<0 */
} /* a!=0 */
}
double P2(double x, double a, double b, double c)
{
return (a*x*x + b*x + c);
}
int main(void)
{
double a,b,c,x1,x2;
do {
cout << "Anna 2. asteen yhtälön a b c >";
cin >> a >> b >> c;
if ( ratkaise_2_asteen_yhtalo(x1,x2,a,b,c) ) {
cout << "Yhtälöllä ei ole reaalisia juuria!" << endl;
}
else {
cout << "1. ratkaisu on " << x1
<< ". Arvoksi tulee tällöin " << P2(x1,a,b,c) << endl;
cout << "2. ratkaisu on " << x2
<< ". Arvoksi tulee tällöin " << P2(x2,a,b,c) << endl;
}
} while (a>0);
return 0;
}
Edellinen funktio on äärimmäinen esimerkki sisäkkäisistä if–lauseista. Jälkeenpäin sen luettavuus on erittäin heikko ja myös kirjoittaminen hieman epävarmaa. Parempi kokonaisuus saataisiin lohkomalla tehtävää pienempiin osasiin aliohjelmien tai makrojen avulla.
Sisäkkäisten if–lauseiden kirjoittamista voidaan helpottaa kirjoittamalla niitä sisenevästi, eli aloittamalla ensin tekstistä:
if ( a == 0 ) { /* bx + c = 0 */
} /* a==0 */
else { /* axx + bx + c = 0 */
D = b*b – 4*a*c;
} /* a!=0 */
Sitten täydennetään vastaavalla ajatuksella sekä if–osan että else–osan toiminta.
Jos funktiosta karsitaan kaikki ylimääräinen (kommentit ja ylimääräiset lausesulut) pois, saamme seuraavan näköisen kokonaisuuden:
c-silm\p2_2l.cpp - karsittu versio 2. asteen yhtälöstä
int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
double a, double b, double c)
{
double D,SD;
x1 = x2 = 0;
if ( a == 0 )
if ( b == 0 ) {
if ( c == 0 ) return 0;
else return 1;
}
else {
x1 = x2 = –c/b;
return 0;
}
else {
D = b*b – 4*a*c;
if ( D >= 0 ) {
SD = sqrt(D);
x1 = (–b–SD)/(2*a);
x2 = (–b+SD)/(2*a);
return 0;
}
else return 1;
}
}
Joskus kannattaa harkita olisiko luettavuuden kannalta paras esitystapa sellainen, että käsitellään "normaaleimmat" tapaukset ensin:
c-silm\p2_2n.cpp - normaalit tapaukset ensin ratkaisussa
int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
double a, double b, double c)
{
double D,SD;
x1 = x2 = 0;
if ( a != 0 ) {
D = b*b – 4*a*c;
if ( D >= 0 ) {
SD = sqrt(D);
x1 = (–b–SD)/(2*a);
x2 = (–b+SD)/(2*a);
return 0;
}
else return 1;
}
else /* a==0 */
if ( b != 0 ) {
x1 = x2 = –c/b;
return 0;
}
else { /* a==0, b==0 */
if ( c == 0 ) return 0;
else return 1;
}
}
Usein aliohjelman return–lauseen ansiosta else osat voidaan jättää poiskin:
c-silm\p2_2r.cpp - else -osat pois
int ratkaise_2_asteen_yhtalo(double &x1, double &x2,
double a, double b, double c)
{
double D,SD;
x1 = x2 = 0;
if ( a == 0 ) {
if ( b == 0 ) {
if ( c == 0 ) return 0;
return 1;
}
x1 = x2 = –c/b;
return 0;
}
D = b*b – 4*a*c;
if ( D < 0 ) return 1;
SD = sqrt(D);
x1 = (–b–SD)/(2*a);
x2 = (–b+SD)/(2*a);
return 0;
}
Edellä oli useita eri ratkaisuja saman ongelman käsittelemiseksi. Liika kommenttien määrä saattaa myös sekoittaa luettavuutta kuten 1. esimerkissä. Toisaalta liian vähillä kommenteilla ei ehkä kirjoittaja itsekään muista jälkeenpäin mitä tehtiin ja miten. Jokainen valitkoon edellä olevista itselleen sopivimman kultaisen keskitien.
Huomattakoon vielä lopuksi, että rakenne
if ( c == 0 ) return 0;
else return 1;
voitaisiin korvata rakenteella
return ( c != 0 );
10.5.2 Useat peräkkäiset ehdot
Vaikka rakenne
if (ehto1) lause1;
else
if (ehto2) lause2;
else
if (ehto3) lause3;
else lause4;
jossain mallissa sisennetäänkin ylläkuvatulla tavalla, on ajatus useimmiten lähempänä seuraavaa sisennystä:
c-silm\postimak.c - esimerkki samanarvoisista ehtolauseista
double postimaksu(double paino)
/* Palautetaan kirjemaksun suuruus. 0 tarkoittaa pakettia */
{
if ( paino < 50 ) return 2.10;
else if ( paino < 100 ) return 3.50;
else if ( paino < 250 ) return 5.50;
else if ( paino < 500 ) return 10.00;
else if ( paino < 1000 ) return 15.00;
else return 0.00;
}
Sovimme siis, että rakenne onkin muotoa:
if ( ehto1 ) lause1
else if ( ehto2 ) lause2
else if ( ehto3 ) lause3
else lause4
if (a<5) if (a<0) a=3; else |
/*1*/ a=1; b=2; c=3; /*5*/ a=1; b=2; c=3; |
b=3; if (a>2) b=3; a=6; |
a=6; c=7; |
c=7; |
/*2*/ a=1; b=2; c=3; /*6*/ a=1; b=2; c=3;
if (a<5) b=3; a=6; c=7; if (a<–5) if (a<0) a=6;
**else** a=2; c=7;
/*3*/ a=1; b=2; c=3; /*7*/ a=1; b=2; c=3;
if (a<5) {b=3; a=6;} if (a<–5) b=3;
c=7; if (a<5) a=6;
**else** a=2; c=7;
/*4*/ a=1; b=2; c=3; if (a<5) /*8*/ a=1; b=2; c=3;
b=3; else { a=6; c=7; } if (a<0) a=3; else;
**if** (a>2) b=3; a=6;
c=7;
10.6 do–while –silmukka
Aikaisemmin olemme tutustuneet erääseen algoritmiin selvittää onko luku alkuluku vai ei. Koska algoritmi on valmis, voimme kirjoittaa vastaavan ohjelman (% –operaattori antaa jakojäännöksen, 10 % 3 == 1 ):
c-silm\alkuluku.c - testataan onko luku alkuluku
#include <stdio.h>
/****************************************************************************/
int pienin_jakaja(int luku)
/* Palautetaan luvun pienin jakaja (alkuluvulle 1).
Algoritmi:
Negatiivisesta luvusta otetaan itseisarvo.
Kokeillaan jokaista pienempää jakajaa 2,3,5,7 jne, kunnes
jako menee tasan tai jakaja on liian iso ollakseen luvun jakaja.
*/
{
int jakojaannos,jakaja=2,kasvatus=1;
if ( luku < 0 ) luku = –luku;
if ( luku == 2 ) return 1;
do {
jakojaannos = luku % jakaja;
if ( jakojaannos == 0 ) return jakaja;
jakaja += kasvatus;
kasvatus = 2;
} while ( jakaja < luku/2 );
return 1;
}
/****************************************************************************/
int main(void)
{
int luku,jakaja;
printf("Tutkin onko luku alkuluku. Anna luku >");
scanf("%d",&luku);
jakaja = pienin_jakaja(luku);
if ( jakaja == 1 )
printf("Luku on alkuluku.\n");
else
printf("Luku on jaollinen esimerkiksi luvulla %d.\n",jakaja);
return 0;
}
Käytimme tässä silmukkaa:
do
lause
while (ehto);
Koska esimerkin silmukassa oli useita suoritettavia lauseita, oli lauseet suljettu lausesuluilla. Jälleen voi olla hyvä tapa käyttää AINA lausesulkuja.
Huomautus! Silmukoiden kanssa on syytä olla tarkkana sekä 1. kierroksen että viimeisen kierroksen kanssa. Myös silmukan lopetusehdon on syytä muuttua silmukan suorituksen aikana.
Eräs tyypillinen esimerkki do–while silmukan käytöstä olisi seuraava:
c-silm\dowhile.cpp - lukujen lukeminen kunnes halutulla välillä
#include <stdio.h>
#include <iostream.h>
#include <string>
using namespace std;
int main(void)
{
int luku; string s;
do {
cout << "Anna luku väliltä [0-20]>";
getline(cin,s);
} while ( ( sscanf(s.c_str(),"%d",&luku) < 1 ) || ( luku<0 ) || ( 20<luku ) );
cout << "Annoit luvun " << luku << endl;
return 0;
}
10.7 while –silmukka
do–while –silmukka suoritetaan aina vähintään 1. kerran. Joskus on tarpeen silmukka, jonka runkoa ei suoriteta yhtään kertaa. Muutamme edellisen esimerkkimme käyttämään while –silmukkaa:
while ( ehto ) lause
Muutamme samalla algoritmia siten, että 2:lla jaolliset käsitellään erikoistapauksena. Näin pääsemme eroon "inhottavasta" kasvatus–muuttujasta.
c-silm\alkuluk2.c - alkulukutesti while-silmukalla
int pienin_jakaja(int luku)
{
int jakaja=3;
if ( luku < 0 ) luku = –luku;
if ( luku == 2 ) return 1;
if ( (luku % 2) == 0 ) return 2;
while ( jakaja < luku /2 ) {
if ( (luku % jakaja) == 0 ) return jakaja;
jakaja += 2;
}
return 1;
}
10.8 for –silmukka, tavallisin muoto
Eräs C–kielen hienoimmista rakenteista on for–silmukka. Usein C–hakkereiden tavoite on saada kirjoitettua koko ohjelma yhteen for–silmukkaan. Tätä ei tietenkään tarvitse tavoitella, mutta se osoittaa for–silmukan mahdollisuuksia.
Tyypillisesti for–silmukkaa käytetään silloin, kun silmukan kierrosten lukumäärä on ennalta tunnettu:
c-silm\valinsum.c - esimerkki for-silmukasta
/* Lasketaan yhteen luvut 1..ylaraja */
int valin_summa(int ylaraja)
{
int i,summa=0;
for (i=1; i<=ylaraja; i++)
summa += i;
return summa;
}
10.9 C–kielen lauseista
10.9.1 Sijoitusoperaattori =
Olemme tutustuneet jo C–kielen "normaaliin" sijoitusoperaattoriin =.
Sen ansiosta, että myös sijoitus palauttaa arvon, pystyimme tekemään mm seuraavia temppuja:
if ( (b=a) != 0 ) ... /* Suoritetaan jos a!=0 */
a = b = c = 0;
Sijoitus monelle muuttujalle yhtäaikaa onnistuu, koska sijoitus jäsentyy seuraavasti:
1. a = ( b = (c = 0) ); – sijoitus c=0 palauttaa arvon 0
2. a = ( b = 0 ); – sijoitus b=0 palauttaa arvon 0
3. a = 0;
10.9.2 Sijoitusoperaattori +=
valin_summa aliohjelmassa meillä esiintyi myös kaksi uutta sijoitusoperaattoria, jotka ovat lyhenteitä tavallisille sijoituksille:
> lyhenne | tavallinen sijoitus |
---|---|
> summa += i; > > i++ | summa = summa + i; |
+= sijoituksessa + voidaan korvata millä tahansa operaattoreista:
- – * / % << >> ^ & |
Esimerkiksi luvun kertominen ja jakaminen 10:llä voitaisiin suorittaa:
luku *= 10;
luku /= 10;
Siis muuttuja O= operandi voidaan ajatella korvattavaksi seuraavasti:
0. laita sulut operandin ympärille
muuttuja O= (operandi)
1. kirjoita muuttujan nimi kahteen kertaan
muuttuja muuttuja O= (operandi)
2. siirrä = –merkki muuttujien nimien väliin
muuttuja = muuttuja O (operandi)
int a=10,b=3,c=5;
a %= b;
b *= a+c;
b >>= 2;
10.9.3 Lisäysoperaattori ++
Erittäin tyypillisiä C–operaattoreita ovat ++ ja ––.
Nämä operaattorit lisäävät tai vähentävät operandin arvoa yhdellä. Operandin tyypin tulee olla numeerinen tai osoitin.
Operandeista on kaksi eri versiota: esilisäys ja jälkilisäys.
> lyhenne | vastaa lauseita |
---|---|
> a = i++; > > a = i--; > > a = ++i; > > a = --i; > | a = i; i = i+1; |
Vaikka C–hakkerit rakentavatkin mitä ihmeellisimpiä kokonaisuuksia ++ –operaattorin avulla, kannattaa operaattorin liikaa käyttöä välttää. Esimerkiksi lauseet joissa esiintyy samalla kertaa useampia lisäyksiä samalle muuttujalle, saattavat olla jopa määrittelemättömiä:
c-silm\plusplus.c - esimerkki ei-yksikäsitteisestä ++ operaattorin käytöstä
#include <stdio.h>
int main(void)
{
double i=1.0,a;
a = i++/i++;
printf("a = %5.2lf, i = %5.2lf\n",a,i);
return 0;
}
Ohjelma saattaa C–kääntäjän toteutuksesta riippuen tulostaa mitä tahansa seuraavista a:n ja i:in kombinaatioista:
a: 0.5 1.0 2.0
i: 2.0 3.0
Aluksi ++ –operaattoria kannattaa ehkä käyttää vain yksinäisenä lauseena lisäämään (tai vähentämään) muuttujan arvoa.
i++;
Lisäysoperaattoria EI PIDÄ käyttää seuraavissa tapauksissa:
Muuttuja johon lisäysoperaattori kohdistuu, esiintyy samassa lausekkeessa useammin kuin kerran.
Makrojen kutsuissa.
Myös funktioiden kutsut ovat vaarallisia, koska funktio saattaakin olla makro!
Kiellettyjä on siis esimerkiksi:
a = ++i + i*i;
b = MACRO(i––);
c = fun(++i,a,2*i);
10.10 for –silmukka, yleinen muoto
Yleensä ohjelmointikielissä for–silmukka on varattu juuri siihen tarkoitukseen, kuin ensimmäinen esimerkkimmekin; tasan tietyn kierrosmäärän tekemiseen.
C–kielen for–silmukka on kuitenkin yleisempi:
/* 1. 2. 5. 4. 7. 3. 6. */
for (alustus_lauseet; suoritus_ehto; kasvatus_lauseet) lause;
for–silmukka vastaa melkein while–silmukkaa (ero tulee continue–lauseen käyttäytymisessä):
alustus_lauseet; /* 1. */
while ( suoritus_ehto ) { /* 2. 5. */
lause; /* 3. 6. */
kasvatus_lauseet; /* 4. 7. */
}
Mikäli esimerkiksi alustuslauseita on useita, erotetaan ne toisistaan pilkulla:
c-silm\valinsum.c - useita alustuslauseita for-silmukassa
/* Lasketaan yhteen luvut 1..ylaraja */
int valin_summa_2(int ylaraja)
{
int i,summa;
for (summa=0, i=1; i<=ylaraja; i++)
summa += i;
return summa;
}
Erittäin C:mäinen tapa tehdä yhteenlasku olisi:
c-silm\valinsum.c - C:mäinen silmukka
int valin_summa_3(int i)
{
int s;
for (s=0; i; s += i––);
return s;
}
Tämä viimeinen esimerkki on juuri niitä C–hakkereiden suosikkeja, joita ehkä kannattaa osin vältellä.
10.11 break ja continue
10.11.1 break
Joskus kesken silmukan tulee vastaan tilanne, jossa silmukan suoritus haluttaisiin keskeyttää. Tällöin voidaan käyttää C–kielen break–lausetta, joka katkaisee sisimmän silmukan suorituksen.
c-silm\break.c - silmukan katkaisu keskeltä
#include <stdio.h>
int main(void)
{
int summa=0,luku;
printf("Anna lukuja. Summaan niitä kunnes annat 0 tai summa > 20\n");
while ( summa <= 20 ) {
printf("Anna luku>");
scanf("%d",&luku);
if ( luku == 0 ) break;
summa += luku;
}
printf("Lukujen summa on %d.\n",summa);
return 0;
}
Jos edellä olisi ollut alustus luku=1, olisi tietenkin while –silmukan ehto voitu kirjoittaa muodossa
while ( ( luku != 0 ) && ( summa <= 20 ) ) {
mutta aina ei voida break –lausetta korvata näin yksinkertaisesti. Esimerkiksi seuraava olisi jo hankalampi muuttaa:
c-silm\break2.cpp - esimerkki, jossa vaikea tulla toimen ilman breakiä
while ( summa <= 20 ) {
cout << "Anna luku>";
getline(cin,s);
if ( sscanf(s.c_str(),"%d",&luku) < 1 ) break;
if ( luku == 0 ) break;
summa += luku;
}
break –lauseen vika on lähinnä siinä, ettei siitä suoraan nähdä sisäkkäisten silmukoiden tapauksessa sitä, mihin saakka suoritus katkeaa. Epäselvissä tapauksissa silmukan katkaisu voidaan hoitaa goto –lauseella.
Silmukka voidaan katkaista tietenkin myös muuttamalla silmukan lopetusehtoon vaikuttavia muuttujia. Varsinkin for–lauseen tapauksessa silmukan indeksin arvon muuttaminen muualla kuin kasvatus–lauseessa on todella väkivaltaista ja rumaa, eikä tällaista pidä mennä tekemään.
10.11.2 continue
Vastaavasti saattaa tulla tilanteita, jolloin itse silmukan suoritusta ei haluta katkaista, mutta menossa oleva kierros halutaan lopettaa. Tällöin continue –lauseella voidaan suoritus siirtää suoraan silmukan loppuun ja näin lopettaa tämän kierroksen suoritus:
c-silm\continue.c - silmukan lopun ohittaminen
#include <stdio.h>
int main(void)
{
int alku= –5, loppu=5,i;
double inv_i;
printf("Tulostan lukujen %d – %d käänteisluvut\n",alku,loppu);
for (i = alku; i<=loppu; i++ ) {
if ( i == 0 ) continue;
inv_i = 1.0/i;
printf("%3d:n käänteisluku on %5.2lf.\n",i,inv_i);
}
return 0;
}
10.12 goto –lause
Lähes kaikissa ohjelmointikielissä on goto–lause, mutta usein strukturoidun ohjelmoinnin kannattajat ovat julistaneet sen pannaan. Julistus on aivan hyvä, sillä ainakin 90% tapauksista, joissa goto–lausetta tekisi mieli käyttää, voidaan korvata strukturoidummalla tavalla.
Seuraavissa tapauksissa goto–lause voidaan hyväksyä:
1. silmukan suorituksen katkaisu
2. aliohjelman loppuun hyppääminen (silloin kun returnia
ei lopetustoimenpiteiden vuoksi voida käyttää)
3. jos strukturoitu rakenne on selvästi monimutkaisempi
Mikäli yhteen aliohjelmaan tulee useita goto –lauseita, on rakenne selvästikin suunniteltu väärin, ja se on mietittävä uudelleen.
goto –lauseen syntaksiin kuuluu paikan nimi, johon hypätään. Tämä nimiö täytyy esitellä ohjelmassa laittamalla se tarvittavaan kohtaan:
c-silm\goto.c - hyppy ulos silmukasta
#include <stdio.h>
int main(void)
{
int summa=0,luku;
printf("Anna lukuja. Summaan niitä kunnes annat 0 tai summa > 20\n");
while (summa <= 20) {
printf("Anna luku>");
if ( !scanf("%d",&luku) ) goto luvut_loppu;
if ( luku == 0 ) goto luvut_loppu;
summa += luku;
}
luvut_loppu:
printf("Lukujen summa on %d.\n",summa);
return 0;
}
Siis break ja continue ovat "piilotettuja" goto–lauseita. Mikäli break tai continue pitäisi saada toimimaan 2 tasoa ulospäin, on pakko käyttää goto–lausetta.
Huom! Ensisijaisesti pyri välttämään goto–lauseita!
10.13 switch –valintalause
Jäsenrekisteriohjelmamme päävalinta olisi näppärintä toteuttaa switch –lauseella:
menut.05\kerho.cpp - päävalinta switch -lauseella
int paavalinta(cKerho &kerho)
{
char nappain;
while (1) {
paamenu(kerho); // Huom päämenun muuttunut parametrilista
nappain = odota_nappain("?012345",0,MERKKI_ISOKSI);
switch (nappain) {
case '?': avustus(nappain); break;
case '0': return 0;
case '1': lisaa_uusi_jasen(nappain); break;
case '2': etsi_jasenen_tiedot(nappain); break;
case '3': tulosteet(nappain); break;
case '4': tietojen_korjailu(nappain); break;
case '5': paivita_jasenmaksuja(nappain); break;
default : cout << "Näin ei voi käydä!" << endl; return 1;
}
}
}
switch –lauseessa case osien lopuksi break on yleensä välttämätön. break estää suorittamasta seuraavia rivejä.
Joskus harvoin breakin puuttumista voidaan käyttää hyväksi, mutta tällöin pitää olla todella tarkkana:
c-silm\switch.c - swicth, jossa break tahallaan jätetty pois
switch (operaatio) {
case 5: /* Operaatio 5 tekee saman kuin 4 */
case 4: x *= 2; break; /* 4 laskee x=2*x */
case 3: x += 2; /* 3 laskee x=x+4 */
case 2: x++; /* 2 laskee x=x+2 */
case 1: x++; break; /* 1 laskee x=x+1 */
default: x=0; break; /* Muut nollaavat x:än */
}
Lause default suoritetaan jos mikään case–osista ei ole täsmännyt (tai tietysti jos jokin break puuttuu). default–lauseen ei tarvitse olla viimeisenä, mutta tällöin vaaditaan taitavaa breakin käyttöä, siis paras pitää default viimeisenä!
Yleistä switch–lausetta ei voi korvata joukolla if–lauseita käyttämättä goto–lausetta. Mikäli kuitenkin jokaisen case rakenteen perässä on break, voidaan switch– korvata sisäkkäisillä if–else –rakenteilla.
10.13.1 || ei toimi switch –lauseessa!
On huomattava, että jos halutaan suorittaa jokin switch–lauseen osista kahdella eri arvolla, EI voida käyttää rakennetta:
switch (operaatio) { /* VÄÄRIN: */
case 4 || 5: x *= 2; break; /* 5 tai 4 laskee x=2*x */
case 3: x += 2; /* 3 laskee x=x+4 */
case 2: x++; /* 2 laskee x=x+2 */
default: x=0; break; /* Muut nollaavat x:än */
}
Kääntäjä ei tästä varoita, koska kaikki on aivan kieliopin mukaista. 4 || 5 on kahden loogisen lausekkeen OR eli 1 || 1 eli 1. Siis
case 4 || 5:
on sama kuin
case 1:
Jos esimerkistämme ei olisi poistettu lausetta case 1:, olisi kääntäjä varoittanut koska 1 olisi esiintynyt kahdesti.
10.14 Ikuinen silmukka
Usein silmukat lipsahtavat tahottomasti sellaisiksi, ettei niistä koskaan päästä ulos. Ikuisen silmukan huomaa heti esimerkiksi siitä, ettei silmukan rungossa ole yhtään lausetta joka muuttaa silmukan ehdon totuusarvoa.
Joskus kuitenkin C–kielessä tehdään tarkoituksella "ikuisia" –silmukoita:
for (;;) {
...
if (lopetus_ehto) break;
...
}
while (1) {
...
if (lopetus_ehto) break;
...
}
do {
...
if (lopetus_ehto) break;
...
} while (1);
Näissä kahdessa ensimmäisessä korostuu silmukan ikuisuus. Viimeinen ei ole hyvä vaihtoehto.
Tällaiset ikuiset silmukat ovat hyväksyttävissä silloin, kun silmukan lopetusehto on luonnollisesti keskellä silmukkaa. Usein kuitenkin lauseiden uudelleen järjestelyllä lopetusehto voidaan sijoittaa silmukan alkuun tai loppuun, jolloin tavallinen while– , do–while – tai for –silmukka kelpaa.
10.14.1 Yhteenveto silmukoista
11. Oliosuunnittelu
Mitä tässä luvussa käsitellään?
- vaatimukset ohjelman toteutukselle
- olioiden etsiminen
- CRC–kortit
- kuka huolehtii harrastuksista?
11.1 Olio on luokan esiintymä
Ennen kuin voimme aloittaa ohjelman toteutuksen, pitää meidän suunnitella mitä luokkia ohjelmassamme tarvitaan. Ohjelmassa olevat oliot ovat sitten näiden luokkien esiintymiä (ilmentymiä, instance).
11.2 Tavoitteet
Asetamme ensin ohjelman toteutukselle ulkoisen toiminnan lisäksi tiettyjä lisätavoitteita:
käyttöliittymä (tekstipohjainen vaiko ikkunoitu) on voitava muuttaa > kohtuullisella ohjelmoinnilla
ohjelma on voitava pienillä muutoksilla muuttaa muuksikin kuin > jäsenrekisteriksi (puhelinluettelo, levyrekisteri)
jäseneen voitava helposti lisätä kenttiä
11.3 Luokat
Tavoitteiden aikaansaamiseksi näyttäisi, että tarvitsemme ohjelmassa ainakin seuraavat kolme luokkaa:
käyttöliittymää ylläpitävä luokka (cNaytto)
rekisteriä ylläpitävä luokka (cKerho)
yksittäinen jäsen (cJasen)
Kullekin luokalle täytyy antaa selvät vastuualueet ja tieto siitä, miten kommunikoidaan muiden luokkien kanssa ja minkä luokan kanssa yleensäkään tarvitsee kommunikoida.
11.4 CRC–kortit
Timothy A. Budd [B] ehdottaa luokkasuunnittelun avuksi CRC-kortteja (Class Responsibility Collaborator, luokan vastuu ja avustajat). Kortti on 4"x6" (10 x15 cm) kooltaan ja se jaetaan 3 osaan: luokan nimi, vastuu (eli tehtävät) ja avustajat. Kortin koko on perusteltu sillä, että se on riittävän iso, jotta luokan vastuualueet voidaan siihen kirjoittaa ja toisaalta jos vastuualueet ei mahdu korttiin, on luokka liian iso ja se pitää jakaa useammaksi osaluokaksi. Huomattakoon että luokkaa suunniteltaessa ei juurikaan oteta kantaa siihen, kuka luokkaa käyttää!
Korttien takapuolelle voidaan kirjoittaa luokan yksityiset tiedot, eli ne tiedot joita joudutaan käyttämään jotta luokka voi hoitaa sovitun vastuunsa.
CRC-kortteja on sitten tarkoitus tutkia työryhmän jäsenten kesken antamalla kortti aina yhdelle ryhmän jäsenelle joka näin voi tarkistaa saako hän ko. luokasta tarvitsemansa tiedot (kääntämättä korttia). Jollei saa, luokkia on vielä helppo muuttaa kun ohjelmaa ei ole kirjoitettu.
11.4.1 Jäsen-luokka (cJasen)
Luokan nimi: cJasen | Avustajat: |
---|---|
Vastuualueet: | - merkkijonot |
(- ei tiedä kerhosta, eikä käyttöliittymästä) | |
- tietää jäsenen kentät (nimi, hetu, puhnro, jne.) | |
- osaa tarkistaa tietyn kentän oikeellisuuden (syntaksin) | |
- osaa muuttaa |Ankka Aku|..| - merkkijonon jäsenen tiedoiksi | |
- osaa antaa merkkijonona i:n kentän tiedot | |
- osaa laittaa merkkijonon i:neksi kentäksi |
11.4.2 Kerho-luokka (cKerho)
Luokan nimi: cKerho | Avustajat: |
---|---|
Vastuualueet: | - cJasen |
- pitää yllä varsinaista rekisteriä, eli osaa lisätä ja poistaa jäsenen | |
- lukee ja kirjoittaa kerhon tiedostoon | |
- osaa etsiä ja lajitella |
11.4.3 Käyttöliittymä-luokka (cNaytto)
Luokan nimi: cNaytto | Avustajat: |
---|---|
Vastuualueet: | - cJasen |
- hoitaa kaiken näyttöön tulevan tekstin | - cKerho |
- hoitaa kaiken tiedon pyytämisen käyttäjältä | |
> (- ei tiedä kerhon eikä jäsenen yksityiskohtia) |
11.4.4 Luokkajaon tarkastelua
Miksi näyttö ei saa tietää jäsenen yksityiskohtia? Jos näyttö tietäisi jäsenen yksityiskohdat, pitäisi myös cNaytto-luokkaa muuttaa kun muutetaan jotakin jäsenen yksityiskohtaa, eli esimerkiksi lisätään fax–numero. Käytännössä näytön pitää kuitenkin saada tämä muutos tietoonsa. Miten?
Näyttö voi esimerkiksi aina kysyä jäseneltä montako kenttää tällä on. Samoin näyttö voi pyytää jäsentä antamaan 1. kentän (nimi) merkkijonona, 2. kentän merkkijonona jne. Näin voidaan tulostaa jäsenen tiedot näyttöön tietämättä tarkkaan mitä kenttiä jäsenessä on.
Entä tietojen lukeminen? Näyttö voi myös kysyä jäseneltä sen tekstin, joka täytyy käyttäjälle kirjoittaa, kun pyydetään käyttäjää antamaan 3. kentän tiedot ("Katuosoite"). Tämän jälkeen näyttö voi tulostaa tämän tekstin näyttöön ja jäädä odottamaan käyttäjältä merkkijonoa. Kun käyttäjä antaa merkkijonon, pyydetään jäsentä muuttamaan annettu merkkijono 3:nen kentän tiedoiksi.
Aikanaan saattaa tulla vastaan tilanne, jossa on edullista jakaa näyttöluokka useampaan alaluokkaan, eli esim. yleinen näyttö (cNaytto), kerhon näyttö (cKerhon_naytto) ja jäsenen näyttö (cJasenen_naytto).
Lisäksi yksittäisten kenttien käsittelyä voi auttaa kenttä-luokan käyttö. Peruskenttäluokasta voidaan periä erikoistuneita kenttäluokkia. Tiedon säilyttämisen apuna kerholuokalla voi olla tietorakenne-luokka. Etsiminen voidaan ehkä jättää etsimis- ja selailu -luokan tehtäväksi. Alkuun päästään kuitenkin suunnitelman mukaisella kolmella luokalla.
11.5 Harrastukset mukaan
Entä sitten kun haluamme lisätä kerholaisille harrastuksia. Ainakin tarvitaan cHarrastus–luokka lisää. Kuka huolehtii harrastuksista?
Jos valinta tehdään valitun tiedostomuodon mukaan, niin mahdollisuuksia on:
11.5.1 "Oliomalli"
Oletetaan aluksi että Jäsen huolehtii harrastuksistaan. Tällöin Kerho ei tarvitse mitään muutoksia ja Näyttökin vain sen verran, että tietää kysellä ja tulostaa Jäsenelle tulleita lisäominaisuuksia. Jäsenen ominaisuuksiin lisätään harrastuksien ylläpito vastaavasti kuin Kerho ylläpiti jäsenistöä.
11.5.2 Relaatiomalli
Jos valitaan relaatiomallin mukainen tiedostorakenne, niin ehkä myös luokat kannattaa suunnitella vastaavasti. Tässä mallissa Jäsen ei tiedä mitään harrastuksistaan ja cJasen pysyykin muuttumattomana. cHarrastus tekee täsmälleen samat asiat kuin cJasen, paitsi tietenkin omille ominaisuuksilleen.
Jos mahdollisimman paljon vastuuta harrastusten ylläpidosta annetaan Kerholle, huomataan että toistetaan samat ominaisuudet, joita kirjoitettiin jo jäsenistöä varten. Tämän vuoksi Kerhon roolia kannattaakin hieman selventää siten, että tietorakenteiden ylläpitoa varten tehdään omat luokat ja Kerho komentaa näitä luokkia.
11.5.3 Harrastus-luokka (cHarrastus)
Vertaa toimintaa cJasen–luokan toimintoihin.
11.5.4 Kerho-luokka (cKerho)
Luokan nimi: cKerho | Avustajat: |
---|---|
Vastuualueet: | - cJasenet |
- huolehtii cJasenet ja cHarrastukset –luokkien välisestä yhteistyöstä ja välittää näitä tietoja pyydettäessä | - cHarrastukset |
- lukee ja kirjoittaa kerhon tiedostoon pyytämällä apua avustajiltaan |
11.5.5 Jäsenet-luokka (cJasenet)
Luokan nimi: cJasenet | Avustajat: |
---|---|
Vastuualueet: | - cJasen |
- pitää yllä varsinaista jäsenrekisteriä, eli osaa lisätä ja poistaa jäsenen | |
- lukee ja kirjoittaa jäsenistön tiedostoon | |
- osaa etsiä ja lajitella |
11.5.6 Harrastukset-luokka (cHarrastukset)
Luokan nimi: cHarrastukset | Avustajat: |
---|---|
Vastuualueet: | - cHarrastus |
- pitää yllä varsinaista harrasterekisteriä, eli osaa lisätä ja poistaa harrastuksen | |
- lukee ja kirjoittaa harrastukset tiedostoon | |
- osaa etsiä ja lajitella |
Koska Harrastukset ja Jäsenet ovat täsmälleen samanlaisia lukuun ottamatta sitä, mitä alkioita ne käsittelevät, voidaan käytännössä C++:lla ensin tehdä malliluokka, josta generoidaan kumpikin hieman eri versio.
Näin päästään siihen tilanteeseen, jossa myös rinnakkaisten rakenteiden lisääminen Harrastuksille vaatii vain hyvin vähän uutta ohjelmointia.
Huomattakoon, että sekä cJasenet että cHarrastukset ovat pelkkiä abstrakteja tietorakenneluokkia, niiden sisäinen talletustapa voi olla mikä vaan (taulukko, lista, puu) ulkoisen rajapinnan ollessa silti edellisen suunnitelman kaltainen.
12. Jäsenrekisterin runko
Mitä tässä luvussa käsitellään?
- tietorakenteen valinta
12.1 Runko ilman kommentteja
Emme vielä täysin osaa tehdä edes runkoa jäsenrekisteriohjelmaamme, mutta esitämme tästä huolimatta jonkinlaisen toimivan rungon ohjelmalistauksen. Koodista on poistettu kommentit, jotta listaus mahtuisi tässä vaiheessa lyhyempään tilaan. Koodissa tulee lukuisia vielä käsittelemättömiä osia, joita käsittelemme tarkemmin myöhemmin. Samoin etsimme myöhemmin sopivan tavan kommentoida aliohjelmia.
Rungon tarkoituksena on tarjota näkyväksi ne toiminnot, jotka ohjelman suunnitelmassa päätettiin tehdä. Samoin tavoitteena on testata valitun tietorakenteen toimivuus. Jatkossa toimintoja lisätään tähän runkoon.
Tämä runko ei tietenkään ole syntynyt kerralla, vaan alkuperäinen runko on ollut esimerkiksi ilman tietorakenteita (vrt. menut.05\kerho.cpp)
12.2 Valittava tietorakenne
Minkälaisen tietotyypin voisimme valita? Vaihtoehtoja tulee ehkä lähes yhtä paljon kuin ohjelmoijiakin on. Voimme kuitenkin vertailla eräiden rakenteiden etuja ja haittoja. Jos mahdollista, tietorakenteiden tulisi olla yhtenäinen tehdyn oliosuunnitelman kanssa. Seuraavista ehdotuksista mikään ei ole ristiriidassa edellisessä luvussa tehdyn oliosuunnitelman kanssa. Vasta kun valitaan harrastusten talletustapaa, joudumme valinnan eteen.
Lähdemme siitä ajatuksesta, että koko käsiteltävä aineisto on kerralla keskusmuistissa. Voisimme tietenkin operoida myös suoraan levylle, mutta oppimisen tässä vaiheessa voi seuraava ratkaisu olla helpompi:
- lue tiedosto muistiin
- käsittele aineistoa muistissa
- talleta aineisto takaisin levylle
12.2.1 Taulukko
Taulukko on kiinteä tietorakenne, jota luotaessa täytyy jo tietää monelleko ihmiselle varaamme tilaa. Tässä tulee äkkiä varattua tilaa joko liikaa, jolloin tila ei riitä muille toiminnoille, tai liian vähän, jolloin kaikki henkilöt eivät mahdu rekisteriin. Esimerkissämme olemme varanneet n. 300 tavua/henkilö. Tilan varaaminen sadalle henkilölle veisi jo 30000 tavua. Usein sata ei edes riitä!
12.2.2 Linkitetty lista
Linkitetty lista on rakenne, jossa meillä on tieto vain listan 1. alkiosta. Tämän jälkeen kukin alkio tietää itseään seuraavan alkion, kunnes listan viimeinen alkio ei enää osoita minnekään.
Listan hyvänä puolena on se, ettei etukäteen tarvita mitään tietoa alkioiden lukumäärästä. Alkioita voidaan lisätä listaan joko alkuun, keskelle tai loppuun niin kauan kuin muistia riittää.
Oppimisen tässä vaiheessa kuitenkin linkitetyn listan käsittelyalgoritmit (lisäys, poisto, lajittelu jne..) saattavat olla liian työläitä.
Mikäli rakennamme ohjelman huolella, ei tietorakenteen vaihtaminen jälkeen päinkään ole mahdoton tehtävä. Tätä auttaa vielä aikaisemmin tekemämme valinta käyttää abstraktia rajapintaa (lisää, poista, etsi) tietorakenneluokan (cKerho tai cJasenet) ja käyttöliittymän (cNaytto) välillä.
12.2.3 Sekarakenne
Valitsemme tähän toteutukseen tietorakenteeksi sekarakenteen:
Siis perusrakenteena meillä on cKerho–tyyppi, joka pitää sisällään kerhon perustiedot. Kerhosta on osoitin taulukkoon, jossa on osoittimet varsinaisiin henkilöiden tietoihin (cJasen).
Henkilöiden tiedoille varattua tilaa ei ole olemassa ennen kuin sitä tarvitaan. Siis varataan kullekin kerhoon lisättävälle henkilölle hänen tiedoilleen tarvittava uusi n. 300 tavun "möykky" lisäyksen yhteydessä.
Osoitintaulukkoon sijoitetaan sitten vastaavaan paikkaan sen muistiosoitteen arvo, josta henkilölle tarvittava tila saatiin varattua.
Tässäkin rakenteessa on se huono puoli, että osoitintaulukon koko pitää päättää ennen kuin sinne voidaan sijoittaa osoitteita. Yksi osoite vie kuitenkin enimmilläänkin tilaa 4 tavua, joten kiinteää tilan varausta esim. 1000 henkilön taulukossa tulee vain 4000 tavua.
Hyvinä puolina rakenteessa on sen suhteellisen helppo käsittely sekä lisäyksen, poiston että lajittelun tapauksessa.
12.2.4 Ohjelman runko
Ohjelmalistaus löytyy listausmonisteesta tiedostosta
runko.1\kerho.cpp
12.3 Harrastukset mukaan
Jos halutaan tallettaa myös kullekin jäsenelle vaihteleva määrä harrastuksia, on jälleen mahdollisuuksia useita. Tietorakennetta valittaessa voidaan käyttää samaa kriteeriä kuin tiedostoakin valittaessa. Välttämätöntä tämä ei kuitenkaan ole, vaan voidaan sisäisesti tietysti käyttää myös erilaista rakennetta kuin ulkoisesti.
12.3.1 Tiedot jäsenen yhteyteen
Jos tiedoston muoto on sellainen että harrastukset on lueteltu jäsenen tietojen yhteydessä, kannattaa tietorakennekin valita vastaavasti.
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
- kalastus | 1955 | 20
- laiskottelu | 1950 | 20
- työn pakoilu | 1952 | 40
Susi Sepe |020347-123T| |12555 |Takametsä | | |
- possujen jahtaaminen | 1954 | 20
- kelmien kerho | 1962 | 2
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
- susiansojen rakentaminen | 1956 | 15
Nyt tietorakenne voisi olla tilanteesta riippuen mikä tahansa edellä esitetyistä siten, että kerhosta alkava rakenne toistuu jäsenen kohdalla.
Esimerkiksi linkitetty lista:
12.3.2 Relaatiomalli
Jos tiedot on talletettu relaatiomallin mukaan, voi olla kannattavaa tehdä myös sisäinen tietorakenne vastaavaksi. Vaikka jatkossa emme toteutakaan kerhoon vielä harrastuksia, teemme tietorakenteen ja oliot sellaisiksi, että harrastusten käsittely jälkeenpäin olisi mahdollisimman helppoa.
Toteutettavaa ohjelmaa ajatellen tästä valinnasta seuraa yksi byrokratiaporras (cKerho <-> cJasenet) lisää, joka aluksi saattaa tuntua turhalta. Valinta maksaa itseään takaisin vasta ongelman monimutkaistuessa. Tähän samaan monimutkaistumisongelmaan tulemme törmäämään jatkossakin. Yksinkertaisin mahdollisuus, jolla vaadittu toimenpide juuri ja juuri voidaan suorittaa, johtaa usein jatkoa ajatellen umpikujaan.
12.3.3 Ohjelman runko harrastusten kanssa
Ohjelmalistaus, jossa harrastuksetkin ovat mukana, löytyy tiedostosta
runko.1\kerhohar.cpp
13. C–kielen taulukoista
Mitä tässä luvussa käsitellään?
- C–kielen taulukot
- taulukoiden ja osoittimien yhteys
- C–merkkijonot (char *)
- C–merkkijonojen ja C++ merkkijonojen yhteiskäyttö
- päällekkäiset muistialueet (union) ja luetteloidut tyypit (enum)
- moniulotteiset taulukot
Syntaksi:
Taulukon esittely: alkiontyyppi taulukonnimi[koko_alkioina];
Alkioon viittaaminen: taulukonnimi[alkion_indeksi]
Muista 1. indeksi = 0
viimeinen = koko_alkiona-1
Silmukoissa for (i=0; i<koko; i++) ...
C-mjonon esittely char jono[tarvittava_merkkimaara+1];
2-ul.taulukon es: alkiontyyppi taulukonnimi[rivimaara][sarakemaara];
13.1 Yksiulotteiset taulukot
C–kielessä taulukoita ei oikeastaan ole, tai ainakin ne ovat '2. luokan kansalaisia'. Lausuma tarkoittaa sitä, että taulukoista on käytettävissä vain 1. alkion osoite ja esimerkiksi taulukon sisällön sijoittaminen toiseen taulukkoon ei onnistu sijoitusoperaattorilla.
13.1.1 Taulukon määrittely
Taulukko määritellään kertomalla taulukon alkioiden tyyppi ja alkioiden lukumäärä:
int k_pituudet[12];
Tällöin taulukon 1. alkion indeksi on 0 ja 12. alkion indeksi on 11.
Määrittelyllä muuttujasta k_pituudet
tulee osoitin kokonaislukuun; taulukon alkuun.
13.1.2 Taulukon alkioihin viittaaminen indeksillä
Taulukon alkioon voidaan viitata alkion indeksin avulla
k_pituudet[0]=31; /* tammikuu */
k_pituudet[1]=28; /* helmikuu */
Vaarallista on, että kukaan ei kiellä viittaamasta
k_pituudet[24]=31;
vaikka moista paikkaa taulukkoon ei alunperin ole edes varattu.
Indeksiviittaus k_pituudet[2]
tarkoittaa itse asiassa viittausta *(k_pituudet+2)
k_pituudet+2 ──┐
│
k_pituudet │
│ v
│ 0 1 2 3 4 5 6 7 8 9 10 11
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│31│28│31│30│31│30│31│31│30│31│30│31│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
eli 2 paikkaa eteenpäin taulukon alusta lukien.
Taulukko voitaisiin nollata seuraavalla silmukalla:
int i;
...
for (i=0; i<12; i++) k_pituudet[i]=0;
Huomautus! Taulukoiden käsittelyssä on muistettava, että indeksi liikkuu välillä [0,YLÄRAJA[.
13.1.3 Viittaaminen osoittimella
Taulukoihin voidaan viitata myös osoittimen avulla:
c-taul\kuut.c - esimerkki osoittimista taulukkoon
int *tammikuu,*helmikuu,*joulukuu;
...
tammikuu = k_pituudet;
helmikuu = tammikuu+1;
joulukuu = k_pituudet+11;
*tammikuu = 31;
*helmikuu = 28;
*joulukuu = 31;
helmikuu ─────┐
tammikuu ──┐ │
│ │ joulukuu ───┐
k_pituudet │ │ │
│ v v v
│ 0 1 2 3 4 5 6 7 8 9 10 11
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└──────>│31│28│ 0│ 0│ 0│ 0│ 0│ 0│ 0│ 0│ 0│31│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
Taulukon nollaaminen voitaisiin hoitaa myös:
int i,*p;
for (i=0, p=k_pituudet; i<12; i++, p++) *p=0;
/* tai */
for (i=0, p=k_pituudet; i<12; i++) *p++=0;
/* tai */
for (p=k_pituudet; p<k_pituudet+12; p++) *p=0;
Viimeistä esimerkkiä lukuun ottamatta näissä ei kuitenkaan ole järkeä, koska myös indeksi i tarvitaan joka tapauksessa. Viimeisenkin esimerkin mukaista käyttöä kannattaa harkita: se mitä tehdään, piiloutuu osoittimien taakse!
13.1.4 Taulukon alustaminen
Taulukko voidaan alustaa (vain) esittelyn yhteydessä:
/* 1. 2. 3. 4. 5. 6. 7. 8. 9.10.11.12 */
int k_pituudet[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
Tehtävä 13.119 Taulukon alkioiden summa
Kirjoita funktio- aliohjelma taulukon_summa
, joka palauttaa taulukon alkioiden summan. Kirjoita pääohjelma, jossa aliohjelmaa kutsutaan valmiiksi alustetulla taulukolla k_pituudet
ja tulostetaan vuoden päivien lukumäärä.
13.2 Merkkijonot
Merkkijonot ovat eräs ohjelmoinnin tärkeimmistä tietorakanteista. Valitettavasti tämä on lähes poikkeuksetta unohtunut ohjelmointikielten tekijöiltä. Heille riittää että kielellä VOI tehdä merkkijonotyypin. Tavallista käyttäjää kiinnostaa tietysti onko se tehty ja onko se hyvä. Usein vastaus on EI. Näin myös C–kielen kohdalla! C++:han on jo välttävä merkkijonoluokka. Itse asiassa tämä oli merkkittävin tekijä miksi tätä kurssia edeltävän alkeisohjelmointikurssin kieleksi valittiin C++ puhtaan C–kielen sijaan.
13.2.1 Merkkityyppi
Yksittäinen merkki on C–kielessä tyyppiä char:
char rek_1_merkki;
rek_1_merkki = 'X';
Huomattakoon, että merkkityypin vakio kirjoitetaan yksinkertaisiin lainausmerkkeihin. Vakio on tosin kokonaislukutyyppinen, eli
sizeof('a') == sizeof(int) ( sizeof(char) == 1 )
Merkkityyppi on käytännössä (yleensä) 8–bittinen kokonaisluku. Toteutuksesta riippuen joko etumerkillinen tai etumerkitön. Siis merkkimuuttujiin voidaan vallan hyvin sijoittaa myös lukuarvoja:
char m;
m = 65;
if ( m == 'A' ) ...
Lukuarvo tarkoittaa merkin (ASCII–) koodia.
Merkkien etumerkillisyys tulee ilmi esimerkiksi Turbo–C:ssä seuraavasti:
c-taul\merkit.c - esimerkki merkkien vertailusta
char m1,m2,m3;
m1='a';
m2='z';
m3='ä';
if ( m1 < m2 ) printf("%c < %c \n",m1,m2);
printf("%c = %d, %c = %d \n",m1,m1,m2,m2);
if ( m1 < m3 ) printf("%c < %c \n",m1,m3);
printf("%c = %d, %c = %d \n",m1,m1,m3,m3);
Jälkimmäistä if tulostusta ei tapahdu. ä:n koodi on 132, joka olisi suurempi kuin a:n ASCII–koodi eli 97, mutta koska Turbo-C:ssä merkit ovat etumerkillisiä, on ä:n koodi –124!
HUOM! Tästä syystä merkkityyppisten muuttujien käyttö taulukoiden indeksinä on vaarallista!
13.2.2 C–kielen merkkijonot
Vaikka C++:sta löytyykin hyvä merkkijonoluokka <string>, niin joudumme silti tutustumaan myös puhtaan C–kielen merkkijonoihin. Miksikö? Koska esimerkiksi sijoituksessa
cpp_jono = "Kissa";
sijoitetaan tosiasiassa C–merkkijono C++:n merkkijonoon. Lisäksi valtava määrä valmiista aliohjelmista haluaa parametrikseen nimenomaan C–merkkijonon tai palauttaa C–merkkijonon. Itse asiassa C++:n merkkijonojen käyttö on vielä tänä päivänä (2002) ei ole yleistä edes kaikissa C++:n omissakaan funktiokirjastoissa.
Tämä on sääli, sillä suurin osa C:n "huonoudesta" johtuu nimenomaan aloittelijoiden (ja kokeneidenkin) käsissä VAARALLISISTA merkkijonoista. Valtava määrä UNIXeistakin löytyvistä tietoturva–aukoista perustuu väärin käytettyihin merkkijonoihin! Näytimmekin jo aikaisemmin valmiiden olioluokkien kohdalla muutamia asioita, joita EI voi tehdä C–merkkijonoilla, tai jotka näyttävät hyvältä, mutta toimivat väärin. Nyt tutustumme vähän tarkemmin ongelmien syihin ja siihen miten niitä voidaan välttää.
Vinkki: Varaa tila myös loppumerkille!
C–kielen merkkijonot eivät ole mitään muuta kuin taulukoita kirjaimista. Merkkijonoista on kuitenkin sovittu, että merkkijonon lopuksi taulukossa on merkki jonka koodi on 0 (NUL, '\0'). Siis merkkijono täytyy muistaa varata yhtä alkiota isommaksi, kuin aiottu merkkien maksimimäärä.
Huomattakoon, ettei ole olemassa valmista NUL–vakiota, vaan pitää käyttää joko muotoa 0 tai '\0'.
string.h–kirjastosta löytyy iso kasa merkkijonojen käsittelyyn tarkoitettuja aliohjelmia. Esimerkiksi sijoitus
elain = "Kissa"
ei onnistu (tai onnistuu, muttei kopioi merkkejä "Kissa" taulukkoon elain), vaan on käytettävä jotakin funktioita (esimerkiksi huippuvaarallista strcpy funktiota):
char elain[12];
strcpy(elain,"Kissa"); /* VAARALLINEN!!! Tässä toimii!!! */
...
Seuraavassa koodit on esitetty heksana ja ? tarkoittaa ettei muistipaikan arvoa tunneta.
elain
│ 0 1 2 3 4 5 6 7 8 9 10 11
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│00│ ?│ ?│ ?│ ?│ ?│ ?│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a NUL ? ? ? ? ? ?
Merkkijonon pituus voitaisiin laskea seuraavalla aliohjelmalla:
c-taul\merkit.c - esimerkki merkkijonon viemisestä parametrina
int pituus(const char jono[])
{
int i;
for (i=0; jono[i]; i++);
return i;
}
/* Tai: */
int pituus(const char jono[]) /* const char [] ei char [] */
{ /* koska jonoa ei muuteta */
char *p;
for (p=jono; *p; p++);
return p- jono;
}
Tosin string.h:sta löytyy funktio strlen, joka tekee täsmälleen saman asian.
Funktio pituus voitaisiin aivan yhtä hyvin esitellä:
int pituus(const char *jono)
13.2.3 Kirjaimen ja merkkijonon ero
Ohjelmasta
{
char c,jono[2];
c = 'A';
strcpy(jono,"A");
...
}
seuraisi seuraavat muistin sisällöt (heksana):
┌──┐ ┌──┬──┐
c: │41│ jono────>│41│00│
└──┘ └──┴──┘
Siis muuttuja c ja taulukon alkio jono[0] käyttäytyvät samalla tavoin ja ovat kumpikin muistipaikkoja jotka sisältävät yhden merkin. Vastaavasti jono on osoitin merkkijonon alkuun (merkkiin jono[0]). Tyhjä merkkijonokin veisi vähintään yhden paikan! Miksi?
13.2.4 Merkkijono, osoitin merkkiin ja vakiojono
Seuraavat esittelyt saattavat näyttää samanlaisilta
char *p = "Kissa", jono[10] = "Kissa";
mutta näiden ero on siinä, että *p on osoitin johonkin merkkiin ja jono 10–paikkaisen merkkijonon 1. merkkiin. Osoitin p on alustettu osoittamaan vakiomerkkijonoa jossa sisältönä on "Kissa" ja 10–paikkainen merkkijono on sisällöltään alustettu arvoksi "Kissa". Osoittimen p arvoa voidaan muuttaa, osoittimen jono arvoa sen sijaan ei voida muuttaa! Osoittimen jono osoittamaa sisältöä saa ja voi muuttaa, osoittimen p osoittamaa sisältöä EI saa (eikä aina edes voi) muuttaa.
Esimerkiksi seuraava selvältä näyttävä ohjelma saattaisi tuottaa yllätyksen:
Ohjelma saattaa (kääntäjän optioista, kääntäjästä ja laiteympäristöstä riippuen) tulostaa:
p: Koira jono: ra
Miksikö? Koska "IsoKissa" on vakiomerkkijono, jonka alkuun osoitin p alustetaan. Tänne osoitteeseen kopioidaan teksti "Koira". Koska kääntäjän mielestä "IsoKissa" on vakiojono ja lauseessa strcpy(jono,"Kissa") tarvitaan vakiojonoa "Kissa", päättää kääntäjä antaa käännösaikana strcpy–funktiolle osoitteen "IsoKissa" –merkkijonon sisältä!
osoitin "Koira"─────────────────────────┐
osoitin "Kissa"─────────────┐ │
osoitin "IsoKissa"────┐ │ │
v v v
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
alustus *p="IsoKissa" p────>│I│s│o│K│i│s│s│a│0│K│o│i│r│a│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
osoitin "Kissa"─────────────┐
v
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
kopiointi strcpy(p,"Koira") p────>│K│o│i│r│a│0│s│a│0│K│o│i│r│a│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌─────────────────────────┐
│ v
│ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
kopiointi strcpy(jono,"Kissa") p────>│K│o│i│r│a│0│s│a│0│K│o│i│r│a│0│
┌───────────────┘ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
v
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│r│a│0│?│?│?│?│?│?│?│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
Edellisessä esimerkissä sijoitus
p = jono; /* OIKEIN */
on sallittu, muttei sijoitus
jono = p; /* VÄÄRIN */
Myös sijoitus
p = "Koira";
olisi sallittu, mutta ei suinkaan sijoita osoitteeseen p
jonoa "Koira", vaan sijoittaa osoittimen p
osoittamaan vakiomerkkijonoon "Koira".
Eroa käsitellään vielä lisää sizeof
–operaattorin kohdalla.
13.3 string.h
C–merkkijonojen käsittelyyn on standardin mukaan seuraavat valmiit aliohjelmat string.h:ssa
memchr memcmp memcpy memmove memset
strcat strchr strcmp strcpy strcspn
strerror strlen strncat strncmp strncpy
strpbrk strrchr strspn strstr strtok
strcoll strxfrm
Lisäksi esim. Turbo-C:ssä on:
memccpy memicmp movedata movmem setmem
stpcpy strcmpi strdup _strerror stricmp
strlwr strncmpi strnicmp strnset strrev
strset strupr
Kirjaston nimien yksi idea on siinä, että str-alkuiset nimet tarkoittavat string–funktioita, eli taulukon käsittely lopetetaan NUL-alkioon. mem-alkuiset toimivat yleensä vastaavasti, mutta niissä välitetään parametrinä taulukon pituus ja NUL-tavusta ei välitetä. strn-alkuiset funktiot toimivat kuten str–funktiot, mutta NUL–tavun lisäksi taulukon käsittely lopetetaan, kun annettu maksimimäärä merkkejä on käsitelty.
13.3.1 strcpy ja strncpy
Esimerkiksi strcpy:n käyttö on erittäin vaarallista, mikäli merkkijono jonne kopioidaan jää pienemmäksi kuin mitä kopioidaan. Tällöin kopioidaan seuraavien muistialueiden päälle. Esimerkiksi saattaisi olla:
c-taul\strcpy.c - esimerkki tilan ylityksestä
char jono1[6],jono2[6];
...
strcpy(jono2,"Koira");
strcpy(jono1,"Kissa on!");
...
printf("%s %s\n",jono1,jono2); /* - > Kissa on! on! */
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│20│6F│6E│21│00│61│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a o n ! NUL a NUL
Edellä jono2:n pilaaminen voitaisiin estää kutsulla strncpy:
char jono1[6],jono2[6];
...
strncpy(jono2,"Koira",6);
strncpy(jono1,"Kissa on!",6);
...
printf("%s %s\n",jono1,jono2); /* - > Kissa Koira Koira */
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│20│6B│6F│69│72│61│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a K o i r a NUL
Vikana on kuitenkin edelleen se, että nyt strncpy
ei laita NUL merkkiä jonon loppuun, mikäli käsittely lopetetaan maksimipituuden takia!
Usein tämä varmistetaan laittamalla NUL-merkki varmuuden vuoksi kopioinnin jälkeen:
...
strncpy(jono1,"Kissa on!",6);
jono1[6- 1] = 0;
...
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│20│6B│6F│69│72│61│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a NUL K o i r a NUL
Tehtävä 13.120 kopioi_jono
Kirjoita aliohjelma
int kopioi_jono(char *tulos, int max_koko, const char *jono)
joka kopioi merkkijonon jono paikkaan tulos, muttei ylitä tulos- jonon maksimipituutta max_pit. tulos jono loppuu aina NUL-merkkiin. Funktio palauttaa 0 mikäli kopiointi onnistuu kokonaan ja muuten jonkin muun arvon.
13.3.2 memmove
Mikäli kopioinnin kohde ja lähde ovat päällekkäin, saattaa kopiointi tuottaa yllätyksiä. Jos jonot olisivat esimerkiksi seuraavasti
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│20│6B│6F│69│72│61│00│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a NUL K o i r a NUL
ja tavoitteena olisi siirtää Kissa merkkijonoa 3 pykälää vasemmalle sekä muuttaa kana muotoon kkana, voitaisiin kuvitella tämän tapahtuvan
strcpy(jono1,jono1+3);
strcpy(jono2+1,jono2);
Yleensä käyttöjärjestelmästä riippuen ainakin toinen edellisistä sekoaa. Ainoa turvallinen tapa tehdä em. temppuja on memmove, jonka taataan toimivan vaikka lähde ja kohde olisivat osittain päällekkäinkin.
memmove(jono1,jono1+3,strlen(jono1+3)+1);
memmove(jono2+1,jono2,strlen(jono2)+1);
13.3.3 strcat ja strncat
Merkkijonojen liittämiseen on funktiot strcat ja strncat. Esimerkiksi:
char jono1[10],jono2[6];
...
strcpy(jono1,"Kissa");
strcpy(jono2," on!");
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│00│??│??│??│??│20│6F│6E│21│00│??│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a NUL o n ! NUL
strcat(jono1,jono2);
┌─────jono2
jono1 v
│ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───>│4B│69│73│73│61│20│6F│6E│21│00│20│6F│6E│21│00│??│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
K i s s a o n ! NUL o n ! NUL
Funktiossa strcat on sama vaara kuin strcpyssäkin. Vaaraa voidaan osittain välttää strncat kutsulla, muistaen kuitenkin, että pituutena annetaan lisättävien merkkien maksimimäärä (NUL mukaan lukien, joka strncatin tapauksessa laitetaan), EI tuloksen maksimikoko. Siis turvallinen muoto olisi:
strncat(jono1,jono2,10- (strlen(jono1)+1));
Tehtävä 13.121 liita_jono
Kirjoita aliohjelma
int liita_jono(char *tulos, int max_koko, const char *jono)
joka liittää merkkijonon jono
jonon tulos perään, muttei ylitä tulos
- jonon maksimipituutta max_pit
. tulos
- jono loppuu aina NUL-merkkiin. Funktio palauttaa 0 mikäli liittäminen onnistuu täysin ja muuten jonkin muun arvon.
13.3.4 Tulos myös funktion nimessä
string.h –kirjaston funktioista useat ovat sellaisia, että ne palauttavat myös nimessään osoittimen tulosmerkkijonoon. Miksikö? Siksi, että tällöin merkkijonokutsuja voidaan ketjuttaa funktiomaisesti:
char jono[30],st[40];
...
strcat(strcpy(st,strcat(strcpy(jono,"Kissa")," istuu"))," puussa!");
Esimerkissä jonot saisi arvokseen:
jono: "Kissa istuu"
st : "Kissa istuu puussa!"
Tehtävä 13.122 Ketjutus
Miksi jonot jono
ja st
saisivat edellä mainitut arvot?
Tehtävä 13.123 string.h
Ota selvää mitä mikäkin string.h
:n aliohjelmista tekee.
13.4 Merkkijonojen lukeminen
13.4.1 Valmiit funktiot VAARALLISIA
ÄLÄ LUOTA valmiisiin merkkijonofunktioihin
Merkkijonojen lukeminen on eräs C–kielen vaarallisimpia operaatioita. Seuraavan malliesimerkin takia merkkijonot on aina luettava käyttäen jotakin itse tehtyä turvallista funktiota. Funktiot scanf ja gets pitää unohtaa.
Funktiolla fgets jono voidaan lukea turvallisesti, koska fgetsille viedään parametrinä jonon maksimipituus:
fgets(jono,max_pituus,tiedosto)
Haittana on kuitenkin se, että jonon loppuun jää '\n'-merkki. Tämä ongelma on poistettu kappaleen lopussa esitetyssä malliohjelmassa.
Seuraava ohjelma on erittäin vaarallinen:
c-taul\jonovaar.c - esimerkki tilan ylityksestä päätesyötössä
Tehtävä 13.124 Kanahaukka
Jos edelliselle ohjelmalle syötetään "Kanahaukka", niin se tulostaa:
|Kanahaukka| 7561 |kka|
Miksi?
13.4.2 Formaatin käyttö helpottaa hieman
Kanahaukka –esimerkin tilanne voitaisiin välttää käyttämällä lukemisessa muotoja:
c-taul\jonovaa2.c - tilanylityksen välttäminen formaatin avulla
scanf("%4s",jono1);
scanf("%4[^\n]",jono1);
Jälkimmäisessä on %s –formaatin tilalla käytetty formaattia [merkit–jotka–sallitaan], ja sen muotoa [^merkit–joita–ei–sallita]. Näin merkkijonoon saadaan mukaan myös välilyönnit, mitä ei %s–formaatissa saada. Muotoa voidaan käyttää tilapäisissä testiohjelmissa, mikäli alla olevaa kirjastoa ei ole käytössä. Vikana on se, että kentän maksimipituus on erittäin vaikeata saada seuraamaan merkkijonolle varattua tilaa!
13.4.3 getline apuun
Onneksi edes C++:ssa on tehty toimiva metodi merkkijonojen lukemiseen.
c-taul\jonolue.cpp - esimerkki toimivasta merkkijonon lukemisesta
13.5 Malliohjelmia merkkijonojen käsittelyyn
13.5.1 Aliohjelmia C–merkkijonojen käsittelyyn
Seuraavassa on joukko aliohjelmia merkkijonojen lukemiseen ja käsittelyyn. Niistä tehokkaimmalla –lue_jono_oletus – voidaan tulostaa näyttöön samalla hoput–teksti ja mikäli kysymykseen vastataan pelkkä [RET], käytetään oletusarvoa. Tämä aliohjelma on suunniteltu nimenomaan toimimaan kerhon jäsenrekisterin tarpeiden mukaisesti.
ali\mjonot.c - aliohjelmia merkkijonojen käsittelyyn
/****************************************************************************/
/*
** M J O N O T . C
**
** Yleisiä merkkijonojen käsittelyyn liittyviä aliohjelmia.
**
** Aliohjelmat:
** tee_jono - luo uuden merkkijonon jonne jono kopioidaan
** kopioi_jono - kopioi kork. annetun määrän merkkejä
** liita_jono - liittää jonon toisen perään, tulos
** korkeintaan max.pituus mittainen
** f_lue_jono - lukee tiedostosta merkkijonon
** alusta_lue_jono - alustaa merkkijonon lukemisen
** lue_jono - lukee päätteeltä merkkijonon
** lue_jono_oletus - lukee päätteeltä merkkijonon.
** Näyttöön tulostetaan haluttu viesti ja
** jonon oletusarvo, mikäli painetaan RET
** lue_kokluku_oletus - luetaan kokonaisluku, jolle käytetään
** oletusarvoa mikäli heti painetaan RET
** poista_alkutyhjat - poistaa merkkijonon alussa olevat välilyönnit
** poista_lopputyhjat - poistaa merkkijonon lopussa olevat välil.
** poista_2_tyhjat - muuttaa merkkijonossa kaikki peräkkäiset
** välilyönnit yhdeksi välilyönniksi
** poista_tyhjat - poistaa alusta ja lopusta kaikki sekä
** muualta moninkertaiset välilyönnit
** poista_alku_ja_2_tyhjat- poistaa alusta ja 2x tyhjät
** isoksi - muuttaa kirjaimen isoksi kirjaimeksi huomioiden
** skandit
** pieneksi - muuttaa pieneksi huomioiden skandit
** jono_isoksi - muuttaa jonon kaikki merkit isoiksi
** jono_pieneksi - muuttaa jonon kaikki merkit pieniksi
** jono_alku_isoksi - muuttaa jonon kaikki sanojen alut isoiksi
** ja kaikki muut pieniksi
** jono_1_isoksi - muuttaa jonon 1. kirjaimen isoksi
** wildmat - vertaa onko sana == maski, missä maskissa
** voi olla jokeri-merkkejä (* tai ?)
** onko_samat - ensin muutetaan jonot isoiksi ja poistetaan
** tyhjät ja sitten wildmat
** (eli " Kalle " == " k* ")
** palanen - ottaa merkkijonosta seuraavan erotinmerkkien
** määräämän palasen
** laske_merkit - laskee annettujen merkkien esiintymismäärän
** merkkijonossa
** paikka - palauttaa kirjaimen 1. indeksin merkkijonossa
** tayta_valit - täyttää syötön "A-F" muotoon "ABCDEF"
** joku_jono - vastaa kysymykseen onko "EY" joku jonoista
** "EU|EY|EL"
** joku_jono_func - kuten edellä, mutta vertailufunktio voidaan
** antaa itse, esim
** joku_jono("EU","E?|USA",wildmat) => 1
** jono_arvosanaksi - muuttaa merkkijonon "7-" reaaliluvuksi 6.75
** arvosana_jonoksi - muuttaa reaaliluvun 6.75 merkkijonoki "7-"
** sallituissa - paluttaa -1 jos tutkittavan jonon kaikki
** merkit ovat annetussa joukossa, muuten
** 1. väärän merkin indeksin
** poista_merkit - poistaa jonosta kaikki valitut merkit
** poista_alusta - poistaa merkkejä jonon alusta
** lisaa_alkuun - lisää merkkejä jonon alkuun
** lisaa_merkki - lisää merkin merkkijonon valittuun kohtaan
** vaihda_jonot - vaihtaa jonossa merkit toisiksi
** ^ - rivin alussa tarkoittaa, etta
** vaihto tehdaan vain rivin alusta
** vaihda_merkit - vaihtaa jonossa yksittäiset merkit
** toisiksi
** muunna_C_symbolit - muuttaa \t, \n ja \0x61 muotoiset
** C-symbolit vastaaviksi merkeiksi
** jonossa_vain_merkit - poistaa jonosta kaikki ne merkit, jotka
** eivät ole valitussa joukossa
*/
Tehtävä 13.125 Merkkijonojen käsittely
Valitse muutamia aliohjelmia mjonot.c aliohjelmakirjastosta ja yritä itse toteuttaa ne.
13.5.2 Aliohjelmia C++ –merkkijonojen käsittelyyn
Edellisistä aliohjelmista suurin osa on toteutettu myös käsittelemään C++ –merkkijonoja:
ali\mjonotpp.h - aliohjelmia C++ -merkkijonojen käsittelyyn
/****************************************************************************/
/* M J O N O T P P. H
**
**
** Tiedostossa on merkkien ja merkkijonojen käsittelyohjelmien yleiset
** aliohjelmien otsikot. Tiedosto on lähinnä muunnostiedosto string-
** tyypille vastaavasta C-kirjastosta mjonot.c
**
** Tekijät: Vesa Lappalainen
** Tehty: 02.01.1996
** Muutettu 07.03.1996/vl
** Mitä muutettu: lue_jono_oletus laitettu toimimaan
**
** Tiedostossa on seuraavia aliohjelmia:
** remove(st) - poistaa tiedoston st
** onko_tiedostoa(st) - palauttaa 1 jos on tiedosto nimellä st
** rename(&vanha,&uusi) - vaihtaa tiedoston nimen
** tarkennin_alku(&tied) - palauttaa mistä kohti tarkennin alkaa
** tiedoston nimessä
** poista_tarkennin(tied) - poistaa tarkentimen tiedostonnimestä
** laita_tarkennin(tied,tark) - laittaa tiedoston nimen tarkentimen
** vaihda_tarkennin(tied,tark)- vaihtaa tiedoston nimen tarkentimen
**
** istream &lue_rivi(istream &is,char *s,int max_koko);
** istream &lue_rivi(istream &is,string &s);
** istream &lue_rivi(istream &is,int &i,int def=0);
**
** Seuraavat ovat mjonot.c:n muunnoksia string-luokalle
** (- merk ei totetuttu erikseen string-luokalle
** tai ei tarvitse toteuttaa)
**
** tee_jono - luo uuden merkkijonon jonne jono kopioidaan
** kopioi_jono - kopioi kork. annetun määrän merkkejä
** liita_jono - liittää jonon toisen perään, tulos
** korkeintaan max.pituus mittainen
** - f_lue_jono - lukee tiedostosta merkkijonon
** - alusta_lue_jono - alustaa merkkijonon lukemisen
** - lue_jono - lukee päätteeltä merkkijonon
** lue_jono_oletus - lukee päätteeltä merkkijonon.
** Näyttöön tulostetaan haluttu viesti ja
** jonon oletusarvo, mikäli painetaan RET
** - lue_kokluku_oletus - luetaan kokonaisluku, jolle käytetään
** oletusarvoa mikäli heti painetaan RET
** poista_alkutyhjat - poistaa merkkijonon alussa olevat välilyönnit
** poista_lopputyhjat - poistaa merkkijonon lopussa olevat välil.
** poista_2_tyhjat - muuttaa merkkijonossa kaikki peräkkäiset
** välilyönnit yhdeksi välilyönniksi
** poista_tyhjat - poistaa alusta ja lopusta kaikki sekä
** muualta moninkertaiset välilyönnit
** - isoksi - muuttaa kirjaimen isoksi kirjaimeksi huomioiden
** skandit
** - pieneksi - muuttaa pieneksi huomioiden skandit
** jono_isoksi - muuttaa jonon kaikki merkit isoiksi
** jono_pieneksi - muuttaa jonon kaikki merkit pieniksi
** jono_alku_isoksi - muuttaa jonon kaikki sanojen alut isoiksi
** ja kaikki muut pieniksi
** wildmat - vertaa onko sana == maski, missä maskissa
** voi olla jokeri-merkkejä (* tai ?)
** onko_samat - ensin muutetaan jonot isoiksi ja poistetaan
** tyhjät ja sitten wildmat
** (eli " Kalle " == " k* ")
** palanen - ottaa merkkijonosta seuraavan erotinmerkkien
** määräämän palasen
** laske_merkit - laskee annettujen merkkien esiintymismäärän
** merkkijonossa
** paikka - palauttaa kirjaimen 1. indeksin merkkijonossa
** tayta_valit - täyttää syötön "A-F" muotoon "ABCDEF"
** joku_jono - vastaa kysymykseen onko "EY" joku jonoista
** "EU|EY|EL"
** jono_arvosanaksi - muuttaa merkkijonon "7-" reaaliluvuksi 6.75
** arvosana_jonoksi - muuttaa reaaliluvun 6.75 merkkijonoki "7-"
** sallituissa - paluttaa -1 jos tutkittavan jonon kaikki
** merkit ovat annetussa joukossa, muuten
** 1. väärän merkin indeksin
** poista_merkit - poistaa jonosta kaikki valitut merkit
** poista_alusta - poistaa merkkejä jonon alusta
** lisaa_alkuun - lisää merkkejä jonon alkuun
** lisaa_merkki - lisää merkin merkkijonon valittuun kohtaan
** vaihda_jonot - vaihtaa jonossa merkit toisiksi
** muunna_C_symbolit - muuttaa \t, \n ja \0x61 muotoiset
** C-symbolit vastaaviksi merkeiksi
** erota - laittaa merkkijonon kahtia valitun merkin
** kohdalta
** erota - laittaa merkkijonon kahtia ensimmäisen
** valitun merkin kohdalta
13.6 C++ –merkkijonojen ja C–merkkijonojen yhteiskäyttö
Koska suurta osaa valmiina olevista aliohjelmista ei ole tehty kuin käsittelemään pelkkiä C–merkkijonoja, tutkimme seuraavassa hieman eri merkkijonotyyppien ristiinkäyttöä. Tämä on tietenkin mahdollista vain C++ –ohjelmissa.
13.6.1 C–merkkijonon muuttaminen C++ –jonoksi
C–merkkijono muuttuu C++ –jonoksi esimerkiksi sijoituslauseessa tai muodostajassa. Tämän ansiosta voidaan myös kutsua C–merkkijonoilla kaikkia niitä C++ –aliohjelmia, jonne viedään parametrinä joko string
tai const string &
. Miksikö? Koska kääntäjä voi tällöin luoda tilapäisen string
–olion kutsun ajaksi ja alustaa tämän kutsussa olleella C–merkkijonolla:
c-taul\c2cpp.cpp - C-jonojen muuttaminen C++ -jonoiksi
Ohjelman tulostus:
Kissa Koira
Kissa Kana A
Kana
Kissa
Mato
Rissa
Kissa
Emu
Kuna
Huomattakoon, että esimerkiksi kutsusta muuta_eka(cs1); seuraa esimerkiksi varoitus:
Warn : c2cpp.cpp(35,17):Temporary used for parameter 'cppS' in call to
'muuta\_eka(string &)'
Tässä varoitetaan ohjelmoijaa siitä, että tilapäinen string–olio luodaan kutsun ajaksi, mutta tämä olio sitten häviää kutsun jälkeen ja cs1 jää muuttumatta ehkä vastoin ohjelmoijan toiveita. Tässäkin on hyvä esimerkki siitä, että varoitukset kannattaa ottaa todesta!
Kutsussa muuta_eka("Emu"); ei ole mitään järkeä, mutta sekin menee kääntäjästä läpi pelkällä varoituksella. Onneksi tulee se tilapäinen jono ja vakiomerkkijono jää muuttumatta!
Tähän asiaan osittain kuulumaton vaaranpaikka on taas osoittimissa. Jos aliohjelma muuta_toka olisikin ollut muodossa (oli nimittäin minulla ja kaatoi koneen):
void muuta_toka(string *cppS)
{ :-(
cppS[1] = 'u';
}
menisi tämä täysin ilman varoituksia kääntäjästä lävitse. Eihän tässä nimittäin ole mitään virhettä. Lauseessahan vain käsketään sijoittamaan merkkijonotaulukon cppS toiseen paikkaan merkkijono u. Kirjain u muuttuu ensin automaattisesti merkkijonoksi ja tämä taas kuten aiemmin todettiin, voidaan sijoittaa C++ merkkijonoon. Taas yksi syy lisää VAROA osoittimia (ei kieltää niiden käyttöä). Toisaalta myös varoitus siitä, etteivät automaattiset tyypinmuunnokset ehkä sittenkään ole niin ihana asia!
13.6.2 C++ –merkkijonon muuttaminen C–jonoksi
Toisensuuntainen muunnos ei yleensä onnistu. C++ –merkkijonosta kyllä saadaan tilapäinen osoitin vastaavaan VAKIO C–merkkijonoon, jota voidaan käyttää hyväksi aliohjelmakutsuissa, joiden parametrilistassa on vakiomerkkijono:
c-taul\cpp2c.cpp - C++ -merkkijonojen käyttö C-merkkijonoja vaativissa kutsuissa
Muoto jono.c_str()
palauttaa nimenomaan osoittimen vakiomerkkijonoon. Tämän osoittimen elinikää ei taata, korkeintaan se elää niin kauan kuin vastaava jonokin. Kuitenkin esimerkiksi edellä joka tapauksessa aliohjelmakutsujen ajan. Tilanne on hankalampi jos jonoa pitää muuttaa, koska (onneksi) kääntäjä ei hyväksy tätä osoitinta muuttuvan merkkijonon tilalle kutsussa. Tällöin ainoaksi mahdollisuudeksi jää tehdä C++–jonosta itse tilapäinen kopio C–merkkijonoksi ja käyttää tätä jonoa aliohjelman parametrinä ja aliohjelman jälkeen sitten sijoitetaan tilapäinen jono takaisin alkuperäiseen.
Jos useasti pitää kutsua samaa C–aliohjelmaa C++ –merkkijonolla, kannattaa tehdä "välittäjä"funktio, joka tekee em. kopioinnin. Funktioiden nimien kuormittamisen ansiostahan tämä välittäjäfunktio voidaan tehdä samalle nimelle. Tätä tekniikkaa on käytetty mm. muutettaessa mjonot.c:n funktioita C++:ksi kirjastossa mjonotpp.h.
Tehtävä 13.126 Välittäjäfunktio
Kirjoita C++ jonon hyväksyvä funktio muuta_eka käyttäen hyväksi cpp2c.cpp:n C- funktiota muuta_eka.
13.7 Tietueet, union ja enum
struct:n tapainen varattu sana on union, jolla tehdään päällekkäin olevia muistialueita, joilla on eri tilanteessa eri käyttö. Esimerkiksi henkilötietueessa naisilla voisi olla synnyttanyt_lapsia ja miehillä armeija_kayty, jotka jakaisivat saman muistialueen (huom. esimerkki tehty -91).
c-taul\union.c - vaihtuva muistipaikan tyyppi
Matti Maija
--------------------------- ---------------------------
| ------------- | | ------------- |
|nimi: |M|a|t|t|i|0| | |nimi: |M|a|i|j|a|0| |
| ------------- | | ------------- |
|sukupuoli:| 0 | | |sukupuoli:| 1 | |
| ----- | | ----- |
|tiedot: | 1 | | |tiedot: | 4 | |
--------------------------- ---------------------------
Tällä kurssilla emme kuitenkaan suosittele union–tyyppisten muuttujien käyttöä! Mieluummin käytämme olioita ja polymorfismia.
Edellä enum tarkoitti lueteltua tyyppiä, jolloin mies vastasi vakiota 0 ja nainen vakiota 1. Tähän palataan myöhemmin.
Tehtävä 13.127 union
Mitä union.c ohjelma tulostaa. Piirrä kuva tietueista jos union vaihdettaisiinkin structiksi. Korvaa enum sekä sen nainen ja mies #define- vakiolla.
Tehtävä 13.128 Tietue tietueessa
Olkoon meillä seuraavat C- määritykset:
typedef struct {
char nimi[50];
int paino;
int ika;
} Elain_tiedot;
typedef struct {
char laji[40];
Elain_tiedot elain;
int nro;
} Laji_tiedot;
int main(void)
{
Laji_tiedot miuku;
...
return 0;
}
Täydennä ... - kohtaan seuraavat "sijoitukset" muuttujalle miuku:
laji <- "Kissa"
nro <- 5
nimi <- "Miuku"
paino <- 3
ika <- 5
Piirrä kuva miuku - muuttujasta sijoitusten jälkeen.
Tehtävä 13.129 Miukusta olio
Muuta edellisen tehtävän tietueet C++ - luokiksi ja tee sijoituksien avuksi metodit, jotka hoitavat sijoitukset. Saat nyt - tottakai - käyttää myös C++:n string- luokkaa.
13.8 Moniulotteiset taulukot
Moniulotteiset taulukot voidaan C–kielessä esitellä monella eri tavalla. Käymme nopeasti lävitse eri vaihtoehtoja.
13.8.1 Kiinteä esittely
Kaikkein helpoin tapa esitellä moniulotteinen taulukko on aivan normaali esittely:
int matriisi[3][4];
┌───┬───┬───┬───┐
matriisi─────> │ │ │ │ │ <─── &matriisi[0][3]
├───┼───┼───┼───┤
matriisi+1 ──> │ │ │ │ │
├───┼───┼───┼───┤
matriisi+2───> │ │ │ │ │
└───┴───┴───┴───┘
Taulukon nimi on osoitin sen 1. RIVIIN!. Mallin tapauksessa kokonaislukuvektoriin int [4].
Taulukon alkioina voi tietysti olla mikä tahansa olemassa oleva tyyppi. C–kielessä matriisi talletetaan rivilistana, eli muistissa on ensin rivin 0 alkiot ja sitten rivin 1 alkiot jne. Myös moniulotteinen taulukko voidaan alustaa esittelyn yhteydessä:
double yks[3][3] = {
{ 1.0, 0.0, 0.0 },
{ 0.0, 1.0, 0.0 },
{ 0.0, 0.0, 1.0 }
}
Tehtävä 13.130 Matriisit
Kirjoita seuraavat aliohjelmat, jotka saavat parametrinään 2 3x3 matriisia ja palauttavat 3x3 matriisin:
- Laskee yhteen 2 matriisia.
- Kertoo kaksi matriisia keskenään. (Kirjoita avuksi funktio, joka kertoo matriisin rivin i toisen matriisin sarakkeella j).
13.8.2 Muuttuvat dimensiot
Kun moniulotteinen taulukko esitellään aliohjelman parametrinä, pitää suurinta ulottuvuutta lukuun ottamatta (tietysti senkin saa kiinnittää) kiinnittää kaikki muut ulottuvuudet. Esimerkiksi:
c-taul\matriisi.c - esimerkki vaihtuvasta rivimäärästä 2-ulotteisessa taulukossa
#include <stdio.h>
#define SARAKKEITA 3
double alkioiden_summa(double mat[][SARAKKEITA], int riveja)
{
int i,j; double summa=0;
for (i=0; i<riveja; i++)
for (j=0; j<SARAKKEITA; j++)
summa +=mat[i][j];
return summa;
}
int main(void)
{
double s2,s3, mat2[2][SARAKKEITA] = { {1,2,3},{4,5,6} },
mat3[3][SARAKKEITA] = { {1,0,0},{0,1,0},{0,0,1} };
s2 = alkioiden_summa(mat2,2);
s3 = alkioiden_summa(mat3,3);
printf("Summat on %5.2lf ja %5.2lf\n",s2,s3);
return 0;
}
13.8.3 Yksiulotteisen taulukon käyttäminen moniulotteisena
On tietysti surullista, ettei taulukon kaikkia ulottuvuuksia voida välittää parametrinä. Eihän juuri koskaan ohjelmaa tehtäessä tiedetä lopullista tilan tarvetta. Tai sitten ohjelman aikana samoja aliohjelmia tarvittaisiin erikoisille taulukoille. Tietysti voitaisiin esitellä liian suuri taulukko, ja "käyttää" vain vasenta ylänurkkaa.
Toisaalta moniulotteinenkin taulukko on todellisuudessa muistissa vain 1–ulotteisena. Tästä muunnoksestahan puhuttiin jo monisteen alkuosassa. On makuasia kumpiko järjestys esimerkiksi matriisissa valitaan: sarakelista vaiko rivilista. Rivilista on C–kielen mukainen, mutta toisaalta maailma on pullollaan Fortran aliohjelmia, joissa matriisit on talletettu sarakelistana. Siis kumpikin tapa on syytä hallita.
Tehtävä 13.131 Matriisi 1- ulotteisena
Kirjoita aliohjelma tee_yksikko, jolle tuodaan parametrinä neliömatriisin rivien lukumäärä ja 1- ulotteisen taulukon alkuosoite, ja joka alustaa tämän neliömatriisin yksikkömatriisiksi.
13.8.4 Taulukko taulukoista
Eräs tapa käsitellä useampiulotteisia taulukoita on määritellä ensin rivityyppi ja sitten 1–ulotteinen taulukko näitä rivityyppejä:
c-taul\mat2.c - matriisi parametrina riviosoittimen avulla
#include <stdio.h>
#define SARAKKEITA 3
typedef double Rivi_tyyppi[SARAKKEITA];
double alkioiden_summa(Rivi_tyyppi *mat, int riveja)
{
int i,j; double summa=0;
for (i=0; i<riveja; i++)
for (j=0; j<SARAKKEITA; j++)
summa +=mat[i][j];
return summa;
}
int main(void)
{
double s2,s3;
Rivi_tyyppi mat2[2] = { {1,2,3},{4,5,6} },
mat3[3] = { {1,0,0},{0,1,0},{0,0,1} };
s2 = alkioiden_summa(mat2,2);
s3 = alkioiden_summa(mat3,3);
printf("Summat on %5.2lf ja %5.2lf\n",s2,s3);
return 0;
}
Tosiasiassa tällä ei ole mitään eroa edelliseen vastaavaan ohjelmamalliin verrattuna, tämä ehkä vain paremmin kuvastaa sitä, mihin matriisin nimi on osoitin.
13.8.5 Taulukko osoittimista
Mikäli halutaan tehdä esimerkiksi 2–ulotteinen taulukko, jonka kumpikin ulottuvuus on muuttuva, mutta silti taulukkoa käytetään kuten 2–ulotteista tavallista taulukkoa, pitää käyttää taulukkoa osoittimista:
mat─┐ mat[0] ┌────────── mat[0][2]
│ │ │
│ │ v
v └────> ┌───┬───┬───┬───┐
┌───┐ ┌───> │ 1 │ 2 │ 3 │ 4 │ r0
0 │ o─┼───┘ └───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┐
1 │ o─┼────────>│ 5 │ 6 │ 7 │ 8 │ r1
├───┤ └───┴───┴───┴───┘
2 │ o─┼───┐ ┌───┬───┬───┬───┐
└───┘ └────>│ 9 │ 0 │ 1 │ 2 │ r2
└───┴───┴───┴───┘
c-taul\mat3.c - matriisi osoitintaulukon avulla
#include <stdio.h>
double alkioiden_summa(double **mat, int riveja, int sarakkeita)
{
int i,j; double summa=0;
for (i=0; i<riveja; i++)
for (j=0; j<sarakkeita; j++)
summa +=mat[i][j];
return summa;
}
int main(void)
{
double s1,s2;
double r0[] = {1,2,3,4}, r1[] = {5,6,7,8}, r2[] = {9,0,1,2};
double *mat[3];
mat[0] = r0; mat[1] = r1; mat[2] = r2;
s1 = alkioiden_summa(mat,2,3);
s2 = alkioiden_summa(mat,3,4);
printf("Summat on %5.2lf ja %5.2lf\n",s1,s2);
return 0;
}
Esimerkissä on sama esitelläänkö aliohjelmassa:
double alkioiden_summa(double **mat,...
/* vai */
double alkioiden_summa(double *mat[],..
Tässä menettelyssä on vielä se etu, ettei kaikkien rivien välttämättä tarvitsisi edes olla yhtä pitkiä. Harvassa matriisissa osa osoittimista voisi olla jopa NULL–osoittimia, mikäli rivillä ei ole alkioita (aliohjelman pitäisi tietysti tarkistaa tämä). Oikeasti rivit usein vielä luotaisiin dynaamisesti ajonaikana tarvittavan pituisina.
Tehtävä 13.132 Transpoosi
Kirjoita taulukko- osoittimia käyttäen aliohjelma, joka saa parametrinään kaksi matriisia ja niiden dimensiot. Aliohjelma tarkistaa voiko toiseen matriisiin tehdä toisen transpoosin (vaihtaa rivit ja sarakkeet keskenään) ja tekee transpoosin jos pystyy. Onnistuminen palautetaan aliohjelman nimessä.
13.9 Komentorivin parametrit (argv)
Esimerkiksi C–kielinen pääohjelma saa käyttöjärjestelmältä tällaisen taulukon kutsussa olleista argumenteista:
c-taul\argv.c - komentorivin parametrit
Edellä nimet tulevat
argc = argument count
argv = argument vector
Kun ohjelma käännettäisiin (vaikkapa nimelle argv.exe) ja ajettaisiin komentoriviltä saattaisi tulostus olla seuraavan näköinen (MS–DOS -koneessa):
C:\KURSSIT\CPP\MONISTE\ESIM\C-TAUL>argv kissa istuu puussa[RET]
Argumentteja on 4 kappaletta:
0: C:\KURSSIT\CPP\MONISTE\ESIM\C-TAUL\ARGV.EXE
1: kissa
2: istuu
3: puussa
C:\KURSSIT\CPP\MONISTE\ESIM\C-TAUL>_
argv─┐ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬
│ ┌────────> │ C │ : │ \ │ K │ U │ R │ S │ S │ I │ T │
v │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴
┌───┐ │ ┌───┬───┬───┬───┬───┬───┐
0 │ o─┼─┘ ┌──────>│ k │ i │ s │ s │ a │NUL│ argc = 4
├───┤ │ └───┴───┴───┴───┴───┴───┘
1 │ o─┼───┘ ┌───┬───┬───┬───┬───┬───┐
├───┤ ┌────>│ i │ s │ t │ u │ u │NUL│
2 │ o─┼─────┘ └───┴───┴───┴───┴───┴───┘
├───┤ ┌───┬───┬───┬───┬───┬───┬───┐
3 │ o─┼──────────>│ p │ u │ u │ s │ s │ a │NUL│
├───┤ └───┴───┴───┴───┴───┴───┴───┘
4 │ o─┼─┐ ^
└───┘ │ argv\[3\]\[2\]───────┘
─┴─
HUOM! Myös C++–ohjelma saa saman taulukon. Siis joukon osoittimia C–merkkijonoihin, ei mitään string taulukkoa
Tehtävä 13.133 Palindromi
Kirjoita ohjelma pali, jota kutsutaan komentoriviltä seuraavasti:
C:\OMAT\OHJELMOI\VESA>pali kissa[RET]
kissa EI ole palindromi!
C:\OMAT\OHJELMOI\VESA>pali saippuakauppias[RET]
saippuakauppias ON palindromi!
C:\OMAT\OHJELMOI\VESA>_
14. Parametrin välityksestä ja osoittimista, kertaus
Mitä tässä luvussa käsitellään?
- kerrataan aliohjelmien kutsumista
- kerrataan aliohjelmien suunnittelua
- kerrataan osoittimia parametreinä
- kerrataan referenssejä parametreinä
14.1 Miksi aliohjelmia käytetään
Aliohjelmia käytettiin mm. seuraavista syistä:
- kyseessä on luonteeltaan oma looginen kokonaisuutensa
- joku toinen (tai itse aikaisemmin) on kirjoittanut halutun tehtävän suorittavan aliohjelman
Mikäli aliohjelma kirjoitetaan itse, tulee ongelmaksi tietysti se, mitä parametrejä aliohjelmaan esitellään.
Mikäli aliohjelman on kirjoittanut joku toinen, on ongelmana se miten aliohjelmaa kutsutaan.
14.2 Yksinkertaisen aliohjelman kutsuminen
Valmiin aliohjelman kutsuminen on helppoa: etsitään aliohjelman esittely ja kirjoitetaan kutsu, jossa on vastaavan tyyppiset parametrit vastaavissa paikoissa.
14.2.1 sin
Esimerkiksi funktion sin esittely saattaa olla muotoa:
double sin (double x);
For real sin, x is in radians.
sin of a real argument returns a value in the range - 1 to 1.
Funktion tyyppi on double ja sille viedään double tyyppinen parametri. Funktio ei muuta mitään parametrilistassa esiteltyä parametriään (mistä tietää?). Siis funktiota ei ole mitään mieltä kutsua muuten kuin sijoittamalla sen palauttama arvo johonkin muuttujaan tai käyttämällä funktiota osana jotakin lauseketta. x:ää vastaava parametri voi olla mikä tahansa double tyyppisen arvon palauttava lauseke (tietysti mielellään sellainen joka tarkoittaa kulmaa radiaaneissa):
double kulman_sini,a,b,x,y;
...
kulman_sini = sin(x);
...
y = sin(x/2) + cos(a/3);
...
Funktiota voitaisiin tietysti kutsua myös muodossa:
double x = 3.1;
sin(x); :-(
mutta kutsussa olisi yhtä vähän järkeä kuin kutsussa
double x=3.1;
x + 3.0; :-(
tai jopa
3.0;
Mihin lausekkeiden arvot menisivät? Eivät minnekään! Siis lausekkeissa ei ole mieltä!
14.2.2 strcmp
Esimerkki moniparametrisesta tavallisesta funktiosta olisi vaikkapa strcmp, joka on esitelty seuraavasti:
Compares one string to another
int strcmp (const char *s1, const char *s2);
Prototype in string.h
Returns a value that is
< 0 if s1 is less than s2
== 0 if s1 is the same as s2
> 0 if s1 is greater than s2
Performs a signed comparison.
Funktio siis vertaa kahta merkkijonoa toisiinsa. Funktion esittelyn mukaan funktiolle viedään parametrinä kaksi osoitinta. const sanalla korostetaan sitä, ettei funktio muuta niiden muistipaikkojen sisältöä, joihin osoittimet osoittavat. Siis tämäkään funktio ei muuta parametrejään, joten sen ainoa järkevä käyttö on sijoittaa tulos johonkin muuttujaan tai käyttää funktiota muuten osana lauseketta:
char j1[20]="Kissa", j2[20]="Koira";
int samat;
...
samat = strcmp(j1,j2);
if ( samat == 0 ) printf("Jonot ovat samat!\n");
...tai..
if ( strcmp(j1,j2) < 0 ) printf("Jono 1 on ensin!\n");
14.2.3 Varo toistoa
Vaikka yksinkertaista funktiota voidaankin käyttää mukavasti lausekkeen osana, tulee seuraavankaltaista rakennetta välttää:
int i,a_lkm=0;
char st[50]="Saippuakauppias";
for (i=0; i<strlen(st); i++) :-(
if ( jono[i]=='a' ) a_lkm++;
...
Miksikö? Koska strlen–funktio suoritetaan jokaisella silmukan kierroksella ja strlen suorittaminenhan vaati koko jonon läpikäymisen! Rakenne tulee korvata seuraavasti:
int i,a_lkm=0,st_pit;
char st[50]="Saippuakauppias";
...
st_pit=strlen(st);
for (i=0; i<st_pit; i++)
if ( jono[i]=='a' ) a_lkm++;
...
Edellä kävisi myös seuraava, miksi?
for (i=strlen(st)- 1; i >= 0; i- - )
if ( jono[i]=='a' ) a_lkm++;
14.3 Funktio, joka muuttaa myös parametrejään
Aina kun palautettavia arvoja on enemmän kuin yksi, on vain kaksi mahdollisuutta: tulos palautetaan funktion nimessä tietueessa tai parametreissä osoitteen avulla. Esimerkiksi C–kielen valmiissa kirjastoissa ei juurikaan käytetä tietueita funktioiden arvoina.
14.3.1 strcpy
Copies string src to dest
char *strcpy (char *dest, const char *src);
Prototype in string.h
Returns dest.
Tämä funktio laittaa tuloksensa (eli src–jonon kopion) dest–merkkijonoon. Toisaalta tulos–jonon osoite palautetaan myös funktion nimessä. Koska C–kielessä lausekkeen arvoa ei ole pakko sijoittaa mihinkään (muuten esim. i++ ei olisi mielekäs), voidaan tällaista funktiota kutsua joko itsenäisenä proseduurimaisesti tai sitten lausekkeen osana:
char jono[30], *p;
...
strcpy(jono,"Kissa");
...
p = strcpy(jono,"Susikoira") + 4; /* p osoittaa jonoon "koira" */
On tietysti olemassa myös void–funktioita, jotka eivät palauta mitään arvoa nimessään.
14.4 Vaihteleva määrä parametrejä
C–kielessä on myös mahdollista tehdä funktiota, joiden parametrilistan pituus (ja jopa parametrien tyyppi) on vaihteleva.
14.4.1 printf ja scanf
Formatted output to stdout
int printf (const char *format
[, argument, ...]);
Prototype in stdio.h
printf formats a variable number of arguments according to the format,
sending the output to stdout. Returns the number of bytes output. In the
event of error, it returns EOF.
Kaikki tällaiset muuttuvaparametriset ohjelmat tarvitsevat ainakin yhden parametrin (1., miksi) joka ilmoittaa muiden parametrien määrän ja luonteen (=tyypin).
Funktiossa printf tämä parametri on 1. merkkijono, format –jono, jonka % –merkkien määrä suurin piirtein ilmoittaa muiden parametrien määrän ja % –merkkien jälkeinen kirjain ilmoittaa niiden tyypin. Kääntäjä ei tietenkään voi tietää merkkijonon sisällöstä, joten parametrien tyypin tarkastus jää tekemättä ja vääristä kutsuista saattaa seurata jopa koneen kaatuminen! Tyypillisin väärä esimerkki on kutsu:
int lkm;
printf("Anna kissojen lkm>");
scanf("%d",lkm); /* VÄÄRIN VÄÄRIN VÄÄRIN */ :-(
14.5 Aliohjelman esitteleminen
Käytännössä usein tulee tilanne, jossa tarvittaisiin aliohjelmaa, jota ei valmiina mistään löydy. Tällöin aliohjelma tietenkin kirjoitetaan itse. Ongelmaksi saattaa muodostua aliohjelman parametrilistan keksiminen.
14.5.1 Syöttö– ja tulosparametrit
Eräs tilanne voisi olla vaikka seuraava. Jostakin on saatu (esim. päätteeltä luettu) merkkijono, jossa on päivämäärä. Toisaalta ohjelmassa on esitelty päivämäärätyyppi
typedef struct {
int pv,kk,vv;
} Pvm_tyyppi;
Merkkijonossa oleva päivämäärä pitäisi saada muutetuksi Pvm_tyyppiseksi. Millainen aliohjelma kirjoitetaan? Helpointa on ehkä miettiä kutsun kautta:
Aliohjelmalle täytyy välittää parametrinä muutettava merkkijono. Aliohjelman tulee palauttaa muutettu päivämäärä. Siis kutsu voisi olla muotoa:
char jono[40]="13.3.1992";
Pvm_tyyppi pvm;
...
muuta_jono_pvmksi(jono,pvm); /* ??? */
Tämä ei tietenkään voi toimia C–kielessä, koska aliohjelma ei pystyisi muuttamaan pvm–muuttujaa! Siis kutsu täytyy olla muotoa:
muuta_jono_pvmksi(jono,&pvm);
Tästä seuraa että aliohjelma täytyy vastaavasti esitellä muodossa:
void muuta_jono_pvmksi(const char *jono, Pvm_tyyppi *pvm)
{
...
}
Toisaalta C++:ssa myös muoto
muuta_jono_pvmksi(jono,pvm); /* ??? */
voisi toimia, mikäli aliohjelma esiteltäisiin viitemuuttujaa käyttäväksi
void muuta_jono_pvmksi(const char *jono, Pvm_tyyppi &pvm)
Aliohjelman tyyppi voisi olla myöskin int, jolloin aliohjelman nimessä palautetaan tieto siitä, onnistuiko muutos vai ei (miksi ei onnistuisi?).
Joskus tällainen aliohjelma voidaan esitellä jopa char * –tyyppiseksi, jotta nimessä voidaan palauttaa osoitin virhettä kuvaavaan viestiin:
const char *viesti,...
...
viesti = muuta_jono_pvmksi(jono,&pvm);
if ( viesti ) {
printf("%s\n",viesti);
return 1;
}
Koko ohjelma testiohjelmineen voisi olla esimerkiksi seuraava:
param\pvmjono.c - esimerkki tietueesta parametrina
Tehtävä 14.134 pvmjono.cpp
Kirjoita pvmjono.c:stä C++ - versio. Ensimmäinen versio pelkästään muuttamalla osoitteet referensseiksi ja toinen muuttamalla koko Pvm_tyyppi luokaksi cPvm ja kaikki mahdolliset funktiot tietysti luokan metodeiksi.
14.5.2 Syöttö– ja tulosparametri samassa
Joissakin tilanteissa ei välttämättä tarvita erillisiä parametrejä syötölle ja tulokselle. Esimerkiksi päivämäärän tapauksessa voisi tulla vastaan tarve lisätä päivämäärää yhdellä (tai useammalla päivällä). Tällöin kutsu olisi muotoa:
Pvm_tyyppi pvm;
...
seuraava_pvm(&pvm);
Aliohjelma esiteltäisiin vastaavasti:
void seuraava_pvm(Pvm_tyyppi *pvm)
{
...
}
Toisaalta funktion nimessä voitaisiin palauttaa vaikkapa tieto siitä, muuttuiko kuukausi
int seuraava_pvm(Pvm_tyyppi *pvm)
{
...
}
...
int kuukausi_muuttui;
kuukausi_muuttui = seuraava_pvm(&pvm);
if ( kuukausi_muuttui )...
Aikojen kuluessa aliohjelman tarve saattaisi muuttua muotoon
lisaa_pvm(&pvm,3);
Esittely muuttuisi tietysti:
int lisaa_pvm(Pvm_tyyppi *pvm, int lkm)
{
...
}
Tehtävä 14.135 Päivämäärän lisäys
Täydennä pvmjono.cpp:ssä luokkaan cPvm myös metodi lisaa.
14.5.3 Tietueet osoitteiden avulla tai viitteiden avulla
Usein tietueet ja oliot välitetään osoitteiden tai viitteiden avulla, vaikkei niitä olisikaan tarkoitus muuttaa aliohjelmassa. Tähän on syynä se, että parametrin välityksessähän kopioidaan kutsumuuttujien arvot aliohjelman lokaaleihin muuttujiin. Tietueiden tapauksessa kopiointi saattaa olla isokin, ja tämän vuoksi osoitteen välityksessä menee pelkkä osoite, ei koko tietueen sisältö.
int vertaa_pvm(Pvm_tyyppi pv1, Pvm_tyyppi pv2)
{
...
}
...
int ero;
Pvm_tyyppi pvm1,pvm2;
ero = vertaa_pvm(pvm1,pvm2); /* Oikein, mutta iso kopiointi */
if ( ero < 0 ) printf("Pvm1 on ensin!\n");
Parametrin välitys osoitteiden avulla kopioinnin välttämiseksi:
int vertaa_pvm(Pvm_tyyppi *pv1, Pvm_tyyppi *pv2)
{
...
}
...
int ero;
Pvm_tyyppi pvm1,pvm2;
ero = vertaa_pvm(&pvm1,&pvm2); /* Oikein */
if ( ero < 0 ) printf("Pvm1 on ensin!\n");
Tällöin tietysti aliohjelman kirjoittajan on oltava huolellinen, ettei muuta pv1 ja pv2 osoitteiden osoittamia päivämääriä! Usein tätä kommentoidaan const esittelyllä:
int vertaa_pvm(const Pvm_tyyppi *pv1, const Pvm_tyyppi *pv2)
{
...
}
...
Tehtävä 14.136 vertaa_pvm
Kirjoita aliohjelma vertaa_pvm, joka palauttaa - 1, mikäli pv1 on ennen pv2:sta, 0 jos päivämäärät ovat samoja ja 1 muuten.
Tehtävä 14.137 vertaa- metodi
Täydennä pvmjono.cpp:ssä luokkaan cPvm myös metodi vertaa(const cPvm &pv2) , joka toimii kuten edellinen vertaa_pvm, mutta verrattavina ovat *this ja pv2.
14.5.4 Useita parametrejä
Olkoon vaikkapa seuraava tilanne: Laskettava henkilön bruttotulosta verottajalle ja henkilölle itselleen jäävät osuudet. Miten tätä aliohjelmaa kutsuttaisiin? Aliohjelma tarvitsee tietysti parametrikseen henkilön bruttotulon, veroprosentin sekä tiedon siitä mihin tulokset laitetaan. Tulos muuttujia pitää voida muuttaa, joten niiden kohdalle kutsuun tulee tietysti osoittimet. Siis kutsu voisi olla esimerkiksi:
double tulo,pid_pros,verottaja,netto;
...
laske_verot(tulo,pid_pros,&verottaja,&netto);
Aliohjelman esittely kutsun perusteella täytyisi olla siis
void laske_verot(double brutto, double pros,
double *pros_osuus, double *netto)
{
...
}
Tehtävä 14.138 Sama viitteiden avulla
Toista edellinen päättely parametrien tyypeistä, jos voit käyttää viiteparametrejä (referenssi).
14.5.5 Parametrien lisääminen
Usein itse tehtyä aliohjelmaa voidaan jälkeenpäin tehdä yleiskäyttöisemmäksi tai paremmaksi lisäämällä siihen parametrejä.
Olkoon alkuperäinen ongelma sellainen, että on esitetty merkkijono muodossa "a–f". Tämä pitäisi saada muutettua muotoon "abcdef". Tarkoitusta varten kirjoitetaan aliohjelma tayta_valit.
Mitkä ovat aliohjelman parametrit? Tietysti alkuperäinen jono. Mihin tulos? Tähän tarvitaan ehkä toinen parametri. Nimessään aliohjelma voi vielä palauttaa osoitteen tulosjonoon, kuten merkkijonofunktioilla yleensäkin on tapana:
char merkit[4]="a- f", tulos[20], sallitut[20];
...
tayta_valit(tulos,merkit);
...tai...
strcat(sallitut,tayta_valit(tulos,merkit));
Funktion esittely on siis muotoa:
char *tayta_valit(char *tulos, const char *jono);
... **char** \*tayta\_valit(**char** \*tulos, **const** **char** \*jono);
Myöhemmin huomataan, että tulos–jonon maksimipituus voidaan ylittää! Siksi funktion kutsuun lisätäänkin yksi parametri:
tayta_valit(tulos,20,merkit);
ja vastaavasti esittelyyn:
char *tayta_valit(char *tulos, int max_pit, const char *jono);
Toinen vastaava esimerkki oli seuraava_pvm aliohjelman muuttaminen aliohjelmaksi lisaa_pvm.
14.5.6 Ei printf eikä scanf tai tietovirtoja
On aina muistettava, että aliohjelman tehtävä on työskennellä syöttö– ja tulosparametriensä avulla. Siis jollei ongelmaan ole erikseen määritelty päätesyöttöä tai tulostusta, ei aliohjelmassa saa olla printf ja scanf eikä muita vastaavia lauseita (mm. cin >>, cout <<) !
Olisihan todella yllätys, mikäli kutsussa
y = sin(x);
rupeaisi tulostumaan näytölle jotakin tai jopa odotettaisiin syöttöä päätteeltä!
14.6 Osoitteista ja osoittimista
Mikäli ohjelmassa esitellään osoitintyyppisiä muuttujia, pitää aina muistaa perustella mihin osoittimet osoittavat. Esimerkiksi seuraava ohjelma olisi todella väärin:
char *jono; /* VÄÄRÄ ESIMERKKI!!!! */
strcpy(jono,"Kissa"); :-(
Mihin muuttuja jono osoittaisi? Satunnaiseen paikkaan? Ja tänne satunnaiseen paikkaan kopioidaan teksti "Kissa"!
Vastaavasti seuraava ohjelma olisi jo oikeampi:
char *jono, st[30];
strcpy(st,"Kissa");
jono = st+5;
strcpy(jono,"tarha"); /* - > st = "Kissatarha" */
Vikana olisi tietysti vielä se, ettei merkkijonojen maksimipituuksien ylittämistä valvota!
14.6.1 Muista aina sijoitus tai malloc tai new
Aina kun ohjelmassa esiintyy osoitintyyppinen muuttuja, pitää muistaa, että ennen sen käyttöä se on alustettu joko sijoituksella toiseen osoitteeseen tai osoittimelle on annettu arvo malloc–funktiolla tai new–operaattorilla (joihin palataan myöhemmin!). Siis aina:
int *osoitin;
/* Aina joko */
/* 1 */ osoitin = &muuttuja;
/* tai */
/* 2 */ osoitin = malloc(...);
// tai
/* 3 */ osoitin = new...
Noudattaako aliohjelman parametrit tätä sääntöä?
void laske_verot(double brutto, double pros,
double *pros_osuus, double *netto)
Kyllä, koska kutsu
laske_verot(10000.0,40.2,&verottajalle,&itselle)
tarkoittaa sijoitusta aliohjelman parametreihin:
brutto = 10000.0
pros = 40.2
pros_osuus = &verottajalle
netto = &itselle
14.6.2 Milloin osoitin?
Yleensä ohjelman muuttajat esitellään tavallisiksi muuttujiksi (paitsi taulukot, jotka ovat aina osoitteita). Osoitinmuuttujia esitellään pääsääntöisesti
- parametrilistoissa
- kun käytetään dynaamisia muuttujia
- kun edetään taulukkoa osoittimen avulla (indeksien sijasta)
- virheviestien (tai sen tyylisten) vakio–osoitteiden saamisessa
14.6.3 Varoitus
Aloittelevalla ohjelmoijalla seuraavannäköinen aliohjelman alku on pääsääntöisesti väärin:
int oma_ali(...)
{
int *a; // aloittelijalla :-(
...
Siis tarkista aina huolellisesti kaikki lohkon alkumerkin jälkeen määritellyt osoitintyyppiset muuttujat!
14.6.4 Kertaustehtäviä
Kirjoita seuraavat funktiot tai aliohjelmat sekä kirjoita kullekin oma testipääohjelma.
Tehtävä 14.139 suurin_kirjeen_paino
Kirjoita funktio int suurin_kirjeen_paino(double rahaa) joka palauttaa mikä on suurin kirjeen paino, joka voidaan lähettää rahamäärällä rahaa.
Olkoon postimaksut:
korkeintaan 50 g 2.10 mk
korkeintaan 100 g 3.40 mk
korkeintaan 200 g 5.20 mk
korkeintaan 500 g 9.30 mk
Tehtävä 14.140 kysy_ika
Kirjoita aliohjelma kysy_ika, joka kysyy henkilön iän ja palauttaa sen parametrinään. Ikää kysytään kunnes iäksi on vastattu luku väliltä 18- 65 vuotta. Aliohjelman nimessä palautetaan tieto siitä, monestiko ikää piti kysyä ennen kuin hyväksytty tulos saatiin. Tehtävä 14.141 palindromi
Kirjoita funktio palindromi, joka palauttaa nimessään tiedon siitä (1=kyllä, 0=ei) onko parametrinä välitetty sana palindromi vai ei.
Tehtävä 14.142 laske_merkin_maarat
Kirjoita funktio laske_merkin_maarat, joka palauttaa nimessään tiedon siitä, monestiko parametrinä välitetty merkki esiintyy parametrinä välitetyssä jonossa (HUOM! ei täysin sama kuin laske_merkit, mikä ero?).
merkki = 's'
jono = "Kissa" - > palauttaa 2
Tehtävä 14.143 vero
Kirjoita aliohjelma vero, joka toimii seuraavasti:
esimerkki
aliohjelma vero (nimessä ei mitään)
parametrit: brutto 20000.0 (syöttö, input)
vero 20.0 (syöttö, input)
- > netto 16000.0 (palautus, output)
- > verottajalle 4000.0 (palautus, output)
Tehtävä 14.144 maara_alennus
Kirjoita funktio maara_alennus, joka laskee tavaroiden hinnan ja myöntää siihen määräalennuksen, mikäli tavaraa ostetaan enemmän kuin tiettyä rajaa (toimii esim. seuraavasti):
esimerkki 1 esimerkki 2
funktio maara_alennus
parametrit: kpl_hinta 10.0 10.0
ostos_maara 3.0 10.0
alennus_raja 5.0 5.0
alennus % 20.0 20.0
nimessä - > 30.0 80.0
Tehtävä 14.145 mjono_ajaksi
Kirjoita muuta_jono_pvmksi matkien aliohjelma mjono_ajaksi. Muuta mjono_ajaksi aliohjelmaa siten, että se toimii seuraavasti:
jono tun min sek sad
"14" - > 14 0 0 0
"14:30" - > 14 30 0 0
"14:30:25" - > 14 30 25 0
"14:30:25.20" - > 14 30 25 20
Tehtävä 14.146 aika_mjonoksi
Kirjoita edelliselle käänteinen aliohjelma aika_mjonoksi. Muuta aika_mjonoksi - aliohjelmaa siten, että välitetään aika- tyypin lisäksi muotoa kuvaava parametri seuraavasti:
0 - > muutetaan muotoon "14:30"
1 - > muutetaan muotoon "14:30:25"
2 - > muutetaan muotoon "14:30:25.20"
Kirjoita testipääohjelma, jossa kysytään kellonaikaa merkkijonoon kunnes ajaksi vastataan q (kannattaa kutsua lue_jono_oletus - aliohjelmaa) ja muutetaan tämä sitten mjono_ajaksi - aliohjelmalla ja tulostetaan aikatyyppi käyttäen apuna aika_mjonoksi - aliohjelmaa. Muuta aika_mjonoksi - aliohjelmaa siten, että muoto- parametrin arvoilla 0 ja 1 suoritetaan katkaisun sijasta pyöristys.
14 30 35 45 - > "14:31"
14 30 29 30 - > "14:30"
14 30 30 00 - > "14:31"
14 30 35 45 - > "14:30:36"
Tehtävä 14.147 cAika
Lisää aiemmin esitettyyn luokkaan cAika metodit jonoksi ja sijoita, jotka mukailevat aliohjelmia mjono_ajaksi ja aika_mjonoksi.
15. Kommentointi ja jakaminen osiin
Mitä tässä luvussa käsitellään?
- ohjelman kommentointi
- ohjelman jakaminen osiin
- ehdollinen kääntäminen
- makefile
- projekti
15.1 Kommentointi
"Löysin vuosi sitten kirjoittamani kolmen rivin aliohjelman. Siinä ei ollut yhtään kommenttia. Miten se hoiti tehtävänsä? Mietin asiaa kaksi päivää ja sitten ymmärsin miksi tehtävä oli triviaali eikä tarvinnutkaan kommenttia." (Vapaa käännös verkossa olleesta kirjeestä / vl).
Kommentoimme jatkossa aliohjelmat huolellisesti. Seuraavana on kuvattu eräs tapa. Tietenkin on miljoona muutakin tapaa, mutta ainakin seuraavan tavan ajatus kannattaa pitää mielessä.
15.1.1 Valmiin kommenttilohkon lukeminen
Aliohjelman kirjoittaminen aloitetaan vaikkapa seuraavan f.c –nimisen tiedoston lukemisella koodin sekaan (seuraavassa kursiivilla kirjoitetut osat eivät ole mukana tiedostossa):
komloh\f.cpp – pohja funktioiden kirjoittamiselle
//***************************************************************************
int //
funktio( // Funktion eri paluuarvot
//
p1 ,// s Selitys Parametrin merkitys
p2 ,// t Selitys
p3 // s Selitys
)
/*
** Funktiolla ... Lyhyt (mutta riittävä) kuvaus siitä, MITÄ funktio tekee
**
** Globaalit: Globaalit muuttujat joita ohjelma tarvitsee
** Muuttuu: Muuttujat (yleensä glob) joita muutetaan ja eivät ole param.listassa
** Syöttö: Mistä saadaan syöttö
** Tulostus: Mihin tulosteet
** Kutsuu: Mitä aliohjelmia kutsutaan (usein tarpeeton rivi)
** Tekijä: Vesa Lappalainen
** Pvm: 26.01.1997
** Algoritmi: Miten funktio tekee tehtävänsä
** Esimerkki: Muutama selventävä esimerkki
––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––*/
{
}
Lohkokommenteissa (kommenttia monta riviä peräkkäin) ** pyritään saamaan alkamaan sarakkeesta 1. Miksikö? Tällöin ohjelman dokumentointia voidaan automatisoida siten, että kerätään kustakin tiedostosta ne rivit, joissa on ** 1. sarakkeessa.
15.1.2 Parametrilistan kommentointi
Tämän jälkeen korjataan funktion tyyppi ja nimi sekä parametrilista (helpointa päällekirjoitustilassa). Tarpeettomat rivit poistetaan tai tarvittaessa kopioidaan rivi uudeksi.
Parametrilistan kommenteissa on esimerkiksi seuraavat symbolit:
s = syöttömuuttuja (tulee aliohjelmaan)
t = tulosmuuttuja (aliohjelma muuttaa tämän arvoa)
s,t = parametri on sekä syöttö, että tulos
Mikäli joku muuttuja on puhtaasti syöttömuuttuja, mutta siitä huolimatta sen arvo muuttuu (esim. jotkin pätkimisaliohjelmat), kannattaa parametri mainita vielä Muuttuu: kohdassa.
15.1.3 Funktion toiminta ja muu esittely
Täytetään lyhyt kuvaus funktion toiminnasta. Täytetään globaalit: yms. kohdat. Kenttien tarkoitus on seuraava:
Globaalit: globaalit muuttujat joita aliohjelma tarvitsee
Muuttuu: ne muuttuvat arvot jotka eivät ole parametrilistassa
= lähinnä globaalit muuttujat (hyvä aliohjelma ei
näitä tarvitse)
Syöttö: pääte, tiedostot
Tulostus: pääte, tiedostot
Kutsuu: mitä EI–tunnettuja aliohjelmia tai makroja tarvitaan
C:n standardikirjastojen kutsuja ei tarvitse luetella
jolleivat ne ole kovin eksoottisia, ei myöskään samassa tiedostossa
tai projektissa olevia "tunnettuja" aliohjelmia
Tekijä: kuka tehnyt (jos triviaali aliohjelma, voidaan
jättää pois)
Pvm: milloin tehty ja myös muutokset
Algoritmi: algoritmin karkea kuvaus ja mahdolliset lähdeviitteet
Esimerkki: esimerkkejä, joilla havainnollistetaan aliohjelman
toimintaa ja saadaan itsellekin kirjattua ylös
se mitä ollaan tekemässä
Näistä tarpeettomat (triviaalit tai tyhjiksi jäävät) rivit poistetaan.
15.1.4 Koodin kommentointi
Itse ohjelmakoodi kommentoidaan seuraavasti:
- selviä C–kielen rakenteita ei saa kommentoida. Ei siis
i=5; /* sijoitetaan i on 5 */ /* TURHA! */
- kuitenkin mikäli lauseella on selvä merkitys algoritmin kannalta, kommentoidaan tämä
i=5; /* aloitetaan puolestavälistä */
ryhmitellään lauseet tyhjien rivien avulla loogisiksi kokonaisuuksiksi. Tällaisen kokonaisuuden alkuun voidaan laittaa kommenttirivi, joka kuvaa kaikkien seuraavien lauseiden merkitystä.
mikäli tekee mieli kommentoida lauseryhmä, kannattaa miettiä voitaisiinko koko ryhmä kirjoittaa aliohjelmaksi. Aliohjelman nimi sitten kuvaisi toimintaa niin hyvin, ettei kommenttia enää tarvittaisikaan. Kuitenkin jos näin suunnitellulle aliohjelmalle tulee iso kasa (liki 10) parametrejä, täytyy asiaa ajatella uudestaan.
muuttujien nimet valitaan kuvaaviksi. Kuitenkin mitä lokaalimpi muuttujan käyttö, sitä lyhyemmäksi nimi voidaan jättää. i ja j sopivat aivan hyvin silmukkamuuttujien nimiksi ja p yms. osoittimen nimeksi (lokaalisti).
globaaleja muuttujia vältetään 'kaikin keinoin'
olioiden ansiosta globaalit muuttujat voidaan yleensä välttää kokonaan!
mikäli globaaleja muuttujia kuitenkin tarvitaan, kasataan ne yhteen struktuuriin
typedef struct {
int jasen_maara;
int nayton_koko;
...
} globaalit_tyyppi;
globaalit_tyyppi GLOBAALIT;
...
GLOBAALIT.jasenmaara=5;
tarvittaessa määritellään useita eri nimisiä globaaleja tietueita.
vakiotyyliset (alustetaan esittelyn yhteydessä eikä ole tarkoitus ikinä muuttaa) globaalit muuttujat on sallittu sellaisenaan ja niiden nimet kannattaa ehkä kirjoittaa isolla.
funktioiden paluuarvolle valitaan tietty tyyli, joka pyritään säilyttämään koko ohjelman ajan. Esimerkiksi 0 = onnistui ja muut virheilmoituksia.
15.2 Omat aliohjelmakirjastot
Aiemmin rakensimme joukon merkkijonojen käsittelyssä tarvittavia apuohjelmia. Nämä aliohjelmat voitaisiin kopioida suoraan myös kerhorekisteriimme. Käytännössä näin ei kuitenkaan kannata tehdä, sillä valmiiksi testatut aliohjelmat olisivat mukana vain turhaan lisäämässä käännösaikaa.
Tämän takia aliohjelmat kirjoitetaan omaksi tiedostokseen, vaikkapa nimelle mjonot.c. Aliohjelmien otsikot (esittelyrivit) ja muut kaikille tarkoitetut määrittelyt kirjoitetaan tiedostoon mjonot.h.
15.2.1 mjonot.h
Tiedosto mjonot.h voisi olla vaikkapa seuraavanlainen:
/* Makro jolla saadaan muuttujan nimi ja koko peräkkäin */
#define N_S (nimi) nimi,sizeof(nimi)
/****************************************************************************/
/* vakiot syötön onnistumiselle */
#define SYOTTO_OK 2
#define EI_MAHDU 1
#define OLETUS 0
#define TIEDOSTO_LOPPU –1
#define VIRHE_SYOTOSSA –2
extern char *VALIMERKIT;
/****************************************************************************/
char *tee_jono(char *);
int f_lue_jono(FILE *, char *, int);
int lue_jono(char *, int );
...
int wildmat(register char *, register char *);
Tämä tiedosto on ehkä helpointa tehdä kopioimalla kaikki alkuperäiset aliohjelmat tiedostoon ja tämän jälkeen tuhoamalla aliohjelmien suoritusosat. On olemassa myös ohjelmia, jotka tekevät tiedostosta näitä prototyyppitiedostoja.
Huomattakoon, että myös seuraavia muotoja voi esiintyä .h –tiedostoissa:
int lue_jono(char *jono, int max_pituus);
...
extern int lue_jono(char *jono, int max_pituus);
...
int lue_jono(char *, int);
...
extern int lue_jono(char *, int);
Funktioiden yhteydessä extern oleminen tai puuttuminen ei kuitenkaan haittaa mitään. Toisin on muuttujien kanssa!
Huom! Nyt funktioiden esittelyjen perään täytyy muistaa laittaa puolipiste!
15.2.2 mjonot.c
Vastaavasti itse tiedosto, jossa aliohjelmat ovat, olisi seuraavan näköinen:
ali\mjonot.c - aliohjelmia merkkijonojen käsittelyyn
/****************************************************************************/
/*
** M J O N O T . C
**
** Yleisiä merkkijonojen käsittelyyn liittyviä aliohjelmia.
**
...
** Aliohjelmat:
** tee_jono – luo uuden merkkijonon jonne jono kopioidaan
** kopioi_jono – kopioi kork. annetun määrän merkkejä
...
** merkkijonossa
** wildmat – vertaa onko sana == maski, missä maskissa
** voi olla jokeri–merkkejä
...
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "mjonot.h"
char *VALIMERKIT=" .,–;:?!";
/****************************************************************************/
char /* = jonon kopion osoite */
*tee_jono( /* NULL = ei voida kopioida */
char *jono /* s Kopioitava merkkijono */
)
...
... Kuten ennenkin ...
...
/****************************************************************************/
int /* */
wildmat( /* 0 = jono täsmää maskiin */
/* 1 = jono ei täsmää maskiin */
const register char *s ,/* s Tutkittava jono */
const register char *m /* s Maski, johon jonoa verrataan */
)
/*
** Funktiolla tutkitaan täsmääkö annettu jono verrattavaan maskiin.
** Maski saa sisältää seuraavia erikoismerkkejä:
** * vastaa 0-n merkkiä
** ? vastaa mitä tahansa yhtä merkkiä
**
** Algoritmi: Kysymysmerkki ja tavallinen kirjain normaalisti
** Jos tulee vastaan tähti joka ei ole jonon lopussa,
** niin ongelmahan on oikeastaan
** (koska tähän asti kaikki on ollut oikein)
** "Onko loppujono sama kuin toisen jonon loppu JOSTAKIN
** kohdasta alkaen"?
** Siis kokeillaan sovittaa loppujonoa aliohjelman itsensä
** (rekursio) avulla kaikkiin mahdollisiin loppupaikkoihin.
** Esimerkki: s = "Kissa" m = "*ss*" -> 0
** = "*ss" -> 1
** Vika alkuperäisessä algoritmissa:
** Jos m="*a" ja s="" ja s[1]!=0 (mikä tietysti sallittua!)
** niin vastaus oli 0.
** Korjattu 29.1.1994/vl muuttamalla rekursion jälkeinen
** if (!*++s) return 1;
** muotoon
** if (!*s || !*++s) return 1;
----------------------------------------------------------------------------*/
{
while (*m) { /* Jos kokeiltavaa jonoa on jäljellä */
if (*m == '?') { /* Jos kysymysmerkki, niin kaikki kelpaa */
if (!*s) return 1; /* paitsi jos jono on loppunut! */
}
else if (*m == '*') { /* Jos * niin katsotaan onko viimeinen */
if (*++m) /* Jollei * viimeinen, niin kokeillaan */
while (wildmat(s, m)) /* loppujonoa jokaiseen eri paikkaan. */
if (!*s || !*++s) return 1;/* Jos jono loppuu kesken ei täsmää! */
return 0; /* Muuten samat (* viimeinen tai loppujono*/
} /* täsmäsi) */
else if (*s != *m) /* Jos samat merkit niin tietysti OK! */
return 1;
s++; m++; /* Kummassakin jonossa eteenpäin */
}
return *s; /* Jos jono loppui yhtäaikaa, niin OK! */
}
...
Olemme lisänneet kirjastoon muutamia uusia aliohjelmia, joiden tarpeellisuuden totesimme jo kerhorekisterin suunnitteluvaiheessa.
Kirjastossa on mm. wildmat–aliohjelma merkkijonojen samaistamiseksi, kun jonossa saa esiintyä jokerimerkkejä * ja ? (vrt. MS–DOS).
aluksi s = "Kissa"; *s='K'
s++ –> s = "issa" *s='i'
s++ –> s = "ssa" *s='s'
15.2.3 Kirjaston testaus
Kirjaston testausta varten voimme kirjoittaa vaikkapa ohjelman t_mjonot.c:
komloh\t_mjonot.c – testiohjelma merkkijonokirjastolle
#include <stdio.h>
#include "mjonot.h"
/****************************************************************************/
/* testiohjelmat: */
void lue_jono_testi(void)
{
/* täytä! */
}
int f_lue_jono_testi(void)
{
/* täytä! */
}
void lue_kok_testi(void)
{
/* täytä! */
}
void lue_jono_oletus_testi(void)
{
int paluu;
char st_ole[50],st[50] = "Ankka Aku";
printf("Testi loppuu, kun painat ^Z.\n");
printf("1234567890123456789012345678901234567890\n");
do {
strcpy(st_ole,st);
paluu=lue_jono_oletus("Anna jäsenen nimi",19,33,st_ole,st,50);
poista_tyhjat(st);
jono_alku_isoksi(st);
} while ( paluu >= OLETUS );
}
void wild_testi(void)
{
char jono[80],maski[80];
while (!feof(stdin)) {
printf("Anna jono ja maski>");
scanf("%s %s",jono,maski); /* Älä käytä oikeasti!!!! */
printf("%d <– %s %s\n",wildmat(jono,maski),jono,maski);
}
}
int main(void)
{
#if 0
lue_jono_testi();
#endif
#if 0
f_lue_jono_testi();
#endif
#if 0
lue_kok_testi();
#endif
#if 0
lue_jono_oletus_testi();
#endif
#if 1
wild_testi();
#endif
return 0;
}
Huomattakoon, että nyt täytyy olla lainausmerkit lauseessa
#include "mjonot.h"
Jotta ohjelma voidaan kääntää, tarvitsemme projektin tai MAKEFILEn. Kunnes olemme ne käsitelleet, voidaan "kerho.h" tilapäisesti korvata "kerho.c". Normaalisti c–tiedostoja EI SAA "includeta"!
15.3 Ehdollinen kääntäminen
Edellisessä pääohjelmassa testattava osa voidaan valita muuttamalla #if –lauseissa esiintyviä 0 ja 1:iä.
Kyseessä on esiprosessorin direktiivi, jolla määrätään minkä osan esiprosessori antaa varsinaiselle kääntäjälle käsiteltäväksi.
Tällä periaatteella voidaan rakentaa ohjelmia, joissa tekstit tulevat eri kielillä (englanti, ruotsi, suomi) sen mukaan, mitä ohjelmassa on määritelty.
Samalla tempulla teimme myös lue_merkki –aliohjelman toimimaan joko Turbo–C:ssä tai standardi ANSI–C:ssä sen mukaan onko määritelty sana __TURBO__ (huomaa kaksi _ kummallakin puolella!):
#ifdef __TURBOC__
# define GETCH
# include <conio.h>
#endif
...
char lue_merkki(void)
{
#ifdef GETCH
... /* lukeminen conio–kirjaston avulla */
#else
... /* lukeminen standardikirjaston avulla */
#endif
}
Usein kääntäjään liittyy kääntäjän määrittelemiä symboleja, joiden olemassaoloa testaamalla voidaan tehdä laiteriippuvia osia. Juuri tällainen oli __TURBOC__. Ehtona voi olla myös jokin oma sana kuten edellä GETCH. Usein itse määriteltyjä sanoja käytetään estämään .h–tiedoston moninkertaiset esiintymät:
ali\mjonot.h - estetään monikertainen käyttö
...
#ifndef MJONOT_H
#define MJONOT_H
... /* varsinainen h–tiedoston sisältö */
#endif
Nyt voidaan pääohjelmassa kirjoittaa
#include "mjonot.h" /* Lukee mjonot.h –tiedoston kokonaan */
#include "mjonot.h" /* Ei tee mitään koska MJONOT_H määr. */
15.4 Header–tiedostot ja prototyypit
Kutakin isompaa aliohjelmakokonaisuutta varten kirjoitetaan oma header–tiedosto (.h –tiedosto). Kääntäjän toiminta on sellainen, että kun kääntäjä tulee esimerkiksi lauseeseen
int a,i;
...
a = ynnaa_yksi(i);
voidaan suluista päätellä aina, että kyseessä on aliohjelman kutsu. Loppu onkin kääntäjälle vaikeampaa. Aliohjelmalle pitäisi välittää oikean tyyppinen parametri. Kääntäjä voi yrittää arvata, että esimerkin aliohjelma olisi muotoa
int ynnaa_yksi(int luku);
koska sille välitetään parametrinä int –tyyppinen luku. Mikäli arvaus osoittautuisi vääräksi ja aliohjelma olisikin esitelty
double ynnaa_yksi(double luku);
olisi käännös mennyt väärin arvauksen kohdalta, koska kokonaisluku ja reaaliluku talletetaan aivan eri tavalla. Virhe voidaan normaalisti välttää sillä, että kirjoitetaan (esitellään) aliohjelma ennen sen käyttöä.
Aliohjelmakirjastojen kanssa on hankalampaa. Jottei aliohjelmia tarvitsisi esitellä useita kertoja, on aliohjelman parametrien tyypit mahdollista esitellä prototyypillä
int lue_jono(char *jono, int max_pituus);
Kun kaikkien tarvittavien aliohjelmien prototyypit laitetaan .h –tiedostoon, ja tämä tiedosto luetaan esiprosessorin
#include "mjonot.h"
direktiivillä käännöksen aikana koodin mukaan, saadaan kullekin aliohjelmalle oikea tyyppi.
Vastaavasti koko ohjelmaan saattaa tulla eri tiedostojen välisiä globaaleita muuttujia, ja kuitenkin muuttujan saa esitellä vain yhdessä paikassa. Jotta kaikki tiedostot näkisivät globaalit muuttujat, voidaan ne esitellä header–tiedostossa extern –määreellä. Itse varsinainen muuttuja varataan sitten siinä TASAN YHDESSÄ tiedostossa, johon se "eniten" kuuluu. Tällöin muuttujaan ei tule extern –määritystä.
kerho.h:
extern const char *VERSIO;
...
kerho.c
const char *VERSIO = "18.2.1992"
..
muissa tiedostoissa esimerkiksi:
printf("Versio %s\n",VERSIO);
Siis yleensä header–tiedostoon tulee
#define – vakiot
typedef – esitellyt tietueet
#define – esitellyt yleiskäyttöiset makrot
extern – muuttujien esittelyt (hyi!)
funktioiden prototyypit
luokkien esittelyt
Header–tiedostoon EI yleensä kirjoiteta varsinaisia kääntyviä lauseita, kuten muuttujien varaamista tai ohjelmakoodia (paitsi inline–funktioiden koodi)!
15.5 Kääntäminen ja linkittäminen
Header –tiedosto oli siis kääntäjää varten.
Kukin kokonaisuuteen kuuluva osa voidaan kääntää erikseen. Lopuksi nämä osat linkitetään yhteen, jolloin muodostuu haluttu valmis ohjelma.
Ongelmana on sitten muistaa kääntää uudestaan aina kaikki muuttuneet osat. Tietysti voisimme aina varmuuden vuoksi kääntää kaikki osat, mutta tämä veisi turhaa aikaa.
Tätä varten on MAKE–niminen ohjelma, jolle kirjoitetaan ohjetiedosto siitä, mitkä tiedostot muodostavat valmiin työmme ja miten eri tiedostot riippuvat toisistaan.
Esimerkiksi mikäli mjonot.c tiedostoon tulee muutos, tarvitsee se kääntää sekä suorittaa linkitys uudestaan. Mikäli mjonot.h muuttuu, pitää sekä mjonot.c ja t_mjonot.c kääntää uudelleen.
15.5.1 Make–ohjelma
Esimerkiksi HP–UX -ympäristössä kerho–ohjelman tarkistukset suorittava versio voitaisiin kääntää seuraavan tiedoston (kirjoitettu nimelle makefile) avulla:
komloh\makefile - kerho-ohjelman C-version kääntäminen Unixissa
# makefile kerho-ohjelmaa varten
kerho: kerho.o kerhorak.o kerhoets.o kerhotar.o kerhoali.o\
kerhotal.o kerhoopt.o kerholra.o \
mjonot.o pvm.o help.o
cc -Aa -o kerho *.o -lm
.c.o:
cc -c -Aa $*.c
Huomattakoon, että cc-alkuisten (c–kääntäjän kutsu Unixissa) rivien on oltava sisennetty TAB-näppäintä käyttäen. Seuraavassa hieman selityksiä:
kerho: – tiedosto kerho riippuu näistä .o (obj) tiedostoista
cc ... – jos jossakin .o:ssa on uudempi päiväys kuin kerho
tiedostossa, luodaan uusi kerho tällä komennolla
–Aa = ANSC–C käännös
–o = tulostiedosto (output)
–lm = linkitetään matematiikkakirjasto
.c.o: – .c tiedostoista tehdään .o tiedosto seuraavasti
jos .c:n päiväys on uudempi kuin .o:n.
Käännös ja ajaminen suoritetaan seuraavasti
$ make[RET]
...
$ kerho[RET]
...
Tällä ohjelmalla...
15.5.2 Projektit
Useissa integroiduissa ympäristöissä on mahdollisuus tehdä projekteja, eli ilmoittaa mitkä ovat työhömme vaikuttavia tiedostoja. Usein näiden projektien käyttäminen on helpompaa kuin MAKE–tiedostojen tekeminen.
Projektin merkitys on vastaava kuin MAKE-tiedostonkin: pitää kirjaa siitä, mitkä tiedostot kuuluvat kokonaisuuteen. Toisaalta projektitiedostoa tarvitaan erityisesti linkityksessä.
Mikäli merkkijonontestaus esimerkkimme mjonot.c käännettäisiin irrallisena, syntyisi tiedosto mjonot.obj. Tähän ei vielä projektia tarvita. Vastaavasti voidaan kääntää pääohjelma t_mjonot.c. Vieläkään ei projektia tarvittaisi. Ongelmaksi tuleekin linkitys. Päämoduulissa viitataan aliohjelmaan lue_jono. Mistä linkittäjä arvaisi etsiä tätä aliohjelmaa tiedostosta mjonot.obj?
Tämä tieto löytyy projektista, johon esimerkissämme mainittaisiin ne C–tiedostot, jotka kääntämisessä tarvitaan, eli
t_mjonot.c
mjonot.c
Mikäli C–kielistä koodia ei ole olemassa, voidaan projektiin usein ilmoittaa myös jo käännettyjä tiedostoja, eli esimerkiksi
t_mjonot.c
mjonot.obj
Siis linkittäjä etsii aliohjelmia ja globaaleita muuttujia projektissa mainituista tiedostoista C–kielen standardikirjastojen lisäksi.
Esimerkiksi Turbo–C:ssä valitaan aluksi uuden projektin luonti. Tämän jälkeen projekti–ikkunaan osoitetaan ne *.c tiedostot, joita haluamme käyttää. Voidaan myös lisätä *.obj tai *.lib tiedostoja, mikäli lähdekielisiä tiedostoja ei ole.
Header–tiedostoja (*.h) ei koskaan laiteta projektiin!
Itse kääntäminen suoritetaan sitten normaalisti.
Projektin perusteella kääntäjä ensin kääntää kaikki ne .c–tiedostot, joista ei vielä ole .obj –tiedostoa tai .obj–tiedoston päiväys on vanhempi kuin .c–tiedoston. Tämän jälkeen linkitetään kaikki projektissa mainitut .obj–tiedostot yhdeksi .exe–tiedostoksi.
Huomattakoon että projektissa tulee olla tasan yksi .c–tiedosto, jossa on main–funktio.
15.6 Kerho-ohjelman jako osiin (ks. jako.15–hakemisto)
Kirjoittamisen ja ylläpidon kannalta voi olla helpompi jakaa ohjelma muutamaan loogiseen osaan. Nyt on valmiiksi kirjoitettuna jäsenrekisterin runko–osa. Tällä rungolla voidaan testata tietorakenteiden toimivuus: Rungossa meillä on käytössä alkeellinen näytölle tulostava aliohjelma.
Rungossa osa aliohjelmista on hyvin yleisluonteisia tulostus/luku aliohjelmia ja nämä kaikki voitaisiin siirtää vaikkapa tiedostoon nimeltä ioali.c. Muuten jako kannattaa tehdä luokkapohjaisesti:
kerhomain.cpp - pääohjelma
jasen.cpp - yksittäisen jäsenen käsittely
jasenet.cpp - jäsenistön käsittely
kerho.cpp - kerho kokonaisuudessaan
naytto.cpp - näyttöön liittyvä kerhon käsittely
Vastaaviin .h–tiedostoihin tulee kunkin luokan esittely
class cLuokka {
...
};
Varsinainen todellinen koodi tulee sitten vastaaviin .cpp –tiedostoihin.
Lisäksi käytämme tietysti valmiita kirjastoja sekä aikaisemmin tehtyä merkkijonokirjastoa mjonot.
On turha toivo, että keksisimme kaikki määritykset ja aliohjelmat kerralla. Tehtävää täytyy hahmotella palanen kerrallaan. Kun jokin homma tuntuu venyvän liian pitkäksi tai monimutkaiseksi, määrittelemme tehtävän useampaan alatoimintoon ja toteutamme nämä toiminnot sitten aliohjelmina/luokkina. Aliohjelmien/metodien parametrit saattavat vielä myöhemmin muuttua, kun huomataan saman tehtävän käyvän sekä tähän että tuohon tehtävään. Esimerkiksi etsiminen ja selailu käy samalla myös korjailuun ja poistoon. Ainoana erona on, että korjaus– ja poistonäppäimet eivät ole pelkässä etsimisessä sallittuja.
16. Dynaaminen muistinkäyttö
Mitä tässä luvussa käsitellään?
Dynaaminen muistinhallinta
Dynaamiset taulukot
Hajottaja (destructor)
STL-vektori
Syntaksi:
Dyn.muut.luonti C: pMuuttuja = malloc(koko);
pTaulukko = malloc(alkioiden_lkm * alkion_koko); // ei alus
pTaulukko = calloc(alkioiden_lkm,alkion_koko); // alust. 0
C++: pOlio = new cLuokka; // oletusmuodostaja
pOlio = new cLuokka(muodostajan_param_lista);
pOlioTaul = new cLuokka[alkioiden_lkm];
Koon vaihto C: pTaulukko = realloc(pVanha,uusi_koko_tavuina);
Hävittäminen C: free(pMuuttuja);
free(pTaulukko);
C++: delete pOlio;
delete [] pOlioTaul;
Hajottaja ~cLuokka();
Yhdessä käytettävät funktiot tai operaattorit:
C: malloc - calloc - realloc - free
C++: new - delete
new [] - delete []
Olemme oppineet varaamaan muuttujia esittelyn yhteydessä. Muuttujia voidaan luoda (= varata muistitilaa) myös ohjelman ajon aikana. Tämä on tarpeellista erityisesti silloin, kun ohjelman kirjoittamisen aikana ei tiedetä muuttujien määrää tai ehkei jopa edes kokoa (taulukot).
16.1 Muistin käyttö
Karkeasti ottaen tavallisen ohjelman muistinkäyttö näyttäisi ajan funktiona seuraavalta:
^
muistin │
kaytto │ ┌───────────────────────────────┐
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
└────┴───────────────────────────────┴──────>
ohjelman ohjelman
alku loppu
Edellinen kuva on hieman yksinkertaistettu, koska "oikeasti" aliohjelmien lokaalit muuttujat (automaattiset muuttujat) syntyvät aliohjelmaan tultaessa ja häviävät aliohjelmasta poistuttaessa. Näin ollen käytetyn muistin yläraja vaihtelee sen mukaan mitä aliohjelmia on kesken suorituksen.
Dynaamisia muuttujia voidaan tarvittaessa luoda ja kun muistitilaa ei enää tarvita, voidaan vastaavat muuttujat vapauttaa:
^
muistin │
kaytto │ ┌──┐
│ ┌────┐ │ │ ┌───┐
│ │ │ │ │ ┌─┘ │
│ ┌──┘ └────┘ └───┘ └─────┐
│ │ │
│ │ │
└────┴──┼────┼────┼──┼───┼─┼───┼─────┴──────>
ohjelman malloc calloc malloc ohjelman
alku free free realloc loppu
free
Näin muistin maksimimäärä saattaa pysyä huomattavasti pienempänä kuin ilman dynaamisia muuttujia. Idea on siis siinä, että muistia varataan aina vain sen verran, kuin sillä hetkellä tarvitaan. Kun muistia ei enää tarvita, vapautetaan muisti.
Ajonaikana luotaviin muuttujiin tarvitaan osoitteet. Nämä osoitteet pitää sitten tallettaa johonkin. Talletus voitaisiin tehdä esimerkiksi taulukkoon tai sitten alkioista pitää muodostaa linkitetty lista.
16.2 Dynaamisen muistin käyttö C–kielessä
Vaikka seuraavassa käsitelläänkin asioita C:n ja C++:n ominaisuuksia rinnakkain, pitää muistaa käyttää samaan muuttujaan kohdistuen aina "saman sarjan" operaatioita (malloc-free tai new-delete, muttei esimerkiksi: malloc–delete).
16.2.1 malloc, C
Tilaa voidaan varata malloc–funktiolla. Funktiolle viedään parametrinä haluttu koko tavuina ja funktio palauttaa muistista löytyneen alueen alkuosoitteen.
Mikäli tilaa ei saada allokoitua, palautetaan NULL. Tämä pitää muistaa AINA tarkistaa!
Apuna alkion koon laskemisessa käytetään usein käännösaikaista operaattoria sizeof (ks. sizeof), joka palauttaa parametrinsä koon.
pKokonaislukuTaulu = malloc(20*sizeof(int));
Esimerkiksi kerhon jäsenten osoitintaulukko voitaisiin luoda seuraavasti:
runko.1\kerho.cpp - jäsentaulukon luominen
const char *cKerho::luo_taulukko(int koko)
{
jasenet = (cJasen **)malloc( koko * (sizeof(cJasen *)) );
jasenia = 0;
max_jasenia = 0;
if ( jasenet == NULL ) return EI_VOI_LUODA;
max_jasenia = koko;
return NULL;
}
Huomautus! malloc ei alusta varattua muistia mitenkään!
16.2.2 calloc, C
calloc toimii lähes kuten malloc, mutta se on tarkoitettu pääasiassa taulukoiden varaamiseen. callocille ilmoitetaan taulukon alkioiden määrä ja koko (huomaa 2 parametriä, mallocissa vain 1). calloc alustaa kunkin taulukon alkion nollia täytteen. Ei kuitenkaan ole syytä uskoa, että osoitintaulukko tulisi täyteen NULL osoittimia tai reaalilukutaulukko täytteen 0.0 lukuja. Toisaalta merkkijonoista tulee tyhjiä ja kokonaislukutaulukoista täynnä 0:ia olevia taulukoita.
Esimerkiksi jäsentaulukko voitaisiin luoda myös kutsulla
jasenet = (cJasen **)calloc( koko , (sizeof(cJasen *)) );
16.2.3 free, C
Varattu muistitila pitää aina vapauttaa, kun sitä ei enää tarvita. Ohjelman lopuksi tietenkin kaikki ohjelman aikana varattu muistitila vapautuu. Kuitenkin usein aliohjelmien pitää varata itselleen työtilaa ja aliohjelman lopuksi tämä työtila pitää vapauttaa. Tällöin aliohjelmasta ei voida poistua return –lauseella, vaan pitää tehdä goto vapauta tai vastaava hyppy aliohjelman loppuun.
Kun jäsenistöstä poistetaan jäsen, voidaan tämä tehdä vaikkapa seuraavasti:
runko.1\kerho.cpp - jäsenistön poistaminen
void cKerho::poista_taulukko()
{
if ( max_jasenia > 0 ) free(jasenet);
max_jasenia = 0;
}
16.2.4 realloc, c
Funktioilla malloc tai calloc varattua muistitilaa voidaan tarvittaessa muuttaa realloc –funktiolla. Funktiolle viedään parametrinä muistitilan osoite ja haluttu uusi koko. Käytännössä realloc voi toimia siten, että se ensin varaa kokonaan uuden muistitilan ja tämän jälkeen kopioi vanhan muistitilan vastaavan osan (kaikki tai osan jos pienennetään) uuteen paikkaan. Funktio palauttaa uuden paikan osoitteen tai NULL, mikäli koon muuttaminen ei onnistu.
Jäsenen lisäyksessä voisimme tehdä myös seuraavasti, eli kasvatettaisiin jäsenistön kokoa 50%, mikäli jäsenistö tulee täyteen (cKerho jaettiin kahteen osaan: cKerho ja cJasenet):
talletus.2\jasenet.cpp - taulukon koon kasvattaminen
//----------------------------------------------------------------------------
const char *cJasenet::kasvata_kokoa()
/*
** Yritetään allokoida uutta tilaa 50% maksimimäärään nähden lisää.
** Paitsi jos vanha koko on 1, niin kasvatetaan 2:ksi.
** Jos vanha tila on 0, niin tehdään uusi tila.
----------------------------------------------------------------------------*/
{
if ( max_lkm <= 0 ) return luo_taulukko(2);
int uusi_koko = 3*(max_lkm)/2; if ( uusi_koko <= 1 ) uusi_koko=2;
cJasen **uusi_tila = (cJasen **)realloc(alkiot,uusi_koko*sizeof(cJasen *));
if ( uusi_tila == NULL ) return LIIKAA_ALKIOITA;
alkiot = uusi_tila;
max_lkm = uusi_koko;
return NULL;
}
...
if ( jasenia >= max_jasenia ) virhe = kasvata_kokoa();
if ( virhe ) return virhe;
16.2.5 Ole varovainen reallocin kanssa
reallocin kanssa on kuitenkin oltava erittäin huolellinen, koska kaikki allokoitavassa osassa olevat kentät saattavat siirtyä uuteen paikkaan. Mikäli meillä on osoittimia allokoitavan alueen sisälle, on niillä reallocin jälkeen täysin väärät arvot!
Siis alkuvaiheessa realloc kannattaa ehkä unohtaa, mutta periaatteessa sillä voitaisiin korjata väärää kokomäärittelyä. Ja voidaanhan se aina lisätä jälkeenpäin kuten edellisessä esimerkissä!
C++:ssa ei ole vastaavaa funktiota. Tämä olisikin osaltaan hankala toteuttaa, koska luomisessa pitää kutsua olion (olioiden) muodostajaa ja vastaavasti poistamisessa hajottajaa. realloc–tilanteessa voidaan koko muistialue joutua siirtämään toiseen kohti muistia ja kloonaamaan sitten sisällöt sinne. Olioiden tapauksessa tämä kloonaaminen ja sitten vanhojen poisto ei kuitenkaan ole aina yksikäsitteistä ja näin kielen tekijät ovat päätyneet siihen, että reallocia vastaavan toiminnon tekeminen jätetään ohjelmoijalle itselleen.
16.3 Dynaamisen muistin käyttö C++:ssa
16.3.1 new, C++
C++:ssa tilan varaamisen kannattaa ilman muuta käyttää new–operaattoria. Tässä on se suuri etu, että luonnin yhteydessä kutsutaan kunkin luotavan olion muodostajaa ja näin mikään syntyvistä olioista ei jää ilman alkuarvoa. Ainoastaan luotaessa perustietotyyppien mukaisia muuttujataulukoita, voidaan puolustella mallocin käyttöä, tällöinkin vain jos taulukon kokoa pitää jälkeenpäin muuttaa.
Esimerkiksi uusi jäsen lisättäisiin seuraavalla aliohjelmalla:
runko.1\kerho.cpp - jäsenen lisääminen
const char *cKerho::lisaa(const cJasen &jasen)
{
cJasen *uusi_jasen;
if ( jasenia >= max_jasenia ) return LIIKAA_JASENIA;
uusi_jasen = new(nothrow) cJasen(jasen); // uudelle jäsenelle jäsenen tiedot
if ( uusi_jasen == NULL ) return EI_SAA_JASENTA;
jasenet[jasenia] = uusi_jasen;
jasenia++;
return NULL;
}
Itse asiassa edellä on kutsuttu Jäsen-luokan kopiointimuodostajaa (copy constructor), joka luo uuden olion ja tekee siitä samalla sisällöltään samanlaisen kuin muodostajan parametrinä viety olio. Kopiointimuodostajan parametrilistassa on tasan yksi parametri ja sen tyyppi on sama kuin luokan tyyppi.
16.3.2 try-catch, C++
Edellisessä esimerkissä on kielletty new-operaattoria heittämästä poikkeusta. Tällöin jos muistia ei saada, palautetaan 0-osoitin. Usein yleisempi tapa tehdä sama asia on käsitellä poikkeus:
try {
uusi_jasen = new cJasen(jasen);
}
catch (std::bad_alloc) {
return EI_SAA_JASENTA;
}
16.3.3 delete, C++
Jos olio on luotu new–operaattorilla, pitää se poistaa delete–operaattorilla:
void cKerho::poista_jasenet()
{
for (int i=0; i<jasenia; i++)
delete jasenet[i];
jasenia = 0;
}
16.3.4 Taulukon luominen new [] ja tuhoaminen delete []
Jos new–operaattorilla on luotu taulukko (luonnissa käytettiin hakasulkeita), pitää vastaavasti tuhoaminen suorittaa delete–operaattorin taulukkoversiolla
pMonta = new cLuokka[20];
...
delete [] pMonta;
Käytettäessä delete–operaattoria, kutsutaan jokaisen tuhottavan olion hajottajaa (ks. vähän myöhemmin). Tällä on se etu, että olion poistuessa muistista se voi samalla siivota mahdolliset muut jäljet oliosta. Esimerkiksi graafinen olio voi pyyhkiä itsensä pois näytöltä samalla kun se hävitetään.
16.4 Dynaamiset taulukot
Kerhon jäsenrekisterissä käytettiin osoitintaulukkoa dynaamisesti. Vastaavan rakenteen tarve tulee usein ohjelmoinnissa. Tällöin tulee aina vastaan ongelma: montako alkiota taulukossa on nyt ja montako sinne mahtuu? Jäsenrekisterissä tämä oli ratkaistu cJasenet–luokassa tekemällä sinne kentät, joista nämä rajat selviävät.
Tavallinen taulukkokin voidaan usein esitellä vastaavasti
dyna\taul_d.c - esimerkki dynaamisesta taulukosta
typedef struct {
int max_koko;
int lkm;
int *alkiot;
} Taulukko_tyyppi;
...
Taulukko_tyyppi luvut;
luvut.max_koko = 7;
luvut.alkiot = calloc(7,sizeof(luvut.alkiot[0]));
luvut.alkiot[0] = 0; luvut.alkiot[1] = 2;
luvut.lkm = 2;
Taulukko_tyyppi luvut
┌─────┐
max_koko │ 7 │ 0 1 2 3 4 5 6
lkm │ 2 │ ┌──┬──┬──┬──┬──┬──┬──┐
*alkiot │ o──┼──────>│ 0│ 2│ ?│ ?│ ?│ ?│ ?│
└─────┘ └──┴──┴──┴──┴──┴──┴──┘
Tällaisen taulukon kuljettaminen parametrinä on helppoa, kun se voidaan välittää vain yhtenä parametrinä.
Viittausten puolesta samanlainen vastaava staattinen esittely olisi:
dyna\taul_s.c - staattinen taulukko
typedef struct {
int max_koko;
int lkm;
int alkiot[7];
} Taulukko_tyyppiS;
...
Taulukko_tyyppiS luvut = { 7, 2, {0,2} };
Taulukko_tyyppiS luvut
┌─────┐
max_koko │ 7 │
lkm │ 2 │
├──┬──┼──┬──┬──┬──┬──┐
alkiot │ 0│ 2│ ?│ ?│ ?│ ?│ ?│
└──┴──┴──┴──┴──┴──┴──┘
0 1 2 3 4 5 6
Staattisessa esittelyssä taulukko tulisi tietueen osaksi. Kuitenkin kummassakin tapauksessa viitataan taulukon yksittäiseen alkioon muodossa luvut.alkiot[3]=5;.
Staattisen esittelyn "hyvä" puoli olisi siinä, että tällainen taulukko voitaisiin sijoittaa toiseen taulukon yhdellä käskyllä:
Taulukko_tyyppiS apu,luvut = { 7, 2, {0,2} };
apu = luvut;
Tietysti sijoitus onnistuu dynaamisessakin tapauksessa, mutta tulos saattaa olla muuta kuin on haluttu! Miksi?
Viimeksi esiteltyä staattistakin esittelyä voidaan käsitellä dynaamisesti luomalla muuttujat luvut dynaamisesti. Koska C–kielessä ei useinkaan ole indeksitarkistusta, niin luomisessa malloc –funktiolle voidaan "valehdella" alkion koko:
Taulukko_tyyppiS *luvut;
...
luvut = malloc( sizeof(Taulukko_tyyppiS) + 4*sizeof(int) );
luvut–>max_koko = 7 + 4;
...
Tosin koko taulukon sijoittaminen yhdellä käskyllä ei enää onnistu tämän tempun jälkeen! Miksi?
16.5 Dynaamiset taulukot C++:ssa
C++:ssa edällä mainittu dynaaminen taulukko voidaan toteuttaa käyttäjän kannalta todella joustavaksi:
dyna\taul_d.cpp -esimerkki dynaamisesta taulukosta
#include <iostream.h>
#include <iomanip.h>
class cTaulukko {
int max_koko;
int lkm;
int *alkiot;
public:
cTaulukko(int akoko) {
max_koko = 0;
alkiot = new(nothrow) int[akoko];
if ( alkiot ) max_koko = akoko;
lkm = 0;
}
~cTaulukko() { if ( max_koko ) delete [] alkiot; max_koko = 0; }
int lisaa(int luku) {
if ( lkm >= max_koko ) return 1;
alkiot[lkm] = luku;
lkm++;
return 0;
}
void tulosta(ostream &os=cout) const;
}; // HUOM! Luokasta puuttuu vielä kopionmuodostin ja kopionsijoitus!!!
void cTaulukko::tulosta(ostream &os) const
{
int i;
for (i=0; i < lkm; i++)
os << setw(5) << alkiot[i];
os << endl;
}
int main(void)
{
cTaulukko luvut(7);
luvut.lisaa(0); luvut.lisaa(2); // Ilo on täällä!!!
luvut.tulosta();
return 0;
}
Edellä olemme käyttäneet muutamia C++:n aikaisemmin käsittelemättömiä ominaisuuksia:
16.6 Hajottaja (destructor)
Muodostaja ja hajottaja (destructor) ovat eräs olio–ohjelmoinnin kulmakiviä. C++:n lisäetuna (?) on vielä automaattisesti kutsuttavat muodostajat ja hajottimet. Eli esimerkiksi edellisessä esimerkissä (taul_d.cpp) kutsutaan automaattisesti olion luvut hajottajaa silloin, kun olion vaikutusalue lakkaa, eli poistutaan tässä tapauksessa pääohjelmasta.
Hajottaja on parametritön ja tyypitön metodi, jonka nimi on ~Luokan_nimi. Vaikka muodostajia saattoi olla useita, on hajottajia aina VAIN YKSI luokkaa kohti.
Jos luokkaa on mahdollista periä, pitää hajottaja esitellä virtuaaliseksi.
16.6.1 Jäsenistön poisto
Esimerkiksi kerhossa on jäsenistö poistettava kun kerho lakkaa olemasta:
runko.1\kerhohar.cpp - hajottaja
class cJasenet {
...
void poista_kaikki() { poista_alkiot(); poista_taulukko(); }
...
~cJasenet() {
if ( muutettu ) talleta();
poista_kaikki();
}
...
};
16.7 Tietovirta parametrinä ja oletusparametri
Metodi tulosta on esitelty
void tulosta(ostream &os=cout) const;
Näin voidaan tulostusvaiheessa valita mille laitteelle tulostetaan. Koska oletuksena on cout, tulostetaan näytölle jos kutsu on muodossa:
luvut.tulosta();
Tiedostoon tulostettaisiin esimerkiksi:
ofstream fo("luvut.dat");
...
luvut.tulosta(fo);
Oletusparametri tarkoittaa sitä, että mikäli kutsussa ei anneta jollekin parametrille arvoa, ”sijoitetaan” sille esittelyssä oleva oletusarvoa. Ominaisuutta käytetään erittäin usein muodostajan yhteydessä.
Tietysti sama asia voidaan hoitaa funktioiden ja metodien kuormituksen avulla:
void tulosta(ostream &os) const; // tulostaa tiedostoon os
void tulosta() const; // tulostaa näyttöön
mutta tästä seuraa enemmän kirjoittamista.
16.8 STL – kirjasto tietorakenteita ja algoritmeja
Koska erilaisten dynaamisten tietorakenteiden (vrt. taul_d.cpp) käyttö on erittäin yleistä, on C++ standardiin lisätty joukko tietorakenteita. Jotta nämä tietorakenteet pystyisivät tallentamaan erilaisia tyyppejä ja silti olisivat samalla tyyppiturvallisia, on rakenteet toteutettu mallien (template) avulla. Tästä tulee kirjaston nimikin: Starndard Template Library (STL). STL sisältää myös lukuisan joukon algoritmeja tietorakenteiden käsittelyyn. Näistä myöhemmin esimerkki lajittelun yhteydessä.
Meidänkin esimerkissämme cJasenet ja cHarrastukset eroavat toisistaan vain hyvin vähän. Ero on itse asiassa muutaman cJasen –sanan muuttuminen cHarrastus –sanaksi. Jos olisimme olleet tarpeeksi ”ovelia”, olisimme voineet tehdä vain yhden geneerisen tietorakenteen, joista olisi sitten luotu kaksi erilaista esiintymää.
16.8.1 vector-luokka
Seuraavassa on taul_d.cpp:tä vastaava esimerkki toteutettu STL:n geneerisen vector-luokan avulla. Geneerisyys tarkoittaa sitä, että voidaan valita minkä ”luokan olioita” vektori tallettaa.
dyna\vector.cpp – vector-luokka
// Malli vector-luokan käytöstä /vl-01
#include <iostream>
#include <iomanip>
#include <vector>
using namespace std;
typedef vector<int> cTaulukko;
void tulosta(ostream &os, const cTaulukko &luvut)
{
cTaulukko::const_iterator p;
for (p=luvut.begin(); p<luvut.end(); ++p)
os << setw(5) << *p;
os << endl;
}
int main(void)
{
cTaulukko luvut;
luvut.push_back(0); luvut.push_back(2);
tulosta(cout,luvut);
return 0;
}
16.8.2 Iteraattori
Esimerkissä taulukon tulostus on tehty iteraattorin avulla. Iteraattorin ideana on tarjota tietty, erittäin suppea joukko operaatiota, joita siihen voidaan kohdistaa. Näin samalla rajapinnalla varustettu iteraattori voidaan toteuttaa hyvin erilaisille tietorakenteille esimerkiksi taulukoille ja linkitetyille listoille. Iteraattorille esitettyjä suomennoksia ovat esimerkiksi selain ja vipellin. Huomattakoon että esimerkiksi C-taulukon osoitin toteuttaa iteraattorin rajapinnan.
Vektorin tapauksessa tietorakenne voitaisiin käydä läpi myös taulukkomaisesti,
for (unsigned i=0; i<luvut.size();i++)
os << setw(5) << luvut[i];
mutta tällöin tietorakenteen vaihtaminen esimerkiksi linkitetyksi listaksi vaatisi muutoksia tulosta-aliohjelmaan. Eli aina kun mahdollista, kannattaa välttää käyttämästä sitä tietoa, mikä tietorakenne on käytössä. Tietyssä mielessä iteraattorin ideaa toteuttaa kerhon jäsenen harrastusten tulostaminen:
void cNaytto::tulosta(ostream &os,const cJasen &jasen)
{
const cHarrastus *har;
int nro = jasen.Tunnus_nro();
jasen.tulosta(os);
har = kerho->eka_harrastus(nro);
while ( har ) {
har->tulosta(os);
har = kerho->seuraava_harrastus(nro);
}
}
Harrastusten tulostamisessa on vain se vika, että tieto viimeksi käytetystä harrastuksesta on cHarrastus-luokassa vain yhtenä esiintymänä. Näin jos samaa kerhoa käyttäisi useampi näyttö yhtäaikaa, niin tulostus menisi sekaisin.
Oikeassa iteraattorissa tieto siitä, millä kohdalla ollaan ja mihin voidaan siirtyä seuraavaksi, asuu itse iteraattorioliossa. Iteraattorikin voi muuttua epäkelvoksi, jos tietorakenteeseen tehdään muutoksia iteroinnin alkamisen jälkeen.
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); 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.
17.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.
17.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.
17.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
<EOF> <– 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 <iostream>
#include <stdio>
#include <fstream> // File stream
#include <string>
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;
}
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
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 ) { ...
17.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).
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++;
}
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.
17.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();
17.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);
...
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 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 */
...
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 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!
17.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.
17.5.1 Ongelma
Olkoon meillä vaikkapa seuraavanlainen tiedosto:
tiedosto\tuotteet.dat - esimerkkitiedosto
Volvo | 12300 | 1
Audi | 55700 | 2
Saab | 1500 | 4
Volvo | 123400 | 1<EOF>
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
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:
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
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);
17.5.3 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);
17.5.4 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;
}
17.5.5 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 <iostream>
#include <string>
#include <iomanip>
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 = ''
17.5.6 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";
}
17.6 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 <iostream>
#include <fstream>
#include <cstdio>
#include <string>
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
17.6.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 <sstream>
...
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.
17.6.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 <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#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;
}
17.7 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 <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#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<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].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; 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;
}
...
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!
...
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!
18. Operaattoreiden kuormitus
Mitä tässä luvussa käsitellään?
uusien merkityksien antaminen operaattoreille
automaattinen tyypinmuunnos
hajottajan hyödyntäminen
Syntaksi:
<< luokan ulkopuolella: ostream &operator<<(ostream &os,const cLuokka &olio);
>> luokan ulkopuolella: istream &operator>>(istream &is,cLuokka &olio);
+= luokassa: cLuokka &operator+=(const cLuokka &olio2);
+ luokan ulkopuolella: cLuokka3 operator+(const cLuo1 &o1, const cLuo2 &o2);
+ luokassa: cLuokka3 operator+(const cLuo2 &o2);
= luokassa: cLuokka &operator=(const cLuo2 &o2);
muunnos intiksi luokassa: operator int();
Ohjelmassa tuoterek.cpp oli esimerkki siitä, miten C++:ssa voidaan lisämääritellä (eli kuormittaa, overload) myös operaattoreita.
18.1 operator+=
Tuote oli sellainen, että siihen lisättiin aina saman tuotteen yhteensä–tiedot:
tuotteet[i] += tuote;
yhteensa += tuote;
Sama voitaisiin tehdä kutsulla
tuotteet[i].ynnaa(tuote);
yhteensa.ynnaa(tuote);
Osittain on makuasia kummalla tavalla lisääminen tehdään. += –operaattorin käyttö on tullut mahdolliseksi, koska luokassa cTuote on esitelty miten operaattorin on käyttäydyttävä jos luokan olioon halutaan lisätä toinen saman luokan olio:
cTuote &operator+=(const cTuote &tuote) { ynnaa(tuote); return *this; }
Operaattori on laitettu palauttamaan saman luokan olio, koska joskus voidaan haluta C:mäistä ketjusijoitusta:
tuote3 = tuote2 += tuote1;
Toisaalta ketjusijoitus ei ole aina selvin mahdollinen tapa tehdä asioita, samahan voitaisiin tehdä
tuote2 += tuote1; tuote3 = tuote2;
joten myös operaattori += voitaisiin suhteellisen hyvällä omalla tunnolla määritellä myös (luokassa cTuote)
void operator+=(const cTuote &tuote) { ynnaa(tuote); }
Oikeasti operaattoria kutsuttaisiin:
tuote2.operator+=(tuote1);
// vrt:
tuote2.ynnaa(tuote1);
mutta operaattori käsitellään erikoistapauksena ja em. kutsu voidaan kirjoittaa lyhyemmässä muodossa
tuote2 += tuote1;
Huomattakoon että samasta operaattorista voi olla useita eri versiota riippuen siitä mitä lisätään. Eli voisi olla esimeriksi:
tuote2 += 2000.0; // lisätään reaaliluku hintaan;
tuote2 += 5; // lisätään kokonaisluku kappalemäärään
Tehtävä 18.158 Useita versioita += operaattorista
Kirjoita em. versiot reaaliluvun ja kokonaisluvun lisäämiseksi tuotteeseen.
18.2 operator<< ja operator>>
Aikaisemmin mainittiin että eräs tietovirtojen merkittävimmistä eduista printf–tyyliseen tulostukseen on se, että tietovirtoihin voi tulostaa myös itse tehtyjä olioita. printf:hän ei ole mahdollista enää itse muuttaa ja lisätä uusia %–formaatteja.
Kuormitettaessa esimeriksi << –operaattoria, pitäisi oikeastaan muuttaa sitä tietovirta–luokkaa, johon ollaan tulostamassa.
cout << "Kissa\n"; // tarkoittaa periaateessa:
cout.operator<<("Kissa\n");
Onneksi operaattoreista on myös 2–operandiset versiot. Eli
operator<<(cout,"Kissa\n");
Tätä tietoa hyödyntäen voimme kirjoittaa operaattorista version, jonka ansiosta tulostus
cout << tuotteet[i] << "\n";
todella tulostaa tietorakenteen i:n tuotteen.
ostream &operator<<(ostream &os,const cTuote &tuote)
{
return tuote.tulosta(os);
}
Koska tuotteen tulostaminen on sinänsä jo valmis, pitää meidän vain kutsua tuotteen tulosta–metodia operaattorin määrittelyssä. Operaattorin PITÄÄ palauttaa vastaava tietovirta, jotta ketjutulostus:
cout << tuotteet[i] << "\n";
olisi mahdollinen. Tässähän oli itse asiassa kyseessä kutsu
operator<<(operator<<(cout,tuotteet[i]),"\n");
Itse tuote on esitelty vakioviitteeksi operaattorin << parametrilistassa. Viitteeksi, koska on turha siirrellä kokonaisia olioita ja vakioviitteeksi koska tulostuksen aikana tuotetta ei tietenkään muuteta!
Aivan vastaavasti esitellään >> –operaattori:
istream &operator>>(istream &is,cTuote &tuote)
{
tuote.lue(is);
return is;
}
Ohjelman tuoterek.cpp lukusilmukka voisi tämän ansiosta olla jopa muodossa
cTuote tuote;
while ( f >> tuote ) ynnaa(tuote);
Toisaalta aikaisemmin todettiin että tässä on vaara sotkea rivien järjestys joten parempana voidaan pitää alunperin esitettyä
lue_rivi
käsittele_rivi
tapaa.
18.3 Binäärisen operaation kuormitus
Vastaavasti kuin binäärisiä << ja >> –operaattoreita, voidaan myös muitakin binäärisiä operaattoreita kuormittaa. Binääristä operaatiota kuormitettaessa vähintään toisen operandin tulee olla luokka (eli ei skalaari kuten int, double jne.)
Olkoon meillä vaikkapa luokka, joka kuvaa hyvin usein vastaan tulevaa tilannetta, jossa käytössä onkin hyvin rajoitettu kokonaislukualue (esim. kellon minuutit [0,59[, tunnit [0,24[ jne.):
operator\rajoit.cpp - esimerkki binäärisen operaation kuormituksesta
Tässä ensimmäinen + –operaattori voitaisiin tehdä myös luokan metodiksi:
operator\rajoit2.cpp - esimerkki operaatioden kuormituksesta
#include <iostream.h>
class cRajoitettu {
...
cRajoitettu operator+(int i) { return cRajoitettu(arvo+i,raja); }
};
Tämä näyttää yksi-parametriselta operaattorilta, mutta on tosiasiassa binäärinen, nimittäin vasempana operandina on *this.
m2 = m1.operator+(20);
18.4 Tyypinmuunnosoperaattori ja sijoitus
Esimerkin rajoit.cpp toinen +–operaattori voitaisiin välttää tekemällä muunnosoperaattori, joka muuttaa cRajoitettu –tyyppisen olion kokonaisluvuksi:
class cRajoitettu {
...
operator int() { return arvo; }
};
Tällöin yhteenlaskussa
i = 20 + m2;
m2 ensin muuttuu kokonaisluvuksi ja sitten normaali kokonaislukujen yhteenlasku huolehtii lopusta. Muoto
m1 = 20 + m2;
saataisiin toimimaan määrittelemällä sijoitusoperaattori:
class cRajoitettu {
...
cRajoitettu &operator=(int i) { aseta(i); return *this; }
};
Tällöin sijoituslauseen oikea puoli lasketaan kokonaislukuna ja sitten tämä kokonaisluku sijoitetaan em. sijoitusoperaattorilla. Sijoitusoperaattorin tulee palauttaa sijoituksen kohteena olevan luokan tyyppinen tulos, jotta ketjusijoitus
m1 = m2 = 20;
olisi mahdollinen.
Tehtävä 18.159 Ajan lisääminen
Aikaisemmin oli esitetty cAika
- luokka. Lisää luokkaan +=
- operaattori ja +-
operaattori joiden ansiosta seuraavat sijoitukset olisivat mahdollisia
cAika a1(10,30),a2(15,55);
a1 += 50;
a2 = a1 + 20;
Tehtävä 18.160 Aika rajoitettujen kokonaislukujen avulla
Miten voisit tehdä luokan cAika
käyttämällä edellä ollutta cRajoitettu
- luokkaa. Tarvitseeko cRajoitettu
joitakin muutoksia?
18.5 Hajottajan hyödyntäminen
18.5.1 Tulostusmuotoilun palauttaminen
Oletetaanpa tuoterek.cpp –ohjelmassa että hinnat haluttaisiinkin tulostaa kahdella desimaalilla:
ostream &tulosta(ostream &os) const {
os.precision(2); os.setf(ios::left | ios::showpoint | ios::fixed ); :-(
os << setw(20) << nimike << " " << setiosflags(ios::right)
<< setw(10) << hinta << " "
<< setw(4) << kpl;
return os;
}
...
ostream &operator<<(ostream &os,const cTuote &tuote) {
return tuote.tulosta(os); }
Nyt tietysti voisi olla ohjelmoijalle yllätys, jos hän kutsuisi
cTuote tuote("Volvo|23700|1");
const double pi = 3.14159265;
cout.precision(6);
cout << pi << '\n';
cout << tuote << '\n';
cout << pi << '\n';
ja jälkimmäinen piin arvo tulostuisikin vain kahdella desimaalilla. Tämän vuoksi pitäisi alkuperäiset tulostusasetukset palauttaa, mikäli niitä muutetaan.
ostream &tulosta(ostream &os) const {
long oldf = os.setf(ios::left | ios::showpoint | ios::fixed );
int olddes = os.precision(2);
... tulostusta ...
os.precision(olddes); os.flags(oldf);
return os;
}
Mutta kuka tämän jaksaa tehdä kerrasta toiseen?
Entäpä jos teemmekin luokan cStreamPre (stream precision):
ostream &tulosta(ostream &os) const {
cStreamPre pre(os,2);
... tulostusta ...
return os;
}
ja nyt piikin tulee taas pyydetyllä 6:lla desimaalilla molemmilla kerroilla! Miten?
ali\StreamPr.h - Luokka tulostustarkkuuden asettamiseksi
class cStreamPre {
ostream &os;
ios::fmtflags oldf;
int oldp;
public:
cStreamPre(ostream &aos=cout,int npre=1,long flags=0) : os(aos) {
oldf = os.setf(ios::showpoint | ios::fixed | flags);
oldp = os.precision(npre);
}
~cStreamPre() { os.flags(oldf); os.precision(oldp); }
};
Luokan muodostaja tallettaa olion attribuutteihin tulostuksen muotoilun ennen kuin muotoiluja on muutettu. Kun syntyneen olion vaikutusalue lakkaa, eli poistutaan siitä ohjelmalohkosta jossa olio on syntynyt (auto), kutsutaan olion hajottajaa, joka palauttaa alkuperäiset arvot. Nyt ohjelmoijan tarvitsee vain antaa olion syntyä automaattisesti
cStreamPre pre(os,2);
lohkon alussa. Ja tämähän saadaan vähemmällä kirjoittamisella kuin tarkkuuden muuttaminen normaalisti:
os.precision(2); os.setf(ios::left | ios::showpoint | ios::fixed );
Huomattakoon, että "temppu" toimii vaikka kesken tulostuksen poistuttaisiin ylimääräisellä return—lauseella. Samoin "temppu" toimii, vaikka tulostuksen sisällä kutsuttaisiin toista samalla tavalla toteutettua tulostusta!
18.5.2 Tiedoston automaattinen sulkeminen
Samaan ideaanhan perustuu myös se, että C++:ssa voidaan tiedosto "jättää sulkematta":
{
ifstream fi(nimi);
... lukeminen ...
}
eli kun olion fi vaikutusalue lakkaa, kutsutaan tietovirtaolion hajottajaa, joka sulkee tiedoston.
18.6 Ystäväfunktiot
Usein kirjallisuudessa mm. tulostus opetetaan tekemään seuraavasti:
class cTuote {
...
friend ostream &operator<<(ostream &os,const cTuote &tuote);
}
ostream &operator<<(ostream &os,const cTuote &tuote) {
... viitataan suoraan luokan suojattuihin attribuutteihin
}
Funktion tai luokan esitteleminen ystäväksi tarkoittaa sitä, että funktio tai luokka pääsee suoraan käsiksi olion attribuutteihin. Tämä ei yleensä kuitenkaan ole kovin suotavaa. Jopa itse kielen tekijäkin on myöntänyt että ystäväfunktiot (friend) ovat tarpeettomia. Mehän vältimme ystäväfunktion seuraavalla tekniikalla:
class cTuote {
...
ostream &tulosta(ostream &os) const { ... }
}
ostream &operator<<(ostream &os,const cTuote &tuote) {
return tuote.tulosta(os);
}
emmekä joutuneet edes kirjoittamaan yhtään enempää koodia. Toinen tapa välttää ystäväfunktioita on saantimetodien käyttäminen:
class cRajoitettu {
...
int Arvo() const { return arvo; }
};
ostream &operator<<(ostream &os, const cRajoitettu &r) {return os << r.Arvo(); }
Tässäkään tapauksessa ei ole kenellekään luovutettu pääsyä muuttamaan suojattuja attribuutteja.
Vastaavasti ystävyyttä esitetään kirjallisuudessa muidenkin operaattoreiden kuormittamisessa käytettäväksi, mutta tottakai voimme em. tekniikoilla välttää ystäväfunktioiden käytön.
Siis pyrimme jatkossa välttämään ystäväfunktioita (jopa enemmän kuin goto–lausetta).
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-> 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 {
...
string talleta(const string &tied="");
...
int TeeBak(const string &bak_tark) { return jasenet.TeeBak(bak_tark);
};
talletus.2\kerho.cpp - talleta
string 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);
}
...
//-------------------------------------------------------------------------
string cJasenet::talleta(const string &tied)
{
if ( !muutettu ) return "";
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 "";
}
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
string cJasen::getAsString() const
{
return
jonoksi(tunnus_nro) + erotin
jonoksi(nimi) + erotin
jonoksi(hetu) + erotin
jonoksi(katuosoite) + erotin
jonoksi(postinumero) + erotin
jonoksi(postiosoite) + erotin
jonoksi(kotipuhelin) + erotin
jonoksi(tyopuhelin) + erotin
jonoksi(autopuhelin) + erotin
jonoksi(liittymisvuosi) + erotin
jonoksi(jmaksu) + erotin
jonoksi(maksu) + erotin
jonoksi(lisatietoja) + erotin;
}
ostream &operator<<(ostream &os,const cJasen &jasen)
{
os << jasen.getAsString();
return os;
}
Tästä on vielä se lisäetu, että voidaan esimerkiksi tehdä jonoksi–funktiosta sellainen, että tietty kokonaislukuarvo tai reaalilukuarvo (esim. -1) tallentuu 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ö.
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
string 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 "";
}
...
string 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;
string rivi;
cJasen uusi;
while ( getline(f,rivi) ) {
if ( rivi == "" || rivi[0] == ';' ) continue;
uusi.setAsString(rivi);
// f >> uusi; // vaatisi kunkin rivin olemisen täydellisenä, muuten OK!
IF_ERR_RETURN(lisaa(uusi));
}
muutettu = 0;
return "";
}
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 - setAsString
int cJasen::setAsString(string &jono)
{
string pala;
pala = erota(jono,'|'); luvuksi(pala,tunnut_nro);
pala = erota(jono,'|'); nimi = poista_tyhjat(pala);
pala = erota(jono,'|'); sotu = poista_tyhjat(pala);
...
pala = erota(jono,'|'); luvuksi(pala,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
int cJasen::setAsString(string &jono)
{
ota(jono,tunnus_nro ,erotin);
ota(jono,nimi ,erotin);
ota(jono,hetu ,erotin);
ota(jono,katuosoite ,erotin);
ota(jono,postinumero ,erotin);
ota(jono,postiosoite ,erotin);
ota(jono,kotipuhelin ,erotin);
ota(jono,tyopuhelin ,erotin);
ota(jono,autopuhelin ,erotin);
ota(jono,liittymisvuosi,erotin);
ota(jono,jmaksu ,erotin);
ota(jono,maksu ,erotin);
ota(jono,lisatietoja ,erotin);
if ( tunnus_nro >= seuraava_nro ) seuraava_nro = tunnus_nro + 1;
return 0;
}
19.3 Kuormitetut funktiot
Funktio ota on polymorfinen eri tietotyypeille ja sen tehtävänä on päästä eroon turhista välilyönneistä ja erottaa jonosta seuraavaan erotinmerkkiin saakka. ota huolehtii myös tyhjän arvon käsittelystä (-1 <=> ""). Funktiot ota voidaan tehdä samannimisiksi C++:n kuormituksen ansiosta. Kääntäjä tunnistaa parametrilistasta mitä funktiota tulee oikeasti kutsua:
void ota(string &s,string &pala,char erotin) {
pala = erota(s,erotin);
poista_tyhjat(pala);
}
void ota(string &s,int &i,char erotin) {
string pala;
ota(s,pala,erotin);
i = -1;
sscanf(pala.c_str(),"%d",&i);
}
void ota(string &s,double &d,char erotin) {
string pala;
ota(s,pala,erotin);
d = -1;
sscanf(pala.c_str(),"%lf",&d);
}
19.4 Virheilmoitukset, static
Talletukseen liittyvät aliohjelmat on suunniteltu string –tyyppisiksi. Mikäli jokin menee pieleen, palauttavat ne nimessään itse virheilmoitukseen, jolloin kutsunut ohjelma voi halutessaan tulostaa virheilmoituksen.
Mikäli aliohjelma päättyy onnellisesti, voidaan palauttaa tyhjä merkkijono, eli ei virheilmoitusta.
jasenet.cpp:
static const char *TIED_EI_AUKEA = "Tiedosto ei aukea!";
...
if ( !f ) return TIED_EI_AUKEA;
...
return "";
...
naytto.cpp:
string 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
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:
...
string 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) { string virhe=(v); if ( virhe != "" ) return virhe; }
19.5 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öä.
20. Päätesyöttö
Mitä tässä luvussa käsitellään?
- päätesyöttö yksinkertaisimmalla tavalla
- näyttö tietämättömäksi jäsenen ominaisuuksista
- "indeksoidut" kentät
Oikeassa ohjelmassa päätesyöttö hoidetaan usein erilaisilta lomakepohjilta. Tällaisen ohjelman tekeminen ensimmäisenä ohjelmana on kuitenkin varsin työlästä. Siispä teemme aluksi suunnitelman mukaisen yksinkertaisen syötön, jossa ainoa korjailumahdollisuus on oletusarvon käyttö. Näppäinten painallusten lukumäärää laskettaessa nyt toteutettava syöttö on aivan yhtä hyvä kuin lomakepohjainen syöttö. Jos pystymme pitämään kaiken laiteriippuvuuden cNaytto–luokassa, on ohjelma kohtuullisella työllä muutettavissa myös lomakepohjaisesti toimivaksi esimerkiksi Windows–käyttöliittymään.
20.1 Lukeminen ilman tarkistuksia
Aluksi kannattaa oikeellisuustarkistukset unohtaa ja yrittää saada edes jotakin luettua.
20.1.1 lisaa_uusi_jasen (naytto.cpp)
Ohjelman edellinen versio osasi lisätä vain aina vakiojäsenen "Ankka Aku". Muutamme lisaa_uusi_jasen aliohjelmaa siten, että vakiojäsenen tilalla kysytäänkin jäsenen tiedot:
lukemine.3\naytto.cpp - päätesyöttö
//----------------------------------------------------------------------------
void cNaytto::lisaa_uusi_jasen(char valinta)
/*
** Kysyllään uusien jäsenien tietoja ja lisätään jäsenet.
----------------------------------------------------------------------------*/
{
cJasen jasen;
otsikko(valinta,"Uuden jäsenen lisäys");
while ( 1 ) {
jasen.tyhjenna_omat();
jasen.rekisteroi();
cout << endl;
cout << "Jäseniä on nyt " << kerho->Jasenia() << "." << endl;
cout << "Anna uusi nimi muodossa sukunimi etunimi"; // ei endl
int ei_lisata = 1;
do {
cout << endl; // Jotta uudella kier. rivi vaiht
if ( kysy_tiedot(jasen) ) return;
cout << "Lisätäänkö" << endl;
tulosta(cout,jasen);
cout << ":";
if ( kylla_vastaus() )
ei_lisata = ilmoitus(kerho->lisaa(jasen));
if ( ei_lisata ) kerho->poista(jasen.Tunnus_nro());
} while ( ei_lisata );
}
}
20.1.2 kysy_tiedot (naytto.cpp)
Varsinainen tietojen kysyminen on jätetty aliohjelmalle kysy_tiedot. Yksinkertaisimmillaan tämä olisi:
int cNaytto::kysy_tiedot(cJasen &jasen)
{
cJasen apujasen(jasen);
string jono;
cout << "Jäsenen nimi >"; getline(cin,jono,);
if ( jono == "" ) return 1;
apujasen.setNimi(jono);
cout << "Hetu >"; getline(cin,jono);
apujasen.setHetu(jono);
...
cout << "Jäsenmaksu mk >"; getline(cin,jono);
apujasen.setJmaksu(jono_doubleksi(jono,"%lf"));
...
jasen = apujasen;
return 0;
}
Tässä on vielä vähän vikoja. Pahimpana tietysti valtava määrä saantimetodeja. Lisäksi syötöstä ei voi poistua minkä tahansa kentän antamisen jälkeen. Tämä voidaan korjata esimerkiksi lisäämällä merkin "q" käsittely jokaisen lukemisen jälkeen:
cout << "Jäsenen nimi >"; getline(cin,jono);
if ( jono == "" ) return 1;
if ( jono == "q" ) return 1;
apujasen.setNimi(jono);
cout << "Hetu >"; getline(cin,jono);
if ( jono == "q" ) return 1;
...
cout << "Jäsenmaksu mk >"; getline(cin,jono);
if ( jono == "q" ) return 1;
apujasen.setJmaksu(jono_doubleksi(jono,"%lf"));
...
Vieläkään ei ole ehdotettu oletusarvoa kentälle syöttöä varten. Lisäyksessähän tämä ei niin haittaa, mutta jos samaa aliohjelmaa haluttaisiin käyttää korjailussa, olisi oletusarvot toivottavia.
Toimiakseen se, että näyttö voi viitata suoraan jäseneen vaatii näytön jäsenen ystäväksi. No tämähän ei ollut kovin toivottava ominaisuus. Lisäksi olisi toivottavaa muutenkin, ettei näyttö tietäisi näin paljoa jäsenestä. Nyt nimittäin jos jäseneen lisätään yksikin kenttä, pitää muutoksia tehdä monessa paikassa (jo turhan monessa ilman näytönkin muuttamista).
20.2 Poistetaan riippuvuus näytön ja jäsenen väliltä
20.2.1 Algoritmi
Lisäämällä byrokratiaa näytön ja jäsenen välille, voidaan näyttö pitää tietämättömänä siitä, mitä kenttiä jäsenessä todella on. Minkälaista byrokratiaa? Esimerkiksi "keskustelu" näytön ja jäsenen välillä voisi olla seuraavanlainen:
Näyttö: montako kenttää sinulla on jäsen?
Jäsen: 13 kenttää
Näyttö: no annappa minulle 1. kenttä merkkijonona!
Jäsen: Ankka Aku
Näyttö: Milläs kysymyksellä tämä kenttä kysytään?
Jäsen: Jäsenen nimi
Näyttö kysyy käytäjältä Jäsenen nimi (Ankka Aku) > => jono
Näyttö tutkii vastattiinko q tms. erikoismerkki, jos niin pois
Näyttö: Sijoitapa jäsen tämä jono 1. kentäksi.
Näyttö jatkaa kohdasta 3 mutta kentälle 2 kunnes kaikki 13 kenttää käsitelty
20.2.2 kysy_tiedot
Kirjoitetaanpa edellinen algoritmi C++:lla:
lukemine.3\naytto.cpp - kysy_tiedot
int cNaytto::kysy_tiedot(cJasen &jasen)
{
cJasen apujasen(jasen);
string jono;
int k,kenttia = apujasen.kenttia(), eka = apujasen.eka_kysymys();
for (k=eka; k<kenttia; k++) {
jono = apujasen.kentta_jonoksi(k);
if ( kysy_kentta(apujasen.kysymys(k),jono) ) return 1;
if ( k == eka && jono == "" ) return 1; /* 1. kys pääsee pois pelk. ret *
apujasen.sijoita(k,jono);
}
jasen = apujasen;
return 0;
}
20.2.3 kysy_kentta
"Likainen työ" jätettiinkin aliohjelmalle kysy_kentta, joka ei juurikaan ole sidoksissa kerho–ohjelmaan:
lukemine.3\naytto.cpp - kysy_kentta
/****************************************************************************/
static int /* */
kysy_kentta( /* */
const string &viesti ,/* s Viesti joka tulee näytölle */
string &jono /* t Jono johon kentän vastaus luetaan. */
)
/*
** Funktiolla luetaan vastaus kenttään.
**
** Globaalit: POIS (jono, jolla syöttö katkaistaan)
** Syöttö: päätteeltä
** Tulostus: näyttöön
** Kutsuu: lue_jono_oletus
----------------------------------------------------------------------------*/
{
string apu; int paluu;
apu = jono;
paluu = lue_jono_oletus(viesti,OLET_ALKU,VAKANEN,apu);
if ( paluu < OLETUS ) return 1;
if ( apu == POIS ) return 1;
poista_tyhjat(apu);
jono = apu;
return 0;
}
Tulosta ei lueta suoraan muuttujaan jono, jotta mahdollisessa q –vastauksessa ei pilattaisi kentän alkuperäistä arvoa.
20.3 Muutokset jäsen–luokaan
Edellä näytti siltä, että onnistuimme muuttamaan näyttö–luokkaa siten, että uuden kentän lisääminen tai kentän poistaminen ei vaadi muutoksia luokaan cNaytto. Tämä on tietenkin jo askel kohti oikeata ohjelmointia.
20.3.1 kenttia ja eka_kysymys
Koska näyttö keskustelee jäsenen kanssa, pitää tietysti toteuttaa myös ne metodit, joilla näyttö (tai kuka tahansa) voi kysellä kriittisiä tietoja jäseneltä:
lukemine.3\jasen.cpp - kenttia, eka_kysymys
class cJasen {
...
int kenttia() const { return 13; }
int eka_kysymys() const { return 1; }
...
};
20.3.2 kentta_jonoksi
Jäsenen tietyn kentän pääsemme muuttamaan merkkijonoksi käyttäen talletuksen yhteydessä tehtyä polymorfista jonoksi–funktiota:
lukemine.3\jasen.cpp - kentta_jonoksi
const string cJasen::kentta_jonoksi(int k) const
{
switch ( k ) {
case 0: return jonoksi(tunnus_nro);
case 1: return jonoksi(nimi);
case 2: return jonoksi(hetu);
case 3: return jonoksi(katuosoite);
...
case 10: return jonoksi(jmaksu);
case 11: return jonoksi(maksu);
case 12: return jonoksi(lisatietoja);
default: return string("VIRHE");
}
}
20.3.3 kysymys()
Näytön pitää päästä myös selvittämään mikä teksti näyttöön pitää laittaa tietyn kentän kysymiseksi käyttäjältä:
lukemine.3\jasen.cpp - kysymys
string cJasen::kysymys(int k) const
{
switch ( k ) {
case 0: return "Tunnus nro";
case 1: return "Jäsenen nimi";
case 2: return "Hetu";
case 3: return "Katuosoite";
...
case 10: return "Jäsenmaksu mk";
case 11: return "Maksettu maksu mk";
case 12: return "Lisätietoja";
default: return "VIRHE!";
}
}
20.3.4 sijoita
Kun näyttö on merkkijonon kysynyt, pitäisi merkkijonon muuttunut arvo saattaa jäsenen tietoon:
lukemine.3\jasen.cpp - sijoita
int cJasen::sijoita(int k,const string &st)
{
switch ( k ) {
case 0: muunna_jono(st,&tunnus_nro); return 0;
case 1: muunna_jono(st,&nimi); return 0;
case 2: muunna_jono(st,&hetu); return 0;
case 3: muunna_jono(st,&katuosoite); return 0;
...
case 9: muunna_jono(st,&liittymisvuosi); return 0;
case 10: muunna_jono(st,&jmaksu); return 0;
case 11: muunna_jono(st,&maksu); return 0;
case 12: muunna_jono(st,&lisatietoja); return 0;
default: return 1;
}
20.3.5 Arvostelu valinnoista
Edellä tehdyillä valinnoilla ohjelmasta saadaan toimiva tiedon päätteeltä lukua myöten. Kuitenkin kuten tehtävässäkin todettiin, tulee muutoksi paljon, jos jäsenen attribuuttien määrä muuttuu.
Itse asiassa sijoita ja kentta_jonoksi sekä kysymys() ovat riittävä määrä jäsenen kysymysten toteuttamiseksi. Näitä metodeja käyttäen voimme samalla muuttaa ison osan aikaisemmin tehtyjä "itseään toistavia" jäsenen metodeja ja muita aliohjelmia silmukoiksi, mm:
string cJasen::getAsString() const
{
string st;
for (int k=0; k<kenttia(); k++)
st += kentta_jonoksi(k) + erotin;
return st;
}
//----------------------------------------------------------------------------
int cJasen::setAsString(string &jono)
{
string pala;
for (int k=0; k<kenttia(); k++) {
pala = erota(jono,erotin);
sijoita(k,pala);
}
if ( tunnus_nro >= seuraava_nro ) seuraava_nro = tunnus_nro + 1;
return 0;
}
20.4 Testaus
Jälleen testataan ohjelman toimivuutta. Mikäli virheitä havaitaan, korjataan ne. Muuten voidaan jatkaa seuraavaan vaiheeseen joka voisi olla joko oikeellisuustarkistusten lisäys tai tietojen etsimisen lisäys.
21. Oikeellisuustarkistukset ja avustukset
Mitä tässä luvussa käsitellään?
- yksinkertainen tarkistus
- muita vaihtoehtoja tarkistusten tekemiseen
- funktio–osoittimet
21.1 Miksi tarkistukset
Oikeassa ohjelmassa on todella tärkeää, että syöttötietojen oikeellisuus tarkistetaan. Aina on lähdettävä siitä oletuksesta, että käyttäjät ovat täysiä idiootteja (älkää kertoko tätä mahdollisille käyttäjille). Toisaalta idioottitarkistuksia ei saa olla liikaa. Esimerkiksi "Oletko varma" –tyyliset kysymykset rupeavat ennen pitkää ärsyttämään kokenutta käyttäjää.
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>kymppi
ja 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.
21.2 Lukeminen rivi kerrallaan
Eräs suhteellisen vaivaton tapa käsitellä syöttöä on lukea syöttö aina merkkijonona. Tämän jälkeen voidaan syöttörivi käsitellä erota -tyyppisillä funktiolla tai C++:n merkkijonovirralla osa kerrallaan. Kustakin palasesta sitten otetaan tarvittava syöttö esimerkiksi C–kielen sscanf–funktiolla, joka toimii aivan scanf–funktiota vastaavasti:
jono = "52 mk";
if ( sscanf(jono.c_str(),"%d",&hinta)<1 ) /* virhe.... */;
21.3 Tarkistusaliohjelmat
Kutakin ohjelmassa esiintyvää tietotyyppiä kohden voidaan kirjoittaa tarkistusfunktiot, jotka tarkistavat parametrinä tuodun tiedon oikeellisuuden ja sitten silmukassa luetaan tietoa kunnes ko. funktio hyväksyy tiedon:
string hetu;
...
do {
cout << "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.
21.4 Kerhorekisterin tarkistukset?
Entä kerhon jäsenrekisteri. Ohjelma on jo kirjoitettu hyvin pitkälle, eikä tarkistuksiin ole puututtu juuri lainkaan. Merkkijonoina lukemisen ansiosta ohjelmaa ei voida kaataa väärillä syöttötiedoilla, mutta rekisterin kannalta virheellisiä tietoja, kuten samoja nimiä ja sotuja, voi esiintyä useita, numeerisiksi tarkoitetuissa kentissä voi olla kirjaimia jne. Osa ominaisuuksista voi olla toivottujakin.
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,jono);
poista_tyhjat(jono);
if ( jono == "q" ) return 1;
} while ( tarkista_hetu(jono) );
apujasen.setHetu(jono);
. . .
do {
cout << "Jäsenmaksu mk >"; cin.getline(cin,jono);
if ( jono == "q" ) return 1;
luvuksi(jono,d);
} while ( (d < 0) || ( 500 < d ) );
apujasen.setMaksu(d);
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 parametrinä viedä tietysti myös käsiteltävä kerho tai ainakin jäsenistö.
21.4.1 grep
Näin käy useasti! Myöhemmin huomataan jonkin aliohjelman vaativan lisää parametrejä ja niitä joudutaan jälkeenpäin lisäämään. Lisätään parametri! Samalla metodille kysy_tiedot kannattaa välittää tieto siitä, onko kyseessä lisäys vaiko päivitys (korjailu).
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).
21.5 Sijoittaminen tarkistaa
Jos tietojen kysyminen hoidetaan silmukassa, voitaisiin tarkistus jättää sijoittamisen huoleksi. Eli jos sijoittaminen ei jäsenen mielestä onnistu, palautetaan tästä tieto. Paluutietoja voisi olla useita, eli osa olisi varoitusluonteisia ja osa tiedon uudelleen kysymistä vaativia. Osa taas vaatii lisätarkistuksia. Esimerkiksi hetu. Jäsenhän ei tiedä mitä muita jäseniä on, joten se ei myöskään voi tarkistaa hetusta muuta kuin muodollisen oikeellisuuden.
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 );
21.5.1 sijoita
Yksinkertaisessa versiossa cJasen voi tarkistaa tiedon järkevyyden sijoituksen yhteydessä.
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.
}
}
21.6 Funktio-osoitin
Usein esimerkiksi matemaattisissa tehtävissä tulee vastaan tilanne, missä jollekin funktiolle tehty aliohjelma kelpaisi tekemään saman homman myös jollekin toiselle funktiolle, mikäli funktio pystyttäisiin vaihtamaan. Tyypillisiä tällaisia esimerkkejä ovat numeerinen derivointi ja integrointi, nollakohdan hakeminen, kuvaajan piirtäminen jne.
Esimerkiksi funktion integrointi suorakaidesäännöllä voitaisiin hoitaa seuraavasti:
integroi.c – esimerkki numeerisesta integroinnista
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ä" parametriä; funktiota jota integroidaan:
integro2.c – esimerkki funktiosta parametrina
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.
Tehtävä 21.165 Minimointi
Kirjoita funktioaliohjelma min_fun
, jolla voidaan etsiä parametrinä annetun funktion minimi väliltä [x1,x2]
. Väliä käydään läpi tarkkuudella dx
. Kirjoita aliohjelmat ja pääohjelma, jolla lasketaan funktioiden
f(x) = x2 + 2 ja f(x) = sin2(x-3)
minimit väliltä [-2,3]
tarkkuudella 0.01
.
21.7 Tarkistus-funktio –osoitin
Joissakin tapauksissa tarkistus voitaisiin hoitaa myös siten, että jäseneltä kysytään minkälaisella funktiolla oikeellisuus tarkistetaan:
/* 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ään
ja vaatii uutta syöttöä!
Siis parametrit eivät riitä! Mitä pitää lisätä? Tästä ajattelutavasta saattaa seurata loputon kasa uusia parametrejä. 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);
. . .
21.7.1 kerhotar.cpp
Itse tarkistusfunktiot kirjoitetaan vaikkapa tiedostoon kerhotar.cpp
21.7.2 pvm.c
Esimerkiksi päivämäärän tarkistukseen tarvittavat rutiinit ovat lähes valmiina aikaisemmin kirjoitetuissa aliohjelmissa. Nämä voitaisiin kasata tiedostoiksi pvm.h ja pvm.c.
21.7.3 Uusien tarkistusfunktioiden lisäys
Aluksi loogisen toimivuuden tarkistamiseksi riittää tietysti kirjoittaa vain aliohjelma tark_ok. Muut edellä esitetyt voidaan lisätä myöhemmin vaikkapa yksi kerrallaan.
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
21.8 Etsiminen
Nimi tai hetu piti tarkistaa siten, että samaa ei saa esiintyä, mutta toisaalta kohdalla olevan jäsenen tiedot eivät saa aiheuttaa virheilmoitusta. Tämä voidaan hoitaa esimerkiksi seuraavalla aliohjelmalla:
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)).
21.9 cKentta ja perintä
Malliohjelmassa tarkistukset on tehty hieman edellä kuvatulla tavalla, paitsi että jäsenen kentät eivät olekaan merkkijonoja tai reaalilukuja, vaan yleisestä cKentta luokasta perittyjä kenttä–luokkia, joista jokainen tietää itse miten ko. kenttä tulee käsitellä (yksikäsitteinen nimi, puhelinnumero, jossa vain numeroita jne.). Nämä luokat hoitavat sitten itse merkkijonosijoitukset, tiedon ottamisen tietovirrasta, oikeellisuustarkistukset jne.
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;
...
21.10 Avustus
Avustusta tarvitaan kahdenlaista:
Yleistä avustusta, jossa mielellään voidaan selata ohjelman eri kohtien toimintaa. Tämä on suhteellisen helppo toteuttaa yleisesti.
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:
tarkistu.4\kerho.hlp - avustustiedosto
[?]
? = 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.
22. Etsiminen ja lajittelu
Mitä tässä luvussa käsitellään?
- yksinkertainen etsiminen
- etsiminen taulukkoon
- lajittelu valmiilla qsort–aliohjelmalla
- permutaatiotaulukko
- "oikeaoppinen lajittelu"
Etsiminen ja lajittelu ovat eräitä yleisimmin tietojenkäsittelyssä vastaan tulevia tilanteita. Esimerkiksi C–merkkijonon pituuden laskeminen on oikeastaan NUL-merkin paikan etsimistä.
HUOM! Tämän luvun ohjelmalistaukset ovat "pseudo"-koodia, ja niissä voi olla pieniä syntaksivirheitä.
22.1 Etsiminen
Etsimistä voidaan suorittaa monella tavalla. Mikäli tietorakenne ei tarjoa parempaa vaihtoehtoa, ei ole muuta mahdollisuutta kuin peräkkäishaku. Järjestetystä taulukosta pystyttiin hakemaan binäärihaulla. Puumaisesta rakenteesta voidaan hakea puun ominaisuuksia käyttäen. Joskus voidaan tehdä avuksi hakemistoja, jotka ohjaavat haun suurin piirtein oikealle paikalleen, josta sitten jatketaan jollakin toisella hakumenetelmällä.
C–kielen kirjastosta löytyy funktio bsearch, joka etsii lajitellusta aineistosta binäärihaulla ja palauttaa NULL mikäli ei löydy ja muuten palauttaa osoittimen taulukon löytyneeseen alkioon. Funktion käyttö on vastaava kuin myöhemmin esiteltävän qsort –funktion käyttö, joten emme tässä puutu siihen enempää.
Kerhorekisterissä tietorakenne on varsin alkeellinen, eikä sitä ole ainakaan toistaiseksi edes järjestetty. Siis tietyn jäsenen etsiminen on suoritettava raakana peräkkäishakuna.
Tehtävä 22.166 etsi_nimi
Kirjoita metodi etsi_nimi, joka etsii jäsenistöstä ensimmäisen henkilön, jolla on TÄSMÄLLEEN parametrinä annettu nimi. Funktio palauttaa nimessään löytöpaikan indeksin ja - 1 jollei löydy. Voitaisiinko metodia käyttää apuna lisäyksessä tarkistamaan onko jäsen jo ennestään tiedostossa? Voitaisiinko metodia käyttää apuna korjaamisessa tarkistamaan onko nimi muuttunut sellaiseksi, joka on jo ennestään tiedostossa?
22.1.1 wildmat
Jos etsimisessä halutaan sallia jokerimerkkien käyttö, saattaa etsiminen johtaa joka tapauksessa peräkkäishakuun. Voitaisiin kuitenkin erotella erikoistapauksia:
Ankka Aku - voitaisiin käyttää mitä tahansa järkevää
järjestetyn joukon hakualgoritmia
Ankka* - voitaisiin etsiä järjestetystä joukosta 1. Ankka
ja tämän jälkeen loppu peräkkäishakuna
*Aku* - vaikea keksiä muuta kuin peräkkäishaku
Lähdimme kuitenkin alunperin oletuksesta, että kerhon koko on pieni. Siis tuskin peräkkäishakukaan vie kohtuuttomasti aikaa. Näin ollen käsittelemme kaikki hakutilanteet samalla tavalla.
Entäpä mikäli etsimisessä löytyy useita ehdon täyttäviä henkilöitä. Eräs tyypillinen tapa on tehdä aliohjelmapari:
etsi_ensimmainen
etsi_seuraava
Vähän samaa ideaa sovellettiin jo aikaisemmin palanen
–aliohjelmassa.
Tehtävä 22.167 etsi_nimi_alkaen
Kirjoita metodi etsi_nimi_alkaen
, joka etsii kerhosta nimeä aloittaen etsimisen parametrinä välitetystä indeksistä. Tällä kertaa hakuehtona saa olla myös jono "Aku". Metodi palauttaa löytöpaikan indeksin tai - 1, mikäli etsittävää ei löydy.
22.1.2 Etsiminen taulukkoon
Toinen mahdollisuus etsimiseen on tallettaa taulukkoon kukin löytynyt ehdot täyttävä tietue. Määrittelemme tyypin
jarjestys.h – taulukko löytyneistä
class cJarjestys { /* Luokka jonka avulla voidaan pitää eri järjestyksiä. */
int max_koko; /* Indeksitaulukon maksimikoko. */
int indekseja; /* Taulukosta nyt käytetty indekseja. */
int *indeksit /* Osoitin indeksitaulukkoon (dynaaminen). */
...
};
Jos seuraavasta aineistosta etsittäisiin vaikkapa hakuehdolla nimi=="*k*"
**jasenet | o |
cJarjestys +------+------+ cJasen
etsi +---+ | +--------------+
max_koko | 7 | v |Kassinen Katto|
indekseja| 2 | +----------+ |Katto |
*indeksit| o | +--->jasenet[0] | o-----+-------------->|3452 |
+-+-+ | +----------| |... |
| | jasenet[1] | o-----+--------+ +--------------+
v | +----------| | +-------------+
+---+ | +->jasenet[2] | o-----+------+ | |Susi Sepe |
0 | 0 +--+ | +----------| | +----->|Takametsä |
+---| | ... | | | |- |
1 | 2 +----+ +----------| | |... |
+---| | | | +-------------+
2 | ? | +----------| | +-------------+
+---| | | +------->|Ankka Aku |
3 | ? | +----------| |Ankkalinna |
+---| | | |1234 |
4 | ? | +----------| |... |
+---| | | +-------------+
5 | ? | +----------+
+---| osoitintaulukko henkilöihin
6 | ? |
+---|
7 | ? | indeksitaulukko löytyneisiin
+---+
löytyisi jasenet[0]
ja jasenet[2]
, jotka täyttävät hakuehdon. Tällöin etsimisaliohjelma palauttaisi tämän tiedon seuraavasti:
cJarjestys etsi
cJarjestys etsi
+-----+
max_koko | 7 | 0 1 2 3 4 5 6
indekseja| 2 | +--------------------+
*indeksit | o--+------>| 0| 2| ?| ?| ?| ?| ?|
+-----+ +--------------------+
Nyt tietäisimme heti, että meillä on kaksi löytynyttä, joten löytyneet voitaisiin tulostaa:
for (i=0; i<etsi.size(); i++)
kerho- >anna_jasen(etsi[i]).tulosta(cout)
Käytännössä nykyisin taulukkona kannattaa käyttää STL:n vector-luokkaa.
22.1.3 Etsiminen valitun kentän mukaan
Mitä jos haluamme etsiä jonkin tietyn kentän mukaan. Välitämme etsimisaliohjelmalle tiedoksi kentän numeron, jonka mukaan haluamme etsiä. Käytännössä osoittautuu helpoimmaksi välittää myös etsittävä isona tietueena (tai taulukkona), johon on täytetty nimenomaan etsittävään kenttään halutut tiedot.
22.1.4 etsi ja indeksit
Etsimistä voitaisiin hieman monipuolistaa siten, että voitaisiin laittaa ehto jokaiseen tutkittavan jasen–luokan kenttään. Sitten löytymisen ehtona voisi olla, että jokin kentistä täsmää (TAI) tai että kaikki kentät täsmäävät (JA). Tämä voitaisiin ilmaista etsimisaliohjelmalle vaikkapa siten, että JA etsimisessä kentta=–1 ja TAI etsimisessä kentta=–2.
Tehtävä 22.168 etsi-olio
Hahmottele mitä metodeja tulisi olla luokassa cJarjestys ja mitä sitä käyttävälle etsimismetodille tulisi viedä parametrinä ja mihin luokkaan etsimismetodi tulisi kirjoittaa?
Tehtävä 22.169 TAI-etsiminen
Miten käyttäjä saisi TAI-etsimisen avulla vastauksen kysymykseen: Onko missään kentässä missään kohti sana aku?
22.1.5 cHaku
Etsimisaliohjelmalla kannattaa ehkä viedä parametrinä cHaku–luokkaa oleva olio sen sijaan, että vietäisiin cJasen –tyyppinen olio. Tähän on kaksi syytä.
Ensinnäkin on käyttäjän kannalta mukavaa, että jos hän on joskus hakenut nimihaussa nimellä "*aku*", niin seuraavassakin nimihaussa hänellä on oletuksena sama hakuehto. Siis eri kentissä viimeksi käytetyt hakuehdot kannattaa kukin säilyttää erikseen.
Toisaalta jos haluamme toteuttaa JA ja TAI –tyyppiset haut, tarvitsemme etsimisaliohjelmalle hakuehdon kullekin kentälle.
cHaku voisi olla myös cJasen, mutta koska cJasen kentillä on mahdollisuus saada myös numeerisia arvoja, ei numeeriseen kenttään pystyttäisi tallettamaan esimerkiksi hakuehtoa "4*" tai "<50". Siis cHaku on parasta tehdä merkkijono taulukoksi
class cHaku {
string kentat[KENTTIA];
...
}
22.2 Selailu
Kun etsittävän ehdon täyttävät tietueet ovat löytyneet, voitaisiin niitä selailla suhteellisen helposti:
22.2.1 selaile
suunta = 0;
while (1) {
- lisää kohdalla olevaan suunta
- korjaa sallitun välin sisälle
- tulosta kohdalla oleva
painettu = odota_nappain(sallitut,RET,VAIN_ISOT);
switch (painettu) {
case '- ': suunta = - 1; break;
case '+': suunta = 1; break;
default : return painettu;
}
}
Tehtävä 22.170 Koko kerhon selailu
Jos meillä on käytössä hakuehdon kysyminen, etsiminen ja selailu, niin tarvitseeko koko kerhon selaamista varten tehdä oma aliohjelma?
22.3 Lajittelu
Lajittelun vaatimia algoritmeja olemme käsitelleet jo aikaisemmin. Mikä tahansa aikaisemmin mainituista algoritmeista olisi helppo soveltaa jäsentaulukkoon. Kuitenkin lähes kaikki oppimamme algoritmit olivat kompleksisuudeltaan O(n2). Jos jäsenmäärä kasvaa paljon yli sadan, tarkoittaa tämä suhteellisen hidasta lajittelua.
C–kielen standardikirjasto stdlib.h tarjoaa valmiin QuickSort lajittelualiohjelman qsort, jonka kompleksisuus on O(n log n). Miten lajitteluohjelma voidaan tehdä yleiskäyttöiseksi? Idea on siinä, että lajiteltiin mitä aineistoa tahansa, niin lähes ainoa aineistosta riippuva seikka on se, miten päätetään kahdesta alkiosta kumpiko on toistaan suurempi vai ovatko ne samoja.
Siis alkioiden vertailun suorittava aliohjelma kirjoitetaan itse. Sitten tämän aliohjelman osoite viedään qsort aliohjelmalle, jolloin tarvittaessa qsort kutsuu tätä vertailijaa.
22.3.1 qsort
Seuraavassa on eräs esimerkki qsort –aliohjelman käytöstä. Ohjelma arpoo (rand) satunnaisen kokonaislukutaulukon ja tämän jälkeen järjestää sen. qsort kutsuu vertailualiohjelmaa kahdella osoitintyyppisellä muuttujalla, jotka osoittavat verrattaviin alkioihin. Osoittimet on määritelty void * –tyyppisiksi. Koska esimerkissämme alkiot ovat kokonaislukuja, pitää osoittimet käytön yhteydessä muuttaa int * –tyyppisiksi.
Vertailualiohjelman pitää palauttaa negatiivinen luku, mikäli 1. alkio on pienempi kuin toinen, 0 mikäli ne ovat samoja ja positiivinen luku, mikäli 1. on suurempi ("erotuksen etumerkki"). Siis tässä tapauksessa lukujen vähennyslasku tuottaa valmiiksi halutunlaisen tuloksen.
...
/* Alkioiden vertailufunktio. */
int sort_function(const void *a, const void *b)
{
return *(int *)a - *(int *)b;
}
int main(void)
{
int i,x[KOKO];
for (i=0; i<KOKO; i++) x[i] = rand();
tulosta(x);
qsort((void *)x,KOKO,sizeof(x[0]),sort_function);
tulosta(x);
return 0;
}
qsort –aliohjelman kutsussa pitää kertoa lajiteltavan taulukon (osaa lajitella vain taulukoita) alkuosoite, koko alkioina sekä kunkin alkion koko, sekä vertailufunktion osoite.
Huomattakoon, ettei kutsun viimeinen parametri sort_function aiheuta kutsua aliohjelmaan sort_function, vaan välittää qsort aliohjelmalle lajittelufunktion osoitteen. Kääntäjä tietää tämän eron siitä, ettei funktion nimen perässä ole sulkuja!
22.3.2 const
Edellä const void *a tarkoitti vain sitä, että tällä korostetaan ettei aliohjelmassa muuteta osoittimen a osoittamaa muistipaikkaa. Kunnon kääntäjä antaa virheen sijoituksesta
int ali(const int *a)
{
int b;
*a = 5; // Laiton sijoitus, koska const osoitin ei saa muuttaa kohdettaan
a = b; // Sallittu sijoitus, kohde ei muutu.
}
22.3.3 volatile
const–määreelle lähes päinvastainen on volatile–määre (suom. epävakaa). Tällä korostetaan sitä, että jokin taustaprosessi tai rinnakkainen aliohjelma saattaa muuttaa muistipaikan sisältöä ilman mitään ohjelmassa näkyvää sijoitusta. Esimerkiksi tietoliikenneohjelmassa voisi olla muuttuja linja_varattu, jota muuttaisi taustalla toimiva kommunikointipaketti:
volatile int linja_varattu;
...
set_param(LINE_RESERVED,&linja_varattu);
...
dial_number("112"); /* soittelee taustalla kunnes pääsee läpi */
while ( linja_varattu && !lopetus() )
do_sound(VARATTU_AANI);
...
Esimerkissä voisi olla myös
const volatile int linja_varattu;
koska itse pääohjelmalla ei ole mielekästä sijoittaa arvoa ko. muuttujaan. Nämä ominaisuudet ovat kuitenkin edistyneempien kurssien asioita.
22.3.4 sort-algoritmi
qsort –aliohjelman vikana on löysä tyypitys ja erityisesti se, ettei sille voi viedä parametrinä lisätietoa lajittelusta. STL:stä löytyy joukko algoritmeja, joita voidaan soveltaa STL:n omien tietorakenteiden lisäksi myös tavallisille taulukoille.
Edellisessä esimerkissä qsort-rivi voitaisiin korvata rivillä:
sort(x,x+KOKO);
Tällöin ei itse asiassa tarvita edes lajittelufunktiota, koska sort lajittelee oletuksena taulukon nousevaan järjestykseen edellyttäen että taulukon alkioita voidaan verrata < -operaattorilla.
Varsinainen etu sort-algoritmista saadaan, mikäli lajittelujärjestystä halutaan muuttaa tai lajittelufunktiolle halutaan antaa parametrejä. Parametrit pystytään toteuttamaan funktio-olioiden (joskus sanotaan myös funktoreiksi) avulla.
Seuraavassa esimerkissä merkkijonotaulukko lajitellaan ensin nousevaan järjestykseen luottaen string-luokan <-operaattoriin. Sitten taulukko lajitellaan laskevaan järjestykseen oman laskeva-vertailufunktion avulla. Tosin kirjastossa on valmiina funktio-oliot tämänkaltaisiin vertailuihin. Lopuksi en esimerkki, jossa jonot lajitellaan nousevaan järjestykseen jonon toisen kirjaimen perusteella. Tämä voitaisiin vielä tehdä qsort-funktiollakin, mutta jos se, monennenko kirjaimen mukaan lajittelu tehdään, halutaan parametriksi, olisi globaali muuttuja ainoa vaihtoehto qsort-funktiota käytettäessä.
sort-algoritmia (tai esimerkissä stable_sort, säilyttää ”samansuuruisten” keskinäisen järjestyksen) kutsuttaessa luodaan tilapäinen (lajittelun ajan elävä) cVertaaPaikka funktio-olio, jonka konstruktorille välitetään lajiteltavan kirjaimen indeksi. Luokkaan määritellyn ()-operaattorin ansiosta luokaa edustavaa oliota voidaan kutsua funktiomaisesti kahdella merkkijojonolla.
lajittelu\lajstr.cpp – merkkijonotaulukon lajittelu sort-algoritmilla
22.3.5 Permutaatiotaulu
Jäsenrekisteristä voitaisiin lajitella se taulukko, jossa on osoittimet jäseniin. Mikäli joku kuitenkin haluaisi säilyttää alkuperäisenkin järjestyksen, voisimme yrittää keksiä tapoja lajitella aineisto siten, että siihen olisi yhtäaikaa useita eri järjestyksiä.
Helpoin ratkaisu tähän on permutaatiotaulukoiden käyttö. Aikaisempi etsimisessä mallina ollut jäsenrekisteri voitaisiin lajitella vaikkapa nimen ja postinumeron mukaan:
**jasenet | o |
cJarjestys +------+------+ Jasen_tyyppi
+---+nimen_mukaan | +--------------+
max_koko | 7 | v |Kassinen Katto|
indekseja| 3 | +----------+ |Katto |
*indeksit| o | +--->jasenet[0] | o-----+-------------->|3452 |
+-+-+ | +----------| |... |
| |+-->jasenet[1] | o-----+--------+ +--------------+
v || +----------| | +-------------+
+---+ +++-->jasenet[2] | o-----+------+ | |Susi Sepe |
0 | 2 +-+|| +----------| | +----->|Takametsä |
+---| || ... | | | |- |
1 | 0 +--+| +----------| | |... |
+---| | | | | +-------------+
2 | 1 +---+ +----------| | +-------------+
+---| | | +------->|Ankka Aku |
3 | ? | +----------| |Ankkalinna |
+---| | | |1234 |
4 | ? | +----------| |... |
+---| | | +-------------+
5 | ? | +----------+
+---| osoitintaulukko henkilöihin
6 | ? |
+---|
7 | ? | indeksitaulukko joka osoittaa järjestyksen
+---+
cJarjestys nimen_mukaan
+-----+
max_koko | 7 | 0 1 2 3 4 5 6
indekseja| 3 | +--------------------+
*indeksit | o--+------>| 2| 0| 1| ?| ?| ?| ?|
+-----+ +--------------------+
cJarjestys postin_mukaan
+-----+
max_koko | 7 | 0 1 2 3 4 5 6
indekseja| 3 | +--------------------+
*indeksit | o--+------>| 1| 2| 0| ?| ?| ?| ?|
+-----+ +--------------------+
Lajittelu aloitetaan siten, että järjestettävä permutaatiotaulukko alustetaan johonkin järjestykseen. Esimerkiksi:
cJarjestys nimen\_mukaan
┌─────┐
max_koko │ 7 │ 0 1 2 3 4 5 6
indekseja│ 3 │ ┌──┬──┬──┬──┬──┬──┬──┐
*indeksit│ o──┼──────>│ 0│ 1│ 2│ ?│ ?│ ?│ ?│
└─────┘ └──┴──┴──┴──┴──┴──┴──┘
^ ^
a───┘ └────b
Tämän jälkeen lajitteluohjelmalle annetaan lajiteltavaksi taulukko nimen_mukaan.indeksit. sort välittää vertailufunktio-oliolle verrattavaksi indeksit a ja b. Meidän täytyy siis katsoa miten suhtautuvat toisiinsa nimet
"kerho- >anna_jasen(a).nimi"
"kerho- >anna_jasen(b).nimi"
Tämän homman hoitaa esimerkiksi <-operaattori, joka tosin järjestää skandit hieman väärään paikkaan, mutta olkoon tällä kertaa. Aliohjelma on helppo kirjoittaa uusiksi, mikäli ominaisuus haittaa enemmän. Vertailu on case–sensitive, eli Zorro < aapinen!
Jos haluamme lajittelusta aliohjelman, joka osaa järjestää minkä kentän mukaan tahansa, viedään kentän numero saada välitettyä vertailufunktio-oliolle!
Taas käytännössä permutaatiotaulu kannattaa toteuttaa vector-luokan avulla.
lajittele –aliohjelma tekee tarvittavat alustukset, mm. laittaa permutaatiotaulukon johonkin järjestykseen (tässä tapauksessa ehdollisesti, eli jos taulukolla jo on järjestys, ei sitä sotketa). Sitten aliohjelma luo vertailufunktio-olion vieden sille parametrinä käsiteltävän jäsenistön ja kentän indeksin ja lopuksi kutsuu sort-algoritmia.
Tehtävä 22.171 Etsimisen tuloksen lajittelu
Tarvitseeko lajittele - aliohjelma muutoksia, jotta se voisi lajitella myös etsimisen tuloksena saadun taulukon? Miten etsimisen tulos lajiteltaisiin etsimisen päätteeksi?
Tehtävä 22.172 Selailu puhelinnumeron mukaan
Miten käyttäjä voisi selata koko kerhon jäsenistöä puhelinnumeron mukaisessa järjestyksessä?
Tehtävä 22.173 Vertailu isot ja pienet samaistaen
Mikäli haluttaisiin järjestys, jossa aapinen < Zorro
, pitäisi vertailun ajaksi isot ja pienet kirjaimet samaistaa. Ongelmaa voitaisiin ratkaista tekemällä vertailuolion operaattori seuraavasti:
bool operator() (int a, int b) const {
string s1,s2;
s1 = jasenet->anna(a).kentta_jonoksi(kentan_nro);
s2 = jasenet->anna(b).kentta_jonoksi(kentan_nro);
jono_isoksi(s1); jono_isoksi(s2);
return s1 < s2;
}
Mitä vikaa operaattorissa kuitenkin on?
22.4 Oikeaoppinen lajittelu
Käytännössä lajittelusta on hyvin tarkat säännöt. Esimerkiksi kirjaston lajittelusääntöjen mukaan kaikki välimerkit samaistetaan. Saksalaisessa lajittelussa Ä:t samaistetaan A kirjaimeen ja englantilaisessa McAnkka ja MacAnkka lajitellaan samaan kohtaan.
Näiden sääntöjen kirjoittaminen vertaile aliohjelmaan hidastaisi lajittelua huomattavasti. Käytännössä ongelma ratkaistaan siten, että lajittelussa käytetään avaimia, jotka muodostetaan ennen lajittelun alkua.
Esimerkiksi luokassa cJasen voisi olla yksi ylimääräinen attribuutti, joka toimisi avaimena. Mikäli lajittelu tehdään nimen mukaan, muodostetaan nimestä lajittelusäännöt täyttävä avain tähän kenttään.
saksa: Äystö Yrjö - > AYSTO YRJO
Gauß Karl F - > GAUSS KARL F
suomi: Äystö Yrjö - > \YST] YRJ] (koska ASCIIssa XYZ[\] )
Mikäli lajittelu tehtäisiin sotun mukaan, muodostettaisiin sotu–kentästä avain siten, että vuosiluku siirrettäisiin ensimmäiseksi, kuukausi seuraavaksi jne.
010245- 123U - > 450201- 123U
020347- 123T - > 470302- 123T
22.4.1 vertaile
Itse vertailu olisi
bool operator() (int a, int b) const {
{
return jasenet->anna(a).Avain() < jasenet->anna(b).Avain();
}
22.4.2 lajittele
lajittele
-metodiin lisättäisiin lauseet avaimien muodostamiseksi:
...
for (unsigned j=0; j<jarj.size(); j++)
anna(jarj[j]).tee_avain(kentan_nro);
sort(jarj.begin(),jarj.end(),cVertaile(this));
22.4.3 alusta_jarjestys
Kirjainten samaistus suoritettaisiin siten, että olisi käytössä taulukko, jossa olisi kunkin kirjaimen ASCII–koodiarvon kohdalla kirjaimelle uusi koodi, sen mukaan mihin kohti kirjain lajitellaan. Voisimme esimerkiksi numeroida:
0x20 = välimerkit
0x30 = '0'
0x31 = '1'
..
0x39 = '9'
0x41 = 'A' ja 'a' (saksassa myös 'Ä' ja 'ä')
0x42 = 'B' ja 'b'
...
Tätä varten tarvitsemme luokan:
cJarjestys_avain { /* Avaimen muodostuksessa käytettä tyyppi */
char avainarvo[MERKKEJA]; /* Miten kukin kirjain muuttuu */
void alusta();
public:
cJarjestys_avain() { alusta(); )
char Avainarvo(char i) { return avainarvo[(unsigned int)i]; }
};
static cJarjestys_avain JARJESTYS;
Taulukko alustettaisiin seuraavasti:
void cJarjestys_avain::alusta() {
avainarvo[(unsigned char) ','] = 0x20;
avainarvo[(unsigned char) ' '] = 0x20;
...
avainarvo[(unsigned char) 'A'] = 0x41;
avainarvo[(unsigned char) 'a'] = 0x41;
avainarvo[(unsigned char) 'B'] = 0x42;
avainarvo[(unsigned char) 'b'] = 0x42;
...
avainarvo[(unsigned char) 'Ä'] = 0x5c;
avainarvo[(unsigned char) 'ä'] = 0x5c;
...
}
Ehkä on kuitenkin helpompi käyttää muutamaa makroa:
#define A_1 (mika,miksi) avainarvo[(unsigned char)mika] = miksi
#define A_A (mi,mp,miksi) A_1(mi,miksi); A_1(mp,miksi)
/****************************************************************************/
void cJarjestys_avain::alusta() {
{
int i;
for (i=0; i<MERKKEJA; i++) A_1(i,0x20); /* Kaikki tuntemat. merkit välil. */
A_1('0',0x30); A_1('1',0x31); A_1('2',0x32); A_1('3',0x33); A_1('4',0x34);
A_1('5',0x35); A_1('6',0x36); A_1('7',0x37); A_1('8',0x38); A_1('9',0x39);
A_A('A','a',0x41); A_A('B','b',0x42); A_A('C','c',0x43);
A_A('D','d',0x44); A_A('E','e',0x45); A_A('F','f',0x46);
A_A('G','g',0x47); A_A('H','h',0x48); A_A('I','i',0x49);
A_A('J','j',0x4a); A_A('K','k',0x4b); A_A('L','l',0x4c);
A_A('M','m',0x4d); A_A('N','n',0x4e); A_A('O','o',0x4f);
A_A('P','p',0x50); A_A('Q','q',0x51); A_A('R','r',0x52);
A_A('S','s',0x53); A_A('T','t',0x54); A_A('U','u',0x55);
A_A('V','v',0x56); A_A('W','w',0x57); A_A('X','x',0x58);
A_A('Y','y',0x59); A_A('Z','z',0x5a);
A_A('[sinvcircumflex]','',0x59);
A_A('Å','å',0x5b); A_A('Ä','ä',0x5c); A_A('Ö','ö',0x5d);
}
#undef A_1
#undef A_A
Avain muodostetaan nyt JARJESTYS.avainarvo –taulukkoa käyttäen. Kuitenkin erikoistapaukset kuten Mac ja Mc täytyisi käsitellä ensin erikseen vaikkapa Mc –>Mac.
Tehtävä 22.174 A_1 ja A_A
Kirjoita auki muutama makrojen A_1
ja A_A
esiintymä.
22.4.4 tee_avain
Seuraava on hahmotelma siitä, miten cJasen voisi alustaa avaimensa. Jos on käytössä "viisaat kenttä–luokat", niin silloin tietenkin kukin kenttä itse osaa muuttua avaimeksi.
/****************************************************************************/
void cJasen::tee_avain(int kentan_nro)
{
avain = kentta(kentan_nro).getAvain();
}
”Tyhmästä versio” voisi olla seuraavan näköinen:
/****************************************************************************/
void cJasen::tee_avain(int kentan_nro)
{
char s[20]; string st;
int i; double d;
switch (kentan_nro) {
case 1: case 3: case 5: // yms. merkkijonot
avain = kentta_jonoksi(kentan_nro);
poista_tyhjat(avain);
for (i=avain.length(); i++)
avain[i] = JARJESTYS.Avainarvo(avain[i]);
poista_tyhjat(avain);
return;
case 2: // hetukenttä
st = hetu + " ";
avain = st;
avain[0] = st[4]; /* Vaihdetaan pv ja vuosi keskenään! */
avain[1] = st[5];
avain[4] = st[0];
avain[5] = st[1];
return;
case 9: // kokonaislukukentät
i = liittymisvuosi;
sprintf(s,"%+05d",i);
avain = s;
if ( avain[0] == '- ' ) avain[0]='+'- 1; /* jotta - < + */
return;
case 10: case 11: // yms. desimaalikentät
d = kentta_doubleksi(kentan_nro); // Tätä ei ole, mutta tehtäisiin.
sprintf(s,"%+015.7lf",d);
avain = s;
if ( avain[0] == '- ' ) avain[0]='+'- 1; /* jotta - < + */
return;
default:
avain = "";
return;
}
}
Tehtävä 22.175 tee_avain
Miksi kokonais- ja reaaliluvut käsiteltiin kuten edellä eikä käyttäen aliohjelmaa kentta_jonoksi
? "Kommentoi" (eli miksi tehdään?) tee_avain
aliohjelman jokainen rivi. Miten MacAnkka
ja McAnkka
käsiteltäisiin? Miten käsiteltäisiin Gauß
?
22.5 Haku epäyhtälö –ehdoilla
Miten toteuttaisimme haun sillä tavoin, että haku olisi mahdollista myös ehdoilla:
>=50
!=*ankka*
<=Kassinen
== eli etsitään tyhjää kenttää
Puuttuuko ohjelmastamme paljon, jotta tämä voitaisiin tehdä? Mistä yleensä on kyse?
Ensin pitäisi erottaa annetusta hakuehdosta onko kyseessä normaali haku vaiko epäyhtälöhaku. Miten tämä tehtäisiin? Tutkitaan onko hakuehdon alussa jokin merkkijonoista
"==", "!=", "<=" "<" ">" ">="
Mitenkä tutkittaisiin? Esimerkiksi:
if ( maski.find("==") == 0 )...
else if ( maski.find("!=") == 0 )...
...
22.5.1 EHDOT –taulukko (kerhoets.c)
Tällainen vertailu sopii, mikäli erikoisehtoja olisi hyvin vähän. Nyt niitä kuitenkin on suhteellisen monta, joten on ehkä helpompi tehdä taulukko, jossa on ehdot ja mitä ehdoilla tehdään:
/****************************************************************************/
typedef enum {
YHT,
ERIS,
PIEN,
PIENYHT,
SUUR,
SUURYHT
} Vertailu_oper_tyyppi;
typedef struct {
char *ehto;
int pit;
Vertailu_oper_tyyppi kasky;
} Vertailu_tyyppi;
static Vertailu_tyyppi EHDOT [] = {
{ "" , 0, YHT},
{ "==", 2, YHT},
{ "!=", 2, ERIS},
{ "<=", 2, PIENYHT},
{ "<" , 1, PIEN},
{ ">=", 2, SUURYHT},
{ ">" , 1, SUUR},
{ NULL, 0, YHT}
};
22.5.2 tutki_tasmaako
Metodissa etsi_indeksit muutetaan wildmat –kutsu kutsuksi tutki_tasmaako. Aliohjelma tarvitsee muutaman lisäparametrin, jotta tarpeen vaatiessa osataan muuttaa kenttiä avaimiksi. Miksikö avaimiksi? No tietysti siksi, että avain rakennettiin siten, että aakkosjärjestys, sotujen järjestys yms. pitävät paikkansa.
Aliohjelman aluksi etsitään onko kyse jostakin erikoisoperaatiosta. Mikäli ei ole toimitaan kuten ennenkin. Mikäli on kyse jostakin erikoisesta operaatiosta, poistetaan hakumaskin alusta operaatio.
/****************************************************************************/
int /* 1 = ei täsmää */
cJasen::tutki_tasmaako( /* 0 = täsmää */
const string &maski ,/* s Maski muutettuna isoiksi kirjaimeksi */
int knro /* s Kentän numero. */
)
{
int i,tulos,ehto_ind=0; string m; cJasen mjasen;
string ikentta; // Kentän knro sisältö isoksi muutettuna merkkijonona
ikentta = kentta_jonoksi(knro); jono_isoksi(ikentta);
for (i=1; EHDOT[i].ehto; i++)
if (maski.find(EHDOT[i].ehto) == 0 ) {
ehto_ind = i; break;
}
m = maski.substr(EHDOT[ehto_ind].pit);
switch ( EHDOT[ehto_ind].kasky ) {
case YHT:
return wildmat(ikentta,m);
case ERIS:
return !wildmat(ikentta,m);
case PIEN:
case PIENYHT:
case SUUR:
case SUURYHT:
mjasen.sijoita(knro,m);
mjasen.tee_avain(knro);
tee_avain(knro);
tulos = Avain().compare(mjasen.Avain());
switch ( EHDOT[ehto_ind].kasky ) {
case PIEN: return !(tulos<0);
case PIENYHT: return !(tulos<=0);
case SUUR: return !(tulos>0);
case SUURYHT: return !(tulos>=0);
}
default:
return 1;
}
}
Tehtävä 22.176 tutki_tasmaako
Miksi EHDOT
taulukossa "<=" on ennen "<" operaatiota? "Kommentoi" metodin tutki_tasmaako
jokainen lause!
22.6 enum
Tyypissä Vertailu_oper_tyyppi "luotiin" luettelo vakioita, joille ei kuitenkaan tarvinnut itse antaa arvoa. Sama olisi voitu tehdä myös vakioilla tai #defineillä:
// Vakioilla // #definellä
const YHT = 0; #define YHT 0
const ERIS = 1; #define ERIS 1
const PIEN = 2; #define PIEN 2
const PIENYHT = 3; #define PIENYHT 3
const SUUR = 4; #define SUUR 4
const SUURYHT = 5; #define SUURYHT 5
Oman tyypin Vertailu_oper_tyyppi luomisella on kuitenkin se etu, että voidaan tehdä myös muuttuja joka on samaa tyyppiä. Tällaiseen muuttujaan ei voi sijoittaa muita arvoja (vaikka muuttuja olisikin todellisuudessa tyyppiä int). Lisäksi useat debuggerit näyttävät luettelotyyppisen muuttujan arvon nimellä eikä mitään sanomattomalla kokonaisluvulla.
Hyvässä ohjelmassa on jokaista selvästi eri luettelomaista tyyppiä kohti tehty oma luettelo ja käytetään vain ko. luettelotyypin tyyppisiä muuttujia. Esimerkiksi aliohjelmien virhepalautukset voisivat olla luettelotyyppiä. Valitettavasti luettelotyypin arvon tulostaminen tulostaa vain kokonaisluvun. Usein tulostuksia varten joudutaan tyypin rinnalle tekemään vähän EHDOT–taulukon kaltainen taulukko, jolla tyyppejä voidaan muuttaa selväkielisiksi ja päinvastoin.
Tehtävä 22.177 Viikonpäivät
Esitä sekä #define
että enum avulla miten symboleille Su
, Ma
, Ti
... saataisiin arvot 0,1,2...
Tehtävä 22.178 Pelikortit
Esitä sekä #define
että enum
avulla miten symboleille Pata
, Hertta
, Risti
ja Ruutu
saataisiin arvot 0,1,2,3. Määrittele sitten cKortti
-luokka, jossa on kortin maa
ja arvo
. Määrittele vielä luokka cKorttipakka
, jossa on 52 korttia jotka ovat tyyppiä cKortti
.
22.7 Käteismuisti
Edellä avaimen muodostus tehtiin erikseen komennolla tee_avain. Toisaalta toimintaa voitaisiin helpottaa niin, että luokassa olisikin metodi
const string &Avain(int kentan_nro);
joka toimisi siten, että luokassa olisi sisäinen attribuutti string avain ja sisäinen indeksi int avain_ind, joihin on talletettu viimeksi laskettu avaimen arvo ja mistä kentästä avain on muodostettu. Sitten joka kerta kun luokan jokin varsinainen kenttä muuttuu, asetetaan avain_ind = -1;. Nyt metodi Avain voisi toimia seuraavasti:
const string &cJasen::Avain(int kentan_nro) {
if ( avain_ind == kentan_nro ) return avain;
avain_ind = kentan_nro;
avain = ... muodosta avain kentän kentan_nro perusteella...
return avain;
}
Näin saataisiin aikaiseksi niin sanottu käteismuisti (cache): Jos tieto on viimeksi laskettu, se saadaan nopeasti, muuten se lasketaan uudelleen ja seuraavan kerran pyydettäessä samaa tietoa, se saadaan nyt nopeasti. Hyötynä olisi se, ettei tarvitse erikseen muistaa muodostaa avaimia, vaan avainta voidaan kutsua milloin vaan. Tällöin esim. vertailufunktio-olion ()-operaattori olisi:
class cVertaile {
cJasenet *jasenet;
int kentan_nro;
public:
cVertaile(cJasenet *jas,int k) { jasenet = jas; kentan_nro = k; }
bool operator() (int a, int b) const {
return jasenet->anna(a).Avain(kentan_nro) < jasenet->anna(b).Avain(kentan_nro);
}
};
...
jasenistön lajittelu:
... alustetaan permuaatiotaulu jarj ..
// avaimia ei tarvitse muodostaa
sort(jarj.begin(),jarj.end(),cVertaile(this,kentan_nro));
Lisäksi jos meillä olisi kaksi tai useampia näyttöjä käsittelemässä samaa kerhoa, ohjelma toimisi avaimenmuodostuksen osalta vaikka eri näytöt lajittelisivat rakennetta eri kentän perusteella. Tietysti käytännössä ohjelmassa on vielä monia kohtia, jossa rakenteen muuttaminen kahdesta eri paikasta sotkee ohjelman varsin pahasti. Täydellisen rinnakkaisuuden sallivan ohjelman tekeminen onkin haaste sinänsä.
22.8 Lopuksi
Tässä luvussa ohjelma on kerralla kehittynyt varsin paljon! Lukijan ei kuitenkaan pidä tälläkään kertaa erehtyä luulemaan, että kaikki olisi tehty kerralla ja oikein. Ohjelmaan on jälleen lisätty aina pienin mahdollinen lisä kerrallaan ja tämä on testattu.
Näin hitaasti edeten koko kokonaisuus on saatu valmiiksi. Välillä on jouduttu muuttamaan joidenkin aliohjelmien parametrejä, jotta ne kävisivät myös jonkin toisen ongelman ratkaisemiseen.
Jos kaikki tässä luvussa esitetty yritettäisiin lisätä kerralla ohjelmaan, tulisi sen testaamisesta varsinainen ongelma: Kuka muistaa testata kaikki kohdat joihin lisäykset vaikuttivat?
Samoin tehdyistä ratkaisuista alkaa näkyä lävitse eräs ohjelmoinnin tärkeimpiä ominaisuuksia: pyrkimys yleisyyteen ja uudelleen käytettävyyteen. Nämä ovat olio–ohjelmoinnin tunnusmerkkejä.
23. Yleisiä virheitä
Mitä tässä luvussa käsitellään?
- Käydään läpi nopeasti yleisiä "sudenkuoppia", joihin C/C++ > –ohjelmoija helposti lankeaa
C–kieli tarjoaa käyttäjälleen mm. Pascalia enemmän vapauksia. Toisaalta nämä vapaudet johtavat helposti virhetilanteisiin, joiden löytyminen saattaa olla todella vaikeata. Seuraavassa varoitukseksi koottu muutamia "sudenkuoppia".
23.1 Kirjoitusvirheet
Emme seuraavassa käsittele varsinaisia syntaksivirheitä, koska kääntäjä osaa niistä huomauttaa.
Kuitenkin muutama kirjoitusvirhe on ylitse muiden:
23.1.1 Unohtunut kommentin loppumerkki
Mikäli kommentin loppumerkki unohtuu, on kaikki kommenttia kunnes sattumoisin tulee vastaan jonkin toisen kommentin loppumerkki. Kaikki koodi tältä väliltä jää puuttumaan. Onneksi tästä yleensä seuraa määrittelemättömiä aliohjelmia yms, mistä kääntäjä osaa huomauttaa.
Mikäli saat hirveän listan täysin käsittämättömiltä tuntuvia virheilmoituksia, saattaa syynä olla puuttuva kommentin loppumerkki.
Nykyisten ohjelmointiympäristöjen (Borland C++, MS Visual C++) värilliset syntaksin esitystavat auttavat huomattavasti poistamaan näitä virheitä. Siis seuraa värejä ja hurraa niiden keksijöille.
23.1.2 Väärät tai puuttuvat lainausmerkit
Myös vakiomerkkijonoja esiteltäessä merkkijonon aloittavan tai lopettavan lainausmerkin unohtaminen saa yllätyksiä aikaan.
Lisäksi tulee muistaa, että
'a' - kirjainvakio a (tavu 61H)
"a" - merkkijonovakio jossa tavut (61H ja 00)
Lainausmerkeissäkin editorin värit auttavat!
23.2 Virheitä joista kääntäjä ei huomauta
23.2.1 switch –lauseesta puuttuu break
Erittäin petollinen on tilanne, jossa switch –lauseen jostakin case –osasta on break unohtunut pois. Pääsääntöisesti jokainen case loppuu joko break tai return. Jos oikeasti halutaan toiminta ilman break-lausetta, niin tämä on syytä mainita kommentilla.
23.2.2 Ylitetään merkkijonolle varattu tila
C–kielessä ei yleensä valvota taulukkojen indeksejä. Tästä syystä erilaiset taulukoiden ylitykset ovat yleisiä. Kaikkein useimmiten tämä tapahtuu merkkijonoille esim. strcpy –lauseella.
Myös dynaamisesti varatun jonon ylittäminen on vaarallista:
char \*p;
p = tee_jono("Kissa!");
strcpy(p,"Kissa istuu puussa!"); /\* VÄÄRIN! \*/
23.2.3 Dynaamiset muuttujat unohtuu vapauttaa
Dynaamisia muuttujia käytettäessä tulee muistaa, että vaikka niitä on helppo varata, pitää ne muistaa myös vapauttaa. Esimerkiksi seuraava on väärin aliohjelmassa:
char *ali(...)
{
char \*p;
...
p = tee_jono("Kissa!");
...
if ( f == NULL ) return VIRHE; /* !!! poistutaan vapauttamatta p:n
tilaa !!! */
...
free(p);
return NULL;
}
Jokaisella aliohjelman kutsukerralla varataan uusi tila merkkijonolle ja mikäli kutsuja tulee riittävästi, muisti loppuu pelkkiin Kissoihin! Tätä voidaan C++:ssa välttää ovelalla hajottimien käytöllä.
23.2.4 Osoitinta ei ole alustettu
Hyvin tavallinen virhe on käyttää alustamatonta osoitinta:
char *p, jono[50];
strcpy(p,"Kissa"); /* VÄÄRIN! */
p = jono+3;
strcpy(p,"Kissa"); /* Kelpaa */
23.2.5 Taulukoiden indeksointi 0..n–1
Usein unohtuu, että kun varataan taulukko
int luvut[10];
niin viimeinen indeksi onkin 9!
23.2.6 scanf
Varsinainen supervaara on scanf. Normaaleissa aliohjelmissa oikein käytettynä kääntäjä osaa varoittaa tyyppivirheestä, mikäli osoitteena esiteltyä parametriä kutsutaan arvolla. scanf on kuitenkin toteutettu siten, että tyypit saavat vapaasti olla mitä tahansa ja unohtuneesta &–merkistä ei kukaan valita.
Lisäksi toinen erittäin vaarallinen tilanne syntyy, mikäli format–osassa on eri määrä parametrejä kuin varsinaisessa parametrilistassa.
Myös parametrin tyypin ja format–osassa olevan formaatin sotkeminen saattaa kaataa koko koneen.
int i;
scanf("%lf",&i); /* VÄÄRIN! */
Aina kun kirjoittaa jonkin scanfin sukuisen lauseen, pitää havahtua ja tarkistaa ainakin 3 kertaa lauseen olevan kunnossa. Tämänkin monisteen malliohjelmia kirjoitettaessa scanf on ollut kymmeniä kertoja väärin!
23.2.7 printf
printfin kanssa on vähän vastaavia ongelmia. Tyypin ja vastaavan format–osan erot aiheuttavat kuitenkin lähinnä vain hassuja tulosteita.
Usein myös newline merkki '\n' unohtuu rivin lopusta. Tällöin tietysti kaikki tulosteet tulevat enempi tai vähempi sekaisin näytölle.
23.2.8 #define
Makrot muodostavat oikeastaan kokonaan oman virheryhmänsä. Niin paljon kuin ne auttavatkin kirjoittamista, saattavat ne tosi paljon myös sotkea ohjelmaa.
#define b 10*10
... // edellinen rivi VÄÄRIN
a = 5.0/b; /* –> a==5.0 !!!! */
Erityisesti runsas sulkujen käyttö auttaa välttämään ongelmia. Käytännössä C++-koodissa on parasta välttää #definen käyttöä!
23.2.9 Funktion prototyyppi puuttuu
Mikäli käännöksessä ei käytetä ANSI–optioita, on mahdollista kääntää ohjelman osia vaikkei funktioita olisi vielä esiteltykään. Tällöin kääntäjä tekee omat oletuksensa funktioiden tyypeistä ja ne saattavat poiketa huomattavasti siitä, mitä kirjoittaja on tarkoittanut. Tämä koskee vain puhdasta C-käännöstä, koska C++:ssa prototyypit ovat pakollisia.
23.2.10 #include –unohtuu
Jotkin ohjelman osat saattavat mennä käännöksestä läpi, vaikka tarvittavat #include lauseet olisivat unohtuneet pois. Erityisesti malloc saattaa tällöin aiheuttaa ongelmia.
23.2.11 'ä' < 'a'
Kirjainvakiot muutetaan kokonaisluvuiksi. Järjestelmästä riippuen kirjain–tyypin arvo voi olla joka etumerkitön (unsigned) (usein hyvä asia) tai etumerkillinen (signed). Itse merkin sisäinen esitys kummassakin tapauksessa on sama, mutta laajennettaessa merkki kokonaisluvuksi saattaa etumerkki sotkea koko homman. Käytännössä on kyse seuraavasta:
'a' = 0x61 = 0110 0001 –> 0000 0000 0110 0001 (unsigned)
0000 0000 0110 0001 (signed)
'ä' = 0x84 = 1000 0100 –> 0000 0000 1000 0100 (unsigned)
1111 1111 1000 0100 (signed)
Erityisesti tämä on muistettava käytettäessä kirjaimia indekseinä:
char c;
...
kirjaimet[c]++; /* Lisätään kirjainten lkm. */ /* VÄÄRIN */
...
kirjaimet[(unsigned char)c]++; /* OIKEIN! */
...
23.2.12 Väärä tyypin muunnos
Väärä tyypin muunnos saattaa myös aiheuttaa harmaita hiuksia:
Edellinen ohjelma tulostaa d = 2.00. Lausekkeen arvo kussakin vaiheessa on sama kuin sen laskemisessa siihen saakka tarvitun "monimutkaisimman" tyypin arvo. Mallissa ollaan kokoajan arvossa int. Siis myös jakolaskun tulos on int. Vika voidaan korjata kahdella tavalla:
d = i/2.0;
d = ((double)i)/2;
23.2.13 Pyöristys– ja katkaisuvirheet
Seuraava ohjelma voisi tulostaa i = 0:
Miksikö? Koska reaaliluku 0.0001 voisi sisäisenä esityksenä olla jotakin 0.000099999 ja kun tämä kerrotaan 10000:lla, tulee vähän alle 1 joka kokonaisluvuksi katkaistuna on 0!
23.2.14 Alustuksessa liian vähän pilkkuja
Esimerkiksi tietueen ja merkkijonotaulukon alustus on kriittinen pilkkujen suhteen:
char *nimet[] = {
"Mikko" :-(
"Pekka",
"Matti"
}
Tästä seuraisi kahden nimen taulukko "MikkoPekka", "Matti"!
23.2.15 Palautetaan lokaalin muuttujan osoite
Erityisesti osoittimia palauttavia funktioita tehtäessä pitää muistaa, ettei vahingossa palauta lokaalin muuttujan osoitetta:
char *kissa(void)
{
char jono[50];
strcpy(jono,"kissa");
return jono; /* VÄÄRIN! */ :-(
return "kissa"; /* OIKEIN! */
}
23.2.16 Käytetään muuttunutta muistipaikkaa
Funktiot, jotka palauttavat merkkijonojen osoitteita, saattavat aiheuttaa yllätyksiä huolimattomasti käytettyinä:
char rivi[80],*p1,*p2; int j;
...
lue_jono(N_S(rivi));
p1 = palanen(rivi," ",&j);
... /* Täällä ei viitata p1:een! */
lue_jono(N_S(rivi));
p2 = palanen(rivi," ",&j);
if ( strcmp(p1,p2 ) ... /* ON AINA p1=p2!!! */
23.2.17 Käytetään vahingossa pilkkuoperaattoria
Vaikka seuraava kutsu onkin C–kielessä laillinen, tekee se aivan muuta kuin käyttäjä olisi ajatellut:
double d;
d = 2,5; // Laskee 2 ja 5 ja sijoittaa d = 2; eli (d=2),5; :-(
24. "C–referenssi"
Mitä tässä luvussa käsitellään?
- Pikainen "C–referenssi"
Seuraavaan on koottu muutamia C–kielen taulukoita.
24.1 Varatut sanat
auto break case char
const continue default do
double else enum extern
float for goto if
int long register return
short signed sizeof static
struct switch typedef union
unsigned void volatile while
Lisäksi "varatuiksi sanoiksi" voidaan tavallaan laskea myöhemmin esiteltävät standardi–otsikkotiedostojen määräämät funktioiden ja makrojen nimet.
24.2 Borland–C:n eri lukutyyppien arvoalueet
Tyyppi | Koko | Arvoalue
----------------------------------------------------------------------------
unsigned char | 8 bits | 0 - 255
char | 8 bits | - 128 - 127
enum | 16 bits | - 32,768 - 32,767
unsigned int | 16 bits | 0 - 65,535
short int | 16 bits | - 32,768 - 32,767
int | 16 bits | - 32,768 - 32,767
unsigned long | 32 bits | 0 - 4,294,967,295
long | 32 bits | - 2,147,483,648 - 2,147,483,647
float | 32 bits | 0 ja +/- 3.4 E- 38 - 3.4 E+38
double | 64 bits | 0 ja +/- 1.7 E- 308 - 1.7 E+308
long double | 80 bits | 0 ja +/- 3.4 E- 4932 - 1.1 E+4932
24.3 Esikääntäjän direktiivit
Seuraava tulkitaan siten, että "–merkeissä oleva osa kirjoitetaan sellaisenaan. [] merkitsevät ehdollisuutta, {} 0–n kerran esiintymää ja | vaihtoehtoa, niitä ei siis kirjoiteta.
"#define" tunnus ["("tunnuslista ")"]{symboli} - makron määrittely
"#undef" tunnus - määrittelyn poisto
"#include" "<" tiedostonnimi ">" | tiedostomäärite - tiedoston lisääminen
"#if" vakiolauseke - käännetään jos != 0
"#elif" vakiolauseke - ELSE IF osa edelliseen
"#else" - ehd. käänt. ELSE- osa
"#error" virheviesti - lopettaa käännöksen
"#endif" - lopettaa ehd. käänt.
"#ifdef" tunnus - kään. jos tunnus olem.
"#if defined tunnus - synonyymi ed.
"#ifndef" tunnus - kään jos ei ole olem.
"#line" kokonaislukuliteraali [tiedostomäärite ] - vaihd. rivinro ja tied.
tiedostomäärite -> """tiedostonnimi"""
Valmiita makroja:
__DATE__ - käännöspäivä muodossa Mmm dd yyyy
__FILE__ - käännettävän tiedoston nimi
__LINE__ - rivinumero käännettävässä tiedostossa
__STDC__ - 1 jos kääntäjä tekee standardikoodia
Esimerkiksi: preceden\error.c - esimerkki virheeseen pakottamisesta
Jos kääntäjä on standardimoodissa, tulostuu:
Ohjelma ERROR.C, rivi 37 päivä Dec 17 1992
Käki kaukana kukkuu
muuten käännös päättyy virheilmoitukseen:
Nyt tuli virhe, ei standardikääntäjä
24.4 Operaattorit
Operaattori merkintä ryhmittely
-----------------------------------------------
jälkilisäys X++ - >
jälkivähennys X- -
alkio X[Y]
funktion kutsu X(Y)
tietueen alkio X.Y
osoittimen avulla X- >Y
alkion koko tavuina sizeof(X) <-
esilisäys ++X
esivähennys - - X
alkion osoite &X
epäsuora osoitus *X
etumerkki +X
neg. etumerkki - X
bittitason NOT ~X
looginen NOT !X
tyypin muunnos (tyyppi)X
kertolasku X * Y - >
jakolasku X / Y
jakojäännös X % Y
yhteenlasku X + Y - >
vähennyslasku X - Y
siirto vasemmalle X << Y - >
siirto oikealle X >> Y
pienempi X < Y - >
pienempi tai yhtäkuin X <= Y
suurempi X > Y
suurempi tai yhtäkuin X >= Y
yhtäsuuri X == Y - >
erisuuri X != Y
bittitason AND X & Y - >
bittitason XOR X ^ Y - >
bittitason OR X | Y - >
looginen AND X && Y - >
looginen OR X || Y - >
ehdollinen tulos Z ? X : Y <-
sijoitus X = Y <-
tulosijoitus X *= Y
jakosijoitus X /= Y
jakojäännössijoitus X %= Y
summasijoitus X += Y
erotussijoitus X - = Y
vas.siirtosijoitus X <<= Y
oik.siirtosijoitus X >>= Y
bittiANDsijoitus X &= Y
bittiXORsijoitus X ^= Y
bittiORsijoitus X |= Y
pilkkuoperaattori X,Y - >
Edellä ryhmittely tarkoittaa sitä, miten joukko perättäisiä operaattoreita tulkitaan:
X = Y <- , eli sijoitus ryhmitellään oikealta vasemmalle
x = y = 5; x = ( y = 5 );
24.5 C:n operaattoreiden sitomisjärjestys
Seuraavaan taulukkoon on koottu C:n operaattorit ja niiden "laskujärjestys" lausekkeen arvoa määrättäessä. Ylimpänä olevilla on korkein prioriteetti lauseketta tulkittaessa eli niillä on korkein presedenssi (precedence == etusija).
Operaattori Assosiatiivisyys
==========================================================
() [] - > . - - - >
! ~ ++ - - - 1(type cast) *2 & sizeof <- - -
*3 / % - - - >
+ - 4 - - - >
<< >> - - - >
< <= > >= - - - >
== != - - - >
& - - - >
^ - - - >
| - - - >
&& - - - >
|| - - - >
?: <- - -
= += - = *= /= %= <<= >>= &= ^= |= <- - -
, - - - >
1 etumerkkinä
2 osoittimen edessä
3 kertolaskussa
4 vähennyslaskussa
24.5.1 Esimerkkejä
if ( 0 < x && x < 10 ) ... <=> if ( ( 0 < x ) && ( x < 10 ) ) ...
*p++ <=> *(p++)
*p.k <=> *(p.k) ( ei siis (*p).k )
24.6 Erikoismerkit merkkijonoissa
Erikoismerkki Merkitys
(escape sequence)
========================================================
\" "
\' '
\? ?
\\ \
\a BEL (alarm,piippaus)
\b BS (backscape) siirto vasemmalle
\f FF (form feed) sivunvaihto
\n NL (newline) rivinvaihto
\r CR (carriage return) rivin alkuun
\t HT (horizontal tab) siirto seuraavan
vaakasuuntaisen tabulointimerkkiin
\v VT (vertical tab) siirto seuraavan
pystysuuntaisen tabulointimerkkiin
\d \dd \ddd merkin koodi oktaalilukuna
\xh \xhh merkin koodi heksadesimaalilukuna
24.7 Tulostuksen muotoilumerkit
printf, sprintf, fprintf –funktioiden formaattimerkkijonojen muotoilumerkit.
d,i etumerkillinen kymmenjärjestelmän kokonaisluku
(Decimal, Integer)
u etumerkitön kymmenjärjestelmän kokonaisluku (Unsigned)
o etumerkitön oktaalijärjestelmän kokonaisluku (Octal)
x,X etumerkitön heksadesimaalijärjestelmän kokonaisluku
(heXadecimal)
f reaaliluku muodossa [- ]ddd.ddd (Floating)
e,E reaaliluku eksoponenttimuodossa [- ]d.dddeñdd (Exponent)
g,G reaaliluku f tai eE muodossa mahdollisimman lyhyesti (General)
c yksi merkki (Character)
s merkkijono (String)
p osoitin, jonka pitää olla void - tyyppinen (Pointer)
n tämä ei tulosta mitään. Vastaavassa kohden parametri-
listaa täytyy olla osoitin kokonaislukuun, johon talle-
tetaan tämän tulostuslauseen aikana tulostettujen merk-
kien määrä.
% % - merkki
Ennen muotoilumerkkiä voi olla lisäksi (seuraavassa järjestyksessä):
- muotoilu tasataan vasemmalle (oletus on oikealle)
+ lukuarvon eteen tulostuu aina + tai -
_ (tyhjä) kuten edellinen, mutta + merkin tilalle
tulostuu välilyönti
# vaihtoehtoinen tulostusmuoto, katso tarkemmin ANSI- C
standardista tai C- kääntäjän manuaaleista
0 (nolla- merkki) täytemerkki välilyöntimerkin sijasta 0
numero.numero kentän vähimmäisleveys ja reaaliluvuissa
pisteen jälkeen tulostuvien merkkien määrä
* vastaavassa paikassa argumenttilistassa täytyy olla
kokonaisluku, joka ilmaisee kentän koon
*.numero
numero.*
*.* kuten edellä
h,l,L vastaava argumentti tyyppiä short int, double tai long double
24.7.1 Esimerkkejä
preceden\printf.c - muotoiluformaatit
123456789012345678901234567890123456789012345678901234567890
1 |Kissa|175|1.750000|
2 | Kissa| 175| 1.75000|
3 |Kissa |175 |1.75000 |
4 |0xfff000a30| af|1.75|
5 | Kissa| AF|1.750000e+00|
6 |Ki | +175|+1.8E+00|
7 | Ki|+000000175|2|
8 | Ki|+000000175|2|
24.8 Standardin mukaiset otsikkotiedostot
24.8.1 Standardit
ANSI X3.159–1989 ja ISO/IEC 9899:1990
ANSI = American National Standards Institute
ISO = International Standards Organization
24.8.2 Otsikkotiedostot
Otsikko | Sisällys lyhyesti
----------------------------------------------------------------------------
assert.h | assert debuggus makro.
ctype.h | merkkien luokitusta ja muunnoksia
errno.h | virhekoodeja
float.h | joukko minimi - ja maksimityylisiä vakioita reaaliluvuille
limits.h | kuten edellä, mutta kokonaisluvuille
locale.h | maa- ja kielikohtaisia toimintoja ja vakioita.
math.h | matemaattisia funktioita
setjmp.h | määritellään tyypit funktioille longjmp ja setjmp.
signal.h | määritellään tyypit funktioille signal ja raise.
stdarg.h | vaihtuparametristen funktioiden tekoon makroja ja funktiota
stddef.h | yleisiä makroja ja tyyppejä
stdio.h | yleisiä syöttö- ja tulostusfunktioita
stdlib.h | yleisiä makroja ja funktioita
string.h | merkkijonojen käsittelyyn liittyviä aliohjelmia
time.h | ajan käsittelyyn liittyviä makroja, tyyppejä ja funktioita
Seuraavassa on lueteltu kunkin otsikkotiedoston alla määritellyt funktiot tai funktiomaiset makrot (f:), vakiot (v:) ja tyypit (t:)
24.8.2.1 assert.h
f: assert
24.8.2.2 ctype.h
f: isalnum isalpha iscntrl isdigit isgraph
islower isprint ispunct isspace isupper
isxdigit tolower toupper
24.8.2.3 errno.h
v: errno
24.8.2.4 float.h
v: DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MAX_10_EXP DBL_MAX_EXP
DBL_MAX DBL_MIN_10_EXP DBL_MIN_EXP DBL_MIN FLT_DIG
FLT_EPSILON FLT_MANT_DIG FLT_MAX_10_EXP FLT_MAX_EXP FLT_MAX
FLT_MIN_10_EXP FLT_MIN_EXP FLT_MIN FLT_RADIX FLT_ROUNDS
LDBL_DIG LDBL_EPSILON LDBL_MANT_DIG LDBL_MAX_10_EXP
LDBL_MAX_EXP LDBL_MAX LDBL_MIN_10_EXP
LDBL_MIN_EXP LDBL_MIN
24.8.2.5 limits.h
v: CHAR_BIT CHAR_MAX CHAR_MIN INT_MAX INT_MIN
LONG_MAX LONG_MIN MB_LEN_MAX SCHAR_MAX SCHAR_MIN
SHRT_MAX SHRT_MIN UCHAR_MAX UINT_MAX ULONG_MAX
USHRT_MAX
24.8.2.6 locale.h
f: localeconv setlocale
v: LC_ALL LC_COLLATE LC_CTYPE LC_MONETARY LC_NUMERIC
LC_TIME NULL
t: lconv
24.8.2.7 math.h
f: acos asin atan atan2 ceil
cos cosh exp fabs floor
fmod frexp ldexp log log10
modf pow sin sinh sqrt
tan tanh
v: EDOM ERANGE
24.8.2.8 setjmp.h
v: longjmp setjmp
t: jmp_buf
24.8.2.9 signal.h
f: raise signal
v: SIG_DFL SIG_ERR SIG_ING SIGABRT SIGFPE
SIGILL SIGINT SIGSEGV SIGTERM
t: sig_atomic_t
24.8.2.10 stdarg.h
f: va_arg va_end va_start
t: va_list
24.8.2.11 stddef.h
f: offsetof
v: NULL
t: ptrdiff_t size_t wchar_t
24.8.2.12 stdio.h
f: clearerr fclose feof ferror fflush
fgetc fgetpos fgets fopen fprintf
fputc fputs fread freopen fscanf
fseek fsetpos ftell fwrite getc
getchar gets perror printf putc
putchar puts remove rename rewind
scanf setbuf setvbuf sprintf sscanf
tempnam tmpfile tmpnam ungetc vfprintf
vprintf vsprintf
v: _IOFBF _IOLBF _IONBF BUFSIZ EOF
FILENAME_MAX FOPEN_MAX L_tmpnam NULL SEEK_CUR
SEEK_END SEEK_SET TMP_MAX
stderr stdin stdout
t: FILE fpos_t size_t
24.8.2.13 stdlib.h
f: abort abs atexit atof atoi
atol bsearch calloc div ecvt
exit free getenv labs ldiv
malloc mblen mbtowc mbstowcs qsort
rand realloc srand strtod strtol
strtoul system wctomb wcstombs
v: EXIT_FAILURE EXIT_SUCCESS MB_CUR_MAX NULL RAND_MAX
t: div_t ldiv_t wchar_t
24.8.2.14 string.h
f: memchr memcmp memcpy memmove memset
strcat strchr strcmp strcoll strcpy
strcspn strerror strlen strncat strncmp
strncpy strpbrk strrchr strspn strstr
strtok strxfrm strupr
v: NULL
t: size_t
24.8.2.15 time.h
f: asctime clock ctime difftime gmtime
localtime mktime stime strftime time
v: CLOCKS_PER_SEC NULL
t: clock_t size_t time_t tm
25. Kirjallisuutta
Bjarne Stroustrup: The C++ Programming Language, Third Edition - Addison-Wesley, 1997. Myös suomenkielinen versio - Teknolit 2000
Matti Rintala ja Jyke Jokinen: Olioiden ohjelmointi C++:lla, satku.fi, 2000 (ISBN 952-14-0369-1)
H.M.Deitel & P.J.Deitel: C++ How to Progam, Prentice Hall, 2001 (ISBN 0-13-089571-7)
Kai Koskimies: Pieni oliokirja, Suomen ATK-kustannus Oy, 1997
Timothy A. Budd: An Introduction to Object-Oriented Programming, Second Edition - Addison Wesley, 1997
Päivi Hietanen: C++ ja olio–ohjelmointi - Teknolit, 1999
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns, Elements of Reusable Object-Oriented Software - Addison Wesley 1995 (ISBN 0-201-63361-2)
Jesse Liberty: Opeta itsellesi C++ , Suomen atk-kustannus Jyväskylä Gummerus, 1995
Bjarne Stroustrup: The Desing and Evolution of C++ - Addison-Wesley, 1994
Bjarne Stroustrup, Margaret A. Ellis: The Annotated C++ Reference Manual - Addison-Wesley, 1991
Herbert Schield: Teach Yourself C++ - Osborne McGraw-Hill, 1992
James O. Coplien: Advanced C++: Programming Styles and Idioms - Addison Wesley, 1992
Saumyendra Sengupta, Carl Phillip Korobkin: C++ Object-Oriented Data Structures - Springer-Verlag, 1994
Stephen Prata: C++ Ohjelmointi - Pagina International AB, 1992
Vesa Lappalainen, Risto Lahdelma: Olio-ohjelmointi ja C++, Luentomoniste 2, Jyväskylän Yliopisto, Tietotekniikan laitos, 1999
Muista myös ohjelmointiympäristöjen helpit!
WWW: http://www.mit.jyu.fi/~vesal/kurssit/linkkeja
http://www.cerfnet.com/~mpcline/C++-FAQs-Lite/
News-ryhmät: comp.std.c++, comp.lang.c++, sfnet.atk.ohjelmointi
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.