Virtuaalifunktiot ja polymorfismi



Samankaltaiset tiedostot
Periytyminen. Luokat ja olio-ohjelmointi

Luokat. Luokat ja olio-ohjelmointi

T Olio-ohjelmointi Osa 5: Periytyminen ja polymorfismi Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

Osoitin ja viittaus C++:ssa

Ohjelman virheet ja poikkeusten käsittely

Operaattoreiden uudelleenmäärittely

815338A Ohjelmointikielten periaatteet Harjoitus 5 Vastaukset

815338A Ohjelmointikielten periaatteet Harjoitus 3 vastaukset

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

C++11 Syntaksi. Jari-Pekka Voutilainen Jari-Pekka Voutilainen: C++11 Syntaksi

Luokassa määriteltävät jäsenet ovat pääasiassa tietojäseniä tai aliohjelmajäseniä. Luokan määrittelyyn liittyvät varatut sanat:

Mallit standardi mallikirjasto parametroitu tyyppi

Luokan operaatiot. Osoittimet ja viittaukset luokan olioihin

12 Mallit (Templates)

Ohjelmoinnin jatkokurssi, kurssikoe

Harjoitus Olkoon olemassa luokat Lintu ja Pelikaani seuraavasti:

Esimerkkiprojekti. Mallivastauksen löydät Wroxin www-sivuilta. Kenttä Tyyppi Max.pituus Rajoitukset/Kommentit

Olio-ohjelmoinnissa luokat voidaan järjestää siten, että ne pystyvät jakamaan yhteisiä tietoja ja aliohjelmia.

Taulukot. Jukka Harju, Jukka Juslin

T Olio-ohjelmointi Osa 3: Luokka, muodostin ja hajotin, this-osoitin Jukka Jauhiainen OAMK Tekniikan yksikkö 2010

12. Monimuotoisuus 12.1

812347A Olio-ohjelmointi, 2015 syksy 2. vsk. X Poikkeusten käsittelystä

Omat tietotyypit. Mikä on olio?

12. Monimuotoisuus 12.1

9. Periytyminen Javassa 9.1

Metodit. Metodien määrittely. Metodin parametrit ja paluuarvo. Metodien suorittaminen eli kutsuminen. Metodien kuormittaminen

Geneeriset luokat. C++ - perusteet Java-osaajille luento 6/7: Template, tyyppi-informaatio, nimiavaruudet. Geneerisen luokan käyttö.

13 Operaattoreiden ylimäärittelyjä

Tehtävä 1. TL5302 Olio-ohjelmointi Koe Malliratkaisuja. Tässä sekä a)- että b)-kohdan toimiva ratkaisu:

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

Olio-ohjelmointi Javalla

Kääntäjän virheilmoituksia

ITKP102 Ohjelmointi 1 (6 op)

15. Ohjelmoinnin tekniikkaa 15.1

Sisällys. Metodien kuormittaminen. Luokkametodit ja -attribuutit. Rakentajat. Metodien ja muun luokan sisällön järjestäminen. 6.2

Oliosuunnitteluesimerkki: Yrityksen palkanlaskentajärjestelmä

11. oppitunti III. Viittaukset. Osa. Mikä on viittaus?

Osa III. Olioiden luominen vapaalle muistialueelle

Olion elinikä. Olion luominen. Olion tuhoutuminen. Olion tuhoutuminen. Kissa rontti = null; rontti = new Kissa();

Sisällys. JAVA-OHJELMOINTI Osa 6: Periytyminen ja näkyvyys. Luokkahierarkia. Periytyminen (inheritance)

4. Luokan testaus ja käyttö olion kautta 4.1

Ohjelmassa muuttujalla on nimi ja arvo. Kääntäjä ja linkkeri varaavat muistilohkon, jonne muuttujan arvo talletetaan.

C-kielessä taulukko on joukko peräkkäisiä muistipaikkoja, jotka kaikki pystyvät tallettamaan samaa tyyppiä olevaa tietoa.

Osoittimet. Mikä on osoitin?

Sisällys. 1. Omat operaatiot. Yleistä operaatioista. Yleistä operaatioista

Ohjelmoinnin perusteet Y Python

Osa III. Edelliset kolme lukua ovat käsitelleet viittausten ja osoittimien käyttöä. Tämän luvun aiheita ovat:

1. Omat operaatiot 1.1

Olio-ohjelmointi Syntaksikokoelma

Opintojakso TT00AA11 Ohjelmoinnin jatko (Java): 3 op Taulukot & Periytyminen

Tutoriaaliläsnäoloista

Rajapinta (interface)

C++ rautaisannos. Kolme tapaa sanoa, että tulostukseen käytetään standardikirjaston iostreamosassa määriteltyä, nimiavaruuden std oliota cout:

Olio-ohjelmointi 2. välikoe HYV5SN

Rekursiolause. Laskennan teorian opintopiiri. Sebastian Björkqvist. 23. helmikuuta Tiivistelmä

Java kahdessa tunnissa. Jyry Suvilehto

Ohjelmointi 2. Jussi Pohjolainen. TAMK» Tieto- ja viestintäteknologia , Jussi Pohjolainen TAMPEREEN AMMATTIKORKEAKOULU

Ohjelmointikieli TIE Principles of Programming Languages Syksy 2017 Ryhmä 19

on ohjelmoijan itse tekemä tietotyyppi, joka kuvaa käsitettä

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

Aalto Yliopisto T Informaatioverkostot: Studio 1. Oliot ja luokat Javaohjelmoinnissa

9. Periytyminen Javassa 9.1

15. Ohjelmoinnin tekniikkaa 15.1

Solidity älysopimus ohjelmointi. Sopimus suuntautunut ohjelmointi

Harjoitustyö: virtuaalikone

Sisällys. 9. Periytyminen Javassa. Periytymismekanismi Java-kielessä. Periytymismekanismi Java-kielessä

ITKP102 Ohjelmointi 1 (6 op)

Sisällys. 6. Metodit. Oliot viestivät metodeja kutsuen. Oliot viestivät metodeja kutsuen

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä ja ulkopuolelta. Attribuuttien arvojen käsittely aksessoreilla. 4.2

Sisällys. Yleistä attribuuteista. Näkyvyys luokan sisällä. Tiedonkätkentä. Aksessorit. 4.2

Metodien tekeminen Javalla

C# olio-ohjelmointi perusopas

1 Tehtävän kuvaus ja analysointi

Ohjelmoinnin perusteet, kurssikoe

Osa. Listaus 2.1. HELLO.CPP esittelee C++ -ohjelman osat. 14: #include <iostream.h> 15: 16: int main() 17: {

A) on käytännöllinen ohjelmointitekniikka. = laajennetaan aikaisemmin tehtyjä luokkia (uudelleenkäytettävyys)

Alkuarvot ja tyyppimuunnokset (1/5) Alkuarvot ja tyyppimuunnokset (2/5) Alkuarvot ja tyyppimuunnokset (3/5)

C++11 lambdat: [](){} Matti Rintala

JAVA-PERUSTEET. JAVA-OHJELMOINTI 3op A JAVAN PERUSTEET LYHYT KERTAUS JAVAN OMINAISUUKSISTA JAVAN OMINAISUUKSIA. Java vs. C++?

Ohjelmointi 1 Taulukot ja merkkijonot

815338A Ohjelmointikielten periaatteet

Ohjelmointi funktioiden avulla

TIE Ohjelmistojen suunnittelu

Ohjelmointitaito (ict1td002, 12 op) Kevät Java-ohjelmoinnin alkeita. Tietokoneohjelma. Raine Kauppinen

TIE Ohjelmistojen suunnittelu

AS C-ohjelmoinnin peruskurssi 2013: C-kieli käytännössä ja erot Pythoniin

2. Olio-ohjelmoinista lyhyesti 2.1

Tietueet. Tietueiden määrittely

Tässä tehtävässä käsittelet metodeja, listoja sekä alkulukuja (englanniksi prime ).

7. Oliot ja viitteet 7.1

Ohjelmointi 2 / 2010 Välikoe / 26.3

Loppukurssin järjestelyt

Sisällys. 7. Oliot ja viitteet. Olion luominen. Olio Java-kielessä

Oliot viestivät metodeja kutsuen

1. Miten tehdään peliin toinen maila?

Ohjelmoinnin perusteet, syksy 2006

Loppukurssin järjestelyt C:n edistyneet piirteet

Ohjelmassa henkilön etunimi ja sukunimi luetaan kahteen muuttujaan seuraavasti:

IDL - proseduurit. ATK tähtitieteessä. IDL - proseduurit

ATK tähtitieteessä. Osa 3 - IDL proseduurit ja rakenteet. 18. syyskuuta 2014

Transkriptio:

Virtuaalifunktiot ja polymorfismi 16 Virtuaalifunktiot ja polymorfismi Polymorfismi on niin tehokas olio-ohjelmoinnin ominaisuus, että tulet varmastikin käyttämään sitä lähes kaikissa C++-ohjelmissasi. Polymorfismi vaatii periytettyjä luokkia, ja tämän luvun sisältö nojaa vahvasti edellisessä luvussa käsiteltyihin periytymisen perusteisiin. Tämän luvun aiheita ovat: Mikä polymorfismi on ja miten luokat saadaan hyödyntämään polymorfismia Mikä on virtuaalifunktio Milloin ja miksi luokkahierarkiassa tarvitaan virtuaalisia tuhoajafunktioita Miten virtuaalifunktioiden oletusparametrejä käytetään Mikä on aito virtuaalifunktio ja miten funktio määritellään sellaiseksi Mikä on abstrakti luokka Miten luokan tyyppejä voidaan muuntaa luokkahierarkiassa Miten olioon osoittavan osoittimen tyyppi saadaan selville suoritusaikana Mitä ovat osoittimet jäseniin ja miten niitä käytetään Polymorfismin perusteet Kuten tulemme huomaamaan, polymorfismiin kuuluu aina olioon osoittavan osoittimen tai viittauksen käyttö kutsuttaessa funktiota. Lisäksi polymorfismi toimii ainoastaan luokkahierarkiassa, joten mahdollisuus periyttää luokka toisesta luokasta on polymorfismin perustana. Mitä polymorfismilla sitten saadaan aikaan? Saamme karkean kuvan sen toiminnasta tutkimalla esimerkkiä, jossa on lisää laatikoita, mutta ensin meidän tulee ymmärtää, mikä merkitys on osoittimella kantaluokkaan. 621

C++ Ohjelmoijan käsikirja Osoitin kantaluokkaan Eräs asia, jota käsittelimme edellisessä luvussa, oli se, että periytetyn luokan olio on kantaluokan olioiden alijoukko - toisin sanoen, jokainen periytetty luokka on myös kantaluokka. Eli voit aina käyttää osoitinta kantaluokkaan tallettamaan periytetyn luokan olion osoitteen - voit itse asiassa käyttää jopa osoitinta epäsuoraan kantaluokkaan tähän tarkoitukseen. Alla olevassa kaaviossa PahviLaatikko-luokka on periytetty Laatikko-kantaluokasta ja KauraLtk-luokka on moniperitty Laatikko- ja Sisalto-luokista. Kaavio havainnollistaa, miten osoittimia kantaluokkiin voidaan käyttää periytettyjen olioiden osoitteiden tallettamiseen tällaisessa luokkahierarkiassa. Class Laatikko KauraLtk aamiainen; // Voit tallettaa aammiainen- // olion osoitteen mihin tahansa // kantaosoittimeen: Class PahviLaatikko: public Class Sisalto PahviLaatikko* ppahviltk = &aamiainen; Laatikko* pltk = &aamiainen; Sisalto* psisalto = &aamiainen; Class KauraLtk: public Carton, public Contents Vastakkaiseen suuntaan tämä ei onnistu. Esimerkiksi et voi käyttää KauraLtk*-tyyppistä osoitinta tallettamaan (suoran tai epäsuoran) kantaluokan osoitetta. Tämä on loogista, koska kantaluokka ei kuvaa täydellistä periytetyn luokan oliota. Periytetyn luokan olio sisältää aina jokaisen kantaluokkansa alijoukon, mutta kukin kantaluokka kuvaa vain osaa periytetyn luokan oliosta. Katsotaan esimerkkiä. Oletetaan, että periytämme kaksi luokkaa edellisen luvun mukaisesta Laatikko-luokasta. Tarkoituksenamme on kuvata niillä erilaisia säiliöitä. PahviLaatikkoluokan määrittely on muotoa: class PahviLaatikko: public Laatikko // Luokan yksityiskohdat... Uuden VahvaLtk-luokan määrittely on samantyyppinen: class VahvaLtk: public Laatikko // Luokan yksityiskohdat... 622

Virtuaalifunktiot ja polymorfismi Oletetaan nyt, että jokaisen periytetyn tyypin tilavuus lasketaan eri tavalla. Pahvista tehdyn PahviLaatikko-luokan kohdalla tilavuutta täytyy vähentää vain vähän, koska materiaalin paksuus on varsin pieni. VahvaLtk:n kohdalla tilavuutta saattaa tarvita vähentää enemmän, koska kyseessä on vahva ja suojaava rakenne. Edellä olleiden luokkien määrittelyjen pohjalta voimme esitellä ja alustaa osoittimen seuraavasti: PahviLaatikko apahviltk(10.0, 10.0, 5.0); Laatikko* pltk = &apahviltk; Osoitin pltk, joka on tyyppiä osoitin Laatikko-olioon, alustetaan apahviltk:n (joka on tyyppiä PahviLaatikko) osoitteella. Tämä on mahdollista, koska PahviLaatikko on periytetty Laatikkoluokasta ja sisältää täten Laatikko-tyyppisen aliolion. Voimme käyttää samaa osoitinta tallettamaan myös VahvaLtk-olion osoitteen, koska VahvaLtk-luokka on myöskin periytetty Laatikko-luokasta: VahvaLtk kassakaappi(12.0, 8.0, 4.0); pltk = &kassakaappi; Osoitin pltk voi tiettynä hetkenä sisältää minkä tahansa olion osoitteen, jonka kantaluokkana on Laatikko. Osoittimen tyyppi on määrittelyhetkellä staattinen tyyppi - pltk:n staattinen tyyppi on osoitin Laatikko-olioon. Koska pltk on osoitin kantaluokkaan, sillä on myös dynaaminen tyyppi, joka vaihtelee sen mukaan, mihin osoitin osoittaa. Kun pltk osoittaa PahviLaatikkoolioon, sen dynaaminen tyyppi on osoitin PahviLaatikko-luokkaan. Kun pltk osoittaa VahvaLtkolioon, sen dynaaminen tyyppi on osoitin VahvaLtk-luokkaan. Kun pltk osoittaa Laatikkotyyppiseen olioon, sen dynaaminen tyyppi on sama kuin sen staattinen tyyppi. Juuri tässä on polymorfismin taika. Käsittelemme hetken kuluttua ehtoja, joiden voimassa ollessa voit käyttää osoitinta pltk kutsuessasi kantaluokassa tai missä tahansa periytetyissä luokissa määriteltyä funktiota. Funktio valitaan tällöin pltk:n dynaamisen tyypin mukaan. Tarkastellaan seuraavaa lausetta: pltk->tilavuus(); Jos pltk sisältää PahviLaatikko-olion osoitteen, voimme käyttää tätä lausetta kutsuessamme PahviLaatikko-olion tilavuus()-funktiota. Jos pltk osoittaa VahvaLtk-olioon, lause kutsuu VahvaLtk-olion tilavuus()-funktiota. Tämä toimii samalla tavalla muidenkin Laatikko-luokasta periytettyjen luokkien kohdalla. Lause pltk->tilavuus(); voi saada aikaan eri toiminnan, riippuen siitä, mihin pltk osoittaa. Tai mikä tärkeämpää, suoritusaikana valitaan oliolle, johon pltk osoittaa, sopiva toiminta. (Ikään kuin osoittimella olisi sisäänrakennettu switch-lause, joka testaa tyypin ja valitsee kutsuttavan funktion sen mukaan.) Tämä on erittäin tehokas mekanismi. Tulet törmäämään useasti tilanteisiin, joissa käsiteltävänä olevan olion tyyppiä ei voida ratkaista etukäteen - eli tyyppiä ei voida ratkaista suunnitteluhetkellä eikä käännöshetkellä, ainoastaan suoritusaikana. Tällaiset tilanteet voidaan ratkaista helposti polymorfismin avulla. Sitä käytetään runsaasti käyttäjän kanssa vuorovaikutuksessa olevissa ohjelmissa, joissa käyttäjä voi syöttää minkä tahansa tyyppisiä syötteitä. 623

C++ Ohjelmoijan käsikirja Esimerkiksi grafiikkasovelluksessa, jossa voidaan piirtää erimuotoisia kuvioita - ympyröitä, viivoja, käyriä jne - voidaan määritellä jokaiselle kuviotyypille oma luokka, joka on periytetty yhteisestä kantaluokasta Kuvio. Ohjelma tallettaa käyttäjän piirtämän kuvion osoitteen kantaluokan osoittimeen pkuvio, joka on tyyppiä osoitin Kuvio-olioon, ja piirtää sopivan kuvion esimerkiksi lauseella pkuvio->piirra();. Tämä kutsuu osoittimen osoittaman kuvion piirra()- funktiota. Eli tämä yksi lause pystyy piirtämään minkä tahansa kuvion. Jotta tämä toimii, on tärkeää, että kutsuttava funktio on kantaluokan jäsen. Katsotaan seuraavaksi tarkemmin perittyjen funktioiden toimintaa. Perittyjen funktioiden kutsuminen Ennen kuin siirrymme tarkemmin polymorfismin saloihin, tulee meidän tarkastella tarkemmin perittyjen funktioiden toimintaa ja miten ne ovat suhteessa sen luokan funktioihin, josta ne on periytetty. Muutamme Laatikko-luokkaa siten, että lisäämme siihen funktion, joka laskee Laatikko-olion tilavuuden sekä toisen funktion, joka tulostaa tilavuuden. Luokkien uudet tiedostot Laatikko.h ja Laatikko.cpp ovat seuraavassa: // Laatikko.h #ifndef LAATIKKO_H #define LAATIKKO_H class Laatikko public: Laatikko(double pitarvo = 1.0, double syvarvo = 1.0, double korkarvo = 1.0); // Funktio, joka näyttää olion tilavuuden void naytatilavuus() const; // Funktio, joka laskee Laatikko-olion tilavuuden double tilavuus() const; protected: double pituus; double syvyys; double korkeus; ; #endif // Laatikko.cpp #include <iostream> #include "Laatikko.h" using namespace std; 624 Laatikko::Laatikko(double parvo, double sarvo, double karvo) : pituus(parvo), syvyys(sarvo), korkeus(karvo) void Laatikko::naytaTilavuus() const

Virtuaalifunktiot ja polymorfismi cout << "Laatikon käytettävissä oleva tilavuus on " << tilavuus() << endl; double Laatikko::tilavuus() const return pituus * syvyys * korkeus; Emme enää tarvitse käyttäjän määrittelemää tuhoajafunktiota emmekä tulostuslausetta muodostinfunktiossa, joten ne on poistettu. Tällaisen Laatikko-luokan avulla voimme tulostaa Laatikko-olion käytettävissä olevan tilavuuden kutsumalla kyseisen olion naytatilavuus()- funktiota. Käytämme samoja jäsenmuuttujia (pituus, syvyys ja korkeus), jotka on määritelty suojatuiksi, joten niihin ei päästä käsiksi luokasta periytettyjen luokkien jäsenmuuttujista. Määrittelemme myöskin VahvaLtk-luokan, jonka kantaluokkana on Laatikko. VahvaLtk-olion materiaali suojaa laatikon sisältöä, joten sen käytettävissä oleva tilavuus on vain 85% tavallisen Laatikko-olion tilavuudesta. Tästä syystä tarvitsemme toisen version tilavuus()-funktiosta, joka ottaa tämän huomioon: // VahvaLtk.h #ifndef VAHVALTK_H #define VAHVALTK_H #include "Laatikko.h" class VahvaLtk : public Laatikko // Periytetty luokka public: // Muodostinfunktio VahvaLtk(double pituusarvo, double syvyysarvo, double korkeusarvo); ; // Funktio, joka laskee VahvaLtk:n tilavuuden (15% menee laatikolle) double tilavuus() const; #endif // VahvaLtk.cpp #include "VahvaLtk.h" VahvaLtk::VahvaLtk(double parv, double sarv, double karv) : Laatikko(pArv,sArv,kArv) double VahvaLtk::tilavuus() const return 0.85 * pituus * syvyys * korkeus; Periytetyssä luokassa olisi tietysti voinut olla muitakin jäseniä, mutta haluamme pitää sen nyt yksinkertaisena, keskittyen vain perittyjen funktioiden toimintaan. Periytetyn luokan muodostinfunktio kutsuu alkuarvolistassaan yksinkertaisesti kantaluokan muodostinfunktiota, joka asettaa jäsenmuuttujien arvot. Periytetyn luokan muodostinfunktiossa ei tarvita yhtään lausetta. Määrittelemme myöskin uuden version tilavuus()-funktiosta, joka korvaa kantaluokasta perityn version. Ideana tässä on, että saamme perityn naytatilavuus()-funktion kutsumaan periytetyn luokan versiota tilavuus()-funktiosta, kun kutsumme sitä VahvaLtk-luokan oliosta. Katsotaan, kuinka se toimii. 625

C++ Ohjelmoijan käsikirja Kokeile itse - Periytetyn luokan käyttö Voimme testata uutta periytettyä luokkaamme yksinkertaisesti luomalla Laatikko-olion ja VahvaLtk-olion, joiden mitat ovat samat, ja vertaamalla, onko niiden lasketut tilavuudet samat. Tämän tekevä main()-funktio näyttää seuraavalta: // Esimerkki 16.1 Perittyjen funktioiden toiminta periytetyssä luokassa #include <iostream> #include "Laatikko.h" // Laatikko-luokka #include "VahvaLtk.h" // VahvaLtk-luokka using namespace std; int main() Laatikko omaltk(20.0, 30.0, 40.0); // Esitellään kantaluokka VahvaLtk vahval(20.0, 30.0, 40.0); // Esitellään periytetty laatikko - sama koko cout << endl; omaltk.naytatilavuus(); // Näytetään kantaluokan laatikon tilavuus vahval.naytatilavuus(); // Näytetään periytetyn luokan laatikon tilavuus return 0; Kun suoritan ohjelman, se tulostaa: Laatikon käytettävissä oleva tilavuus on 24000 Laatikon käytettävissä oleva tilavuus on 24000 Kuinka se toimii Periytetyn luokan olion tilavuuden pitäisi olla pienempi kuin kantaluokan olion, joten ohjelmamme ei toimi niin kuin pitäisi. Yritetään saada selville, missä menee väärin. Funktion naytatilavuus() toinen kutsu on periytetyn luokan oliolle VahvaLtk, mutta selvästikään tätä ei oteta huomioon. VahvaLtk-olion tilavuuden tulisi olla 85% tavallisen samankokoisen Laatikkoolion tilavuudesta. Ongelmana on se, että kun tilavuus()-funktiota kutsutaan naytatilavuus()-funktiosta, kääntäjä kutsuu aina kantaluokan tilavuus()-funktiota. Sillä ei ole vaikutusta, miten naytatilavuus()- funktiota kutsutaan, se ei koskaan kutsu VahvaLtk-luokan versiota tilavuus()-funktiosta. Kun funktioiden kutsut ovat tällä tavalla kiinteät jo ennen ohjelman suoritusta, funktion kutsua kutsutaan staattiseksi sidonnaksi. Termiä aikainen sidonta käytetään myös usein. Tässä esimerkissä tietty tilavuus()-funktio sidotaan funktioon naytatilavuus() ohjelman suorituksen aikana. Joka kerta, kun naytatilavuus()-funktiota kutsutaan, kutsutaan kantaluokan tilavuus()-funktiota, joka siihen on sidottu. 626

Virtuaalifunktiot ja polymorfismi! Samanlainen sidonta tapahtuu myös periytetyssä luokassa VahvaLtk, jos asetamme ehdot oikein. Eli jos lisäämme naytatilavuus()-funktion (joka kutsuu tilavuus()- funktiota) VahvaLtk-luokkaan, tilavuus()-funktion kutsu sidottaisiin staattisesti periytetyn luokan funktioon. Entäpä jos kutsummekin tilavuus()-funktiota suoraan VahvaLtk-oliosta? Lisätään lauseet, jotka kutsuvat tilavuus()-funktiota suoraan VahvaLtk-oliosta kantaluokan osoittimen avulla: cout << vahval:n tilavuus on << vahval.tilavuus() << endl; Laatikko *pltk = &vahval; cout << vahval:n tilavuus pltk:n kautta on << pltk->tilavuus() << endl; Lisää nämä lauseet main()-funktioon juuri ennen return-lausetta. Kun suoritat ohjelman, saat tulostuksena: Laatikon käytettävissä oleva tilavuus on 24000 Laatikon käytettävissä oleva tilavuus on 24000 vahval:n tilavuus on 20400 vahval:n tilavuus pltk:n kautta on 24000 Tämä on varsin informatiivinen. Kuten huomaamme, periytetyn luokan tilavuus()-funktion kutsu kutsuu periytetyn luokan tilavuus()-funktiota, mitä juuri halusimmekin. Kutsu kantaluokan osoittimen pltk avulla kutsuu kuitenkin kantaluokan versiota tilavuus()-funktiosta, vaikka pltk sisältää vahval:n osoitteen. Toisin sanoen molemmat kutsut sidotaan staattisesti. Kääntäjä toteuttaa nämä kutsut seuraavasti: cout << vahval:n tilavuus on << vahval.vahvaltk::tilavuus() << endl; Laatikko *pltk = &vahval; cout << vahval:n tilavuus pltk:n kautta on << pltk->laatikko::tilavuus() << endl; Funktion staattinen kutsu osoittimen avulla ratkaistaan pelkästään osoittimen tyypin perusteella eikä osoitetun olion perusteella. Osoitin pltk on tyyppiä osoitin Laatikko-olioon, joten kaikki staattiset kutsut pltk:n avulla kutsuvat Laatikko-luokan jäsenfunktiota. Kaikki kantaluokan staattisesti sidotun osoittimen avulla kutsutut funktiot kutsuvat kantaluokan funktiota. Haluamme esimerkkimme avulla selvittää, mitä tilavuus()-funktiota missäkin tilanteessa käytetään. Eli, jos kutsumme naytatilavuus()-funktiota periytetyn luokan oliosta, haluamme, että kutsutaan periytetyn luokan tilavuus()-funktiota, ei kantaluokan versiota. Vastaavasti, jos kutsumme tilavuus()-funktiota kantaluokan osoittimen avulla, haluamme, että kutsutaan osoitetulle oliolle sopivaa funktiota. Tällaista kutsutaan dynaamiseksi sidonnaksi tai myöhäiseksi sidonnaksi. Ohjelma ei kuitenkaan vielä toimi haluamallamme tavalla, koska meidän täytyy kertoa kääntäjälle, että tilavuus()-funktio Laatikko-luokassa ja siitä periytetyissä luokissa on erikoinen. Haluamme, että kutsut ratkaistaan dynaamisesti. Nyt meidän tulee määritellä tilavuus()- funktio virtuaalifunktioksi. 627

C++ Ohjelmoijan käsikirja Virtuaalifunktiot Kun esittelet funktion kantaluokassa virtuaaliseksi, kerrot kääntäjälle, että haluat funktion sidottavan dynaamisesti kaikista kyseisestä kantaluokasta periytetyissä luokissa. Virtuaalifunktio esitellään kantaluokassa virtual-avainsanalla. class Laatikko public: virtual double tilavuus() const... ; class PahviLaatikko: public Laatikko public: virtual double tilavuus() const... ; class VahvaLtk: public Laatikko public: virtual double tilavuus() const... ; class MuuLtk: public Laatikko public: virtual double tilavuus() const... ; PLtk on tyyppiä osoitin Laatikko-olioon. Kutsuttava tilavuus() -funktion versio riippuu sen olion tyypistä, johon pltk osoittaa double tulos = pltk->tilavuus(); Funktio, jonka esittelet virtuaaliseksi kantaluokassa, on virtuaalinen kaikissa kantaluokasta periytetyissä (joko suorissa tai epäsuorissa) luokissa. Jotta polymorfisminen toiminta saadaan aikaan, jokainen periytetty luokka voi toteuttaa oman virtuaalifunktion (vaikka se ei pakollista olekaan; palaamme tähän kohta). Virtuaalifunktion kutsut voidaan suorittaa muuttujan, joka on osoitin tai viittaus kantaluokan olioon, avulla. Yllä oleva kaavio havainnollistaa, miten virtuaalifunktion kutsu osoittimen avulla ratkaistaan dynaamisesti. Osoitinta kantaluokkaan käytetään tallettamaan jonkin periytetyn luokan olion osoite. Se voi osoittaa mihin tahansa kolmesta periytetystä luokasta tai luonnollisestikin kantaluokan olioon. Mitä tilavuus()- funktiota milloinkin kutsutaan, riippuu osoittimen kutsuhetkellä osoittaman olion tyypistä. Luokan määrittely polymorfistiseksi tarkoittaa, että se on periytetty luokka, jossa on vähintään yksi virtuaalifunktio. Ennen kuin siirrymme eteenpäin, huomaa, että virtuaalifunktion kutsu olion avulla ratkaistaan aina staattisesti. Virtuaalifunktion kutsu ratkaistaan dynaamisesti vain silloin, kun kutsu tapahtuu osoittimen tai viittauksen avulla. Nyt voimme testata virtuaalifunktioita. 628

Virtuaalifunktiot ja polymorfismi Kokeile itse - Virtuaalifunktioiden käyttö Meidän tulee muuttaa edellisen esimerkkimme Laatikko-luokkaa hieman, jotta saamme esimerkin toimimaan haluamallamme tavalla. Meidän tulee lisätä virtual-avainsana tilavuus()-funktion esittelyyn: class Laatikko public: Laatikko(double pitarvo = 1.0, double syvarvo = 1.0, double korkarvo = 1.0); // Funktio, joka näyttää olion tilavuuden void naytatilavuus() const; // Funktio, joka laskee Laatikko-olion tilavuuden virtual double tilavuus() const; protected: double pituus; double syvyys; double korkeus; ; Huomaa, että sinun ei tarvitse lisätä virtual-avainsanaa funktion määrittelyyn. Itse asiassa olisi virhe tehdä niin. Jotta saamme esimerkin vielä kiinnostavammaksi, toteutamme tilavuus()-funktion uuteen PahviLaatikko-luokkaan hieman eri tavalla: // Pahvil.h #ifndef PAHVILAATIKKO_H #define PAHVILAATIKKO_H #include <string> #include "Laatikko.h" using std::string; class PahviLaatikko : public Laatikko public: // Muodostinfunktio, joka kutsuu kantaluokan muodostinfunktiota PahviLaatikko(double pa, double sa, double ka, string materiaali = "Pahvi"); // Kopiomuodostin PahviLaatikko(const PahviLaatikko& apahviltk); // Tuhoajafunktio ~PahviLaatikko(); // Funktio, joka laskee PahviLaatikko-olion tilavuuden double tilavuus() const; 629

C++ Ohjelmoijan käsikirja private: string* pmateriaali; ; #endif // Pahvil.cpp #include "Pahvil.h" PahviLaatikko::PahviLaatikko(double pa, double sa, double ka, string materiaali) : Laatikko(pa, sa, ka) pmateriaali = new string(materiaali); PahviLaatikko::PahviLaatikko(const PahviLaatikko& apahviltk) pituus = apahviltk.pituus; syvyys = apahviltk.syvyys; korkeus = apahviltk.korkeus; pmateriaali = new string(*apahviltk.pmateriaali); PahviLaatikko::~PahviLaatikko() delete pmateriaali; double PahviLaatikko::tilavuus() const double til = (pituus - 0.5) * (syvyys - 0.5) * (korkeus - 0.5); return til > 0.0? til : 0.0; PahviLaatikko-luokan tilavuus()-funktio olettaa, että materiaalin paksuus on 0.25, joten 0.5 vähennetään jokaisesta mitasta. Jos PahviLaatikko-olio on jostain syystä luotu siten, että jokin mitta on pienempi kuin 0.5, tilavuudeksi lasketaan 0. Käytämme myöskin esimerkissä 16.1 määriteltyä VahvaLtk-luokkaa. Voimme nyt muuttaa edellisen esimerkin main()-funktiota ottamalla PahviLaatikko-olion käsittelyyn mukaan. Kutsumme myöskin naytatilavuus()-funktiota pltk-osoittimen avulla: // Laatikko-luokka // VahvaLtk-luokka // PahviLaatikko- // Esimerkki 16.2 Virtuaalifunktioiden käyttö #include <iostream> #include "Laatikko.h" #include "VahvaLtk.h" #include "Pahvil.h" luokka using namespace std; 630 int main() Laatikko omaltk(20.0, 30.0, 40.0); // Esitellään kantalaatikko VahvaLtk vahval(20.0, 30.0, 40.0); // Esitellään periytetty laatikko - sama koko PahviLaatikko apahviltk(20.0, 30.0, 40.0); // Toinen periytetty laatikko

Virtuaalifunktiot ja polymorfismi cout << endl; omaltk.naytatilavuus(); vahval.naytatilavuus(); apahviltk.naytatilavuus(); cout << endl; // Näytetään kantalaatikon tilavuus // Näytetään periytetyn laatikon tilavuus // Näytetään periytetyn laatikon tilavuus // Nyt kokeillaan kantaluokan osoitinta Laatikko-oliolle Laatikko* pltk = &omaltk; // Osoittaa tyyppiin Laatikko cout << "omaltk:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); cout << endl; // Nyt kokeillaan kantaluokan osoitinta VahvaLtk-oliolle pltk = &vahval; // Osoittaa tyyppiin Laatikko cout << "vahval:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); cout << endl; // Nyt kokeillaan kantaluokan osoitinta PahviLaatikko-oliolle pltk = &apahviltk; // Osoittaa tyyppiin Laatikko cout << "apahviltk:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); return 0; Voit nyt kääntää esimerkin uudelleen. Kun suoritat sen, tulostuksen tulisi näyttää seuraavalta: Laatikon käytettävissä oleva tilavuus on 24000 Laatikon käytettävissä oleva tilavuus on 20400 Laatikon käytettävissä oleva tilavuus on 22722.4 omaltk:n tilavuus pltk:n kautta on 24000 Laatikon käytettävissä oleva tilavuus on 24000 vahval:n tilavuus pltk:n kautta on 20400 Laatikon käytettävissä oleva tilavuus on 20400 apahviltk:n tilavuus pltk:n kautta on 22722.4 Laatikon käytettävissä oleva tilavuus on 22722.4! Kuinka se toimii Avainsana virtual kantaluokan tilavuus()-funktion yhteydessä on riittävä määrittely, jotta kaikki tilavuus()-funktiot periytetyissä luokissa ymmärretään myös virtuaalisiksi. Voit halutessasi käyttää virtual-avainsanaa periytettyjen luokkien funktioiden yhteydessä - kuten edellisessä kaaviossa havainnollistettiin. Tässä esimerkissä jätimme virtual-avainsanan pois tilavuus()-funktioiden määrittelyistä havainnollistaaksemme, että sen käyttö ei ole pakollista. Suosittelen kuitenkin, että käytät virtual-avainsanaa kaikissa periytettyjen luokkien virtuaalifunktioiden esittelyissä, koska se tekee periytetyn luokan esittelyä lukevalle selväksi, että funktio on itse asiassa virtuaalinen ja että se sidotaan dynaamisesti. 631

C++ Ohjelmoijan käsikirja Ohjelma tekee selvästikin nyt sen, mitä halusimmekin. Funktion naytatilavuus() ensimmäinen kutsu Laatikko-tyyppiselle oliolle omaltk on seuraava: omaltk.naytatilavuus(); // Näytetään kantalaatikon tilavuus Tämä yksinkertaisesti kutsuu kantaluokan versiota tilavuus()-funktiosta, koska omaltk on tyyppiä Laatikko. Seuraava naytatilavuus()-funktion kutsu on VahvaLtk-tyyppiselle oliolle vahval: vahval.naytatilavuus(); // Näytetään periytetyn laatikon tilavuus Tämä lause kutsuu Laatikko-luokassa määriteltyä naytatilavuus()-funktiota - itse asiassa naytatilavuus()-funktiosta ei ole muuta versiota. Funktio on peritty VahvaLtk-luokan julkisena jäsenenä, joten sitä voidaan kutsua tällä tavalla. Funktion tilavuus() kutsu naytatilavuus()- funktiossa sidotaan periytetyssä luokassa määriteltyyn versioon, koska tilavuus() on virtuaalifunktio. Saamme näin oikein lasketun tilavuuden VahvaLtk-oliolle. Kolmas naytatilavuus()-funktion kutsu on PahviLaatikko-oliolle: apahviltk.naytatilavuus(); // Näytetään periytetyn laatikon tilavuus 632 Funktio naytatilavuus() on peritty PahviLaatikko-luokkaan ja tilavuus()-funktion kutsu sidotaan PahviLaatikko-luokan versioon, joten saamme jälleen oikein lasketun tilavuuden. Seuraavaksi kutsumme pltk-osoittimen avulla tilavuus()-funktiota suoraan sekä epäsuorasti naytatilavuus()-funktion kautta. Osoitin sisältää aluksi Laatikko-olion omaltk osoitteen ja sen jälkeen kahden periytetyn olion osoitteen. Kunkin olion tulostus näyttää, että oikea versio tilavuus()-funktiosta valitaan automaattisesti jokaisessa tilanteessa. Eli kyseessä on täydellisen polymorfismin esimerkki. Virtuaalifunktion vaatimukset Jotta funktio toimii virtuaalisesti, sinun tulee esitellä ja määritellä se periytetyssä luokassa saman nimiseksi kuin kantaluokassa ja sen parametriluettelon tulee olla sama kuin kantaluokassa. Lisäksi, jos olet esitellyt kantaluokan funktion const-tyyppiseksi, tulee sinun esitellä periytetyn luokan funktio myös const-tyyppiseksi. Yleensä periytetyn luokan funktion paluuarvon tyypin tulee olla sama kuin kantaluokassa, mutta tähän on poikkeus, kun kantaluokan funktion paluuarvo on osoitin tai viittaus luokkatyyppiin. Tällöin periytetyn luokan versio virtuaalifunktiosta voi palauttaa osoittimen tai viittauksen muuhunkin tyyppiin kuin kantaluokan tyyppiin. Emme käsittele tätä tämän enempää. Voit päätellä virtuaalifunktioiden määrittelyn säännöistä, että jos yrität käyttää eri parametrejä periytetyn luokan virtuaalifunktiossa, kuin mitä käytit kantaluokassa, virtuaalifunktion mekanismi ei toimi. Periytetyn luokan funktio sidotaan tällöin staattisesti käännösaikana. Näin on myös silloin, jos unohdat esitellä periytetyn luokan funktion const-tyyppiseksi, jos kantaluokan funktio on const-tyyppinen. Voit testata tätä poistamalla const-avainsanan PahviLaatikko-luokassa olevan tilavuus()- funktion esittelystä ja suorittamalla edellisen esimerkin uudelleen. Tämä tarkoittaa sitä, että PahviLaatikko-luokan tilavuus()-funktio ei enää täsmää Laatikko-luokassa esiteltyä

Virtuaalifunktiot ja polymorfismi virtuaalifunktiota, joten periytetyn luokan tilavuus()-funktio ei ole virtuaalinen. Näin ollen sidonta on staattinen, eli PahviLaatikko-luokan kohdalla kutsuttava funktio on kantaluokan versio, kun kutsutaan kantaluokan osoittimen avulla tai epäsuorasti naytatilavuus()-funktion kautta. Jos periytetyn funktion nimi ja parametriluettelo ovat samat kuin kantaluokassa esitetyllä virtuaalifunktiolla, paluuarvon tyypin pitää olla virtuaalifunktion sääntöjen mukainen. Jos se ei ole, periytetyn luokan funktio ei käänny. Tässä yhteydessä haluan lisätä pienen varoituksen, että älä koskaan esittele virtuaalifunktiota avoimeksi (inline). Avointen funktioiden tarkoitus on se, että ne korvataan käännösaikana, joten virtuaalifunktio ei voi olla avoin. Sinun tulee aina esitellä virtuaalifunktiot virtuaalisiksi. Virtuaalifunktiot ja luokkahierarkia Jos haluat, että funktiotasi kohdellaan virtuaalisena kantaluokan osoittimen kanssa, sinun tulee esitellä se virtuaaliseksi kantaluokassa. Voit esitellä kantaluokassa niin monta virtuaalifunktiota kuin haluat, mutta kaikkia virtuaalifunktioita ei tarvitse esitellä virtuaalisiksi kaikkein ylimmässä kantaluokassa. Tätä havainnollistetaan seuraavassa kaaviossa. class Laatikko virtual double tilavuus(); Class PahviLaatikko: public Laatikko virtual double tilavuus(); virtual void teetuo(int i); class VahvaLtk: public Laatikko virtual double tilavuus(); virtual string teetama(int i); class SaleLtk: public PahviLaatikko virtual void teetuo(int i); Perii tilavuus() -funktion PahviLaatikko-luokasta class SuuriLtk: public VahvaLtk virtual string teetama(int i); Perii tilavuus() -funktion VahvaLtk- luokasta class PieniLtk: public vahvaltk virtual double tilavuus(); virtual string teetama(int i); class Paketti: public PahviLaatikko virtual double tilavuus(); virtual void teetuo(int i); Tilavuus() On virtuaalinen kaikissa luokissa. TeeTuo() On virtuaalinen PahviLaatikko- SaleLtk- ja Pakettiluokissa TeeTama() On virtuaalinen VahvaLtk-, SuuriLtk- ja PieniLtkluokissa Kun esittelet funktion virtuaaliseksi yhdessä luokassa, funktio on virtuaalinen kaikissa luokissa, jotka on periytetty suoraan tai epäsuoraan tästä luokasta. Esimerkiksi kaikki luokat, jotka on periytetty Laatikko-luokasta, perivät tilavuus()-funktion virtuaalisena. Voit kutsua näiden luokkien tilavuus()-funktiota Laatikko*-tyyppisen osoittimen pltk avulla, koska osoitin voi sisältää minkä tahansa hierarkiassa olevan olion osoitteen: double tulos = pltk->tilavuus(); //Kutsuu minkä tahansa luokan funktiota SaleLtk-luokassa ei esitellä virtuaalifunktiota tilavuus(), joten PahviLaatikko-luokalta perittyä versiota käytetään SaleLtk-olioiden kohdalla. Se on peritty virtuaalifunktiona, joten sitä kutsutaan polymorfistisesti. 633

C++ Ohjelmoijan käsikirja Tyyppiä PahviLaatikko* olevaa osoitinta ppahviltk voidaan käyttää kutsuttaessa tilavuus()-funktiota, mutta vain PahviLaatikko-luokan kohdalla tai SaleLtk- ja Pakettiluokkien kohdalla, joiden kantaluokkana on PahviLaatikko: tulos = ppahviltk->tilavuus() //PahviLaatikko-, SaleLtk- ja Paketti-luokista PahviLaatikko-luokka ja siitä periytetyt luokat sisältävät myös virtuaalifunktion teetuo(). Tätä funktiota voidaan kutsua polymofistisesti PahviLaatikko*-tyyppisen osoittimen avulla: ppahviltk->teetuo(12); //PahviLaatikko-, SaleLtk- ja Paketti-luokista Huomaa, että et voi kutsua näiden luokkien teetuo()-funktiota käyttämällä osoitinta pltk, koska Laatikko-luokka ei sisällä funktiota teetuo(). Vastaavasti virtuaalifunktiota teetama() voidaan kutsua luokista VahvaLtk, SuuriLtk ja PieniLtk käyttämällä kantaluokan osoitinta pvahvaltk: string vastaus = pvahvaltk-teetama(3); //VahvaLtk-, SuuriLtk- ja PieniLtk-luokista Luonnollisestikin samaa osoitinta voidaan käyttää kutsuttaessa näiden luokkien tilavuus()- funktioita. Saantitavat ja virtuaalifunktiot Periytetyn luokan virtuaalifunktion saantitapa voi olla eri kuin saantitapa kantaluokassa. Kun kutsut virtuaalifunktiota osoittimen kautta, kantaluokan saantitapa ratkaisee, onko funktio kutsuttavissa periytetystä luokasta. Jos virtuaalifunktio on kantaluokassa julkinen, sitä voidaan kutsua kantaluokan osoittimen (tai viittauksen) avulla kaikkien periytettyjen luokkien kohdalla, riippumatta periytettyjen luokkien saantitavasta. Voimme havainnollistaa tätä muuttamalla hieman edellistä esimerkkiä. Kokeile itse - Virtuaalifunktioiden saantitavan vaikutus Muuta edellisen esimerkin VahvaLtk-luokan määrittelyä muuttamalla tilavuus()-funktio suojatuksi ja lisäämällä virtual-avainsana sen esittelyyn: class VahvaLtk : public Laatikko // Periytetty luokka public: // Muodostinfunktio VahvaLtk(double pituusarvo, double syvyysarvo, double korkeusarvo); protected: // Funktio, joka laskee VahvaLtk:n tilavuuden (15% menee laatikolle) virtual double tilavuus() const; ; main()-funktiota tulee myös muuttaa hieman: 634

Virtuaalifunktiot ja polymorfismi // Laatikko-luokka // VahvaLtk-luokka // PahviLaatikko- // Esimerkki 16.3 Saantitavat ja virtuaalifunktiot #include <iostream> #include "Laatikko.h" #include "VahvaLtk.h" #include "Pahvil.h" luokka using namespace std; int main() Laatikko omaltk(20.0, 30.0, 40.0); // Esitellään kantalaatikko VahvaLtk vahval(20.0, 30.0, 40.0); // Esitellään periytetty laatikko - sama koko PahviLaatikko apahviltk(20.0, 30.0, 40.0);// Toinen periytetty laatikko cout << endl; omaltk.naytatilavuus(); // Näytetään kantalaatikon tilavuus vahval.naytatilavuus(); // Näytetään periytetyn laatikon tilavuus apahviltk.naytatilavuus(); // Näytetään periytetyn laatikon tilavuus cout << endl; //cout << "vahval:n tilavuus on " << vahval.tilavuus() << endl; //Poista kommentointi niin saat virheen! // Nyt kokeillaan kantaluokan osoitinta Laatikko-oliolle Laatikko* pltk = &omaltk; // Osoittaa tyyppiin Laatikko cout << "omaltk:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); cout << endl; // Nyt kokeillaan kantaluokan osoitinta VahvaLtk-oliolle pltk = &vahval; // Osoittaa tyyppiin Laatikko cout << "vahval:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); cout << endl; // Nyt kokeillaan kantaluokan osoitinta PahviLaatikko-oliolle pltk = &apahviltk; // Osoittaa tyyppiin Laatikko cout << "apahviltk:n tilavuus pltk:n kautta on " << pltk->tilavuus() << endl; pltk->naytatilavuus(); return 0; Ei varmastikaan ole yllätys, että tämä koodi tuottaa saman tulostuksen kuin edellinenkin esimerkki. Kuinka se toimii Vaikka tilavuus()-funktio on esitelty suojatuksi VahvaLtk-luokassa, voimme silti kutsua sitä vahval-olion kohdalla naytatilavuus()-funktion kautta, joka on peritty Laatikko-luokasta. Voimme kutsua sitä myös kantaluokan osoittimen pltk avulla. Jos kuitenkin poistat kommentoinnin riviltä, jossa kutsutaan tilavuus()-funktiota suoraan vahval-olion kautta, koodi ei käänny. Tässä vaikuttaa se, sidotaanko kutsu dynaamisesti vai staattisesti. Kun käytät luokan oliota, kutsu sidotaan staattisesti (eli kääntäjän toimesta) ja koska tilavuus()-funktio on suojattu 635

C++ Ohjelmoijan käsikirja VahvaLtk-oliossa, kutsu vahval-olion kautta ei käänny. Kaikki muut kutsut sidotaan ohjelman suorituksen aikana - ne ovat polymorfistisia funktiokutsuja. Tällöin kantaluokan virtuaalifunktion saantitapa periytyy kaikkiin periytettyihin luokkiin. Tämä siitä huolimatta, että saantitapa on määritelty eksplisiittisesti periytetyssä luokassa: eksplisiittinen määrittely on voimassa vain staattisesti sidotuissa funktiokutsuissa. Parametrien oletusarvot virtuaalifunktioissa Koska oletusarvot käsitellään käännösaikana, voit saada odottamattomia vaikutuksia, jos käytät virtuaalifunktiossa parametrien oletusarvoja. Jos kantaluokan virtuaalifunktion esittelyssä on parametreillä oletusarvoja, ja kutsut funktiota kantaluokan osoittimen avulla, saat aina oletusparametrin arvon kantaluokan versiosta. Periytetyn luokan version parametrien oletusarvoilla ei ole vaikutusta. Voimme havainnollistaa tätä muuttamalla edellistä esimerkkiä lisäämällä tilavuus()-funktion parametrille oletusarvon. Kokeile itse - Parametrien oletusarvot Muuta Laatikko.cpp-tiedostossa olevaa tilavuus()-funktion esittelyä seuraavasti: double Laatikko::tilavuus(const int i) const cout << "Parametri = " << i << endl; return pituus * syvyys * korkeus; Muuta hieman myös funktion esittelyä Laatikko-luokassa: // Funktio, joka laskee Laatikko-olion tilavuuden virtual double tilavuus(const int i = 5) const; Parametrillä ei ole muuta tarkoitusta kuin havainnollistaa, miten oletusarvoja käsitellään. Muuta PahviLaatikko-luokkaa samalla tavalla, mutta anna parametrin oletusarvoksi 50 ja VahvaLtkluokassa oletusarvoksi 500. Voit lisäksi palauttaa VahvaLtk-luokan tilavuus()-funktion saantitavaksi julkisen. Äläkä unohda, että sinun tulee sisällyttää iostream-otsikkotiedosto kahteen cpptiedostoon. Tekemällä nämä muutokset luokkien määrittelyihin, voimme kokeilla oletusarvoja edellä näkemällämme main()-funktiolla, jossa kommentoimme rivin, jossa kutsutaan vahval-olion tilavuus()-funktiota suoraan. Saat seuraavanlaisen tulostuksen: Parametri = 5 Laatikon käytettävissä oleva tilavuus on 24000 Parametri = 5 Laatikon käytettävissä oleva tilavuus on 20400 Parametri = 5 Laatikon käytettävissä oleva tilavuus on 22722.4 636 Parametri = 500 vahval:n tilavuus on 20400 Parametri = 5 omaltk:n tilavuus pltk:n kautta on 24000

Virtuaalifunktiot ja polymorfismi Parametri = 5 Laatikon käytettävissä oleva tilavuus on 24000 Parametri = 5 vahval:n tilavuus pltk:n kautta on 20400 Parametri = 5 Laatikon käytettävissä oleva tilavuus on 20400 Parametri = 5 apahviltk:n tilavuus pltk:n kautta on 22722.4 Parametri = 5 Laatikon käytettävissä oleva tilavuus on 22722.4 Kuinka se toimii Yhtä kutsua lukuun ottamatta tulostettu oletusarvo on peräisin kantaluokan funktiosta. Poikkeuksena on, kun kutsumme tilavuus()-funktiota vahval-olion avulla. Tämä sidotaan staattisesti, joten VahvaLtk-luokan parametrin oletusarvoa käytetään. Kaikki muut kutsut sidotaan dynaamisesti, joten kantaluokan oletusarvoa käytetään. Viittausten käyttö kutsuttaessa virtuaalifunktioita Voit kutsua virtuaalifunktioita myös viittauksen avulla ja viittausparametrit ovat tehokas tapa polymorfismin soveltamisessa. Virtuaalifunktion kutsumisella viittaustyyppisen muuttujan avulla ei ole samaa taikaa kuin osoittimen avulla kutsuttaessa, koska viittausmuuttuja alustetaan vain kerran ja ne voivat näin ollen kutsua vain sen olion funktioita. Funktion viittausparametrit ovat kuitenkin eri asia. Oletetaan, että määrittelet funktion, jonka parametri on viittaus kantaluokkaan. Voit tällöin välittää periytetyn luokan olion funktiolle parametrinä. Funktiossa voit käyttää viittausparametriä virtuaalifunktion kutsumisessa. Kun funktiotasi suoritetaan, välitetylle oliolle sopiva virtuaalifunktio valitaan automaattisesti. Näemme, miten tämä toimii, kun muutamme esimerkin 16.2 main()-funktiota siten, että se kutsuu funktiota, jonka parametri on tyyppiä viittaus Laatikko-olioon. Kokeile itse - Viittausten käyttö virtuaalifunktioissa Tässä esimerkissä lisäämme uuden, erillisen globaalin funktion naytatilavuus(), joka tulostaa olion tilavuuden. Välitämme sille viittausparametrin, jota käytämme olion naytatilavuus()- funktiota kutsuessamme. Funktion määrittely on seuraava: void naytatilavuus(const Laatikko& rltk) rltk.naytatilavuus(); Voimme kutsua tätä funktiota main()-funktiostamme ja välittää sille parametrinä viittauksia periytettyjen luokkien olioihin. Ainoat muutokset, jotka sinun tulee tehdä luokkien määrittelyihin, ovat tilavuus()-jäsenfunktioiden määrittelyt, joista sinun tulee poistaa parametri i ja i:n tulostavat rivit. Meidän tulee muuttaa main()-funktiota seuraavasti: 637

C++ Ohjelmoijan käsikirja // Esimerkki 16.5 Virtuaalifunktioiden käyttö viittausten kautta #include <iostream> #include "Laatikko.h" // Laatikko-luokka #include "VahvaLtk.h" // VahvaLtk-luokka #include "Pahvil.h" // PahviLaatikkoluokka using namespace std; void naytatilavuus(const Laatikko& rltk); // Globaalin funktion prototyyppi int main() Laatikko omaltk(20.0, 30.0, 40.0); // Esitellään kantalaatikko VahvaLtk vahval(20.0, 30.0, 40.0); // Esitellään periytetty laatikko - sama koko PahviLaatikko apahviltk(20.0, 30.0, 40.0);// Toinen periytetty laatikko cout << endl; naytatilavuus(omaltk); naytatilavuus(vahval); naytatilavuus(apahviltk); cout << endl; // Näytetään kantalaatikon tilavuus // Näytetään periytetyn laatikon tilavuus // Näytetään periytetyn laatikon tilavuus // Rivit poistettu return 0; // Globaali funktio, joka näyttää laatikon tilavuuden void naytatilavuus(const Laatikko& rltk) rltk.naytatilavuus(); Esimerkin suorittaminen saa aikaan tulostuksen: Laatikon käytettävissä oleva tilavuus on 24000 Laatikon käytettävissä oleva tilavuus on 20400 Laatikon käytettävissä oleva tilavuus on 22722.4 Kuinka se toimii Funktiossa main() luomme kantaluokan olion omaltk ja kaksi eri periytetyn luokan oliota vahval ja apahviltk. Tämän jälkeen kutsumme globaalia naytatilavuus()-funktiota, kukin olio vuorollaan parametrinä. Kuten tulostuksesta huomaat, jokaisessa tapauksessa kutsutaan oikeaa tilavuus()-funktiota. Eli polymorfismi toimii viittausparametrin välityksellä. Joka kerta, kun funktiota kutsutaan, viittausparametri alustetaan parametrinä välitetyn olion mukaan. Koska parametri on viittaus kantaluokan olioon, kääntäjä tekee suoritusaikana sidonnan virtuaaliseen tilavuus()-funktioon. Jos olisit määritellyt parametrin viittaukseksi periytettyyn luokkaan, funktiokutsu olisi sidottu staattisesti, koska se olisi ollut täydellisesti määritelty. Dynaaminen sidonta suoritetaan vain, jos kutsu suoritetaan kantaluokan viittauksen avulla. 638

Virtuaalifunktiot ja polymorfismi Virtuaalifunktion kantaluokan version kutsuminen Olemme jo nähneet, kuinka helppoa on periytetyn luokan olion osoittimen tai viittauksen avulla kutsua periytetyn luokan versiota virtuaalifunktiosta - kutsut sidotaan tällöin dynaamisesti. Mutta mitä meidän tarvitsee tehdä samassa tilanteessa, jos haluammekin kutsua kantaluokan funktiota periytetyn luokan olion avulla? Laatikko-luokassa on tilanne, jossa saattaisimme haluta kutsua kantaluokan funktiota. Saattaisi olla hyödyllistä laskea tilavuuden ero PahviLaatikko- tai VahvaLtk-olioiden kohdalla verrattuna Laatikko-olioon. Yksi tapa tämän suorittamiseen on laskea erotus kantaluokan ja periytetyn luokan tilavuus()-funktion palauttamien tulosten välillä. Voit pakottaa, että kantaluokan virtuaalifunktiota kutsutaan staattisesti, kun käytät luokan nimeä yhdessä näkyvyysalueoperaattorin kanssa. Oletetaan, että meillä on osoitin pltk, joka on määritelty seuraavasti: PahviLaatikko apahviltk(40.0, 30.0, 20.0); Laatikko* pltk = &apahviltk; Voimme laskea tilavuuksien erotuksen PahviLaatikko-olion kohdalla seuraavasti: double erotus = pltk->laatikko::tilavuus() - pltk->tilavuus(); Lauseke pltk->laatikko::tilavuus() kutsuu kantaluokan versiota tilavuus()-funktiosta. Luokan nimi yhdessä näkyvyysalueoperaattorin kanssa määrittelee tietyn tilavuus()-funktion, joten kyseessä on staattinen sidonta, joka ratkaistaan käännösaikana.! Näkyvyysalueoperaattorin avulla voit kutsua kantaluokan versiota mistä tahansa jäsenfunktiosta, jos saantitapa sallii funktion kutsumisen. Huomaa, että et voi käyttää tätä tekniikkaa tietyn periytetyn luokan funktion kutsumiseen osoittimen avulla. Lauseke pltk->pahvilaatikko::tilavuus() ei käänny, koska PahviLaatikko::tilavuus() ei ole Laatikko-luokan jäsen. Kun kutsut funktiota osoittimen avulla, kutsu on joko staattinen, osoitettavan luokan jäsenfunktion kutsu tai dynaaminen virtuaalifunktion kutsu. Kantaluokan virtuaalifunktion kutsuminen periytetyn luokan olion avulla on myöskin helppoa. Voit käyttää static_cast-muunnosta periytetystä luokasta kantaluokkaan ja käyttää muunnoksen tulosta kantaluokan funktion kutsumiseen. Voimme laskea tilavuuden erotuksen apahviltk-olion kohdalla lauseella: double erotus = static_cast<laatikko>(apahviltk).tilavuus() - apahviltk.tilavuus(); Tämän lauseen molemmat funktiokutsut sidotaan staattisesti. Muuntamalla apahviltk Laatikko-tyyppiseksi, kutsuttava funktio on Laatikko-luokan tilavuus()-funktio. Virtuaalifunktioiden kutsut olion avulla sidotaan aina staattisesti. 639

C++ Ohjelmoijan käsikirja Olioiden osoittimien muuntaminen Jos ohjelmassasi on osoitin periytettyyn luokkaan, voit helposti muuntaa sen osoittimeksi kantaluokkaan. Tämä voidaan tehdä sekä suorien että epäsuorien kantaluokkien kohdalla. Esitellään esimerkiksi osoitin PahviLaatikko-luokkaan seuraavasti: PahviLaatikko* ppahviltk = new PahviLaatikko(30, 40, 10); Voimme muuntaa tämän osoittimen automaattisesti PahviLaatikko-luokan suoran kantaluokan osoittimeksi (Laatikko-luokkahan on PahviLaatikko-luokan suora kantaluokka): Laatikko* pltk = ppahviltk; Tuloksena on osoitin Laatikko-olioon, joka alustetaan osoittamaan uuteen PahviLaatikko-olioon. Voimme automaattisesti muuntaa periytetyn luokan olion osoittimen epäsuoran kantaluokan osoittimeksi. Oletetaan, että meillä on viereisen kaavion mukainen hierarkia: class Laatikko class PahviLaatikko: public Laatikko class KauraLtk: public PahviLaatikko Tässä Laatikko on PahviLaatikko-luokan suora kantaluokka, joten se on KauraLtk-luokan epäsuora kantaluokka. Voimme nyt kirjoittaa seuraavaa: Laatikko* pltk = pkauraltk; Tämä lause muuntaa pkauraltk:n sisältämän osoitteen tyypin tyypistä osoitin KauraLtk-olioon tyyppiin osoitin Laatikko-olioon. Jos sinun tulee määritellä muunnos eksplisiittisesti, voit käyttää static_cast()-operaattoria: Laatikko* pltk = static_cast<laatikko*>(pkauraltk); Kääntäjä pystyy yleensä suorittamaan tämän muunnoksen, koska se pystyy päättelemään, että Laatikko on KauraLtk:n kantaluokka. Koska KauraLtk-olio sisältää Laatikko-olion, muunnos on mahdollinen. Muunnos ei ole mahdollinen, jos Laatikko-luokkaan ei päästä käsiksi, tai jos Laatikko-luokka on virtuaalinen kantaluokka. Kääntäjä huomaa tällaiset tilanteet. Seuraavassa kaaviossa on kaikki mahdolliset muunnokset alkaen KauraLtk-luokasta. 640

Virtuaalifunktiot ja polymorfismi Muunnos suoraan kantaluokkaan: ppahviltk = ppak; KauraLtk* ppak = new KauraLtk; ppak KauraLtk-olio ppak KauraLtk-olio ppahviltk PahviLaatikko -olio PahviLaatikko -olio Laatikko- olio Laatikko- olio Muunnos epäsuoraan kantaluokkaan: pltk = ppak; ppak KauraLtk-olio Osoittimien muunnokset hierarkiassa ylöspäin pltk PahviLaatikko -olio Laatikko- olio Kuten huomaat, jokaisen tapauksen tuloksena on osoitin kohteena olevan tyypin aliolioon. On erittäin helppoa mennä sekaisin, kun pohdit osoittimen muunnoksia luokkatyyppien välillä. Muista, että osoitin tiettyyn luokkatyyppiin voi osoittaa vain kyseisen tyyppisiin olioihin tai periytetyn luokan olioihin, ei toisinpäin. Tarkasti ottaen, osoitin ppahvilaatikko voi sisältää PahviLaatikko-tyyppisen olion osoitteen (joka on KauraLtk-olion aliolio) tai KauraLtk-tyyppisen olion osoitteen. Se ei voi sisältää Laatikko-tyyppisen olion osoitetta, koska KauraLtk on eräänlainen PahviLaatikko, mutta Laatikko ei ole. Edellä kerrotusta huolimatta on joskus mahdollista tehdä muunnoksia toiseenkin suuntaan. Osoittimen muuntaminen luokkahierarkiassa alaspäin, kantaluokasta periytettyyn luokkaan, on erilainen tilanne, koska muunnos riippuu siitä, mihin kantaluokan osoitin osoittaa. Muunnos esimerkiksi kantaluokan olion osoittimesta pltk periytetyn luokan olion osoittimeen voidaan suorittaa vain, jos kantaluokan osoitin osoittaa PahviLaatikko-luokan Laatikko-aliolioon. Jos näin ei ole, muunnoksen tulos on määrittelemätön. PahviLaatikko* ppahviltk = static_cast<pahvilaatikko*>(pltk); Laatikko* pltk = new PahviLaatikko; ppahviltk PaviLaatikko-olio PaviLaatikko-olio pltk Laatikko -olio pltk Laatikko -olio KauraLtk* ppak = static_cast<kauraltk*>(pltk); PaviLaatikko-olio Osoittimien muunnokset hierarkiassa alaspäin ppak pltk Laatikko -olio??? - määrittelemätön 641

C++ Ohjelmoijan käsikirja Kaavio näyttää muunnoksen osoittimesta pltk, joka sisältää PahviLaatikko-olion osoitteen. Muunnos tyyppiin PahviLaatikko* toimii, koska olio on tyyppiä PahviLaatikko. Tyyppiin KauraLtk* muunnoksen tulos on kuitenkin määrittelemätön, koska sen tyyppistä oliota ei ole olemassa. Jos et ole aivan varma, että muunnos on sallittu, älä käytä sitä. Osoittimen muuntaminen hierarkiassa alaspäin onnistuu, jos osoitin sisältää kohteena olevan tyypin osoitteen. Muunnos ei tarkista, onko tämä totta, joten jos yrität käyttää muunnosta tilanteessa, jossa et ole aivan varma mihin osoitin osoittaa, voit saada määrittelemättömän tuloksen. Tästä syystä sinun tulee muuntaa luokkahierarkiassa alaspäin eri tavalla: tavalla, jolla muunnos voidaan tarkistaa suoritusaikana. Dynaaminen muunnos Dynaaminen muunnos on muunnos, joka tehdään suoritusaikana. Se suoritetaan dynamic_cast<>()-operaattorilla. Voit käyttää tätä operaattoria ainoastaan polymorfististen luokkien osoittimien ja viittausten yhteydessä - eli luokkien yhteydessä, joilla on vähintään yksi virtuaalifunktio. Tämä siitä syystä, että ainoastaan polymorfististen luokkien osoittimet sisältävät tietoa, jota dynamic_cast<>()-operaattori tarvitsee muunnoksen tarkistamiseen. Tämä operaattori on tarkoitettu muuntamaan luokkahierarkian luokkien osoittimien välillä tai luokkahierarkian luokkien viittausten välillä. Huomaa, että tyypit, joiden välillä teet muunnoksen, täytyy olla osoittimia tai viittauksia saman luokkahierarkian luokkiin. Et voi käyttää dynamic_cast<>()-operaattoria mihinkään muuhun. Aloitamme operaattorin tutkimisen osoittimien dynaamisesta tyypinmuunnoksesta. Osoittimien dynaamiset muunnokset Voimme erottaa kaksi dynaamista muunnosta. Ensimmäinen on muunnos hierarkiassa alaspäin, suoran tai epäsuoran kantaluokan osoittimesta periytetyn luokan osoittimeen. Tätä kutsutaan alasmuunnokseksi. Toinen mahdollisuus on muuntaa hierarkiassa ristiin; tätä kutsutaan ristiinmuunnokseksi. Näitä kumpaakin havainnollistetaan alla olevassa kaaviossa. class Laatikko Alasmuunnoksen esimerkki: Tyypistä osoitin Laatikko-olioon tyyppiin osoitin PahviLtk-olioon Ristiinmuunnoksen esimerkki: Tyypistä osoitin Laatikko-olioon tyyppiin osoitin Sisalto-olioon class PahviLaatikko: public Laatikko class Sisalto class KauraLtk : public PahviLaatikko, publicsisalto 642

Virtuaalifunktiot ja polymorfismi Osoittimelle pltk, joka on tyyppiä osoitin Laatikko-luokkaan, voimme kirjoittaa kaaviossa kuvatun alasmuunnoksen seuraavasti: PahviLaatikko* ppahviltk = dynamic_cast<pahvilaatikko*>(pltk); Kuten huomaat, dynamic_cast<>()-operaattori kirjoitetaan samaan tapaan kuin static_cast<>()- operaattorikin. Kohdetyyppi kirjoitetaan kulmasulkeiden sisään ja lauseke, jonka haluat muuntaa uuteen tyyppiin, kirjoitetaan tavallisten sulkeiden sisään. Jotta tämä muunnos on sallittu, luokilla Laatikko ja PahviLaatikko tulee olla virtuaalisia funktioita, joko omia tai perittyjä. Jotta muunnos toimii, pltk:n tulee osoittaa joko PahviLaatikko-olioon tai KauraLtk-olioon, koska vain näiden tyyppiset oliot sisältävät PahviLaatikko-aliolion. Jos muunnos ei onnistu, osoittimen ppahviltk arvoksi asetetaan 0. Kaaviossa esitetty ristiinmuunnos voidaan kirjoittaa seuraavasti: Sisalto* psisalto = dynamic_cast<sisalto*>(pltk); Kuten edellisessäkin tapauksessa, Sisalto- ja Laatikko-luokkien tulee kummankin olla polymorfistisia, jotta muunnos on sallittu. Muunnos onnistuu ainoastaan, jos pltk sisältää KauraLtk-tyyppisen olion osoitteen, koska se on ainut tyyppi, joka sisältää Sisalto-olion ja johon voidaan viitata Laatikko*-tyyppisellä osoittimella. Jälleen, jos muunnos ei onnistu, psisaltoosoittimen arvoksi asetetaan 0. Alasmuuntaminen luokkahierarkiassa dynamic_cast<>()-operaattorin avulla voi epäonnistua, mutta tällöin palautetaan null-osoitin, eikä vain määrittelemätöntä arvoa. Tämä antaakin sinulle jo vihjeen, miten tätä muunnosta tulee käyttää. Oletetaan, että sinulla on jokin olio, johon osoitetaan Laatikko*-tyyppisellä osoittimella ja haluat kutsua ei-virtuaalista PahviLaatikkoluokan jäsenmuuttujaa. Kantaluokan osoittimen avulla voit kutsua ainoastaan periytetyn luokan virtuaalifunktiota, mutta dynamic_cast<>()-operaattorin avulla voit kutsua ei-virtuaalista funktiota. Oletetaan, että pinta_ala() on ei-virtuaalinen PahviLaatikko-luokan jäsenfunktio. Voit kutsua sitä lauseella: dynamic_cast<pahvilaatikko*>(pltk)->pinta_ala(); Tämä on kuitenkin varsin vaarallista. Sinun täytyy yhä olla varma, että pltk osoittaa PahviLaatikko-olioon tai sellaisen luokan olioon, jolla on PahviLaatikko-luokka kantaluokkana. Jos näin ei ole, dynamic_cast<>()-operaattori palauttaa null-osoittimen ja kutsu epäonnistuu. Voit korjata tämän käyttämällä ennen funktion kutsua dynamic_cast<>()-operaattoria esimerkiksi seuraavasti: if(pahvilaatikko* ppahviltk = dynamic_cast<pahvilaatikko*>(pltk)) ppahviltk->pinta_ala(); Nyt kutsumme funktiota vain, jos muunnoksen tulos ei ole null. Huomaa, että et voi poistaa const-määreen vaikutusta dynamic_cast<>()-operaattorilla. Jos osoitin, josta olet muuntamassa, on const-tyyppinen, osoittimen tyyppi, joksi olet muuntamassa, tulee myöskin olla const-tyyppinen. Jos haluat muunta const-tyyppisestä osoittimesta muuhun kuin const-tyyppiseen osoittimeen, sinun tulee ensin muuntaa muunnettavaa tyyppiä olevaan muuhun kuin const-tyyppiseen osoittimeen const_cast<>()-operaattorilla. 643