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.
© Copyright 2024