Juha Niemimäki
Olio-ohjelmointia C++-kielellä

Alkusanat
Jutun kirjoittaja haluaa painottaa, ettei hän ole mikään koodiguru, joten erinäiset virheet tekstissä ja käsitteissä voivat olla mahdollisia. Rakentavaa kritiikkiä otetaan myös vastaan. Aihe on laaja ja hankalahko, ja esitettävien asioiden valitseminen on haastavaa. Onneksi aiheesta on olemassa paljon tasokasta kirjallisuutta, jonka avulla voi jatkaa opiskelua alkukipinän saatuaan.

Tekstissä oletetaan, että lukija ymmärtää ohjelmoinnin perusteista jotakin, C-kielen (osoittimet) ja Java-kielen (olio-ominaisuudet) osaaminen auttaa varmasti paljon. Esimerkiksi varhaisissa Saku-lehdissä ja Mikrobitissä on käsitelty C-ohjelmointia, joten sieltä on hyvä kerrata perusasioita. Esimerkit on testattu GCC-kääntäjän avulla, joka on Amiga OS -ympäristöön saatavana GeekGadgets-projektista.
Nimeämiskäytäntö esimerkeissä
Artikkelin ohjelmointiesimerkeissä luokkien nimet alkavat isolla alkukirjaimella, ja jos ne sisältävät useita sanoja, jokainen niistä alkaa isolla kirjaimella (esim. class FileHandler). Muuttujien ja funktioiden nimet aloitetaan pienellä alkukirjaimella, mutta jokainen eri sana alkaa jälleen isolla kirjaimella (esim. int energyLeft, tai void loadHighScores()). Nimeämiskäytäntö muistuttaa Javan tyyliä. Luokkien jäsenmuuttujien (member variable) eteen lisätään "m_" (esim. m_energyLeft) ja jos kyseessä on osoitin (pointer), niin muuttuja saa vielä etuliitteen "p" (esim. FileHandler* pFileHandler, tai jäsenmuuttujan tapauksessa m_pFileHandler).

Kuten arvata saattaa, nimeämiskäytäntöä voisi jalostaa loputtomiin, mutta kannattaa noudattaa jonkinlaista yhtäläisyyttä projektissa, jolloin koodi on paremmin luettavissa ja ymmärrettävissä.

C++-koodia sisältävät tiedostot tallennetaan yleensä päättellä ".cpp", ja vastaavat header-tiedostot (jotka sisältävät esim. luokkien määrittelyn), päätteellä ".h". Tosin tämän jutun esimerkeissä kaikki on luettavuuden takia yhdessä tiedostossa, koska esimerkkien koodi on loppujen lopuksi aika lyhyt.

Tämä artikkelin esimerkeissä on lisäksi epätavallinen puoli, että kaikki luokkien jäsenfunktiot on toteutettu luokkamäärittelyn sisällä. Tämä aiheuttaa sen, että kääntäjä yrittää tulkita niitä ns. inline-funktioina, ja se voi vaikuttaa koodin kokoon (sekä myöskin suoritusnopeuteen) negatiivisesti.
Mikä on C++-kieli?
C++-kieli on suosittu ohjelmointikieli, joka laajentaa C-kieltä oliokäsitteillä. Kieli on alunperin Bjarne Stroustrup -nimisen kaverin kehittämä, ja sittemmin myös standardoitu. C++-kieltä on perinteisesti käytetty aika vähän Amigaa ohjelmoitaessa, johtuen ehkä osaksi siitä, että kääntäjistä GCC lienee parhaiten C++-yhteensopiva, ja kirjallisuutta tai esimerkkejä ei juurikaan ole. Lisäksi C++:aa usein haukutaan tehottomaksi, mutta on ohjelmoijan päätettävissä, haluaako hän käyttää C++:n kaikkia ominaisuuksia vai ei. "Sitä saa mitä tilaa".

C++:aan kytkeytyy läheisesti STL-kirjasto (Standard Template Library), jonka avulla voi välttää pyörän keksimisen uudelleen, ja ohjelmoija voi keskittyä ennemminkin oman ohjelmansa kehittämiseen.
Mitä ovat luokat ja oliot?
Ohjelmointiprojektien kasvaessa niiden hallinta vaikeutuu. Jos ohjelmassa on tuhansia rivejä koodia ja se on toteutettu perinteisin strukturaalisen ohjelmoinnin keinoin, esim. C-kielellä, sen ylläpidettävyys ja ymmärrettävyys heikkenee.

Oliot (objects) auttavat ihmismieltä hahmottamaan asioita selkeämmin, ja pilkkomaan tietoa ja toiminnallisuutta loogisiin yksiköihin. Oliot voivat siis sisältää tietoa (jäsenmuuttujia) ja koodia (jäsenfunktioita, usein puhutaan myös metodeista), ja sen sisältämät ominaisuudet voivat olla muiden olioiden käytettävissä, tai niiltä piilotettuina. C++-kielessä olioita luodaan luokkien avulla. Luokat (class) ovat abstrakteja tietotyyppejä, ja toimivat ikäänkuin malleina tai muotteina olioita luotaessa.

Olio siis kuvataan luokan avulla, olio on luokan ilmentymä (instance). Luokka voidaan periä toisesta luokasta (inheritance), jolloin yläluokan ominaisuudet siirtyvät perivän luokan käytettäväksi. Tällä tavoin voidaan muodostaa hierarkisia rakenteita, ja ennenkaikkea uudelleenkäytettävää ja paremmin ylläpidettävää koodia.

Esimerkiksi suunniteltaessa piirto-ohjelmaa, voidaan sen graafiset elementit kuvata mm. seuraavanlaisen luokkarakenteen avulla:


 
Element
 
LineElement
CircleElement
TextElement

Viiva-, ympyrä- ja tekstielementit sisältävät samankaltaista tietoa, ja lähinnä niiden piirtämistoteutus eroaa, joten tässä tapauksessa voisi olla fiksua määritellä graafisten elementtien ominaisuudet yläluokkaan (Element), periä ne, ja toteuttaa eri elementeille niiden vaatima piirtotapa.

> Esimerkki luokan määrittelystä C++:ssa
(//-merkit listauksessa tarkoittavat kommenttirivin alkamista)

Java-ohjelmoille luokka näyttänee tutunpuoleiselta, kun taas C-koodaajat löytänevät yhtäläisyyttä C:n struct-käsitteeseen, jonka avulla voidaan koostaa uusia tietotyyppejä. C++:ssa struct- ja class-rakenteiden ero onkin vähäinen, sillä molemmat voivat sisältää tiedon lisäksi myös jäsenfunktiota. Erona on se, että structin jäsenet ovat oletuksellisesti julkisia (public), kun taas luokan jäsenet yksityisiä (private), jolloin niihin ei pääse suoraan käsiksi luokan ulkopuolelta, vaan luokkaan täytyy rakentaa rajapinta get ja set -tyyppisten funktioiden avulla, jotka on määritelty julkisiksi.

Esimerkissämme muuttujat ovat private-tyyppisiä, koska se on luokan oletusarvo. get ja set -funktiot on määritelty julkisiksi public:-avainsanan avulla. Tieto on siis kapseloitu luokan sisään, ja siihen pääsee käsiksi vain sovitulla tavalla. Jos jäsenmuuttujat olisi esitelty public:-sanan jälkeen, niin niitä voisi käyttää suoraan, esim:



Esimerkissämme oikea tapa olisi:



Kuten aiemmin mainittiin, luokkien jäsenfunktioiden toteutus kannattaa jättää luokkamäärittelyn ulkopuolelle. Inline-funktioiden käyttö on perusteltua, mutta kannattaa muistaa, että ne kasvattavat koodia, ja että kovin monimutkaisiin funktioihin inline-tekniikka ei sovellu. Get ja set -tyyppiset asetus/lukufunktiot voisivat yksinkertaisuudestaan johtuen ollakin "inline-kandidaatteja". Alla on uusi versio, jossa toteutus on omassa tiedostossaan, kuten pitääkin.

> Pacman.h-tiedoston sisältö
> Pacman.cpp-tiedoston sisältö

Muodostimen (constructor) ja hajottimen (destructor) merkitys on olion luomisessa ja tuhoamisessa. Kyseessä ovat mekanismit, joita kutsutaan kun luodaan olio, jotta se voidaan alustaa (parametrein tai ilman), ja oliota tuhottaessa voidaan siivota sen jälkiä, esimerkiksi vapauttamalla dynaamisesti varattuja muistialueita jne. Jos käyttäjä ei määrittele muodostinta, niin kääntäjä luo oletusmuodostimen automaattisesti. Oletusmuodostimella ei ole yhtään parametriä.
Esimerkkiohjelma 1: "Terve maailma!".
Ohjelmaa käyttää cout ja cin -syöte- ja tulostusvirtoja muuttujien lukemiseen näppäimistöltä ja tulostamiseen näytölle. "endl" tarkoittaa rivinlopetusmerkkiä ('\n').

> esim1.cpp

Esimerkki 1 on kokonaisuudessaan tiedostossa esim1.cpp, ja se käännetään ajettavaksi tiedostoksi esimerkiksi GCC:tä käyttäen kutsumalla:



Jos kaikki meni hyvin, niin nyt voit kokeilla mitä ohjelma tekee. Ohjelmassa luodaan dynaamisesti olio new-operaattorin avulla, joten se pitää muistaa myös tuhota (delete). New/delete-pari muistuttaa hieman malloc()/free() -funktioita C-kielen puolelta.

On myös mahdollista varata oliolle tilaa pinosta, jolloin new/delete-operaattoreita ei tarvita. Esimerkiksi näin:



Huom: '*'-operaattoria ei tarvita, koska kyseessä ei ole osoitin, vaan viittaus olioon.

Nyt olio tuhotaan automaattisesti kun poistutaan main()-funktiosta, jonka näkyvyysalueella (scope) se luotiin. Tällä tavalla luodulle oliolle kutsutaan jäsenfunktiota pisteoperaattorin avulla (aivan kuten Javassa), eli:



Delete-operaattoria käytettäessä kannattaa vielä asettaa osoittimen arvo NULL-arvoon tuhoamisen jälkeen. Näin voidaan välttyä esimerkiksi olion uudelleentuhoamisyritykseltä, josta seuraisi takuuvarmasti ongelmia. Jos new:llä varattua muistia ei vapauteta deleten avulla, niin seuraa muistivuoto, aivan kuten jos unohtaa free()-kutsun malloc()-kutsun jälkeen. Javassa taas on automaattinen roskienkeruu (garbage collection), joka hoitaa muistinvapautuksen.

Oliokäsitteisiin kuuluu tärkeänä osa-alueena perintä, joka mahdollistaa ohjelmien modulaarisen suunnittelun. Usein yläluokkaan kerätään kaikille olioille yhteisiä ominaisuuksia, ja periytyvästä luokasta tehdään erikoistuvampi. Jos ajatellaan vaikkapa kissaeläimiä, niin ensinnäkin kissaeläimille tyypillisiä ominaisuuksia sisältävä luokka Kissaeläin voitaisiin periä Eläin-luokasta, ja edelleen Leijona ja Ilves -luokat voidaan periä Kissaeläimestä. Periytymistä voidaan kuvata "on"-sanalla: "Leijona on Kissaeläin", tai "Leijona on eräänlainen Kissaeläin".
Esimerkkiohjelma 2: perintä
> esim2.cpp

Ohjelmassa luodaan kaksi erilaista oliota, joista Lion on lievästi erikoistettu versio Animal-tyypistä - tiedetään, että leijonat syövät yleensä lihaa. Perintä ilmaistaan luokan määrittelyn alussa, lisäämällä kaksoispiste, suojausmääre ja perittävän luokan nimi. C++:ssa on mahdollista käyttää moniperintää, mutta yleisesti se ei ole kovin suositeltavaa ja voi aiheuttaa turhaa monimutkaisuutta.

Toisessa esimerkissämme käytetään uudenlaista näkyvyysmäärettä nimeltä protected. Protected-sanan merkitys on rajoittavampi kuin publicin, mutta toisaalta kevyempi kuin privaten. Protected mahdollistaa sen, että yläluokan jäsenmuuttujia voidaan käyttää suoraan perivissä luokassa. Voit kokeilla ottaa protected-määrityksen pois Animal-luokasta, ja katsoa mitä tapahtuu. Esimerkkiohjelman Lion-luokan muodostimessa kutsutaan yläluokan muodostinta Animal().
Esimerkkiohjelma 3: olioiden monimuotoisuus
Monimuotoisuus (polymorfismi) viittaa olioiden kykyyn toimia eri tavoin ja muuntautumiskykyyn. Siihen liittyviä käsitteitä ovat ylikuormittaminen (overloading) ja uudelleenmäärittely (overriding). Ylikuormittaminen tarkoittaa, että funktiota voidaan kutsua erityyppisillä parametreillä (esimerkiksi int tai float), ja tämä tapahtuu käytännössä kirjoittamalla funktiosta kaksi eri versiota, esim:



C-kielellä tämä ei onnistuisi ihan yhtä tyylikkäästi, vaan funktiot pitäisi määritellä eri nimisiksi, jotta kääntäjä olisi tyytyväinen. Uudelleenmäärittely tarkoittaa sitä, että yläluokan määrittelemä funktio kirjoitetaan uudelleen aliluokassa. Mitä hyötyä tästä on? Tämä liittyy luokkien erikoistamiseen. Ajatellaanpa vaikka eläinten liikkumista: jalalliset oliot kävelevät, siivekkäät yleensä lentävät, ja käärmeet luikertelevat. Olioilla voisi olla yksi, samanniminen funktio, esimerkiksi liiku(), jolla olio kuin olio saadaan liikkumaan. Hyödyllisempi esimerkki on tiedoston luku tai tallennus vaikkapa vektorigrafiikkaan perustuvassa piirto-ohjelmassa. Piirto-ohjelman dokumentti sisältää erityyppisiä olioita. Jokaisella oliolla on jäsenfunktiot lue() ja kirjoita(), jotka määrittelevät miten ko. olio luetaan tiedostosta tai tallennetaan. Nyt dokumentin ei tarvitse tietää, minkä tyyppisiä olioita sen pitää tallentaa, tai miten. Jokaiselle oliolle vain kutsutaan vuorotellen kirjoita()-funktiota, ja jokainen olio huolehtii oman datansa kirjoittamisesta tiedostoon, omalla tavallaan.

Java-kielellä ohjelmoineet ovat todennäköisesti törmänneet käsitteeseen abstract. Javassa jonkin luokan metodin määritteleminen abstraktiksi johtaa ko. luokan muuttumiseen abstraktiksi. Abstraktista luokasta ei voi muodostaa oliota, vaan niitä käytetään esim. yhteisten rajapintojen määrittelyihin. C++ käyttää termiä virtuaalifunktio, tai erityisesti puhdas virtuaalifunktio (pure virtual) abstraktien luokkien määrittelyyn. Puhtaat (tai avoimet) virtuaalifunktiot pakottavat ohjelmoijan toteuttamaan ko. funktiot aliluokissa. Puhdas virtuaalifunktio määritellään seuraanvanlaisesti:



Eli käyttämällä virtual-määrettä, ja asettamalla funktion runko "nollaksi". Pelkkää virtual-määrettä käytetään merkkaamaan alaluokan mahdollisuutta ylikirjoittaa merkattu jäsenfunktio. Tämä liittyy myöhäisen sidonnan mekaniikkaan, joka mahdollistaa sen, että voidaan kutsua oikeaa versiota funktiosta ajon aikana. Esimerkkinä tilanne, jossa on on luotu kaksi erityyppistä oliota, joista toinen on peritty toisesta, ja molemmat toteuttavat puhu()-funktion.

> esim3.cpp

Ohjelmassa kutsutaan molemmille oliolle puhu()-funktiota, mutta tulos ei ole ihan haluttu, koska vaavi-olion on kerrottu olevan "eräänlainen mamma" - mikä on mahdollista, koska se on peritty Mother-luokasta. Ohjelmaa ajettaessa käy nyt niin, että vaavi puhuukin samoin kuin mamma, koska oliolle ei ole suoraan kerrottu että sen pitäisi käyttää Baby-luokan puhu()-funktiota Mother-luokan sijaan:



Tästä ongelmasta päästään määrittelemällä yläluokan puhu()-jäsenfunktio virtuaaliseksi:



On huomattavaa, että virtuaalifunktioita käytettäessä on syytä määritellä myös luokan hajotin virtuaaliseksi, koska muuten voi olla, että tuhottaessa oliota ei käydäkään läpi koko perimähierarkiaa, ja osa siivouksesta jää tekemättä:

Esimerkkiohjelma 4: abstraktin yläluokan periminen
> esim4.cpp

Esimerkissä käytetään Shape-rajapintaluokkaa määrittelemään alaluokille yhteiset toiminnot, mutta samalla pakotetaan eri olioita toteuttamaan oma yksilöllinen piirtototeutuksensa - joka tosin tässä tapauksessa jää vain jokaisen mielikuvituksen varaan. Pääohjelmassa käytetään eri tyyppisiä olioita yläluokan osoittimen (Shape *) kautta, mutta tällä kertaa piirra()-funktio kohdistuu oikeisiin olioihin, virtual-mekanismin avulla.
Esimerkkiohjelma 5: käyttöjärjestelmäkutsuja mukaan
> esim5.cpp

Viimeinkin ohjelma, joka näyttäisi tekevän jotain konkreettista. Ohjelma käyttää Dos ja Intuition -libraryjen kutsuja ikkunoiden avaamiseen, sulkemiseen ja pienen viiveen aiheuttamiseen. Jokainen luotu MyWindow-olio avaa muodostimessaan uuden ikkunan, ja sulkee sen tuhoutuessaan hajottimessaan. MyWindow-luokalle on määritelty yksi staattinen muuttuja, joka pitää lukua luotujen ikkunoiden lukumäärästä. Huomattavaa on oliotaulukon käsittely, erityisesti tuhottaessa pitää muista hakasulut, sillä muutoin tuhotaankin vain taulukon ensimmäinen alkio ja tässä tapauksessa loput 9 MyWindow-oliota jäisivät tuhoamatta aiheuttaen muistivuodon (lisäksi myös loput ikkunoista jäisi sulkematta).
Loppusanat
Toivon mukaan artikkelista oli jollekin jotain hyötyä. Aiheen laajuuden vuoksi käsittelemättä jäi lukuisia asioita, kuten operaattoreiden ylikuormitus, STL, templatet, nimiavaruudet, poikkeuskäsittely, suunnittelumallit jne. mutta onneksi lisätietoa löytyy runsaasti kirjallisuuden puolelta, ja onpa osa kirjoista saatavana ilmaiseksi myös Internetistä. Hauskaa loppukesää :)
Kirjallisuutta
  • The C++ Programming Language (Stroustrup, Bjarne). Myös suomennettu.
  • Effective C++ (Meyers, Scott).
  • C++-ohjelmointi (Hietanen, Päivi)
  • Pieni oliokirja ja Oliokirja (Koskimies, Kai)
Linkit

Sivun alkuun