Johdatus ohjelmointiin -kurssilla kuvailtiin tietuetta seuraavasti:
"Tietue on kokoelma yhteen kuuluvia muuttujia aivan kuten taulukkokin. Erona on, että taulukon kaikki muuttujat ovat aina täsmälleen samantyyyppisiä, kun taas tietueessa voi olla useita erityyppisiä muuttujia. Taulukon tapauksessa sen yksittäisiin muuttujiin voitiin viitata indeksin (numeron) avulla, mutta tietueille tämä ei olisi järkevää. Sen sijaan jokaisella tietueeseen kuuluvalla muuttujalla, kentällä, on oma tunnuksensa, jonka avulla siihen viitataan."
Olio on "kehittynyt" tietue eli hienosti sanottuna tietojen ja palvelujen kokonaisuus. Käytännössä tietueen käyttämät aliohjelmat voidaan nyt viedä olioon metodeiksi, jotka siten ovat olion itsensä käytettävissä. Olion kanssa kommunikoidaan näiden metodien muodostaman rajapinnan kautta.
Kun siirrytään tietueista oliohin, aletaan tietuetta kutsua luokaksi. Olio on sitten luokan ilmentymä. Esim. kaikki ihmiset ovat luokka, mutta Sinä itse olet olio, luokkasi ilmentymä. Eli aina kun suunnitellaan olioita, suunnitellaan käytännössä olioluokkia, joiden ilmentymiä sitten käytetään.
Attribuutit ovat olion muuttujia, joille voidaan antaa arvo.
Metodit ovat olion "omistamia" funktioita ja aliohjelmia. Metodi liittyy aina kiinteästi tiettyyn olioluokkaan. Yleistä aliohjelmaa voi sen sijaan käyttää ts. kutsua mikä tahansa luokka tai vaikkapa toinen aliohjelma.
Kyseessä on osoitin, joka yksilöi tietyn olion. Jokainen olio tuntee automaattisesti oman this-osoittimensa, ja näin ollen olion omaan attribuuttiin voidaankin viitata kirjoittamalla
this->muuttujan_nimi
C++:ssa this-osoitinta ei ole pakko käyttää. Sen käyttö on kuitenkin hyödyllistä esim. konstruktorissa, jolloin samaa attribuutin nimeä voidaan käyttää kahdessa eri merkityksessä:
class cLuokka { int a; public: cLuokka(int a) { // tuodaan parametri a this->a = a; // sijoitetaan parametrina tuotu arvo omaan a-muuttujaan } };
Jos käytettävissä on valmiita luokkia ja oma luokkamme voisi käyttää jotain jo luotua luokkaa, on tyhmää keksiä pyörää uudelleen eli uudelleenkirjoittaa luokassa esitellyt attribuutit ja metodit omaan luokkaamme. Sen sijaan voimme koostaa oma luokkamme vanhasta kirjoittamalla:
class cOmaLuokka { int oma_muuttujamme; cVanhaLuokka meille_tarpeellinen; public: ... };
jolloin meillä on käytettävissä vanhan luokan ominaisuudet, ja tämä vanha luokka esiintyy oman luokkamme attribuuttina. Toinen vaihtoehto on periä oma luokkamme jo kirjoitetusta luokasta.
class cOmaLuokka : public cVanhaLuokka { int oma_muuttujamme; public: ... };
Perinnällä ja koostamisella on siis ainakin kirjoittamisen kannalta pieni ero. Kummallakin tavalla voidaa ottaa käyttöön vanhan luokan ominaisuudet, ja onkin aina tapauskohtaisesti ratkaistava kumpaa tapaa käytetään. Nyrkkisääntönä:
"Jos voi sanoa että lapsiluokka on (is-a) isäluokka, niin peritään.
Jos sanotaan että lapsiluokassa on (has-a) isäluokka, niin koostetaan "
Otetaan esimerkkinä oliot Pekka, koululaukku ja opiskelija. Nyt Pekka on varmasti opiskelija - siispä peritään Pekka yleisestä opiskelijasta. Koululaukku ei ole opiskelija, mutta opiskelijalla on koululaukku - siispä koostetaan opiskelija koululaukusta.
Lapsiluokka eli aliluokka on aina se joka perii, ja isä- eli yliluokka on se josta peritään. Siis esimerkissämme käytetään nimityksiä Pekka on opiskelijan aliluokka ja opiskelija on Pekan yliluokka.
Toisaalta voitaisiin määrittää luokka opiskelija siten, että kasataan olioiden Pekka ja Liisa yhteiset ominaisuudet (silmät, suu, koululaukku jne), ja näistä luodaan yliluokka opiskelija. Tällöin puhutaan yleistämisestä.
Moniperintä tarkoittaa, että aliluokka periytyy useammasta luokasta. Tätä ei yleensä suositella käytettäväksi, sillä seurauksena on helpommin kaaos kuin järjestelmän rakenteen yksinkertaistuminen.
Kun olio tai mikä tahansa muuttuja luodaan ts. siihen viitataan ensimmäisen kerran, täytyy sitä varten varata muistia ja mahdollisesti alustaa muuttuja jollain arvolla. Alustuksen hoitaa muuttujan muodostaja (konstruktori), jota kutsutaan automaattisesti.
String on esimerkki luokasta, jonka muodostaja mm. alustaa muuttujan sisällön tyhjäksi merkkijonoksi.
Vastaavasti omille luokille täytyy kirjoittaa muodostaja, joka määrää kuinka luokan attribuutit alustetaan. Esimerkiksi oman luokan kokonaislukumuuttujiin voidaan alustuksen yhteydessä viedä alkuarvot.
Seuraavassa esimerkki mahdollisimman yksinkertaisesta muodostajasta:
class cLaskuriLuokka { string laskettava; int maara; public: cLaskuriLuokka() {} // muodostaja };
Tämä muodostaja ei siis tee yhtään mitään; muodostajaa kutsutaan olion syntyessä, minkä jälkeen ohjelman suoritus jatkuu. Sen sijaan muodostaja
cLaskuriLuokka() { maara = 0; }
alustaa samalla maara-attribuutin arvon nollaksi, ja laskettava-merkkijono alustuu tyhjäksi. Sijoitusta
laskettava = "";ei tarvitse erikseen kirjoittaa, sillä string on olio, joka siis pitää itsestään huolen. Muodostajalle voidaan viedä parametreja kuten muillekin metodeille, esim.
cLaskuriLuokka(int alustus) { if (alustus > 0) maara = alustus; else maara = 0; }
alustaa attribuutin parametrin arvolla, jos tuotu parametri on positiivinen luku, muutoin attribuutti saa alkuarvon nolla. Yhdelle luokalle voidaan kirjoittaa monta muodostajaa.
class cLaskuriLuokka { string laskettava; int maara; public: cLaskuriLuokka() { maara = 0; } cLaskuriLuokka(int alustus) { if (alustus > 0) maara = alustus; else maara = 0; } };
Tällöin kutsutaan sitä muodostajaa jonka parametrilista täsmää kutsuun, siis pääohjelmasta
int main(void) { cLaskuriLuokka laskuri1, laskuri2(6), laskuri3; return 0; }
kutsutaan laskuri1- ja laskuri3-olioille ensimmäistä muodostajaa, ja laskuri2-oliolle jälkimmäistä muodostajaa. Huomaa, että kun kutsutaan muodostajaa jolle ei viedä parametreja, ei sulkuja kirjoiteta näkyviin! Kutsu
cLaskuriLuokka laskuri4();
ei nimittäin tarkoita muodostajan kutsua, vaan siinä esitellään laskuri4 -niminen funktio, jolle ei viedä parametreja, ja joka palauttaa jotain cLaskuriLuokka-tyyppistä! Siksi tämäkin lause menee kyllä kääntäjästä läpi ilman varoituksia, mutta sen toiminta ei vastaa tarkoitusta.
Muodostaja on hajoittimen(destruktori) ohella olion tärkein metodi. Hajoitin puolestaan toimii täsmälleen päinvastaisella tavalla kuin muodostaja, eli sitä kutsutaan kun olion elinkaari päättyy. Hajoittimessa huolehditaan mm. olion varaaman muistin vapauttamisesta. Hajottimen syntaksi on:
~cLaskuriLuokka() {}
Olion attribuutteihin ei koskaan pitäisi viitata olion ulkopuolelta. Jotta saadaan tietää olin attribuutin arvo, täytyy sitä varten kehittää (saanti)metodi.
class cLaskuriLuokka { string laskettava; int maara; public: cLaskuriLuokka() { maara = 0; } ~cLaskuriLuokka() {} int anna_maara() const { return maara; } };
Määre const metodin parametrilistan jälkeen tarkoittaa, ettei olion tila muutu kun metodia kutsutaan.
Olioden keskinäinen kommunikointi tapahtuu aina turvallisimmin saanti- ja muiden metodien avulla, mutta muitakin tapoja on. Esimerkiksi kaikki saman olioluokan ilmentymät ovat C++:ssa keskenään ystäviä, jolloin ne pääsevät käsiksi toistensa attribuutteihin. Tämä ominaisuus voi tosin aiheuttaa paljon harmaita hiuksia. Esimerkki:
class cLaskuriLuokka { string laskettava; int maara; public: cLaskuriLuokka() { maara = 0; } ~cLaskuriLuokka() {} void muuta_toista(cLaskuriLuokka &kaveri) { kaveri.maara = 5; } }; int main(void) { cLaskuriLuokka eka, toka; eka.muuta_toista(toka); return 0; }
Tässä toka-olion attribuuttia maara käytiin häikäilemättömästi muuttamassa.
Voisikin sanoa, että metodi on luokan metodi, joten se pääsee käsiksi kaikkiin luokan ilmentymiin (kunhan vain tietää mistä ne löytää eli osoittimen avulla). This ei siis ole sen kummempi erikoistapaus.
Olio-ohjelmointi noudattaa joissain määrin ihmisen loogista ajattelutapaa ja käsitystä maailman toiminnasta. Siinä lokeroidaan asiat luokikseen, jotka kommunikoivat keskenään tietyin ehdoin - juuri niinkuin me reaalimaailmassakin toimimme.
Perinteiseen ohjelmointiin verrattuna saavutamme paremmat mahdollisuudet ohjelman osien uudelleenkäyttöön, sillä muusta ympäristöstä erillisenä koodattu olio voidaan usein hyvin pienin muutoksin siirtää toiseen järjestelmään. Pienissä ohjelmissa (tai demotehtävissä) ei saavuteta tätä etua. Perintä, koostaminen, funktioiden kuormittaminen jne. tuntuvat turhita, kun kaiken vielä jaksaisi kirjoittaa omiksi metodeikseen ja olioikseen, mutta suurissa projekteissa näiden arvo on mittaamaton.
Näin ainakin teoriassa. Käytännössä on hirvittävän vaikea tehdä luokkia, jotka todella kelpaavat "uusiokäyttöön".
Lisää olion ja luokan määritelmästä sekä yleisestä terminologiasta monisteen luku 9.3
Paluu juurille, eli Johdatus ohjelmointiin -kurssin moniste
Viimeksi muutettu: su 10 helmikuu 2002 22:00:00
Minna Hillebrand <mmhilleb@cc.jyu.fi>
Alunperin kirjoitettu Ohjelmointi++ kurssille kesällä 2001
Perustuu osittain Antti-Juhani Kaijanahon kurssisivustoon. Tekstissä käytetty apuna kurssin
luentomonistetta, Johdatus ohjelmointiin -kurssin monistetta ja B. Stroustrupin C++ -kirjaa.