* yksinkertaiset luokat
* olioiden perusteet
* olioterminologia
* koostaminen
* perintä
* polymorfismi
luokan esittely: class cNimi : public cIsa, public cIsa { // 0-n x public cIsa private: // päällä oletuksena yksityiset_attribuutit // vain itse näkee yksityiset_metodit protected: suojatut_attribuutit // perilliset näkee suojatut_metodit public: julkiset_attribuutit // kaikki näkee julkiset_metodit }; // HUOM! puolipiste attr kuten muuttuja attrib.esitt. tyyppi attr; metodin esitt. kuten aliohjelman esittely metodin esitt. luokan ulkop. tyyppi cNimi::metodin_nimi(param_lista) viit.olion metod: olio.metodin_nimi(param,param) // 0-n x param jos osoitin: pOlio->metodin_nimi(param,param) yliluokan metodiin viit. yliluokka::metodin_nimi(param,param) muodostaja cNimi(param_lista) // voi olla monta eri param_listoilla hajoittaja ~cNimiTähän lukuun on kasattu suuri osa olioihin liittyvää asiaa yhden esimerkin valossa. Esimerkin yksinkertaisuuden takia se ei anna joka tilanteessa täyttä hyötyä esitetyistä ominaisuuksista. Lisäksi asiaa voi olla yhdelle lukukerralla liikaa ja esimerkiksi perintä ja polymorfismi kannattaa ehkä jättää myöhemmälle lukukerralle.
Aloitetaanpa tutkimalla aikalisa esimerkkiämme. Pääohjelmassa esiteltiin muuttuja tunteja varten ja muuttuja minuutteja varten. Aluksi tämä saattaa tuntua hyvin luonnolliselta ja niin se onkin, niin kauan kuin ohjelman koko pysyy pienenä. Entäpä ohjelma jossa tarvitaan paljon kellonaikoja?
... alku kuten aikalis3.cpp int main(void) ? { int h1=12,m1=15; int h2=13,m2=16; int h3=14,m3=25; lisaa(h1,m1,55); tulosta(h1,m1); lisaa(h2,m2,27); tulosta(h2,m2); lisaa(h3,m3,39); tulosta(h3,m3); return 0; }Hyvinhän tuo vielä toimii? Ja jos otettaisiin taulukot käyttöön, ei tarvitsisi edes numeroida muuttujia. Entäpä jos joku tulee ja sanoo, että sekunnitkin mukaan! Tulee paljon työtä jos on paljon aikoja.
#include <iostream.h> #include <iomanip.h> struct tAika { int h,m; }; void lisaa(tAika &rAika, int lisa_min) { int yht_min = rAika.h * 60 + rAika.m + lisa_min; rAika.h = yht_min / 60; rAika.m = yht_min % 60; } void tulosta(tAika Aika) { // Muotoilu jotta 15:04 tulostuisi oikein eikä 15:4 cout << setfill('0') << setw(2) << Aika.h << ":" << setw(2) << Aika.m << endl; } int main(void) { tAika a1={12,15}, a2={13,16}, a3={14,25}; lisaa(a1,55); tulosta(a1); lisaa(a2,27); tulosta(a2); lisaa(a3,39); tulosta(a3); return 0; }
tAika a1;
tAika a1={12,15};
struct tAika { // struct ja tyypin nimi int h,m; // alkioiden tyypit ja nimet }; // voi olla myös struct tAika { int h; int m; };
Huom! C- kielessä määrittely täytyy tehdä typedef - lauseella:
typedef struct { int h,m; } tAika;
Muuten kaikki tässä sanottu pätee myös C- kieleen. C++:ssakin voitaisiin tietue määritellä em. tavalla, yhteensopivuussyistä ehkä jopa kannattaisi?
minuutit = a1.m;
void lisaa(tAika *pAika, int lisa_min) { int yht_min = (*pAika).h * 60 + (*pAika).m + lisa_min; (*pAika).h = yht_min / 60; (*pAika).m = yht_min % 60; } ... lisaa(&a1,55);
void lisaa(tAika *pAika, int lisa_min) { int yht_min = pAika->h * 60 + pAika->m + lisa_min; pAika->h = yht_min / 60; pAika->m = yht_min % 60; }Muista että tämä osoitemerkintä toimii VAIN silloin kun kyseessä on osoitin muuttuja. Tavallisen tietuemuuttujan tapauksessa viitataan alkioon pisteellä.
tAika a1={12,15};jättäisi sekunnit alkuarvoonsa, joka tässä tapauksessa on valitettavasti tuntematon! Muuten riittää muuttaa pelkkiä aliohjelmia!
Suurin hyöty tietueesta olikin lähinnä kasata yhteen kuuluvat asiat saman nimikkeen alle. Tämä on kapselointia (encapsulation) väljässä mielessä.
tAika aika={12,30}; tPvm pvm={14,1,1997}; tulosta(aika); tulosta(pvm);niin kumpikin tulosta- kutsu kutsuu eri aliohjelmaa. Funktioiden kuormitus onkin varsin mukava lisä ohjelmointiin, se ei kuitenkaan ole varsinaisia olio- ohjelmoinnin piirteitä.
Itse tietue saakin nimen luokka (class) ja tietuetta vastaava muuttuja - luokan ilmentymä - on sitten se kuuluisa olio (object)
#include <iostream.h> #include <iomanip.h> struct cAika { int h,m; void lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } void tulosta() const { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl; } }; int main(void) { cAika a1={12,15}, a2={13,16}, a3={14,25}; a1.lisaa(55); a1.tulosta(); a2.lisaa(27); a2.tulosta(); a3.lisaa(39); a3.tulosta(); return 0; }Siinäpä se! Onko muutokset edelliseen nähden suuria? Siis iso osa koko olio- ohjelmoinnista (ja tietotekniikasta muutenkin) on markkinahenkilöiden huuhaata ja yleistä hysteriaa "kaiken ratkaisevan" teknologian ympärillä. No, tosin olio- ohjelmonnissa on puolia, joita emme vielä ole nähneetkään, joiden ansiosta olio- ohjelmointia voidaan pitää uutena ja ohjelmointia ja ylläpitoa helpottavana teknologiana. Näitä ovat mm. perintä ja polymorfismi (monimuotoisuus), joihin emme valitettavasti tällä kurssilla ehdi perehtyä kovinkaan syvällisesti.
No takaisin esimerkkiimme. Uutta on lähinnä se, että metodien (no sanotaan tästä lähtien funktioita metodeiksi) parametrilistat ovat lyhentyneet. Itse oliota ei tarvitse enää viedä parametrina, koska metodit ovat luokan sisäisiä ja tällöin luokkaa edustava olio kyllä tuntee itse itsensä. Samoin on jäänyt pois metodien sisältä viite olioon - samasta syystä:
... void lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } ...Metodia kutsutaan analogisesti tietueen kenttään viittaamisen kanssa, eli
a1.lisaa(55); a1.tulosta();Tällekin on keksitty oma nimi: välitetään oliolle viesti "tulosta" (message passing). Tässä kuitenkin jatkossa voi vielä lipsahtaa ja vahingossa sanomme kuitenkin, että kutsutaan metodia tulosta, vaikka ehkä pitäisi puhua viestin välittämisestä.
Metodin tulosta esittelyssä
void tulosta() const {oleva const tarkoittaa että metodi ei muuta itse olion tilaa (sen attribuutteja). Metodi lisaa taas vastaavasti muuttaa olion tilaa hyvinkin paljon.
Älä hämäänny termeistä
oliotermi perinteinen termi ----------------------------------------------------------------------------- cAika - aika-luokka tietuetyyppi h,m - aika-luokan attribuutteja tietueen alkio lisaa,tulosta - aika-luokan metodeja funktio,aliohj. a1,a2,a3 - olioita, jotka ovat aika-luokan ilmentymiä muuttuja a1.lisaa(55) - viesti olioille a1: lisää 55 minuuttia aliohjelman kutsu
Kuka voi käyttää metodia/attribuuttia
|
Oletuksena
|
|||||
Suojaus
|
kaikki
|
aliluokat
|
friend-
funktiot
|
luokan
metodit
|
struct union |
class
|
private
|
x
|
x
|
x
| |||
protected
|
x
|
x
|
x
|
|||
public
|
x
|
x
|
x
|
x
|
x
|
class cAika { int h,m; void lisaa(int lisa_min) {...} void tulosta() const {...} };lakkaisi ohjelmamme toimimasta, koska esimerkiksi pääohjelman kutsu
a1.lisaa(55)tulisi laittomaksi kaikkien luokan jäsenten ollessa yksityisiä (private). Ongelmaa voisi yrittää korjata esittelemällä metodit julkisiksi:
class cAika { int h,m; public: void lisaa(int lisa_min) {...} void tulosta() const {...} };Nyt kääntäjä valittaisi esimerkiksi:
Error: aikacla.cpp(21,13):Objects of type 'cAika' cannot be initialized with { }rivistä
cAika a1={12,15},...Nyt vasta alkaakin olio- ohjelmoinnin hienoudet! Aloittelijasta saattaa tuntua että mitä turhaa tehdään asioista monimutkaisempaa kun se onkaan! Nimittäin aikaolio.cpp saattoi tuntua kohtuullisen ymmärrettävältä. Mutta todellakin jos olisi ilman muuta sallittua sijoittaa oliolle mitä tahansa arvoja, niin mitä mieltä oltaisiin seuraavasta:
cAika a1={42,175};Väärinkäytetyt ja virheelliset arvot muuttujilla on ollut ohjelmoinnin kiusa alusta alkaen. Nyt meillä on mahdollisuus päästä niistä eroon kapseloinnin (jotkut sanovat kotelointi, encapsulation) ansiosta. Eli kaikki arvojen muutokset (eli olio tapauksessa olion tilojen muutokset) voidaan suorittaa kontrolloidusta, vain olion itse siihen suostuessa. Mutta miten sitten alustuksen tapauksessa?
Joissakin oliokielissä konstruktori ilmoitetaan omalla avainsanallaan. C++:ssa muodostaja on metodi, jolla on sama nimi kuin luokalla. Muodostajia voi olla useitakin. Muodostaja on aina tyypitön, siis ei edes void- tyyppiä.
#include <iostream.h> #include <iomanip.h> class cAika { int h,m; public: cAika(int ih, int im) { h = ih; m = im; } void lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } void tulosta() const { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl; } }; int main(void) { cAika a1(12,15), a2(13,16), a3(14,25); a1.lisaa(55); a1.tulosta(); a2.lisaa(27); a2.tulosta(); a3.lisaa(39); a3.tulosta(); return 0; }Esimerkissämme muodostaja on esitelty 2-parametriseksi
cAika(int ih, int im) { h = ih; m = im; }ja sitä kutsutaan olion esittelyn yhteydessä
cAika a1(12,15);
cAika aika;Kääntäjä antaisi esimerkiksi virheilmoituksen:
Error: aikacla.cpp(26,14):Could not find a match for 'cAika::cAika()'Parametritonta muodostajaa sanotaan oletusmuodostajaksi (default constructor). Sellainen on luokalla aina ilman muuta, jos luokalle ei ole esitelty yhtään muodostajaa. Jos luokalle esitellään muodostaja, ei oletusmuodostaja enää tulekaan automaattisesti.
Meidän pitäisi päättää nyt paljonko kellomme on, jos sitä ei erikseen ilmoiteta. Olkoon kello vaikka 0:0, eli keskiyö. Esittelemme oletusmuodostajan
... class cAika { int h,m; public: cAika() { h = 0; m = 0; } cAika(int ih, int im) { h = ih; m = im; } void lisaa(int lisa_min) { ... } void tulosta() const { ... } }; int main(void) { cAika a1(12,15), a2(13,16), a3(14,25); a1.lisaa(55); a1.tulosta(); ... cAika aika; aika.tulosta(); return 0; }Oletusmuodostajaa kutsutaan hieman epäloogisesti ilman sulkuja
cAika aika;eikä
cAika aika();kuten muiden parametrittomien funktioiden kutsusta saattaisi päätellä!
... public: cAika(int ih=0, int im=0) { h = ih; m = im; } void lisaa(int lisa_min) { ... };Nyt oliot voitaisiin esitellä millä tahansa seuraavista tavoista; ilman parametreja, yhdellä tai kahdella parametrillä:
cAika a1, a2(13), a3(14,25);Tulostus olisi vastaavasti:
a1.tulosta(); a2.tulosta(); a3.tulosta(); => 00:00 13:00 14:25Oletusparametri tarkoittaa siis sitä, että mikäli parametrilla ei ole kutsussa arvoa, käytetään funktion/metodin esittelyssä ollutta oletusarvoa.
cAika a1(42,175);Toisaalta miten joku voisi muuttaa ajan arvoa muutenkin kuin lisaa- metodilla? Teemmekin aluksi metodin aseta, jota kutsuttaisiin
a1.aseta(12,15); a2.aseta(16);Nyt pitää kuitenkin päättää mitä tarkoittaa laiton asetus! Jos sovimme että minuuteissa yli 59 arvot ovat aina 59 ja alle 0:n arvot ovat aina 0, voisi aseta- metodi olla kirjoitettu seuraavasti:
void aseta(int ih,int im=0) { h = ih; m = im; if ( m > 59 ) m = 59; if ( m < 0 ) m = 0; }Jos taas haluaisimme, että ylimääräinen osuus siirtyisi tunteihin, voitaisiinkin tämä tehdä "kierosti" lisaa- metodin avulla
#include <iostream.h> #include <iomanip.h> class cAika { int h,m; public: void aseta(int ih,int im=0) { h = ih; m = im; lisaa(0); } cAika(int ih=0, int im=0) { aseta(ih,im); } void lisaa(int lisa_min) { ... } void tulosta() const { ... } }; int main(void) { cAika a1, a2(13), a3(14,25); a1.tulosta(); a2.tulosta(); a3.tulosta(); a1.aseta(12,15); a2.aseta(16,-15); a1.tulosta(); a2.tulosta(); return 0; }Huomattakoon, että samalla kun tehdään aseta- metodi, kannattaa muodostajassakin kutsua sitä. Näin olion tilaa muutetaan vain muutamassa metodissa, jotka voidaan ohjelmoida niin huolella, ettei mitään yllätyksiä pääse koskaan tapahtumaan. Tämä rupeaa jo muistuttamaan olio- ohjelmointia!
#include <iostream.h> #include <iomanip.h> class cAika { int h,m; public: cAika(int ih=0, int im=0); void aseta(int ih,int im=0); void lisaa(int lisa_min); void tulosta() const; }; void cAika::aseta(int ih,int im) { h = ih; m = im; lisaa(0); } cAika::cAika(int ih, int im) { aseta(ih,im); } void cAika::lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } void cAika::tulosta() const { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl; } int main(void) { cAika a1, a2(13), a3(14,175); a1.tulosta(); a2.tulosta(); a3.tulosta(); a1.aseta(12,15); a2.aseta(16,-15); a1.tulosta(); a2.tulosta(); return 0; }Jos metodin toteutus on luokan ulkopuolella, pitää metodin nimen eteen liittää tieto siitä, minkä luokan metodista on kyse. Esimerkiksi:
void cAika::lisaa(int lisa_min)Nyt metodit ovat todellisia kutsuttavia aliohjelmia. Tähän saakka metodit ovat olleet inline- funktioita, eli niihin ei ole tehty aliohjelmakutsua, vaan niiden koodi on sijoitettu kutsun tilalle. Etuna on nopeampi suoritus kun aliohjelmaan siirtyminen ja paluu jäävät pois, mutta haittana suurempi ohjelman koko, erityisesti jos kutsuja on paljon ja metodit ovat suuria.
Jos nopeussyistä jälkeenpäin jokin luokan ulkopuolelle kirjoitettu metodi haluttaisiinkin takaisin inline- metodiksi, voidaan tämä tehdä myös inline- avainsanalla:
inline void cAika::aseta(int ih,int im) { h = ih; m = im; lisaa(0); }Siis inline- metodeja voidaan tehdä kahdella eri tavalla:
Esimerkiksi seuraavasta ohjelmasta:
#include <iostream.h> inline void ynnaa_yksi(int &ri) { ri = ri + 1; } inline void ynnaa_kaksi(int &ri) { ynnaa_yksi(ri); ynnaa_yksi(ri); } int main (void) { int i = 10; ynnaa_kaksi(i); cout << i << endl; return 0; }riveistä
int i = 10; ynnaa_kaksi(i);hyvä kääntäjä kääntää koodin:
mov eax,10 // Suoraan kääntäjä ei välttämättä uskalla laittaa mov eax,12 inc eax // koska se ei voi tietää tarvitaanko inc lauseen ominaisuutta inc eax // Oikein hyvä kääntäjä ehkä laittaisikin mov eax,12joka on täsmälleen sama koodi, joka generoituu lauseista
i = 10; ++i; ++i;Jos inline- määreet jätettäisiin ynnaa- aliohjelmista pois, olisi ynnaa_kaksi kutsu yli 30 kertaa hitaampi!
Oikeasti siis jaamme koko ohjelman 3 eri tiedostoon:
aika.h luokan esittely
aika.cpp luokan toteutus
aikatest.cpp luokan testaava pääohjelma
#ifndef AIKA_H // Suoja, jottei samaa koodia "includata" kahta kertaa! #define AIKA_H class cAika { int h,m; public: cAika(int ih=0, int im=0); void aseta(int ih,int im=0); void lisaa(int lisa_min); void tulosta() const; }; #endif // AIKA_H
#include <iostream.h> #include <iomanip.h> #include "aika.h" void cAika::aseta(int ih,int im) { h = ih; m = im; lisaa(0); } cAika::cAika(int ih, int im) { aseta(ih,im); } void cAika::lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } void cAika::tulosta() const { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl; }
#include "aika.h" // Projektiin aika.cpp ja aikatest.cpp int main(void) { cAika a1, a2(13), a3(14,175); a1.tulosta(); a2.tulosta(); a3.tulosta(); a1.aseta(12,15); a2.aseta(16,-15); a1.tulosta(); a2.tulosta(); return 0; }Lopuksi tehtäisiin vielä projekti tai makefile johon kuuluvat aika.cpp ja aikatest.cpp.
void lisaa(tAika *pAika, int lisa_min) { int yht_min = pAika->h * 60 + pAika->m + lisa_min; pAika->h = yht_min / 60; pAika->m = yht_min % 60; } ... lisaa(&a1,55);ja metodia
void cAika::lisaa(int lisa_min) { int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } ... a1.lisaa(55);niin helposti näyttää, että ensin mainitussa funktiossa on enemmän parametreja. Tosiasiassa kummassakin niitä on täsmälleen sama määrä. Nimittäin jokaisen metodin ensimmäisenä näkymättömänä parametrina tulee aina itse luokan osoite, this. Voitaisiinkin kuvitella, että metodi onkin toteutettu:
"void cAika::lisaa(cAika *this,int lisa_min)" // Näin EI SAA KIRJOITTAA!!! { int yht_min = this->h * 60 + this->m + lisa_min; this->h = yht_min / 60; this->m = yht_min % 60; } ... "a1.lisaa(a1,55)";Oikeasti this - osoitinta ei saa esitellä, vaan se on ilman muuta mukana parametreissa sekä esittelyssä että kutsussa. Mutta voimme todellakin kirjoittaa:
void cAika::lisaa(int lisa_min) { int yht_min = this->h * 60 + this->m + lisa_min; this->h = yht_min / 60; this->m = yht_min % 60; }Jonkun mielestä voi jopa olla selvempi käyttää this- osoitinta luokan attribuutteihin viitattaessa, näinhän korostuu, että käsitellään nimenomaan tämän luokan attribuuttia h, eikä mitään globaalia muuttujaa h. Joskus this- osoitinta tarvitaan välttämättä palautettaessa oliotyyppisellä metodilla olion koko tila (esim. viite olioon). Lisäksi joissakin kielissä this- osoittimen vastinetta (usein self) on aina käytettävä.
Tämän laajennuksen tekemiseen on olio- ohjelmoinnissa kolme mahdollisuutta: Joko muuttaa alkuperäistä luokkaa, periä alkuperäisestä luokasta laajempi versio tai tehdä uusi luokka, jossa on alkuperäinen luokka yhtenä attribuuttina.
Tutustumme seuraavassa kaikkiin kolmeen eri mahdollisuuteen.
Jos luokka on saatu joltakin kolmannelta osapuolelta, ei luokan päivittäminen edes ole mahdollista, vaan silloin täytyy turvautua muihin (parempiin) tapoihin.
Päivitämme nyt kuitenkin alkuperäistä luokkaa:
#include <iostream.h> #include <iomanip.h> class cAika { int h,m,s; // lisätty s public: cAika(int ih=0, int im=0, int is=0); // lisätty is=0 void aseta(int ih,int im=0, int is=0); // lisätty is=0 void lisaa(int lisa_min, int lisa_sek=0); // lisätty lisa_sek=0 void tulosta(int tulsek=0) const; // lisätty tulsek=0 }; void cAika::aseta(int ih,int im, int is) // lisätty is { h = ih; m = im; s = is; lisaa(0,0); // lisätty s=is ja ,0 } cAika::cAika(int ih, int im, int is) // lisätty int is { aseta(ih,im,is); } void cAika::lisaa(int lisa_min, int lisa_sek) // lisätty int lisa_sek { s += lisa_sek; // lisätty int yht_min = h * 60 + m + lisa_min + s / 60; // lisätty s / 60 s = s % 60; // lisätty h = yht_min / 60; m = yht_min % 60; } void cAika::tulosta(int tulsek) const // lisätty int tulsek { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m; if ( tulsek ) cout << ":" << setw(2) << s; // lisätty cout << endl; } int main(void) { cAika a1, a2(13), a3(14,175); // ei muutoksia! a1.tulosta(); a2.tulosta(); a3.tulosta(); a1.aseta(12,15); a2.aseta(16,-15); a1.tulosta(); a2.tulosta(); cAika a4(12,55,45); a4.tulosta(1); // lisätty uusi a4.lisaa(3,30); a4.tulosta(1); return 0; }Huh huh! Tulipa paljon muutoksia, mutta onnistuimme kuitenkin pitämään alkuperäisen pääohjelman koodin muuttumattomana. Tulostuksessa olisi tietysti voitu valita sekin linja, että aina tulostetaan sekunnit tai sekuntien tulostus on oletuksena. Kumpikin valinta olisi aiheuttanut olemassa olevan koodin toiminnan muuttumisen. Jos näin olisi haluttu, niin sitten olisi valittu niin.
#include <iostream.h> #include <iomanip.h> class cAika { ... kuten aikacla5.cpp, mutta seuraava muutos: void tulosta(int lf=1) const { cout << setfill('0') << setw(2) << h << ":" << setw(2) << m; if ( lf ) cout << endl; } }; class cAikaSek { cAika hm; int s; public: void aseta(int ih=0, int im=0, int is=0) { s = is; hm.aseta(ih,im); lisaa(0); } cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); } void lisaa(int lisa_min, int lisa_sek=0) { s += lisa_sek; hm.lisaa(lisa_min+s/60); s %= 60; } void tulosta(int lf=1) const { hm.tulosta(0); cout << ":" << setw(2) << s; if ( lf ) cout << endl; } }; int main(void) { cAika a1, a2(13), a3(14,175); a1.tulosta(); a2.tulosta(); a3.tulosta(); a1.aseta(12,15); a2.aseta(16,-15); a1.tulosta(); a2.tulosta(); cAikaSek a4(12,55,45); a4.tulosta(1); // lisätty uusi a4.lisaa(3,30); a4.tulosta(1); return 0; }Valitettavasti emme aivan täysin onnistuneet. Nimittäin alkuperäisen luokan tulosta oli niin tyhmästi toteutettu, että se tulosti aina rivinvaihdon. Tämmöistä hölmöyttä ei pitäisi mennä koskaan tekemään ja siksipä alkuperäinen luokka pitää joka tapauksessa palauttaa valmistajalle remonttiin. No luokan valmistaja muutti tulosta- metodia siten, että se oletuksena tulostaa rivinvaihdon (merk. lf = line feed), mutta pyydettäessä jättää sen tekemättä. Näin vanha koodi voidaan käyttää muuttamattomana.
Palaamme tulostusongelmaan myöhemmin ja keksimme silloin paremman ratkaisun, jota olisi pitänyt käyttää jo alunperin.
Luokassa on niin vähän ominaisuuksia, että uudessa luokassamme olemme joutunee itse asiassa tekemään kaiken uudelleen ja on kyseenalaista olemmeko hyötyneet vanhasta luokasta lainkaan. Tämä on onneksi lyhyen esimerkkimme vika, todellisilla luokilla säästö kokonaan uudestaan kirjoitettuun verrattuna olisi moninkertainen.
Jos voi sanoa että lapsiluokka on (is-a) isäluokka, niin peritään. Jos sanotaan että lapsiluokassa on (has-a) isäluokka, niin koostetaanKokeillaanpa: "luokka jossa on aika sekunteina" on "aika- luokka". Kuulostaa hyvältä. Siis perimään (taas inline- muoto tilan säästämiseksi):
... kuten aikacla7.cpp class cAikaSek : public cAika { int s; public: void aseta(int ih=0, int im=0, int is=0) { s = is; cAika::aseta(ih,im); lisaa(0); } cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); } void lisaa(int lisa_min, int lisa_sek=0) { s += lisa_sek; cAika::lisaa(lisa_min+s/60); s %= 60; } void tulosta(int lf=1) const { cAika::tulosta(0); cout << ":" << setw(2) << s; if ( lf ) cout << endl; } }; int main(void) ... kuten aikacla7.cppTässä tapauksessa kirjoittamisen vaiva oli tasan sama kuin koostamisessakin. Joissakin tapauksissa perimällä pääsee todella vähällä. Otamme tästä myöhemmin esimerkkejä, kunhan pääsemme eroon syntaksin esittelystä.
Lapsiluokka, aliluokka (child class, subclass) on se joka perii ja isäluokka, yliluokka (parent class, superclass) se joka peritään. Käytetään myös nimitystä välitön ali/yliluokka, kun on kyseessä perintä suoraan luokalta toiselle, kuten meidän esimerkissämme.
C++:ssa välitön yliluokka ilmoitetaan aliluokan esittelyssä:
class cAikaSek : public cAika {Jos täytyy viitata yliluokan metodeihin, joille on kirjoitettu aliluokassa oma määrittely, käytetään näkyvyysoperaattoria :: (scope resolution operator):
cAika::lisaa(lisa_min+s/60);Näkyvyysoperaattoria EI käytetä, mikäli samannimistä metodia ei ole aliluokassa.
Perintää kuvataan piirroksessa:
Lisätäänpä vielä testiohjelman loppuun:
cAika a1; ... a1.aseta(12,15); cAikaSek a4(12,55,45); a4.lisaa(3,30); pAika = &a1; pAika->tulosta(); pAika = &a4; pAika->tulosta();Tulostus:
12:15 12:59Mistä tässä oli kyse? Osoitin pAika oli monimuotoinen, eli sama osoitin osoitti kahteen eri tyyppiseen luokkaan. Tämä on mahdollista, jos luokat ovat samasta perimähierarkiasta kuten tässä tapauksessa ja osoitin on tyypiltään näiden yhteisen kantaluokan olion osoitin.
Mutta hei! Eikös jälkimmäinen tulostus olisi pitänyt olla sekuntien kanssa, olihan jälkimmäinen tulostettava luokka "aikaluokka jossa sekunnit"? Totta! Taas on alkuperäisen luokan tekijä möhlinyt eikä ole lainkaan ajatellut että joku voisi periä hänen luokkaansa. Tässä syy, miksi otamme perinnän mukaan tässä vaiheessa opintoja. Vaikka jatkossa emme hirveästi perinnän varaan rakennakaan, emme myöskään saa tehdä luokkia, jotka olisivat "väärin" tehtyjä!
Myöhäinen sidonta saadaan C++:ssa aikaan liittämällä virtual- avainsana metodien eteen. Kaikki ne metodit täytyy ilmoittaa myöhäiseen sidontaan, joita mahdolliset perilliset tulevat muuttamaan. Siis luokat vielä kerran remonttiin:
#include <iostream.h> #include <iomanip.h> class cAika { int h,m; public: virtual void aseta(int ih,int im=0) { // lisätty virtual h = ih; m = im; lisaa(0); } cAika(int ih=0, int im=0) { aseta(ih,im); } virtual void lisaa(int lisa_min) { // lisätty virtual int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } virtual void tulosta(int lf=1) const { // lisätty virtual cout << setfill('0') << setw(2) << h << ":" << setw(2) << m; if ( lf ) cout << endl; } }; class cAikaSek : public cAika { int s; public: virtual void aseta(int ih=0, int im=0) { cAika::aseta(ih,im); } // lisätty virtual void aseta(int ih, int im, int is) { // lisätty virtual s = is; aseta(ih,im); lisaa(0,0); // pois cAika:: } cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); } virtual void lisaa(int lisa_min) { cAika::lisaa(lisa_min); } // lisätty virtual void lisaa(int lisa_min, int lisa_sek) { // lisätty virtual s += lisa_sek; lisaa(lisa_min+s/60); s %= 60; // pois cAika:: } virtual void tulosta(int lf=1) const { // lisätty virtual cAika::tulosta(0); cout << ":" << setw(2) << s; if ( lf ) cout << endl; } }; int main(void) .. kuten aikacla8.cpp + cAika *pAika; ...Virtual sanat on lisätty kaikkien metodien eteen. Aliluokassa virtual on tarpeeton uudelleenmääriteltävien (korvaaminen, syrjäyttäminen, overriding) metodien kohdalle, mutta se on hyvä pitää siellä kommenttimielessä.
Sitten tuleekin hankalammin selitettävä asia. Miksi yksi- parametrinen lisaa ja kaksi- parametrinen aseta on täytynyt kirjoittaa uudelleen? Tämä johtuu kielen ominaisuuksista, sillä muuten vastaavat useampi- parametriset metodit syrjäyttäisivät alkuperäiset metodit ja alkuperäisiä ei olisi lainkaan käytössä. Tässä tapauksessa olisimmekin voineet antaa niiden syrjäytyä, mutta koska tulevaisuudesta ei koskaan tiedä, on alkuperäiselläkin parametrimäärällä olevien metodien ehkä hyvä säilyä.
Parempi ratkaisu olisi ehkä kuitenkin ollut, jos jo alkuperäisessä luokassa olisi varauduttu sekuntien tuloon, vaikka niitä ei olisi mitenkään otettukaan huomioon:
... class cAika { // Muutokset aikacla8.cpp:hen verrattuna int h,m; public: virtual void aseta(int ih=0,int im=0, int lisa_sek=0) { // virtual, lisa_sek h = ih; m = im; lisaa(0); } cAika(int ih=0, int im=0) { aseta(ih,im); } virtual void lisaa(int lisa_min, int lisa_sek=0) { // virtual, lisa_sek int yht_min = h * 60 + m + lisa_min; h = yht_min / 60; m = yht_min % 60; } virtual void tulosta(int lf=1) const { ... } // virtual }; class cAikaSek : public cAika { int s; public: virtual void aseta(int ih=0, int im=0, int is=0) { // virtual s = is; cAika::aseta(ih,im); lisaa(0); // järj. vaihd. } cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); } virtual void lisaa(int lisa_min, int lisa_sek=0) { // virtual s += lisa_sek; cAika::lisaa(lisa_min+s/60); s %= 60; } virtual void tulosta(int lf=1) const { // virtual }; ... int main(void) { ... cAika *pAika; pAika = &a1; pAika->tulosta(); pAika = &a4; pAika->tulosta(); return 0; }
Varoitus: Borlandin C++5.1:en optimoivalla kääntäjällä käännettynä em. koodi kaatoi kääntäjän ympäristöineen jos cAikaSek::aseta oli muodossa:
cAika::aseta(ih,im); s = is; lisaa(0);
cAikaSek a4(12,55,45);kutsuu kaikkiaan seuraavia metodeja:
cAikaSek::cAikaSek(12,55,45); cAika::cAika(0,0); // Oletusalustus yliluokalle cAika::aseta(0,0,0); cAika::lisaa(0,0); // Tässä cAika, koska ei vielä "kasvanut" cAikaSek cAikaSek::aseta(12,55,45); cAika::aseta(12,55); cAikaSek::lisaa(0,0); // HUOM! Todellakin cAikaSek::lisaa virt.takia cAika::lisaa(0,0); cAikaSek::lisaa(0,0); cAika::lisaa(0,0);Olisitko arvannut! Enää ei tarvitse ihmetellä miksi olio- ohjelmat ovat isoja ja hitaita. Totta kai voitaisiin sanoa, että hyvän kääntäjän olisi pitänyt huomata tuosta optimoida päällekkäisyydet pois. Mutta tämä on vielä tänä päivänä kova vaatimus kääntäjälle. Mutta ehkäpä voisimme ohjelmoijina vähän auttaa:
... class cAika { int h,m; public: virtual void aseta(int ih=0,int im=0, int is=0) { ... } cAika() { h = 0; m = 0; } // tai cAika() : h(0), m(0) {} // Oletusmuodostaja cAika(int ih, int im=0) { aseta(ih,im); } // ih=0 oletus pois virtual void lisaa(int lisa_min, int lisa_sek=0) { ... } virtual void tulosta(int lf=1) const { ... } }; class cAikaSek : public cAika { int s; public: virtual void aseta(int ih=0, int im=0, int is=0) {...} cAikaSek() : s(0), cAika() {} // Oletusmuodostaja cAikaSek(int ih, int im=0, int is=0) : cAika(ih,im), s(is) { lisaa(0); } }; ...Nyt on saatu ainakin seuraavat edut: oletustapauksessa kumpikin luokka alustuu pelkillä 0:ien sijoituksilla, ilman yhtään ylimääräistä kutsua. Oletusmuodostaja luokalla cAika voidaan esitellä esim. seuraavilla tavoilla, joista keskimmäistä voidaan pitää parhaana:
cAika() { h = 0; m = 0; } // Normaalit sijoitukset cAika() : h(0), m(0) {} // h:n muodostaja arvolla 0, eli h=0 kokonaisluvulle // m:n muodostaja arvolla 0, eli m=0 cAika() : h(0) { m=0; }Vastaavasti luokalle cAikaSek pitää oletusmuodostaja tehdä seuraavanlaiseksi:
cAikaSek() : s(0), cAika() { } // s alusetetaan 0:ksi ja peritty yliluokka // alustetaan muodostajalla cAika() cAikaSek() { s = 0; } // TÄMÄ on TÄSSÄ tapauksessa sama kuin // edellinen, koska jos yliluokan muodostajaa // ei itse kutsuta, kutsutaan // oletusmuodostajaaELI! Perityn yliluokan muodostajaa kutsutaan automaattisesti, jollei sitä itse tehdä. Nyt parametrillisessa muodostajassa kutsutaan yliluokan muodostajaa, ja näin voidaan välttää ainakin oletusmuodostajan kutsu:
cAikaSek(int ih, int im=0, int is=0) : cAika(ih,im), s(is) { lisaa(0); }Nyt alustuksesta
cAikaSek a4(12,55,45);seuraa seuraavat metodikutsut
cAikaSek::cAikaSek(12,55,45); cAika::cAika(12,55); // NYT EI oletusalustusta yliluokalle cAika::aseta(12,55,0); cAika::lisaa(0,0); cAikaSek::lisaa(0,0); cAika::lisaa(0,0);Turhaahan tuossa on vieläkin, mutta tippuihan kuitenkin noin puolet pois! Joka tapauksessa periminen tuottaa jonkin verran koodia, aina kun yliluokan metodeja käytetään hyväksi. Ja jollei käytettäisi, kirjoitettaisiin samaa koodi uudelleen, eli palattaisiin 70- luvulle. Nykyisin kasvanut koneteho kompensoi kyllä tehottomamman oliokoodin ja olio- ohjelmoinnin ansiosta pystytään kirjoittamaan luotettavampia (?) ja monimutkaisempia ohjelmia.
"Lopullinen" versio aikaluokastamme voisikin siis olla seuraava:
class cAika { int h,m; public: virtual void aseta(int ih=0,int im=0, int is=0) {...} ... virtual void tulosta(int lf=1) const { ... } int hh() const { return h; } // saantimetodi int mm() const { return m; } // saantimetodi };Huomattakoon nyt, että perinnässä ei tarvitse määritellä uudestaan saantifunktioita hh() ja mm(), ainoastaan uudet, eli esimerkissämme ss().
Nyt voitaisiin esimerkiksi kutsua:
cout << a1.hh() << ":" << pAika->mm() << ":" << a4.ss() << endl;Mikä tässä sitten on erona attribuuttien julkaisemiseen verrattuna? Se että attribuutit ovat nyt tietyssä mielessä vain luettavissa (read-only), eli niitä voi lukea saantimetodien avuilla, mutta niitä voi asettaa vain aseta- metodin avulla, joka taas pystyy suorittamaan oikeellisuustarkistukset ja näin olion tila ei koskaan pääse muuttumaan olion itsensä siitä tietämättä.
Saantimetodit kannattaa ilman muuta kirjoittaa inline, sillä niistä kääntäjä voi sitten kääntää nopeaa koodia, eikä saantibyrokratiasta tule yhtään kustannuksia suoraan attribuuttiin viittaamiseen verrattuna.
Kannattaakin harkita, nimeäisikö attribuutit uudelleen: ah,am,as, jotta nimet h,m,s jäisi vapaaksi saantimetodeille! Toisaalta tärkeimmistä attribuuteista voisi tehdä myös pitkillä nimillä varustetut saantimetodit: esimerkiksi tunnit(), minuutit() ja sekunnit(). Rakkaalla lapsella voi olla montakin nimeä ja inlinen ansiosta tämähän ei "maksa mitään".
... class cAika { int yht_min; public: virtual void aseta(int ih=0,int im=0, int is=0) { yht_min = 0; lisaa(60*ih+im); } cAika(int ih=0, int im=0) { aseta(ih,im); } virtual void lisaa(int lisa_min, int lisa_sek=0) { yht_min += lisa_min; } virtual void tulosta(int lf=1) const { cout << setfill('0') << setw(2) << hh() << ":" << setw(2) << mm(); if ( lf ) cout << endl; } int hh() const { return yht_min / 60; } int mm() const { return yht_min % 60; } }; ...kaikki muu kuten aikaclab.cpp ...
Paremmalla suunnittelulla luokasta olisi heti voinut tulla yleiskäyttöisempi. Usein jopa joudutaan tekemään kahden luokan yläpuolelle abstrakti, tai muuten vaan yhteinen yliluokka, josta hieman toisistaan poikkeavat luokat peritään. Kuljimme tämän pitkän tien sen vuoksi, että lukija oppisi ymmärtämään miksi valmiit luokat eivät ole parin rivin koodinpätkiä.
Tulevaisuudessa ohjelmoijat jakaantunevatkin selvästi kahteen ryhmään: toiset käyttävät valmiita luokkia (mikä on helppoa, jos luokat ovat kunnossa, vrt. Delphi tai Visual Basic, ehkä myös osittain Java ja Jonnen pyynnöstä mainitaan tietysti Python). Ammattitaitoisempi ryhmä sitten suunnittelee ja tekee näitä yleiskäyttöisiä luokkia. Sitä mukaa kun luokkia saadaan valmiiksi eri "elämän aloille", siirtyy ammattilaiset yhä spesifimmille aloille.
Esimerkiksi M$:in Windowsin ohjelmointiin tarkoitetut luokkakirjastot ovat paisuneet niin suuriksi, että niiden käyttämistä tuskin kukaan enää hallitsee, ja ennenkuin entisen kirjaston on ehtinyt edes auttavasti oppia, tuleekin uusi versio. Tästä tulee oravanpyörä, jossa voi olla kova homma pysyä mukana, jollei ohjelmateollisuus keksi jotakin uutta ja mullistavaa avuksi.
Joka tapauksessa haave siitä, että näkee näyn ja keksii hyvän luokan, jota muut sitten yhtään muuttamatta voivat käyttää hyväkseen, kannattaa heittää Ylistönrinteen sillan alle. Mieluummin kannattaa alistua siihen, että opettelee käyttämään hyviä luokkia ja imee niitä käyttäessään ideoita siitä, miten parantaa omia luokkiaan seuraavalla kerralla.
C- kielen char jono[10] on todellinen aikapommi, jonka aukkoisuuteen perustuu vielä tänäkin päivänä useat hakkereiden kikat murtautua vieraisiin tietojärjestelmiin. Katsotaanpa ensin mitä C- merkkijonoille voi/ei voi tehdä:
char s1[10],s2[5],*p; p = "Kana" // Toimii! p[0] = 'S'; // Toimii! Mutta jatkossa käy huonosti... // Männikön monisteessakin on väärin tämän käytöstä ... s1 = "Kissa"; // ei toimi! strcpy(s2,"Koira"); // Huonosti käy! Miksi? Älä käytä koskaan... if ( s1 < s2 ) ... // Sallittu, mutta tekee eri asian kuin lukija arvaakaan... gets(s1); // Itsemurha, tämä on eräs kaikkein hirveimmistä funktioista // lukee päätteeltä rajattomasti merkkejä ... fgets(s1,sizeof(s1),stdin); // Oikein! Tosin rivinvaihto jää jonoon jos syöte // on lyhyempi kuin printf(s1); // Ohohoh! Tämä jopa toimii!!! cout << s1; // Ja jopa tämäkin!!! cin >> s1; // Taas itsemurha ....Palaamme myöhemmin monisteessa C:n merkkijonoihin, ja siihen miten niitä voidaan kohtuullisen turvallisesti käyttää.
Onneksi C++:aan on tulossa kohtuullinen merkkijonoluokka. Nyt jo! Yli 10 vuotta kielen kehittämisen jälkeen...
Merkkijonoluokalla voi tehdä mm. seuraavia:
#include <iostream.h> #include <string> using namespace std; int main(void) { string mjono1 = "Ensimmainen"; string mjono2 = "Toinen"; string mjono; // syotto ja tulostus cout << "Anna merkkijono > "; getline(cin,mjono,'\n'); cout << "Annoit merkkijonon : " << mjono << endl; // kasittely merkeittain mjono[0] = mjono1[4]; mjono[1] = mjono2[1]; mjono[2] = 't'; mjono[3] = '\0'; // Laitonta jos merkkijono ei ole nain iso! // Ei vaikuta! cout << mjono << endl; // tulostaa: mot ...+jotakin // sijoitukset mjono = mjono1; cout << mjono << endl; // Ensimmainen mjono = "Eka"; cout << mjono << endl; // Eka // katenointi mjono = mjono1 + mjono2; cout << mjono << endl; // EnsimmainenToinen mjono = mjono1 + "Toka"; cout << mjono << endl; // EnsimmainenToka mjono = "Eka" + mjono2; cout << mjono << endl; // EkaToinen // vertailut if (mjono1 == mjono2) cout << "1 on 2" << endl; // ei tulosta if (mjono1 == "Apua") cout << "1 on Apua" << endl; // ei tulosta if ("Apua" == mjono2) cout << "Apua on 2" << endl; // ei tulosta if (mjono1 != mjono2) cout << "1 ei ole 2" << endl; // 1 ei ole 2 if (mjono1 != "Apua") cout << "1 ei ole Apua" << endl; // 1 ei ole Apua if ("Apua" != mjono2) cout << "Apua ei ole 2" << endl; // Apua ei ole 2 if (mjono1 < mjono2) cout << "1 pienempi kuin 2" << endl; // 1 pienempi ku if (mjono1 < "Apua") cout << "1 pienempi kuin Apua" << endl; // ei tulosta if ("Apua" < mjono2) cout << "Apua pienempi kuin 2" << endl; // Apua pienempi if (mjono1 > mjono2) cout << "1 suurempi kuin 2" << endl; // ei tulosta if (mjono1 > "Apua") cout << "1 suurempi kuin Apua" << endl; // 1 suurempi ku if ("Apua" > mjono2) cout << "Apua suurempi kuin 2" << endl; // ei tulosta // nama eivat valttamattomia, mutta jollekin voi tulla mieleen kayttaa... if (mjono1 <= mjono2) cout << "1 pienempi tai yhtasuuri kuin 2" << endl; if (mjono1 >= mjono2) cout << "1 suurempi tai yhtasuuri kuin 2" << endl; // Vastaavat vakiomerkkijonoilla EI onnistu, koska vakiomerkkijonot ovat // OSOITTIMIA! #if 0 // Seuraavat jos linkittaa: mjonot.c ja string.cpp mjono1.remove(4,2); cout << mjono1 << endl; // Ensiainen mjono1.insert(4,"mm"); cout << mjono1 << endl; // Ensimmainen #endif return 0; }Luokan string pitäisi tulla nykyisten C++- kääntäjien mukana. Se saadaan käyttöön lisäämällä koodin alkuun:
#include <string> // Otetaan standardiehdotelman merkkijonoluokka using namespace std; // nyt ei tarvitse kirjoittaa std::stringJos koneessa on niin vanha kääntäjä, ettei uudet luokat käänny sillä (ei esim. poikkeutusten käsittelyä), voi käyttää \kurssit\c\ali hakemistosta löytyvää "lasten" vastinetta, string, eli em. testiohjelma toimii jos otsikkotiedostojen hakupolkuun lisätään mainittu ali- hakemisto.
// Taydenna ja korjaile. Mista puuttuu virtual? Mista const? // Mita metodeja viela puuttuu? #include <iostream.h> #include <string> using namespace std; class cHenkilo { string nimi; int ika; double pituus_m; public: cHenkilo(string inimi="???",int iika=0,int ipituus=0) {} void tulosta() {} void kasvata(double cm) {} }; class cOpiskelija : public cHenkilo { double keskiarvo; public: cOpiskelija(string inimi="???",int iika=0, int ipituus=0, int ikarvo=0.0) : cHenkilo(inimi,iika,ipituus), keskiarvo(ikarvo) {} }; int main(void) { cHenkilo Kalle("Kalle",35,1.75); Kalle.tulosta(); Kalle.kasvata(2.3); Kalle.tulosta(); cOpiskelija Ville("Ville",21,1.80,9.9); Ville.tulosta(); return 0; }