Esipuhe
Monisteen alkuperäinen HTML-versio
Tämä moniste on kirjoitettu alunperin "Graafisen käyttöliittymien ohjelmointi" –kurssille syksyllä 1992. Monisteen alkuperäinen tarkoitus oli olla pikakurssi siirtymiseksi C–kielestä C++ –kieleen. Alkuperästä johtuen monisteen esimerkit eivät olleet parhaita mahdollisia olio–ohjelmoinnin esimerkkejä. C++:ssa voidaan kirjoittaa aivan muuta kuin olio–pohjaista koodia ja kääntäen jopa C–kielellä voidaan ohjelmoida oliomaisesti.
Monisteen seuraavaan painokseen on lisätty alkuun Risto Lahdelman pikakatsaus olio–ohjelmointiin yleensä. Monisteen loppuun on lisätty muutama esimerkki C++:an käytöstä Windows–ohjelmoinnissa. Monisteen kolmannen painoksen kriittisistä huomautuksista kiitokset Jonne Itkoselle.
Monisteen neljänteen painokseen on esimerkkejä viilattu hieman enemmän olio–ohjelmoinnin "sääntöjen" mukaiseksi. Myös standardikirjastojen luonnoksia on käsitelty hieman edellistä painosta runsaammin. Lopun graafisen ohjelmoinnin osa on muutettu enemmän OWL ja MFC–luokkakirjastoja vertailevaksi lisäämällä kummallakin kirjastolla tehtyjä samanlaisia esimerkkejä.
Jos lukijalle tulee parannusehdotuksia, ne otetaan mielellään vastaan ja monistetta pyritään jatkossa kehittämään paremmin olio–ohjelmointi –oppaaksi esimerkkinä C++ –kieli.
Kuten aluksi todettiin monisteen nykymuoto ei ole hyvä olio–ohjelmoinnin opas, koska eräät perintäesimerkit ovat olio–ohjelmoinnin ideologian vastaisia. Tähän versioon esimerkkejä ei kuitenkaan korjattu, koska niihin perustuvia lisäesimerkkejä oli niin paljon, ettei käytössä ollut aika olisi riittänyt uusien virheettömien esimerkkien keksimiseen.
Monisteen malliohjelmat on saatavissa sähköisesti:
FTP: kone: tarzan.math.jyu.fi
polku: /pub/pc/kurssit/winohj/oop
käyttäjätunnus: ftp
salasana: oma käyttäjätunnus
Mikroluokka: hakemisto: N:\KURSSIT\WINOHJ\OOP
WWW: URL: http://www.math.jyu.fi/~vesal/kurssit/winohj/winohj/oop
Edellä mainittuun polkuun lisätään:
joko mainittu polku tai
cpp/esim luku2:ssa
cpp/vaarat luku3:ssa
Lopuksi vielä verkosta löytynyt lainaus (Oscar Nierstrasz, Simon Gibbs, Dionysios Tsichritzis):
Reusable object classes are like poems ---
it is easy to talk about them,
but it is hard to write a good one.
Palokassa 3.9.1996 Vesa Lappalainen
1. Oliopohjainen suunnittelu ja ohjelmointi
1.1 Oliosuunnittelu
1.1.1 Tavoitteet
Ohjelmistokehityksessä pyritään mahdollisimman tehokkaisiin, helppokäyttöisiin, monipuolisiin, luotettaviin, ymmärrettäviin, ylläpidettäviin ja muunneltaviin toteutuksiin. Laajamittaisessa ohjelmistotyössä nämä tavoitteet ovat hankalia saavuttaa, varsinkin kun helppokäyttöisyys, monipuolisuus ja tehokkuus yleensä johtavat ohjelmiston koon kasvuun, ja suuri koko puolestaan useimmiten lisää mutkikkuutta.
Onkin yleensä hyväksytty tosiasia, että laajemmissa ohjelmistoissa on aina virheitä tai epäilyttävästi virheitä muistuttavia 'piirteitä' (features), eli ohjelmistoon pesiytynyt sitkeä virhe onkin korjattu muuttamalla dokumentaatio vastaamaan ohjelmiston toimintaa. Ohjelmiston kriittiseksi kooksi sanotaan sitä rajaa, jonka jälkeen yhden virheen korjaaminen keskimäärin aiheuttaa ainakin yhden uuden virheen syntymisen. Näinollen kriittiselle rajalleen kasvaneen ohjelmiston virheitä ei kannata enää summassa ruveta korjailemaan, sillä uusia virheitä syntyy samaa tahtia. Pikemminkin on yritettävä jaotella virheet vakaviin ja vähemmän vakaviin virheisiin, ja pyrittävä ensisijaisesti korjaamaan ne, jotka todella estävät ohjelman käytön. Tällaisessa ympäristössä luonnonvalinta lopulta synnyttää virhepopulaation, joka on enimmäkseen vain hieman häiritsevä.
Ohjelmiston kriittiseen kokoon vaikuttavat monet seikat kuten
- käytetyt suunnittelu- ja spesifiointimenetelmät,
- käytetty ohjelmointikieli (tai kielet),
- ohjelmiston arkkitehtuuri (esim spagetti),
- ohjelmoijien ja suunnittelijoiden henkilökohtaiset ominaisuudet eli > taito, sekä heidän lukumääränsä.
Viimeksimainitusta kohdasta sanottakoon, että niin kauan kuin ohjelmistoa kykenee kehittämään ja ylläpitämään yksi ihminen, vaikkapa 100-tuntisia työviikkoja tehden, vältytään monilta ongelmilta projektin organisoinnissa, henkilöiden välisessä kommunikaatiossa ja yleensä rinnakkain tapahtuvan ohjelmakehityksen järjestämisessä. Suuremmissa organisaatioissa ei kuitenkaan yleensä ole hyväksyttävää, että kriittisen ohjelmiston kehitys on yhden ainoan henkilön varassa, sillä tämä saattaa yllättäen kuolla tai vaihtaa työpaikkaa.
1.1.2 Mutkikkuuden hallinta
Mutkikkuuden hallinnassa tärkein periaate on ison ongelman pilkkominen useisiin pienempiin. Tilanne on analoginen vaikkapa shakkipelin kanssa: ei yleensä ruveta miettimään ensimmäistä siirtoa sillä mielellä, että mitenköhän nyt tekisin vastustajasta matin. Sen sijaan ratkaistaan ongelma esimerkiksi kolmessa vaiheessa. Ensin yritetään soveltaa tunnettua tai itse kehitettyä avausteoriaa hyvän alkuaseman saavuttamiseksi. Keskipelissä yritetään erilaisilla sommitelmilla saavuttaa materiaaliylivoima, eli lyödä vastustajalta enemmän tärkeitä nappuloita kuin itse menettää. Loppupelissä voidaankin jo tukeutua melko valmiisiin algoritmeihin vastustajan matittamiseksi, mikäli on saavutettu riittävä ylivoima. Edelleen alku-, keski- ja loppupelissä voidaan muodostaa alitavoitteita ja näin pienentää ratkaistavaa tehtävää.
Ongelman ratkaisu voidaan pilkkoa myös meta–tasolla erilaisiin vaiheisiin. Shakkipelin voittamiseksi kannattanee ensin opiskella ja harjoitella runsaasti ja vasta sitten osallistua varsinaiseen kilpailuun. Vastaavasti ohjelmistotyössä olisi ensin pyrittävä kartoittamaan ongelma mahdollisimman tarkasti, ettei vahingossa ratkaista väärää ongelmaa. Sen jälkeen kannattaa suunnitella ratkaisu mahdollisimman yleisellä tasolla, ja asteittain tarkentaen lähestyä lopullista implementaatiota. Korkean tason suunnitelmaa on paljon helpompi muuttaa hyvinkin radikaalisti kuin jo valmiiksi väärin toteutettua ohjelmaa. Laajamittaisessa ohjelmakehityksessä suunnittelua seuraa hyvinkin yksityiskohtainen määrittely ja vasta sen jälkeen toteutusvaihe.
Useimmat käytetyt ohjelmointiparadigmat tarjoavat erilaisia tapoja ongelman osittamiseen. Esimerkiksi proseduraalisessa ohjelmoinnissa aliohjelmien avulla ratkaistaan osatehtäviä ja niiden osia.
Osatehtäviin jakoa jatketaan niin pieniin osiin, että yksittäinen aliohjelma on helppo ymmärtää. Yleensä on eduksi, jos aliohjelma tulostumaan mahtuu yhdelle kirjoittimen sivulle tai jopa päätteen ruudulle. Kunkin aliohjelman sisäistä rakennetta voidaan pyrkiä selkiinnyttämään nk. rakenteisella ohjelmoinnilla (structured programming). Tällöin rajoitutaan ohjelmoinnissa peräkkäisyyteen, valintaan ja toistoon ja vältetään esimerkiksi goto-lauseiden käyttöä.
Aliohjelmista kannattaa tehdä mahdollisimman yleiskäyttöisiä siten, että samaa aliohjelmaa voidaan käyttää samassa ohjelmistossa eri osatehtävien ratkaisemisessa tai mahdollisesti myös eri sovelluksissa. Tämä pienentää ohjelmiston kokoa, vähentää ohjelmointityön määrää ja tekee siten ohjelmistosta yksinkertaisemman. Lisäksi jos samaa aliohjelmaa käytetään monissa eri paikoissa, on todennäköistä, että siinä mahdollisesti olleet virheet huomataan ja korjataan. Näinollen yleiskäyttöisyys ja etenkin uudelleenkäyttö pitemmän päälle vähentää virheitä.
Laajemmissa ohjelmistoissa aliohjelmien keskinäisiä kutsuja kuvaava puurakenne voi olla hyvinkin mutkikas, sillä sama aliohjelma voi esiintyä siinä monta kertaa ja lisäksi aliohjelmat saattavat rekursiivisesti kutsua toisiaan tai itseään. Jotta aliohjelman toiminta ohjelmiston osana olisi mahdollisimman helppo ymmärtää, ja jotta se olisi mahdollisimman yleiskäyttöinen, olisi sen mahdollisuuksien mukaan käsiteltävä ainoastaan parametreinaan saamaansa dataa, eikä globaaleja muuttujia. Jos aliohjelma on tällä tavoin sivuvaikutukseton, on mahdollista pelkkää parametrilistan määrittelyä tai kutsuriviä tarkastelemalla mahdollista selvittää, mihin tietoalkioihin se voi vaikuttaa ja mihin ei. Näin esimerkiksi ohjelmavirheitä haettaessa on mahdollista heti rajoittua niihin aliohjelmiin, jotka ovat käsitelleet virheellisiä arvoja saaneita muuttujia. Vastaavasti voidaan sivuvaikutuksetonta aliohjelmaa helposti testata itsenäisesti. Joissakin tapauksissa tosin parametrilistoista tulee kohtuuttoman pitkiä. Tällöin parametreja kannattaa sijoittaa tietueisiin.
Funktionaalisessa ohjelmoinnissa on sivuvaikutuksettomuus viety vielä pitemmälle. Puhdas funktio on aliohjelma, joka laskee parametreinaan saamastaan datasta arvon ja palauttaa sen kuten funktiot matematiikassa. Esimerkiksi funktio sin(x) laskee trigonometrisen sinus-funktion ja palauttaa sen, mutta ei muuta x:n eikä muidenkaan muuttujien arvoja. Puhtaasti funktionaalisen ohjelman testaaminen on siten vielä helpompaa ja virheettömyyden tutkimiseksi löytyy mielenkiintoisia lambda–kalkyyliin pohjautuvia teoreettisia menetelmiä. Haittapuolena on tietyn tyyppisten ongelmien ratkaisemisessa lisääntyvä koodin pituus ja sitä myötä havainnollisuuden katoaminen.
Havaitaan, että on kaksi vastakkaista lähestymistapaa ohjelmistojen parantamiseksi:
Suuri kielen ilmaisuvoima =>
tiiviimpi ja lyhyempi ohjelmistototeutus, mutta vaikeammin ymmärrettävä.
Voimakkaasti rajoitettu ohjelmointiparadigma =>
ohjelmiston osat ja niiden väliset vuorovaikutukset ovat yksinkertaisempia, helpommin testattavia ja toivottavasti virheettömämpiä, mutta ohjelmakoodin määrä kasvaa.
Olisiko ratkaisu sellainen paradigma, joka mahdollistaisi tilanteen mukaan suuren ilmaisuvoiman käytön tai ohjelmiston osien kapseloinnin siten, että niiden väliset vuorovaikutukset ovat hyvin rajoitetut?
1.1.3 Semanttiset verkot ja käsitemallit
Kaiken maailman esineitä, asioita ja ilmiöitä voidaan kuvata semanttisilla verkoilla, joissa käsitteet piirretään solmuiksi ja niiden väliset riippuvuudet kaariksi, joihin liittyy riippuvuuden tyyppi. Esimerkiksi seuraavassa kuvassa on esitetty, miten eläimet, nisäkkäät, ihmiset, opiskelijat, kurssit, R. Lahdelma ja Sim&sysanal liittyvät toisiinsa:
Ensinnäkin graafissa on kahdenlaisia solmuja. Neliskulmaiset solmut ovat joukkoja eli luokkia kuvaavia käsitteitä ja pyöreäkulmaiset yksittäisiä alkioita eli olioita. Nisäkkäiden joukko on osajoukko kaikkien eläinten joukosta, vastaavasti ihmisten joukko on osajoukko eläimistä (biologisessa mielessä) ja opiskelijat muodostavat osajoukon ihmisten joukosta (ainakin Jyväskylän yliopistossa on näin). Opiskelijat ovat kuitenkin myös nisäkkäitä ja eläimiä, mutta ei välttämättä päinvastoin. R. Lahdelma on eräs ihminen, joten myös hän on nisäkäs ja eläin. Lisäksi R. Lahdelma opettaa kurssia Sim&sysanal. Opiskelijat voivat suorittaa kursseja yleensä, ja vaikka sitä ei ole kuvaan piirrettykään, eräät opiskelijat voivat suorittaa nimenomaan Sim&sysanal-kurssin.
Jako luokkiin ja olioihin ei itse asiassa ole semanttisissa verkoissa yksikäsitteistä. Esimerkiksi Sim&sysanal voisikin tarkoittaa joukkoa eri vuosina luennoitavaa samaa kurssia. Tarvittaisiin esimerkiksi olio Sim&sysanal_94 esittämään tämän vuoden kurssia ja Sim&sysanal_95 ensi vuodelle. Yleensäkin abstrakteista käsitteistä puhuttaessa luokan ja yksittäisen olion välinen ero ei aina ole selvä. Sen sijaan konkreettiset esineet ovat yleensä selkeästi yksittäisiä olioita.
Semanttisen verkon solmuihin ja väleihin voidaan liittää erilaisia lisäattribuutteja, eli ominaisuuksia. Esimerkiksi kurssilla näitä voisivat olla opintoviikkomäärä, suoritusvaatimukset, tenttipäivämäärä jne.
1.1.4 Oliosuunnittelu ja määrittely
Jos jokin olemassaoleva tai suunniteltava systeemi pystytään kuvaamaan semanttisena verkkona, kuten yleensä aina pystytään, voidaan se välittömästi tulkita oliomalliksi. Oliopohjaiseen suunnitteluun on olemassa useita formaaleita tai puoliformaaleita notaatioita, jossa systeemistä piirretään sopivia symboleja käyttäen kuva. Tämä voidaan asteittain tarkentaa oliopohjaiseksi määrittelyksi ja edelleen oliopohjaiseksi toteutukseksi.
Oliopohjaisessa määrittelyssä kuvataan yksityiskohtaisesti eri olioiden tai luokkien tietosisältö, niille sallitut operaatiot sekä olioiden väliset riippuvuudet ja vuorovaikutukset.
Olionotaation standardiksi on tämän eepoksen kirjoittamisen jälkeen vakiintunut Unified Modeling Language (UML). Tässä oppaassa käytetään vanhempia notaatioita.
—1.2 Olio–ohjelmointi
1.2.1 Oliot, luokat ja metodit
Olio on tietorakenteen yleistys johon liittyy joukko sen käsittelemiseen erikoistuneita funktioita eli metodeja.
Oliot ovat ajasta ja paikasta riippuvia, niitä voidaan luoda, tuhota, kopioida ja päivittää. Olioilla on näinollen sisäinen tila eli tietorakenne, joka voi muuttua. Lisäksi olioilla on identiteetti: voidaan tutkia ovatko kaksi oliota sama vai eri olio siinäkin tapauksessa, että niillä sattuu olemaan sama tila.
Olion sisäinen tietorakenne voi sinänsä olla minkälainen tahansa. Erityisesti se voi sisältää toisia olioita tai viittauksia toisiin olioihin. Esimerkiksi opiskelija–olio saattaisi sisältää viittauksen kursseihin, joita hän on suorittamassa tai suorittanut ja kurssi–oliot voisivat sisältää osanottajaluettelon.
Luokka on tietotyypin yleistys ja se määrittelee joukon olioita, jotka ovat rakenteellisesti samankaltaisia, eli niillä on samanlainen sisäinen tietorakenne ja samat metodit.
Metodit ovat aliohjelmia, jotka voivat operoida olion sisäisellä tietorakenteella, muuttaa sen tilaa tai palauttaa tietoa siitä.
Olio on luokan instanssi. Kun luodaan uusi olio, on määrättävä se luokka, josta olio instantioidaan. Esimerkiksi luokasta Opiskelija voitaisiin luoda instanssit Anna Ahkera, Tiina Taitava, Timo Terävä:
Kullakin opiskelijalla on samankaltainen sisäinen tietorakenne, joka sisältää esimerkiksi opiskelijan nimen ja mahdollisesti muita tietoja. Tämä tietorakenne on määritelty kaikille yhteisesti Opiskelija–luokassa.
1.2.2 Tietoabstraktiot ja tiedon piilottaminen
Abstrakti tietotyyppi on olion tapaan tietorakenteen ja sen käsittelemiseen liittyvien operaatioiden yhdistelmä. Erityisesti abstraktin tietotyypin käsittely on kuitenkin mahdollista ainoastaan määriteltyjen operaatioiden avulla. Tämä saadaan olio–ohjelmoinnissa aikaan määrittelemällä tietorakenne yksityiseksi (private).
Abstraktille tietotyypille löytyy tarkempi määritelmä esim. osoitteesta: https://en.wikipedia.org/wiki/Abstract_data_type
—1.2.3 Oliot ja arvot
Toisin kuin oliot, arvot ovat ajasta riippumattomia vakioita, joiden sisältöä ei voi muuttaa. Esimerkiksi matemaattiset vakiot kuten nolla, yksi, pii, e ovat arvoja. Ei ole mielekästä ajatella, että ykkösen arvoa muutettaisiin. Puhtaasti funktionaalinen ohjelmointi operoi arvoilla, eli funktioiden argumentit ja paluuarvo ovat käsitteellisesti arvoja.
Vaikka oliot ja arvot ovatkin näennäisesti epäyhteensopivia, käytetään puhtaassa olioympäristössä olioita toteuttamaan arvoja. Olio, jota käytetään arvon esittämiseen täyttää seuraavat ehdot:
arvo–olio luodaan ja tuhotaan implisiittisesti tarpeen mukaan
arvo–olio on vakio - sen tilaa ei voi muuttaa
arvo–oliot, joilla on sama tila, ovat sama olio
Epäpuhtaissa oliokielessä, kuten C++, perustietotyypit kuten > kokonaisluvut ja reaaliluvut eivät itse asiassa ole olioita.
1.3 Periytyminen
1.3.1 Periytymisen semantiikka
Periytyminen (inheritance) on mekanismi, jolla joukko yliluokan ominaisuuksia voidaan sisällyttää aliluokkaan eli johdettuun luokkaan määrittelemällä yliluokka–aliluokka –relaatio. Tällöin aliluokkaan sisältyvät automaattisesti yliluokan tietorakenteet ja metodit. Periytyminen on hyödyllistä kahdesta syystä:
Eri luokkien väliset yhteiset funktionaalisuudet voidaan toteuttaa > kertaalleen yhteisessä yliluokassa. Periytyminen on siten > ohjelmien hierarkinen strukturointimekanismi .
Yliluokka–aliluokka -relaatio määrittelee luokkahierarkian siten, > että aliluokan oliot kuuluvat myös yliluokkaan eli aliluokka on > tyyppiyhteensopiva yliluokan kanssa.
Aliluokassa voidaan perittyjen ominaisuuksien lisäksi määritellä lisää uusia tietorakenteita ja metodeja tai korvata perittyjä metodeja uusilla.
Yhteisten funktionaalisuuksien toteuttaminen yliluokissa tekee ohjelmista tiiviimpiä, sillä samaa koodia ei tarvitse monistaa useaan paikkaan. Tämä tekee myös ohjelmista vähemmän virhealttiita. Perintä on myös tehokas mekanismi uudelleenkäytettävien luokkakirjastojen määrittelemiseksi.
1.3.2 Periytymishierarkia
Luokkahierarkian luoma tyyppi–yhteensopivuus tekee mahdolliseksi käyttää yliluokalle kirjoitettuja metodeja ja muita aliohjelmia myös aliluokkien olioilla, ja on siten eräs polymorfismin (ks. 1.3.5) ilmentymä.
Periytymistä kannattaa käyttää ennenkaikkea silloin, luokka on osajoukko toisesta (is-a) ja todella halutaan saada aikaan luokkahierarkian luoma tyyppi–yhteensopivuus. Luonnollisia tyyppihierarkioita ovat esimerkiksi kasvi- ja eläintieteelliset luokitukset, useat matematiikan käsitemallit ja yleensäkin monet puumaiset hierarkiat. Perintää ei pidä käyttää silloin, kun käsitemallissa kuvataan olion osia (has-a). Esimerkiksi ihmisen osia ovat kädet, pää, jalat ja keskivartalo.
Puuseppäluokkaa ei pidä määritellä siten, että perittäisiin kaksi kättä ja black&decker ihmisluokkaan, sillä puuseppä ei ole käsi eikä porakone. Sen sijaan ihmis–olioon voidaan sisällyttää viittaukset ruumiinosiin ja ihminen peritään puuseppään ja sisällytetään lisäksi viittaukset hänen työkaluihinsa.
Tehtävä 1.1 Periytymishierarkia
Piirrä periytymishierarkia, jossa on
- puuseppä ja opettaja
- kissa, kanarianlintu
- kissa, kanarianlintu ja perhonen
1.3.3 Yksiperintä ja moniperintä
Jos kullekin aliluokalle sallitaan korkeintaan yksi välitön yliluokka, puhutaan yksiperinnästä. Jos välittömiä yliluokkia voi olla useita, puhutaan moniperinnästä. Moniperintä on hyödyllistä silloin, kun olio kuuluu käsitemallissa yhtäaikaa useampaan luokkaan. Esimerkiksi graafisia käyttöliittymiä toteutettaessa halutaan toisaalta seurata jotakin sovellusspesifista luokkahierarkiaa (esim. Eläin-Selkärankainen-Nisäkäs-Apina-Simpanssi) ja toisaalta määritellä olio kuulumaan luokkaan Graafinen_Olio tai Hiirelle_Herkka_Olio tms. jolloin se ilmestyy kuvaputkelle ja on siellä käsiteltävissä.
Tehtävä 1.2 Moniperintä
Piirrä periytymishieararkia, jossa on
- pelikortti, näytölle piirrettävä objekti ja näytölle piirrettävä pelikortti
- edelliseen vielä lisäykset joilla saadaan näytöllä liikuteltava pelikortti
1.3.4 Oliopohjaisuuden edut
Oliopohjaisuus on erittäin yleinen ja ilmaisuvoimainen ohjelmointiparadigma, jota voidaan helposti käyttää yhdessä muiden paradigmojen, kuten funktionaalisen ohjelmoinnin, rakenteisen ohjelmoinnin, rinnakkaisuuden, logiikkaohjelmoinnin jne. kanssa.
Olio–ohjelmoinnissa pystytään joustavasti matkimaan oliopohjaisesti suunniteltua systeemiä, siten että jokaista käsitettä vastaa luokka tai olio.
Olio–ohjelmoinnin avulla on helppo kapseloida tietorakenteet ja niitä käsittelevät aliohjelmat abstrakteiksi tietotyypeiksi. Olio on näinollen 'aktiivinen' tai 'älykäs' tietorakenne. Oliopohjaisuus ei kuitenkaan (kuten ei mikään muukaan paradigma) pakota ohjelmoijaa tuottamaan hyvin organisoituja ohjelmia, mutta se antaa siihen hyvät työvälineet.
1.3.5 Polymorfismi eli monimuotoisuus
Funktiota sanotaan monimuotoiseksi, jos samaa funktiosymbolia voidaan käyttää eri tyyppisillä argumenteilla. Esimerkiksi aritmeettiset operaattorit +-*/ toimivat yleensä sekä kokonaisluvuilla että liukuluvuilla, ja sopeuttavat toimintansa argumenttien tyyppien mukaan.
Olio–ohjelmointi toteuttaa jo kaikkein yksinkertaisimmassa muodossa polymorfismin siten, että eri luokille voidaan määritellä saman nimisiä metodeja. Esimerkiksi graafisille olioille Circle, Rectangle ja Line voi kaikille olla määriteltynä metodi Draw, joka osaa kyseisen kuvion piirtää. Sen sijaan normaalikielissä tarvittaisiin kolme erinimistä funktiota esim Circle_Draw, Rectangle_Draw ja Line_Draw kunkin tyyppisen kuvan piirtämiseen.
1.3.6 Varhainen ja myöhäinen sidonta
Eräs tärkeä monimuotoisuuteen liittyvä käsite on myöhäinen sidonta (late binding). Tämä viittaa siihen, että laskentaympäristö vasta ajonaikana päättää, mitä funktiota on käytettävä kun kutsutaan monimuotoista funktiota. Varhaisessa sidonnassa (early binding) kääntäjä käännösaikana tutkii olion luokan ja tuottaa koodiin kutsun oikean luokan metodiin. Tämä tarkoittaa sitä, että kääntäjän on käännösaikana pystyttävä päättämään, minkä tyyppinen kukin olio on. Tämä voi tuottaa ongelmia, mikäli aliluokassa on määritelty uudestaan yliluokassa määritelty metodi ja kääntäjä ei voi tietää onko käsiteltävä yliluokan olio todellisuudessa (tyyppiyhteensopiva) aliluokan olio.
C++:ssa tämä ongelma on ratkaistu nk. virtuaalisilla funktioilla, joissa olioihin on talletettu osoitin luokka–spesifiseen taulukkoon sen luokan omista metodeista. Olkoon esimerkiksi yliluokka Graphical_Object yhteinen Circle-, Rectangle- ja Line-luokille ja kaikissa määritelty virtuaalinen Draw-metodi. Jos vaikkapa ympyrä välitetään parametrina aliohjelmalle, joka haluaa Graphical_Object-olion, ja kutsuu tämän Draw-metodia, tapahtuu kutsu hakemalla olioon talletetun osoittimen (vptr = virtual pointer) päästä Circle-luokan Draw-metodi ja kutsumalla sitä.
1.4 Oliopohjaiset ohjelmointikielet
Ensimmäinen oliopohjainen ohjelmointikieli oli Simula, joka kehitettiin Algolin laajennuksena jo vuonna 1967. Siihen kuuluivat tuolloin kaikki merkittävät olio–ohjelmoinnin piirteet kuten oliot, luokat, perintä ja virtuaaliset metodit. Kehittymätön kääntäjien toteutustekniikka ja puutteellinen ymmärtämys ohjelmointikielten syntaksin ja semantiikan suunnittelusta tekivät kuitenkin Simulasta tehottoman yleiskielenä. Tärkein este Simulan leviämiselle lienee kuitenkin ollut se, ettei sitä keksitty USA:ssa. Smalltalk on ehkä tunnetuin puhdas oliokieli, mutta sen leviämistä on rajoittanut puhtaan paradigman eksoottisuus, tulkkaavan ympäristön tehottomuus ja vaikeudet integroida Smalltalk-ympäristö muihin järjestelmiin. Lisp-järjestelmien päälle on tehty useita oliolaajennuksia kuten Lisp Flavours, LOOPS, ja CLOS. Uudempia oliokieliä ovat mm. C++ ja Eiffel.
Usein kuulee väittelyä siitä, onko jokin kieli 'oikea' oliokieli vai ei. Esimerkiksi C++ on hybridikieli, jossa on C-kielestä perittyinä proseduraalisen ja funktionaalisen kielen piirteet. Eräille puristeille tämä on liikaa. Toiset taas väittävät, että C++-kielessä ei ole tarpeeksi ominaisuuksia - esimerkiksi ajonaikaista tyypin tunnistusta (on lisätty ANSI 2.0 standardi -ehdotukseen). Merkittävin oliokielen tunnusmerkki on kuitenkin se, että sillä voi suoraan toteuttaa olioita. Muut piirteet, kuten perintä, polymorfisuus, virtuaaliset metodit tai ajonaikainen tyypin tunnistus ovat mukavia ja käyttökelpoisia lisäpiirteitä.
Mainittakoon, että oliokielet eivät sinänsä tuo ohjelmointiin mitään sellaista lisää, mitä ei voisi toteuttaa millä tahansa muulla kielellä - vaikkapa Turingin koneella - enemmän tai vähemmän hankalasti. Olio–ohjelmointia voi siten harrastaa ohjelmointikonventiona vaikkapa esimerkiksi Ada-, Assebler, C-, FORTRAN, Pascal tai Prolog -kielillä.
2. C++:n tärkeimmät erot C–kieleen
C–kieli on suurinpiirtein C++ –kielen aito osajoukko. Siis "kaikki" C–kielellä kirjoitetut ohjelmat "pitäisi" kääntyä C++ kääntäjällä. Seuraavat C:n ominaisuudet eivät toimi C++:ssa:
void -osoitin voidaan sijoittaa mihin tahansa osoittimeen
tyhjä parametrilista aliohjelman esittelyssä tarkoittaa C-kielessä ettei parametreihin "oteta kantaa". C++:ssa vastaavasti
int main() <=> int main(void)
2.1 Yleisiä uudistuksia
2.1.1 Yhden rivin kommentti
C++:ssa voidaan C–kielen normaalin kommenttimerkin lisäksi käyttää loppurivin kommenttia: //
suunta = pohjoinen; // Lähdetään aluksi ylöspäin
...
suunta = lansi; // seuraavaksi etsitään lännestä
Tehtävä 1.1 Kommentit makroissa
Mitä ongelmia kommenttimerkillä voi saada aikaan käytettynä yhdessä #define:n kanssa (voi olla kääntäjäriippuvaista)?
2.1.2 Vakiot (const)
C–kielessä vakioita voidaan tehdä vain esiprosessorin #define -direktiivillä. C++:ssa on avainsanalla const uusi lisämerkitys vakioiden määrittelemistä varten:
const.cpp - vakion käyttö C++:ssa
/* CONST.CPP */
#include <stdio.h>
const int koko = 3+3;
int main(void)
{
int a[koko],i;
i=2*koko; // i = 12, define -> 2*3+3 = 9 jos ei sulkuja
return i;
}
Tehtävä 1.2 Miksi const parempi kuin #define
Keksi muita esimerkkejä const- sanan käytöstä jossa se on parempi kuin #define (Vihje: tyypitys).
2.2 Funktiot
2.2.1 Parametrin tyypitys pakollista
C–kielessä prototyypin tai parametrin tyypityksen käyttö oli vapaaehtoista. Hyvässä ohjelmakoodissa niitä on tietysti aina käytetty. C++:ssa prototyyppien käyttö on pakollista.
proto.c - vanha C ilman prototyyppiä
/* PROTO.C */
#include <stdio.h>
void tulosta(i) /* C++ pitää olla: */
int i; /* void tulosta(int i) */
{
printf("Luku on %d\n",i);
}
int main(void)
{
tulosta(3);
return 0;
}
Tehtävä 1.3 Miksi tyypitys on tärkeä
Anna esimerkki PROTO.C- ohjelman tulosta- aliohjelman kutsusta, jossa kutsu tekee aivan muuta kuin haluttiin. Miksi sama esimerkki toimii oikein prototyypin kanssa?
2.2.2 Tyhjä parametrilista on void
C++:ssa voidaan funktion esittelystä jättää pois sana void, mikäli funktion parametrilista on tyhjä.
int main()
C–kielessähän tyhjä parametrilista ei tarkoita samaa kuin void!
Tuskinpa void sanan kirjoittaminen kuitenkaan C++ ohjelmoijalle jännetupin tulehdusta aiheuttaa, pysyypähän koodi voidin kanssa paremmin takaisin C:ksi muutettavana!
2.2.3 Lisämäärittely (overloading, polymorphism)
C–kielessä jokaiselle eri tyypille tehdylle funktiolla on keksittävä eri nimi, vaikka toiminta ja tarkoitus olisi sama. C++ –kielessä voi saman niminen funktio esiintyä useassa eri tarkoituksessa, mikäli kunkin funktion eri muodossa parametrilistat ovat erilaiset ainakin yhden tyypin kohdalta:
overload.cpp - funktion lisämäärittely (overloading)
/* OVERLOAD.CPP */
#include <stdio.h>
void tulosta(int i)
{
printf("Kokonaisluku on %d\n",i);
}
void tulosta(double d)
{
printf("Reaaliluku on %4.2lf\n",d);
}
int main(void)
{
tulosta(3); tulosta(3.2);
return 0;
}
Käytännössä kääntäjä nimeää funktion eri muodot eri nimillä funktiossa olevien tyyppien mukaan (ks. C–funktioiden käyttö). Kun funktion kutsu käännetään, etsitään kutsussa olevien parametrien tyypit ja näiden perusteella yritetään päätellä mitä funktion muotoa todella halutaan kutsua.
Jos edellä olisi kirjoitettu vain funktiot ja kutsu:
tulosta(double d)
...
tulosta(float f)
...
tulosta(3);
...
ei kääntäjä tietäisi kummaksiko 3 muutettaisiin; doubleksi vai floatiksi.
Tehtävä 1.4 Ero palautustyypissä
Voiko olla funktiot double summa(int a, int b) int summa(int a,int b) Miksi?
2.2.4 Oletusparametrit (default arguments)
Kuvitteellisesti funktion lisämäärittelemisen eräs erikoistapaus on oletusparametrit. Parametrilistan esittelyssä voidaan ilmoittaa mitä arvoa parametrilla käytetään, mikäli parametria ei ole kutsussa annettu. Funktion uudelleen määrittelyllähän voitaisiin kirjoittaa:
overloa2.cpp - funktion lisämäärittely (overloading)
/* OVERLOA2.CPP */
#include <stdio.h>
void tulosta(int i)
{
printf("Kokonaisluku on %d\n",i);
}
void tulosta(int i, int tila)
{
printf("Kokonaisluku on %*d\n",tila,i);
}
int main(void)
{
tulosta(3); tulosta(3,10);
return 0;
}
Sama voidaan kirjoittaa oletusparametrin avulla:
default.cpp - oletusparametrit
/* DEFAULT.CPP */
/* DEFAULT.CPP */
#include <stdio.h>
void tulosta(int i, int tila=1)
{
printf("Kokonaisluku on %*d\n",tila,i);
}
int main(void)
{
tulosta(3); tulosta(3,10);
return 0;
}
Oletusarvoja tulee kirjoittaa oikealta vasemmalle, eli jos yhdelläkin parametrilla on oletusarvo, pitää kaikilla sen oikealle puolella olevilla olla oletusarvo.
Oletusarvot saavat esiintyä vain funktion ensimmäisessä esittelyssä, eli ensimmäisessä prototyypissä jos funktiolle on prototyyppi tai funktion esittelyssä jollei prototyyppiä ole!
void tulosta(int i, int tila=1);
void tulosta(int i, int tila=1)
{
printf("Kokonaisluku on %*d\n",tila,i);
}
Tehtävä 1.5 Oletusarvot
Miksi oletusarvoja tulee kirjoittaa oikealta vasemmalle? Keksi 2 muuta esimerkkiä oletusarvon käytöstä.
2.2.5 Inline–funktiot
C–kielessä makrot oli ainoa tapa kirjoittaa ohjelmakoodia, jossa käännöksessä funktion kutsu varmasti jätetään pois ja kutsukohtaan kopioidaan funktion runko. Optimoiva kääntäjä saattaa kuitenkin tehdä tämän.
C++ -kielessä funktion esittelyn eteen voidaan lisätä sana inline, jolloin pyydetään, että käännöksessä funktion runko kopioidaan kutsun tilalle (kääntäjän ei tosin ole pakko uskoa tätä pyyntöä). C–kielen makroon verrattuna etu on kuitenkin se, että mahdolliset sivuvaikutukset jäävät pienemmiksi:
inline.cpp - malli inline-funktiosta
/* INLINE.CPP */
#include <stdio.h>
#define NELIO(a) ( (a) * (a) )
inline int nelio(int d)
{
return d*d;
}
int main(void)
{
int dm=2,di=2, d2m, d2i;
d2m = NELIO(++dm); // Yhyy!
d2i = nelio(++di);
printf("Makro: %3d %3d\n",dm,d2m);
printf("Inline: %3d %3d\n",di,d2i);
return 0;
}
Tietenkin makron eduksi jää vielä se, että se ei välitä parametrien tyypeistä. C++:ssahan tämä tietysti voidaan kiertää määrittelemällä useita eri inline -funktioita. Näitä määriteltäessä makrot saattavat jälleen osoittautua käteviksi kirjoituksen lyhentäjiksi!
Kaikkia funktioita ei aina toteuteta inlinena. Usein esimerkiksi silmukat ja monimutkaiset ehtorakenteet on kielletty. Tämä saattaa osittain olla kääntäjäriippuvaista.
Inline -funktioita voidaan esitellä myös toisella tavalla: kirjoitetaan funktio luokan sisään (ks. Tietueessa funktioita).
Tehtävä 1.6 Miksi inline- funktio on parempi kuin makro?
Kerro miksi edellä inline- funktio on parempi kuin vastaava #define- makro?
2.2.6 Funktiomallit (function template)
#define-makroilla voidaan tehdä tyypistä riippumaton funktio. Makrojen huonot puolet on kuitenkin jo todettu. C++:ssa voidaan template -"tyyppimäärityksen" avulla generoida useita funktiota samalla "funktiomallilla":
Käytännössä kääntäjä generoi uuden tyyppejä vastaavan funktion (generoitu funktio = template function), kun funktiomallia kutsutaan uusilla "tuntemattomilla" parametreilla.
Tehtävä 1.7 Funktiomalli
Tutki miten käy TEMPFUN.CPP:ssä jos funktioita swap ja max kutsutaan sekaparametreilla.
2.3 Tietovirrat (io–stream)
Tavallisten C-kielen printf, scanf jne. syöttö- ja tulostusfunktioiden lisäksi C++:ssa voidaan käyttää tietovirtoja:
- cin - pääsyöttövirta
- cout - päätulostusvirta
- cerr - puskuroimaton virhetulostus
- clog - puskuroitu virhetulostus
Näitä tietovirtoja käytetään esimerkiksi seuraavasti:
console.cpp - vertailu printf/scanf <-> tietovirta
/* CONSOLE.CPP */
#include <stdio.h>
#include <iostream.h>
int main(void)
{
int i=5; double d=6.7; char *jono = "Kissa";
printf("i=%d d=%lg jono=%s\n",i,d,jono);
printf("Anna uusi i d >");
scanf("%d %lf",&i,&d);
printf("i=%d d=%lg jono=%s\n",i,d,jono);
cout << "i=" << i <<" d=" << d << " jono=" <<jono <<"\n";
cout << "Anna uusi i d >";
cin >> i >> d ;
cout << "i=" << i <<" d=" << d << " jono=" <<jono <<"\n";
return 0;
}
2.3.1 Formatointi tietovirtoihin
Formatoitu tulostus tuntuu kuitenkin hankalammalta kuin printf:ää käytettäessä, kannattanee siis käyttää tuttuja tulostuslauseita kunnes pääsee sinuiksi tietovirtojen kanssa:
console2.cpp - tietovirran formatointi
i=5 d=6.7 jono=Kissa
Anna uusi i d >3 5.45678
i= 3 d= 5.46 jono='Kissa '
i= 3 d= 5.46 jono='Kissa '
i=3 d=5.46 jono='Kissa'
2.3.2 IO–manipulaattorit
Täsmälleen sama tulostus saadaan aikaiseksi käyttämällä IO–manipulaattoreita:
console3.cpp - IO-manipulaattorit
2.3.3 Tietovirtojen lisämäärittely
Operaattorit << ja >> on kuitenkin mahdollista määritellä uudelleen, joten olioiden perimisen jälkeen uudetkin oliot voidaan tulostaa samalla koodilla. Tämä puoltaa vahvasti C++:n tietovirtojen käyttöä (ks. MYSTREAM.CPP).
2.3.4 Tietovirrat ja merkkijonot
Kuten C–kielen sprintf ja scanf käsittelee merkkijonoja, voidaan myös tietovirrat ohjata merkkijonoihin:
arrayio.cpp - tietovirrat ja merkkijonot
/* ARRAYIO.CPP */
#include <stdio.h>
#include <iostream.h>
#include <strstream.h>
int main(void)
{
int i=5; double d=6.7; char *jono = "Kissa puussa"; char s[80];
char iobuf[255];
sprintf(iobuf,"i=%d d=%lg jono=%s\n",i,d,jono);
printf(iobuf);
ostrstream ojono(iobuf,sizeof(iobuf));
ojono << "i=" << i <<" d=" << d << " jono=" <<jono << endl;
cout << iobuf;
i=0; d=0; s[0]=0;
sscanf(iobuf,"i=%d d=%lf jono=%s",&i,&d,s);
printf("i=%d d=%lg jono=%s\n",i,d,s);
i=0; d=0; s[0]=0;
istrstream ijono(iobuf);
ijono.ignore(100,'='); ijono >> i;
ijono.ignore(100,'='); ijono >> d;
ijono.ignore(100,'='); ijono.getline(s,sizeof(s));
// ijono >> s; toimisi samoin kuin scanf, eli lopettaisi välilyöntiin
cout << "i=" << i <<" d=" << d << " jono=" <<s << endl;
return 0;
}
Edellä cin.getline
toimisi tietysti standardi io:lle ja on selvästi scanf("%s",s)
:ää turvallisempi ja parempi, koska myös välilyönnillisiä jonoja voidaan lukea.
2.3.5 Tiedostot ja tietovirrat
Vastaavasti ulkoiset tiedostot voidaan yhdistää tietovirtoihin:
filestre.cpp - tiedostot ja tietovirrat
/* FILESTRE.CPP */
/* Ohjelma lukee kokonaislukuja sisältävän tiedoston
des.dat ja kirjoittaa siitä tiedoston hex.dat
jossa kullakin rivillä on luku 10-järjestelmässä ja heksana
Esim:
des.dat:
10 12 15 19 175
hex.dat:
10 = 0x000a
12 = 0x000c
15 = 0x000f
19 = 0x0013
175 = 0x00af
*/
#include <stdio.h>
#include <fstream.h>
#include <iomanip.h>
#define DEC(lkm) setfill(' ') << dec << setw(lkm)
#define HEX(lkm) setfill('0') << hex << setw(lkm)
int main(void)
{
ifstream fi("des.dat"); if ( !fi ) return 1;
ofstream fo("hex.dat"); if ( !fo ) return 2;
int i;
fo << setiosflags(ios::showbase | ios::internal); // Täytä heksat!
while ( fo ) {
if ( !(fi >> i) ) break;
fo << DEC(5) << i << " = " << HEX(6) << i << endl;
}
fi.close(); fo.close();
return 0;
}
Tehtävä 1.8 Tiedostot
Kirjoita tietovirtoja käyttäen ohjelma, joka kysyy kokonaislukuja (ASCIIna) sisältävän tiedoston nimen, laskee tiedostossa olevien lukujen keskiarvon ja tulostaa keskiarvon 3:lla desimaalilla näyttöön.
2.4 Luokat (class)
C++:n tärkein olio–ohjelmointiin liittyvä lisä C–kieleen verrattuna on luokkakäsite. Luokka tarkoittaa suurinpiirtein sitä, että luokan alkioiden lisäksi luokassa voidaan esitellä ne funktiot, jotka käsittelevät luokan alkioita. Kun tähän lisätään vielä luokan periminen, saadaan olio–ohjelmointi hyötykäyttöön.
Mikäli ohjelmat on aiemmin kirjoitettu tekemällä jokaisesta omasta kokonaisuudesta uusi tietue (struct), ei olio–ohjelmointi tuota niinkään suurta muutosta ajatusmaailmaan.
2.4.1 typedef määritystä ei tarvita
C–kielessä uusi "olio"-tyyppi määriteltiin:
piste1.c - tietue C-kielessä
/* PISTE1.C */
#include <stdio.h>
typedef struct{
int x,y;
} tPiste;
void tulosta_piste(tPiste *p)
{
printf("(%d,%d)\n",p->x,p->y);
}
int main(void)
{
tPiste p={3,2};
tulosta_piste(&p);
p.x = 7; p.y = 1;
tulosta_piste(&p);
return 0;
}
C++:ssa sama voidaan esitellä muodossa
struct tPiste {
int x,y;
};
2.4.2 Tietueessa funktioita (method, member function)
C++:ssa tietueeseen voidaan kirjoittaa alkioiden (data members) lisäksi tietueen alkioita käsitteleviä funktioita - jäsenfunktioita (metodeja, luokan funktioita - methods, member functions) joko tavallisesti:
piste2.cpp - malli jäsenfunktiosta (metodista)
/* PISTE2.CPP, ei INLINE -muoto */
#include <stdio.h>
struct cPiste {
int x,y;
void tulosta() const;
};
void cPiste::tulosta() const
{
printf("(%d,%d)\n",x,y);
}
int main(void)
{
cPiste p={3,2};
p.tulosta();
p.x = 7; p.y = 1;
p.tulosta();
return 0;
}
tai jommassa kummassa INLINE-muodossa:
pistei1.cpp - malli inline-funktiosta
/* PISTEi1.CPP, INLINE -muoto 1 */
#include <stdio.h>
struct cPiste {
int x,y;
void tulosta() const;
};
inline void cPiste::tulosta() const
{
printf("(%d,%d)\n",x,y);
}
pistei2.cpp - malli inline-funktiosta
/* PISTEi2.CPP, INLINE -muoto 2 */
#include <stdio.h>
struct cPiste {
int x,y;
void tulosta() const {
printf("(%d,%d)\n",x,y);
}
};
Kaikkia funktion tulosta esittelymuotoja kutsutaan samalla tavalla. Määrittely const funktioiden nimen perässä tarkoittaa sitä, että funktiot eivät muuta (eivätkä voikaan muuttaa) itse oliota.
Huomautus! Vaikka tässä monisteessa tilan säästämiseksi usein käytetäänkin inline–muotoa 2, kannattaa oikeassa ohjelmassa ehdottomasti kirjoittaa metodit luokan esittelyn ulkopuolelle. Jos nopeussyistä inline todetaan tarpeelliseksi, voidaan muutos tehdä helposti muodolla 1.
2.4.3 this–osoitin
Jäsenfunktiolla on aina käytettävissä this -osoitin. Jos funktiota on kutsuttu esimerkiksi muodossa
p.tulosta();
on this -osoittimen arvo &p. Funktion sisällä ei ole pakko käyttää viittausta
this->x
vaan tämä on oletuksena mikäli viitataan tietueen sisäisiin alkioihin. Edellä tulosta -funktio olisi voitu kirjoittaa myös muodossa:
void piste::tulosta() const
{
printf("(%d,%d)\n",this->x,this->y);
}
Tehtävä 1.9 Tietovirrat
Kirjoita funktio cPiste::tulosta tietovirtoja käyttäen.
Tehtävä 1.10 Kumpi parempi?
Voitko tässä vaiheessa sanoa miksi C++ - versio PISTE2.CPP olisi parempi kuin C- versio PISTE1.C?
2.4.4 Alkioiden suojaukset
Edellä kaikki pääsivät käsiksi pisteen x- ja y-koordinaattiin. Tämä ei suinkaan aina ole toivottavaa ja yksi olio–ohjelmoinnin tärkeimmistä ominaisuuksista onkin tietojen suojaaminen (encapsulation): alkioon pääsee käsiksi vain sille tarkoitetut funktiot.
C++:ssa tietueen esittelyn yhteydessä alkiolle voidaan antaa seuraavia suojaustasoja:
/* PISTEPRI.CPP */
#include <stdio.h>
struct cPiste {
void tulosta() const { printf("(%d,%d)\n",x,y); }
void aseta(int nx,int ny) { x = nx; y = ny; }
private:
int x,y;
};
int main(void)
{
cPiste p;
p.aseta(3,2); p.tulosta();
p.aseta(7,1); p.tulosta();
return 0;
}
2.4.5 Luokat (class)
Koska tietue yleensä aloitetaan luokkaa eniten kuvaavien alkioiden esittelyllä ja itse tietoalkiot tahdotaan suojata, on C++:ssa uusi tietuemäärittely class, jonka alkiot ovat oletuksena suojausluokkaa private.
pistecla.cpp - malli luokasta public-määreestä
/* PISTECLA.CPP */
/* PISTECLA.CPP */
#include <stdio.h>
class cPiste {
int x,y;
public:
void tulosta() const { printf("(%d,%d)\n",x,y); }
void aseta(int nx,int ny) { x = nx; y = ny; }
};
...
Tosin moni kirjoittaja laittaa silti ensin näkyville public-jäsenet:
class cPiste {
public:
void tulosta() const { printf("(%d,%d)\n",x,y);}
void aseta(int nx,int ny) { x = nx; y = ny; }
private:
int x,y;
};
Tehtävä 1.11 Private- suojaus
Kokeile mitä tapahtuu, jos yrität tehdä sijoituksen p.x = 5 ohjelmassa PISTECLA.CPP. Voiko tulosta -funktion enää lainkaan kirjoittaa ilman C++:an jäsenfunktion käyttöä?
Tehtävä 1.12 Suojattujen tietojen palautus
Muuta ohjelmaa PISTECLA.CPP siten, että siinä on jäsenfunktiot x ja y, jotka palauttavat vastaavien alkioiden arvot. Mitä tästä on hyötyä verrattuna siihen, että x ja y olisivat publiceja?
2.4.6 Rakentajafunktio (constructor) ja hävittäjäfunktio (destructor)
C++:ssa on kaksi erikoista jäsenfunktiota (metodia): rakentaja (constructor) ja hävittäjä (destructor). Näiden nimet ovat aina luokan_nimi ja ~luokan_nimi. Funktioiden tehtävänä on huolehtia luokan alustuksesta ja hävittämisestä. Alustus tehdään kun olion vaikutusalue alkaa ja hävittäminen vastaavasti kun se loppuu. Rakentajalla voi olla useita muotoja, hävittäjällä vain yksi.
string.cpp - malli useasta konstruktorista
/* STRING.CPP */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
class cJono{
char *s;
int max_pit;
int pit;
public:
cJono(char *p) { /* Rakentaja jos alustus merkkijonolla */
max_pit = pit = strlen(p);
s = (char *)malloc(pit+1); // Huom! Oikeasti new kunhan opitaan
if ( !s ) pit = max_pit = 0;
else { strcpy(s,p); max_pit = pit; }
}
cJono(int l=255) { /* Rakentaja jos kutsu kokonaisluvulla */
pit = 0;
max_pit = l;
s = (char *)malloc(max_pit+1);
if ( !s ) max_pit = 0;
}
~cJono() { /* Hävitys aina merkkijonon poisto */
free(s); pit = max_pit = 0;
}
int pituus() const { return pit; }
char kirjain(int i=0) const { if (i<pit) return s[i]; return '#'; }
int sijoita(char *p) {
strncpy(s,p,max_pit); s[max_pit]=0;
return pit = strlen(s);
}
void tulosta(char *fmt="%s\n") const { if ( pit > 0 ) printf(fmt,s); }
};
int main(void)
{
cJono kissa("Kissa"),kana(4);
kana.sijoita("Kana");
kissa.tulosta(); // Kissa
kana.tulosta(); // Kana
kissa.sijoita("Muuttohaukka"); // Muutt
kissa.tulosta();
return 0;
}
Huomattakoon, ettei kumpikaan rakentaja eikä hävittäjä ole pakollinen. Näin on teoriassa, käytännössä kumpikin on parasta kirjoittaa!
2.5 Muuttujista
2.5.1 Viittaukset (references,&)
C–kielessä muuttujaparametrit toteutetaan osoittimien avulla. Pascalissahan vastaava ilmiö saatiin aikaiseksi VAR-määrittelyllä. C++:ssa on VAR-määrittelyä vastaava viittaustapa muuttujiin: referenssimuuttujat.
Seuraavassa perinteinen vaihtoesimerkki–ohjelma on kirjoitettuna sekä C:llä että C++:lla:
C–versio ilman referenssimuuttujia:
C++ -versio referenssimuuttujien avulla:
swap.cpp - vaihto referenssien avulla
Referenssimuuttujien etuna on siis erityisesti se, että aliohjelma voidaan muuttaa toimimaan joko arvoparametreja käyttäväksi tai muuttujaparametreja käyttäväksi vain muuttamalla aliohjelman prototyypissä muuttujien tyyppiä.
Tosin osoitemuuttujaa käytettäessä kutsuja erityisesti korostaa sitä, että muuttujien arvot tulevat muuttumaan (vrt. swap(&x,&y); tai swap(x,y);)!
Myös seuraava käyttö on mahdollista, mutta sitä ei suositella:
referenc.cpp - muuttuja referenssin avulla
/* REFERENC.CPP */
#include <stdio.h>
int main(void)
{
int i=4; int &p=i;
p = 6;
printf("i=%d p=%d\n",i,p); // Tulostaa i=6 p=6
return 0;
}
2.5.2 Muuttujien esittely kesken koodin
C–kielessä muuttujia ei saa esitellä enää sen jälkeen kun lohkossa on ollut yksikin "suoritettava lause". C++:ssa tätä rajoitusta ei ole:
varlater.cpp - muuttujien esittely kesken koodin
/* VARLATER.CPP */
#include <stdio.h>
int main(void)
{
int a=5;
printf("a = %2d\n",a);
int k=3;
printf("k = %2d\n",k);
for (int i=0; i<10; i++) { // Huom! Älä luota i:n olemassaoloon silm. jälkeen!
printf("i = %2d i^2=%3d\n",i,i*i);
}
// printf("i = %2d\n",i); // Tämä ei toimi uuden std-ehdotuksen mukaan
return 0;
}
2.5.3 Muistinkäsittely (new, delete)
C–kielen malloc on tyypitön (tai oikeastaan palauttaa void * -tyypin, joka ei C++:ssa ole sijoitusyhteensopiva muiden tyyppien kanssa). Tätä varten C++:ssa on muistinvarausoperaattori new. Esimerkiksi merkkijonoesimerkissä STRING.CPP muistinvaraus olisi voitu suorittaa:
s = new char[max_koko+1];
ja vastaavasti muistin vapautus kutsulla
delete [] s; // Huom! [] koska oli luotu taulukko! Muulloin ei.
Hakasulkuja tulee käyttää hävittämisessä, mikäli luotu olio oli taulukko. Muuten hävitetään vain taulukon ensimmäinen alkio.
2.5.4 Rakentajan ja hävittäjän kutsuminen
Seuraava esimerkki kuvannee rakentaja–funktion ja hävittäjäfunktion kutsujärjestystä:
conorder.cpp - malli konstruktorien kutsujärjestyksestä
/* CONORDER.CPP */
#include <stdio.h>
#include <string.h>
#include <conio.h>
#define RIVEJA 14
class cMalli {
int luku;
char nimi[10];
static int rivi; // Tulostusrivien laskuri
public:
cMalli(int i=0, char *s="?") {
luku = i; strcpy(nimi,s);
tulosta("Rakentaja");
}
~cMalli() { tulosta("Hävittäjä"); }
void tulosta(char *s="???") const {
rivi++;
if (rivi>RIVEJA) gotoxy(40,1+rivi-RIVEJA);
else gotoxy(1,1+rivi);
printf("%2d %-10s: %-10s %3d\n",rivi,s,nimi,luku);
}
void vaihda(int i) { luku = i; tulosta("Vaihto");}
};
int cMalli::rivi = 0; // ks. luokkamuuttujat
int main(void)
{
clrscr();
printf("--------------------------------------------------------\n");
// rivinro
cMalli m1(1,"m1"); // # 1
cMalli *p,*a;
cMalli t[3]={ // # 2-4
cMalli(10,"t[0]"),cMalli(11,"t[1]"),cMalli(12,"t[2]")
};
{
m1.tulosta("Onko muita"); // # 5
cMalli m2(2,"m2"); // # 6
m2.tulosta("Nyt on 2"); // # 7
{
cMalli m3(3,"m3"); // # 8
m3.tulosta("Nyt 3"); // # 9
p = new cMalli(4,"*p"); // #10
} // #11
} // #12
a = new cMalli[3]; // #13-15
delete p; // #16
a[1].tulosta("Dyn.taul."); // #17
for (int i=0; i<3; i++) a[i].vaihda( (i+1)*100 ); // #18-20
a[1].tulosta("Dyn.taul."); // #21
delete [] a; // #22-24
return 0;
}
Alla on ohjelman tulostus. Riveihin viitataan ohjelmakoodin kommenteissa:
1 Rakentaja : m1 1 | 15 Rakentaja : ? 0
2 Rakentaja : t[0] 10 | 16 Hävittäjä : *p 4
3 Rakentaja : t[1] 11 | 17 Dyn.taul. : ? 0
4 Rakentaja : t[2] 12 | 18 Vaihto : ? 100
5 Onko muita: m1 1 | 19 Vaihto : ? 200
6 Rakentaja : m2 2 | 20 Vaihto : ? 300
7 Nyt on 2 : m2 2 | 21 Dyn.taul. : ? 200
8 Rakentaja : m3 3 | 22 Hävittäjä : ? 300
9 Nyt 3 : m3 3 | 23 Hävittäjä : ? 200
10 Rakentaja : *p 4 | 24 Hävittäjä : ? 100
11 Hävittäjä : m3 3 | 25 Hävittäjä : t[2] 12
12 Hävittäjä : m2 2 | 26 Hävittäjä : t[1] 11
13 Rakentaja : ? 0 | 27 Hävittäjä : t[0] 10
14 Rakentaja : ? 0 | 28 Hävittäjä : m1 1
Mikäli a -taulukon tuhoamisessa ei olisi ollut taulukkomerkintää delete[] a
;, ei a:n jokaista alkiota kohti olisi kutsuttu vastaavaa hävittäjä–funktiota.
Tehtävä 1.13 new:llä luotu ei häviä itsekseen!
Todista seuraava (sopivalla malliohjelmalla): globaalin tai lokaalin objektin hajottaja (destructor) suoritetaan kun muuttuja lakkaa olemasta. Kuitenkin mikäli muuttuja on osoite olioon, hajottaja suoritetaan vain delete komennolla. Mitä tästä voi olla haittaa?
2.5.5 Luokkamuuttujat
Edellisessä esimerkissä CONORDER.CPP luokassa oli määritys
static int rivi;
Luokan staattinen muuttuja on yhteinen kaikille saman luokan olioille, eikä siitä tule varsinaista luokan jäsentä. Muuttujan alustus tehdään luokan ulkopuolella.
Luokkamuuttujat ovat tapa tehdä kapseloinnin (tiedon piilottaminen, encapsulation) mukaisia "globaaleja" muuttujia.
Tehtävä 1.14 Luokkamuuttujat
Kirjoita luokka, jonka jokainen jäsen tietää montako saman luokan jäsentä on kullakin hetkellä käytössä.
2.6 Perintä (inheritance)
Vasta olion ominaisuuksien periminen tuo uuden tehokkuuden olio–ohjelmointiin.
2.6.1 IS_A
Perintää käytettäessä olisi hyvä, jos voitaisiin sanoa että johdettu luokka (aliluokka) on kantaluokan (yliluokan) erikoistapaus. Eli esimerkiksi kantaluokkana nisäkäs ja johdettuna luokkana ihminen. Tällöin voidaan sanoa että ihminen ON nisäkäs. Tätä voidaan kutsua IS-A (is-a) -säännöksi.
2.6.2 Perinnän esimerkki
Voitaisiin esimerkiksi rakentaa graafista kirjastoa ja haluttaisiin graafisille olioille seuraavia ominaisuuksia:
- olion piirtäminen näytölle (sytyta)
- olion poistaminen näytöltä (sammuta)
- olion siirtäminen uuteen paikkaan (siirra)
- olion koon muuttaminen (muuta_koko)
Kaikki edellä mainitut ominaisuudet ovat oliosta riippumattomia lukuunottamatta olion fyysistä piirtämistä näytölle. Voisimme aluksi kirjoittaa ominaisuudet pisteelle. Ympyrälle vastaavat ominaisuudet saisimme perimällä pisteen ominaisuudet. Ympyrälle tulee tietysti lisäksi ominaisuuksiin säteen käsittely. Tämä esimerkki ei kuitenkaan olisi IS-A -säännön mukaista, sillä eihän voida sanoa että "ympyrä on piste". Ohjelmateknisesti näin voitaisiin kyllä tehdä.
Ideaa voidaan kuitenkin jalostaa tekemällä ensin yleinen graafinen olio, jolla on "kaikkien" graafisten olioiden ominaisuudet. Tästä luokasta voidaan sitten periä muita graafisia luokkia, jolloin lisätään vain uuden luokan omat "hienoudet". Yleisestä graafisesta luokasta ei voi olla yhtään esiintymää, eli varsinaista oliota. Tällaista luokkaa sanotaan abstraktiksi luokaksi.
Aluksi suunnittelemme luokkahierarkian (c=class, a=abstract):
Kuva 2.2 Graafisten kuvioiden oliohierarkia
circle.cpp - malli perimisestä
// circle.cpp, esimerkki perinnasta ja luokista/vl-96
// Todellisen piirtämisen sijasta tulostetaan näyttöön keskipiste & koko
// tulosta-metodi on vain tätä tarkoitusta varten!
#include <stdio.h>
//----------------------------------------------------------------------------
class caKuvio {
int nakyy; // Sytytetty vai sammutettu
void vaihda_paikka(int nx,int ny) { x = nx; y = ny; }
protected: // Jos halutaan periä luokkaa, pitää jälkeläisten
int x,y; // käyttöön sallittujen attrib. olla protected
public:
caKuvio(int ix=0, int iy=0) { vaihda_paikka(ix,iy); nakyy = 0; }
virtual ~caKuvio() { }
int nakyvissa() const { return nakyy; }
virtual void piirra() const = 0; // Pure virtual-metodi => abstr. luokka
void sammuta() { if ( nakyvissa() ) { nakyy = 0; piirra(); }}
void sytyta() { if ( !nakyvissa() ) { nakyy = 1; piirra(); }}
void siirra(int nx, int ny) {
if ( !nakyvissa() ) { vaihda_paikka(nx,ny); return; }
sammuta(); vaihda_paikka(nx,ny); sytyta();
}
virtual void tulosta(const char *s="") const {
printf("%-10s: %-10s (%02d,%02d)",nakyy?"Sytytetty":"Sammutettu",s,x,y);
}
}; // caKuvio
//----------------------------------------------------------------------------
class caKuvioJollaSade : public caKuvio {
int koko(int nr) { r = nr; return 0; }
protected:
int r; // Uusi atribuutti, koko
public:
caKuvioJollaSade(int ix=0,int iy=0,int ir=1) :
caKuvio(ix,iy), r(ir) {}
void muuta_koko(int nr) { // Uutena ominaisuutena koon muuttaminen
if ( !nakyvissa() ) { koko(nr); return; }
sammuta(); koko(nr); sytyta();
}
virtual void tulosta(const char *s="") const {
caKuvio::tulosta(s);
printf( " r=%d",r);
}
}; // caKuvioJollaSade
//----------------------------------------------------------------------------
class cPiste : public caKuvio {
public:
cPiste(int ix=0, int iy=0) : caKuvio(ix,iy) {}
virtual ~cPiste() { sammuta(); }
virtual void piirra() const { tulosta("Piste"); printf("\n"); }
}; // cPiste
//----------------------------------------------------------------------------
class cYmpyra : public caKuvioJollaSade {
public:
cYmpyra(int ix=0, int iy=0, int ir=1) : caKuvioJollaSade(ix,iy,ir) {}
virtual ~cYmpyra() { sammuta(); }
virtual void piirra() const { tulosta("Ympyra"); printf("\n"); }
}; // cYmpyra
//----------------------------------------------------------------------------
int main(void)
{
printf("-------------------------------------------------\n");
cPiste p1,p2(10,20); cYmpyra y1(1,1,2);
p1.sytyta(); // Sytytetty : Piste (00,00)
p2.sytyta(); // Sytytetty : Piste (10,20)
p1.siirra(7,8); // Sammutettu: Piste (00,00)
// Sytytetty : Piste (07,08)
y1.sytyta(); // Sytytetty : Ympyra (01,01) r=2
y1.muuta_koko(5); // Sammutettu: Ympyra (01,01) r=2
// Sytytetty : Ympyra (01,01) r=5
return 0; // Sammutettu: Ympyra (01,01) r=5
// Sammutettu: Piste (10,20)
// Sammutettu: Piste (07,08)
}
Piste on siis perinyt kaikki ominaisuudet graafiselta oliolta ja lisännyt tiedon siitä, miten piste "todellisuudessa" piirretään. Säteellinen olio on myös perinyt graafisen olion ominaisuudet, lisäten vielä olion kokoa kuvaavan suureen. Tästä taas ympyrä voidaan periä suoraan, kunhan kerrotaan miten tietyn kokoinen ympyrä piirretään.
Selitämme jatkossa hieman enemmän perinnän syntaksia tämän esimerkin avulla.
Tehtävä 1.15 Virtual
Kokeile mitä tapahtuu ohjelmassa CIRCLE.CPP jos kuvion piirra - metodi määritelläänkin seuraavasti: void piirra() const {}. Entä jos tämän eteen lisätään vielä sana virtual. Vaikuttaa virtual mitään tulosta- metodin edessä?
Tehtävä 1.16 Neliö
Lisää luokkahierarkiaan ja ohjelmaan CIRCLE.CPP luokka cNelio (säde on joko lävistäjän tai sivunpituuden puolikas, = pienin kuvion ympärille tai suurin kuvion sisälle piirretty ympyrä).
Tehtävä 1.17 Väri ja asento
Miten luokkahierarkia muuttuu, mikäli halutaan värillisiä kuvioita? Entä jos edelleen halutaan myös mustavalkoisia kuvioita? Entä jos mustavalkoisia kuvioita on yli 90% kaikista kuvioista. Entäpä jos halutaan eri asennoissa olevia kuvioita, kuitenkin siten, että pisteitä on 99% kaikista kuvioista?
2.6.3 Luokan esittely ja konstruktorin kutsuminen
Jos luokka periytyy toisesta luokasta, esitellään se muodossa
class jälkeläisluokka : public yliluokka { ... }
// Esim:
class cPiste : public caKuvio { ... }
Tässä public tarkoittaa, että kaikki yliluokan julkiset ominaisuudet ovat myös jälkeläisluokan julkisia ominaisuuksia. Perintä voisi olla myös private, jolloin ainoastaan jälkeläisluokan julkiset ominaisuudet olisivat julkisia.
Luokan konstruktori kutsuu lähes poikkeuksetta yliluokan konstruktoria:
cPiste(int ix=0, int iy=0) : caKuvio(ix,iy) { /* mahd. omat toimet */ }
Jos rakentaja olisi muodossa:
cPiste(int ix=0, int iy=0) { siirra(ix,iy); }
niin kutsuttaisiin ensin caKuvio()
(oletuskonstruktori, joka löytyy oletusarvojen ansiosta) ja tämän jälkeen siirrettäisiin piste (0,0):sta uuteen paikkaan. Tässä voisi jossakin muussa tapauksessa tulla kohtuuttomasti turhaa työtä. Esimerkinkin tapauksessa tulisi sijoitukset:
x=0; y=0; nakyy=0; if ( !nakyy ) { x=ix; y=iy; }
kun "oikeassa" tavassa tulee vain sijoitukset:
x=ix; y=iy; nakyy=0;
Samalla tavalla ja samasta syystä luokan omat "uudet" attribuutit alustetaan jo enen varsinaisen konstruktorin suorittamista:
caKuvioJollaSade(int ix=0,int iy=0,int ir=1) :
caKuvio(ix,iy), r(ir) { /* mahd. omat toimet */}
Tässä esimerkissä r on "tavallinen muuttuja", ei olio, joten olisi yhtä tehokasta kutsua:
caKuvioJollaSade(int ix=0,int iy=0,int ir=1) : caKuvio(ix,iy) { r=ir;}
Kuitenkin jos r olisikin olio, kutsuttaisiin jälkimmäisessä rakentajassa ensin r:n oletusrakentajaa ja sitten vielä pyydettäisiin r:n sijoitusoperaattoria suorittamaan sijoitus r=ir. Merkintä attribuutti(arvo) (esim. r(ir)) tarkoittaa siis sitä, että kun olio syntyy, niin kutsutaan sen attribuutin rakentajaa ko. arvolla. Skalaarin tapauksessa tämä rakentaja on sama kuin sijoitus.
2.6.4 const –metodit => *this pysyy vakiona
Esimerkistä kannattaa huomata runsas - ehdottomasti oikeaoppinen - const –sanan käyttö. C–kielestä onkin jo tuttu
void tulosta(const char *s); // s:n sisältöa ei voida muuttaa
Metodin (jäsenfunktion) esittelyssä vastaavasti
void tulosta(...) const;
tarkoittaa, ettei itse olio (eli sen attribuutit) muutu metodin suorituksen aikana. Siis "näkymätön" this –osoitin on const.
2.6.5 Yliluokan metodin kutsuminen
Kutsuttaessa yliluokan metodeja tulee olla tarkkana ettei vaan aiheuteta tahatonta rekursiota. Esimerkissähän oli
class caKuvioJollaSade ...
virtual void tulosta(const char *s="") const {
caKuvio::tulosta(s);
printf( " r=%d",r);
}
Jos tästä olisi unohtunut ilmoittaa minkä luokan tulosta–metodia kutsutaan:
caKuvio::tulosta(s); // Ilmoitetaan minkä luokan metodia kutsutaan
// jos tilalla olisi ollut
tulosta(s); // #%&%#
olisi seurauksena ollut päättymätön rekursio.
2.6.6 Virtuaaliset funktiot
Funktion virtuaalisuudella määrätään se, mitä funktiota kutsutaan. Ei–virtuaalisia funktioita kutsutaan aina siitä luokasta (perintätasosta), jossa niiden kutsu on määritelty (early binding, kutsu selviää käännösaikana).
Ympyrä–esimerkissä graafisen olion siirto poistaa olion näytöltä "piirtämällä" olion uudelleen (oikeasti XOR-kynällä tai negatiivisella värillä). Sitten olio piirretään uuteen paikkaansa. Itse toiminnot ovat tietysti samoja pisteelle ja ympyrälle, mutta pisteen piirtämisen sijasta pitääkin piirtää ympyrä. Määrittelemällä graafisen olion piirtofunktio virtuaaliseksi, saadaan perinnässä siirra-metodi kutsumaan ylimmän mahdollisen luokan piirra-metodia (myöhäinen sidonta, late binding, kutsu selviää ajon aikana).
Joskus - kuten tässä esimerkissä - ylimpään luokkaan halutaan virtuaalinen funktio, mutta sille ei ole mitään järkevää toimintaa. Kuitenkin halutaan, että johdetuissa luokissa funktio on pakko esitellä. Tällöin käytetään pelkkää virtuaalista funktiota (pure virtual):
virtual void piirra() = 0;
Luokkaa, jossa on yksikin pelkkä virtuaalinen funktio, sanotaan abstraktiksi luokaksi (abstract class), koska tällaista luokkaa voidaan käyttää vain perimiseen (eli luokasta ei voi olla esiintymää, instance).
Huomautus! Jos luokkaa on tarkoitus käyttää perusluokkana (sitä voidaan periä), on syytä esitellä kaikki mahdollisesti joskus muutettavat jäsenfunktiot virtuaalisiksi. Erityisesti hävittäjä PITÄÄ aina esitellä virtuaaliseksi perusluokassa (yliluokassa, superclass)!
Toisaalta jos luokkaa ei ole tarkoitus käyttää perusluokkana, ei siihen ole syytä esitellä yhtään virtuaalista funktiota (ei edes hävittäjää virtuaalisena). Syynä tähän on virtuaalifunktioiden toteutus virtuaalitaulun (vtbl = virtual table) ja sen osoittimen (vptr=virtual table pointer) avulla:
sytyta-metodi kääntyy suurinpiirtein seuraavasti:
void caKuvio::sytyta()
{
if ( !this->nakyy ) {
this->nakyy = 1;
this->vptr->piirra();
}
}
Jos luokassa on turhaan virtuaalisia funktiota, menetetään luokan yhteensopivuus C–tietueiden kanssa. Samoin esimerkiksi piste-luokka jossa olisi vain x- ja y-koordinaatit mahtuisi ilman virtuaalisia funktioita yhteen long-muuttujaan ja näin sen käyttö saattaisi olla huomattavasti nopeampaa esimerkiksi parametrin välityksessä.
Toisaalta usein sanotaan virtuaalisten funktioiden kutsujen olevan hitaampia kuin vastaavien ei–virtuaalisten funktioiden kutsujen. Tämä on totta esimerkiksi sytyta-funktiossa. Jos kuitenkin kutsutaan p1.piirra(), niin kääntäjä voi heti ratkaista oikean kutsun käännösaikana käyttämättä virtuaalitaulua apuna. Olio–osoittimen tapauksessa kutsu p->piirra() joudutaan ratkaisemaan ajon aikana virtuaalitaulun avulla.
2.6.7 Olio–osoittimet ja virtuaaliset funktiot (=> polymorfismi)
Polymorfismi (polymorphism, monimuotoisuus) on parhaimmillaan osoitinmuuttujia käytettäessä. Osoitinmuuttuja voi olla kantaluokan osoittimeksi esitelty, mutta siitä huolimatta siihen voidaan sijoittaa johdetun luokan olion osoite. Tällöin käytettävissä on tietysti vain ne olion ominaisuudet, jotka ovat samoja kuin kantaluokassa. Virtuaalisten funktioiden tapauksessa kutsutaan saman (tai lähinnä ylimmän = perityn) luokan funktiota, johon osoitin sillä hetkellä osoittaa.
Lisätään esimerkiksi edelliseen ympyrä–esimerkkiin (CIRCLE.CPP) osoitin graafiseen olioon. Seuraavassa ensimmäiset kutsut kutsuvat pisteen funktioita ja jälkimmäiset ympyrän funktioita:
...
cPiste a; cYmpyra y;
caKuvio *p; // p voi osoittaa mihin tahansa saman tai aliluokan olioon!
p = &a; p->sytyta(); p->siirra(1,2); // Piirtää pisteen
p = &y; p->sytyta(); p->siirra(4,5); // Piirtää ympyrän
Tässä ei ole kyseessä sama asia kuin funktioiden lisämäärittelyssä (overloading), päinvastoin, virtuaalisten funktioiden tulee olla määritelty täsmälleen samoin parametrein kussakin luokassa.
2.6.8 Miksi pitää olla virtuaalinen hävittäjä?
Nimenomaan olio–osoittimien takia perusluokan mahdolliset muutettavat funktiot on esiteltävä virtuaalisiksi. Samoin olio–osoittimien takia myös kantaluokan hävittäjä on esiteltävä virtuaaliseksi. Miksi?
Lisätään edelliseen esimerkkiin vielä dynaaminen ympyrä:
...
p = new cyYmpyra(11,12,13);
p->sytyta();
delete p;
...
Miksi ~cGraafinenOlio pitää olla virtuaalinen ja miksi jälkeläisiin pitää esitellä oma hävittäjä? Eikö riittäisi että abstraktin kuvion hävittäjä olisi muotoa
~caKuvio() { sammuta(); }
Tässähän sammuta()
kutsuu virtuaalitaulun kautta piirtämisfunktiota ja koska p osoittaa ympyrän tietoihin, niin vieläpä ympyrän piirtämistä? Ei riitä! Nimittäin jos kuvion hävittäjä ei ole virtuaalinen, kutsutaan operaattorilla delete p suoraan kuvion hävittäjää ja olion tyyppinä pidettäisiin kuviota. Tällöin käytetäisiin kuvion pure virtual–funktiota, joka on laitonta.
Entä kelpaisiko jos graafisen olion hävittäjä olisi muotoa:
virtual ~caKuvio() { sammuta(); }
mutta ympyrällä EI olisi omaa hävittäjää? Tämäkään ei kelpaa! Hävittäjä toimii siten, että se "vaihtaa" olion virtuaalitaulun osoittimen (vptr) aina osoittamaan sen luokan virtuaalitaulua (vtbl), jonka luokan hävittäjää ollaan suorittamassa. Eli ensin oltaisiin ympyrän virtuaalitaulussa, jossa ei olisi hävittäjää. Seuraavaksi oltaisiin säteellisen kuvion virtuaalitaulussa, jossa ei myöskään olisi hävittäjää. Lopulta ~caKuvion kutsuma sammuta() suorittaa taas laittoman kutsun caKuvio::piirra(). Siis vptr "pienenee" luokkahierarkian purkautuessa kun perittyjen luokkien hävittäjiä käydään läpi. Itse asiassa myös rakentaja toimii vastaavasti. Rakentajan kussakin vaiheessa olion vptr osoittaa sen luokan virtuaalitauluun, jonka luokan rakentajassa ollaan menossa. Viimeisenähän ollaan aina itse juuri rakennettavan luokan rakentajassa ja näin vptr jää osoittamaan hävittäjään saakka oikeata arvoa! (Viimeinen versio hävittäjästä toimisi tässä erikoistapauksessa jos ympyrällä on oma hävittäjä: ympyrän sammutahan laittaa nakyy=0, jolloin ~caKuvion kutsuma sammuta ei teekään mitään.)
Eli hävittäjän tulee olla virtuaalinen ja sen tulee "perua" ainoastaan ne toimenpiteet, joita saman luokan rakentaja tai jokin muu metodi on suorittanut! Ei saa yrittää "korjata" mahdollisia tulevien luokkien jälkiä hävittäjässä!
Tehtävä 1.18 Miksi metodit virtuaalisiksi
Keksi esimerkki, joka havainnollistaa miksi kantaluokan hävittäjän tulee olla virtuaalinen. Keksi myös esimerkki miksei ei–virtuaalista funktiota ole suotavaa määritellä uudelleen.
2.6.9 Sekasijoitukset
Lopuksi tutkimme miten perittyjä olioita on mahdollista sijoittaa toisilleen. Esimerkkiin lisätään muutamia "seka"-sijoituksia:
cGraafinenOlio *p;
cPiste a(7,2),b; cYmpyra y(5,4,12); y.sytyta();
...
b = a; // OK!
//a = y; // Ei toimi jollei ole konstruktoria
// cPiste(cYmpyra)
p = &a; // OK! Tällä voidaan kiertää edell.
*p = y; // "a = y". Pisteelle ympyrän
// keskipiste ja näkyy (väärin!?).
//y = b; // Ei toimi jollei ole konstruktoria
// cYmpyra(cPiste);
p = &y; // OK! => y = b
*p = b; // Sijoistus laillinen koska
// caKuvio<cPiste
// Sijoitetaan ne pisteen ominaisuud
// jotka on graafisessa oliossa, eli
// kaikki. p:n osoittama tyyppi ei
// muutu, eli säilyy ympyränä. Tosin
// "sammuu", mikä ei ole oikein.
Huomattakoon, että sijoituksissa "viipaloidaan" olioiden data–osat (attribuutit) toiseen olioon. vptria ei sijoiteta, eli olion luokka ei muutu. "Seka"-sijoituksissa osa data–alkioista menetetään ja tätä kutsutaan englanniksi nimellä slicing problem. Erityisesti ongelma ilmenee arvoparametrien välityksessä ja tässä onkin jälleen yksi syy välittää osoittimia joko suoraan tai käyttäen referenssejä (josta taas saattaa seurata muita ongelmia, mm. eri osoitteiden viitatessa samaan olioon = aliasing).
Huomautus: Hierarkiassa "uudempi" voidaan sijoittaa "vanhempaan":
class cA ....
class cB : public cA ...
...
cA a; cB b;
a = b; // OK! Eli aliluokan olio voidaan sijoitta yliluokkaan. Slicing
b = a; // Ei sellaisenaan mielekäs, koska osa attribuutteja jää ilman arvoa.
Tehtävä 1.19 Miksi näkyvyyden muuttuminen on väärin
Miksi edellä on sanottu, että näkyvyyden muuttuminen on väärin sijoituksissa p=y ja p=b?
2.6.10 RTTI (Run Time Type Information)
Palataan vielä esimerkkiin, jossa p on osoitin graafiseen olioon. Vaikka sijoituksen p = &y jälkeen voisikin kuvitella, että voidaan tehdä kutsu
p->muuta_koko(5);
ei tämä ole mahdollista, koska graafisessa oliossa ei ole ao. metodia. Toisaalta jos ollaan VARMOJA siitä, että p osoittaa ympyrään, voidaan tietysti tehdä tyyppikonversio:
((cYmpyra *)p)->muuta_koko(5);
Mistä tämä varmuus saadaan? Viimeisimmässä standardiluonnoksessa on tähän joitakin keinoja:
#include <typeinfo.h> // Esittelee typeinfo-luokan
...
printf("%s\n",typeid(*p).name); // Jos halutaan merkkijonona olion tyyppi
if ( typeid(*p) == typeid(cYmpyra) )...// Verrataan onko olio haluttua luokkaa
Toisaalta joskus haluttaisiin tietää onko olio jonkin toisen olion jälkeläinen. Joissakin oliokielissä tämä voidaan selvittää kutsulla
if ( *p is cSateellinenOlio) ... // EI valitettavasti C++:ssa
Usein kuitenkin pärjätään dynaamisella tyypinmuunnoksella :
caKuvioJollaSade *ps = dynamic_cast<caKuvioJollaSade *>(p);
if ( ps ) ps->muuta_koko(4);
Eli jos p voidaan konvertoida osoittimeksi säteelliseen olioon, niin ps on osoitin samaan paikkaan kuin p, mutta tyypiltään sellainen että esimerkissä ps->muuta_koko on laillinen. Luokassa typeinfo on vielä funktio before:
if ( typeid(cSateellinenOlio).before(typeid(*p)) ...
mutta tämä ei suinkaan palauta tietoa siitä, onko ehto totta perimisjärjestyksen mukaan, ainoastaan tietyn järjestysrelaation mukaan, jonka avulla tyypit voidaan sijoittaa esimerkiksi binääripuuhun.
2.6.11 Miksi polymorfismi ja dynaamiset oliot ovat tärkeitä?
Dynaamisten olioiden tärkeyden huomaa kun rupeaa esimerkiksi tekemään graafista sovellusta, jossa samalla ruudulla on pisteitä, ympyröitä yms. Jollei olisi luokkien välistä polymorfismia käytettävissä, pitäisi pisteet tallettaa piste–taulukkoon, ympyrät ympyrä–taulukkoon jne. Polymorfismin ansiosta voidaan tehdä yksi ainoa taulukko osoittimista graafisiin olioihin. Kun kaikki oliot pitää piirtää uudelleen, riittää tuon ainoan taulukon läpikäyminen:
caKuvio *kuvat[10];
...
kuvat[0] = new cYmpyra(10,10,100);
kuvat[1] = new cPiste(11,11);
kuvat[2] = new cYmpyra(12,12,102);
...
kuvat[3] = NULL;
for (i=0; kuvat[i]; i++) kuvat[i]->sytyta();
...
for (i=0; kuvat[i]; i++) delete kuvat[i];
Tehtävä 1.20 Olio- osoittimet
Lisää edellinen kuvat- taulukko siihen esimerkkiin, johon lisäsit luokan cNelio. Voiko samaan taulukkoon tallettaa neliöitäkin.
2.7 Ystävät
2.7.1 Ystäväfunktiot (friend functions)
Ystäväfunktioita käytetään, kun funktion pitää päästä käsiksi yhden tai useamman luokan alkioihin.
friend.cpp - ystäväfunktiot
/* FRIEND.CPP */
/* Esimerkissä tehdään funktiot, jotka kertovat mahtuuko
ympyrä neliöön ja päinvastoin. Jottei tehtävästä tulisi
liian matemaattista, tutkitaan vain olioden kokoja.
Oikeasti mielekkäämpi tehtävä tutkia leikkaavatko
oliot toisiaan ja tällöin tarvittaisiin tietysti
myös keskipisteitä
*/
#include <stdio.h>
#include <math.h>
class cNelio; // Eteenpäin viittaus jotta void. käyt. ympyrässä
class cYmpyra{
double r; // Säde
public:
cYmpyra(double d=1.0) { r = d; }
friend mahtuuko(const cYmpyra &y, const cNelio &n);
friend mahtuuko(const cNelio &n, const cYmpyra &y);
friend mahtuuko(const cYmpyra &y1, const cYmpyra &y2);
};
class cNelio{
double s; // Sivun pituus
public:
cNelio(double d=1.0) { s = d; }
friend mahtuuko(const cYmpyra &y, const cNelio &n);
friend mahtuuko(const cNelio &n, const cYmpyra &y);
friend mahtuuko(const cNelio &n1, const cNelio &n2);
};
int mahtuuko(const cYmpyra &y, const cNelio &n)
{
return ( 2*y.r < n.s );
}
int mahtuuko(const cNelio &n, const cYmpyra &y)
{
return ( sqrt(2.0)*n.s/2 < y.r );
}
int mahtuuko(const cYmpyra &y1, const cYmpyra &y2)
{
return ( y1.r < y2.r );
}
int mahtuuko(const cNelio &n1,const cNelio &n2)
{
return ( n1.s < n2.s );
}
int main(void)
{
cNelio n1(2.1),n2(2.5); cYmpyra y1(0.9),y2(1.2);
if ( mahtuuko(n1,y1) ) printf("Neliö 1 mahtuu ympyrän 1 sisälle.\n");
if ( mahtuuko(y1,n1) ) printf("Ympyrä 1 mahtuu neliön 1 sisälle.\n");
if ( mahtuuko(y1,y2) ) printf("Ympyrät mahtuvat sisäkkäin.\n");
if ( mahtuuko(n1,n2) ) printf("Neliöt mahtuvat sisäkkäin.\n");
return 0;
}
Edellä olisi tietysti voitu määritellä esimerkiksi neliölle ystäväfunktio, joka on ympyrän jäsenfunktio ja sillä olisi parametrina vain neliö tai ympyrä ja toinen parametri otettaisiin sitten ympyrästä this -parametrilla. Eli funktio voi olla yhden luokan jäsenfunktio ja toisen luokan ystäväfunktio.
Tehtävä 1.21 Jäsenfunktio ja ystävä
Toteuta friend.cpp:stä versio, jossa kaikki mahtuuko- funktiot ovat jäsenfunktioita.
2.7.2 Ystäväluokka
Ilmoittamalla luokka toisen luokan ystäväksi saadaan ehkä edellistä oliomaisempi ratkaisu:
/* FRIEND2.CPP */
/* Esimerkissä tehdään funktiot, jotka kertovat mahtuuko
ympyrä neliöön ja päinvastoin.
Toteutus ystäväluokan avulla.
*/
#include <iostream.h>
#include <math.h>
class cNelio; // Eteenpäin viittaus jotta void. käyt. ympyrässä
class cYmpyra{
friend cNelio;
double r; // Säde
public:
cYmpyra(double d=1.0) { r = d; }
int mahtuuko(const cNelio &n) const;
int mahtuuko(const cYmpyra &y2) const {
return ( r < y2.r );
}
};
class cNelio{
double s; // Sivun pituus
friend cYmpyra;
public:
cNelio(double d=1.0) { s = d; }
int mahtuuko(const cYmpyra &y) const {
return ( sqrt(2.0)*s/2 < y.r );
}
int mahtuuko(const cNelio &n2) {
return ( s < n2.s );
}
};
int cYmpyra::mahtuuko(const cNelio &n) const
{
return ( 2*r < n.s );
}
int main(void)
{
cNelio n1(2.1),n2(2.5); cYmpyra y1(0.9),y2(1.2);
if ( n1.mahtuuko(y1) ) cout << "Neliö 1 mahtuu ympyrän 1 sisälle.\n";
if ( y1.mahtuuko(n1) ) cout << "Ympyrä 1 mahtuu neliön 1 sisälle.\n";
if ( y1.mahtuuko(y2) ) cout << "Ympyrät mahtuvat sisäkkäin.\n";
if ( n1.mahtuuko(n2) ) cout << "Neliöt mahtuvat sisäkkäin.\n";
return 0;
}
Tehtävä 1.22 Inline ei aina onnistu
Miksi edellä ympyra::mahtuuko
ei ole kirjoitettu inline- funktioksi niinkuin vastaava nelio::mahtuuko
.
Tehtävä 1.23 Uuden ystävän lisääminen
Mitä tapahtuu, mikäli FRIEND.CPP:ssä pitäisi lisätä vaikkapa uusi luokka: kolmio?
2.8 Moniperintä (multiple inheritance)
Moniperintä on eräs olio–ohjelmoinnin kiistellyimmistä aiheista. Seuraavassa tulee lyhyt katsaus moniperinnästä mahdollisesti aiheutuviin ongelmiin.
2.8.1 Luokan jäseninä muita luokkia (member objects)
Moniperintä voidaan usein välttää käyttämällä luokan jäseninä muita luokkia (member objects). Esimerkiksi värillinen piste on piste, jolla on väritieto (is-a, has-a). Jos suhde on has-a–tyyppinen, koostamme luokan muista luokista:
multi1.cpp - luokan alkoina muita luokkia
/* MULTI1.CPP - alkioina muita luokkia */
#include <stdio.h>
class caKuvio { ... / kuten aikaisemmin
class cPiste : public caKuvio { ... / kuten aikaisemmin
//----------------------------------------------------------------------------
class cVari {
int r,g,b;
public:
cVari(int ir=0,int ig=0, int ib=0) : r(ir),g(ig), b(ib) {}
virtual void tulosta() const {
printf("%02x%02x%02x",r,g,b);
}
}; // cVari
//----------------------------------------------------------------------------
class cVariPiste : public cPiste {
cVari vari;
public:
cVariPiste(int ix,int iy,const cVari &ivari) : cPiste(ix,iy),vari(ivari) {}
virtual void piirra() const {
vari.tulosta(); printf(" ");
tulosta("Väripiste"); printf("\n");
}
};
int main(void)
{
cVariPiste p(2,4,cVari(255,0,255));
p.sytyta();
return 0;
}
Tehtävä 1.24 Kutsujärjestys
Tutki missä järjestyksessä muodostajia kutsutaan esimerkissä multi1.cpp. (Vihje: lisää luokkien muodostajiin ja hajottajiin tulostuslauseet.)
2.8.2 Peritään suoraan useampia luokkia
Voidaan myös periä useita luokkia samalla:
multi2.cpp - oikea moniperintä
/* MULTI2.CPP - oikea moniperintä */
#include <stdio.h>
class cAsunto {
int huoneita;
public:
cAsunto(int ih=1) : huoneita(ih) {}
void tulosta() const { printf("Huoneita: %d",huoneita); }
};
class cLaiva {
double pituus;
public:
cLaiva(double ipit=10.0) : pituus(ipit) {}
void tulosta() const { printf("Pituus: %3.1lf m",pituus); }
};
class cAsuntolaiva : public cAsunto, public cLaiva {
public:
cAsuntolaiva(int ih=1,double ipit=5.0) : cAsunto(ih) , cLaiva(ipit) {}
void tulosta() const {
cAsunto::tulosta(); printf(" "); cLaiva::tulosta();
}
};
int main(void)
{
cAsuntolaiva koti(2,5.8);
koti.tulosta(); printf("\n");
return 0;
}
Tehtävä 1.25 Kutsujärjestys
Tutki missä järjestyksessä muodostajia ja hajottajia kutsutaan multi2.cpp:ssä.
Perintähierarkia olisi seuraavan näköinen:
Huomautus: Tämä esimerkki on juuri ja juuri ISA-säännön mukainen. Itse asiassa perimällä ISA-säännön mukaan, on moniperintä varsin harvinainen!
Tehtävä 1.26 Asuntolaiva
Lisää asuntolaivaan jokin oma ominaisuus jota ei ole laivalla eikä asunnolla.
2.8.3 Peritty moniperintä (multifaceted)
Jos ympyrä–esimerkissähän oli seuraava perimähierarkia:
Tässä ei suinkaan ole kyse moniperinnästä, vaan itse asiassa kaikkein tavallisimmasta ja toivotuimmasta perinnän muodosta.
2.8.4 Sama perusluokka (base class) = pahin ongelma
Ongelmia tulee lähinnä, mikäli suora moniperintä on sellainen, että molemmilla (tai vähintään kahdella) perittävillä on sama kantaluokka. Haluttaisiin vaikkapa piirtää seuraavanlaisia objekteja:
Esimerkkimme kantaluokilla perimähierarkiasta tulisi seuraava:
Ohjelmana:
multi4.cpp - moniperinnän ongelma
/* MULTI4.CPP - moniperinnän ongelmia */
#include <stdio.h>
//----------------------------------------------------------------------------
class caKuvio {...
class caKuvioJollaSade : public caKuvio {...
class cPiste : public caKuvio {...
class cYmpyra : public caKuvioJollaSade {...
//----------------------------------------------------------------------------
class cNelio : public caKuvioJollaSade {
public:
cNelio(int ix=0, int iy=0, int ir=1) : caKuvioJollaSade(ix,iy,ir) {}
virtual ~cNelio() { sammuta(); }
virtual void piirra() const { tulosta("Neliö"); printf("\n"); }
}; // cNelio
//----------------------------------------------------------------------------
class cYmpyranelio: public cYmpyra, public cNelio {
public:
cYmpyranelio(int ix=0, int iy=0, int ir = 1, int is = 2)
: cYmpyra(ix,iy,ir), cNelio(ix,iy,is) { }
void piirra() const { cYmpyra::piirra(); cNelio::piirra(); }
~cYmpyranelio() { }
}; // cYmpyranelio
//----------------------------------------------------------------------------
int main(void)
{
printf("\n----------------------------------------------------------\n");
cYmpyranelio yn(1,2,3,4);
yn.sytyta(); // Ympyrän vain Neliön sytytä?
return 0;
}
Ongelmana on siis kaksinkertainen esiintymä säteelliselle oliolle. Esimerkiksi mitä tarkoittaa sytyta. Ympyrän vai neliön sytyttämistä. Sama koskee sammuta–metodia. Osittain ongelmaa voitaisiin kiertää lisäämällä ympyräneliölle jäsenfunktio:
void sytyta() { cYmpyra::sytyta(); }
Näillä muutoksilla ohjelma voidaan kääntää ja ajaa, mutta se toimii väärin. Nimittäin neliötä ei sytytetä, vaikka sen piirtämistä kutsutaan! Lisäksi kantaluokassa sytyta ei ollut virtuaalinen, eli tämä luokka ei toimi jos siihen on polymorfinen osoitin! Eli luokkahierarkia olisi pitänyt miettiä paremmin. Mutta miten?
2.8.5 Virtuaalinen perintä
Nyt erityinen ongelma on kaksi erillistä nakyy–attribuuttia. Samoin kaksi erillistä keskipistettä. Jos haluamme, että kummallakin on sama keskipiste ja näkyvyys sekä säde, täytyy cSateellinenOlio periä ympyrään ja neliöön virtuaalisena (inherited base as virtual):
multi5.cpp - virtuaalinen perintä
class cYmpyranelio: public cYmpyra, public cNelio {
public:
cYmpyranelio(int ix=0, int iy=0, int ir = 1, int is = 2)
: caKuvioJollaSade(ix,iy,ir), cYmpyra(ix,iy,ir), cNelio(ix,iy,is) { }
void piirra() const { cYmpyra::piirra(); cNelio::piirra(); }
~cYmpyranelio() { sammuta(); }
};
Nyt ympyräneliölle tulee vain yksi yhteinen keskipiste, säde ja näkyvyys. Ongelmaksi tulee kuitenkin se, että pisteelle kutsutaan nyt oletusrakentajaa caKuvioJollaSade(), koska kutsu tehdään vain kerran. Siis jos ympyräneliölle halutaan alustaa keskipiste, on vielä ympyräneliön rakentajaa ja hävittäjää muokattava:
class cYmpyranelio: public cYmpyra, public cNelio {
public:
cYmpyranelio(int ix=0, int iy=0, int ir = 1, int is = 2)
: caKuvioJollaSade(ix,iy,ir), cYmpyra(ix,iy,ir), cNelio(ix,iy,is) { }
void piirra() const { cYmpyra::piirra(); cNelio::piirra(); }
~cYmpyranelio() { sammuta(); }
};
Ilman virtual-määritystä muodostajien ja hajottajien kutsut olisivat (ku = kuvio, ks = kuvio jolla säde):
muodostajat: ku ks ympyra ku ks nelio ympyranelio
hajottajat: ympyranelio nelio ks ku ympyra ks ku
Virtual-määrityksen kanssa kutsut ovat vastaavasti:
muodostajat: ku ks ympyra nelio ympyranelio
hajottajat: ympyranelio nelio ympyra ks ku
Edellä saatiin virtuaalisella perinnällä toimiva rakenne, mutta entäpä jos halutaisiinkin oma säde sekä neliölle että ympyrälle?
2.8.6 Voidaanko tehdä virtuaalisesti perittyjä apuluokkia
Toisaalta miten virtual päästään lisäämään, jos meille toimitetaan valmis luokkakirjasto, josta virtual sattuisi puutumaan? Sitä ei voi lisätä jälkikäteen tekemällä apuluokkia:
class cvYmpyra : virtual public cYmpyra {...
class cvNelio : virtual public cNelio {...
koska virtuaalinen perintä pitää tehdä heti sitä luokkaa perittäessä, josta halutaan ainutkertainen esiintymä.
2.8.7 Virtuaalinen perintä aikaisemmaksi
Ympyräneliön erikoistapauksessa kaikin puolin toimiva ratkaisu saadaan kun virtuaalinen perintä suoritetaankin jo aikaisemmassa vaiheessa
multi5_v.cpp - virtuaalinen perintä aikaisemmaksi
class caKuvioJollaSade : virtual public caKuvio {...
ja ympyräneliö esitellään seuraavasti:
class cYmpyranelio: public cYmpyra, public cNelio {
public:
cYmpyranelio(int ix=0, int iy=0, int ir = 1, int is = 2)
: caKuvio(ix,iy), cYmpyra(ix,iy,ir), cNelio(ix,iy,is) { }
void piirra() const { cYmpyra::piirra(); cNelio::piirra(); }
~cYmpyranelio() { sammuta(); }
};
Tällä ympyräneliöllä on vain yksi nakyy
ja vain yksi keskipiste, mutta molemmilla kuvioilla on oma säde.
2.8.8 Koostaminen
Moniperinnän kiertämiseksi kokeillaan aluksi versiota:
class cYmpyranelio: public cYmpyra {
cNelio nelio;
public:
cYmpyranelio(int ix=0, int iy=0, int ir = 1, int is = 2)
: cYmpyra(ix,iy,ir), nelio(ix,iy,is) {}
void piirra() const { cYmpyra::piirra(); n.piirra(); }
}; // cYmpyranelio
Tämä toimisi muuten hyvin, mutta taas jää neliö sytyttämättä. Eli joko pitäisi sytyta kirjoittaa uusiksi (mutta ei voida, koska se ei ollut virtuaalinen) tai piirra–metodiin laittaa ihmeellinen ehto, jolla tutkitaan onko piirrettäessä ympyrän ja neliö näkyvyys samanlainen ja korjata tämä jollei ole.
Seuraava yritys voisi olla sellainen, että ympyräneliötä ei peritäkään mistään, vaan se koostetaan sekä ympyrästä että neliöstä. Tässä ratkaisussa olisi se huono puoli, ettei ympyräneliö–osoitin taaskaan voisi olla samassa taulukossa muiden graafisista olioista periytyvien olioiden osoittimien kanssa.
Toimivimmalta ratkaisulta näyttäisi sellainen, jossa suosiolla muutetaan kantaluokkaan sytyta ja sammuta virtuaalisiksi. Lisäksi esimerkiksi caKuvioJollaSade –luokasta peritään vielä yksi abstrakti luokka: caKoostettuKuvio, josta sitten tällaiset ympyräneliöt voidaan periä lisäämälle ympyrä ja neliö attribuuteiksi.
Tehtävä 1.27 Kokonaan koostettu ympyräneliö
Kokeile suunnitella ja toteuttaa luokkahierarkia, jossa on em. caKoostettuOlio
.
2.9 Operaattorit ja referenssit
2.9.1 Operaattori–funktiot
C++ –kielessä voidaan myös operaattoreita lisämääritellä (kuormittaa, operator overloading). Huomattakoon, että vaikka operaattoreille annetaankin "uusia merkityksiä", säilyy niiden precedenssi ennallaan. Lisäksi operaattoreiden määrittelyssä tulee olla tarkkana, jotta säilyttää niiden "alkuperäisen" tarkoituksen. Eli ei ole suotavaa määritellä esimerkiksi operaattoria (miinus) - joka tekee jotakin yhdistämiseen liittyvää.
Esimerkiksi kahden pisteen yhteenlaskussa olisi mielekästä laskea yhteen pisteen x-koodinaatit ja pisteen y-koordinaatit. Samoin tietenkin pisteiden vähennyslaskussa (vrt. vektorit).
operato1.cpp - operaattoreiden lisämääritteleminen
Aina ei ole ilmeistä kannattaako operaattori esitellä luokan jäsenfunktioksi (metodiksi) vaiko ystäväfunktioksi. Esimerkiksi yhteenlasku olisi voitu tehdä myös:
class cPaikka...
friend cPaikka operator+(cPaikka &p1,cPaikka &p2); // p3 = p1 + p2
}; // cPaikka
cPaikka operator+(cPaikka &p1,cPaikka &p2) // Tätä kutsutaan edelleen p3=p1+p2
{
cPaikka t;
t.x = p1.x + p2.x;
t.y = p1.y + p2.y;
return t;
}
Tehtävä 1.28 Miksi sijoituksen määrittely
Miksi operato1.cpp:ssä on täytynyt lisämääritellä sijoitus piste=piste samalla kun on lisätty muunnos (int)p tai (double)p? (Vihje: Kokeile poistaa sijoitus ja päättele mikä menee väärin ja miksi; vaikkapa ajamalla askel kerrallaan debuggerilla).
Tehtävä 1.29 Ylimääräiset muuttujat operaattoreissa
Tee ohjelman operato2.cpp tehtävät.
Tehtävä 1.30 Lisää operaatiota
Lisää ohjelmaan operato1.cpp seuraavat:
- Kirjoita samaan tiedostoon pisteelle itseisarvofunktio, jota voidaan kutsua fabs(p);
- Tee vertailuoperaattori ==, joka vertaa kahta pistettä.
- Määrittele -= –operaattori vähentämään pistettä pisteellä.
- Määrittele pisteelle etumerkinvaihto operaattori -. (mitä ko. oper. palauttaa?)
- Kirjoita vielä + operaattori, jolloin +p muuttaa p:n kaikki koordinaatit positiivisiksi (onko ko. operaattori oikeasti järkevä ja miksi on tai ei?).
Tehtävä 1.31 Luokka 2x2 matriiseille
Kirjoita luokka 2x2 matriiseille. Laita luokkaan "riittävä" määrä operaattoreita. Kuinka helppoa luokka olisi modifioida yleiseksi nxn matriiseja käsitteleväksi luokaksi? Entäpä nxm?
2.9.2 Referenssin palauttaminen
Seuraava esimerkki näyttää miten referenssin palauttamista voitaisiin käyttää "turvallisen" taulukon tekemiseen.
retref.cpp - refenssin palauttaminen
Vielä enemmän taulukon näköinen "olio" saadaan määrittelemällä luokalle []-operaattori uudelleen:
retref2.cpp - referenssin palauttaminen []-operaattorilla
/* RETREF2.CPP - referenssin palauttaminen, "turva"taulukko */
...
class cTaulukko {
...
public:
...
int &operator[] (int i) {
if ( (i<0) || (i>=koko) ) {
printf("Laiton taulukon indeksi %d!\n",i);
return laiton;
}
return t[i]; // Palautetaan viittaus t[i]:hin!
}
...
};
int main(void)
{
cTaulukko a(5);
a.tulosta();
a[3] = 3;
a.tulosta();
printf("a[1] = %d\n",a[1]);
a[8] = 175;
a.tulosta();
printf("a[8] = %d\n",a[8]);
return 0;
}
Tehtävä 1.32 Referenssin palauttaminen
Miksi RETREF2.CPP toimii?
2.9.3 Omien luokkien käsittely tietovirrassa
Kuten aikaisemmin mainittiin, on C++:n tietovirtojen eräs hyvä puoli siinä, että omat luokat voidaan käsitellä niillä helpommin kuin stdio:n funktiolla.
mystream.cpp - malli tietovirtojen käytöstä omille luokille
Ohjelma tulostaa pisteen ja kysyy uuden, kunnes tulee syöttövirhe:
Piste on: (3,7)
Anna uusi piste muodossa (x,y) tai x,y tai x y >1 2
Piste on: (1,2)
Anna uusi piste muodossa (x,y) tai x,y tai x y >7,9
Piste on: (7,9)
Anna uusi piste muodossa (x,y) tai x,y tai x y >(-2,3)
Piste on: (-2,3)
Anna uusi piste muodossa (x,y) tai x,y tai x y >2,3)
Piste on: (2,3)
Anna uusi piste muodossa (x,y) tai x,y tai x y > ... ohjelma loppui!
Tehtävä 1.33 Extractor
Muuta mystream.cpp sellaiseksi että pisteen syöttöön kelpaa suluiksi myös [] ja {}. Sekasulkuja ei kuitenkaan sallita, eli esim. (1,2) ja [1,2] ovat oikeita syöttöjä, mutta (1,2] on väärä.
2.10 Template–luokan esittelyssä (class template)
2.10.1 Miksi luokkamallia tarvitaan?
Tarkastellaan aluksi seuraavaa ongelmaa: halutaan tehdä luokka list, johon voidaan tallettaa yksittäisiä kirjaimia. Yksinkertaisuuden vuoksi luokka toteutetaan seuraavassa taulukon avulla, mutta luokan ulkoinen käyttö ei muutu yhtään luokan sisäisen toteutuksen muuttuessa. Jotta cList-luokan alkioiden tyyppi olisi helppo muuttaa, on tyyppi esitelty #define:n avulla
listtarr.cpp - lista taulukon avulla
/* LISTARR.CPP
Esimerkki (yksisuuntaisen) listan toteuttamisesta taulukon avulla.
Kaksi esimerkkiä mahdollisuuksista käydä lista kokonaan läpi.
joko: for (c=l.first(); !l.out(); c=l.next() ) -> käytä c ??
tai: for (l.tobegin(); !l.out(); l.forward() ) -> käytä l.current()
Ei tapahdu "vahinkoa", vaikka listaa yritetään käyttää liian pitkälle.
vl 6.11.1993
Tehtäviä 1) Peri luokasta luokka cList2, joka on kaksisuuntainen ja
jossa voidaan siis liikkua kumpaankin suuntaan.
2) Toteuta list "oikeana" dynaamisena listana.
3) Peri tehtävän 2 listasta 2-suuntainen lista.
Onnistuuko kunnolla?
4) Mitä tässä pitää muuttaa, mikäli halutaan käyttää
muun tyyppisiä listan alkioita?
Kokeile reaalilukuja ja piste-luokan alkioita!
(Piste-luokka ks. MYSTREAM.CPP)
5) Entä jos halutaan käyttää samassa ohjelmassa kahta
tai useampaa eri listaan menevää tyyppiä?
*/
#include <iostream.h>
inline int inside(int a,int x,int b) // x välille [a,b], jos b<a niin a
{ return b < a ? a : x < a ? a : x < b ? x : b; }
#define MAX_ELEM 100
#define TYPE char
class cList {
protected:
int nelem; // Alkioiden lukumäärä
TYPE data[MAX_ELEM]; // Alkioden taulukko
int cursor; // Sijainti listassa
public:
cList() { nelem = 0; tobegin(); }
~cList() { nelem = 0; }
int out() const { return ( cursor < 0 || nelem <= cursor ); }
int tobegin() { cursor = 0; return out(); }
int forward() { ++cursor; return out(); }
int toend() { cursor = nelem-1; return out(); }
TYPE current() const { return data[inside(0,cursor,nelem-1)]; }
TYPE first() { tobegin(); return current(); }
TYPE next() { forward(); return current(); }
TYPE last() { toend(); return current(); }
int empty() const { return ( nelem == 0 ); }
int add(TYPE p) { if ( nelem >= MAX_ELEM ) return 1;
data[nelem++] = p; return 0;
}
}; /* cList */
#define VIIVA "------------------------------------------------------------\n"
/****************************************************************************/
int main(void)
{
cout << VIIVA;
cList lc; lc.add('a'); lc.add('b'); lc.add('c'); lc.add('d');
for (char c=lc.first(); !lc.out(); c=lc.next() )
cout << c << " ";
cout << c << "\n" << VIIVA;
for (lc.tobegin(); !lc.out(); lc.forward() )
cout << lc.current() << " ";
cout << lc.current() << "\n" << VIIVA;
return 0;
}
Luokka toimii varsin hyvin, kunnes tulee tarve käyttää yhtäaikaa listaa esimerkiksi sekä kirjaimille, että reaaliluvuille. Huomattakoon, että luokassa on tarjottu valmiit funktiot listan kaikkien alkioiden läpikäymiseksi.
Tehtävä 1.34 Lineaarinen lista
Tee ohjelman LISTARR.CPP tehtävät.
2.10.2 Luokkamalli (class template)
Yleiskäyttöisyysongelma ratkaistaan esittelemällä cList-luokka luokkamallin avulla:
listtemp.cpp - lista luokkamallin avulla
listtemp.cpp - lista luokkamallin avulla
/* LISTTEMP.CPP
...
Tehtäviä 1) Peri luokasta luokka cList2, joka on kaksisuuntainen ja
jossa voidaan siis liikkua kumpaankin suuntaan.
2) Toteuta cList "oikeana" dynaamisena listana.
3) Peri tehtävän 2 listasta 2-suuntainen lista.
Onnistuuko kunnolla?
4) Jäsenfunktiot first,last ja next ovat oikeastaan
tarpeettomia, koska sama ominaisuus saadaan aikaan
tobegin, toend, forward ja current -funktioiden avulla.
Kuitenkin joskus tarvitaan ainakin kahta toisistaan
riippumattomasti listaa käsittelevää "iteraattoria".
Esitä ratkaisuehdotus, jolla lista voidaan samanaikaisesti
tulostaa sekä alusta että lopusta päin, eli esim lista 1 2 3
tulostuisi: 1 3 2 2 3 1
*/
...
template <class TYPE>
class cList {
protected:
int nelem; // Alkioiden lukumäärä
TYPE data[MAX_ELEM]; // Alkioden taulukko.
int cursor; // Sijainti listassa
public:
cList() { nelem = 0; tobegin(); }
~cList() { nelem = 0; }
int out() const { return ( cursor < 0 || nelem <= cursor ); }
int tobegin() { cursor = 0; return out(); }
int forward() { ++cursor; return out(); }
int toend() { cursor = nelem-1; return out(); }
TYPE current() const { return data[inside(0,cursor,nelem-1)]; }
...
}; /* cList */
...
int main(void)
{
...
cList<char> lc; lc.add('a'); lc.add('b'); lc.add('c'); lc.add('d');
for (char c=lc.first(); !lc.out(); c=lc.next() ) cout << c << " ";
...
cList<double> ld; ld.add(1.2); ld.add(20.3); ld.add(300.4); ld.add(400.5);
for (double d=ld.first(); !ld.out(); d=ld.next() ) cout << d << " ";
...
cList<cPiste> lp; lp.add(cPiste(1,1)); lp.add(cPiste(2,4));
for (cPiste p=lp.first(); !lp.out(); p=lp.next() ) cout << p << " ";
...
cList<int> li; li.add(1); li.add(2); li.add(3);
for (li.tobegin(); !li.out(); li.forward() ) cout << li.current() << " ";
...
}
Template toimii siis lähes kuten #define. Luokasta monistuu eri versio kutakin tyyppiä kohti (generoitu luokka = template class). Jos monistuminen haluttaisiin välttää, pitäisi luokasta tehdä void -osoittimia käsittelevä versio. Tällöinkin kannattaa template:lla tehdä apuluokat tyypinmuunnoksia varten.
Template on voimassa vain luokan määrittelyn lopettavaan puolipisteeseen. Mikäli esimerkiksi funktio add haluttaisiin tehdä ei-inline-funktiona, pitäisi template aloittaa uudelleen jokaista funktiota kohden:
listtem2.cpp - lista luokkamallin avulla, ei-inline funktio
...
template <class TYPE>
int cList<TYPE>::empty() const {
return ( nelem == 0 );
}
template <class TYPE>
int cList<TYPE>::add(TYPE p)
{
if ( nelem >= MAX_ELEM ) return 1;
data[nelem++] = p;
return 0;
}
Tehtävä 1.35 Template ja luokat
Tee ohjelman LISTTEMP.CPP tehtävät.
2.11 C ja C++ –kielien yhteiskäyttö
2.11.1 C–funktioiden käyttö
C++ nimeää funktiot eri tavalla kuin C–kieli (vrt. overloading). Esimerkiksi jos C–kielellä on kirjoitettu funktio tulosta, on sen nimi linkitysvaiheessa _tulosta (alleviiva nimen edessä).
Borland C++ 3.1 nimeäisi funktioita seuraavasti (Microsoft hieman erilailla):
Funktion protoyyppi nimi linkitysvaiheessa
----------------------------------------------------------------------
void tulosta(int i,int j) tulosta$qii
int tulosta(double d,int tila=5,int desi=2) tulosta$qdii
void tulosta(char near *s,int tila=1) tulosta$qpzci
void tulosta(char far *s,int tila=1) tulosta$qnzci
Kuva 2.7 Funktioiden nimeäminen
Jos C++ ohjelmaan halutaan linkittää C–kielellä kirjoitettuja ja käännettyjä funktioita, pitää niiden prototyypit esitellä:
extern "C" void tulosta(int i);
Jos C–kirjastolle on valmis include-tiedosto tulosteet.h, missä on kaikkien kirjaston funktioiden prototyypit, on tietysti turha lähteä kirjoittamaan prototyyppejä uudelleen, koska voidaan käyttää C++:ssa muotoa:
extern "C" {
#include "tulosteet.h"
}
Mikäli aliohjelmien lähdekoodi (source) on käytettävissä, kannattaa ne tietenkin kääntää itse C++:lla, jolloin nimeämisvaikeuksista ei tarvitse välittää.
2.11.2 C++ funktioiden kutsuminen C–kielestä
Jos on valmiina C++ funktioita, joiden kutsut on periaatteessa mahdollista muuttaa C–funktioiksi, pitää rakentaa väliin C++:lla funktio joka on tyyppiä "C". Muuttaminen on mahdollista, mikäli funktion parametrilistassa olevat tyypit ovat myös C–tyyppejä tai sellaisiksi muunnettavissa. Usein myös C++ luokat voidaan muuttaa C–tietueiksi, koska itse luokan alkiot ovat samanlaisia kuin C–tietueen alkiot.
piste.c - C++ funktiota kutsuva C-ohjelma
/* PISTE.C */
#include "piste2ali.h"
int main(void)
{
tPiste p={4,5};
tulosta(&p);
return 0;
}
piste2ali.h - C:stä kutsuttava C++ prototyyppi
/* PISTE2ALI.H */
typedef struct {
int x,y;
} tPiste;
#ifdef __cplusplus
extern "C"
#endif
void tulosta(const tPiste *p);
piste2ali.cpp - C:stä kutsuttava C++ funktio
/* PISTE2ALI.CPP */
#include <stdio.h>
#include "piste2ali.h"
class cPiste {
int x,y;
public:
void tulosta() const;
};
void cPiste::tulosta() const
{
printf("(%d,%d)\n",x,y);
}
extern "C" void tulosta(const tPiste *p)
{
((cPiste *)p)->tulosta();
}
Huomautus! Esimerkki toimii vain jos C++ tietueessa (luokassa) ei ole yhtään virtuaalista funktiota! Jos luokassa on virtuaalisia funktioita, pitää tehdä osoittimen tyypinmuunnoksen (cPiste *) sijasta rehellinen sijoitus C++ tyyppiseen apumuuttujaan (ks. Olio–osoittimet):
piste3ali.cpp - C:stä kutsuttava C++ funktio
extern "C" void tulosta(const tPiste *p)
{
cPiste ap(p); // Luokkassa cPiste esitelty konstruktori cPiste(tPiste *)
ap.tulosta();
}
2.12 Poikkeusten käsittely (exeption handling)
Jokainen ohjelmoija joutuu aina painimaan ajonaikana havaittavien virheiden (muistin loppu, nollalla jako, tiedoston puuttuminen) kanssa.
2.12.1 Virheiden käsittely C–kielen ehdoilla
Usein ongelmaa voidaan kiertää siten, että kukin aliohjelma tehdään funktioksi, joka palauttaa 0 jos aliohjelma pystyttiin toteuttamaan virheettömästi ja nollasta eroavan virhekoodin jos jokin virhe havaittiin.
int laske(...)
{
if ( homma1_pielessa ) return 1;
... onnistuneita hommia ...
if ( homma2_pielessa ) return 2;
... onnistuneita hommia ...
return 0;
}
Tällöin virheen käsittelyvastuu jää kutsuvalle ohjelmalle:
joko siirretään vastuu edelleen kutsujalle:
joko siirretään vastuu edelleen kutsujalle:
if ( ( ret = laske(...) ) ) return ret;
...
tai yritetään toipua virheestä tai kaadetaan koko ohjelma:
assert(laske(...));
Ohjelman kaatamista ei voida pitää hyvänä vaihtoehtona muulloin kuin pienissä testiohjelmissa.
Lisäongelma tulee usein vielä purkua vaativista rakenteista (varattu muisti, avonaiset tiedostot). Usein goto-lauseen käyttö antaa ainoan kohtuullisen siistin ratkaisun:
int laske(...)
{
int ret = 1;
...varaa_1...
if ( homma1_pielessa ) goto pura_1;
ret = 2;
... onnistuneita hommia ...
...varaa_2...
if ( homma2_pielessa ) goto pura_2;
... onnistuneita hommia ...
ret = 0;
pura_2:
... pura_2 ...
pura_1:
... pura_1 ...
return ret;
}
Joskus voidaan jopa joutua turvautumaan longjumpiin
!
2.12.2 Purkaminen hävittäjän avulla
C++:ssa purkamisongelmaa voidaan kiertää tekemällä kustakin tiedostosta ja varattavasta muistityypistä oma luokkansa, jonka hävittäjää sitten kutsutaan automaattisesti funktiosta poistuttaessa. Kukin muuttuja (esim. tiedostoluokan ilmentymä) esitellään vasta kun sitä tarvitaan. Näin jos funktiosta poistutaan return-lauseella kesken kaiken, ei pureta muita kuin ne rakenteet, jotka on jo ehditty alustaa.
Tehtävä 1.36 Rakenteiden purkaminen hajottajan avulla
Kirjoita mallifunktio, joka aukaisee 3 tiedostoa. Funktio lukee kustakin tiedostosta 1. kirjaimen ja palauttaa niiden ASCII- koodien summan. Jos jokin tiedostoista ei aukea, palautetaan sen tiedoston numero, joka ei aukea (esim. jos 1. ei aukea, palautetaan 1). Kun funktiosta lähdetään pois, pitää kaikkien tiedostojen jäädä suljetuksi.
2.12.3 throw, try ja catch
C++:ssa (ANSI ehdotus 2.0?) on poikkeusten käsittelyä varten komento, jolla "heitetään virhe": throw. Kun jokin funktio päätyy tilanteeseen, josta se ei voi enää jatkaa, siivoaa funktio ensin jälkensä ja sitten poistuu funktiosta "väkivaltaisesti" throw -komennolla. throw- komennossa voi olla parametrina jokin luokka, jolloin suoritus siirtyy kutsuhierarkiassa sille tasolle, jossa on ilmoitettu käsittelijä kyseiselle virheelle. Virheen käsittelyyn ilmoittaudutaan halukkaaksi try - catch parilla. catchin tulee olla heti try-lohkon jälkeen:
cpp\ansicpp\throw0.cpp - yksinkertainen try-catch -esimerkki
// throw0.cpp
#include <iostream.h>
int main(void)
{
int i; double x;
cout << "Anna kokonaisluku >"; cin >> i;
try {
if ( i == 0 ) throw int();
x = 1.0/i; cout << x << endl;
}
catch ( ... ) {
cout << "Nollalla jako?" << endl;
return 0;
}
cout << "Täällä ollaan onnellisesti!" << endl;
return 0;
}
Erityisen käyttökelpoinen try-lohko on matemaattisten funktioiden tapauksessa, koska jos funktioista tehtäisiin jonkin virhekoodin palauttavia, tulisi itse lausekkeesta varsin sotkuinen, eli käytännössä operaattoreiden lisämäärittelemisestä (operator overloading) ei tulisi mitään. Seuraavana alkeellinen sovellus edelliselle esimerkille (... tarkoittaa että otetaan kaikki virheet vastaan ja on tällä kertaa kielen rakennetta):
cpp\ansicpp\throw1.cpp - kutsun puolella ei enää tarvita if-lauseita
// throw1.cpp
#include <iostream.h>
//--- Funktio, joka aiheuttaa virheen: ----------------------------------
double inv(int i)
{
if ( i == 0 ) throw int();
return 1.0/i;
}
int main(void)
{
double x;
try {
x = inv(-1); cout << x << endl;
x = inv(0); cout << x << endl;
x = inv(1); cout << x << endl;
}
catch ( ... ) {
cout << "Nollalla jako?" << endl;
return 0;
}
cout << "Täällä ollaan onnellisesti!" << endl;
return 0;
}
Itse asiassa throw "heittää" olion ja tämä olio voidaan alustaa "heittämisen yhteydessä", jolloin oliolla voidaan tuoda tietoa siitä, mistä virheestä oli kyse:
cpp\ansicpp\throw.cpp - virhepaluu useamman tason yli
// throw.cpp
#include <iostream.h>
#include <cstring.h>
//--- Virheluokat: ------------------------------------------------------
class cVirhe{
int err_n;
string err_msg;
public:
cVirhe(int i=0,char *s=""):err_n(i),err_msg(s){};
int n() { return err_n; }
const string &err() const { return err_msg; }
};
class cNollallaJako: public cVirhe {};
class cNegatiivinen: public cVirhe {
public: cNegatiivinen(int i=-1,char *s=""):cVirhe(i,s) {};
};
class cLiianIso: public cVirhe {
public: cLiianIso(int i=10,char *s=""):cVirhe(i,s){}
};
//--- Funktio, joka aiheuttaa virheen: ----------------------------------
double inv(int i)
{
if ( i < -1 ) throw cNegatiivinen(i,"ja pahasti");
if ( i < 0 ) throw cNegatiivinen();
if ( i == 0 ) throw cNollallaJako();
if ( i > 2 ) throw cLiianIso(i,"ja iso");
double x = 1.0/i;
cout << "i = " << i << " x = " << x << endl;
return x;
}
int laske(void)
{
for (int i=-2; i<4; i++) {
try {
double x;
x = inv(i); (void)x;
}
catch ( cNollallaJako ) {
cout << "Tulipa jaettua nollalla!" << endl;
}
catch ( cNegatiivinen Neg) {
cout << "Tulipa negatiivinen luku: "
<< Neg.n() << " " << Neg.err() << endl;
}
}
cout << "Täällä ollaan onnellisesti!" << endl;
return 0;
}
int main(void)
{
try {
laske();
}
catch ( cVirhe Err ) {
cout << "Tänne loput viat (" << Err.n() << "," << Err.err() << ")!" << endl;
}
catch ( ... ) {
cout << "Ja tänne sitten vielä jämät!" << endl;
}
return 0;
}
Seuraavana ohjelman tulostus:
Tulipa negatiivinen luku: -2 ja pahasti
Tulipa negatiivinen luku: -1
Tulipa jaettua nollalla!
i = 1 x = 1
i = 2 x = 0.5
Tänne loput viat (3, ja iso)!
Tehtävä 1.37 Virhekäsittely throw:n avulla
Kirjoita "Rakenteiden purkaminen hajottajan avulla" - tehtävä uudelleen käyttäen throw poistumista.
2.13 Nimiavaruus (namespace)
Vaikka kaikenlaisilla etuliitteillä (mycPiste) ja muilla nimeämistempuilla saadaan periaatteessa rajaton määrä erilaisia symboleja, niin kuitenkin käytännön ohjelmissa käyttökelpoiset nimet rupeavat käymään vähiin. Normaalisti tietyssä modulissa esitellyt nimet jäävät lokaalisti tähän moduuliin, mutta kaikki globaalit nimet jakavat samaa nimiavaruutta. Tällaisiahan on esimerkiksi otsikkotiedostoissa esitellyt funktiot ja tyypit (luokat).
C++:n nimiavaruudet (namespaces) tuovat helpotusta tähän ongelmaan. Itse asiassa luokathan muodostavat tietyllä tavalla jo oman nimiavaruutensa. Seuraavassa esimerkissä kannattaa ehkä eniten kiinnittää huomiota tyypin tTyyppi käyttöön :
/* namespac.cpp */
// Esimerkki nimiavaruuksien (namespace) käytöstä /vl-96
// Vaikka esimerkissä on paljon muuttujia, niin yleensä suurin hyöty
// saadaan nimiavaruukisen käytöstä tyyppien kanssa.
#include <iostream.h>
namespace nsOmat { // Nimiavaruuden esittely
typedef char tTyyppi; // Yleensä nimenomaan tyypit aiheuttaa päällekkäis.
int a = 5;
int b = 6;
int c = 7;
namespace nsErikoiset { // Sisäkkäisetkin nimiarvaruudet on sallittu
double d = 8.5;
}
}
namespace nsVieraat {
typedef double tTyyppi;
char a = 'a';
char b = 'b';
char c = 'c';
}
namespace nsKamalanPitkaNimiAvaruudenNimi {
double a = 9.2;
namespace nsJaViela {
double f = 10.3;
}
}
namespace nsOmat { // Nimiavaruuteen voi lisätä määrityksiä
double e = 9.6;
}
namespace nsKPNAN = nsKamalanPitkaNimiAvaruudenNimi; // Namespace alias
namespace nsJV = nsKPNAN::nsJaViela; // Namespace alias
int main(void)
{
double c = 70; // Lokaali nimi voittaa aina
using namespace nsOmat;
using namespace nsErikoiset;
cout << "d = " << d << endl; // 8.5 nsOmat::nsErikoiset::d
tTyyppi d = 'x'; // nsOmat::tTyyppi
cout << "a = " << a << endl; // 5 nsOmat::a
cout << "b = " << b << endl; // 6 nsOmat::b
cout << "c = " << c << endl; // 70 lokaali c
cout << "d = " << d << endl; // x lokaali d
cout << "e = " << e << endl; // 9.6 nsOmat::e
cout << "b = " << nsVieraat::b << endl; // b nsVieraat::b
{ // Uusi vaikutusalue
using nsVieraat::a; // nyt a on aina nsVieraat::a
cout << "a = " << a << endl; // a nsVieraat::a
} // em. vaikutusalue loppu
cout << "a = " << a << endl; // 5 nsOmat::a
using nsKPNAN::a;
cout << "a = " << a << endl; // 9.2 nsKamalan..Nimi::a
using namespace nsJV;
cout << "f = " << f << endl; // 10.3 nsKam..Nimi::nsJaViela::f
using nsVieraat::tTyyppi;
tTyyppi f = 1.75; // nsVieraat::tTyyppi
cout << "f = " << f << endl; // 1.75 lokaali f
return 0;
}
Joskus nimiavaruuksia voidaan hyödyntää myös "siirtämällä" valmiita kirjastoja omaan nimiavaruuteensa ja näin voidaan kiertää ongelmia, jos kahdessa eri kirjastossa on määritelty samoja funktioita:
/* namespa2.cpp */
// Esimerkki nimiavaruuksien (namespace) käytöstä /vl-96
#include <iostream.h>
namespace nsCIO {
#include <stdio.h>
}
int printf(const char *s)
{
cout << s << flush;
return 1;
}
int main(void)
{
nsCIO::printf("Kissa\n"); // Alkuperäinen printf-funktio
printf("Kissa\n"); // Oma printf-funktio
return 0;
}
3. C++ vaarat
3.1 Kirjoitusvirheitä
3.1.1 Puolipiste puuttuu
Jos luokan määrittelyn lopettavasta sulusta } puuttuu puolipiste, tulee tukku vallan ihmeellisiä virheitä mm.
Too many types in declaration
3.1.2 Luokan nimi jää pois kutsusta
Jos perinnän esimerkissä (CIRCLE.CPP) olisi jäänyt kutsusta caKuvio::tulosta() luokan nimi caKuvio pois, niin tuloksena olisi ollut rekursiivinen kutsu, josta kääntäjä ei edes varoita:
class caKuvioJollaSade : public caKuvio {
...
virtual void tulosta(const char *s="") const {
caKuvio::tulosta(s);
printf( " r=%d",r);
}
}; // caKuvioJollaSade
3.2 Perintä (inheritance)
3.2.1 Private attribuutit eivät näy
Luokkamäärityksen oletuksena on suojaus private. Siis oletuksen mukaisesti kirjoitetut attribuutit ja metodit eivät ole aliluokan (child) käytettävissä.
class cKuvio {
int nakyy;
int x,y;
public:
void sammuta();
void sytyta();
...
3.2.2 Hyvien luokkien keksiminen vaikeata
On varsin vaikeata keksiä hyviä yleiskäyttöisiä luokkia, joita muut sitten voivat periä itselleen. Aluksi kaikki näyttää menevän hyvin, mutta jonkin tietyn ominaisuuden saamiseksi koko luokkahierarkia saatetaan joutua kirjoittamaan uusiksi.
3.2.3 Mitä tapahtuu kulissien takana
Kun ominaisuuksia peritään, pitäisi kaiken tietysti sujua itsekseen. Usein kuitenkin tulee ongelmia ja tällöin voi joutua ottamaan selvää siitä, mitä missäkin luokassa kulissien takana tapahtuu.
3.2.4 Perittävän muodostajan kutsuminen
Kun luokka peritään, kutsutaan aina jotakin perusluokan (base class, ancestor) muodostajaa (constructor).
Esimerkiksi suunnitellaan graafisia objekteja ja niille (huonoa) hierarkiaa:
cPiste <- cYmpyra <- cNelio jne.
class cPiste {
int nakyy;
protected:
int x,y;
public:
void sammuta();
void sytyta();
cPiste(int ix=0,int iy=0) {
nakyy = 0;
x=ix; y=iy; sytyta();
}
~cPiste() { sammuta(); }
void siirra(int nx=0,int ny=0);
void piirra() const;
int nakyvissa() const { return nakyy; }
};
Pisteluokan muodostajan oletetaan samalla piirtävän pisteen näytölle. Tällöin myös ympyrän muodostaja tulee piirtämään keskipisteen näytölle!
Muodostajaan on ei siis saa laittaa ominaisuuksia, joita ei haluta perittäväksi. Edellä siis sytyta pois muodostajasta, jos perillisten ei haluta sytyttävän myös keskipistettä! Tai sitten muodostajaan on lisättävä yksi parametri, jolla estetään piirtäminen perinnän tapauksessa.
cPiste(char *s,int x,int y,int piirto) ...
3.2.5 Virtuaaliset metodit
Edellä todennäköisesti pisteen metodit sytyta ja sammuta kutsuvat metodia piirra. Kun cPiste–luokka peritään, olisi kussakin uudessa luokassa toivottavaa, että niille kelpaisi samat sytyta ja sammuta -metodit kuin pisteellekin. Kuitenkin C++ -kielen sidonnan (binding) takia nämä kutsuvat AINA pisteen metodeja.
Muuttamalla piirra virtuaaliseksi
virtual void piirra();
saadaan sidontajärjestys muuttumaan (late binding) siten, että jos aliluokasta löytyy piirra -metodi, käytetään aina sitä (alinta mahdollista). Myös hajottaja olisi muistettava ilmoittaa virtuaaliseksi!
Mahdollisen virtual määrityksen poisjääminen ei anna käännösvirhettä. Ohjelma ei vaan toimi odotetusti.
3.3 Olio parametrina ja olion sijoittaminen
Aliohjelmiin kannattaa viedä olioiden osoittimia tai viittauksia, ei itse olioita. Seuraavassa selvitetään miksi tämä on suositeltavaa sen lisäksi, että se on nopeampaa.
3.3.1 Olio parametrina
Koska arvoparametrin välityksessä täytyy välitettävästä parametrista tehdä tilapäinen kopio, kutsutaan olioparametrin kohdalla silloin tietysti aliohjelman loppuessa tilapäisen olion hävittäjää.
Jos aliohjelman aikana on oliolle tehty jotakin, jota ei saisi hävittää, menevät nämä sen sileän tien aliohjelman loppuessa. Erityisesti dynaamista muistia käsittelevien olioiden kanssa saa olla tarkkana. Siispä useimmiten onkin parempi välittää olion osoite tai referenssi kuin olio itse.
Seuraavassa malli ongelmasta:
objectar.cpp - olion välittäminen arvoparametrina ja sen ongelmat
Ohjelman tulostus olisi seuraava:
---------------------------------------------------
Rakentaja: Osoitteet: olio FFF2, jono 06B2 Jono j1
Rakentaja: Osoitteet: olio FFEE, jono 06BA Jono j2
Menossa aliohjelmaan ftulosta:
Aliohjelmassa ftulosta!
Osoitteet: olio FFE8, jono 06B2 Jono j1
Pois aliohjelmassa ftulosta!
Hävittäjä: Osoitteet: olio FFE8, jono 06B2 Jono j1
Menossa aliohjelmaan tulosta:
Osoitteet: olio FFF2, jono 06B2 Jono <<<<
Vertailu:
Aliohjelmassa vertaa!
Osoitteet: olio FFE4, jono 06B2 Jono <<<<
Osoitteet: olio FFE8, jono 06BA Jono j2
Hävittäjä: Osoitteet: olio FFE8, jono 06BA Jono j2
Hävittäjä: Osoitteet: olio FFE4, jono 06B2 Jono <<<<
Vertailun tulos 68
Hävittäjä: Osoitteet: olio FFEE, jono 06BA Jono j2
Hävittäjä: Osoitteet: olio FFF2, jono 06B2 Jono <<<<(c)<<2
Null pojÂr assignment
Ongelma poistuu kun ystäväfunktioiden parametrit esitellään referensseiksi (const &j1...):
---------------------------------------------------
Rakentaja: Osoitteet: olio FFF2, jono 06B2 Jono j1
Rakentaja: Osoitteet: olio FFEE, jono 06BA Jono j2
Menossa aliohjelmaan ftulosta:
Aliohjelmassa ftulosta!
Osoitteet: olio FFF2, jono 06B2 Jono j1
Pois aliohjelmassa ftulosta!
Menossa aliohjelmaan tulosta:
Osoitteet: olio FFF2, jono 06B2 Jono j1
Vertailu:
Aliohjelmassa vertaa!
Osoitteet: olio FFF2, jono 06B2 Jono j1
Osoitteet: olio FFEE, jono 06BA Jono j2
Vertailun tulos -1
Hävittäjä: Osoitteet: olio FFEE, jono 06BA Jono j2
Hävittäjä: Osoitteet: olio FFF2, jono 06B2 Jono j1
Mikä oli ongelmana? Se selviää seuraavassa kappaleessa.
3.3.2 Olioiden sijoittaminen
Olioiden sijoittaminen toisiinsa tapahtuu oletuksena muistikopiona. Esimerkiksi edellisessä esimerkissä ftulosta aliohjelman kutsussa on luotu tilapäinen cJono–tyyppinen olio (olkoon vaikkapa nimellä temp), jolle on sijoitettu tulostettava olio j1:
Siis aliohjelman aikana kumpikin olio (j1 ja temp) osoittivat samaan merkkijonoon. Kun aliohjelmasta poistuttiin ja kutsuttiin jonon hävittäjää, vapautettiin tietenkin temp -olion osoittama jono, joka onnettomuudeksi olikin sama kuin j1 -olion osoittama jono! Tämä on aliasing-ongelma
Vielä huonommin kävisi, jos edellisen mallin pääohjelmassa olisi koodi:
{
cJono j1("j1");
{
cJono j2("Kissa");
j2 = j1; // j2:en Kissa häviäisi kuin p...
j2.tulosta();
}
j1.tulosta(); // j1:en jono jo vapautettu!!
...
}
Tällöin j1:en jono vapautettaisiin ja j2:en alunperin osoittama jono jäisi ilman osoitinta ja jäisi näin ollen kellumaan muistiin:
Aliohjelman parametrin välittämisongelma voidaan korjata referensseillä kuten edellä sanottiin tai sitten kirjoittamalla luokalle yksi rakentaja lisää: kopiointimuodostaja (copy constructor, parametrina olion tyyppi), jota kutsutaan parametrin välityksen yhteydessä:
copycons.cpp - sijoitus ja kopiointi tehtävä itse
/* COPYCONS.CPP - copy constructor */
...
cJono(const cJono &j) {
max_pit = j.max_pit;
s = new char[max_pit+1];
strcpy(s,j.s);
printf("Copy constructor: "); tulosta(); // Testejä varten!
}
Näin korvataan oletus kopiointimuodostaja (default copy constructor), joka tekisi vain binäärisen sijoituksen. Kopiointimuodostaja on muodostaja, jolla on parametrinaan saman luokan olio tai viite saman luokan olioon.
Sijoitusongelma ei ratkea tälläkään, vaan joudutaan määrittelemään sijoitus uudelleen:
/* COPYCONS.CPP - copy constructor */
...
cJono &operator=(const cJono &j) {
if ( &j == this ) return *this; // Jos yritetään sijoittaa its.
if ( max_pit < j.max_pit ) { // Jollei mahdu niin uusi tila
if ( max_pit ) delete [] s;
s = new char[j.max_pit+1];
max_pit = j.max_pit;
}
strcpy(s,j.s);
return *this;
}
Huomattakoon, että turvallisuussyistä on tarkistettava, ettei arvoa sijoiteta oliolle itselleen (vrt. aliasing)! Lisäksi on syytä palauttaa sijoituksen tulos, jotta seuraava ketju olisi mahdollinen:
j3 = j2 = j1;
Sijoitusoperaattoria kutsutaan sijoituksissa ja kopiointimuodostajaa aliohjelmien arvoparametrikutsuissa ja funktion arvon palautuksessa.
Huomautus! Sijoitusoperaattori ei periydy!
3.3.3 Olion palauttaminen funktion arvona
Edellinen ongelma toistuu vielä kerran, kun palautetaan olioita funktion arvona. Palauttamista varten luodaan tilapäinen olio, jonka hävittäjää kutsutaan kun funktion palautusarvo on sijoitettu. Siksi onkin useimmiten parasta palauttaa viittauksia olioihin tai olioiden osoitteita, ei itse olioita!
Tosin esimerkiksi yhteenlaskuoperaattorissa + joudutaan yleensä palauttamaan nimenomaan uusi olio ( c = a + b ).
3.4 Opetus
Tee luokkaan aina copy constructor ja sijoitusoperaattori, mikäli luokassa on yksikin dynaaminen attribuutti.
Muista ettei sijoitusoperaattori periydy, joten molemmat joudut tekemään jokaiseen jälkeläisluokkaan!
3.5 Olion attribuutin osoitteen tai referenssin palauttaminen
Olion attribuutin osoitteen tai referenssin palauttaminen (erityisesti private-attribuutin) ystäväfunktion arvona on kuitenkin huonoa ohjelmointia ja on tiedon suojaamista vastaan:
// HUONO:
int &x_koord(cPiste &p)
{
return p.x;
}
...
cPiste p;
x_koord(p) = 5;
Tilanne paranee huomattavasti jos palautustyypiksi esitellään vakio referenssi:
const int &x_koord(cPiste &p)
{
return p.x;
}
...
cPiste p;
// x_koord(p) = 5; // Ei ole enää sallittu!
3.6 Move constructor
Copy constructorilla halusimme luoda oliosta kopion, jonka muuttaminen ei vaikuta alkuperäiseen olioon. Tuloksena saatiin kaksi toisistaan riippumatonta oliota. Move constructorilla haluamme alustaa uuden olion tilapäisen olion tiedoilla. Tilapäiseen olioon vain viitataan hieman eri tavalla. Tavallinen referenssimuuttuja ei käy. Kyse on siis samasta asiasta eri käyttötilanteessa.
cJono(cJono &&j) /* move constructor */
{
max_pit = j.max_pit;
s = j.s;
j.max_pit = 0;
j.s = nullptr;
}
Erilainen viittaustapa johtuu yksinkertaisesti siitä ettei arvoa voi käyttää muuttujana. Esim. 7 = x aiheuttaisi virheen. Const-muuttujat eivät ole poikkeus, sillä nekin alustetaan, mutta vain kerran.
Muutamia ominaisuuksia joita hyödynnetään toteutuksessa. Tilapäisen olion ei tarvitse säilyä muuttumattomana, koska sitä ei tarvita eikä siihen voi viitata käytön jälkeen. Siksi kuvainnollisesti usein siirretään tai liikutetaan tiedot tilapäisestä oliosta uuteen olioon, mistä tulee nimitys "Move constructor". Täytyy vain pitää huoli ettei tilapäisen olion destruktori käy tuhoamassa uuden olion tietoja (aliasing-ongelma).
Pitää kutsua:
cJono j2 = std::move(j1);
3.7 Move sijoitusoperaattori
Copy sijoitusoperaattorilla halusimme korvata olion (muuttuja) tiedot toisen olion (muuttuja) tiedoilla, siten että muutokset vaikuttavat vain siihen itseensä. Eli tiedot tietyssä mielessä kopioidaan oliolta toiselle. Move sijoitusoperaattorilla haluamme korvata olion (muuttuja) tiedot tilapäisen olion tiedoilla (arvo), jonka tietoja ei tarvitse säilyttää. Eli tietoja usein siirretään, mistä nimitys "Move sijoitusoperaattori". Lopuksi ohjataan tilapäisen olion osoittimet paikkaan, jonka poistamisesta ei ole harmia, esim. nullptr. Lisäksi huolehditaan ettei olio voi sijoittaa itseä itseensä.
cJono& operator=(cJono &&j) /* move assignment operator */
{
if (this == &j) return *this;
delete[] s;
max_pit = j.max_pit;
s = j.s;
j.max_pit = 0;
j.s = nullptr;
return *this;
}
Muistettavaa:
- estä sijoitus itseensä
- vapauta resurssit
3.8 Vaihtoehtoinen ratkaisu
cJono& operator=(cJono j) /* kopioi ja vaihda */
{
std::swap(max_pit, j.max_pit);
std::swap(s, j.s);
return *this;
}
Vaihtoehtoisessa ratkaisussa luodaan Copy constructorilla uusi olio käsiteltäväksi. Koska muuttuja esitellään jo parametrilistassa, ei aliohjelmaan mennä lainkaan jos muuttujan alustuksessa tapahtuu virhe. Vaihda-metodi vaihtaa olioiden osoittimet keskenään. Aliohjelmasta poistuttaessa väliaikaisen olion tuhoaja vapauttaa resurssit. Sijoitus tuottaa siis aina halutun lopputuloksen, vaikka menetetään tavoiteltu suorituskyky. Vaihdossa saadaan käyttökelpoinen ja vikasietoinen muistinhallinta. Monessa tapauksessa se riittää.
Move sijoitusoperaattoria kannattaa hyödyntää Move constructorissa. Koodin määrä vähenee, ylläpidettävyys paranee.
Pitää kutsua:
cJono j2;
j2 = std::move(j1);
4. C++ ja valmiit luokkakirjastot
Tässä luvussa tutustumme muutamiin tyypillisiin C++:n luokkakirjastoihin.
4.1 Merkkijonoluokka string
Koska merkkijonojen käsittely on eräs C–kielen heikkouksista, olisi toivottavaa että luokkakirjastoista löytyisi jokin string-luokka.
1990-luvun lopulla C++ -standardiin tuli vihdoin merkkijonoluokka string
, jossa on vielä toki puutteita moninen muiden kielten merkkijonoihin verrattuna. Tätä ennen jokaisella kääntäjävalmsitajalla oli oma merkkijonoluokkansa ja osin tätä esiintyy vieläkin.
cpp\ansicpp\stringt2.cpp - testataan std:n string-luokkaa
Kunpa standardienkin tekijät joskus tekisivät itse oikean ohjelman! Erityisesti jää kaipaamaan välilyöntien siivoamista ja vertailua, jossa isot ja pienet kirjaimet samaistetaan.
Käsittämätöntä että esimerkiksi niin yksinkertainen asia kuin trim
puuttuu vielä C++ -standardista. Tuossa mielenkiintoisia ehdotuksia miten tuo tehdään:
http://stackoverflow.com/questions/216823/whats-the-best-way-to-trim-stdstring
Voisi kuvitella, että myös seuraavat ominaisuudet olisivat hyödyllisiä:
cpp\ansicpp\istrtes2.cpp - merkkijonotoiveiden testaus
string s1("3"),s2("7"),s3;
s3 = s2+"-4"; T(s3); // => |3|
s3 = 'A'; T(s3); // => |A|
s3 = s1+5; T(s3); // => |8|
s3 = 5+s1; T(s3); // => |8|
s3 = s1+s2; T(s3); // => |10|
s3 = "4"+s2; T(s3); // => |11|
s3 = s2+"4"; T(s3); // => |11|
s3 = string("56")+"7"; T(s3); // => |63|
s3 = "K"+s1; T(s3); // => |K3|
s3 = s1+'5'; T(s3); // => |8|
s3 = s1+'A'; T(s3); // => |3A|
// char ch = s1; T(ch); // => |3|
// s3 = 5; T(s3); // => |5|
//int i = s3; T(i); // => |5|
Tehtävä 1.1 string- luokan muuttaminen
Mitä tarvitsee tehdä, jotta edellä mainitut toiveet saataisiin toteutettua string-luokalle. Entä jos halutaan tehdä luokka jossa kommenteiksi laitetut sijoituksetkin ovat mahdollisia.
4.2 STL = Standard Template Library
Usein tulee vastaan tilanne, jossa olioita tarvitsee tallettaa jonnekin. Teimme aikaisemmin alkeellisen yleiskäyttöisen lineaarisen listan. Ongelma on kuitenkin niin yleinen, että on valmiina tarjolla erilaisia yleisiä säiliöluokkia (container class) ja niitä läpikäyviä luokkia (iterator class).
Usein on jopa monia eri mahdollisuuksia valittavan itse fyysiselle toteutukselle. Esimerkiksi pino voi olla toteutettu joko linkitettynä listana tai vektorina.
Tietorakenne voi olla järjestetty tai järjestämätön. Järjestetyn tietorakenteen käyttämiseksi talletettavan olion luokassa täytyy olla tietysti vertailumetodi.
Standarditietorakenteet perustuvat luokkamallien ja algoritmien käyttöön. Itse asiassa varsinaista linkitettävää koodia ei juurikaan tule standardikirjaston takia. Kääntäminen on hitaampaan ajonaikaisen suoritusnopeuden kustannuksella.
4.2.1 Yksinkertainen listaesimerkki
cpp\luokat\stdlist.cpp - esimerkki STL:n listasta
Iteraattorin tarkoituksena on antaa "indeksi" (lci
= list char iterator), jonka avulla joukon alkioita voidaan käydä lävitse.
auto // C++11 osaa itse päätellä oikean tyypin nimen begin()
// metodin palauttamasta arvosta
lci = lc.begin() // iteraattori tietorakenteen alkuun
lci++ // siirtyy seuraavaan alkioon
lci != lc.end(); // palauttaa tiedon siitä, onko alkioita vielä jäljellä
*lci // palauttaa lci:n kohdalla olevan alkion
4.2.2 Järjestetty joukko
4.2.3 Assosiatiivinen taulukko
Seuraava esimerkki paljastaa ehkä paremmin STL:n voimaa.
cpp\luokat\stdmap.cpp - esimerkki STL:n assosiatiivisesta taulukosta
4.2.4 Algoritmit
Edellisessä esimerkissä stdmap.cpp käytettiin "algoritmia" for_each tulostamaan jokainen alkio. Samoja algoritmeja voidaan käyttää muillekin tietorakenteille. Jopa tavallisille taulukoille:
cpp\luokat\foreach.cpp - esimerkki STL:n algoritmeista
Tehtävä 1.3 foreach
Muuta esimerkkien stdlist.cpp ja stdset.cpp tulostus toimimaan käyttäen algoritmia foreach. Lisää kumpaankin esimerkkiin vielä suurimman alkion etsiminen ja tulostaminen.
5. APF - ohjelmarunko
5.1 Mikä on APF (Application Framework)
Nykyisin on varsin suosittu tapa tehdä käänteisiä kirjastoja: annetaan käyttäjälle valmiina pääohjelma ja yleisimmät operaatiot hoitavat aliohjelmat. Erityisesti graafisten käyttöliittymien ohjelmoinnissa täytyy tavallisilla menetelmillä ohjelmoitaessa kopioida pohjaksi jokin "kaikkien ohjelmien äiti" = viestisilmukka, ikkunafunktio, sovelluksen ja ikkunoiden alustukset.
APF-kirjastoissa nämä rutiinitoiminnot on piilotettu omaan pääohjelmaan, yleensä vieläpä luokkaan, joka tarvittavien alustusten jälkeen kutsuu kirjaston (luokkakirjaston) käyttäjän alustuksia (pääohjelmaa). Näin ohjelmoinnin rutiinityö pitäisi vähentyä.
5.1.1 Ei standardia
Kuitenkin APFät eivät ole mitenkään standartoituneet ja kukin valmistaja tekee tietenkin oman versionsa joka ei ole mitenkään yhteensopiva muiden versioiden kanssa.
Uuden APFän käytön opiskelu saattaa viedä yhtä kauan kuin omaan käyttöön tehdyn riittävillä toiminnoilla varustetun APFän suunnittelu. Oman kirjaston etuna on täysi tuntemus sen luokkahierarkiasta ja näin varmuus toiminnasta.
5.1.2 Edut ja haitat
Vierasta APFää käytettäessä ohjekirja täytyy selata varsin ahkeraan. Usein saattaa tuntua turhauttavalta esimerkiksi, että "tutut" Windows API-kutsut on piilotettu luokkien sisään. Tällöin herääkin kysymys: Kannattaako opetella jokin APF vaiko ohjelmoida samantien Visual Basicillä tai Borland Delphillä? Kaikissa tapauksissa on oikeastaan kyseessä uuden kielen opettelu eli itse C++ jää APF:än tapauksessa varsin pienelle merkitykselle.
Kuitenkin hyvä APF tarjoaa epäilemättä lisää varmuutta ohjelmointiin. Luokkien avulla päästään esimerkiksi lähes kokonaan eroon muuten niin kiusallista globaaleista muuttujista.
Erityinen etu on siinä, että hyvin tehdyn APFän alla oleva toteutus voidaan muuttaa mihin käyttöliittymään tahansa. Tosin tällöin APF ei voi tietenkään tukea tietyn käyttöliittymän erityisominaisuuksia.
APFien päälle on varsin helppo tehdä Visual Basicmäisiä koodigenaraattoreita, joissa ohjelmoija maalaa resurssieditorilla haluamansa sovelluksen ja generaattori tekee tyhjiä luokkien metodeja erilaisten tapahtumien hoitamiseksi. Ohjelmoija sitten täyttää nämä tyhjät metodirungot haluamillaan toiminnoilla joko heti tai myöhemmin.
5.1.3 Yleisiä luokkia
Yleensä kaikissa APF-kirjastoissa on ainakin ikkunaluokka. Sovelluksen ikkunalle peritään jonkin perusikkunan ominaisuudet ja sitten luokkaan lisätään sovelluksen erityisominaisuuksia. Esimerkiksi kirjoitetaan metodit siitä, mihin viestiin vastataan milläkin tavalla. Se miten metodit liitetään tiettyihin viesteihin riippuu vahvasti APF:stä.
Esimerkiksi LAFissa on tietty määrä valmiita metodien nimiä, jotka uudelleenkirjoitetaan haluttaessa. Samoin on Windows++:ssa. Borlandin OWL 1.0:ssa (Object Windows Library) oli varsin mukava tapa ilmoittaa metodin nimen perässä sitä vastaavan viestin numero. Tämä oli kuitenkin C++:aan lisätty epästandardi ominaisuus - joten se siitä (tosin koska OWL:ääkään ei käytetä muuta kuin Borlandin kääntäjän kanssa, niin olisiko tuosta sittenkään ollut paljoa haittaa).
OWL 2.0:sta alkaen Borland on käyttänyt Microsoftin Visual C++:an MFC:n (Microsoft Foundation Class Library) tapaa muistuttavaa makroilla toteutettua taulukkoa viestien ja funktioiden yhdistämiseksi. Tämä voidaan toteuttaa standardi C++:lla, mutta on OWL 1.0:aan verrattuna sotkuisemman näköinen ja lisäksi pahimmassa tapauksessa sama funktio tulee esiintymään kolmessa eri kohdassa ohjelmaa, joten nimiä muutettaessa tulee olla tarkkana. Toteutus mahdollistaa kuitenkin saman funktion käytön useamman eri viestin käsittelyyn.
Ikkunaluokan lisäksi usein on vielä sovellusluokka, joka perii yleisen sovelluksen ominaisuudet. Joissakin kirjastoissa sovellus- ja ikkunaluokka on yhdistetty (esim. LAF).
Kirjastojen "pääohjelma" ei välttämättä ole WinMain eikä main. "Pääohjelmassa" luodaan yleensä sovellusluokka ja sitten kutsutaan sovellusluokan run-funktiota, joka käytännössä pyörittää ohjelman viestisilmukkaa.
5.2 "MopoCad"–esimerkki
Seuraavaksi esitetään esimerkkejä muutamista Windows-ohjelmointiin tarkoitetuista APF-toteutuksista. Kaikilla on tehty "MopoCad" –piirto–ohjelma.
5.2.1 Toiminta ja toiminnot
Piirretään jatkuvaa murtoviivaa aina kun hiiren vasen nappi on alhaalla. Koska Windows ei itse huolehdi näytön sisällön tiedoista, joudutaan piirretty kuvio tallettamaan omaan tietorakenteeseen. Tietorakenne on toteutettu APF:än omilla ominaisuuksilla tai niiden sopivalla luokkakirjastolla. Kun Windows pyytää näytön päivitystä, piirretään koko tietorakenne uudelleen (josta tosin fyysistä piirtoa tapahtuu vain ikkunan "pilaantuneisiin osiin").
Oikealla hiiren napilla tyhjennetään näyttö ja samalla tietysti tietorakenne. Lisäksi ikkunassa on alkeellinen menu ja Help-valikosta saadaan About-dialogi, joka näyttää tietoja ohjelmasta.
Ohjelmasta voidaan poistua joko menun File/Exit avulla, painamalla Alt-X tai painamalla Alt-F4.
5.2.2 Eri versioiden yhteiset tiedostot
Toteutusesimerkit on pyritty tekemään siten, että ainoastaan itse ohjelmaosa on kussakin esimerkissä erilainen. Samat resurssit kelpaavat kaikkiin esimerkkeihin. Koska Windows++ esimerkki on kirjoitettu viimeisenä, ei resurssien nimeämisessä ole älytty käyttää Windows++ ehdotuksia, jolloin Windows++ ohjelma olisi hieman lyhentynyt.
5.2.3 Otsikkotiedosto komennoille
Menun ja pikavalintanäppäinten tunnukset on kasattu yhteen tiedostoon.
apf\mopocad.rh - resurssitiedoston tunnukset mopocad -esimerkkejä varten
#define CM_HELP_ABOUT 1011
#define CM_FILE_EXIT 101
5.2.4 Resurssitiedosto
Resurssitiedostossa on esitelty ohjelman menurakenne ja About-dialogin ulkoasu.
apf\mopocad.rc - resurssitiedosto mopocad -esimerkkejä varten
#include "mopocad.rh"
PAAMENU MENU
{
POPUP "&File"
{
MENUITEM "E&xit", CM_FILE_EXIT
}
POPUP "&Help", HELP
{
MENUITEM "&About", CM_HELP_ABOUT
}
}
DLG_ABOUT DIALOG 6, 15, 128, 68
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
{
DEFPUSHBUTTON "Ok", IDOK, 9, 49, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 69, 49, 50, 14
ICON "Ikoni", -1, 7, 13, 18, 20
LTEXT "MopoCad / C++ VL 14.11.1993", -1, 34, 16, 60, 24
}
IKONI ICON "mopocad.ico"
PIKA ACCELERATORS
{
"x", CM_FILE_EXIT, ASCII, ALT
}
5.3 Autolaskuri-esimerkki
Autolaskuri on yksinkertainen ohjelma, jossa pääikkunana on dialogi. Tästä on esitetty ohjelmalistaus OWL:llä ja MFC:llä.
apf\laskuri.rh - tunnukset autolaskuria varten
/**************/
/* laskurir.h */
#define LASKUREITA 2
#define HA 1000
#define KA HA+1
#define HAL 1010
#define KAL HAL+1
#define EXIT 1027
#define NOLLAA 1028
apf\laskuri.rc - resurssitiedosto autolaskuria varten
#include "laskuri.rh"
LASKURI DIALOG 17, 25, 180, 88
STYLE WS_CHILD | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "LASKURI"
{
PUSHBUTTON "&Henkilöautoja", HA, 33, 10, 50, 20
PUSHBUTTON "&Kuorma-autoja", KA, 100, 10, 50, 20
RTEXT "0 ", HAL, 31, 40, 50, 10, SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_GROUP
RTEXT "0 ", KAL, 100, 40, 50, 10, SS_RIGHT | WS_CHILD | WS_VISIBLE | WS_BORDER |
WS_GROUP
PUSHBUTTON "&Nollaa", NOLLAA, 50, 60, 80, 20
PUSHBUTTON "e&Xit", EXIT, 0, 0, 24, 14
}
5.4 LAF (Little Application Framework)
LAF on tehty pieneksi perusrungoksi, jolla olio–ohjelmointikursseilla esiteltyjä ideoita päästään nopeasti kokeilemaan ilman aikaisempaa graafisten liittymien ohjelmointikokemusta. Kirjaston tarkoitus ei ole tarjota täyttä toiminnallisuutta, vaan aivan tärkeimmät perustoiminnot. Tämän vuoksi käskyvalikoima on varsin suppea ja malliohjelmakin on toiminnoiltaan toteutettu muista poikkeavaksi.
Esimerkiksi hiirestä saadaan vain alas–viesti. Näin ollen jatkuvaa viivan piirtämistä ei ole voitu toteuttaa, vaan viiva vedetään aina edellisestä napsautuskohdasta seuraavaan. Koska toiminnot jäävät rajoitetuksi, on esimerkistä jätetty myös viivojen talletus pois, eli kuva häviää, mikäli toinen ikkuna käy kuvan päällä.
LAF tukee vain yhtä ikkunaa, joten esimerkiksi About-dialogiakaan ei voida toteuttaa kirjaston ehdoilla.
Siis LAF-esimerkin tarkoitus onkin näyttää APF-ohjelmoinnin mahdollisuudet piilottaa rutiinit pois käyttäjän näkyvistä.
Katso esimerkki tiedostosta: laf\mopocad.cpp
5.5 OWL 5.0
Borlandin Object Windows Library 2.0:an tarkoitus on antaa OWL 1.0:aa paremmin "standardi" C++:aa vastaava APF. Muutokset OWL 1.0:aan nähden ovat varsin radikaaleja ja isomman OWL 1.0 ohjelman muuttaminen OWL 2.0:ksi teettää jonkin verran työtä. OWL 5.0:aan mennessä on vain lisätty uusia ominaisuuksia ja ruvettu paremmin tukemaan WIN32-rajapintaa.
5.5.1 Pääluokat
TApplication
- Varsinainen sovellusluokka.
TFrameWindow
- Sovelluksen pääikkuna peritään tästä luokasta. Luokkaan lisätään metodit haluttujen viestien (tapahtumien) käsittelemiseksi sekä ikkunan "sisäiset" muuttujat.
TDC
- Tästä luokasta ja sen perillisistä saadaan laiteyhteydet:
HoldDC = new TClientDC(HWindow);
- Piirtäminen suoritetaan käyttämällä tämän luokan metodeja:
HoldDC->MoveTo(point);
5.5.2 Yksinkertainen esimerkki: hello.cpp
apf\owl5\hello.cpp - OWL 5.0 -versio
/****************/
/* hello.cpp */
/****************/
// Pienin OWL 5.0 ohjelma.! Projektiin vain tämä tiedosto
#include <owl\applicat.h>
class THelloApp : public TApplication {
public:
THelloApp(const char far *name = 0) : TApplication(name) {};
};
int OwlMain(int , char far * [])
{
return THelloApp("Hello World!").Run();
}
5.5.3 Piirtäminen ikkunaan
Lisätään edelliseen esimerkkiin alkeellinen piirto ikkunaan:
apf\owl5\hello2.cpp - OWL 5.0: piirtäminen ikkunaan
/****************/
/* hello2.cpp */
/****************/
// Ohjelma, joka aukaisee yhden ikkunan ja kirjoittaa siihen Hello World!
// Projektiin vain tämä tiedosto.
#include <owl\pch.h>
//------------------------------------------------------------------------------
class TMainWindow : public TFrameWindow {
public:
TMainWindow(TWindow *Parent, LPCSTR ATitle) : TFrameWindow(Parent, ATitle) {}
void Paint(TDC &hdc,bool, TRect &) { hdc.TextOut(10,10,"Hello World!"); }
DECLARE_RESPONSE_TABLE(TMainWindow);
};
DEFINE_RESPONSE_TABLE1(TMainWindow,TFrameWindow)
EV_WM_PAINT,
END_RESPONSE_TABLE;
//------------------------------------------------------------------------------
class THelloApp : public TApplication {
public:
THelloApp(const char far *name = 0) : TApplication(name) {};
void InitMainWindow() { MainWindow = new TMainWindow(NULL, Name); }
};
//------------------------------------------------------------------------------
int OwlMain(int ,char far * [])
{
return THelloApp("Hello World!").Run();
}
5.5.4 Viestien käsittely
Viestin ja sitä käsittelevän metodin yhdistäminen on toteutettu seuraavasti:
- Ikkunan luokassa kutsutaan makroa DECLARE_RESPONSE_TABLE:
class TMainWindow : ...
...
virtual void Exit();
virtual void About();
void EvLButtonDown(UINT modKeys, TPoint& point);
void Paint(TDC &hdc,bool erase, TRect &rect);
DECLARE_RESPONSE_TABLE(TMainWindow);
}
- Varsinainen taulukko täytetään luokan esittelyn jälkeen:
DEFINE_RESPONSE_TABLE1(TMainWindow,TFrameWindow)
EV_COMMAND(CM_FILE_EXIT, Exit),
EV_COMMAND(CM_HELP_ABOUT, About),
EV_WM_LBUTTONDOWN,
EV_WM_PAINT,
END_RESPONSE_TABLE;
Taulukossa osassa tapahtumista voidaan antaa mukaan funktion nimi ja tällöin nimi voidaan vapaasti keksiä itse. EV-tapahtumista (makroista) on erinimisiä versioita erilaisille parametrikombinaatioille. Esimerkissä on käytetty void -parametrilistoja. Osalle funktioita nimenä täytyy käyttää Windowsin viestin nimestä muokattua nimeä.
5.5.5 owl5\laskuri.cpp
Seuraavassa autolaskuri toteutettuna OWL 5.0:lla:
apf\owl5\laskuri.cpp - OWL 5.0 -versio autolaskurista
/****************/
/* laskuri.cpp */
/****************/
// Esimerkki Autolaskurista. Projektiin laskuri.cpp ja laskuri.rc
#include <owl\pch.h>
#include "..\laskuri.rh"
//------------------------------------------------------------------------------
class TLaskuriDialog : public TDialog {
public:
TLaskuriDialog(TWindow *parent, TResId resId, TModule *module = 0) :
TDialog(parent,resId,module) {}
virtual ~TLaskuriDialog() { Destroy(); }
void BNHAClicked() { SetDlgItemInt(HAL,GetDlgItemInt(HAL)+1); }
void BNKAClicked() { SetDlgItemInt(KAL,GetDlgItemInt(KAL)+1); }
void BNNollaaClicked() { SetDlgItemInt(HAL,0); SetDlgItemInt(KAL,0); }
void BNExitClicked() { Destroy(); }
DECLARE_RESPONSE_TABLE(TLaskuriDialog);
};
DEFINE_RESPONSE_TABLE1(TLaskuriDialog, TDialog)
EV_BN_CLICKED(HA,BNHAClicked),
EV_BN_CLICKED(KA,BNKAClicked),
EV_BN_CLICKED(NOLLAA,BNNollaaClicked),
EV_BN_CLICKED(EXIT,BNExitClicked),
END_RESPONSE_TABLE;
//------------------------------------------------------------------------------
class TLaskuriApp : public TApplication
{
public:
TLaskuriApp(const char *title) : TApplication(title) {}
void InitMainWindow() {
SetMainWindow(new TFrameWindow(0, Name,
new TLaskuriDialog(0, "LASKURI") , true)); // Tyyli WS_CHILD
}
};
/********************* Pääohjelma *********************************************/
int OwlMain(int ,char far * [])
{
return TLaskuriApp("Autolaskuri").Run();
}
Tehtävä 5.43 cLaskuri
Lisää laskuri.cpp:hen luokka cLaskuri, jonka avulla itse tapahtumat saadaan "siistiksi":
class TLaskuriDialog : public TDialog {...
cLaskuri hal,kal;
...
void BNHAClicked() { hal++; }
void BNKAClicked() { kal++; }
void BNNollaaClicked() { hal = kal = 0; }
...
5.5.6 owl5\mopocad.cpp
Seuraavassa on MopoCad–toteutus OWL 5.0:lla. Hakemistosta owl2 löytyy vastaava OWL 2.5 –toteutus. Suurin ero on se, että pisteiden talletukseen on käytetty nyt STL:n deque–luokkaa ja aikaisemmin kirjastossa määritelty tyyppi BOOL on muuttunut kieleen kuuluvaksi bool–tyypiksi
apf\owl5\mopocad.cpp - OWL 5.0 -versio
/****************/
/* MopoCad.CPP */
/****************/
// Esimerkki Borland C++ 5.0:n OWL 5.0-kirjaston käytöstä.
// Ohjelmalla piirretään viivaa kun hiiri pidetään pohjassa.
#include <owl\applicat.h>
#include <owl\framewin.h>
#include <owl\dc.h>
#include <owl\dialog.h>
#include <owl\owlpch.h>
#include "..\mopocad.rh"
#include <deque>
#pragma warn -aus
using namespace std;
typedef deque<TPoint> cPoints;
typedef cPoints::iterator cPointsIterator;
TPoint MovePoint(-2000,-2000); // Merkki taulukkoon siirrosta
/************************ Luokat **********************************************/
class TMopoCadApp : public TApplication {
public:
TMopoCadApp(const char far *name = 0) : TApplication(name) {};
virtual void InitMainWindow();
virtual void InitInstance();
};
class TMainWindow : public TFrameWindow {
TClientDC* HoldDC;
BOOL ButtonDown;
cPoints Points;
protected:
LPSTR GetClassName() { return "MopoCadWndClass"; }
void DeleteAllPoints();
void ClearWindow();
public:
TMainWindow(TWindow *Parent, LPSTR ATitle);
~TMainWindow();
virtual void Exit();
virtual void About();
void EvLButtonDown(UINT modKeys, TPoint& point);
void EvLButtonUp(UINT modKeys, TPoint& point);
void EvMouseMove(UINT modKeys, TPoint& point);
void EvRButtonDown(UINT modKeys, TPoint& point);
void Paint(TDC &hdc,bool erase, TRect &rect);
DECLARE_RESPONSE_TABLE(TMainWindow);
};
/********************* Pääikkunan tapahtumataulu ******************************/
DEFINE_RESPONSE_TABLE1(TMainWindow,TFrameWindow)
EV_COMMAND(CM_FILE_EXIT, Exit),
EV_COMMAND(CM_HELP_ABOUT, About),
EV_WM_LBUTTONDOWN,
EV_WM_LBUTTONUP,
EV_WM_MOUSEMOVE,
EV_WM_RBUTTONDOWN,
EV_WM_PAINT,
END_RESPONSE_TABLE;
class TDLG_AboutDlg : public TDialog{ // Tässä tarpeeton, ks. TMainWin::About!
public:
TDLG_AboutDlg(TWindow *Parent, TResId ResId):
TDialog(Parent,ResId) {};
virtual void Ok();
DECLARE_RESPONSE_TABLE(TDLG_AboutDlg);
};
DEFINE_RESPONSE_TABLE1(TDLG_AboutDlg,TDialog)
EV_COMMAND(IDOK, Ok),
END_RESPONSE_TABLE;
/********************* TDLG_AboutDlg metodit: *********************************/
void TDLG_AboutDlg::Ok()
{
Destroy();
}
/********************* TMainWindow metodit ************************************/
TMainWindow::TMainWindow(TWindow *Parent, LPSTR ATitle)
: TFrameWindow(Parent, ATitle)
{
ButtonDown = FALSE;
SetIcon(GetModule(),"IKONI");
Attr.AccelTable = "PIKA";
AssignMenu("PaaMenu");
}
TMainWindow::~TMainWindow()
{
DeleteAllPoints();
}
void TMainWindow::Exit()
{
PostQuitMessage(0);
}
void TMainWindow::About()
{
GetModule()->ExecDialog(new TDLG_AboutDlg(this, "DLG_ABOUT"));
// Tosin voitaisiin kutsua suoraan geneeristä TDialog ja jättää
// koko TDLG_AboutDlg luokka pois!
// GetModule()->ExecDialog(new TDialog(this, "DLG_ABOUT"));
}
void TMainWindow::EvLButtonDown(UINT, TPoint& point)
{
if ( ButtonDown) return;
ButtonDown = TRUE;
SetCapture(); // Direct all subsequent mouse input to this window
HoldDC = new TClientDC(HWindow);
HoldDC->MoveTo(point);
Points.push_back(MovePoint);
Points.push_back(point);
}
void TMainWindow::EvMouseMove(UINT, TPoint& point)
{
if ( !ButtonDown ) return;
HoldDC->LineTo(point);
Points.push_back(point);
}
void TMainWindow::EvLButtonUp(UINT, TPoint&)
{
if ( !ButtonDown ) return;
ReleaseCapture();
delete HoldDC;
HoldDC = 0;
ButtonDown = FALSE;
}
void TMainWindow::EvRButtonDown(UINT, TPoint&)
{
if ( ButtonDown ) return;
ClearWindow();
}
void TMainWindow::Paint(TDC &hdc,bool, TRect &)
{
cPointsIterator pi;
for (pi=Points.begin(); pi != Points.end(); pi++)
if ( *pi == MovePoint ) hdc.MoveTo(*++pi);
else hdc.LineTo(*pi);
}
void TMainWindow::DeleteAllPoints()
{
Points.erase(Points.begin(),Points.end());
}
void TMainWindow::ClearWindow()
{
DeleteAllPoints();
Invalidate(TRUE);
}
/********************* TMopoCadApp metodit ************************************/
void TMopoCadApp::InitInstance()
{
TApplication::InitInstance();
}
void TMopoCadApp::InitMainWindow()
{
MainWindow = new TMainWindow(NULL, Name);
}
/********************* Pääohjelma *********************************************/
int OwlMain(int ,char far *[])
{ /* Huom! varmista että edellä on char *[], minulla meni 2h kun ei ollut! */
TMopoCadApp MopoCadApp("MopoCad");
int ret = MopoCadApp.Run();
return ret;
}
Tehtävä 5.44 cKuvioLista
Modifioi edellistä esimerkkiä lisäämällä siihen cKuvioLista, jolla on seuraavia ominaisuuksia (toteuta samalla graafinen versio cKuvio- luokasta ja sen tarvittavista jälkeläisistä):
PoistaKaikki()
- poistaa kaikki listassa olevat kuviotPiirra(TDC &hdc)
- piirtää koko listan uudelleen laiteyhteyteen hdcLisaa(cKuvio *kuvio)
- lisää listaan uuden kuvion
5.5.7 OWL 5.0 luokkahierarkia (osa)
5.6 Windows++
Windows++ on Paul DiLascia:n hieno esitys siitä, miten APF voidaan rakentaa itse. Kirjaston kehittämisen idea on kuvattu kirjassa Windows++ (Paul DiLascia, Windows++: Writing Reusable Windows Code in C++, Addison Wesley 1992).
Tarkoituksena ei niinkään ole ollut kehittää kaupallista tuotetta, vaan kuvata sitä miten kirjasto rakennetaan ja miten luokkarakenne muuttuu ja kasvaa käyttötilanteiden asettamien vaatimusten mukaisesti. Valmis kirjasto onkin sitten esimerkkien käyttötilanteisiin varsin hyvin soveltuva. Luokat ovat hyvin suunniteltuja ja toimivia.
Lisäksi kirjastossa on valmiina hieno dialogien käsittely, jossa varsin vaivattomasti voidaan dialogi yhdistää sitä kuvaavaan tietueeseen (struct). Normaalistihan tällaisten syöttödialogien ylläpito vaatii varsin paljon ohjelmakoodia, joka toistuu useissa eri kohdissa ohjelmaa.
Windowsin tavat käsitellä eri tyyppisiä dialogeja ja muitakin eri tyyppisiä ikkunoita eri arvoisina on piilotettu luokkarakenteen taakse. Nyt eri tyyppiset ikkunat ja dialogit näyttävät ohjelmoijan kannalta samanarvoisilta ja vältytään virheiltä vaihdettaessa esimerkiksi dialogin tyyppiä modaalisesta modeless-dialogiksi.
5.6.1 Pääluokat
Sovellusluokka Sovellusluokka on kirjastoon sisäänrakennettuna ja pääohjelman ainoa tehtävä on luoda ohjelman pääikkuna.
WPMainWin
- Luokka josta peritään ohjelman pääikkuna. Luokkaan lisätään jälleen viestinkäsittelystä vastaavat funktiot ja ikkunan "sisäiset" muuttujat.
WPWin
- "Kaikkien ikkunoiden äiti". Kaikki ikkunaan liittyvät ominaisuudet. Esimerkiksi sijoitus hdc = this antaa laiteyhteyden jos this on ikkunatyyppinen osoitin.
GPShape
- Geneerinen luokka erilaisia graafisia olioita varten
GPShapeList
- Luokka joka tallettaa graafisia olioita. Antaa mm. mahdollisuuden piirtää kaikki talletetut oliot yhdellä metodikutsulla.
WPWinDC
- Luokka, joka vastaa laiteyhteyksiä. Kuten OWL 2.5:ssa tämä luokka käsittelee varsinaiset piirtämiset.
5.6.2 Viestien käsittely
Viestien käsittelyä varten funktioiden täytyy olla tietyn nimisiä. Esimerkiksi mouse-niminen metodi käsittelee kaikki hiiriviestit ja ohjelmoija joutuu itse metodissa tutkimaan mistä viestistä oli kyse.
Tämä johtaa taas helposti siihen suureen switch-lauseeseen, josta koko APF-idealla yritetään päästä eroon. Toisaalta ainakin kaikkien hiiriviestien käsittely on mahdollista eikä tule vastaavia rajoitteita kuin LAFissa. Kirjoittajan mielestä kukin ohjelmoija voi sitten itse helposti toteuttaa funktion joko switch-lauseella tai hyppytaulukon avulla. Tämä on tietysti totta.
5.6.3 Muita ideoita
Windows++ kirjassa ei tyydytä pelkästään C++ ohjelmointiin, vaan siinä tarjotaan ideaa: nimetään myös resursseissa aina kaikki eri ohjelmissa toistuvat toiminnot ja resurssit samalla nimellä, jolloin resurssit voidaan "periä" toiseenkin ohjelmaan. Näin perusohjelmassakin voi olla esimerkiksi menu ja About-dialogi ilman mitään muutoksia itse ohjelmaan. Varsinainen APF:än pääohjelma on tehty käyttämään valittuja nimiä automaattisesti!
Tällöin pitää olla mm. seuraavat nimet:
- AppMenu - päämenu
- AppIcon - ohjelman ikoni
- DLGABOUT - About-dialogi
- AppAccel - ohjelman pikanäppäimet
- WPIDM_EXIT - menun Exit-kohdasta tuleva viesti
- WPIDM_ABOUT - menun About-kohdasta tuleva viesti
5.6.4 Esimerkit
Katso esimerkkejä tiedostoista: wpp\hello.cpp ja wpp\mopocad.cpp
5.6.5 Windows++ –luokkahierarkia
5.7 MicroSoft MFC
MicroSoftinFoundation Classia (MFC) pidetään yleisesti hieman vähemmän abstraktiota tarjoavana kuin OWL:ää. Eräs esimerkki on tästä on dialogien käyttö. OWL:ssä ohjelmoija voi aina lopettaa dialogin kutsulla Destroy(); MFC:ssä ohjelmoijan pitää käyttää eri lopetusta sen mukaan onko kyseessä modal vai modeless dialogi.
Viestinkäsittely ja luokkahierarkia on suurinpiirtein samanlainen kuin OWL:ssä. Luokien nimet alkavat T:n sijasta C:llä. Pääohjelmaa ei ole lainkaan, sovellusluokan konstruktori vastaa pääohjelmaa. Seuraavat esimerkit puhukoon puolestaan.
5.7.1 mfc\hello.cpp
Huomaa seuraavassa pääohjelman puute:
apf\mfc\hello.cpp - MFC 4.0 -versio
// hello.cpp
#include <afxwin.h>
class CMainWindow : public CFrameWnd {
public:
CMainWindow() { Create(NULL,"Hello World!"); }
};
class CHelloApp : public CWinApp {
public:
virtual BOOL InitInstance() {
m_pMainWnd = new CMainWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
};
CHelloApp HelloApp; // HelloApp's constructor initializes and runs the app
Tehtävä 5.45 Automaattinen ikkunan näyttäminen
Muuta hello.cpp siten, että ikkunan näytetään automaattisesti luotaessa.
5.7.2 mfc\hello2.cpp
Oikeasti viestin käsittelijöiden eteen laitetaan afx_msg
class CMainWindow : public CFrameWnd {
public:
CMainWindow() { Create(NULL,"Hello World!"); }
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
};
mutta koska afx_msg on määritelty tyhjäksi, ei tätä käytetä jatkoesimerkeissä jotta esimerkit olisivat enemmän OWL –esimerkkien näköisiä.
apf\mfc\hello2.cpp - MFC 4.0: piirtäminen ikkunaan
// hello2.cpp
#include <afxwin.h>
class CMainWindow : public CFrameWnd {
public:
CMainWindow() { Create(NULL,"Hello World!"); }
void OnPaint() {
CPaintDC dc(this);
dc.TextOut(10,10,"Hello world!");
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP( CMainWindow, CFrameWnd )
ON_WM_PAINT()
END_MESSAGE_MAP()
class CHelloApp : public CWinApp {
public:
virtual BOOL InitInstance() {
m_pMainWnd = new CMainWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
};
CHelloApp HelloApp; // HelloApp's constructor initializes and runs the app
5.7.3 mfc\laskuri.cpp
Ilman kohtuutonta temppuilua ei ikkunasta saatu sellaista, joka piirtäisi ikoninsa automaattisesti ohjelman ollessa minimoituna.
apf\mfc\laskuri.cpp - MFC 4.0 -versio autolaskurista
/****************/
/* laskuri.cpp */
/****************/
// Esimerkki Autolaskurista. Projektiin laskuri.cpp ja laskuri.rc
#include <afxwin.h>
#include "laskuri.rh"
//------------------------------------------------------------------------------
class TLaskuriDialog : public CDialog {
public:
TLaskuriDialog(LPCSTR name,CWnd *parent=NULL) : CDialog(name,parent) {}
void BNHAClicked() { SetDlgItemInt(HAL,GetDlgItemInt(HAL)+1); }
void BNKAClicked() { SetDlgItemInt(KAL,GetDlgItemInt(KAL)+1); }
void BNNollaaClicked() { SetDlgItemInt(HAL,0); SetDlgItemInt(KAL,0); }
void BNExitClicked() { EndDialog(0); }
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(TLaskuriDialog, CDialog)
ON_COMMAND(HA, BNHAClicked)
ON_COMMAND(KA, BNKAClicked)
ON_COMMAND(NOLLAA,BNNollaaClicked)
ON_COMMAND(EXIT, BNExitClicked)
END_MESSAGE_MAP()
class TLaskuriApp : public CWinApp {
public:
virtual BOOL InitInstance() {
TLaskuriDialog dlg("LASKURI"); // Tyylinä WS_OVERLAPPED
m_pMainWnd = &dlg;
dlg.DoModal();
return FALSE; // Lopetetaan samalla koko ohjelma
}
};
TLaskuriApp LaskuriApp; // constructor initializes and runs the app
5.7.4 mfc\mopocad.cpp
Seuraavassa MopoCadin toteutuksesta Microsoftin Foundation Class -luokkakirjastoa käyttäen. Koska STL–kirjastoa ei yrityksistä huolimatta saatu toimimaan, on se korvattu "omatekoisella" luokalla, jossa on ulospäin samat toiminnot.
apf\mfc\mopocad.cpp - MFC -versio
// mopocad.cpp
// Mopocad for Visual C++ 4.0/vl-96
#include <afxwin.h>
#include "..\mopocad.rh"
//-----------------------------------------------------------------------------
// Koska STL:aa ei saatu yli 10 h kokeiluista huolimatta toimimaan,
// rakennetaan tilalle vastaavat toiminnot (jotka riittavat tahan esim)
// MFC:n omalla CArray -sailo-luokalla.
#include <afxtempl.h>
class cPoints { // Sailoluokka joka osaa tallettaa vain pisteitä
CArray<CPoint,CPoint> points;
public:
class cPIte { // Iteraattori, jolla sailoluokkaa voidaan kasitella
int i;
cPoints *p;
public:
friend cPoints;
cPIte() { i=0; p = NULL; }
cPIte(int ii,cPoints *ip) { i= ii; p = ip; }
CPoint &operator*() const { return p->points[i]; }
int operator!=(const cPIte &i2) const { return i != i2.i; }
cPIte &operator++() { i++; return *this; }
cPIte &operator++(int) { i++; return *this; }
}; // cPIte
friend cPoints::cPIte;
void push_back(const CPoint &pt) { points.Add(pt); }
cPIte begin() { return cPIte(0,this); }
cPIte end() { return cPIte(points.GetSize(),this);}
void erase(const cPIte &b, const cPIte &e) { points.RemoveAt(b.i,e.i-b.i); }
}; // cPoints
typedef cPoints::cPIte cPointsIterator;
//-----------------------------------------------------------------------------
CPoint MovePoint(-2000,-2000); // Merkki taulukkoon siirrosta
//-----------------------------------------------------------------------------
class CMainWindow : public CFrameWnd {
CClientDC *HoldDC;
BOOL ButtonDown;
cPoints Points;
protected:
void DeleteAllPoints();
void ClearWindow();
public:
CMainWindow();
~CMainWindow() { DeleteAllPoints(); }
virtual void Exit();
virtual void About();
void OnLButtonDown(UINT nFlags,CPoint point);
void OnMouseMove(UINT nFlags,CPoint point);
void OnLButtonUp(UINT nFlags,CPoint point);
void OnRButtonDown(UINT nFlags,CPoint point);
void OnPaint();
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP( CMainWindow, CFrameWnd )
ON_COMMAND(CM_FILE_EXIT, Exit)
ON_COMMAND(CM_HELP_ABOUT, About)
ON_WM_LBUTTONDOWN()
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_PAINT()
END_MESSAGE_MAP()
//-----------------------------------------------------------------------------
class CDLG_AboutDlg : public CDialog{ // Tässä tarpeeton, ks. CMainWin::About!
public:
CDLG_AboutDlg(LPCTSTR name,CWnd *Parent): CDialog(name,Parent) {}
virtual void Ok();
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP( CDLG_AboutDlg, CDialog )
ON_COMMAND(IDOK, Ok)
END_MESSAGE_MAP()
//-------------------- CDLG_AboutDlg metodit: ---------------------------------
void CDLG_AboutDlg::Ok()
{
EndDialog(0);
}
//-----------------------------------------------------------------------------
CMainWindow::CMainWindow()
{
Create(NULL,"MopoCad");
ButtonDown = FALSE;
HoldDC = NULL;
LoadAccelTable("PIKA");
CMenu menu;
menu.LoadMenu("PAAMENU");
SetMenu(&menu);
menu.Detach();
}
void CMainWindow::Exit()
{
DestroyWindow();
}
void CMainWindow::About()
{
// Tosin voitaisiin kutsua suoraan geneeristä CDialog ja jättää
// koko CDLG_AboutDlg luokka pois!
// CDialog dlg("DLG_ABOUT",this);
CDLG_AboutDlg dlg("DLG_ABOUT",this);
dlg.DoModal();
}
void CMainWindow::OnLButtonDown(UINT ,CPoint point)
{
if ( ButtonDown ) return;
ButtonDown = TRUE;
SetCapture();
HoldDC = new CClientDC(this);
HoldDC->MoveTo(point);
Points.push_back(MovePoint);
Points.push_back(point);
}
void CMainWindow::OnMouseMove(UINT ,CPoint point)
{
if ( !ButtonDown ) return;
HoldDC->LineTo(point);
Points.push_back(point);
}
void CMainWindow::OnLButtonUp(UINT ,CPoint)
{
if ( !ButtonDown ) return;
ReleaseCapture();
delete HoldDC;
HoldDC = NULL;
ButtonDown = FALSE;
}
void CMainWindow::OnRButtonDown(UINT ,CPoint)
{
if ( ButtonDown ) return;
ClearWindow();
}
void CMainWindow::OnPaint() {
CPaintDC hdc(this);
cPointsIterator pi;
for (pi=Points.begin(); pi != Points.end() ; pi++)
if ( *pi == MovePoint ) hdc.MoveTo(*++pi);
else hdc.LineTo(*pi);
}
void CMainWindow::DeleteAllPoints()
{
Points.erase(Points.begin(),Points.end());
}
void CMainWindow::ClearWindow()
{
DeleteAllPoints();
Invalidate(TRUE);
}
//-----------------------------------------------------------------------------
class CMopoCadApp : public CWinApp {
public:
virtual BOOL InitInstance() {
m_pMainWnd = new CMainWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
m_pMainWnd->SetIcon(LoadIcon("IKONI"),TRUE);
return TRUE;
}
};
CMopoCadApp MopoCadApp; // constructor initializes and runs the app
6. Sanasto
Yläindeksillä C on merkitty niitä sanoja, jotka liittyvät lähinnä vain C++ –kieleen.
abstract class abstrakti luokka
abstract data type abstrakti tietotyyppi
aggregation koostaminen
aliasing moninimisyys
attribute attribuutti, jäsenmuuttuja
base classC kantaluokka
child class aliluokka, lapsiluokka, jälkeläinen
class luokka
class template luokkamalli
constant vakio
constructor konstruktori, rakentaja, muodostaja, alustaja
container säiliö, säilö, varasto
copy constructor kopiointikonstruktori,
kopioija, kopiointimuodostaja
data memberC jäsenmuuttuja, tietokenttä, attribuutti
declaration esittely
default constructor oletuskonstruktori
default value oletusarvo
definition määrittely, (≈alustus)
derived classC johdettu luokka, aliluokka
destructor destruktori, hävittäjä, hajottaja, lopetusoperaatio
early binding aikainen sidonta
encapsulation kapselointi, kotelointi
exeption poikkeus
friendC ystävä
function templateC funktiomalli, –muotti
header fileC header–tiedosto, otsikkotiedosto
inheritance periytyminen
inline function inline–funktio
instance instanssi, ilmentymä, esiintymä
interface rajapinta, liittymä
late binding myöhäinen sidonta
member functionC jäsenfunktio, metodi
method metodi, jäsenfunktio
multiple inheritance moniperintä
object olio
overloading lisämäärittely, ylimäärittely, kuormitus (huono: ylikuormitus)
overriding korvaus, uudelleen määrittely, syrjäyttäminen
parent class yliluokka, isäluokka, vanhempi
pointer osoitin
polymorphism monimuotoisuus
private yksityinen
protected suojattu
prototype prototyyppi
public julkinen
pure virtualC puhdas virtuaalifunktio, avoin virtuaalioperaatio
reference viittaus, viite
scope näkyvyysalue
single inheritance yksiperintä, yksittäisperintä
static member– luokka(kohtainen) muuttuja/metodi
structureC tietue
subclass aliluokka, lapsiluokka, jälkeläinen
superclass yliluokka, isäluokka, vanhempi
template class generoitu luokka
template function generoitu funktio
type conversion tyyppimuunnos
virtual virtuaalinen, (väistyvä)
Ks. http://www.cs.helsinki.fi/~laine/oliosanasto
7. Kirjallisuutta
Bjarne Stroustrup: The C++ Programming Language, Second Edition - Addison-Wesley, 1991
Timothy A. Budd: An Introduction to Object-Oriented Programming, Second Edition - Addison Wesley, 1997
Päivi Hietanen: C++ ja olio–ohjelmointi - Teknolit, 1996
Jesse Liberty: Opeta itsellesi C++ , Suomen atk-kustannus Jyväskylä Gummerus, 1995
Bjarne Stroustrup: The Desing and Evolution of C++ - Addison-Wesley, 1994
Bjarne Stroustrup, Margaret A. Ellis: The Annotated C++ Reference Manual - Addison-Wesley, 1991
Herbert Schield: Teach Yourself C++ - Osborne McGraw-Hill, 1992
Paul DiLascia: Windows++ - Addison-Wesley, 1992
Borland: Borland C++ 5.0: Programmers guide - Borland International Inc, 1996
James O. Coplien: Advanced C++: Programming Styles and Idioms - Addison Wesley, 1992
Saumyendra Sengupta, Carl Phillip Korobkin: C++ Object-Oriented Data Structures - Springer-Verlag, 1994
Microsoft: Microsoft Visual C++: C++ Tutorial, Class Library User's Guide, Programming Techniques - Microsoft Corporation, 1993
Microsoft: Microsoft Visual C++: Reference Volume I Class Library Reference for the Microsoft Foundation Class Library - Microsoft Corporation, 1993
Borland: Borland C++ 3.1: Programmers guide - Borland International Inc, 1992
Stephen Prata: C++ Ohjelmointi - Pagina International AB, 1992
Muista myös helpit!
WWW: http://www.math.jyu.fi/~vesal/kurssit/winohj/linkkeja
http://www.hut.fi/~jkorpela/Cinfo.html
http://www.cerfnet.com/~mpcline/C++-FAQs-Lite/
News-ryhmät: comp.std.c++, comp.lang.c++.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.