Tietokoneen käytön ja ohjelmoinnin alkeet

Kurssin kotisivulle


3 Muuttujat ja tietorakenteet

Kaikissa ohjelmointikielissä tarvitaan muuttujia, joihin voi tallettaa erilaisia asioita. Muuttujien määrittely tapahtuu eri kielissä eri tavoin, mutta ne toteutetaan fyysisellä laitteistolla, joka tukee vain muutamia talletustapoja. Ohjelmoijan ei useinkaan tarvitse tietää, missä muodossa esimerkiksi reaaliluvut on talletettu. Numeerisissa sovelluksissa on kuitenkin muistettava, mitä rajoituksia laitteisto voi asettaa.

Binääri-, oktaali- ja heksadesimaaliluvut

Tietokoneet perustuvat 2-kantaiselle eli binääriselle lukujärjestelmälle. Käytännön syynä on, että näin tarvitsee erottaa vain kaksi erilaista tilaa (virta kulkee tai ei kulje) tai jännitetasoa ja laskutoimitukset ovat hyvin yksinkertaisia. Ohjelmoija käyttäjästä puhumattakaan joutuu aniharvoin tekemisiin binäärilukujen kanssa.

Desimaalilukuja vastaavat binääriluvut ovat yleensä pitkiä ja siten melko epähavainnollisia. Hieman ymmärrettävämpiä ovat 8-kantaiset oktaaliluvut ja 16-kantaiset heksadesimaaliluvut, jotka kuitenkin vastaavat hyvin yksinkertaisella tavalla binäärilukuja. Näitä käytetäänkin joissakin yhteyksissä tavallaan binäärilukujen luettevampina korvikkeina.

Jos kantaluku ei ole itsestään selvä, se voidaan ilmoittaa alaindeksinä. Esimerkiksi 148 tarkoittaa, että kyseessä on oktaaliluku, jonka arvo 10-järjestelmässä on 1210.

Seuraavassa taulukossa on lukujen 0--15 vastineet eri järjestelmissä.

  0  0000   0   0
  1  0001   1   1
  2  0010   2   2
  3  0011   3   3
  4  0100   4   4
  5  0101   5   5
  6  0110   6   6
  7  0111   7   7
  8  1000  10   8
  9  1001  11   9
 10  1010  12   A
 11  1011  13   B
 12  1100  14   C
 13  1101  15   D
 14  1110  16   E
 15  1111  17   F

Merkkikoodit

Kirjaimet ja muut merkit esitetään tietokoneessa binäärilukuina. Valitettavasti erilaisia koodaustapoja on lukuisia.

Vakiintunein on 7 bitin ASCII-koodi, joka käsittää latinalaisten kirjainten, numeroiden, välimerkkien ja joidenkin erikoismerkkien koodit. Erilaisia koodeja on 27=128 kappaletta. Näistä kuitenkin osa (koodit 0--31 ja 127) on kirjoittumattomia ohjausmerkkejä, joten vain 95 koodia on käytettävissä tavallisille merkeille. Skandinaaviset kirjaimet ja muut aksentteja ja diakriittisiä merkkejä sisältävät kirjaimet eivät mahdu joukkoon. Aikaisemmin ongelma kierrettiin korvaamalla jotkin erikoismerkit kuten aalto- ja hakasulut ä- ja ö-kirjaimilla. Nykyisin näin ei enää tehdä, ja siksi joissakin vanhoissa pc-tiedostoissa saattaa tulla vastaan sulkumerkkejä skandikirjainten paikalla.

Uudemmassa 8 bitin koodissa koodeja on kaksinkertainen määrä, mikä riittää eurooppalaisten kielten aksentillisille merkeille Suomessa käytetään standardin ISO 8859-1 mukaista koodistoa.

UTF (Unicode Transformation Format) on uudempi merkkikoodisto, jonka avulla voi esittää myös monien eri kirjaimistojen merkkejä. UTF-8 käyttää 1--4 tavua yhden merkin esittämiseen. Uusissa Linuxeissa tämä on oletuksena, joten vanhemmat tiedostot joutuu konvertoimaan tai määrittelemään merkkikoodistoksi aikaisemman ISO-koodiston. Teki kummin tahansa, joidenkin ohjelmien kanssa voi tulla ongelmia.

Yksinkertaiset muuttujat

Tietokoneen muistin pienin yksikkö, johon voidaan suoraan osoittaa, on yleensä tavu (byte), jossa on kahdeksan 2-kantaista lukua eli bittiä. Tavu on siis pienin muistin osa, jolla on oma osoite. Joissakin tietokonearkkitehtuureissa pienin yksikkö voi olla myös pitempi sana; tällaisia ovat olleet erityisesti numeerisiin tehtäviin tarkoitetut supertietokoneet, kuten Cray.

Kokonaisluvun (integer) esittämiseen käytetään tavallisesti kahta tavua. Yksi bitti varataan etumerkille, jolloin kokonaisluvun itseisarvo voi olla korkeintaan 215-1=32767. C:ssä on mahdollista määritellä myös etumerkittömiä (unsigned) kokonaislukuja, jolloin suurin luku on 216-1. Kovin suurta lukua tavalliseen kokonaislukumuuttujaan ei siis mahdu. Hallinnollisissa ohjelmissa tämä ei yleensä ole ongelma, mutta numeerisissa tehtävissä raja voi tulla vastaan.

Käytettävissä voi olla myös 4 tavun mittaisia kaksoistarkkuuden kokonaislukuja, jolloin suurin esitettävissä oleva luku on 231-1=2 147 483 647.

Symbolisen matematiikan ohjelmat ja jotkin muut erityisohjelmat osaavat käsitellä myös "mielivaltaisen tarkkuuden" lukuja. Silloin ne kuitenkin vaativat jotakin erikoista talletustapaa, jolloin laskutoimitukset tulevat hyvin hitaiksi.

Reaaliluvut (real) vievät tavallisesti tilaa 4 tavua ja talletetaan liukulukumuodossa. Muuttujassa on silloin seuraavat osat:

Esimerkiksi PC:n yksinkertaisen tarkkuuden reaaliluvun mantissa on 23 bittiä. Desimaalipisteen jälkeinen bitti on aina ykkönen, joten sitä ei tarvitse tallettaa. Tarkkuus on siten 24 bittiä eli log10224 ~ 7 desimaalia. Eksponenttiosan pituus on 8 bittiä ja se talletetaan muodossa e = 127 (bias) + todellinen eksponentti. Eksponenttiosa on siis välillä 1 <= e <= 254. Tällä tavoin esitettävissä oleva lukualue on 2-126 ~ 10-38 -- 2128 ~ 3x1038. Lukualue on riittävä useimpien todellisten suureiden ja mittaustulosten esittämiseen. Laskennassa syntyvät välitulokset saattavat kyllä aiheuttaa ylivuotoja, mutta järkevällä ohjelmoinnilla ja muuttujien skaalauksilla nämä ongelmat voidaan yleensä välttää. Seitsemän merkitsevän numeron tarkkuus sen sijaan ei ole kaikkiin tarkoituksiin riittävä.

Kaksoistarkkuuden reaaliluvuissa (double) merkitsevien numeroiden määrä on noin kaksinkertainen ja mahdollisesti myös lukualue laajempi. Esimerkiksi PC:n kaksoistarkkuuden muuttujien tarkkuus on noin 15 desimaalia ja lukualue noin 10-308 ... 10308. Lukualue on siis vähintäänkin riittävä. Jos tuloksena on lukuja tämän välin ulkopuolelta, algoritmi on syytä miettiä uusiksi. Lukujen äärellinen esitystarkkuus saattaa joskus aiheuttaa mutkikkaampia ongelmia, joihin palataan myöhemmin.

PC:ssä käytetään IEEE:n standardoimaa aritmetiikkaa, jonka lukualuetta on täydennetty kahdella erikoisarvolla, Inf ja NaN. Esimerkiksi nollalla jakaminen ei kaada ohjelmaa, vaan tuloksena on ääretön, Inf. Laskutoimituksen tulos voi olla myös epämääräinen, kuten jaettaessa nolla nollalla tai ääretön äärettömällä. Silloin tulos on NaN (Not a Number) eli epäkelpo luku. Jos ohjelman tuottama tulos on Inf tai NaN, algoritmia on kyllä syytä tarkistaa.

Looginen muuttuja (logical) voi saada vain arvot tosi (true) tai epätosi (false). Muuttuja arvon esittämiseen tarvitaan vain yhtä bittiä, mutta käytännössä sille varataan tilaa kokonainen tavu.

Merkki (character) on yleensä yhden tavun mittainen ja sisältää yhden merkin merkkikoodin.

Taulukot

Yksinkertaisista muuttujista voidaan muodostaa mutkikkaampia tietorakenteita, kuten taulukoita tai tietueita.

Taulukossa (array) on joukko samantyyppisiä muuttujia, jotka yleensä talletetaan peräkkäin muistiin. Taulukon määrittelyn yhteydessä ilmoitetaan taulukon koko. Taulukon alkioon viitataan indeksin avulla, C:ssä esimerkiksi a[1] ja Fortranissa a(1). C:ssä taulukon indeksointi alkaa aina nollasta, mikä voi joskus johtaa ikävään indeksiaritmetiikkaan. Vanhimmissa Fortranin versioissa alaraja oli 1, mikä aiheutti vielä enemmän harmeja. Fortran 77:stä lähtien taulukon alarajan on voinut määritellä vapaasti, jolloin taulukko saadaan vastaamaan tehtävää luontevimmalla tavalla.

Taulukkoviittauksessa indeksit ovat usein muuttujia, jotka saavat arvonsa vasta ohjelmaa suoritettaessa. Jos ohjelma on virheellinen, indeksit saattavat saada arvoja, jotka viittaavat taulukon ulkopuolelle. Toteutuksesta riippuu, mitä silloin tapahtuu. Suoritusaikainen tarkistus tietää lisää työtä, kun indeksiä verrataan taulukon määrittelyyn. Siksi monissa toteutuksissa tarkistusta ei tehdä lainkaan, ellei sitä erikseen vaadita sopivalla kääntäjän valitsimella. Kun viittaus kohdistuu taulukon alkion sijasta jonnekin muualle muistiin, tulokset voivat olla erikoisia. Parhaassa tapauksessa suoritus päättyy virhetilanteeseen (kuten Unixin legendaarinen 'segmentation fault'), mutta joskus ohjelma saattaa jatkaa toimintaansa ja tuottaa omituisia tai pahimmassa tapauksessa virheellisiä vaikkakin uskottavan tuntuisia tuloksia.

Taulukot voivat olla myös useampiulotteisia, jolloin yhteen alkioon viittaamiseksi tarvitaan useampia indeksejä. C:ssä kaksiulotteisen taulukon alkio voisi olla a[1][5] ja Fortranissa a(1,5).

C:ssä ja monissa muissakin kielissä useampiulotteiset taulukot talletetaan muistiin niin, että viimeinen indeksi muuttuu nopeimmin, joten esimerkiksi alkiot a[1][3] ja a[1][4] ovat peräkkäisissä muistipaikoissa. Kaksiulotteiset taulukot talletetaan siis vaakariveittäin, joten indeksointi vastaa matematiikan matriisinotaatiota.

Fortran on tässä suhteessa poikkeus; siinä ensimmäinen indeksi kasvaa nopeimmin ja kaksiulotteiset taulukot talletetaan sarakkeittain. Taulukon alkiota a(1,3) seuraava alkio muistissa on a(2,3).


Useampiulotteisten taulukkojen talletustapa on toteutettu eri kielissä eri tavoin. Esimerkiksi kaksiulotteinen taulukko talletetaan C-kielessä ja monissa muissakin kielissä vaakariveittäin, mutta Fortranissa sarakkeittain. C:ssä indeksointi alkaa aina nollasta. Fortranin vanhimmissa versioissa indeksointi alkoi ykkösestä. Nykyisin indeksin alaraja voidaan valita vapaasti, mutta oletusarvo on yksi, ellei sitä ole ilmoitettu taulukon määrittelyssä.


Tietueet

Tietue (record, structure) on taulukkoa yleisempi tietotyyppi. Se käsittää joukon muuttujia, jotka voivat olla eri tyyppisiä. Tietuiden avulla voidaan määritellä omaan tehtävään luonnollisella tavalla liittyviä abstrakteja tietotyyppejä.

Esimerkiksi C-kielessä voitaisiin määritellä tietuetyyppi paikka, joka sisältää paikkakunnan nimen merkkijonona, pituus- ja leveysasteet reaalilukuina ja aikavyöhykkeen kokonaislukuna:

struct paikka { char *nimi;
                float pituus, leveys;
                int aikavyohyke ; } ;

Tämän jälkeen voidaan määritellä, että muuttujat a ja b ovat tyyppiä paikka sanomalla

  struct paikka a, b ;

Nyt muuttujan a sisältö voidaan kopioida muuttujaan b lauseella b=a tarvitsematta käsitellä erikseen kaikkia paikan komponentteja.

Osoittimet

Osoitin (pointer) sisältää muuttujan osoitteen ja mahdollisesti jotakin muuta tietoa, kuten minkä tyyppiseen olioon osoitin voi viitata.

Osoittimilla on paljon käyttöä käsiteltäessä linkitettyjä tietorakenteita (seuraava kappale).

Osoittimiin liittyy muutama ongelma, joihin voi törmätä erityisesti linkitettyjen rakenteiden yhteydessä. Muuttujalle tai tietueelle varattu tila voidaan vapauttaa ohjelmassa eksplisiittisesti sopivalla aliohjelmakutsulla. Aliohjelman paikallisten muuttujien tilat vapautetaan yleensä automaattisesti aliohjelmasta poistuttaessa. Tällaisiin vapautettuihin muistialueisiin saatetaan kuitenkin viitata osoittimilla, jotka muuttujan kadotessa jäävät osoittamaan väärään paikkaan. Tällaista osoitinta kutsutaan 'roikkuvaksi viittaukseksi' (dangling reference). Osoittimen käyttö voi aiheuttaa ohjelman kaatumisen tai pahimmassa tapauksessa tuottaa hämäräperäisiä tuloksia.

Linkitetyissä rakenteissa tietueisiin päästään käsiksi vain niihin osoittavien osoittimien (linkkien) avulla. Jos kaikki linkit poistetaan, mutta tietueen tilaa ei vapauteta, se jää kuluttamaan muistia, mutta siihen ei voi enää viitata millään tavoin. Hukkatila vapautuu vasta ohjelman suorituksen päättyessä. Jos tällaista tilaa kertyy paljon, se voi hidastaa toimintaa. Hukkatilan siivoamiseksi tarvitaan 'roskien keruuksi' (garbage collection) kutsuttua järjestelmää tai ohjelmaa, joka etsii tavoittamattomissa olevat muistialueet ja merkitsee ne käytettävissä oleviksi.

Tietorakenteet

Tietueita käytetään usein rakennettaessa yleisempiä tietorakenteita. Tietorakenteen kussakin tietueessa eli solmussa (node) on jotakin tietoa; sen lisäksi siinä voi olla linkkejä  muihin solmuihin. Linkki on olio, joka viittaa johonkin toiseen tietueeseen; yleensä se on siis osoitin.

Pino (stack) on tietorakenne, joka muistuttaa pöydällä olevaa kirjapinoa. Kirjapinon päälle voidaan laittaa uusi kirja tai pinosta voidaan poistaa päällimmäinen kirja. Tietokoneissa pinoa käytetään esimerkiksi laskutoimituksissa. Menetelmä on tuttu kaikille, jotka ovat käyttäneet HP:n laskimia. Se tunnetaan käänteisenä puolalaisena notaationa puolalaisen loogikon Lukasiewiczin mukaan. Esimerkiksi yhteenlasku 1+2 suoritetaan tällaisella laskimella näppäilemällä

  1  2 +

Ensin talletetaan luku 1 pinoon, sitten 2 sen päälle, ja lopuksi otetaan pinosta kaksi päällimmäistä alkiota, lasketaan ne yhteen ja talletetaan takaisin pinoon. Lauseke 1+2*(3-4) voitaisiin laskea seuraavasti:

  1  2  3  4 - * +

Laskimissa pinoon mahtuu vain muutama luku (HP:llä 4). Tietokoneella tällaista rajoitusta ei ole.

Pinosta poistetaan aina sinne viimeksi talletettu alkio. Siksi rakenne tunnetaan myös nimellä LIFO (Last In First Out).

Jono (queue) vastaa kaupan kassajonoa. Jonon takapäähän tulee uusia asiakkaita, ja kauimmin jonossa olleet poistuvat sen etupäästä. Rakenteesta käytetään myös nimeä FIFO (First In First Out).

Tietokoneessa jonoja liittyy erilaisten resurssien hallintaan. Moniajoympäristössä prosessoria odottavat työt ovat jonossa, josta kauimmin jonottanut poimitaan seuraavana suoritettavaksi. Tässä tapauksessa jono voi olla myös sillä tavoin yleisempi, että korkean prioriteetin työt voivat kiilata jonossa jo olevien väliin.

Jono esiintyy myös poltettaessa dataa CD-levylle. Ohjelma siirtää tavaraa tietokoneen levyltä jonoon, josta poltto-ohjelma tallentaa sen CD:lle. Mikäli jono pääsee tyhjenemään, tallennus epäonnistuu.

Pakka (Knuthin terminologiassa deque, double-ended queue) on jonon yleistys, jossa tietueita voidaan lisätä kumpaan päähän tahansa ja poistaa kummasta päästä tahansa.

Kaikki nämä rakenteet ovat erikoistapauksia yleisestä lineaarisesta listasta. Sille on ominaista, että kaikki alkiot ovat peräkkäin tietyssä järjestyksessä. Rakenne voidaan tallettaa tietokoneen muistiin peräkkäisiin muistipaikkoihin.

Tällainen talletustapa on hankala esimerkiksi jonon tapauksessa, sillä se siirtyy koko ajan eteenpäin. Vaikka jonon pituus pysyisi pienenä, se voi elinaikansa kuluessa tarvita hyvin suuren tilan.

Yksi ratkaisu on linkitetty lista. Listan alkioiden ei tarvitse olla peräkkäin muistissa, vaan ne voivat sijaita miten tahansa. Kussakin tietueessa on aina osoite, josta seuraava tietue löytyy, eli linkki tai osoitin seuraavaan listan alkioon. Kaikki listan alkiot löytyvät seuraamalla tätä linkkiketjua.

Jos kustakin listan alkiosta on linkki vain seuraavaan, kyseessä on yksisuuntainen linkitys. Tällaista linkkiketjua voidaan edetä vain yhteen suuntaan. Kaksisuuntaisessa linkityksessä kustakin alkiosta on linkki sekä sen seuraajaan että edeltäjään. Silloin listassa voidaan helposti seilata kumpaan suuntaan tahansa.


Listan talletus peräkkäisiin muistipaikkoihin, yksisuuntainen linkitetty lista ja kaksisuuntainen linkitetty lista. Jos tietue on linkkiketjun viimeinen, puuttuvaa linkkiä voidaan merkitä "maadoituksen" symbolilla.


Kaikki tietorakenteet eivät ole lineaarisia, vaan haarautuvia, jolloin vain linkitetty versio on mahdollinen.

Puu (tree) on rakenne, jossa kustakin tietueesta voi lähteä useita haaroja eli alipuita. Puu lähtee yhdestä tietueesta, jota sanotaan puun juureksi. Tietueet, joilla ei ole enää alipuita, ovat puun lehtiä.

Puun erikoistapaus on binääripuu, jossa kullakin tietueella voi olla korkeintaan kaksi alipuuta, joista käytetään nimityksiä oikea ja vasen alipuu. Matemaattisissa lausekkeissa esiintyy operaattoreita (+, -, *, /), joilla on vain kaksi operandia. Siksi lausekkeet voidaan esittää binääripuina.


Lauseketta 2*(1+3) esittävä binääripuu. Kukin solmu sisältää lausekkeen yhden merkin (luvun tai operaattorin) ja linkit vasempaan ja oikeaan alipuuhun. Jos alipuuta ei ole, linkki on nollalinkki.


Vielä yleisempi tietorakenne on verkko (network), jossa kustakin solmusta voi lähteä mielivaltainen määrä linkkejä mihin tahansa muihin solmuihin. Verkolla voidaan kuvata vaikkapa eri kaupunkien välisiä lentoreittejä: jokaista reittiä vastaa linkki sen lähtöpisteestä määränpäähän.

Taulukoiden ja myös joidenkin muiden lineaaristen tietorakenteiden tila voidaan varata jo ohjelmaa käynnistettäessä. Usein taulukon koolle tiedetään ainakin jokin yläraja, jolloin sille voidaan määritellä kerralla riittävän iso tila. Liian suuren taulukon määrittelemisestä ei ole haittaa, koska todellisuudessa tilaa varataan yleensä sivun kokoisina palasina tarpeen mukaan. Käyttöjärjestelmä huolehtii tästä täysin automaattisesti.

Nykyisissä ohjelmointikielissä taulukkojen tila voidaan varata myös dynaamisesti siinä vaiheessa, kun tarvittavan tilan määrä on saatu selville. Tämä voi kuitenkin tehdä ohjelmasta mutkikkaamman, ja on usein turhaa hienostelua. Eniten merkitystä dynaamisella tilanvarauksella on taulukoiden ollessa todella isoja.

Linkitetyn rakenteen tietueita luodaan ja hävitetään tarpeen mukaan, ja niiden ei tarvitse olla peräkkäin muistissa. Tällöin niiden tila on helpointa varata dynaamisesti esimerkiksi funktiolla, joka varaa halutun suuruisen tilan ja palauttaa siihen osoittavan osoittimen (linkin). Tietueisiin viitataan vain osoittimien avulla, mikä voi johtaa edellä osoittimien yhteydessä kerrottuihin ongelmiin.