Abstraktit tietotyypit¶
Abstraktilla tietotyypillä (abstract data type, ADT) tarkoitetaan tietotyyppiä, joka määritellään tyypin mahdollisten arvojen ja näitä käsittelevien operaatioiden avulla. Oleellista on tyypin käyttäjän näkökulma: Käyttäjä pystyy käyttämään tyyppiä tarjottujen operaatioiden avulla. Käyttäjän ei siis tarvitse tietää, miten tyyppi ja sen operaatiot on määritelty.
Attention
Abstraktia tietotyyppiä ei pidä sekoittaa abstraktiin tyyppiin tai abstraktiin luokkaan, joita ei käsitellä tällä kurssilla.
Esimerkkejä abstrakteista tietotyypeistä¶
Esimerkiksi int
-tyyppiä voidaan ajatella abstraktina tietotyyppinä.
Ohjelmoija voi käyttää sitä esimerkiksi normaalien aritmeettisten
operaatioiden avulla (+
, -
, *
, /
) välittämättä siitä,
miten int
-tyyppi on toteutettu.
(Mahdollisia toteutustapoja voisivat olla yhden komplementti, kahden
komplementti ja BCD-koodi (binary coded decimal), mutta näistä ei
olla kiinnostuneita tällä kurssilla.)
Mahdollisten operaatioiden pitää olla järkeviä kyseiselle tyypille.
Esimerkiksi liikennevalotyypin mahdollisia arvoja voisivat olla punainen,
keltainen ja vihreä.
Tyyppi voitaisiin toteuttaa luettelotyyppinä (enum
), jolloin arvot
vastaavat kokonaislukuja 0, 1 ja 2.
Kuitenkaan yhteenlasku värien välillä ei olisi järkevä operaatio tälle
tyypille, mutta valon vaihtuminen toiseksi olisi.
Aikaisemmin olemme toteuttaneet pinoa muistuttavan korttipinon. Pinoa voidaan pitää abstraktina tietotyyppinä, jonka arvoina ovat pinoon talletettavat alkiot ja operaatioina push (alkion lisääminen pinon päällimmäiseksi) ja pop (päällimmäisen alkion poistaminen). Käytännössä tarvitaan myös operaatiot tyhjän pinon luomiseen ja tyhjyyden testaamiseen.
Edellä mainittu korttipino toteutettiin linkitettynä listana, mutta siitähän pinon käyttäjän ei tarvitse välittää. Pino voitaisiin toteuttaa myös taulukkona tai vektorina, mutta silloinkin operaatioiden push ja pop julkinen rajapinta pysyisi ennallaan.
Tiedon piilotus ja kapselointi¶
Abstraktit tietotyypit liittyvät läheisesti modulaarisuuteen, sillä abstraktit tietotyypit voidaan toteuttaa moduulien tai luokkien avulla. Nämä mahdollistavat julkisen rajapinnan määrittelyn, jonka avulla käyttäjä pääsee käyttämään abstraktia tietotyyppiä.
Abstraktin tietotyypin toteutus piilotetaan rajapinnan taakse, eikä käyttäjän tarvitse välittää siitä. Abstrakteihin tietotyyppeihin liittyykin läheisesti kaksi termiä: tiedon piilotus ja kapselointi.
Tiedon piilotuksella (information hiding) tarkoitetaan yleensä juuri toteutuksen yksityiskohtien piilottamista tai niihin pääsyn rajoittamista. Julkinen rajapinta paljastaa operaation nimen sekä kommentin, jossa on kerrottu, mitä operaatio tekee. Kommentissa kuvataan myös parametrit ja paluuarvo sekä niiden tarkoitus. Tämän pitäisi riittää käyttäjälle. Näin on tehty myös kirjastofunktioiden kohdalla. Käyttäjä pystyy käyttämään kirjastofunktioita tuntematta niiden tarkkaa toteutusta.
Tiedon piilottaminen voi koskea myös mitä tahansa funktioita.
Jos funktiolla on kuvaava nimi, sen voi ajatella kätkevän taakseen jonkin
tietyn toiminnallisuuden.
Esimerkiksi jos funktion nimi on print
, kutsujalle riittää tietää
nimi ja tarvittavat parametrit.
Kapselointi (encapsulation) taas tarkoittaa sitä, että data ja sitä käsittelevät operaatiot sidotaan tai paketoidaan yhteen. Näinhän juuri tehtiin edellä kuvatuissa esimerkeissä abstrakteista tietotyypeistä. Esimerkiksi pinototeutuksessa meillä on pinoluokka (jota voidaan käyttää tyypin tavoin) ja tämän luokan julkiset metodit. Luokka (samoin kuin moduuli) mahdollistaa siis kapseloinnin.
Abstraktit tietotyypit, modulaarisuus, kapselointi ja tiedon piilotus liittyvät siis hyvin läheisesti yhteen. Erona on oikeastaan näkökulma, mistä asiaa tarkastellaan.
Murtolukuesimerkki¶
Tämän kierroksen esimerkeistä löytyy hakemisto examples/11/fraction
,
joka sisältää murtolukuluokan Fraction
.
Murtolukuluokka on esimerkki abstraktista tietotyypistä. Samalla se on esimerkki operaattoreiden kuormittamisesta.
Murtolukuluokalla on kaksi attribuuttia: osoittaja (numerator_
) ja
nimittäjä (denominator_
).
Sillä on kuormitetut operaattorit yhtäsuuruusvertailua, yhteenlaskua ja
kertolaskua varten.
Kun yhteenlasku- ja kertolaskuoperaattorit kuormitetaan, niitä voidaan käyttää samoin kuin vastaavia operaattoreita käytetään primitiivisille tyypeille. Esimerkiksi:
Fraction frac1(2, 3); // luodaan murtoluku 2/3
Fraction frac2(3, 4); // luodaan murtoluku 3/4
Fraction frac3 = frac1 + frac2;
Toki olisi mahdollista käyttää operaattorifunktiota normaalin funktion tavoin:
frac3 = frac1.operator+(frac2);
Yllä olevasta kutsutavasta huomataan, että frac3
vastaa funktion
paluuarvoa, frac1
sitä oliota, johon kutsu kohdistuu, ja frac2
parametria.
Samoin tästä paljastuu, että yhteenlaskun ensimmäisen operandin tulee
olla olio eikä murtolukuliteraali.
Operaattoreiden kuormittaminen voitaisiin tehdä myös ilman tällaista
rajoitetta, mutta tämä riittää tässä tapauksessa.
Huomaa, että kuormitettuja operaattoreita voidaan yhdistellä tavanomaiseen tapaan, eli samassa lausekkeessa voi olla useita yhteenlasku- ja kertolaskuoperaattoreita.
Myös tulostusoperaattori <<
on kuormitettu.
Tulostusoperaattorin kuormittamisella on se etu, että myös ohjelmoijan
määrittelemien abstraktien tietotyyppien ilmentymiä pystytään tulostamaan
samaan tapaan kuin muita tulostettavia arvoja.
Esimerkiksi:
Fraction frac(2, 3); // luodaan murtoluku 2/3
cout << "Murtoluvun frac arvo on " << frac << endl;
Operaattorin <<
kuormittaminen tapahtuu eri tavalla kuin
laskutoimitusten kuormittaminen.
Operaattori <<
on luokan ostream
metodi (ja cout
on tämän
luokan ilmentymä).
Kohdeoliona ei siis nyt olekaan Fraction
-luokan olio.
Jotta yllä oleva, tulostamiselle luonteva käyttötapa olisi mahdollista,
kuormitettu tulostusfunktio kannattaa toteuttaa itsenäisenä funktiona,
joka ei ole minkään luokan metodi.
Tulostusfunktiossa joudutaan kuitenkin käyttämään Fraction
-luokan
yksityisten attribuuttien numerator_
ja denominator_
arvoja.
Tätä varten tarvitaan metodit getNumerator
ja getDenominator
.
Tulostusfunktion ensimmäisenä parametrina on tulostuvirta ja toisena
ohjelmoijan määrittelemän luokan ilmentymä eli tässä tapauksessa luokan
Fraction
ilmentymä.
Huomaa myös, että tulostusfunktio palauttaa parametrina saamansa
tulostusvirran.
Tämä mahdollistaa operaattorin <<
ketjuttamisen, mikä näkyy
edellisessä koodin pätkässä.
Vastaavalla myös lukuoperaattori >>
voitaisiin kuormittaa.
Näin ole esimerkissä kuitenkaan tehty, vaan murtolukujen lukeminen
tapahtuu pääohjelmassa.
Syynä tähän on se, että on haluttu tarkistaa, ettei nimittäjäksi
anneta nollaa.
Murtolukuluokalla on myös kaksi yksityistä metodia: gcd
ja reduce
.
Metodi reduce
muuttaa murtoluvun mahdollisimman supistettuun muotoon.
Tätä tarvitaan esimerkiksi yhtäsuuruusvertailussa.
Samoin yhteen- ja kertolaskun tulos annetaan mahdollisimman supistetussa
muodossa.
Supistaminen tapahtuu niin, että lasketaan osoittajan ja nimittäjän suurin
yhteinen tekijä (greatest common divisor, gcd), ja molemmat jaetaan tällä
tekijällä.
Tässä tarvitaan metodia gcd
, joka on toteutettu rekursiivisesti.
Kaiken kaikkiaan esimerkki näyttää, miten ohjelmoijan itsensä kirjoittamaa luokkaa ja tämän luokan olioita voidaan käyttää hyvin samaan tapaan kuin kielen valmiita primitiivisiä tyyppejä.
Toteutettu Fraction
-luokka ei kuitenkaan ole täydellinen.
Kun yhteenlaskuoperaattori on kuormitettu, jolloin +
-symbolia
voi käyttää tavalliseen tapaan, luokan käyttäjä voi helposti kuvitella,
että myös operaattoria +=
voisi käyttää kuten yleensä.
Näin ei kuitenkaan ole, sillä kyseistä operaattoria ei ole kuormitettu.
Ylimääräisenä harjoitustehtävänä voit toteuttaa kyseisen operaattorifunktion.