* 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 ~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.
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.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:

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
// 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.
"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;
}