previous next Title Contents Index

9. Kohti olio- ohjelmointia


Mitä tässä luvussa käsitellään?

* tietueet

* yksinkertaiset luokat

* olioiden perusteet

* olioterminologia

* koostaminen

* perintä

* polymorfismi

Syntaksi:

	luokan esittely:  class cNimi : public cIsa, public cIsa { // 0-n x public cIsa 
	                  private:                              // päällä oletuksena
	                     yksityiset_attribuutit             // vain itse näkee
	                     yksityiset_metodit
	                  protected: 
	                     suojatut_attribuutit               // perilliset näkee
	                     suojatut_metodit
	                  public:
	                     julkiset_attribuutit               // kaikki näkee
	                     julkiset_metodit
	                  };                                    // HUOM! puolipiste
	attr              kuten muuttuja
	attrib.esitt.     tyyppi attr;
	metodin esitt.    kuten aliohjelman esittely
	metodin esitt. 
	luokan ulkop.     tyyppi cNimi::metodin_nimi(param_lista)
	viit.olion metod: olio.metodin_nimi(param,param)        // 0-n x param
	jos osoitin:      pOlio->metodin_nimi(param,param)
	yliluokan 
	metodiin viit.    yliluokka::metodin_nimi(param,param)
	muodostaja        cNimi(param_lista)       // voi olla monta eri param_listoilla
	hajoittaja        ~cNimi
Tähän lukuun on kasattu suuri osa olioihin liittyvää asiaa yhden esimerkin valossa. Esimerkin yksinkertaisuuden takia se ei anna joka tilanteessa täyttä hyötyä esitetyistä ominaisuuksista. Lisäksi asiaa voi olla yhdelle lukukerralla liikaa ja esimerkiksi perintä ja polymorfismi kannattaa ehkä jättää myöhemmälle lukukerralle.

9.1 Miksi olioita tarvitaan

Emme tässä ryhdy pohtimaan kovin syvällisiä siitä, miten olio- ohjelmointiin on päädytty. Todettakoon kuitenkin että olio- ohjelmointi on hyvin luonnollinen jatke rakenteelliselle ohjelmoinnille heti, kun huomataan siirtää käsiteltävä data ja dataa käsittelevä koodi samaan ohjelman osaan. Tämä toimenpide voidaan tehdä tietenkin myös perinteisillä ohjelmointikielilläkin. Puhtaat oliokielet eivät vaan jätä edes muuta mahdollisuutta. Lähestymme asiaa evoluutiomaisesti - niinkuin kehitys on olioihin johtanut. Loput ylilaulut olioista kannattaa lukea jostakin hyvästä kirjasta.

Aloitetaanpa tutkimalla aikalisa esimerkkiämme. Pääohjelmassa esiteltiin muuttuja tunteja varten ja muuttuja minuutteja varten. Aluksi tämä saattaa tuntua hyvin luonnolliselta ja niin se onkin, niin kauan kuin ohjelman koko pysyy pienenä. Entäpä ohjelma jossa tarvitaan paljon kellonaikoja?

olioalk\aikalis4.cpp - useita aika "muuttujia"

	... alku kuten aikalis3.cpp
	int main(void) 	?
	{
	  int h1=12,m1=15;
	  int h2=13,m2=16;
	  int h3=14,m3=25;
	  lisaa(h1,m1,55);
	  tulosta(h1,m1);
	  lisaa(h2,m2,27);
	  tulosta(h2,m2);
	  lisaa(h3,m3,39);
	  tulosta(h3,m3);
	  return 0;
	}
Hyvinhän tuo vielä toimii? Ja jos otettaisiin taulukot käyttöön, ei tarvitsisi edes numeroida muuttujia. Entäpä jos joku tulee ja sanoo, että sekunnitkin mukaan! Tulee paljon työtä jos on paljon aikoja.

Tehtävä 9.77 Tulostus

Mitä ohjelma aikalis4.cpp tulostaa?

9.2 Tietueet, välivaihe kohti olioita

Jo pitkään ovat ohjelmointikielet tarjonneet erään apuvälineen tietojen yhdistämiseen: tietueet. Kasataan asiaan liittyvät muuttujat yhteen "joukkoon" ja annetaan tälle "joukolle" uusi nimi, meidän esimerkissämme vaikkapa unkarilaisella nimeämisellä tAika - aika tyyppi:

olioalk\aikalist.cpp - yhteiset asiat samaan tietueeseen

	#include <iostream.h>
	#include <iomanip.h>
	
	struct tAika {
	  int h,m;
	};
	
	void lisaa(tAika &rAika, int lisa_min)
	{
	  int yht_min = rAika.h * 60 + rAika.m + lisa_min;
	  rAika.h = yht_min / 60;
	  rAika.m = yht_min % 60;
	}
	
	void tulosta(tAika Aika)
	{ // Muotoilu jotta 15:04 tulostuisi oikein eikä 15:4
	  cout << setfill('0') << setw(2) << Aika.h << ":" << setw(2) << Aika.m << endl;
	}
	
	int main(void)
	{
	  tAika a1={12,15}, a2={13,16}, a3={14,25};
	  lisaa(a1,55);   tulosta(a1);
	  lisaa(a2,27);   tulosta(a2);
	  lisaa(a3,39);   tulosta(a3);
	  return 0;
	}

9.2.1 Tietuemuuttujan esittely

Eli muutos ohjelmassa on varsin syntaktinen. Siinä missä ennen esittelimme 2 muuttuja, h1 ja m1, esitellään nyt vain yksi muuttaja a1 joka onkin aika- tyyppiä.
	tAika a1;

9.2.2 Tietueen alustaminen esittelyn yhteydessä

Jos tietuetyyppinen muuttuja halutaan alustaa esittelyn yhteydessä, voidaan se tehdä (ja vain esittelyn yhteydessä):
	tAika a1={12,15};

9.2.3 Uuden tietuetyypin määrittely

Uusi tietuetyyppinen tyyppi määritellään
	struct tAika {  // struct ja tyypin nimi
	  int h,m;      // alkioiden tyypit ja nimet
	};
	
	// voi olla myös
	struct tAika {  
	  int h;
	  int m;
	};

Huom! C- kielessä määrittely täytyy tehdä typedef - lauseella:

	typedef struct { 
	  int h,m; 
	} tAika;

Muuten kaikki tässä sanottu pätee myös C- kieleen. C++:ssakin voitaisiin tietue määritellä em. tavalla, yhteensopivuussyistä ehkä jopa kannattaisi?

9.2.4 Viittaus tietueen alkioon

Jos ohjelmassa tarvitsee viitata yksittäiseen tietueen alkioon, voidaan tämä tehdä ilmoittamalla tietuen nimi, piste ja tietueen alkio:
	minuutit = a1.m;

9.2.5 Viittaus osoitteen avulla

Mikäli lisaa- aliohjelma olisi kirjoitettu C:mäisesti, pitäisi muistaa "tähdätä" osoitinta pitkin:

olioalk\aikalisp.cpp - osoittimen avulla viittaaminen tietueeseen

	void lisaa(tAika *pAika, int lisa_min)
	{
	  int yht_min = (*pAika).h * 60 + (*pAika).m + lisa_min;
	  (*pAika).h = yht_min / 60;
	  (*pAika).m = yht_min % 60;
	}
	...
	  lisaa(&a1,55);

9.2.6 Lyhennetty viittaus osoitteen avulla (->)

Muoto (*pAika).m on aika pitkähkö kirjoittaa. Siksi sille onkin synonyymi pAika->m, joka on lisäksi varsin havainnollinen; tähdätään osoitinta pitkin tietueen tiettyyn alkioon :
	void lisaa(tAika *pAika, int lisa_min)
	{
	  int yht_min = pAika->h * 60 + pAika->m + lisa_min;
	  pAika->h = yht_min / 60;
	  pAika->m = yht_min % 60;
	}
Muista että tämä osoitemerkintä toimii VAIN silloin kun kyseessä on osoitin muuttuja. Tavallisen tietuemuuttujan tapauksessa viitataan alkioon pisteellä.

9.2.7 Tietueen muuttaminen?

Joko nyt on helpompi lisätä sekunnit, niin ettei esimerkiksi pääohjelmassa tarvitse tehdä muutoksia? Osittain, sillä jos tietueen viimeiseksi lisättäisiin int s, niin alustus
	tAika a1={12,15};
jättäisi sekunnit alkuarvoonsa, joka tässä tapauksessa on valitettavasti tuntematon! Muuten riittää muuttaa pelkkiä aliohjelmia!

Suurin hyöty tietueesta olikin lähinnä kasata yhteen kuuluvat asiat saman nimikkeen alle. Tämä on kapselointia (encapsulation) väljässä mielessä.

Tehtävä 9.78 Päivämäärätyyppi

Esittele tietue, jolla kuvataan päivämäärä. Kirjoita aliohjelma tulosta, joka tulostaa päivämäärän.

9.2.8 Funktioiden kuormittaminen (lisämäärittely, overloading)

Edellisessä tehtävässä pyydettiin kirjoittamaan aliohjelma tulosta, joka tulostaa päivämäärätyyppisen muuttujan arvon. Onko tämä järkevää, koska meillä jo oli aliohjelma tulosta, joka tulostaa kellonajan? Eräs C++:n uusia ominaisuuksia on mahdollisuus kuormittaa, eli määritellä lisää merkityksiä (eng. overloading) funktion nimelle. Varsinainen kutsuttava funktio tunnistetaan nimen ja parametrilistassa olevien lausekkeiden avulla. Funktion nimi koostuukin tavallaan nimen ja parametrilistan yhdisteestä. Siten jos on esitelty
	tAika aika={12,30};
	tPvm pvm={14,1,1997};
	tulosta(aika);
	tulosta(pvm);
niin kumpikin tulosta- kutsu kutsuu eri aliohjelmaa. Funktioiden kuormitus onkin varsin mukava lisä ohjelmointiin, se ei kuitenkaan ole varsinaisia olio- ohjelmoinnin piirteitä.

Tehtävä 9.79 Mitäs me tehtiin kun ei ollut kuormitusta?

Miten asiat on hoidettava C- kielessä, kun siellä funktioiden nimien kuormitus ei ole mahdollista, vaan kunkin funktion nimen tulee olla yksikäsitteinen.

9.3 Hynttyyt yhteen, eli muututaan olioksi

Itse asiassa vanhalla C- kielelläkin pystyi kirjoittamaan "olioita", kirjoittamalla tietuetyypin esittely ja sitä käyttävät aliohjelmat yhdeksi aliohjelmakirjastoksi. Näin data ja sitä käsittelevät aliohjelmat on kapseloitu yhdeksi paketiksi.

9.3.1 Terminologiaa

Asia voidaan viedä vielä hieman pitemmälle. Kasataankin käsittelevät aliohjelmat suoraan tietueen sisälle. Nyt astuu kuvan mukaan olio- ohjelmoijat ja he nimittävät sitten näin syntyneitä aliohjelmia metodeiksi (method), tai C++- kirjallisuudessa jäsenfunktioiksi (member function). Tietueen alkioita, kenttiä nimitetään sitten attribuuteiksi.

Itse tietue saakin nimen luokka (class) ja tietuetta vastaava muuttuja - luokan ilmentymä - on sitten se kuuluisa olio (object)

9.3.2 Ensimmäinen olio- esimerkki

Muutetaanpa aikalisa luokaksi ja olioksi:

olioalk\aikaolio.cpp - tietueesta olioksi

	#include <iostream.h>
	#include <iomanip.h>
	
	struct cAika {
	  int h,m;
	  void lisaa(int lisa_min)  {
	    int yht_min = h * 60 + m + lisa_min;
	    h = yht_min / 60;
	    m = yht_min % 60;
	  }
	  void tulosta() const  {
	    cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
	  }
	};
	
	
	int main(void)
	{
	  cAika a1={12,15}, a2={13,16}, a3={14,25};
	  a1.lisaa(55);  a1.tulosta();
	  a2.lisaa(27);  a2.tulosta();
	  a3.lisaa(39);  a3.tulosta();
	  return 0;
	}
Siinäpä se! Onko muutokset edelliseen nähden suuria? Siis iso osa koko olio- ohjelmoinnista (ja tietotekniikasta muutenkin) on markkinahenkilöiden huuhaata ja yleistä hysteriaa "kaiken ratkaisevan" teknologian ympärillä. No, tosin olio- 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.

Tehtävä 9.80 const-metodi

Kokeile mitä kääntäjä ilmoittaa, jos muutat myös lisaa- metodin const- tyyppiseksi.

9.3.3 Taas terminologiaa

Kerrataanpa vielä termit edellisen esimerkin avulla: Vinkki

Älä hämäänny termeistä

	                oliotermi                                perinteinen termi
	-----------------------------------------------------------------------------
	cAika         - aika-luokka                                tietuetyyppi
	h,m           - aika-luokan attribuutteja                  tietueen alkio
	lisaa,tulosta - aika-luokan metodeja                       funktio,aliohj.
	a1,a2,a3      - olioita, jotka ovat aika-luokan ilmentymiä muuttuja
	a1.lisaa(55)  - viesti olioille a1: lisää 55 minuuttia     aliohjelman kutsu

9.3.4 Luokka (class) ja kapselointi

Virallisesti luokat esitellään C++:ssa class- avainsanalla struct- avainsanan sijasta. Mikä ero sitten class- avainsanalla on? Kumpikin käy, mutta attribuuteille ja metodeille on suojaustasot, jotka oletuksena struct- määritellyssä luokassa kaikki ovat julkisia, eli metodeja voi kutsua kuka tahansa ja erityisesti kuka tahansa voi muuttaa attribuuttien arvoja ilman että olio tätä itse huomaa.

Kuka voi käyttää metodia/attribuuttia




Oletuksena

Suojaus
kaikki
aliluokat
friend- funktiot
luokan metodit
struct
union
class
private


x
x

x
protected

x
x
x


public
x
x
x
x
x

Kuva 9.1 Suojaustasot
Jos esimerkkimme luokka esiteltäisiin:
	class cAika {
	  int h,m;
	  void lisaa(int lisa_min)  {...}
	  void tulosta() const      {...}
	};
lakkaisi ohjelmamme toimimasta, koska esimerkiksi pääohjelman kutsu
	a1.lisaa(55)
tulisi laittomaksi kaikkien luokan jäsenten ollessa yksityisiä (private). Ongelmaa voisi yrittää korjata esittelemällä metodit julkisiksi:
	class cAika {
	  int h,m;
	public:
	  void lisaa(int lisa_min)  {...}
	  void tulosta() const      {...}
	};
Nyt kääntäjä valittaisi esimerkiksi:
	Error:  aikacla.cpp(21,13):Objects of type 'cAika' cannot be initialized with { }
rivistä
	cAika a1={12,15},...
Nyt vasta alkaakin olio- ohjelmoinnin hienoudet! Aloittelijasta saattaa tuntua että mitä turhaa tehdään asioista monimutkaisempaa kun se onkaan! Nimittäin aikaolio.cpp saattoi tuntua kohtuullisen ymmärrettävältä. Mutta todellakin jos olisi ilman muuta sallittua sijoittaa oliolle mitä tahansa arvoja, niin mitä mieltä oltaisiin seuraavasta:
	cAika a1={42,175};
Väärinkäytetyt ja virheelliset arvot muuttujilla on ollut ohjelmoinnin kiusa alusta alkaen. Nyt meillä on mahdollisuus päästä niistä eroon kapseloinnin (jotkut sanovat kotelointi, encapsulation) ansiosta. Eli kaikki arvojen muutokset (eli olio tapauksessa olion tilojen muutokset) voidaan suorittaa kontrolloidusta, vain olion itse siihen suostuessa. Mutta miten sitten alustuksen tapauksessa?

9.3.5 Muodostajat (constructor)

C++:ssa on yksi erityinen metodi: muodostaja (konstruktori, rakentaja, constructor), jota kutsutaan muuttujan syntyessä. Muodostajan tehtävä on alustaa olion tila ja luoda mahdollisesti tarvittavat dynaamiset muistialueet. Näin voidaan järjestää se, että olion tila on aina tunnettu olion syntyessä.

Joissakin oliokielissä konstruktori ilmoitetaan omalla avainsanallaan. C++:ssa muodostaja on metodi, jolla on sama nimi kuin luokalla. Muodostajia voi olla useitakin. Muodostaja on aina tyypitön, siis ei edes void- tyyppiä.

olioalk\aikacla.cpp - muodastaja alustamaan tiedot

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	  int h,m;
	public:
	  cAika(int ih, int im) { h = ih; m = im; }
	  void lisaa(int lisa_min)  {
	    int yht_min = h * 60 + m + lisa_min;
	    h = yht_min / 60;
	    m = yht_min % 60;
	  }
	  void tulosta() const  {
	    cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
	  }
	};
	
	
	int main(void)
	{
	  cAika a1(12,15), a2(13,16), a3(14,25);
	  a1.lisaa(55);    a1.tulosta();
	  a2.lisaa(27);    a2.tulosta();
	  a3.lisaa(39);    a3.tulosta();
	  return 0;
	}
Esimerkissämme muodostaja on esitelty 2-parametriseksi
	cAika(int ih, int im) { h = ih; m = im; }
ja sitä kutsutaan olion esittelyn yhteydessä
	cAika a1(12,15);

9.3.6 Oletusmuodostaja (default constructor)

Nyt ei kuitenkaan voida esitellä oliota ilman alkuarvoa
	cAika aika;
Kääntäjä antaisi esimerkiksi virheilmoituksen:
	Error:  aikacla.cpp(26,14):Could not find a match for 'cAika::cAika()'
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

olioalk\aikacla2.cpp - lisätään oletusmuodostaja

	... 
	class cAika {
	  int h,m;
	public:
	  cAika()                   { h = 0;  m = 0;  }
	  cAika(int ih, int im)     { h = ih; m = im; }
	  void lisaa(int lisa_min)  { ... }
	  void tulosta() const      { ... }
	};
	
	
	int main(void)
	{
	  cAika a1(12,15), a2(13,16), a3(14,25);
	  a1.lisaa(55);    a1.tulosta();
	...
	  cAika aika;
	  aika.tulosta();
	  return 0;
	}
Oletusmuodostajaa kutsutaan hieman epäloogisesti ilman sulkuja
	cAika aika;
eikä
	cAika aika(); 	
kuten muiden parametrittomien funktioiden kutsusta saattaisi päätellä!

9.3.7 Oletusparametrit

C++:ssa on myös ominaisuus antaa funktioiden ja metodien parametreille oletusarvoja oikealta vasemmalle päin. Koska oletusmuodostajaksi riittää se, että muodostajaa voi kutsua ilman parametreja, voitaisiin esitellä myös:

olioalk\aikacla3.cpp - oletusparametrit

	... 
	public:
	  cAika(int ih=0, int im=0) { h = ih; m = im; }
	  void lisaa(int lisa_min)  {
	...
	};
Nyt oliot voitaisiin esitellä millä tahansa seuraavista tavoista; ilman 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:25
Oletusparametri tarkoittaa siis sitä, että mikäli parametrilla ei ole kutsussa arvoa, käytetään funktion/metodin esittelyssä ollutta oletusarvoa.

9.3.8 Sisäinen tilan valvonta

Emme edelleenkään ole ottaneet kantaa siihen, mitä tapahtuu, jos joku yrittää alustaa oliomme mielettömillä arvoilla, esim:
	cAika a1(42,175);
Toisaalta miten joku voisi muuttaa ajan arvoa muutenkin kuin lisaa- metodilla? Teemmekin aluksi metodin aseta, jota kutsuttaisiin
	a1.aseta(12,15); a2.aseta(16);
Nyt pitää kuitenkin päättää mitä tarkoittaa laiton asetus! Jos sovimme että minuuteissa yli 59 arvot ovat aina 59 ja alle 0:n arvot ovat aina 0, voisi aseta- metodi olla kirjoitettu seuraavasti:
	  void aseta(int ih,int im=0) {
	    h = ih; m = im; 
	    if ( m > 59 ) m = 59;
	    if ( m <  0 ) m =  0;
	  }
Jos taas haluaisimme, että ylimääräinen osuus siirtyisi tunteihin, voitaisiinkin tämä tehdä "kierosti" lisaa- metodin avulla

olioalk\aikacla4.cpp - sisäinen tilan valvonta asetuksessa

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	  int h,m;
	public:
	  void aseta(int ih,int im=0) {
	    h = ih; m = im; lisaa(0);
	  }
	  cAika(int ih=0, int im=0) { aseta(ih,im); }
	  void lisaa(int lisa_min)  { ... }
	  void tulosta() const      { ... }
	};
	
	int main(void)
	{
	  cAika a1, a2(13), a3(14,25);
	  a1.tulosta();  a2.tulosta();  a3.tulosta();
	  a1.aseta(12,15); a2.aseta(16,-15);
	  a1.tulosta(); a2.tulosta();
	  return 0;
	}
Huomattakoon, että samalla kun tehdään aseta- metodi, kannattaa muodostajassakin kutsua sitä. Näin olion tilaa muutetaan vain muutamassa metodissa, jotka voidaan ohjelmoida niin huolella, ettei mitään yllätyksiä pääse koskaan tapahtumaan. Tämä rupeaa jo muistuttamaan olio- ohjelmointia!

Tehtävä 9.81 Negatiivinen minuuttiasetus

Mitä ohjelma aikacla4.cpp tulostaisi? Miksi ohjelma toimisi halutulla tavalla?

Tehtävä 9.82 Tuntien tarkistus

Ohjelmoi myös tunneille tarkistus, missä pidetään huoli siitä, että tunnit ovat aina välillä 0-23.

9.3.9 inline- metodit ja tavalliset metodit

Olemme kaikissa edeltävissä olioesimerkeissä kirjoittaneet metodien toteutuksen myös luokan esittelyn sisään. Javassa näin tehdään aina, mutta C++:ssa oikeastaan varsin harvoin. Tyypillisempi tapa kirjoittaa on sellainen, missä ensin esitellään luokka ilman metodien toteutusta ja sitten metodit esitellään luokan ulkopuolella:

olioalk\aikacla5.cpp - metodit luokan ulkopuolella

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	  int h,m;
	public:
	  cAika(int ih=0, int im=0);
	  void aseta(int ih,int im=0);
	  void lisaa(int lisa_min);
	  void tulosta() const;
	};
	
	void cAika::aseta(int ih,int im)
	{
	  h = ih; m = im; lisaa(0);
	}
	
	cAika::cAika(int ih, int im)
	{
	  aseta(ih,im);
	}
	
	void cAika::lisaa(int lisa_min)
	{
	  int yht_min = h * 60 + m + lisa_min;
	  h = yht_min / 60;
	  m = yht_min % 60;
	}
	
	void cAika::tulosta() const
	{
	  cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
	}
	
	int main(void)
	{
	  cAika a1, a2(13), a3(14,175);
	  a1.tulosta(); a2.tulosta();  a3.tulosta();
	  a1.aseta(12,15); a2.aseta(16,-15);
	  a1.tulosta(); a2.tulosta();
	  return 0;
	}
Jos metodin toteutus on luokan ulkopuolella, pitää metodin nimen eteen liittää tieto siitä, minkä luokan metodista on kyse. Esimerkiksi:
	void cAika::lisaa(int lisa_min)
Nyt metodit ovat todellisia kutsuttavia aliohjelmia. Tähän saakka metodit ovat olleet inline- funktioita, eli niihin ei ole tehty aliohjelmakutsua, vaan niiden koodi on sijoitettu kutsun tilalle. Etuna on nopeampi suoritus kun aliohjelmaan siirtyminen ja paluu jäävät pois, mutta haittana suurempi ohjelman koko, erityisesti jos kutsuja on paljon ja metodit ovat suuria.

Jos nopeussyistä jälkeenpäin jokin luokan ulkopuolelle kirjoitettu metodi haluttaisiinkin takaisin inline- metodiksi, voidaan tämä tehdä myös inline- avainsanalla:

	inline void cAika::aseta(int ih,int im)
	{
	  h = ih; m = im; lisaa(0);
	}
Siis inline- metodeja voidaan tehdä kahdella eri tavalla:
1.
Kirjoitetaan metodi luokan sisälle. Tässä monisteessa käytetään jatkossa tätä tapaa lähinnä siitä syystä, että ohjelmalistaukset tulevat näin hieman lyhyemmiksi.
2.
Kirjoitetaan metodi luokan ulkopuolella ja kirjoitetaan metodin toteutuksen eteen inline.
Sopivalla inline- määreen käytöllä mahdollinen olio- ohjelmoinnin byrokratiasta aiheutuva hitaus voidaan poistaa. Tosin inline kannattaa laittaa vain usein kutsuttavien lyhyiden metodien eteen.

Esimerkiksi seuraavasta ohjelmasta:

olioalk\inline.cpp - esimerkki optimoinnista

	#include <iostream.h>
	
	inline void ynnaa_yksi(int &ri)
	{
	  ri = ri + 1;
	}
	
	inline void ynnaa_kaksi(int &ri)
	{
	  ynnaa_yksi(ri);
	  ynnaa_yksi(ri);
	}
	
	int main (void)
	{
	  int i = 10;
	  ynnaa_kaksi(i);
	  cout << i << endl;
	  return 0;
	}
riveistä
	  int i = 10;
	  ynnaa_kaksi(i);
hyvä kääntäjä kääntää koodin:
	mov  eax,10    // Suoraan kääntäjä ei välttämättä uskalla laittaa mov eax,12
	inc  eax       // koska se ei voi tietää tarvitaanko inc lauseen ominaisuutta
	inc  eax       // Oikein hyvä kääntäjä ehkä laittaisikin mov eax,12
joka on täsmälleen sama koodi, joka generoituu lauseista
	  i = 10;  ++i;  ++i;
Jos inline- määreet jätettäisiin ynnaa- aliohjelmista pois, olisi ynnaa_kaksi kutsu yli 30 kertaa hitaampi!

9.3.10 Jako useampaan tiedostoon

Kaiken tämän jälkeen lukija varmaan taas kysyy: Miksi vielä tuplata koodin pituus, kun aikacla4.cpp oli vielä jotenkin siedettävä alkuperäisen aikalis4.cpp:n rinnalla! Vastaus on uudelleen käytettävyys. Eli kun lopulta aika- luokkamme on valmis, paketoimme sen sellaiseksi, että se on helppo liittää myöhempiin omiin ohjelmiimme tai jopa luovuttaa (mahdollisesti korvausta vastaan :-) muille erillisenä komponenttina. Tuplatyö nyt saattaa olla 1/100 työ tulevaisuudessa.

Oikeasti siis jaamme koko ohjelman 3 eri tiedostoon:

aika.h luokan esittely

aika.cpp luokan toteutus

aikatest.cpp luokan testaava pääohjelma

olioalk\aika.h - luokan esittely omaan tiedostoon

	#ifndef AIKA_H           // Suoja, jottei samaa koodia "includata" kahta kertaa!
	#define AIKA_H
	class cAika {
	  int h,m;
	public:
	  cAika(int ih=0, int im=0);
	  void aseta(int ih,int im=0);
	  void lisaa(int lisa_min);
	  void tulosta() const;
	};
	#endif // AIKA_H

olioalk\aika.cpp - luokan metodit kirjoitettu omaan tiedostoon

	#include <iostream.h>
	#include <iomanip.h>
	#include "aika.h"
	
	void cAika::aseta(int ih,int im)
	{
	  h = ih; m = im; lisaa(0);
	}
	
	cAika::cAika(int ih, int im)
	{
	  aseta(ih,im);
	}
	
	void cAika::lisaa(int lisa_min)
	{
	  int yht_min = h * 60 + m + lisa_min;
	  h = yht_min / 60;
	  m = yht_min % 60;
	}
	
	void cAika::tulosta() const
	{
	  cout << setfill('0') << setw(2) << h << ":" << setw(2) << m << endl;
	}

olioalk\aikatest.cpp - aika.cpp:n testi

	#include "aika.h"
	// Projektiin aika.cpp ja aikatest.cpp
	int main(void)
	{
	  cAika a1, a2(13), a3(14,175);
	  a1.tulosta(); a2.tulosta();  a3.tulosta();
	  a1.aseta(12,15); a2.aseta(16,-15);
	  a1.tulosta(); a2.tulosta();
	  return 0;
	}
Lopuksi tehtäisiin vielä projekti tai makefile johon kuuluvat aika.cpp ja aikatest.cpp.

9.3.11 this- osoitin

Jos verrataan funktiota
	void lisaa(tAika *pAika, int lisa_min)
	{
	  int yht_min = pAika->h * 60 + pAika->m + lisa_min;
	  pAika->h = yht_min / 60;
	  pAika->m = yht_min % 60;
	}
	...
	lisaa(&a1,55);
ja metodia
	void cAika::lisaa(int lisa_min)
	{
	  int yht_min = h * 60 + m + lisa_min;
	  h = yht_min / 60;
	  m = yht_min % 60;
	}
	...
	a1.lisaa(55);
niin helposti näyttää, että ensin mainitussa funktiossa on enemmän 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ä.

9.4 Perintä

9.4.1 Luokan ominaisuuksien laajentaminen

Pidimme jo aikaisemmin toiveena sitä, että voisimme laajentaa luokkaamme käsittelemään myös sekunteja. Miksi emme tehneet tätä heti? No tietysti olisi heti pitänyt älytä laittaa mukaan myös sekunnit, mutta tosielämässäkin käy usein näin, eli hyvästäkin suunnittelusta huolimatta toteutuksen loppuvaiheessa tulee vastaan tilanteita, jossa alkuperäiset luokat todetaan riittämättömiksi.

Tämän laajennuksen tekemiseen on olio- ohjelmoinnissa kolme mahdollisuutta: Joko muuttaa alkuperäistä luokkaa, periä alkuperäisestä luokasta laajempi versio tai tehdä uusi luokka, jossa on alkuperäinen luokka yhtenä attribuuttina.

Tutustumme seuraavassa kaikkiin kolmeen eri mahdollisuuteen.

9.4.2 Alkuperäisen luokan muuttaminen

Läheskään aina ei voi täysin välttää sitäkään, etteikö alkuperäistä luokkaa joutuisi muuttamaan. Jos näin joudutaan tekemään, pitäisi tämä pystyä tekemään siten, että jo kirjoitettu luokkaa käyttävä koodi säilyisi täysin muuttumattomana (tai ainakin voitaisiin päivittää minimaalisilla muutoksilla) ja vasta uudessa koodissa käytettäisiin hyväksi luokan uusia ominaisuuksia.

Jos luokka on saatu joltakin kolmannelta osapuolelta, ei luokan päivittäminen edes ole mahdollista, vaan silloin täytyy turvautua muihin (parempiin) tapoihin.

Päivitämme nyt kuitenkin alkuperäistä luokkaa:

olioalk\aikacla6.cpp - laajentaminen muuttamalla kantaluokkaa

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	  int h,m,s;                                  // lisätty s
	public:
	  cAika(int ih=0, int im=0, int is=0);        // lisätty is=0
	  void aseta(int ih,int im=0, int is=0);      // lisätty is=0
	  void lisaa(int lisa_min, int lisa_sek=0);   // lisätty lisa_sek=0
	  void tulosta(int tulsek=0) const;           // lisätty tulsek=0
	};
	
	void cAika::aseta(int ih,int im, int is)      // lisätty is
	{
	  h = ih; m = im; s = is;  lisaa(0,0);        // lisätty s=is ja ,0
	}
	
	cAika::cAika(int ih, int im, int is)          // lisätty int is
	{
	  aseta(ih,im,is);
	}
	
	void cAika::lisaa(int lisa_min, int lisa_sek) // lisätty int lisa_sek
	{
	  s += lisa_sek;                                 // lisätty
	  int yht_min = h * 60 + m + lisa_min + s / 60;  // lisätty s / 60
	  s = s % 60;                                    // lisätty
	  h = yht_min / 60;
	  m = yht_min % 60;
	}
	
	void cAika::tulosta(int tulsek) const         // lisätty int tulsek
	{
	  cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
	  if ( tulsek ) cout << ":" << setw(2) << s;  // lisätty
	  cout << endl;
	}
	
	int main(void)
	{
	  cAika a1, a2(13), a3(14,175);               // ei muutoksia!
	  a1.tulosta(); a2.tulosta();  a3.tulosta();
	  a1.aseta(12,15); a2.aseta(16,-15);
	  a1.tulosta(); a2.tulosta();
	  cAika a4(12,55,45); a4.tulosta(1);          // lisätty uusi
	  a4.lisaa(3,30);     a4.tulosta(1);
	  return 0;
	}
Huh huh! Tulipa paljon muutoksia, mutta onnistuimme kuitenkin pitämään alkuperäisen pääohjelman koodin muuttumattomana. Tulostuksessa olisi tietysti voitu valita sekin linja, että aina tulostetaan sekunnit tai sekuntien tulostus on oletuksena. Kumpikin valinta olisi aiheuttanut olemassa olevan koodin toiminnan muuttumisen. Jos näin olisi haluttu, niin sitten olisi valittu niin.

Tehtävä 9.83 Sekuntien tulostus aina tai oletuksena

Muuta ohjelmaa aikacla6.cpp siten, että sekunnit tulostetaan aina.
Muuta ohjelmaa aikacla6.cpp siten, että sekunnit tulostetaan oletuksena jos ne on != 0.

9.4.3 Koostaminen

Seuraava mahdollisuus olisi uuden luokan koostaminen (aggregation) vanhasta aikaluokasta ja sekunneista. Tämä mahdollisuus meillä on aina käytössä vaikkei alkuperäistä lähdekoodia olisikaan käytössä. Tätä vaihtoehtoa pitää aina vakavasti harkita. Tehdään koodi sellaiseksi, että uusi luokka tulostaa aina sekunnit (inline- muotoa käytetään tilan säästämiseksi):

olioalk\aikacla7.cpp - laajentaminen koostamalla

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	... kuten aikacla5.cpp, mutta seuraava muutos:
	  void tulosta(int lf=1) const  {
	    cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
	    if ( lf ) cout << endl;
	  }
	};
	
	class cAikaSek {
	  cAika hm;
	  int s;
	public:
	  void aseta(int ih=0, int im=0, int is=0) {
	    s = is; hm.aseta(ih,im); lisaa(0);
	  }
	  cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
	  void lisaa(int lisa_min, int lisa_sek=0) {
	    s += lisa_sek;
	    hm.lisaa(lisa_min+s/60);
	    s %= 60;
	  }
	  void tulosta(int lf=1) const  {
	    hm.tulosta(0);
	    cout <<  ":" << setw(2) << s;
	    if ( lf ) cout << endl;
	  }
	};
	
	
	int main(void)
	{
	  cAika a1, a2(13), a3(14,175);
	  a1.tulosta(); a2.tulosta();  a3.tulosta();
	  a1.aseta(12,15); a2.aseta(16,-15);
	  a1.tulosta(); a2.tulosta();
	  cAikaSek a4(12,55,45); a4.tulosta(1);        // lisätty uusi
	  a4.lisaa(3,30);     a4.tulosta(1);
	  return 0;
	}
Valitettavasti emme aivan täysin onnistuneet. Nimittäin alkuperäisen luokan tulosta oli niin tyhmästi toteutettu, että se tulosti aina rivinvaihdon. Tämmöistä hölmöyttä ei pitäisi mennä koskaan tekemään ja siksipä alkuperäinen luokka pitää joka tapauksessa palauttaa valmistajalle remonttiin. No luokan valmistaja muutti tulosta- metodia siten, että se oletuksena tulostaa rivinvaihdon (merk. lf = line feed), mutta pyydettäessä jättää sen tekemättä. Näin vanha koodi voidaan käyttää muuttamattomana.

Palaamme tulostusongelmaan myöhemmin ja keksimme silloin paremman ratkaisun, jota olisi pitänyt käyttää jo alunperin.

Luokassa on niin vähän ominaisuuksia, että uudessa luokassamme olemme joutunee itse asiassa tekemään kaiken uudelleen ja on kyseenalaista olemmeko hyötyneet vanhasta luokasta lainkaan. Tämä on onneksi lyhyen esimerkkimme vika, todellisilla luokilla säästö kokonaan uudestaan kirjoitettuun verrattuna olisi moninkertainen.

9.4.4 Perintä

Viimeisenä vaihtoehtona tarkastelemme perintää (inheritance). Valinta koostamisen ja perinnän välillä on vaikea. Aina edes olioasiantuntijat eivät osaa sanoa yleispätevästi kumpiko on parempi. Nyrkkisääntönä voisi pitää seuraavaa is-a - sääntöä:
	Jos voi sanoa että lapsiluokka on (is-a) isäluokka, niin peritään.
	Jos sanotaan että lapsiluokassa on (has-a) isäluokka, niin koostetaan
Kokeillaanpa: "luokka jossa on aika sekunteina" on "aika- luokka". Kuulostaa hyvältä. Siis perimään (taas inline- muoto tilan säästämiseksi):

olioalk\aikacla8.cpp - laajentaminen perimällä

	... kuten aikacla7.cpp
	
	class cAikaSek : public cAika {
	  int s;
	public:
	  void aseta(int ih=0, int im=0, int is=0) {
	    s = is; cAika::aseta(ih,im); lisaa(0);
	  }
	  cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
	  void lisaa(int lisa_min, int lisa_sek=0) {
	    s += lisa_sek;
	    cAika::lisaa(lisa_min+s/60);
	    s %= 60;
	  }
	  void tulosta(int lf=1) const  {
	    cAika::tulosta(0);
	    cout <<  ":" << setw(2) << s;
	    if ( lf ) cout << endl;
	  }
	};
	
	
	int main(void)
	... kuten aikacla7.cpp
Tässä tapauksessa kirjoittamisen vaiva oli tasan sama kuin koostamisessakin. Joissakin tapauksissa perimällä pääsee todella vähällä. Otamme tästä myöhemmin esimerkkejä, kunhan pääsemme eroon syntaksin esittelystä.

Lapsiluokka, aliluokka (child class, subclass) on se joka perii ja isäluokka, yliluokka (parent class, superclass) se joka peritään. Käytetään myös nimitystä välitön ali/yliluokka, kun on kyseessä perintä suoraan luokalta toiselle, kuten meidän esimerkissämme.

C++:ssa välitön yliluokka ilmoitetaan aliluokan esittelyssä:

	class cAikaSek : public cAika {
Jos täytyy viitata yliluokan metodeihin, joille on kirjoitettu aliluokassa oma määrittely, käytetään näkyvyysoperaattoria :: (scope resolution operator):
	cAika::lisaa(lisa_min+s/60);
Näkyvyysoperaattoria EI käytetä, mikäli samannimistä metodia ei ole aliluokassa.

Perintää kuvataan piirroksessa:


Kuva 9.2 Perintä

9.4.5 Polymorfismi, eli monimuotoisuus

Edellisestä esimerkistä ei oikeastaan paljastunut vielä mitään, mikä olisi puoltanut perintää. Korkeintaan snobbailu uudella syntaksilla. Mutta tosiasiassa pääsemme tästä kiinni olio- ohjelmoinnin tärkeimpään ominaisuuteen, ominaisuuteen jota on vaikea saavuttaa perinteisellä ohjelmoinnilla: polymorfismi (polymorphism) eli monimuotoisuus.

Lisätäänpä vielä testiohjelman loppuun:

	  cAika a1; ...          a1.aseta(12,15);
	  cAikaSek a4(12,55,45); a4.lisaa(3,30);
	  pAika = &a1;  pAika->tulosta();
	  pAika = &a4;  pAika->tulosta();
Tulostus:
	12:15
	12:59
Mistä tässä oli kyse? Osoitin pAika oli monimuotoinen, eli sama osoitin osoitti kahteen eri tyyppiseen luokkaan. Tämä on mahdollista, jos luokat ovat samasta perimähierarkiasta kuten tässä tapauksessa ja osoitin on tyypiltään näiden yhteisen kantaluokan olion osoitin.

Mutta hei! Eikös jälkimmäinen tulostus olisi pitänyt olla sekuntien kanssa, olihan jälkimmäinen tulostettava luokka "aikaluokka jossa sekunnit"? Totta! Taas on alkuperäisen luokan tekijä möhlinyt eikä ole lainkaan ajatellut että joku voisi periä hänen luokkaansa. Tässä syy, miksi otamme perinnän mukaan tässä vaiheessa opintoja. Vaikka jatkossa emme hirveästi perinnän varaan rakennakaan, emme myöskään saa tehdä luokkia, jotka olisivat "väärin" tehtyjä!

9.4.6 Myöhäinen sidonta

Ongelma täytyy ratkaista siten, että jo alkuperäisessä luokassa kerrotaan että vasta ohjelman suoritusaikana selvitetään mistä luokasta todella on kyse, kun metodia kutsutaan. Tällaista ominasuutta sanotaan myöhäiseksi sidonnaksi (late binding) (tälle monisteellekin tulee kyllä myöhäinen sidonta), vastakohtana sille, jota olemme edellä käyttäneet, eli aikainen sidonta (early binding). Sidonnan sisäisen mekanismin opettelun jätämme jollekin toiselle kurssille (ks. vaikkapa Olio- ohjelmointi ja C++/VL).

Myöhäinen sidonta saadaan C++:ssa aikaan liittämällä virtual- avainsana metodien eteen. Kaikki ne metodit täytyy ilmoittaa myöhäiseen sidontaan, joita mahdolliset perilliset tulevat muuttamaan. Siis luokat vielä kerran remonttiin:

olioalk\aikacla9.cpp - myöhäinen sidonta

	#include <iostream.h>
	#include <iomanip.h>
	
	class cAika {
	  int h,m;
	public:
	  virtual void aseta(int ih,int im=0) {                      // lisätty virtual
	    h = ih; m = im; lisaa(0);
	  }
	  cAika(int ih=0, int im=0) { aseta(ih,im); }
	  virtual void lisaa(int lisa_min)  {                        // lisätty virtual
	    int yht_min = h * 60 + m + lisa_min;
	    h = yht_min / 60;
	    m = yht_min % 60;
	  }
	  virtual void tulosta(int lf=1) const  {                    // lisätty virtual
	    cout << setfill('0') << setw(2) << h << ":" << setw(2) << m;
	    if ( lf ) cout << endl;
	  }
	};
	
	class cAikaSek : public cAika {
	  int s;
	public:
	  virtual void aseta(int ih=0, int im=0) { cAika::aseta(ih,im); } // lisätty
	  virtual void aseta(int ih, int im, int is) {               // lisätty virtual
	    s = is; aseta(ih,im); lisaa(0,0);                        // pois cAika::
	  }
	  cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
	  virtual void lisaa(int lisa_min) { cAika::lisaa(lisa_min); } // lisätty
	  virtual void lisaa(int lisa_min, int lisa_sek) {           // lisätty virtual
	    s += lisa_sek; lisaa(lisa_min+s/60); s %= 60;            // pois cAika::
	  }
	  virtual void tulosta(int lf=1) const  {                    // lisätty virtual
	    cAika::tulosta(0);
	    cout <<  ":" << setw(2) << s;
	    if ( lf ) cout << endl;
	  }
	};
	
	
	int main(void)
	.. kuten  aikacla8.cpp + cAika *pAika; ...
Virtual sanat on lisätty kaikkien metodien eteen. Aliluokassa virtual on tarpeeton uudelleenmääriteltävien (korvaaminen, syrjäyttäminen, overriding) metodien kohdalle, mutta se on hyvä pitää siellä kommenttimielessä.

Sitten tuleekin hankalammin selitettävä asia. Miksi yksi- parametrinen lisaa ja kaksi- parametrinen aseta on täytynyt kirjoittaa uudelleen? Tämä johtuu kielen ominaisuuksista, sillä muuten vastaavat useampi- parametriset metodit syrjäyttäisivät alkuperäiset metodit ja alkuperäisiä ei olisi lainkaan käytössä. Tässä tapauksessa olisimmekin voineet antaa niiden syrjäytyä, mutta koska tulevaisuudesta ei koskaan tiedä, on alkuperäiselläkin parametrimäärällä olevien metodien ehkä hyvä säilyä.

Parempi ratkaisu olisi ehkä kuitenkin ollut, jos jo alkuperäisessä luokassa olisi varauduttu sekuntien tuloon, vaikka niitä ei olisi mitenkään otettukaan huomioon:

olioalk\aikaclaA.cpp - myöhäinen sidonta, sekunnit jo kantaluokassa

	... 
	class cAika {                           // Muutokset aikacla8.cpp:hen verrattuna
	  int h,m;
	public:
	  virtual void aseta(int ih=0,int im=0, int lisa_sek=0) {   // virtual, lisa_sek
	    h = ih; m = im; lisaa(0);
	  }
	  cAika(int ih=0, int im=0) { aseta(ih,im); }
	  virtual void lisaa(int lisa_min, int lisa_sek=0)  {       // virtual, lisa_sek
	    int yht_min = h * 60 + m + lisa_min;
	    h = yht_min / 60;
	    m = yht_min % 60;
	  }
	  virtual void tulosta(int lf=1) const  { ... }                // virtual
	};
	
	class cAikaSek : public cAika {
	  int s;
	public:
	  virtual void aseta(int ih=0, int im=0, int is=0) {           // virtual
	    s = is; cAika::aseta(ih,im); lisaa(0);                     // järj. vaihd.
	  }
	  cAikaSek(int ih=0, int im=0, int is=0) { aseta(ih,im,is); }
	  virtual void lisaa(int lisa_min, int lisa_sek=0) {           // virtual
	    s += lisa_sek; cAika::lisaa(lisa_min+s/60); s %= 60; 
	  }
	  virtual void tulosta(int lf=1) const  {                      // virtual
	};
	...
	int main(void)
	{
	...
	  cAika *pAika;
	  pAika = &a1;  pAika->tulosta();
	  pAika = &a4;  pAika->tulosta();
	  return 0;
	}

Varoitus: Borlandin C++5.1:en optimoivalla kääntäjällä käännettynä em. koodi kaatoi kääntäjän ympäristöineen jos cAikaSek::aseta oli muodossa:

	cAika::aseta(ih,im); s = is; lisaa(0);	

Tehtävä 9.84 Miksi ensin sekuntien alustus?

Osaatko selittää miksi sekunnit pitää alustaa ensin metodissa: cAikaSek::aseta? Jos osaat, olet jo melkein valmis C++- ohjelmoija!

Tehtävä 9.85 2-3 - parametrinen aseta

Onko nyt olemassa 2 ja 3 - parametrinen aseta- metodi?

9.4.7 Yliluokan muodostajan kutsuminen ennen muodostajaa

Joskus kirjoittamalla vähän enemmän voi saada aikaan nopeampaa ja turvallisempaa koodia. Niin tässäkin tapauksessa. Nimittäin säätäminen siinä, ettemme vaivautuneet kirjoittamaan kumpaankin luokkaan erikseen oletusmuodostajaa, johtaa siihen että esimerkiksi alustus
	cAikaSek a4(12,55,45);
kutsuu kaikkiaan seuraavia metodeja:
	cAikaSek::cAikaSek(12,55,45);
	  cAika::cAika(0,0);           // Oletusalustus yliluokalle
	    cAika::aseta(0,0,0);
	      cAika::lisaa(0,0);       // Tässä cAika, koska ei vielä "kasvanut" cAikaSek
	  cAikaSek::aseta(12,55,45);
	    cAika::aseta(12,55);
	      cAikaSek::lisaa(0,0);    // HUOM!  Todellakin cAikaSek::lisaa virt.takia
	        cAika::lisaa(0,0);
	    cAikaSek::lisaa(0,0);
	      cAika::lisaa(0,0);
Olisitko arvannut! Enää ei tarvitse ihmetellä miksi olio- ohjelmat ovat isoja ja hitaita. Totta kai voitaisiin sanoa, että hyvän kääntäjän olisi pitänyt huomata tuosta optimoida päällekkäisyydet pois. Mutta tämä on vielä tänä päivänä kova vaatimus kääntäjälle. Mutta ehkäpä voisimme ohjelmoijina vähän auttaa:

olioalk\aikaclaB.cpp - oletusmuodostajat

	... 
	class cAika {
	  int h,m;
	public:
	  virtual void aseta(int ih=0,int im=0, int is=0) { ... }
	  cAika() { h = 0; m = 0; } // tai cAika() : h(0), m(0) {}  // Oletusmuodostaja
	  cAika(int ih, int im=0) { aseta(ih,im); }                 // ih=0 oletus pois
	  virtual void lisaa(int lisa_min, int lisa_sek=0)  { ... }
	  virtual void tulosta(int lf=1) const  { ... }
	};
	
	class cAikaSek : public cAika {
	  int s;
	public:
	  virtual void aseta(int ih=0, int im=0, int is=0) {...}
	  cAikaSek() : s(0), cAika() {}                             // Oletusmuodostaja
	  cAikaSek(int ih, int im=0, int is=0) : cAika(ih,im), s(is) { lisaa(0); }
	};
	...
Nyt on saatu ainakin seuraavat edut: oletustapauksessa kumpikin luokka alustuu pelkillä 0:ien sijoituksilla, ilman yhtään ylimääräistä kutsua. Oletusmuodostaja luokalla cAika voidaan esitellä esim. seuraavilla tavoilla, joista keskimmäistä voidaan pitää parhaana:
	cAika() { h = 0; m = 0; }   // Normaalit sijoitukset
	cAika() : h(0), m(0) {}     // h:n muodostaja arvolla 0, eli h=0 kokonaisluvulle
	                            // m:n muodostaja arvolla 0, eli m=0
	cAika() : h(0) { m=0; }    
Vastaavasti luokalle cAikaSek pitää oletusmuodostaja tehdä seuraavanlaiseksi:
	cAikaSek() : s(0), cAika() { }      // s alusetetaan 0:ksi ja peritty yliluokka
	                                    // alustetaan muodostajalla cAika()
	cAikaSek() { s = 0; }               // TÄMÄ on TÄSSÄ tapauksessa sama kuin
	                                    // edellinen, koska jos yliluokan muodostajaa
	                                    // ei itse kutsuta, kutsutaan 
	                                    // oletusmuodostajaa
ELI! Perityn yliluokan muodostajaa kutsutaan automaattisesti, jollei sitä itse tehdä. Nyt 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.

Tehtävä 9.86 Yliluokan alustajan kutsu

Pöytätestaa sekä aikaclaA.cpp että aikaclaB.cpp alustuksella cAikaSek a4(1,95,70);

9.5 Saantimetodit

Osa aikaisemmista ongelmista olisi voitu kiertää, mikäli olisimme päässeet käsiksi luokan yksityisiin tietoihin. Esimerkiksi alkuperäisen luokan tulosta olisi voitu jättää koskemattomaksi vaikka se olikin väärin tehty (paitsi se virtual sinne kyllä olisi pitänyt lisätä joka tapauksessa). Tätä olisi voitu helpottaa sillä, että kantaluokassa cAika olisi julistettu h ja m protected suojauksella. Tosin vain perityssä versiossa tästä olisi ollut apua.

9.5.1 Miksi ja miten

Muutenkin saattaa tulla tilanteita, joissa luokan ulkopuolinen haluaa päästä käsiksi sisäisiin tietoihin. Ainakin lukemana niitä. Eihän ole ollenkaan tavatonta ajankaan kanssa, että joku haluaisi tietää tunnit, muttei tulostaa? Mikä ratkaisuksi? Julistetaanko kaikki attribuutit julkisiksi (public)? No ei sentään! Kirjoitetaan saantimetodi kullekin attribuutille, jonka perustellusti voidaan katsoa tarpeelliseksi jollekin ulkopuoliselle voitavan julkaista:

"Lopullinen" versio aikaluokastamme voisikin siis olla seuraava:

olioalk\aikaclaC.cpp - saantimetodit

	class cAika {
	  int h,m;
	public:
	  virtual void aseta(int ih=0,int im=0, int is=0) {...}
	  ...
	  virtual void tulosta(int lf=1) const  { ... }
	  int 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".

Tehtävä 9.87 Saantimetodi sekunneille

Täydennä aikaclaB.cpp:hen em. saantimetodit ja lisäksi ss() aliluokkaan cAikaSek.
Kirjoita myös pitemmillä nimillä varustetut saantimetodit.

Tehtävä 9.88 Saantimetodien käyttäminen

Muuta vielä edellisessä tehtävässä jokainen mahdollinen viittaus luokan sisälläkin saantimetodeja käyttäväksi suoran attribuuttiviittauksen sijasta. Käännä alkuperäinen koodi ja uusi koodi, sekä vertaa .exe tiedostoja keskenään jollakin vertailuohjelmalla.

9.5.2 Rajapinta ja sisäinen esitys

Kapseloinnin ansiosta luokan käyttämiseksi on tullut selvä rajapinta (interface): metodit, joilla olion tilaa muutetaan. Tämän rajapinnan ansiosta luokka muuttuu "mustaksi laatikoksi", jonka sisällöstä ulkomaailma ei tiedä mitään, mutta jonka kanssa voi kommunikoida metodien avulla.


Kuva 9.3 Musta laatikko
Tämä luokan sisustan piilottaminen antaa meille mahdollisuuden toteuttaa luokka oleellisesti eri tavalla. Voimme esimerkiksi toteuttaa ajan minuutteina vuorokauden alusta laskien:

olioalk\aikaclaD.cpp - sisäinen toteutus minuutteina

	... 
	class cAika {
	  int yht_min;
	public:
	  virtual void aseta(int ih=0,int im=0, int is=0) {
	    yht_min = 0; lisaa(60*ih+im);
	  }
	  cAika(int ih=0, int im=0) { aseta(ih,im); }
	  virtual void lisaa(int lisa_min, int lisa_sek=0)  { yht_min += lisa_min; }
	  virtual void tulosta(int lf=1) const  {
	    cout << setfill('0') << setw(2) << 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 ...

Tehtävä 9.89 minuutteina()

Lisää aikaclaB:hen ja aikaclaD:hen, eli molempiin sisäisiin toteutustapoihin saantimetodi minuutteina, joka palauttaa kellonajan vuorokauden alusta minuutteina laskettuna. Lisää vielä saantimetodi sekunteina.

9.6 Mistä hyviä luokkia

Alunperin kirjoittamamme luokka cAika kokikin varsin kovia tarkemmassa tarkastelussa. Näistä muutoksista osa oli vielä aivan perusasioita; läheskään kaikkea emme vieläkään ole ottaneet huomioon (vertailu, syöttö päätteeltä, muuntaminen merkkijonoksi ja takaisin, lisäyksessä tapahtuvan ylivuodon luovuttaminen päivämäärälle, jne...). Miten sitten monimutkaisempien luokkien kanssa? Niin kauan pärjää, kun luokat on omaan käyttöön. Heti kun yritetään tehdä yleiskäyttöisiä luokkia (joka on yksi olio- ohjelmoinnin tavoite), tuleekin ongelmia vastaan.

Paremmalla suunnittelulla luokasta olisi heti voinut tulla yleiskäyttöisempi. Usein jopa joudutaan tekemään kahden luokan yläpuolelle abstrakti, tai muuten vaan yhteinen yliluokka, josta hieman toisistaan poikkeavat luokat peritään. Kuljimme tämän pitkän tien sen vuoksi, että lukija oppisi ymmärtämään miksi valmiit luokat eivät ole parin rivin koodinpätkiä.

Tulevaisuudessa ohjelmoijat jakaantunevatkin selvästi kahteen ryhmään: toiset käyttävät valmiita luokkia (mikä on helppoa, jos luokat ovat kunnossa, vrt. Delphi tai Visual Basic, ehkä myös osittain Java ja Jonnen pyynnöstä mainitaan tietysti Python). Ammattitaitoisempi ryhmä sitten suunnittelee ja tekee näitä yleiskäyttöisiä luokkia. Sitä mukaa kun luokkia saadaan valmiiksi eri "elämän aloille", siirtyy ammattilaiset yhä spesifimmille aloille.

Esimerkiksi M$:in Windowsin ohjelmointiin tarkoitetut luokkakirjastot ovat paisuneet niin suuriksi, että niiden käyttämistä tuskin kukaan enää hallitsee, ja 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.

9.7 Valmiita luokkia

Olio- ohjelmoinnin eräs tavoite on tuottaa ohjelmoijien käyttöön yleiskäyttöisiä komponentteja, jotta jokainen ei keksisi samaa pyörää uudelleen. Erityisesti graafisen ohjelmoinnin puolella ja sekä myös tietokantaohjelmoinnin puolella näitä komponentteja onkin varsin mukavasti. Borlandin Delphillä syntyy melkein Kerho- ohjelmaamme vastaava Windows- ohjelma lähes koodaamatta, pelkästään pudottelemalla komponentteja lomakkeelle.

9.7.1 Merkkijonot

Jos kerrankin pääsisin vastakkain nykykielten kehittäjien kanssa, niin tekisi kovasti mieli kysyä ovatko he koskaan tehneet oikeaa ohjelmaa. Nimittäin lähes kielestä riippumatta kunnolliset merkkijonot loistavat poissaolollaan. Ja ohjelmoijat ovat käyttäneet äärettömästi työtunteja tehdessään itselleen aluksi edes auttavaa merkkijonokirjastoa. Ainoastaan "lelukielissä" - Basicissä ja Turbo Pascalissa on ollut hyvät ja turvalliset merkkijonot.

C- kielen char jono[10] on todellinen aikapommi, jonka aukkoisuuteen perustuu vielä tänäkin päivänä useat hakkereiden kikat murtautua vieraisiin tietojärjestelmiin. Katsotaanpa ensin mitä C- merkkijonoille voi/ei voi tehdä:

	char s1[10],s2[5],*p; 	
	p = "Kana"           // Toimii!
	p[0] = 'S';          // Toimii! Mutta jatkossa käy huonosti...  
	                     // 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:

\kurssit\c\ali\strtest.cpp - merkkijonojen testaus

	#include <iostream.h>
	#include <string>
	using namespace std;
	
	int main(void)
	{
	  string mjono1 = "Ensimmainen";
	  string mjono2 = "Toinen";
	  string mjono;
	
	// syotto ja tulostus
	  cout << "Anna merkkijono    > ";
	  getline(cin,mjono,'\n');
	  cout << "Annoit merkkijonon : " << mjono << endl;
	
	// kasittely merkeittain
	  mjono[0] = mjono1[4];
	  mjono[1] = mjono2[1];
	  mjono[2] = 't';
	  mjono[3] = '\0';       // Laitonta jos merkkijono ei ole nain iso!
	                         // Ei vaikuta!
	  cout << mjono << endl; // tulostaa: mot ...+jotakin
	
	// sijoitukset
	  mjono = mjono1;
	  cout << mjono << endl;       // Ensimmainen
	
	  mjono = "Eka";
	  cout << mjono << endl;       // Eka
	
	// katenointi
	  mjono = mjono1 + mjono2;
	  cout << mjono << endl;       // EnsimmainenToinen
	
	  mjono = mjono1 + "Toka";
	  cout << mjono << endl;       // EnsimmainenToka
	
	  mjono = "Eka" + mjono2;
	  cout << mjono << endl;       // EkaToinen
	
	// vertailut
	  if (mjono1 == mjono2) cout << "1 on 2" << endl;           // ei tulosta
	  if (mjono1 == "Apua") cout << "1 on Apua" << endl;        // ei tulosta
	  if ("Apua" == mjono2) cout << "Apua on 2" << endl;        // ei tulosta
	
	  if (mjono1 != mjono2) cout << "1 ei ole 2" << endl;       // 1 ei ole 2
	  if (mjono1 != "Apua") cout << "1 ei ole Apua" << endl;    // 1 ei ole Apua
	  if ("Apua" != mjono2) cout << "Apua ei ole 2" << endl;    // Apua ei ole 2
	
	  if (mjono1 < mjono2) cout << "1 pienempi kuin 2" << endl;     // 1 pienempi ku
	  if (mjono1 < "Apua") cout << "1 pienempi kuin Apua" << endl;  // ei tulosta
	  if ("Apua" < mjono2) cout << "Apua pienempi kuin 2" << endl;  // Apua pienempi
	
	  if (mjono1 > mjono2) cout << "1 suurempi kuin 2" << endl;     // ei tulosta
	  if (mjono1 > "Apua") cout << "1 suurempi kuin Apua" << endl;  // 1 suurempi ku
	  if ("Apua" > mjono2) cout << "Apua suurempi kuin 2" << endl;  // ei tulosta
	
	// 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::string
Jos koneessa on niin vanha kääntäjä, ettei uudet luokat käänny sillä (ei esim. poikkeutusten käsittelyä), voi käyttää \kurssit\c\ali hakemistosta löytyvää "lasten" vastinetta, string, eli em. testiohjelma toimii jos otsikkotiedostojen hakupolkuun lisätään mainittu ali- hakemisto.

Tehtävä 9.90 Ensimäinen melkein järkevä olio

Täydennä seuraava ohjelma

olioalk\oppilas.cpp - 1. järkevä olio

	// Taydenna ja korjaile.  Mista puuttuu virtual?  Mista const?
	// Mita metodeja viela puuttuu? 
	#include <iostream.h>
	#include <string>
	using namespace std;
	
	class cHenkilo {
	  string nimi;
	  int ika;
	  double pituus_m;
	public:
	  cHenkilo(string inimi="???",int iika=0,int ipituus=0) {}
	  void tulosta()          {}
	  void kasvata(double cm) {}
	};
	
	class cOpiskelija : public cHenkilo {
	  double keskiarvo;
	public:
	  cOpiskelija(string inimi="???",int iika=0, int ipituus=0, int ikarvo=0.0) :
	    cHenkilo(inimi,iika,ipituus), keskiarvo(ikarvo) {}
	};
	
	int main(void)
	{
	  cHenkilo Kalle("Kalle",35,1.75);
	  Kalle.tulosta();
	  Kalle.kasvata(2.3);
	  Kalle.tulosta();
	  cOpiskelija Ville("Ville",21,1.80,9.9);
	  Ville.tulosta();
	  return 0;
	}


previous next Title Contents Index