The page has been modified since the last reload. Refresh now?

There are {{ $ctrl.pendingUpdatesCount() }} pending paragraph updates.

{"classes": ["hidden-print"], "ra": "clinkit", "rd": "113991", "rl": "no"}

Luennot: C1 C2 C3 C4

{"classes": ["hidden-print"], "ra": "clinkit", "rd": "113991", "rl": "no"}

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:

  1. tehtävän saaminen
  2. tehtävän tarkentaminen ja tarvittavien toimintojen hahmottaminen
  3. ohjelman toimintojen ja tietorakenteiden suunnittelu, > oliosuunnittelu
  4. yksityiskohtaisten olioiden ja algoritmien suunnittelu
  5. OHJELMOINTITYÖKALUN VALINTA
  6. algoritmien/luokkien metodien tarkentaminen valitulle työkalulle
  7. ohjelmakoodin kirjoittaminen
  8. ohjelman testaus
  9. ohjelman käyttöönotto
  10. 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 erikoistapau­kset 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 ala­toiminnoista 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

  1. tiedoston lukeminen
  2. tiedoston tallettaminen
  3. henkilön tietojen kysyminen päätteeltä
  4. tiedoston lajittelu haluttuun järjestykseen
  5. tiedon etsiminen tiedostosta tietyllä hakuehdolla
  6. uuden henkilön lisääminen tiedostoon
  7. henkilön poistaminen tiedostosta

2.6.2 Alemman tason aliohjelmat

Mikäli tutkimme yo. palasia tarkemmin, tarvitsemme ehkä seuraavia pienempiä ohjelman palasia (apualiohjelmia):

  1. yhden merkin vastauksen lukeminen mahdollisen oletusarvon kanssa
  2. merkkijonon lukeminen päätteeltä siten, että sille voidaan jättää oletusarvo
  3. pitkän merkkijonon pilkkominen osamerkkijonoihin annetun merkin kohdalta
  4. loppuvälilyöntien poistaminen merkkijonosta
  5. isojen ja pienien kirjainten muuttaminen merkkijonossa esimerkiksi:
  6. sotun oikeellisuuden tarkistus
  7. 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:

  1. Hän istuu ruokapöydässä Ruotsissa ja lukee menua. Pitäisi saada selville mitä “ärtsoppa” tarkoittaa.
  2. Hän haluaa “hernekeittoa”. Mitä se on ruotsiksi?
  3. Hän näkee sukellusveneen Tukholman rannikolla ja haluaa kertoa tästä vieressään seisovalle merisotilaalle.
  4. 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:

  1. tiedoston lukeminen
  2. sanan lukeminen ja kielten erottaminen
  3. 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ä:

  1. merkkijonon lukeminen päätteeltä siten, että sille voidaan jättää oletusarvo (laitetaan oletusarvoksi tyhjä)
  2. pitkän merkkijonon pilkkominen osamerkkijonoihin annetun merkin kohdalta
  3. loppuvälilyöntien poistaminen merkkijonosta
  4. isojen ja pienien kirjainten muuttaminen merkkijonossa. Esimerkiksi:
  5. ovatko merkkijonot “*aku*” ja “AKU ANKKA” samoja?
{}



{}

Tehtävä 3.5 Aliohjelmien käyttö

  1. Mikä edellisistä sopii sanan perässä olevan kommentin poistoon (suluissa oleva sana)?
  2. Voidaanko jotain edellistä soveltaa sanan ja kielten erottamiseen?
  3. 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:

  1. siirry kummankin nimen ensimmäiseen kirjaimeen
  2. jos kummankin nimen viimeinen merkki on ohitettu, niin nimet ovat samat
  3. jos toisessa nimessä viimeinen merkki on ohitettu, niin se on ennen aakkosissa
  4. 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ä”:

  1. ota kädessä olevan kasan päällimmäinen kortti
  2. sijoita se pöydällä olevaan kasaan paikalleen
  3. 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ä”:

  1. etsi kädessäsi olevista korteista pienin
  2. laita se pöydällä olevan pinon päällimmäiseksi
  3. 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

  1. lisätä alkio aina taulukkoon oikealle paikalleen
  2. 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):

  1. vedä kädessä olevan pakan ylin kortti hieman esille ota ensimmäinen kortti tutkittavaksi
  2. vertaa tutkittavaa korttia ja esiinvedettyä korttia
  3. mikäli tutkittava on pienempi, vedä se esiin ja työnnä edellinen takaisin
  4. 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:

  1. laita uusi kortti päällimmäiseksi lajiteltuun kasaan
  2. vertaa uutta ja seuraavaa
  3. 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ä?

  1. laita pakka pöydälle kuvapuolet ylöspäin
  2. laita pakka puoliksi
  3. laita molemmat pakat pöydälle kuvapuolet ylöspäin
  4. kummassako kasassa etsittävä on?
  5. heitä se pakka pois jossa etsittävä ei ole
  6. 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
Kuva 5.1 Ehtolauseet
Kuva 5.1 Ehtolauseet
{}



{}

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
Kuva 5.2 swicth- valintalause
Kuva 5.2 swicth- valintalause



{}

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:

Kuva 5.3 do- silmukka ja do-while- silmukka
Kuva 5.3 do- silmukka ja do-while- silmukka

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 (osoit­teet) 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])?

{!!! ------------- -- ---------------------- sivu 1: C++:lla: sivu\[1\] = &Batman; Batman Gotham City 999 ------------- -- ---------------------- !!!}

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?

  1. Viekö tämä tie pääkaupunkiin?
  2. Olisitko sanonut, että tämä tie vie pääkaupunkiin, jos olisin kysynyt sinulta sitä?
  3. Onko niin, että tämä joko on tie pääkaupunkiin, tai sitten sinä puhut totta (mutta ei molemmin tavoin)?
  4. 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.

  1. Mitä saat selville kysymyksellä “Tarkoittaako BAL KYLLÄ”?
  2. 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.

  1. Välilyöntien poistaminen jonon lopusta.
  2. Ylimääräisten (2 tai useampia) välilyöntien poistaminen jonosta.
  3. Kaikkien ylimääräisten (alku-, loppu- ja monikertaiset) välilyöntien poistaminen.
  4. Jonon muuttaminen siten, että kunkin sanan 1. kirjain on iso kirjain.
  5. Tietyn merkin esiintymien laskeminen jonosta.
  6. Esiintyykö merkkijono toisessa merkkijonossa (kissatarha, sata –>; esiintyy; kissatarha, satu –>; ei esiinny).
{}



Tehtävä: Onko vuosi karkausvuosi vai ei. (Huom! 1900 ei, 2000 on)

  1. Montako karkausvuotta on kahden vuosiluvun välillä.
  2. 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.)
  3. 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

#

Please to interact with this component

/* C-kieli */
#include 
int main(void)
{
  printf("Terve! Olen C-kielellä kirjoitettu ohjelma.\n");
  return 0;
}
{}

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
#

Please to interact with this component

/* C-kieli */
#include 
int main(void)
{
  printf("Terve! Olen C-kielellä kirjoitettu ohjelma.\n");
  return 0;
}

6.1.3 C++

#

Please to interact with this component

// C++ -kieli
#include 
using namespace std;

int main()
{
     cout << "Terve!  Olen C++ -kielellä kirjoitettu ohjelma." << endl;
    return 0;
}

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

#

Please to interact with this component

// Java -kieli
package esimerkki;
public class Olen {
    public static void main(String[] args) {
        System.out.println("Terve! Olen Java-kielella kirjoitettu ohjelma.");
    }
}

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

#

Please to interact with this component

;; Common Lisp
(princ "Olen Common Lispillä kirjoitettu ohjelma.")
{}

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?

  1. C-kielisen ohjelman peruskäsitteet
  2. C++-kielisen ohjelman peruskäsitteet
  3. kääntämisen ja linkittämisen merkitys
  4. vakiotyyliset makrot (#define)
  5. 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 #includetiedoston_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

#

Please to interact with this component

/* Ohjelma tulostaa tekstin Hello world! */
#include 
int main(void)
{
      printf("Hello world!\n");
      return 0;
}
{}

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

#

Please to interact with this component

//

// Ohjelma tulostaa tekstin Hello world!
#include 
using std::cout;
using std::endl;

int main(void)
{
  cout << "Hello world!" << endl;
  return 0;
}

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

Please to interact with this component

/* Ohjelma tulostaa tekstin ??? */
#include 
int main(void)
{
      printf("Hello world!\n");
      return 0;
}
#

Please to interact with this component

// Ohjelma tulostaa ???
#include 
using std::cout;
using std::endl;

int main(void)
{
  cout << "Hello world!" << endl;
  return 0;
}

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.

Kuva 7.1 Ohjelman kääntäminen ja linkittäminen
Kuva 7.1 Ohjelman kääntäminen ja linkittäminen
{}

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 None

{ } 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

#

Please to interact with this component

/* Ohjelma tulostaa tekstin Hello world! */
#include 
#define TERVE   "Hello "
#define MAAILMA "world"
int main(void)
{
    printf(TERVE MAAILMA"!\n");
    return 0;
}

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?

  1. muuttujat
  2. malliohjelma jossa tarvitaan välttämättä muuttujia
  3. osoiteoperaattori, &
  4. osoitin muuttujat ja epäsuora osoitus, *
  5. aliohjelmat, eli funktiot
  6. erilaiset aliohjelmien kutsumekanismit
  7. 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:

Kuva 8.1 Muistipaikan osoite
Kuva 8.1 Muistipaikan osoite

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:

{}
#

Please to interact with this component

// Mitä ohjelma tulostaa?
#include 

int main(void)
{
  int kissoja,koiria;
  int *pElaimia;   // Osoitinmuuttuja kokonaislukuun. (p = pointer)

  pElaimia  = &kissoja;
  *pElaimia = 5;

  pElaimia  = &koiria;
  *pElaimia = 3;

  printf("Kissoja on %d ja koiria %d.\n",kissoja, koiria);

  return 0;
}

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:

Kuva 8.2 Epäsuora osoitus
Kuva 8.2 Epäsuora osoitus

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:

  1. Haluttu tehtävä on valmiiksi jonkun toisen kirjoittamana aliohjelmana esimerkiksi standardikirjastossa.
  1. Haluttua tehtävää suoritetaan usein liki samanlaisena joko samassa ohjelmassa tai jossain toisessa ohjelmassa.

  2. Haluttu tehtävä muodostaa selvän kokonaisuuden, jonka toiminta on ilmaistavissa muutamalla sanalla riittävän selkeästi (= aliohjelman nimi).

  3. 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ä.

  4. 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)

  1. ohjeet();– parametriton aliohjelma
  2. kysy_matka(&matka_mm); – aliohjelma joka palauttaa tuloksen parametrissään (vrt. esim. scanf).
  3. mittakaava_muunnos(matka_mm); – funktio, joka palauttaa tuloksen nimessään
  4. tulosta_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;
}

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:

  1. Osoiteoperaattori: Jos & on jo olemassa olevan muuttujan edessä, otetaan muuttujan osoite. - scanf(“%d”,&matka_mm);

  2. Referenssin esittely: Jos & on muuttujan esittelyn yhteydessä, esitellään viitemuuttuja (referenssi) - int &rMatka_mm

  3. Bittitason AND-operaattori: Jos & esiintyy kahden lausekkeen välissä, on kyseessä bittitason JA-operaattori: parillinen = luku & 0xfffe

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

  1. Jos kyseessä on osoiteparametrin välitys tulee * ylös, koska tähdet ovat taivaalla ja niitä osoitetaan, tällöin &–merkit tulevat alas!

  2. 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!

  3. 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:

Kuva 8.3 Sijoitus aliohjelmasta kutsuvan ohjelman muuttujaan
Kuva 8.3 Sijoitus aliohjelmasta kutsuvan ohjelman muuttujaan
{}



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

Pöytätesti
Pöytätesti

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?

  1. tietueet
  2. yksinkertaiset luokat
  3. olioiden perusteet
  4. olioterminologia
  5. koostaminen
  6. perintä
  7. 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.


Kuka voi käyttää metodia/attribuuttia

Oletuksena
Suojaus
kaikki
aliluokat

friend- funktiot

luokan metodit

struct
union
class
private


x
x

x
protected

x
x
x


public
x
x
x
x
x

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:

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

  2. 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 thisosoitinta 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:

Kuva 9.2 Aika perinnällä
Kuva 9.2 Aika perinnällä
{}

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.

Kuva 9.3 Musta laatikko
Kuva 9.3 Musta laatikko

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ä si­se­ne­vä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&gt;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; summa = summa + i;

i++ i = i + 1;

+= 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; i = i+1;

a = i--; a = i; i = i-1;

a = ++i; i = i+1; a = i;

a = --i; i = i-1; a = i;


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:

  1. Muuttuja johon lisäysoperaattori kohdistuu, esiintyy samassa lausekkeessa useammin kuin kerran.

  2. Makrojen kutsuissa.

  3. 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?

  1. vaatimukset ohjelman toteutukselle
  2. olioiden etsiminen
  3. CRC–kortit
  4. 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:

  1. käyttöliittymä (tekstipohjainen vaiko ikkunoitu) on voitava muuttaa > kohtuullisella ohjelmoinnilla

  2. ohjelma on voitava pienillä muutoksilla muuttaa muuksikin kuin > jäsenrekisteriksi (puhelinluettelo, levyrekisteri)

  3. jäseneen voitava helposti lisätä kenttiä

11.3 Luokat

Tavoitteiden aikaansaamiseksi näyttäisi, että tarvitsemme ohjelmassa ainakin seuraavat kolme luokkaa:

  1. käyttöliittymää ylläpitävä luokka (cNaytto)

  2. rekisteriä ylläpitävä luokka (cKerho)

  3. 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?

  1. 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ä!

Kuva 12.1 Taulukko
Kuva 12.1 Taulukko
{}

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.

Kuva 12.2 Linkitetty lista
Kuva 12.2 Linkitetty lista

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.

Kuva 12.3 Tietorakenne kun kerho tallettaa jäsenet
Kuva 12.3 Tietorakenne kun kerho tallettaa jäsenet
{}

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:

Kuva 12.4 Harrastukset linkitettynä listana
Kuva 12.4 Harrastukset linkitettynä listana
{}

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.

Kuva 12.5 Harrastukset relaation avulla
Kuva 12.5 Harrastukset relaation avulla
{}

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?

  1. C–kielen taulukot
  2. taulukoiden ja osoittimien yhteys
  3. C–merkkijonot (char *)
  4. C–merkkijonojen ja C++ merkkijonojen yhteiskäyttö
  5. päällekkäiset muistialueet (union) ja luetteloidut tyypit (enum)
  6. 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ä UNIXeis­ta­kin 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 loppu­merkille!

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:

#

Please to interact with this component

#include 
#include 
int main(void)
{
    char *p="IsoKissa", jono[10];
    strcpy(p,"Koira");          /* Tämä on tyhmää ja voi kaataa ohjelman!!! */
    strcpy(jono,"Kissa");

    printf("p: %s jono: %s\n",p,jono);
    return 0;
}

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 merkkijono­funktioihin

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ä

#

Please to interact with this component

#include 

char jono0[]="Kotka";
char jono1[5]="Koira";
int  i=0;
char jono2[5]="Kana";

int main(void)
{
    printf("Anna merkkijono>");
    jono0[4] = 'u';
    scanf("%s",jono0); 	// KAMALA VAARA!!!
    printf("|%s| |%s| %x |%s|\n",jono0, jono1,i,jono2);
    return 0;
}
{}



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

#

Please to interact with this component

#include 
#include 
using namespace std;

char jono1[5] = "";
int  i=0;
char jono2[5] = "Kana";

int main(void)
{
    cout << "Anna merkkijono>";
    cin.getline(jono1,sizeof(jono1));
    printf("|%s| %x |%s|\n",jono1,i,jono2);
    return 0;
}

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

#

Please to interact with this component

#include 
#include 
using namespace std;

void tulosta(const string &cppS)
{
    cout << cppS << endl;
}

void muuta_eka(string &cppS)
{
    cppS[0] = 'R';
}

void muuta_toka(string *cppS)
{
    (*cppS)[1] = 'u';
}

int main(void)
{
    char cs1[10] = "Kissa";
    string cppS1(cs1), cppS2("Koira");  // 1. tapa muuttaa
    cout << cppS1 << " " << cppS2 << endl;

    string cppS3,cppS4,cppS5;
    cppS3 = cs1;  cppS4 = "Kana";       // 2. tapa muuttaa
    cppS5 = 'A';                        // Jopa merkki voidaan sijoittaa
    cout << cppS3 << " " << cppS4 << " " << cppS5 << endl;

    tulosta(cppS4);     // Toimii ilman muuta!
    tulosta(cs1);       // Muuttaa automaattisesti cs1    => tilapäinen apujono
    tulosta("Mato");    // Muuttaa automaattisesti "Mato" => tilapäinen apujono

    muuta_eka(cppS3);  tulosta(cppS3); // Tottakait toimii
    // muuta_eka(cs1);    tulosta(cs1);   // Virheitä ja ei toimi
    // muuta_eka("Emu");  tulosta("Emu"); // Virhe ja onneksi ei toimi!

    muuta_toka(&cppS4); tulosta(cppS4);// Tottakait toimii
    //  muuta_toka(&cs1);   tulosta(cs1);  // Ei käänny

    return 0;
}

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

#

Please to interact with this component

#include 
#include 
#include 
using namespace std;

int montako_pta(const char *s)
{ /* oikein C:mäinen */
    int i,n = 0;
    for (i=0; s[i]; i++) n +=  s[i] == 'p';
    return n;
}

void muuta_eka(char s[])
{
    s[0] = 'R';
}

int main(void)
{
    string cppS1("Kolme porsasta padassa porisee pippurikastikkeen kera!");

    int p_lkm = montako_pta(cppS1.c_str());
    cout << "Jonossa oli " << p_lkm << " kpl p-kirjaimia\n";

    //  muuta_eka(cppS1.c_str()); // Ei onnistu! Täytyy tehdä apumuuttujan kautta
    char cs[100];
    strncpy(cs,cppS1.c_str(),100); cs[99] = 0;
    muuta_eka(cs);
    cppS1 = cs;
    cout << cppS1 << endl;

    return 0;
}

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 |          |  
---------------------------       ---------------------------  
#

Please to interact with this component

#include 

typedef enum {
    mies,
    nainen
} tSukupuoli;

typedef union {
    int syn_lapsia;
    int armeija_kayty;
} tTiedot;

typedef struct {
    char       nimi[40];
    tSukupuoli sukupuoli;
    tTiedot    tiedot;
} tHenkilo;

void tulosta_henkilo(const tHenkilo *henkilo)
{
    printf("Nimi: %-40s\n",henkilo->nimi);
    switch (henkilo->sukupuoli) {
        case mies:
            printf("Mies, armeija käyty: %d\n",henkilo->tiedot.armeija_kayty);
            break;
        case nainen:
            printf("Nainen, synnyttanyt lapsia: %d\n",henkilo->tiedot.syn_lapsia);
            break;
        default: printf("Muu\n");
    }
}

int main(void)
{
    tHenkilo Matti = {"Matti",mies,{1}}, Maija = {"Maija",nainen,{4}};

    tulosta_henkilo(&Matti);
    tulosta_henkilo(&Maija);
    return 0;
}

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:

  1. Laskee yhteen 2 matriisia.
  2. 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

#

Please to interact with this component

#include 

int main(int argc, char *argv[])
{
    int i;
    printf("Argumentteja on %d kappaletta:\n",argc);
    for (i=0; i

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ä:

  1. kyseessä on luonteeltaan oma looginen kokonaisuutensa
  2. 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

#

Please to interact with this component

#include 
#include 

typedef struct {
    int pv,kk,vv;
} Pvm_tyyppi;

static char *VAARA_MUOTO="Väärä muoto!";

const char *muuta_jono_pvmksi(const char *jono, Pvm_tyyppi *pvm)
{
    if ( sscanf(jono,"%d.%d.%d",&pvm->pv,&pvm->kk,&pvm->vv) != 3 )
        return VAARA_MUOTO;
    return NULL;
}

int tulosta_pvm_jono(const char *jono)
{
    const char *viesti;
    Pvm_tyyppi pvm;
    viesti = muuta_jono_pvmksi(jono,&pvm);
    if ( viesti ) {
        printf("%s\n",viesti);
        return 1;
    }
    printf("%02d.%02d.%d",pvm.pv,pvm.kk,pvm.vv);
    return 0;
}

int main(void)
{
    tulosta_pvm_jono("18.5.1992");  printf("\n");
    tulosta_pvm_jono("12.3");  printf("\n");

    return 0;
}
{}



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:

  1. selviä C–kielen rakenteita ei saa kommentoida. Ei siis

i=5; /* sijoitetaan i on 5 */ /* TURHA! */

  1. kuitenkin mikäli lauseella on selvä merkitys algoritmin kannalta, kommentoidaan tämä

i=5; /* aloitetaan puolestavälistä */

  1. ryhmitellään lauseet tyhjien rivien avulla loogisiksi kokonaisuuksiksi. Tällaisen kokonaisuuden alkuun voidaan laittaa kommenttirivi, joka kuvaa kaikkien seuraavien lauseiden merkitystä.

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

  3. 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).

  4. globaaleja muuttujia vältetään ‘kaikin keinoin’

  5. olioiden ansiosta globaalit muuttujat voidaan yleensä välttää kokonaan!

  6. 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;

  1. tarvittaessa määritellään useita eri nimisiä globaaleja tietueita.

  2. vakiotyyliset (alustetaan esittelyn yhteydessä eikä ole tarkoitus ikinä muuttaa) globaalit muuttujat on sallittu sellaisenaan ja niiden nimet kannattaa ehkä kirjoittaa isolla.

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

  1. #define – vakiot

  2. typedef – esitellyt tietueet

  3. #define – esitellyt yleiskäyttöiset makrot

  4. extern – muuttujien esittelyt (hyi!)

  5. funktioiden prototyypit

  6. 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 mai