Opiskelumateriaali - Muuttujat ja tietotyypit

Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Muuttujat imperatiivisissa ohjelmointikielissä
Seuraavaksi keskitytään nykyään valta-asemassa oleviin, imperatiivisiin
ohjelmointikieliin. Tässä dokumentissa perehdytään keskeiseen muuttujan
käsitteeseen ja tietotyyppeihin. Esimerkeissä esiintyviä kieliä käsitellään
yksityiskohtaisemmin mm. seuraavissa teoksissa: [Ker] (C-kieli), [Strou] (C++ -kieli),
[Arc] (C#), [Arn] (Java), [Kor] (Pascal), [Kur] (Ada) ja [KLS] (FORTRAN).
1. Muuttujat
Imperatiivisen ohjelmoinnin aivan keskeisimpiä asioita on muuttujan käsite. Esimerkiksi
Edsger Dijkstra on todennut: "Kun on ymmärtänyt tavan, miten ohjelmoinnissa
käsitellään muuttujia, on ymmärtänyt olennaisimman ohjelmoinnista." Kuten aiemmin
on todettu, imperatiivinen ohjelmointiparadigma perustuu von Neumannin malliin
tietokoneesta. Siinä muuttujat mallintavat tietokoneen muistipaikkoja; näin ollen
muuttuja on itse asiassa nimi jollekin tietokoneen muistialueelle, jonka sisältö kuvaa
muuttujan arvoa. Tosin muuttuja on muutakin kuin pelkkä nimi, ajatellaan esimerkiksi
seuraavaa Pascal-kielen sijoituslausetta
e := 2.71828;
Tässä e on jonkin fyysisen muistiosoitteen nimi, 2.71828 kuvaa muuttujan arvoa, joka
sijoitetaan kyseiseen muistipaikkaan lausetta suoritettaessa. Kuitenkin tietokoneen
muistissa luvut säilytetään binäärisessä muodossa, joten tarvitaan ennakkotietoa
muuttujan tyypistä, jotta se voidaan tulkita kyseiseksi desimaaliluvuksi. Näin ollen
muuttujalla on
1. Nimi (name): tapa yksilöidä muuttuja,
2. Osoite (address): tietokoneen muistiosoite, jossa muuttujan arvo sijaitsee ja
3. Arvo (value): se data joka kullakin hetkellä on muuttujan osoitteen osoittamassa
muistipaikassa,
ja lisäksi siihen liittyvät seuraavat ominaisuudet eli attribuutit:
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
4. Tyyppi (type): muuttujan tietorakenteen nimi,
5. Näkyvyysalue (scope): se ohjelman osa, jossa muuttuja on käytettävissä,
6. Elinaika (lifetime, extent): muuttujan muistinvaraamisen ja
muistinvapauttamisen välinen aika.
Nimi on merkkijono, jota käytetään tunnistamaan jokin ohjelman itsenäinen
kokonaisuus, esimerkiksi muuttuja. Sanaa "tunniste" (identifier) käytetään nimen
synonyymina. Varhaisimmat ohjelmointikielet sallivat ainoastaan yksikirjaimisia nimiä
muuttujille perintönä matemaatikkojen tavasta merkitä muuttujia matemaattisessa
tekstissä. Kuitenkin jo FORTRAN luopui tästä rajoitteesta ja salli maksimissaan kuuden
merkin mittaiset nimet muuttujille. Alun perin C-kielessä käytettiin nimen
maksimierottelupituutena 31 ensimmäistä merkkiä, mutta ainakin jotkut C-kääntäjät
käsittelevät pitempiä nimiä. FORTRAN 90 käyttää 31 merkin rajoitusta. Nykyään
useimmat kielet, kuten C++ ja Java sallivat mielivaltaisen pitkät nimet. Tosin joissakin
C++ -toteutuksissa saattaa rajoituksia esiintyä.
Ohjelmoijan on tärkeää tietää myös, erotetaanko nimissä isot ja pienet kirjaimet.
Vaikka kirjainten erottaminen heikentää kielen luettavuutta ja johtaa helpommin
ohjelmointivirheisiin, useimmat moderneista kielistä, kuten Java, C++ ja C# tekevät
eron isojen ja pienten kirjainten välillä. Samoin on C-kielessä. Näin ollen muuttujat
Muuttuja, muuttuja, mUuttuja ja muuttujA ovat kaikki eri muuttujia näissä kielissä. Sen
sijaan Pascal ja FORTRAN eivät tee eroa isojen ja pienten kirjainten välillä ja näissä
kyseiset muuttujan nimet viittaisivat samaan muuttujaan. Itse asiassa varhemmat
FORTRANin versiot eivät sallineet pieniä kirjaimia käytettävän lainkaan muuttujan
nimissä. Sittemmin tässäkin kielessä on siirrytty samaan käytäntöön kuin Pascalissa ja
pienet kirjaimet tulkataan isoiksi käännösvaiheessa.
Kielen kontrollirakenteita kuvaavat sanat on jotenkin erotettava muuttujista
määrittelemällä ne erikoissanoiksi (special words). Tämä tehdään yleisesti kahdella
tavalla: käyttämällä varattuja sanoja (reserved words) tai avainsanoja (keywords).
Varattua sanaa ei voi ohjelmassa käyttää missään yhteydessä muuten kuin sille
varattuun tarkoitukseen; näin ollen esimerkiksi C-, Java- tai Pascal-kielessä ei voi
määritellä if -nimistä muuttujaa. Avainsana on erikoismerkityksessä ainoastaan
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
tietyissä yhteyksissä. Joissakin aiemmissa ohjelmointikielissä, kuten PL/I ja FORTRAN on
käytetty avainsanoja. Moderneissa kielissä on kuitenkin siirrytty käytännöllisesti
katsoen yksinomaan varattujen sanojen käyttöön siitä yksinkertaisesta syystä, että
avainsanojen käyttö tekee mahdolliseksi erittäin hankalalukuisen ja virhealttiin koodin
kirjoittamisen. Esimerkiksi FORTRAN -kielessä INTEGER tarkoittaa kokonaislukutyyppiä
ja REAL liukulukutyyppiä, joten ne ovat avainsanoja. Ne eivät kuitenkaan ole varattuja
([KLS] s. 18), joten on mahdollista määritellä tämän nimiset muuttujat, esimerkiksi
INTEGER REAL
REAL INTEGER
INTEGER = 1.23
REAL = 32
Samoin kontrollirakenteita kuvaavien sanojen käyttäminen muuttujan niminä johtaa
kammottavaan ohjelmakoodiin.
Muuttujan osoite on muuttujaan liittyvän fyysisen muistiosoitteen arvo, joka yleensä
ilmaistaan (mikrotietokoneissa tavallisesti 32-bittisenä) heksalukuna. Muuttujan osoite
ei yleensä ole staattinen, vaan samannimisen muuttujan osoite saattaa vaihdella
ohjelman suorituksen aikana. Usein muuttujan osoitteesta käytetään nimitystä l-value
(left value), koska muuttujan osoite on tiedettävä, kun muuttuja sijaitsee
sijoituslauseen vasemmalla puolella. On myös mahdollista, että kaksi erinimistä
muuttujaa viittaa samaan muistipaikkaan. Tästä ilmiöstä nimiä käytetään termiä
moninimisyys (aliasing) ([Har], s. 58). Moninimisyys heikentää koodin luettavuutta ja
luotettavuutta, joten nykyään sitä pidetään ei-toivottuna piirteenä ohjelmointikielessä.
Mistään ohjelmointikielestä ei kuitenkaan ole onnistuttu poistamaan kokonaan
moninimisyyttä. Aikoinaan FORTRANissa oli jopa erityinen mekanismi ominaisuuden
toteuttamiseksi (EQUIVALENCE -lause, ks [KLS], s. 128), mikä katsottiin tarpeelliseksi
muistin uudelleenkäyttämiseksi. Tapoihin muodostaa eri kielissä muuttujia viittaamaan
samaan muistipaikkaan palataan vielä myöhemmin.
Muuttujan tyyppi määrittelee sen arvoalueen ja minkälaisia operaatioita kyseisen
tyypin muuttujalle voidaan tehdä. Esimerkiksi Java-kielessä suurin kokonaislukutyypin
suurin arvo on
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Integer.MAX_VALUE 2147483647
ja pienin
Integer.MIN_VALUE -2147483648
Tyypin muuttujiin voidaan soveltaa perusaritmetiikan operaatioita.
Muuttujan arvo on sen muistiosoitteen kulloinenkin sisältö. Yleensä tietokoneen
muistiosoitteen koko on yksi tavu: tässä muistiosoitteella tarkoitetaan kuitenkin
hieman abstrahoidusti sellaista muistialuetta, johon koko muuttujan data mahtuu.
Esimerkiksi Javan double -tyyppinen muuttuja on kahdeksan tavun kokoinen, joten
puhuttaessa tällaisen muuttujan osoitteesta, tarkoitetaan kahdeksan tavun kokoista
muistialuetta, jossa säilytetään muuttujan arvoa. Usein muuttujan arvoa nimitetään rvalueksi (right value), koska sitä tarvitaan sijoituslauseen oikealla puolella. Huomaa,
että päästäkseen käsiksi r-valueen, on l-value aina määritettävä ensin.
Yksi keskeisimmistä muuttujiin liittyvistä käsitteistä on sidonta (binding). Sidonnalla
tarkoitetaan jonkin ominaisuuden liittämistä ohjelman itsenäiseen kokonaisuuteen.
Muuttujan tapauksessa se tarkoittaa yleensä tyypinsidontaa tai muistinsidontaa. Näistä
ensimmäinen liittää muuttujaan jonkin tietotyypin ja jälkimmäinen muistiosoitteen
(varaa eli allokoi riittävästi muistia, jotta muuttujan arvo voidaan tallentaa). Erityisesti
on tärkeää tarkastella sidonta-aikaa (binding time), ts. milloin tietty sidonta tehdään.
Yleisesti ottaen voidaan sanoa, että mahdollisimman varhainen sidonta lisää
tehokkuutta, kun taas mahdollisimman myöhäinen sidonta joustavuutta. Ei olekaan
ihme, että ohjelmointikielten kehityksen trendi on kohti yhä myöhäisempiä sidontaaikoja, kun resurssit lisääntyvät ja tehokkuus ei ole aina välttämätön kriteeri. Kaikki
ennen ajoa tapahtuva sidonta staattista (static binding) ja ajonaikainen dynaamista
(dynamic binding). Staattinen sidonta voidaan jakaa moneen osaan. Tutkitaan
esimerkiksi C++ -kielen lauseita
float desiNum = 0.0;
desiNum = desiNum + 1.1;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Tässä mahdolliset tyypit muuttujalle desiNum on pitänyt sitoa jo ohjelmointikieltä
suunniteltaessa ja mahdolliset merkitykset + -operaattorille kieltä määriteltäessä. Sen
sijaan muuttujan desiNum tyyppi ja operaattorin + merkitys jälkimmäisessä lauseessa
sidotaan vasta käännösvaiheessa. Vakioiden 0.0 ja 1.1 esitystapa on sidottu kääntäjän
suunnitteluvaiheessa ja lopulta muuttujan desiNum arvo jälkimmäisessä lauseessa
sidotaan muuttujaan dynaamisesti ajonaikaisesti lausetta suoritettaessa. Erityisesti
dynaamisen ja staattisen sidonnan erottaminen ja eron ymmärtäminen on olennaista
ohjelman semantiikan ymmärtämiseksi. Staattinen sidonta tapahtuu siis ennen
ohjelman suoritusta eikä muutu suorituksen aikana; dynaaminen sidonta tapahtuu
vasta ohjelmaa ajettaessa ja voi muuttua ohjelman suorituksen aikana.
Ennen kuin muuttujaa voi ohjelmassa käyttää, siihen on liitettävä tietotyyppi.
Staattisesti tämä tapahtuu liittämällä tyyppi muuttujaan joko eksplisiittisellä tai
implisiittisellä esittelyllä. Eksplisiittinen esittely on ohjelmalause jossa listataan joukko
muuttujan nimiä ja määritellään ne tietyn tyyppisiksi, esimerkiksi Java-kielen lause
float f1,f2,f3;
esittelee kolme reaalilukutyyppistä muuttujaa f1, f2 ja f3. Implisiittinen esittely liittää
muuttujaan tietotyypin jonkin sopimuksen mukaan ilman erillistä esittelyä. Esimerkiksi
FORTRAN -kielessä voidaan muuttuja eksplisiittisesti esitellä, mutta ilman esittelyä
esiintyvä muuttuja on INTEGER-tyyppinen, mikäli sen nimi alkaa I, J, K, L, M tai N kirjaimella; muussa tapauksessa muuttuja on REAL -tyyppinen ([KLS], kappale 4.2.1).
Joissakin BASIC -kielen versioissa on vastaavanlainen ominaisuus: Mikäli muuttujan
nimi loppuu merkkiin "%", muuttuja on kokonaisluku, merkkiin "$" loppuvat muuttujat
ovat merkkijonoja ja kaikki muut ovat reaalilukutyyppiä. Mahdollisuus implisiittiseen
esittelyyn katsotaan nykyään suuremmaksi haitaksi kuin ohjelmoijan saama
mukavuushyöty. Implisiittinen esittely tapauksissa, joissa esittelyn poisjäänti on
ohjelmoijan laiminlyönti, voi johtaa hankalasti löydettäviin virhetoimintoihin
ohjelmassa. Useimmissa (staattista tyypinsidontaa käyttävissä) kielissä eksplisiittinen
esittely on pakollinen.
Kun muuttujan tyyppi sidotaan dynaamisesti, ei käytetä esittelylausetta, vaan
muuttujan tyyppi sidotaan sijoituslauseen yhteydessä. Muuttujan tyyppi määrittyy siis
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
vasta ohjelman suorituksen aikana sijoituslausetta suoritettaessa. Dynaamista
tyypinsidontaa käyttävät kielet eroavat voimakkaasti staattiseen tyypinsidontaan
perustuvista kielistä. Tällaiset kielet ovat erittäin joustavia ja mahdollistavat monissa
tapauksissa yksinkertaisen geneerisen ohjelmoinnin. Varhaisimpia dynaamisia kieliä
olivat jo 1960 -luvun alussa kehitetyt APL ja SNOBOL, joilla on vieläkin käyttäjänsä.
Useat skriptikielet, kuten JavaScript, käyttävät dynaamista tyypinsidontaa. Esimerkiksi
JavaScript -kielinen lause
xz = [1.5, 2.2, 3.7]
aiheuttaa muuttujan xz tyypin muuttumisen lukuja sisältäväksi yksiulotteiseksi
taulukoksi, ja mikäli tätä seuraa lause
xz = 234
xz muuttuu numeeriseksi muuttujaksi. Saavutetusta joustavuudesta aiheutuu myös
haittoja: Aiemmin mainittiin jo tehokkuuden menetys, dynaamisesti tyypitetty kieli
häviää suoritusajassa yleensä melkoisesti staattisesti tyypitetyn kielen ohjelmille. Usein
dynaamisesti tyypitetyt kielet ovat lisäksi tulkattavia johtuen ongelmista, joita
dynaaminen tyypittäminen asettaa kääntäjälle. Toinen haitta on koodin luotettavuuden
heikkeneminen, koska kääntäjä ei voi havaita ohjelmoijan tyyppivirheitä. Väärän
tyyppinen muuttuja sijoituslauseen oikealla puolella aiheuttaa ainoastaan vasemman
puolen muuttujan tyypinvaihdoksen.
Kun muuttujan tietotyyppi on määrätty, sille voidaan varata muistista alue (osoite),
johon muuttujan arvo voidaan tallentaa. Muistin varaamista kutsutaan myös
allokoinniksi (allocation). Allokoinnin käänteisprosessi on varatun muistin
vapauttaminen, ts. muistialueen antaminen jälleen käytettäväksi; tätä kutsutaan myös
deallokoinniksi (deallocation). Muuttujan elinikä on sen käyttämän muistin
varaamisen ja vapauttamisen välinen aika. Muuttujat voidaan jakaa eliniän perusteella
neljään eri kategoriaan:
1. staattisiin,
2. pinodynaamisiin,
3. kekodynaamisiin (explicit heap dynamic) ja
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
4. implisiittisesti kekodynaamisiin (implicit heap dynamic).
Yleisesti ohjelman muistialue jaetaan kolmeen osaan:
1. staattiseen (globaaliin) osaan,
2. pinomuistiin ([runtime] stack) ja
3. kekomuistiin (heap).
Yleensä pino- ja kekomuisti ovat muistialueen vastakkaisissa päissä ja kasvavat
toisiansa kohti seuraavan kuvion osoittamalla tavalla:
Kuva. Ohjelman muistialue
Staattinen muuttuja sidotaan muistosoitteeseen ennen ohjelman suoritusta ja se säilyy
sidottuna tähän samaan osoitteeseen, kunnes ohjelman suoritus päättyy. Staattisten
muuttujien käyttö tuo tehokkuutta ohjelmaan, mutta tekee ohjelmasta
joustamattoman. Esimerkiksi rekursiivisten aliohjelmien toteuttaminen on mahdotonta
käyttämällä pelkästään staattisia muuttujia. FORTRANin varhaisemmissa versioissa
kaikki muuttujat olivat staattisia. Sen sijaan C-kielessä ohjelmoija voi static-määreellä
määritellä muuttujan staattiseksi ([Ker], kappale 4.6). Tämä mahdollistaa esimerkiksi
aliohjelman paikallisen muuttujan arvon säilymisen aliohjelmakutsujen välillä.
Esimerkiksi Pascal -kielessä staattisia muuttujia ei voi määritellä.
Muuttujat, joiden tyyppi on staattisesti sidottu, mutta joiden muistiosoite sidotaan
ajonaikaisesti esittelylausetta suoritettaessa, ovat pinodynaamisia muuttujia. Nimitys
johtuu siitä, että aina Algol 60 -kielestä lähtien sen sukuisissa kielissä paikallisten
muuttujien muisti on varattu pinomuistista dynaamisesti, vaikka muuttujan tyyppi
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
onkin staattisesti sidottu. Tähän kategoriaan kuuluvat yleisimmin käytetyt
imperatiiviset nykykielet. Juuri pinon käyttäminen muuttujien muistin varaamiseen
sallii rekursiiviset aliohjelmakutsut, koska tällöin aliohjelman tietyn nimiselle
muuttujalle varataan jokaista aliohjelmakutsua kohti uusi muistiosoite. Toinen etu, joka
saavutetaan pinodynaamisten muuttujien käytöllä, on yhteinen muistialue kaikkien
aliohjelmien paikallisille muuttujille. Dynaaminen muistinvaraus hidastaa hieman
suoritusta, mutta nykyisillä resursseilla tämä ei ole olennaisesti ohjelman suoritusaikaa
rajoittava tekijä. Lisäksi historiatietoja ylläpitävien muuttujien käyttäminen
aliohjelmissa ei onnistu pinodynaamisilla muuttujilla vaan vaatii jonkinlaisen staattisen
tai globaalin muuttujan.
Kekomuistista varattavat muuttujat ovat yleisimmin (eksplisiittisesti) kekodynaamisia.
Nämä varataan kekomuistista ohjelmoijan käskystä ajonaikaisesti. Kekodynaamisiin
muuttujiin voidaan viitata ainoastaan osoitinmuuttujan tai viitetyypin muuttujan avulla.
Tällainen muuttuja voidaan varata joko erityisen operaattorin (Java ja C++ -kielessä
new) avulla tai C-kielen tapaan kutsumalla kirjastofunktiota (malloc()), joka huolehtii
muistinvarauksesta. Kekodynaamisten muuttujien varaama muisti voidaan vapauttaa
automaattisesti, kun muuttujaa ei enää käytetä ohjelmassa, kuten Java -kielessä
tapahtuu. Tätä prosessia nimitetään roskien keruuksi (garbage collection). Monissa
kielissä (esimerkiksi C ja C++) kekodynaamisten muuttujien muistia ei vapauteta
automaattisesti, vaan vapauttaminen on ohjelmoijan vastuulla ja siihen on C++ kielessä oma operaattorinsa delete ja C:ssä funktio free().
Esimerkki. Kekodynaamisen muuttujan varaaminen (ja vapauttaminen) C ja C++ kielissä.
C:
C++:
int *pnewint;
pnewint = malloc(sizeof(int));
*pnewint = 10;
free(pnewint);
int *pnewint;
pnewint = new int;
*pnewint = 10;
delete pnewint;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Mikäli muistin vapauttaminen jätetään ohjelmoijan vastuulle, on hyvin mahdollista,
että ohjelman suorituksen aikana jää käyttämätöntä muistia vapauttamatta ja syntyy
ns. roikkuvia osoittimia, ts. osoitinmuuttujia, jotka osoittavat jo vapautettuun muistiin.
Tällaisen muistialueen käyttäminen johtaa ohjelman ennustamattomaan
käyttäytymiseen. Esimerkiksi C++ koodissa
int *pnewint;
pnewint = new int;
pnewint = 0;
muistia, joka varattiin, ei voida enää vapauttaa, koska siihen osoittavaan muuttujaan
sijoitettiin 0. Nyt lause
delete pnewint;
ohjelmassa ei tekisi mitään. Jokin muuttujan nollasta poikkeava arvo, joka ei ole
varatun muistipaikan osoite kaataisi ohjelman, koska mainitussa paikassa olevaa
muistia ei voisi vapauttaa. Edelleen koodissa
int *pnewint, *panothernewint;
pnewint = new int;
*pnewint = 10;
panothernewint = pnewint;
delete pnewint;
pnewint = 0;
muuttuja panothernewint on roikkuva osoitin, koska se osoittaa paikkaan, josta muisti
vapautettiin. Jos kyseisen muistipaikan arvoa käytetään kokonaislukuna, arvo on
satunnainen.
Implisiittisesti kekodynaamiset muuttujat sidotaan kekomuistiin vasta sijoituslauseen
yhteydessä. Tällaisten muuttujien käyttö sallii hyvin joustavan ohjelmoinnin ja erittäin
geneerisen koodin kirjoittamisen. Monet skriptikielet, kuten Perl ja JavaScript
käsittelevät merkkijonomuuttujia ja taulukoita näin.
Tyypintarkistuksen (type checking) avulla varmistetaan, että ohjelman kaikissa
operaatioissa käytettävien muuttujien tyypit ovat yhteensopivat, esimerkiksi
kokonaislukutyyppimuuttujaan ei saisi sijoittaa liukulukumuuttujan arvoa.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Tyypintarkistus voi olla joko staattista tai dynaamista. Mikäli tyypit tarkistetaan
ajonaikaisesti, kyseessä on dynaaminen tarkistus, muuten staattinen. Kääntäjät
käyttävät staattista ja tulkit dynaamista tarkistusta. Tyyppiyhteensopivuus (type
compatibility) voidaan määritellä tiukemmin nimityypin yhteensopivuutena (name
type compatibility) tai hieman löysemmin rakennetyypin yhteensopivuutena
(structure type compatibility). Edellinen tarkoittaa, että muuttujat ovat yhteensopivaa
tyyppiä ainoastaan, jos niiden määritellyt tyypit ovat samat. Rakennetyypin
yhteensopivuus puolestaan toteutuu, jos muuttujien rakenne on identtinen, vaikka
niiden määrittely käyttää eri nimeä. Esimerkiksi Pascal-kielisessä määrittelyssä
TYPE MYINT = INTEGER; var a: INTEGER;
var ma:MYINT;
muuttujat a ja ma eivät ole nimityyppiyhteensopivat, mutta ovat
rakennetyyppiyhteensopivat. Pascal -kielessä ei tarkisteta nimityypin yhteensopivuutta,
sijoituslause ma := a; on laillinen ylläolevassa ohjelmassa. Standardi-Pascalissa
tyypintarkistus on pääosin rakennetyypin tarkistusta, mutta joissakin tilanteissa
vaaditaan nimityypin yhteensopivuutta (ks. [Seb], kappale 5.7 tai [Kor], kappale 5.2).
Yleensäkin ohjelmointikielissä käytetään tyypintarkistukseen jotain mainittujen tapojen
välimuotoa, koska nimityypin tarkistus on liian rajoittava ja rakennetyypin tarkistus liian
hankala toteutettava, esimerkiksi C-kielessä
typedef struct {
int x;
int y;
} myStruct;
typedef struct {
int xx;
int yy;
} myOtherStruct;
myStruct str1;
myOtherStruct str2;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
muuttujat str1 ja str2 ovat rakennetyyppiyhteensopivat, mutta sijoituslause str1 = str2;
ei ole sallittu. Itse asiassa C-kieli käyttää muuten rakenneyhteensopivuutta paitsi
tietueiden (struct) ja unionien (union) suhteen ([Ker], Appendix A 8.10). Huomaa, että
C- ja C++ -kielen typedef ei oikeastaan määrittele uutta tyyppiä, vaan määrittelee
ainoastaan nimen jo olemassa olevalle tyypille. Näin ollen C/C++ -kielisessä
määrittelyssä
typedef int myInt;
int i;
myInt mi;
muuttujat i ja mi ovat nimityyppiyhteensopivat ja siten sijoituslause i = mi; on sallittu
myös C++ -kielessä, joka käyttää nimityyppiyhteensopivuutta (ks. [Seb], kappale 6.14).
Ohjelmointikieltä sanotaan vahvasti tyypitetyksi (strongly typed), mikäli
1. Jokaisella muuttujalla on oltava hyvin määritelty tyyppi.
2. Tyyppivirheet havaitaan aina.
Yleensä vahvaa tyypitystä pidetään tavoittelemisen arvoisena piirteenä
ohjelmointikielessä aina 1970 -luvulla rakenteellisen ohjelmoinnin ihanteista alkaen.
Ominaisuus estää nimittäin monenlaiset ohjelmointivirheet, jotka johtuvat
vääräntyyppisen muuttujan sijoittamisen tai käyttämisen parametrina
aliohjelmakutsussa. Kuitenkin vain varsin harvat kielet täyttävät tämän kriteerin, jos
sitä sovelletaan tiukasti. Esimerkiksi Pascal-kieli on lähes vahvasti tyypitetty, mutta siinä
on mahdollisuus määritellä ns. vaihtelevia tietueita (variant records), joiden tyyppiä ei
aina voi tarkistaa. C -kielessä on hieman enemmän tapauksia, joissa tyyppivirhe voi
jäädä havaitsematta, joten C-kieli ei ole niin vahvasti tyypitetty kuin Pascal. Myöskään
C++ ei ole vahvasti tyypitetty, vaikka tyypintarkistus on vahvempi kuin C:ssä. Adaa,
Javaa ja C# -kieliä voidaan pitää vahvasti tyypitettyinä, sillä näissä kielissä tyyppivirhe
voi syntyä ainoastaan niin, että ohjelmoija itse pakottaa väärän tyypin muuttujalle.
Kielessä käytettävät muunnossäännöt (coercion) vaikuttavat olennaisella tavalla
tyypintarkistukseen; nimittäin vahvasti tyypitetyssäkin kielessä saattaa olla esimerkiksi
aritmeettisille operaatioille sääntöjä, jotka rikkovat periaatteessa tyypitystä vastaan.
Esimerkiksi Javassa saadaan laskea liukulukumuuttuja ja kokonaislukumuuttuja yhteen,
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
jolloin kokonaisluvusta pakotetaan liukuluku ja operaatio suoritetaan. Tällaiset
muunnokset heikentävät kielen luotettavuutta. Tässä mielessä Ada on luotettavampi
kuin Java, koska se sisältää huomattavasti vähemmän tällaisia muunnoksia.
Muuttujan näkyvyysalue (scope) on yksi tärkeimmistä käsitteistä tarkasteltaessa
ohjelmointikieliä. Näkyvyysalueella tarkoitetaan niiden ohjelman lauseiden
kokonaisuutta, joiden alueella muuttuja on näkyvä (visible), toisin sanoen
käytettävissä. Globaalin (global) muuttujan näkyvyysalue on koko ohjelma.
Ohjelmalohkon tai muun vastaavan yksikön paikalliset eli lokaalit (local) muuttujat
ovat sellaisia muuttujia, jotka on määritelty kyseisessä lohkossa. Lohkon sisällä näkyvät
muuttujat, joita kuitenkaan ei ole määritelty kyseisessä lohkossa, ovat lohkon eipaikallisia (nonlocal) muuttujia. Muuttujan näkyvyysalue voi määräytyä staattisesti
(static scoping) ennen ohjelman suoritusta tai dynaamisesti (dynamic scoping)
ohjelman suorituksen aikana.
Staattinen näkyvyysalueen määräytyminen on ollut yleisin menetelmä imperatiivisissa
kielissä aina sen jälkeen, kun se esiintyi ensi kertaa Algol 60 -kielessä. Nimensä
mukaisesti staattista näkyvyysaluetta käyttävien kielten muuttujien näkyvyysalueet
määräytyvät jo ennen ohjelman suoritusta ohjelman rakenteen perusteella. Ohjelman
rakenteet voivat olla toisilleen alisteisia, yleensä tämä tarkoittaa ohjelmakoodissa
määrittelyjen (ja siten myös näkyvyysalueiden) sisäkkäisyyttä. Useimmissa
ohjelmointikielissä aliohjelmilla on oma näkyvyysalueensa; samoin nykyisissä kielissä on
useimmiten mahdollista muodostaa uusia näkyvyysalueita määrittelemällä
ohjelmalohkoja (blocks). Ohjelmalohkon ja kootun lauseen (compound statement) ero
on siinä, että lohkon sisällä voidaan esitellä uusia muuttujia, kun kootun lauseen sisällä
tämä ei ole mahdollista. Esimerkiksi Pascal -kielessä voidaan määritellä koottuja
lauseita keräämällä niitä begin - end parin sisään, mutta muuttujia ei niissä voi
määritellä. Siten koodi (muuttujat a ja b ovat aiemmin esitelty)
begin
var x:integer;
a := a+b;
end;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
on Pascalissa virheellinen. Sen sijaan C, C++ ja Java -kielissä sulkujen { ja } sisällä olevat
lauseet muodostavat lohkon ja muuttujia voidaan niissä esitellä. Kaikissa kielissä
muuttuja saadaan nykyään esitellä missä kohdassa lohkoa tahansa, mutta C-kielen
aiemmissa versioissa esittelyt oli aina sijoitettava lohkon alkuun.
Staattisesta näkyvyysalueen määräytymisestä seuraa, että mikäli sisemmissä
näkyvyysalueissa esitellään samannimisiä muuttujia kuin jo on alueen sisältävässä
lohkossa olemassa, on ulomman alueen muuttuja piilotettava sisemmässä lohkossa,
jotta esiteltyä muuttujaa voidaan käyttää. Esimerkiksi C-kielisessä ohjelmassa
int i = 100;
int ki = 35;
{
int i = 10;
printf("Lohkon i = %d\n", i);
i = ki;
printf("Lohkon i = %d\n", i);
}
printf("Ulompi i = %d\n", i);
Tulostuu
Lohkon i = 10
Lohkon i = 35
Ulompi i = 100
Lohkon sisällä siis aiemmin esitelty muuttuja i on piilotettu ja lohkossa esitelty
muuttuja i näkyvissä. Kun lohkosta poistutaan, on jälleen aiemmin esitelty muuttuja i
näkyvissä. Joissakin kielissä, kuten Pascalissa, sallitaan myös aliohjelmien sisäkkäisyys,
jolloin havaitaan sama ilmiö, vaikka Pascalissa ei voikaan muodostaa lohkoja
mielivaltaisesti. Esimerkiksi Pascal -ohjelmassa
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
program main;
var x:integer;
procedure A;
var x:integer;
procedure B;
begin {scope B...}
end;
begin {scope A...}
end;
procedure C;
var x: integer;
procedure D;
begin{scope D...}
end;
procedure E;
var x:integer;
var y:integer;
begin {scope E...}
end;
begin {scope C...}
end;
begin
{scope main...}
end.
aliohjelma A sisältää aliohjelman B ja aliohjelma C aliohjelmat D ja E. Tämä vaikuttaa
paitsi muuttujien, myös aliohjelmien näkyvyyteen. Näin ollen pääohjelman rungosta voi
kutsua aliohjemia A ja C, mutta ei aliohjelmia B, D ja E. Muuttujien näkyvyysalueet
(tässä x(main) tarkoittaa pääohjelmassa esiteltyä muuttujaa x ja x(A) aliohjelmassa A
esiteltyä muuttujaa x jne) ovat aliohjelmittain seuraavat:
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
main: x(main)
A:
x(A), x(main) piilotettu
B:
x(A), x(main) piilotettu
C:
x(C), x(main) piilotettu
D:
x(C), x(main) piilotettu
E:
y(E), x(E), x(C) piilotettu, x(main) piilotettu
Hyvin harvat ohjelmointikielet käyttävät dynaamista näkyvyysalueen määräytyvyyttä.
Tällaisia kieliä ovat APL, SNOBOL ja LISP -kielen varhaisimmat versiot. Perl sallii myös
dynaamisesti määräytyvän näkyvyysalueen muuttujien määrittelyn. Dynaaminen
näkyvyysalueen määräytyminen perustuu aliohjelmien suoritusjärjestykseen eikä
aliohjelmien rakenteelliseen sijaintiin ohjelmakokonaisuudessa. Näin ollen aktiivisen
aliohjelman muuttujat ovat näkyvissä kaikille aliohjelmille, joita kutsutaan kyseisen
aliohjelman käynnistämisen jälkeen. Tällöin voi luonnollisesti sattua samannimisten
muuttujien törmäyksiä, staattinen tyypintarkistus on mahdoton, joten se on tehtävä
ohjelman suorituksen aikaisesti. Oletetaan että C käyttäisi dynaamista näkyvyysalueen
määräytymistä (näin ei tietenkään asia oikeasti ole). Silloin ohjelma
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
int x;
void fun_1()
{
printf("%d\n", x);
}
void fun_2()
{
int x = 10;
fun_1();
}
int main()
{
x = 5;
fun_2();
return 0;
}
tulostaisi 10, koska paikallinen muuttuja x piilottaisi globaalin muuttujan x. Kun
käytetään staattista näkyvyysalueen määräytymistä, ohjelma tulostaa 5, koska
aliohjelman fun_2 paikallinen muuttuja x on näkyvissä ainoastaan kyseisessä
aliohjelmassa. Dynaaminen näkyvyysalueen määräytyminen aiheuttaa monenlaisia
ongelmia: Ohjelman luotettavuus heikkenee, koska aliohjelmien paikallisia muuttujia ei
voi suojella niiden ulkopuolista muuttamista vastaan. Edelleen koodin luettavuus
huononee, koska muuttujien määräytyminen perustuu suoritusjärjestykseen. Etu tässä
näkyvyysalueen määräytymisessä on aliohjelmien tiedonvälityksen helpottuminen.
Nykyisin katsotaan haittojen olevan huomattavasti suuremmat, joten juuri mikään
moderneista kielistä ei määrää näkyvyysalueita dynaamisesti. Maarit Harsun kirjan
([Har]) kolmas luku sisältää myös yllä mainittuja asioita.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
2. Tietotyypit
Jokainen tietokoneohjelma manipuloi dataa jollakin tavalla; data ohjelman sisällä
esitetään tietorakenteiden avulla. Ohjelman logiikka koostuu algoritmeista, joten
algoritmit ja tietorakenteet ovat ohjelmien perusrakennusaineita, tämän on Niklaus
Wirth sisällyttänyt jopa kirjansa "Algorithms+Data Structures = Programs" nimeen. Näin
ollen tietotyyppi (data type) on keskeinen käsite ohjelmoinnissa, koska se luokittelee
ohjelman datan. Tietotyypeistä ja tyypin tarkistuksesta on puhuttu jo edellä. Tässä
käsitellään ja luokitellaan ohjelmointikielissä esiintyviä tietotyyppejä sekä perehdytään
hieman eri tapoihin implementoida niitä. Tietotyyppi voidaan määritellä joukoksi
arvoja, joihin liittyy joukko näihin arvoihin sovellettavia operaatioita. Tietotyyppejä
käsitellään myös Maarit Harsun kirjan ([Har]) luvussa 4.
Tietotyyppejä, joiden määrittelemiseen ei käytetä muita tietotyyppejä, sanotaan
primitiiviksi tietotyypeiksi (primitive data types). Lähes jokaisessa ohjelmointikielessä
määritellään joukko primitiivisiä kielen mukana tulevia primitiivisiä tietotyyppejä.
Käsitteenä primitiivinen tietotyyppi muistuttaa läheisesti Loudenin (ks. [Lou] s. 158)
käyttämää yksinkertaisen (simple) tietotyypin käsitettä. Louden määrittelee
yksinkertaisen tietotyypin koskemaan kuitenkin sellaisia tietotyyppejä, joilla ei ole
muuta rakennetta kuin sisäänrakennettu aritmeettinen tai peräkkäinen rakenne.
Tyypillisesti primitiiviset tyypit jaetaan numeeriseen, loogiseen ja merkkitietoon.
Useissa varhaisissa ohjelmointikielissä ainoat primitiiviset tietotyypit olivat numeerisia.
Kaikkein yleisin numeerinen primitiivinen tietotyyppi on kokonaislukutyyppi (integer).
Ohjelmointikielestä ja ympäristöstä riippuen kokonaisluvun pituus voi vaihdella;
nykyään yleisin (ja esimerkiksi int Java -kielessä spesifioituna, [Arn] kappale 5.5) on 32
bitin mittainen luku. Lisäksi yleensä voidaan määritellä pitempi kokonaisluku (Javassa ja
C:ssä long) sekä lyhempi kokonaisluku (Javassa ja C:ssä short). C-kielessä voidaan myös
käyttää eripituisia etumerkittömiä kokonaislukutyyppejä (unsigned int jne). Normaalisti
korkein bitti ilmaisee luvun etumerkin (1 tarkoittaa negatiivista lukua), ja yleisimmin
negatiiviset luvut tallennetaan ns. kahden komplementtina. Tällöin. luvun merkki
vaihdetaan tekemällä sille looginen komplementti ja lisäämällä siihen luku 1.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Esimerkiksi 8-bittisten kokonaislukujen ollessa kyseessä, luku -12 esitettäisiin
seuraavasti:
12 = 00001100, looginen komplementti = 11110011 joten -12 = 11110011 + 1 = 11110100
Liukulukutyyppi (floating-point type) esittää reaalilukuja (likimääräisesti)
tietokoneessa. On luonnollisesti lukuja, joita ei voi tarkasti esittää millään valittavalla
tietokoneen käyttämällä merkintätavalla. Liukulukutyypin luvut esitetään nykyisin
useimmiten IEEE:n suosittamaa standardia 754 käyttämällä. Siinä 32-bittinen (monissa
kielissä float) liukuluku esitetään tieteellisestä merkintätavasta tutussa muodossa
(1+mantissa)*2eksponentti, missä mantissa on välillä [0,1). Ylin bitti on etumerkki (1
tarkoittaa negatiivista lukua), kahdeksan seuraavaa bittiä on varattu esittämään
eksponenttia ja loput 23 bittiä mantissaa. Luku esitetään aina binäärisessä muodossa ja
eksponentti esitetään poikkeamana luvusta 127, joten esimerkiksi luvulle 0,46875 =
15/32 saataisiin esitys
0,4687510 = 15/8 *2-2 = (1+ 7/8)* 2-2 , joten
eksponentti = 127-2 = 125
Mantissa on tällöin 7/8 = 0.1112,
joten saadaan esitys
Merkki(+) Eksponentti (125)
0
01111101
Mantissa (0.111)
111000000000000000000000
Kaksinkertaisen tarkkuuden liukuluvuille on vastaavan kaltainen esitysmuoto, siinä
käytetään mantissalle 52 bittiä ja eksponentille 11 bittiä, poikkeaman arvo on 1023.
Joissakin ympäristöissä käytetään lisäksi desimaalityypin esitystä, jolloin tietyn
mittaisille desimaaliluvuille saadaan tarkka esitys. Desimaalilukuesitys rajoittaa
esitettävien lukujen kokoa, mutta tämä esitysmuoto on käytössä ainakin C# -kielessä,
jossa sen pituus on 64 bittiä. Lukujen esittämistä tietokoneessa on käsitelty aiemmin
diskreettien rakenteiden kurssilla.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Looginen tietotyyppi (boolean, logical type) on - ainakin periaatteessa - tietotyypeistä
yksinkertaisin, sillä sen tarvitsee sisältää vain kaksi arvoa, tosi ja epätosi. Loogisen
tietotyypin esitystapa vaihtelee kielestä toiseen varsin paljon, esimerkiksi C-kielessä
mikä tahansa nollasta poikkeava lukuarvo tulkitaan todeksi ja nolla epätodeksi. Yleensä
kuitenkin kehittyneemmissä kielissä loogisella tietotyypillä on mahdollisuus saada arvot
true ja false. Myös C-kielessä on standardista C99 lähtien määritelty looginen
tietotyyppi (bool), joka voi saada arvot true ja false. Nämä ovat kuitenkin
kokonaislukutyyppiä ja niiden arvot ovat 1 ja 0.
Merkkitieto esitetään tietokoneen sisäisesti numeerisina koodeina. Vanhastaan yleisin
koodauskäytäntö on ollut ASCII (American Standard Code for Information Interchange),
jossa yleisimmin esiintyvät merkit esitetään luvuilla 0 - 127. Tämä tapa on
muistinkäytön suhteen tehokas, sillä merkit saadaan mahtuvaan yhteen tavuun, ja vielä
ylin bitti jää vapaaksi. Tätä bittiä on käytetty muodostamaan erilaisia laajennuksia
standardimerkistöön, jolloin käytettävä laajennus riippuu ympäristöstä. Esimerkiksi se,
sisältääkö käytettävä lisämerkistö skandinaavisia kirjaimia, on tällainen seikka. ASCII merkinnän peruja on se seikka, että useimmissa varhemmissa kielissä merkkityypin
muuttuja (esimerkiksi C:ssä char) on ollut yleisimmin, mutta ei välttämättä, yhden
tavun eli kahdeksan bitin mittainen. Nykyään tilansäästö ei kuitenkaan ole niin
tarpeellista ja lisäksi tarve käyttää yhä laajempaa merkistöä kasvaa koko ajan. Näin
ollen uudemmissa kielissä, kuten Java ja C#, käyttävät merkkitietotyypin esittämiseen
16 -bittistä Unicode-merkistöä.
Merkkijonotyyppi (character string type) ei useimmiten ole primitiivinen, koska se
määritellään merkkitietotyypin avulla. Se on kuitenkin monessa kielessä valmiiksi
määriteltyjen perustietotyyppien joukossa, joten käsitellään se tässä yhteydessä.
Periaatteessa olisi mahdollista määritellä merkkijonotyyppi omana primitiivisenä
tyyppinään; näin ei useimmissa kielissä tehdä vaan käytetään yksiulotteista, merkkien
muodostamaa taulukkoa. Poikkeuksia ovat FORTRAN (version 77 jälkeiset versiot) ja
BASIC, joissa merkkijonot ovat primitiivisiä. Merkkijonoille toteutetaan yleensä joitakin
perusoperaatioita, joita ovat mm. osajonoon viittaaminen, merkkijonojen katenointi,
viittaaminen tietyssä kohdassa esiintyvään merkkiin jne.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
C-kielessä merkkijono on merkkiin '\0' päättyvä yksiulotteinen merkkitaulukko. Tätä
muotoa voidaan käyttää myös C++:ssa, vaikka tässä kielessä onkin suositeltavampaa
käyttää standardikirjaston string -luokkaa. Merkkijono-operaatiot hoidetaan C-kielessä
käyttämällä kirjastofunktioita, esimerkiksi merkkijonojen vertailu funktioilla strcmp() ja
strncmp(), merkkijonojen katenointi funktioilla strcat() ja strncat() sekä tietyn merkin
(strchr()) tai merkkijonon(strstr()) etsiminen merkkijonosta. Pascal -kielessäkin
merkkijonot ovat merkkien muodostamia taulukoita (ns. pakattuja taulukoita);
Pascalissa merkkijonot ovat staattisia pituudeltaan ja ainoastaan samanpituisten
merkkijonojen järjestystä voidaan vertailla relaatio-operaattoreilla. Standardi-Pascalista
puuttuvat lisäksi merkkijonojen yhdistely ja pilkkomismahdollisuudet, mitä voidaan
pitää melkoisena puutteena. Pascalista onkin useita toteutuksia, joissa merkkijonojen
esitysmuotoa ja käsittelyä on parannettu. Adassa (kuten myös FORTRANissa) on
vastaavan kaltainen määrämittaisuuden vaatimus merkkijonoille. Ada tarjoaa
mahdollisuuden katenoida merkkijonoja käyttämällä & -operaattoria:
JONO1 := JONO1 & JONO2;
FORTRANissa merkkijonojen katenaatio-operaattori on //. Lisäksi FORTRANin
merkkijonojen vertailu sallii eripituiset merkkijonot. Tällöin lyhempää merkkijonoa
käsitellään kuin se olisi täytetty tyhjillä merkeillä samanmittaiseksi kuin pitempi jono.
Staattisen pituuden merkkijonot ovat aina täynnä merkkejä: jos lyhempi merkkijono
sijoitetaan pitempään, loppu täytetään tyhjillä merkeillä. C -tyyppiset merkkijonot ovat
pituudeltaan dynaamisia, mutta rajoitettuja: merkkejä voi olla mielivaltainen määrä,
mutta sitä rajoittaa taulukolle varattu tila. C -merkkijonon lopun osoittaa aina merkki
'\0'. Muuten merkkijonon pituutta ei pidetä yllä. Monissa kielissä merkkijonot voivat
olla vaihtelevan pituisia ilman ylärajaa; tällöin sanotaan että kielessä merkkijonot ovat
pituudeltaan dynaamisia.
Java -kielen toteutuksessa merkkijonot ovat suoraan Object -luokasta periytyvän String
-luokan ilmentymiä, joten merkkijonotyyppi on (jossakin mielessä) primitiivinen tyyppi.
Oikeastaan merkkijono ei Javassa ole lainkaan kieleen sisäänrakennettu tyyppi, vaan
luokka määritellään java.lang -paketissa, joka sisältyy automaattisesti kaikkiin Javalla
kirjoitettuihin ohjelmiin. Javan merkkijonot ovat vakioita, ts. merkkijonon merkkejä ei
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
voi muuttaa muuten kuin luomalla uusi merkkijono. Muokattavat merkkijonot ovat
Javassa StringBuffer -luokan ilmentymiä. Javassa, kuten C++ -kielessäkin sen string luokalle, on varsin laaja kokoelma valmiita metodeja merkkijonojen käsittelyyn.
Ordinaalityyppi (ordinal type) on tietotyyppi, jonka arvot voidaan yhdistää positiivisiin
kokonaislukuihin luonnollisella tavalla. Useat kielet sallivat käyttäjän määrittelemiä
ordinaalityyppejä; nämä eivät täytä primitiivisen tyypin määritelmää, mutta ovat
kuitenkin yksinkertaisia tietotyyppejä. Käyttäjän määrittelemiä ordinaalityyyppejä ovat
luetellut tyypit (enumeration types) ja rajoitetut tyypit (subrange type).
Luetellun tyypin muuttujien arvot määritellään luettelemalla symbolisiksi vakioiksi.
Lueteltuun tyyppiin liittyy järjestys, joten luetellun tyypin arvoja voidaan vertailla.
Esimerkiksi viikonpäivät voitaisiin esittää lueteltuna tyyppinä Pascal -kielellä
seuraavasti:
TYPE paiva = (su, ma, ti, ke, tor, pe, la);
Sama C-kielellä olisi:
enum paiva {su, ma, ti, ke, tor, pe, la};
Vertailu tämän tyypin muuttujien välillä tehtäisiin Pascalissa
var ps,pt:paiva;
begin
ps:=su;
pt:=tor;
if pt > ps then
begin
writeln('Torstai sunnuntain jälkeen!');
end;
end.
ja C -kielessä:
enum paiva x = su, y = tor;
if(x < y)
printf("Sunnuntai ennen torstaita\n");
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
C++ -kielessä sama koodi toimisi, mutta muuttujien esittelystä voitaisiin myös jättää
enum pois. Useimmiten ei ole mahdollista määritellä lueteltua tyyppiä, joka sisältäisi
arvona sellaisen merkkijonovakion, joka esiintyy jo toisessa luetellussa tyypissä. Näin
ylläolevaan Pascal -esimerkkikoodiin ei voi lisätä uutta tyyppiä
TYPE bile_paiva = (ke, pe, la);
eikä C-koodiin
enum bile_paiva {ke, pe, la};
Adassa puolestaan tämä on mahdollista; tällaisessa tapauksessa voidaan merkitä,
minkä tyypin arvosta on kysymys (ks. [Kur], kappale 4.3). Näin ollen Adassa voitaisiin
kirjoittaa
type PAIVA is (su, ma, ti, ke, tor, pe, la);
type BILE_PAIVA is (ke, pe, la);
ja viitata PAIVA -tyypin keskiviikkoon seuraavasti:
PS: PAIVA;
PS := PAIVA'(ke);
Varhaisemmissa kielissä lueteltuja tyyppejä ei ollut ja ohjelmoijat joutuivat yleisesti
käyttämään kokonaislukuarvoja merkitsemään tällaisia tyyppejä. Voitaisiin sopia, että 1
tarkoittaa sunnuntaita, 2 maanantaita jne. Tällaisella tavalla on kuitenkin useita
varjopuolia. Ensiksikin koodin luettavuus kärsii, koska koodin lukija joutuu koko ajan
pitämään mielessään, mitä eri arvot tarkoittavat. Sen sijaan määritellyt tyypit ovat
helposti tulkittavissa. Lisäksi koodin luotettavuus kärsii: Kahden eri tyypin muuttujat
voivat erehdyksessä sotkeutua toisiinsa ja lisäksi muuttujilla voidaan tehdä
aritmeettisia operaatioita, jotka johtavat rajojen ylitykseen. Esimerkiksi yllä lauantai
saisi arvon 7, johon voidaan lisätä luku 1, mutta 8 ei tarkoita mitään päivää. Tällaiset
virhemahdollisuudet poistuvat mikäli kielessä tarkistetaan lueteltujen tyyppien
yhteensopivuus. Näin tehdään esimerkiksi Pascalissa, mutta C-kielessä luetellut tyypit
ovat tavallisia kokonaislukuja, joihin voidaan soveltaa kokonaislukujen operaatioita.
Siten ylläolevissa esimerkeissä
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
var ps,pt:paiva;
begin
ps:=10;
on Pascal -koodissa virhe, mutta C -koodissa
enum paiva x;
x = 10;
sallitaan. Silti C-kielessäkin lueteltujen tyyppien käyttö parantaa kodin luettavuutta ja
jonkin verran myös luotettavuutta. C++-kielessä suoritetaan myös luetelluille tyypeille
voimakkaampi tyypintarkistus, joten yllämainittu C-kielinen koodi on virheellistä C++ koodia.
Java-kielessä ei voinut alun perin määritellä lueteltua tyyppiä, minkä takia ohjelmissa
käytettiin usein monia nimettyjä vakioita. Tämä puute korjattiin Javan myöhempiin
versioihin: kieleen lisättiin tyyppi enum (käytössä kielen versiosta 1.5 lähtien). Tämän
tietotyypin muuttuja voi olla joukko esimääriteltyjä vakioita, esimerkiksi seuraavasti:
public enum Paiva {
SUNNUNTAI, MAANANTAI, TIISTAI, KESKIVIIKKO,
TORSTAI, PERJANTAI, LAUANTAI
}
Javan enum-tyyppi on monipuolisempi kuin aiemmin mainituissa kielissä. Javassa vakiot
ovat olioita, joille voidaan määritellä mitä tahansa luokkaan liitettäviä ominaisuuksia.
Javan luetellun tyypin vakioita ei voi vertailla suoraan operaattorilla <, vaan siihen on
käytettävä metodia compareTo esimerkiksi seuraavasti:
if( (Paiva.KESKIVIIKKO).compareTo(Paiva.MAANANTAI) < 0)
System.out.println("Keskiviikko tulee ennen maanantaita.");
else
System.out.println("Maanantai tulee ennen keskiviikkoa.");
Rajoitettu tyyppi on jonkin ordinaalityypin peräkkäisten arvojen osajono. Rajoitetun
tyypin muuttujat esiintyivät ensimmäistä kertaa Pascal -kielessä ja niitä voidaan käyttää
myös Pascaliin pohjautuvassa Adassa. Pascalissa voidaan määritellä minkä tahansa
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
ordinaalityypin, myös käyttäjän määrittelemän, rajoitettu tyyppi antamalla ala- ja
yläraja seuraavasti
TYPE allesata = 1..100;
pienetkirjaimet = 'a'..'z';
arkipaivat = ma..la;
Rajoitetun tyypin käyttäminen lisää luotettavuutta ainakin siinä tapauksessa, että
kääntäjä tarkistaa virheelliset operaatiot. Muissa yleisissä kielissä Pascalin ja Adan
lisäksi ei rajoitettua tyyppiä käytetä.
Seuraavaksi siirrytään käsittelemään ohjelmointikieliin toteutettuja rakenteellisia
tietotyyppejä, joita ovat



taulukot,
tietueet ja
unionit.
Lisäksi Pascal -kieleen on toteutettu joukkotyyppi. Rakenteellinen tietotyyppi koostuu
yhdestä tai useammasta yksinkertaista tai rakenteista tyyppiä olevasta komponentista.
Taulukot ovat epäilemättä yleisimmin käytettyjä tietorakenteita ohjelmoinnissa.
Taulukko muodostuu kiinteästä määrästä samaa tyyppiä olevia tietoalkioita, jotka
sijaitsevat peräkkäin yhtenäisessä muistialueessa. Taulukon käyttö on tästä syystä
tehokasta, koska minkä tahansa sen alkion muistiosoite voidaan suoraan laskea,
kunhan vain tunnetaan taulukon alkuosoite. Taulukon alkiot voivat olla mitä tahansa
tietotyyppiä, joko primitiivistä, kielessä määriteltyä tai ohjelmassa määriteltyä.
Taulukon alkioihin viitataan taulukon nimellä ja alkion indeksillä taulukossa. Indeksi on
useimmiten positiivinen kokonaisluku, mutta joissakin kielissä se voi mikä tahansa
ordinaalityypin arvo. Esimerkiksi C-kielessä, jossa taulukon indeksit alkavat nollasta
int lukutaulu[50];
lukutaulu[10] = 34;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
esittelee 50 -paikkaisen kokonaislukutaulukon ja sijoittaa sen yhdenteentoista paikkaan
luvun 34. Samoin Javassa ja C++:ssa taulukon indeksit alkavat nollasta. Vastaava
tehtäisiin Pascal -kielessä seuraavasti
VAR lukutaulu: ARRAY [0..49] OF INTEGER;
begin
lukutaulu[10] := 34;
ja FORTRANissa
INTEGER LTAULU(0:49)
LTAULU(10) = 34;
Näissä kielissä (tosin FORTRANissa vasta versiosta 77 alkaen, sitä varhemmissa
versioissa taulukon indeksit alkavat aina luvusta 1) voidaan taulukon ylä- ja alarajat
antaa esittelyn yhteydessä. Pascalissa voidaan myös käyttää indeksointiin muitakin
ordinaalityypin arvoja ([Kor], kappale 7.2), esimerkiksi
TYPE paiva = (su,ma,ti,ke,tor,pe,la);
VAR tokataulu: ARRAY [ti..pe] OF INTEGER;
begin
tokataulu[ke] := 22;
Taulukot voidaan jakaa neljään tyyppiin taulukon indeksien rajojen sidonnan ja
taulukon muistin allokoinnin tapahtuma-ajan perusteella. Staattisten taulukoiden
(static arrays) rajat sidotaan staattisesti, samoin taulukon vaatima muisti varataan
staattisesti. Tämän tyypin taulukot ovat suoritusajan suhteen tehokkaimpia käyttää,
koska ne eivät vaadi sidontaa eivätkä muistinvarausta ohjelman suorituksen aikana.
Kiinteiden pinodynaamisten taulukoiden (fixed stack-dynamic arrays) rajat sidotaan
staattisesti, mutta muisti varataan ajonaikaisesti pinomuistista. Tässä tapauksessa
muistin käyttö on tehokkaampaa kuin staattisten taulukoiden tapauksessa.
Pinodynaamisten taulukoiden (stack-dynamic arrays) rajat sidotaan dynaamisesti ja
muisti varataan pinomuistista. Molemmat pysyvät vakioina taulukon elinajan. Tällaisten
käyttö lisää joustavuutta, koska taulukon kokoa ei tarvitse tietää etukäteen.
Kekodynaamisten taulukoiden (heap-dynamic array ) rajat sidotaan dynaamisesti ja
taulukon muisti varataan dynaamisesti kekomuistista. Taulukon rajat ja varattu muisti
voivat muuttua sen elinaikana. Näin ollen tämä on kaikkein joustavin tyyppi.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
FORTRANissa (ennen versiota 90) kaikki muuttujat varataan staattisesti. Tämä koskee
myös taulukoita, jotka siten ovat FORTRANissa aina staattisia. C-kielessä taulukko,
samoin kuin muuttujakin, voidaan esitellä static-määreellä staattiseksi. Yleensä C:n ja
C++:n funktioissa määritellyt taulukot ovat kiinteitä pinodynaamisia taulukoita, koska
niiden rajat sidotaan käännösaikana, mutta niille varataan muisti pinomuistista
suorituksen aikana. Pinodynaamiset taulukot olivat ennen harvinaisia yleisimmissä
imperatiivisissa ohjelmointikielissä: Ada-kielessä on alusta lähtien voinut määritellä
pinodynaamisen taulukon declare-lohkoa.
AR_LEN := 25;
declare
AR: array(1..AR_LEN) of INTEGER;
begin
…
end;
Tällöin muuttujaan AR_LEN voidaan syöttää jokin arvo ja taulukko varataan
dynaamisesti declare-lohkoon tultaessa. Muisti vapautetaan jälleen poistuttaessa
lohkosta. Yleensä rajojen dynaamisen sidonnan yhteydessä on käytetty kekodynaamisia
taulukoita. C- ja C++-kielten uusimmissa standardeissa sallitaan kuitenkin
pinodynaamiset taulukot. Siten seuraavan kaltainen C-ohjelmakoodi on korrekti:
int koko = 0;
scanf("%d",&koko);
int taulukko[koko];
taulukko[koko-1] = 123;
int i;
for(i = 0; i < koko; i++){
printf("taulukko[%d] = %d\n",i,taulukko[i]);
}
Javassa, samoin kuin C#:ssa taulukot ovat olioita ja siten kaikki taulukot ovat
kekodynaamisia. Uuden taulukon luominen tapahtuu joko käyttämällä new operaattoria tai luettelemalla taulukon alkiot; tässäkin tapauksessa luodaan
kekodynaaminen taulukko. Esimerkiksi Javassa
int[] lukutaulu = new int[50];
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
C-kielessä kekodynaaminen taulukko varataan, samoin kuin muukin dynaamisesti
varattava muisti, käyttämällä malloc-funktiota:
int *lukutaulu;
lukutaulu = malloc(50*sizeof(int));
lukutaulu[10] = 34;
free(lukutaulu);
Huomaa, että C-kielessä taulukko muuttujana on osoitin taulukon alkiotyyppiin; siksi
lukutaulu esitellään osoittimena, mutta taulukon alkioihin voidaan viitata hakasulkeita
käyttämällä. Huomaa myös, että C:ssä (samoin kuin C++ -kielessä) dynaamisesti
varattava muisti on ohjelmoijan vapautettava itse. Pascal-kielessä oli alkujaan se
hankala ominaisuus, että taulukon indeksirajat olivat osa taulukon tyyppiä: näin ollen
oli esimerkiksi mahdotonta kirjoittaa aliohjelmaa, joka olisi parametrinaan ottanut
erikokoisia taulukoita. Tämä kierrettiin ottamalla käyttöön ns. taulukkomallit
(conformant arrays) ks esimerkiksi [Kor, s. 135]. Tällä mallilla voitiin antaa aliohjelmalle
parametrina taulukko, jonka indeksit ovat jonkin ordinaalityypin rajoitettuja tyyppejä.
Esimerkiksi
PROCEDURE laskesumma(VAR summa: INTEGER;
taulu: ARRAY [alaraja..ylaraja:INTEGER] OF INTEGER);
VAR ind:INTEGER;
BEGIN
summa := 0;
FOR ind:= alaraja TO ylaraja DO
summa := summa + taulu[ind];
END;
jolloin pääohjelmassa voidaan kutsua aliohjelmaa seuraavasti:
VAR luvut: ARRAY[1..50] OF INTEGER;
kokosumma: INTEGER;
laskesumma(kokosumma,luvut);
Tätä toteutusta eivät kaikki kääntäjät tue. Yleisemmin tämä voidaan nykyään toteuttaa
ns. avoimien taulukoiden avulla (esimerkiksi Delphissä); tässä aliohjelmalle annetaan
parametriksi vain taulukon tyyppi ja sen indeksien oletetaan alkavan nollasta. Korkein
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
indeksi saadaan kutsumalla funktiota HIGH(). Tällöin ylläolevan aliohjelman koodi tulisi
muotoon
PROCEDURE laskesumma(VAR summa:INTEGER; taulu: ARRAY OF INTEGER);
VAR ind:INTEGER;
BEGIN
summa := 0;
FOR ind:= 0 TO HIGH(taulu) DO
summa := summa + taulu[ind];
END;
Taulukot voivat olla myös moniulotteisia, ts. tarvitaan useampia indeksejä viittaamaan
taulukon alkioihin. Yleensä tämä tarkoittaa sitä että taulukon ensimmäisen indeksin
(dimension) alkiot ovat (n-1) -ulotteisia taulukoita jne. Esimerkiksi Pascal -kielen
määrittely
ARRAY[1..6,3..21,0..3] OF INTEGER
on täsmälleen sama kuin
ARRAY[1..6] OF
ARRAY[3..21] OF
ARRAY[0..3] OF INTEGER
Yleisesti ohjelmointikielissä ei aseteta ylärajaa taulukon dimensioille; FORTRAN on
poikkeus tästä. Alunperin FORTRANissa sai käyttää korkeintaan kolmiulotteisia
taulukoita ja FORTRAN 77 salli seitsenulotteiset taulukot.
Pascalissa ei ole mahdollista alustaa taulukkoa sen esittelyn yhteydessä. Monissa
kielissä tämä on kuitenkin mahdollista. Esimerkiksi FORTRANissa voidaan DATA lauseella antaa taulukoille (sekä muuttujille ja merkkijonoille) alkuarvoja, esimerkiksi
REAL REAALILUVUT(4)
DATA REAALILUVUT/1.1,2.5,99.3,12.2234/
alustaa taulukon luetelluilla arvoilla. Sekä C, C++, C# että Java -kielessä on mahdollista
esitellä ja alustaa taulukko luettelemalla sen alkiot ilman sen dimensioiden
määrittelemistä, esimerkiksi
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
int vektori[] = {2,4,6,8};
luo samalla nelipaikkaisen taulukon. Tämä lisää luonnollisesti ohjelmoijan mukavuutta,
mutta heikentää koodin luotettavuutta, koska taulukon koko määräytyy
automaattisesti eikä välttämättä huomata onko esimerkiksi jokin alkio jäänyt
vahingossa pois tai kirjoitettu kahdesti. Sama esittely voitaisiin luonnollisesti tehdä
myös
int vektori[4] = {2,4,6,8};
Tämä tapa on sikäli turvallisempi, että kääntäjä huomaa, mikäli alustuslukuja on liikaa.
Kaksi- ja useampiulotteinen taulukko voidaan C/C++ -kielessä alustaa luettelemalla
ainoastaan niin, että ainoastaan yksi dimensio jätetään vapaaksi:
int matriisi[][4] = {{1,2,3,4},{2,3,4,5}};
Javassa sen sijaan esittelyt
int matriisi[][] = {{1,2,3,4},{2,3,4,5}};
int[][] matriisi2 = {{3,2,1,4},{5,3,4,2}};
on sallittu. C# -kielessä voidaan myös esitellä ja alustaa useampiulotteiset taulukot
täysin luettelemalla, mutta tällöin on kirjoitettava
int[,] matriisi = {{1,2,3,4},{2,3,4,5}};
Lisäksi Javassa taataan se, että taulukko on alustettu oletusarvoilla, esimerkiksi
numeeriset taulukot arvoilla 0 ja oliotaulukot arvoilla null, vaikka ohjelmoija ei
kirjoittaisi alustuskoodia taulukolle. Tätä ei taata esimerkiksi C-kielessä, jossa
alustamaton taulukko sisältää satunnaista dataa.
Yleensä ohjelmointikieleen sinänsä ei ole sisällytetty juuri taulukko-operaatioita, ts.
sellaisia operaatioita, jotka käsittelisivät taulukkoa itsenäisenä yksikkönä. Tavallisesti
nämä on toteutettu kirjastofunktioina tai sisällytetty luokkakirjastoihin. FORTRAN 90
sisältää kuitenkin taulukko-operaatioita, esimerkiksi taulukkojen summan (alkioittain).
Adassa on mahdollista viitata taulukon osiin indeksirajoilla ja näin käsitellä esimerkiksi
matriisin rivejä tai sarakkeita vektoreina. Laajin kokoelma kieleen rakennettuja vektori-
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
ja matriisioperaatioita on eittämättä APL-kielessä, joka onkin pääasiassa suunniteltu
tällaisten tehtävien ohjelmoimiseen. APL-kielessä on operaattorit mm. matriisin
transponointiin, sen käänteismatriisin etsimiseen sekä vektoreiden piste- ja ristituloille.
Ohjelmoinnissa tarvitaan usein loogisia kokonaisuuksia, jotka koostuvat erilaisista
tietoalkioista. Tätä tarvetta varten on luotu tietue (record). Tietue on erityyppisten
tietoalkioiden kooste, jossa tiettyyn alkioon eli kenttään (field) viitataan sen nimellä.
FORTRANia lukuunottamatta tietue on sisältynyt kaikkiin yleisiin ohjelmointikieliin.
Oliokielissä ei kuitenkaan ole tarvetta tietueille, koska olioilla voidaan toteuttaa myös
tietueet. C++ -kielessä voi vielä käyttää C:n jäänteenä tietueita, mutta tällöin nekin itse
asiassa määrittelevät tietynlaisen luokan. Sama koskee C# -kieltä. Java ei sisällä
tietuetietotyyppiä. Tietueen tarvitsema muisti varataan peräkkäisistä muistipaikoista
kuten taulukonkin. Tietueen kenttiin viittaaminen ei voi tapahtua kuitenkaan niin
suoraviivaisesti, kuin taulukon alkioihin, koska tietueen kentät ovat yleensä eripituisia.
Tietueiden muisti voidaan allokoida staattisesti tai dynaamisesti kuten muidenkin
muuttujien. Yleensä
Esimerkiksi C-kielessä (ks. [Ker], luku 6) voitaisiin määritellä
struct Asiakas
{
char nimi[25];
int tilinro;
};
jolloin tietue voitaisiin esitellä ja sen kenttiin voitaisiin viitata tyypillisellä
pistenotaatiolla:
struct Asiakas a;
strcpy(a.nimi,"Huijari");
a.tilinro = 21;
Mikäli tietue varattaisiin dynaamisesti ja käytettäisiin osoitintyyppistä muuttujaa
viittaamaan tietueeseen, C-kielessä käytetään erityistä operaattoria -> viittaamaan
kenttiin seuraavasti:
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
struct Asiakas* pa;
pa = malloc(sizeof(struct Asiakas));
strcpy(pa->nimi,"Huijari");
pa->tilinro = 21;
Pascalissa (ks. [Kor], kappale 7.3) vastaava tietue toteutettaisiin seuraavasti:
TYPE asiakas = RECORD
nimi: PACKED ARRAY[0..24] OF CHAR;
tilinro: INTEGER;
END;
VAR a:asiakas;
BEGIN
a.nimi := 'Huijari';
a.tilinro := 21;
Pascal -kielessä on toinenkin tapa viitata yllämääritellyn tietueen a kenttiin käyttämällä
WITH -lausetta:
WITH a DO
BEGIN
WRITELN(nimi);
WRITELN(tilinro);
END;
Tällöin lauseen sisällä oletetaan että annetut muuttujan nimet viittaavat tietueen
kenttiin. Tietueet ovat ohjelmoinnissa käyttökelpoisia tietotyyppejä; lisäksi niiden
toteutus on varsin suoraviivainen eikä sisällä ongelmia yleensä missään
ohjelmointikielessä.
Tietue vastaa yhden tai useamman tietotyypin karteesista tuloa ja sisältää siten kentät
jokaisen tietotyypin muuttujalle. Unioni (union) vastaa puolestaan tietotyyppien
unionia, joten unionityypin muuttuja sisältää vain yhden kentän, mutta tämän kentän
muuttuja voi olla tyypiltään jokin annetuista tietotyypeistä. Näin ollen unionityypin
muuttujan sisältämä tieto voi vaihdella tyypiltään ohjelman suorituksen aikana.
Unionityyppisten muuttujien käyttö voi olla hyödyllistä esimerkiksi kirjoitettaessa
koodia, jonka on tarkoitus toimia eri ympäristöissä. Joissakin koneissa esimerkiksi int tyyppinen kokonaisluku voi olla 16 -bittinen, toisissa 32 tai 64 -bittinen. Unionityypin
avulla voidaan kirjoittaa koodia, jossa tehdään ei-standardimaisia tyypinmuunnoksia.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Tässä piilee myös unionityypin käytön vaara: se voi aiheuttaa monia ongelmatilanteita
koodissa juuri tästä syystä.
C ja C++ -kielissä union -lauseella voidaan määritellä niin sanottu vapaa unioni (free
union) (ks. [Ker], kappale 6.8 ja [Strou] Appendix C 8.2). Nimitys johtuu siitä, että
unionissa ei ole minkäänlaista valitsinkenttää kertomassa minkä tyyppinen tieto on
tällä hetkellä talletettu muuttujaan. Muuttujalle varataan tila suurimman mahdollisen
tietotyypin mukaan ja mitään tyypintarkistusta ei tehdä, joten ohjelmoija on täysin
vapaa muuntamaan tietotyypin miksi tahansa vaihtoehdoista. Esimerkiksi C-ohjelmassa
union IntMerkki
{
int myInt;
char myChar;
};
int arvo;
union IntMerkki imer;
imer.myChar = 'A';
voidaan sijoittaa kokonaislukumuuttujaan
arvo = imer.myInt;
jolloin muutttujan arvo on satunnainen, koska ainoastaan yhtä tavua on käytetty
sijoitettaessa merkki 'A'. Samoin voidaan tehdä
imer.myInt = 5432;
jolloin
imer.myChar == '8';
Unioneja käyttämällä on näin ollen mahdollista kiertää C-kielen tyypintarkistus ja
turvallisuus jää ohjelmoijan vastuulle. Näin ollen unionien käyttäminen ohjelmassa
pitäisi aina tehdä suurta harkintaa noudattaen.
Jotta unioni- tyypissä voitaisiin tehdä tyypintarkistus, on tietotyyppiin liitettävä jokin
tietokenttä, jonka perusteella päätellään, mikä on kulloinkin talletettu tyyppi. Tällaista
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
kenttää kutsutaan valitsinkentäksi (tag field, discriminant). Pascal -kielen
unionityyppiä kutsutaan vaihtelevaksi tietueeksi (variant record) ([Kor], kappale
7.3.3) . Tällaisessa tietueessa voi olla kiinteä ja vaihteleva osa. Kiinteä osa säilyy aina
samanlaisena, mutta vaihteleva osa voi sisältää eri tietotyypin arvoja. Tällöin
TYPE muoto = (ympyra,suorakaide);
kuvio = RECORD
x_coord:REAL;
y_coord:REAL;
case tyyppi:muoto OF
ympyra:(sade:REAL);
suorakaide:(leveys:REAL;korkeus:REAL);
END;
Määrittelee tietueen, jossa vaihtelevassa osassa on kentän tyyppi perusteella joko
reaalikenttä sade tai kaksi reaalikenttää leveys ja korkeus. Tällaista tietuetta voi käyttää
ohjelmassa kuten tavallistakin, ts. viittaamalla pistenotaatiolla tietueen kenttiin tai
käyttämällä WITH -lausetta:
VAR hahmo:kuvio;
VAR ala:REAL;
BEGIN
hahmo.x_coord:=1.1;
hahmo.y_coord:=2.1;
hahmo.tyyppi := ympyra;
hahmo.sade:=1.2;
WITH hahmo DO
CASE tyyppi OF
suorakaide: ala := leveys*korkeus;
ympyra: ala := 3.14 * sade * sade;
END;
Pascalin unionityyppi on hieman turvallisempi kuin C-kielen, mutta silti turvaton, koska
se rikkoo Pascalin muuten vahvan tyypityksen, yleensä ei voida havaita vaihtelevien
tietueiden väärinkäyttöä. Esimerkiksi ylläolevassa koodissa voitaisiin muuttaa rivi
hahmo.tyyppi := ympyra;
riviksi
hahmo.tyyppi := suorakaide;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
muuttamatta muuta koodia. Tällöin ohjelmassa viitattaisiin suorakaide -tyyppisen
muuttujan sade -kenttään, jota ei pitäisi olla olemassakaan. Mutta tätä virhettä ei
kääntäjä havaitse. Samoin suorakaiteeksi määritellyn muuttujan voisi muuttaa
myöhemmin ympyräksi kajoamatta tietueen kenttiin, jotka suorakaiteen tapauksessa
olivat leveys ja korkeus. Näin ollen unionitietotyypin toteutuksessa on ongelmansa. Ada
-kielessä on samankaltainen vaihtuvan tietueen tyyppi kuin Pascalissa, mutta sitä on
parannettu niin, että valitsinkenttää ei voi muuttaa muuttamatta koko tietuetta ja
lisäksi Adassa on tarkistettava väärät kenttäviittaukset. Näin ollen Adan unionityypin
toteutuksen tulisi olla turvallinen. Monissa kielissä, kuten Javassa ja C#:ssa ei
unionityyppiä ole toteutettu.
Joukkotyypin (set type) muuttujat voivat sisältää järjestämättömän kokoelman jonkin
ordinaalityypin muuttujan erillisiä arvoja. Näin ollen joukkotyypillä mallinnetaan
matematiikasta tutun joukon käsitettä. Yleisistä (imperatiivisista) ohjelmointikielistä
ainoastaan Pascalissa on toteutettu joukkotyyppi ([Kor], kappale 7.4). Esimerkiksi
TYPE paiva = (su,ma,ti,ke,tor,pe,la);
paivaset = SET OF paiva;
VAR joukko:paivaset;
BEGIN
joukko := [ma,pe,ke];
IF pe IN joukko THEN
WRITELN('PAIVA OLI JOUKOSSA')
ELSE
WRITELN('PAIVA EI OLLUT JOUKOSSA');
määrittelee joukkotyypin, jonka alkiot voivat olla päiviä, luettelee erään
joukkomuuttujan alkiota ja tarkistaa, onko annettu alkio joukossa. Pascalissa on lisäksi
määritelty joukoille tavallisimmat joukko-opin operaatiot leikkaus, unioni ja joukkojen
vertailu (onko toinen joukko toisen osajoukko, ovatko joukot samat vai eri joukot).
Lopuksi tarkastellaan osoitintyyppiä (pointer type). Osoitintyypin muuttujien arvot
ovat muistiosoitteiden arvoja ja lisäksi erityinen arvo null (tai nil). Osoitintyypin
muuttujia kutsutaan yleensä osoittimiksi (pointers). Osoitintyypin muuttujia tarvitaan
käsittelemään dynaamisia muuttujia ja erityisesti dynaamisia tietorakenteita. Lähes
kaikissa imperatiivisissa ohjelmointikielissä on mahdollista käyttää osoittimia.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Poikkeuksena tästä on FORTRAN (77 ja varhaisemmat), jossa dynaamisten
tietorakenteiden käyttö onkin hankalaa. Osoitintyypin muuttuja siis sisältää
muistiosoitteen, joten muuttujana se on aina saman tyyppinen, mutta ohjelmoinnin
kannalta on tiedettävä minkälaiseen muuttujaan osoitin osoittaa; tätä tyyppiä sanotaan
osoitinmuuttujan tarkoitetyypiksi (reference type). Osoitin esitelläänkin aina
kirjoittamalla tarkoitetyyppi ja sen jälkeen tyyppioperaattori ennen muuttujan nimeä.
Tyyppioperaattori on C/C++ -kielessä *, Pascalissa ^ ja Adassa access. Periaatteessa
osoitinmuuttuja voi viitata mihin tahansa tietotyyppiin, myös toisen osoittimeen. Se
muuttuja, johon osoitinmuuttujan arvo kulloinkin viittaa, on sen tarkoitemuuttuja.
Osoitintyypin muuttujaan liittyy oleellisesti kaksi operaatiota: sijoitus (assignment) ja
muistipaikan sisältöön viittaaminen (viittauksen purkaminen, dereferencing).
Ensimmäinen operaatio sijoittaa osoitinmuuttujan arvoksi jonkin (järkevän)
muistipaikan. Jälkimmäinen toteutetaan yleensä jollakin sisältö-operaattorilla.
Esimerkiksi C-kielisessä ohjelmassa voisi kokonaislukumuuttuja luku olla muistipaikassa
9876 ja sen arvo olla 1000. Olkoon toinen kokonaislukumuuttuja toinenluku esitelty
ohjelmassa. Jos nyt kokonaislukuosoitinmuuttujan ptr arvo on 9876, käsky
toinenluku = *ptr;
kopioi muistipaikasta 9876 arvon 1000 muuttujan toinenluku arvoksi, ts. tekee
täsmälleen saman operaation kuin
toinenluku = luku;
Muita tyypillisiä osoitinmuuttujille tehtäviä operaatioita on muuttujien vertailu, ts. sen
vertaaminen osoittavatko ne samaan muistipaikkaan. Pascalissa muita operaatioita
osoittimille ei (ehkä viisaasti) sallitakaan. Pascalissa muuttuja varataan dynaamisesti ja
sitä käytetään sijoituslauseessa seuraavasti:
VAR r:REAL;
VAR pr:^REAL;
BEGIN
NEW(pr);
pr^ := 21.1;
r := pr^;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Tämän jälkeen muuttujalla r on arvo 21.1.
C ja C++ -kielissä osoittimia voi käyttää hyvin monipuolisesti, mikä antaa ohjelmoijalle
monenlaisia mahdollisuuksia, mutta tekee niiden virheellisen käytön myös helpoksi.
Minkä tahansa muuttujan muistiosoite voidaan selvittää ja sijoittaa osoitinmuuttujaan,
esimerkiksi
int muuttuja = 20;
int *pm;
pm = &muuttuja;
*pm = 55;
sijoittaa muuttujan arvoksi 55. Lisäksi C-kielessä osoitintyyppien tyypintarkistus on
varsin väljää; C:ssä
float f = 0.46875;
int *pif;
float *pf;
pif = &f;
pf = pif;
on sallittu, mutta C++ ei anna tehdä kumpaakaan osoitinsijoitusta. Tällainen
ohjelmointitapa on luonnollisesti arveluttava. Lisäksi C/C++-kielessä voidaan soveltaa
osoitinaritmetiikkaa (ks. esim.[Ker], luku 5). Jos ptr on osoitintyypin muuttuja, ptr+1
osoittaa seuraavan tarkoitetietotyypin muuttujan osoitteeseen. Toisin sanoen, jos ptr
on tyyppiä char*, ptr+1 osoittaa seuraavaan tavuun, mutta jos se on tyyppiä int*, ptr+1
osoittaa neljän tavun päähän ptr:stä. (Olettaen, että char on yhden tavun mittainen ja
int neljän tavun mittainen.) Esimerkiksi taulukko voidaan käydä läpi osoittimia
käyttämällä:
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
char c;
int ki;
double rl;
int index;
char merkit[4] = {'a','b','c','d'};
int kokoluvut[4] = {1,2,3,4};
double rluvut[4] = {1.1,2.2,3.3,4.4};
char* pc = merkit;
int* pi = kokoluvut;
double* pd = rluvut;
for(index=0;index < 4; index++)
{
c = *(pc + index);
ki = *(pi + index);
rl = *(pd + index);
printf("Merkki on %c, kokonaisluku on %d, reaaliluku on %e\n", c,ki,rl);
}
Huomaa, että kaikkien taulukoiden alkioihin viitataan samalla tavalla, vaikka
taulukoiden sisältämät tietotyypit ovatkin erikokoisia. C ja C++ -kielessä on lisäksi
olemassa geneerinen osoitintyyppi void*, joka voi osoittaa minkä tyyppiseen
muuttujaan tahansa. Tällainen osoitin on aina tyypitettävä jonkin tyyppiseksi
osoittimeksi ennen muistiosoitteeseen viittaamista. Yleisimmin void* -osoittimia
käytetään muistia käsittelevien funktioiden parametreina.
Osoitintyyppi on dynaamisten muuttujien ja tietorakenteiden käsittelyssä hyödyllinen,
mutta osoitintyyppisten muuttujien käyttö johtaa myös moniin ongelmiin. Tyypillisiä
ongelmia ovat roikkuvat osoittimet, muistivuoto ja moninimisyys. Mahdollisuutta
moninimisyyteen (aliasing) käyttämiseen ohjelmointikielessä ei pidetä kovin suotavana,
koska tällöin voidaan kirjoittaa koodia, jossa muuttujan arvon vaihtumista ei ole helppo
havaita. Tällöin koodin luettavuus kärsii. Osoitintyypin muuttujien salliminen johtaa
väistämättä moninimisyyteen. Esimerkiksi Pascal -koodissa
VAR r:REAL;
VAR pr,pra:^REAL;
BEGIN
NEW(pra);
NEW(pr);
pr^ := 21.1;
pra := pr;
pra^:=33.2;
r := pr^;
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
ei välttämättä huomaa sijoittavansa muuttujaan r arvoa 33.2. C/C++-kielessä
moninimisyys ilmenee vielä moninaisimmin tavoin, varsinkin koska myös staattisesti
sidottujen muuttujien muistiosoitteita voi sijoittaa osoitintyypin muuttujiin. Roikkuva
osoitin (dangling pointer) tarkoittaa osoitinta, joka osoittaa jo vapautettuun muistiin.
Tyypillisesti tämä perustuu siihen, että dynaamisesti varatut muuttujat joudutaan myös
erikseen vapauttamaan ja koska useampi osoitinmuuttuja voi osoittaa samaan
muistiosoitteeseen näitä voi jäädä roikkumaan muistia vapautettaessa. Useimmiten
ohjelmassa tämä tapahtuu seuraavasti: Osoitinmuuttuja p1 osoittaa dynaamisesti
varattuun muuttujaan, toiseen osoitinmuuttujaan p2 sijoitetaan p1 ja p1:n
tarkoitemuuttuja vapautetaan (ja yleensä sijoitetaan p1=null). Mutta p2 osoittaa
edelleen muistiosoitteeseen, jossa dynaaminen muuttuja sijaitsi: p2 on muuttunut
roikkuvaksi osoittimeksi ja sen muistialueeseen viittaaminen tuottaa satunnaisia arvoja.
Mikäli aiemmin esiintynyttä Pascal-koodia muutetaan seuraavasti:
NEW(pra);
NEW(pr);
pr^ := 21.1;
pra^:=33.2;
pra := pr;
dispose(pra);
pra := NIL;
r := pr^;
muuttujassa r on jokin satunnainen kokonaisluku (useimmiten luultavasti 0). Samoin
C++ -kielisessä esimerkissä
int *pv,*pu;
pv = new int;
*pv = 55;
pu = pv;
delete pu;
pu = 0;
int ki = *pv;
muuttujassa ki on satunnainen arvo. Muistivuodoksi (memory leakage) sanotaan
tilannetta, jossa ohjelmassa varattua dynaamista muistia ei vapauteta. Tämä voi syntyä
hävittämällä viite dynaamiseen muuttujaan. Tyypillisesti tämä tapahtuu niin, että
osoitin, joka osoittaa dynaamisesti varattuun muuttujaan, sijoitetaan osoittamaan
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
johonkin toiseen muuttujaan. Ellei ensimmäistä muuttujaa sitä ennen vapauteta, siihen
ei osoita mikään muuttuja ja sen varaamaa muistia on mahdotonta enää vapauttaa.
Esimerkiksi C++ -koodissa
int *pv,*pu;
pu = new int;
pv = new int;
pu = pv;
ensimmäinen varattu kokonaisluku jää ohjelmassa vapauttamatta. Sama ongelma voi
esiintyä kaikissa kielissä, joissa dynaamisesti varattu muisti on ohjelmallisesti
vapautettava, esimerkiksi Pascalissa ja C:ssä.
Osoitintyypin muuttujien ongelmallisuuden takia ainakin Javassa ja C#:ssa on luovuttu
niistä ja korvattu ne viitteillä. Viitetyypin (reference type) muuttujat sisällytettiin aluksi
C++ -kieleen lähinnä toteuttamaan viitetyypin parametrinvälitys funktioille.
Viitemuuttujan avulla voidaan luoda muuttujalle alias. C++ -kielen viite on vakioosoitin, jolle tehdään implisiittinen muistiosoitteeseen viittaaminen (ks. [Strou] kappale
5.5). Koska viite on vakio, se on alustettava jollakin muistiosoitteen arvolla, ja se viittaa
samaan muistiosoitteeseen koko elinaikansa. Esimerkiksi
float f;
float &ref_f = f;
tekee muuttujista f ja ref_f aliaksia. Tällöin sijoituslause
ref_f = 2.78;
sijoittaa muuttujan f arvoksi 2.78. Java on yleistänyt viitemuuttujan tyyppiä verrattuna
C++-kieleen niin, että osoittimet on voitu täysin korvata viitteillä. Kaikki luodut oliot
Javassa varataan dynaamisesti kekomuistista ja olion nimi on itse asiassa viite tähän
olioon. Minkäänlainen osoitinaritmetiikka ei ole Javan viitteille sallittua; Javan viitteet
eivät myöskään ole vakio-osoittimia vaan viite voidaan asettaa osoittamaan toiseenkin
olioon. Muistivuotojen estämiseksi Javan dynaamisesti varattuja olioita ei tarvitse
ohjelmoijan vapauttaa, vaan roskien keruu (garbage collection) huolehtii siitä, että
oliot vapautetaan, kun niihin ei enää ole viittauksia.
Ari Vesanen, Tietojenkäsittelytieteiden laitos, Oulun yliopisto
815338A Ohjelmointikielten periaatteet: Muuttujat imperatiivisissa kielissä
Lähteet
[Arc] Archer, Tom. Inside C#. Edita, IT Press, 2001.
[Arn] Arnold, Ken Gosling, James. The Java Programming Language, Second Edition,
Addison-Wesley 1998
[Har] Harsu, Maarit. Ohjelmointikielet. Periaatteet, käsitteet,
valintaperusteet, Talentum 2005.
[Ker] Kernighan, Brian Richie Dennis. The C Programming Language. Prentice Hall 1988.
[KLS] Kortela, Larmela, Salmela. FORTRAN 77. OtaData 1985.
[Kor] Kortela, Larmela, Planman. Pascal-ohjelmointikieli. OtaData 1980.
[Kur] Kurki-Suonio Reino. Ada-kieli ja ohjelmointikielten yleiset perusteet. MODEEMI ry
Tampere 1983.
[Lou] Louden, Kenneth C. Programming Languages, Principles and Practice, PWS-KENT
1993.
[Seb] Sebesta, Robert W. Concepts of Programming Languages 10th edition, Pearson
2013.
[Strou] Stroustrup, Bjarne. The C++ Programming Language, 3rd edition, Murray Hill
1997.