Aliohjelmat imperatiivisissa ohjelmointikielissä Tässä dokumentissa käsitellään aliohjelmien suunnittelu- ja toteuttamisperiaatteita imperatiivisissa ohjelmointikielissä lähinnä Sebestan ([Seb], luvut 9 ja 10) pohjalta. Esimerkeissä esiintyviä kieliä käsitellään tarkemmin mm. seuraavissa teoksissa: [Ker] (Ckieli), [Strou] (C++ -kieli), [Kor] (Pascal), [Kur] (Ada), [Arc] (C#), [Arn] (Java) ja [KLS] (FORTRAN). Harsun kirjan ([Har]) luku 6 käsittelee aliohjelmia. 1. Aliohjelmat Erityisesti rakenteisen ohjelmoinnin periaatteisiin on aina kuulunut ohjelmointiongelman ratkaiseminen asteittain tarkentamalla, mitä kutsutaan myös ohjelman osittavaksi tai top-down -suunnitteluksi. Tällöin ylätasolla ohjelma kuvataan erittäin karkeasti suurina toimintakokonaisuuksina, joita tarkennetaan, kunnes päästään ohjelmointikielen lauseiden tasolle. Useimmiten tätä ylemmät tasot on luonnollista kuvata aliohjelmina, jotka suorittavat jonkin tietyn toiminnon "mustan laatikon" tapaan: tiedetään, mitä aliohjelman tarkoitus on tehdä vaikka yksityiskohtia ei vielä tunnetakaan. Aliohjelmat ovat prosessien abstrahointiväline, joka on esiintynyt ohjelmointikielissä aina niiden historian alusta saakka. Toinen merkittävä abstrahointimenetelmä, data-abstraktio, on saavuttanut asemansa vasta oliokielten yleistymisen yhteydessä. Kaikki ohjelmointikielet sisältävät jonkinlaisen aliohjelmatoteutuksen ja aliohjelmat ovatkin yksi merkittävimmistä ohjelmointikielten käsitteistä. Sebestan kirjan ([Seb]) luvussa 9 käsitellään ohjelmointikielten aliohjelmien periaatteita.
Kaikkien tässä käsiteltävien ohjelmointikielten aliohjelmilla on muutamia yhteisiä piirteitä. 1. Jokaisella aliohjelmalla on täsmälleen yksi tulokohta. 2. Aliohjelmaa kutsuvan ohjelman suoritus estetään kutsun ajaksi, siis vain yhtä aliohjelmaa suoritetaan kerrallaan. 3. Ohjelmakontrolli palaa aina kutsuvalle ohjelmalle, kun aliohjelman suoritus loppuu. Itse asiassa FORTRANin aliohjelmilla saattaa olla useita tulokohtia, mutta tämän ominaisuuden merkitys on verrattain vähäinen. Aliohjelmat, joita voidaan suorittaa toistensa kanssa yhtäaikaisesti, kuuluvat rinnakkaiseen ohjelmointiin, jota käsitellään suhteellisen lyhyesti myöhemmin kurssissa. Kullakin hetkellä suoritettavaa aliohjelmaa sanotaan aktiiviseksi aliohjelmaksi. Aliohjelman määrittely koostuu kahdesta osasta: aliohjelman otsikosta (header) ja aliohjelman rungosta. Aliohjelman otsikko määrittelee sen rajapinnan, ts. aliohjelman nimen ja sen muodolliset parametrit ja niiden tyypin. Lisäksi otsikossa määritellään paluuarvon tyyppi, mikäli sellainen on. Joissakin kielissä (esim. Java) otsikkoon voi myös kuulua aliohjelman suorituksen aiheuttamien poikkeuksien tyyppi. Poikkeuksia käsitellään tuonnempana. Aliohjelman runko sisältää sen toimintalogiikan, rungossa ovat ne lauseet, jotka suoritetaan aliohjelmaa kutsuttaessa. Aliohjelman kutsu (call) tarkoittaa pyyntöä suorittaa aliohjelma; yleensä ohjelmointikielissä aliohjelmaa kutsutaan sen nimellä. FORTRANissa on aliohjelmakutsulle oma käsky CALL. Lisäksi rungossa esitellään aliohjelman paikalliset muuttujat ja määritellään aliohjelmalle alisteiset aliohjelmat. Kaikki kielet eivät kuitenkaan tue sisäkkäisiä aliohjelmia; Pascalissa sisäkkäiset aliohjelmat ovat mahdollisia, C-pohjaisissa kielissä eivät. Joissakin kielissä aliohjelmat voidaan myös esitellä (otsikkonsa avulla) erillään määrittelystä; tällöin puhutaan aliohjelman prototyypistä. Aliohjelmien prototyyppejä käytetään yleisesti C-kielessä, samoin Adassa ja Pascalissa. Java ei salli aliohjelmia esiteltävän; esittelyä ei Javan näkyvyyssääntöjen takia tarvitakaan.
Aliohjelman otsikossa määriteltävät parametrit ovat muodollisia (formaaleja) parametreja, koska ne sidotaan muistiin (yleensä muualla esiteltyihin muuttujiin) vasta aliohjelmaa kutsuttaessa. Siksi aliohjelman kutsussa on oltava lista parametreista, jotka sidotaan aliohjelman muodollisiin parametreihin. Näitä sanotaan todellisiksi parametreiksi. Lähes kaikissa ohjelmointikielissä sidonnan määrää parametrin sijainti listassa. Joissakin kielissä (Ada ja FORTRAN 90) voi käyttää myös avainsanaparametreja, jolloin avainsanalla määrätään parametrin sitominen. Tämä voi olla tarpeellista, mikäli aliohjelma käyttää lukuisia parametreja. Ada ja FORTRAN 90 sekä C++ sallivat myös määritellä parametreille oletusarvoja, joita käytetään, mikäli kutsuva ohjelma ei anna parametrille arvoa, esimerkiksi C++: ssa voitaisiin kirjoittaa funktio int funk(int eka, float toka, double kolmas = 1.0) jota kutsuttaessa kahdella parametrilla, esimerkiksi funk(2,3.14); käytettäisiin muuttujalle kolmas oletusarvoa 1.0. Mikäli ohjelmoija kuitenkin antaa kolmannen parametrin, sen arvoa käytetään. Ks. esimerkiksi [Strou], kappale 7.5. Aliohjelmat voidaan jakaa kahteen tyyppiin: proseduureihin (procedures) ja funktioihin (functions). Proseduuri määrittelee jonkin parametrisoidun operaation, jolla ei ole paluuarvoa. Näin ollen proseduuri määrittelee tavallaan uuden lauseen ohjelmointikieleen. Funktio taas muistuttaa matemaattista funktiota siinä mielessä, että se laskee argumenttejaan (parametrit) käyttämällä jonkin tuloksen, joka palautetaan kutsuvaan ohjelmaan. Funktiot ovat siis enemmän ohjelmointikielen lausekkeiden kaltaisia. Koska funktiot ja proseduurit muistuttavat rakenteellisesti toisiaan, voidaan proseduuria ajatella funktiona, joka ei palauta mitään arvoa. Näin onkin tehty C-pohjaisissa kielissä, joissa ei erikseen voida määritellä proseduuria, vaan sellainen kirjoitetaan funktiona, jonka paluuarvo on void. Aliohjelmat ovat verrattain monimutkaisia ohjelmointikielten rakenteita, joten niiden toteutuksen suunnittelussakin on otettava huomioon varsin monia kysymyksiä. Tärkeimpiä ja luonnollisimpia valittavia ominaisuuksia on parametrien välitysmekanismi, jonka toteutukseen on käytetty useita malleja eri
ohjelmointikielissä. On siis valittava se tapa, jolla muodollinen parametri sidotaan todelliseen parametriin. Edelleen parametrien tyypintarkistuksen aste on päätettävä, samoin kuin se, minkälaisia parametreja yleensä voidaan välittää (ovatko esimerkiksi aliohjelmat välitettävissä). Lisäksi on valittava aliohjelman paikallisten muuttujien allokointitapa (staattinen tai dynaaminen). Kielen suunnittelijan on myös ratkaistava, sallitaanko aliohjelmien määritysten sijaita aliohjelmien sisällä. Joissakin kielissä voidaan aliohjelmia ylikuormittaa (overload), ja joissakin kielissä aliohjelmat voivat olla geneerisiä, ts. niiden parametrien tyypit voivat vaihdella. Vielä joudutaan miettimään, onko mahdollista kääntää aliohjelmia riippumattomina yksikköinä. 3.1 Proseduurit ja funktiot Aliohjelmat voidaan siis jakaa proseduureihin ja funktioihin toimintaperiaatteensa mukaan. Silti niiden toteutus ohjelmointikielissä on varsin samantyyppinen. FORTRANissa proseduuri esitellään sanasella SUBROUTINE, esimerkiksi SUBROUTINE OPEROI(I,J) Jota kutsuttaisiin pääohjelmasta esimerkiksi CALL OPEROI(10,20) (ks. [KLS] kapple 11.2). Myös Pascalissa on proseduuri omana aliohjelmatyyppinään. Se esitellään seuraavasti PROCEDURE OPEROI(I:INTEGER;J:INTEGER) ja kutsutaan koodissa aliohjelman nimellä, ts. esimerkiksi OPEROI(10,20); Myös Adassa on erotettu proseduuri- ja funktiotyyppiset aliohjelmat. C -pohjaisissa kielissä funktion ja proseduurin ainoa ero on proseduurin paluuarvon tyyppi void (ja luonnollisesti se, että tällainen funktio ei voi palauttaa mitään arvoa). Näin ollen C- tyyppisissä kielissä proseduuri kirjoitetaan void operoi(int x, int y)
ja funktio (joka tässä palauttaa double -tyyppisen arvon) double laskearvo(int x, int y) Yleisimmin ohjelmointikielissä funktion paluuarvo annetaan return-lauseessa, joka aiheuttaa aliohjelman suorituksen loppumisen, esimerkiksi edellisessä C-funktiossa voisi olla lauseet double retval;... retval =...... return retval; Tällöin funktion paluuarvo olisi muuttujan retval se arvo, joka sillä on return-lausetta suoritettaessa. C -kielessä (samoin kuin C++:ssa ja Javassa) funktio voi sisältää useita return -lauseita. On kuitenkin suositeltavaa käyttää ainoastaan yhtä poistumiskohtaa funktiossa mikäli mahdollista. Myös Adassa käytetään return-lausetta. FORTRANissa myös aliohjelmasta (sekä proseduurista että funktiosta) palataan RETURN-lauseella, mutta sen yhteydessä ei kuitenkaan anneta paluuarvoa, vaan se annetaan funktiolle sijoituslauseessa, esimerkiksi FUNCTION SUMMA(II,JJ) SUMMA = II+JJ RETURN END Samoin annetaan Pascal -kielisen funktion paluuarvo, mutta Pascal ei tunne return - lausetta, joten funktio suoritetaan loppuun saakka. Esimerkiksi FUNCTION MYFUNK(para1:INTEGER;para2:INTEGER): REAL; var m:integer; BEGIN MYFUNK := 2.1; FOR m:=1 TO para1+para2 DO writeln ('turhaa'); END;
Tätä ominaisuutta voidaan pitää sisäisen yhdenmukaisuuden puutteena Pascal- ja FORTRAN-kielissä, koska funktion paluuarvon antaminen muistuttaa sijoituslausetta. FORTRANissa funktiota kutsutaan sen nimellä toisin kuin proseduuria. 3.2 Parametrien välittäminen Yleensä aliohjelmissa voidaan määritellä omia muuttujia, joita kutsutaan paikallisiksi muuttujiksi (local variables). Muuttujien näkyvyysalueita on käsitelty aiemmin tässä dokumentissa. Aliohjelma voi siirtää tietoa kutsuvan ohjelman kanssa joko globaalien (yhteisten) muuttujien avulla tai parametrien välityksellä. Globaalien muuttujien käyttäminen lisää kuitenkin aliohjelmien sivuvaikutuksia, mikä ei ole toivottu piirre ohjelmassa. Siksi tiedot on suositeltavaa välittää parametrien avulla. Parametrit tekevät aliohjelmasta yleiskäyttöisen, niiden avulla voidaan välittää tietoa sekä aliohjelmaan että aliohjelmasta. Siten parametrit voidaan jakaa ryhmiin käyttötapansa (semantiikkansa) perusteella: 1. Parametrit, jotka välittävät tietoa aliohjelmaan 2. Parametrit, jotka välittävät tietoa aliohjelmasta kutsuvaan ohjelmaan 3. Parametrit, jotka välittävät tietoa kumpaankin suuntaan Tällöin puhutaan myös parametrin sidonnan moodista. Ensimmäisessä kohdassa puhutaan in -moodista, toisessa out -moodista ja kolmannessa inout -moodista. Joissakin kielissä parametri voidaan erikseen määritellä kuuluvaksi johonkin tyyppiin, joissakin kielissä välitystavat pitää toteuttaa muulla tavoin. Aina myöskään kielen määrittely ei kuvaa parametrien välitysmekanismia, vaan se voidaan jättää toteutuksesta riippuvaksi. Datan siirto parametrin välityksessä voi tapahtua periaatteessa kahdella tavalla: Joko välitettävä arvo kopioidaan tai tiedon saantipolku (käytännössä useimmiten tiedon sisältävän muistipaikan osoite) siirretään. Jälkimmäinen malli, josta käytetään myös nimitystä viitevälitys, on siirrettävän datan suhteen tehokkaampi, erityisesti mikäli siirrettävä arvo on jokin muistia kuluttava olio. Edellinen on turvallisempi tapa siirtää
tietoa, koska aliohjelma käsittelee alkuperäisen tiedon kopiota. Huomaa, että tiedon kopiointi voi tapahtua molempiin suuntiin, ts. sekä aliohjelmaan että aliohjelmasta. Seuraavaksi tarkastellaan parametrien välitysmallien toteutusmekanismeja. Yleisimmät mekanismit ovat: Moodi Välitysmekanismi Parametrityyppi in Arvovälitys (Pass-by-Value) Arvoparametri, Vakioparametri out Tulosvälitys (Pass-by-Result) Tulosparametri inout Arvo-tulosvälitys (Pass-by-Value-Result), Viitevälitys (Pass-by-Reference), Nimivälitys Arvo-tulosparametri, Viiteparametri, Nimiparametri Kun käytetään arvovälitystä, muodollinen parametri on eräänlainen paikallinen muuttuja, joka alustetaan todellisen parametrin arvolla (rvalue). Tämä toteutetaan yleisimmin kopioimalla todellisen parametrin data paikalliseen muuttujaan. Näin ollen parametrimuuttujan muuttaminen aliohjelmassa ei vaikuta todellisen parametrin arvoon, esimerkiksi C-ohjelmassa void f(int x) x = 10; } int y = 20; f(y); muuttujan y arvo funktiokutsun jälkeenkin on 20. Vakioparametriksi kutsutaan sellaista parametria, jonka arvoa ei voida muuttaa, ts. parametri onkin nyt paikallinen vakio. Tämän parametrinvälitystavan pääasiallinen haitta on suurikokoisten parametrien välittäminen, jolloin kopiointiin kuluu resursseja. Tulosvälityksessä todellisten parametrien arvoja ei välitetä aliohjelmalle, vaan aliohjelma käsittelee parametria kuin paikallista muuttujaa, jonka arvo kopioidaan
kutsuvan ohjelman parametrin arvoksi. Todellisen parametrin on näin ollen aina oltava muuttuja. Tulosvälitys toimii ikään kuin arvovälitys päinvastoin. Tulosvälityksessä voi kuitenkin esiintyä törmäämisongelma, jota ei esiinny arvovälityksessä. Olkoon esimerkiksi aliohjelmassa ali (int x, int y) x = 2; y = 3; } käytössä tulosvälitys muuttujille x ja y. Jos pääohjelmassa on määritelty muuttuja z, niin mikä sen arvo on kutsun ali(z,z) jälkeen? Arvo-tulosvälitys on yhdistelmä edellisistä ja toteuttaa tiedonvälityksen molempiin suuntiin. Tässä mallissa todellisen parametrin arvo kopioidaan ensin paikallisena muuttujana toimivan muodollisen parametrin arvoksi ja aliohjelman palatessa tämän paikallisen muuttujan arvo kopioidaan todellisen parametrin arvoksi. Tässäkin tapauksessa todellisen parametrin on oltava muuttuja. Arvo-tulosvälitys sisältää sekä arvo- että tulosvälityksen haittapuolet. Viitevälitys toteuttaa myös tiedonvälityksen molempiin suuntiin. Tässä tapauksessa ei kuitenkaan välitetä arvojen kopioita, vaan parametrina annetaan muuttujan muistiosoite, jolloin aliohjelma muokkaa alkuperäistä muuttujaa. Viitevälitys on resurssien käytön kannalta huomattavan tehokas, koska dataa ei tarvitse siirtää, eikä ylimääräistä muistia varata. Turvallisuus kärsii esimerkiksi siitä syystä, että aliohjelman muokatessa alkuperäistä muuttujaa, ei virhetilanteessa ole enää mahdollista palauttaa sitä aliohjelmakutsua edeltäneeseen tilaan. Toiseksi viitevälitys voi tuottaa moninimisyyttä, joka voi olla haitallista. Esimerkiksi C-funktiota void funk(int *pi1,int *pj2) kutsuttaessa int muuttuja = 1; funk(&muuttuja, &muuttuja);
funktiossa pi1 ja pj2 viittaavat samaan muuttujaan. Tämä voi aiheuttaa hankalia virhetilanteita. Muillakin tavoin viitevälityksessä voi syntyä moninimisyyttä. Näitä ongelmia ei esiinny arvo-tulosvälityksessä, mutta sekään menetelmä ei ole ongelmaton. Aivan oma tyyppinsä on myöhäiseen sidontaan perustuva nimivälitys. Tunnetuista kielistä vain ALGOL 60 käytti tätä menetelmää parametrien välityksessä. Ideana on ollut toteuttaa tekstuaalinen kopiointi, jossa todellinen parametri (ilmauksena) kopioituu muodollisen parametrin kaikkiin esiintymiskohtiin ja tämän jälkeen aliohjelma kopioituu sen kutsumiskohtaan. Täten tosiasiassa tapahtuva parametrin välitys riippuu parametrin tyypistä. Mikäli todellinen parametri on primitiivisen tietotyypin muuttuja, välitysmekanismi vastaa viitevälitystä. Jos taas todellinen parametri on lauseke, joka sisältää muuttujia, mekanismi ei muistuta mitään edellä esitetyistä mekanismeista. Käytännössä viitevälitys on vaikea toteuttaa ja ohjelmoijan kannalta hankala, joten sitä ei ole sittemmin juuri muissa kielissä käytetty lukuunottamatta SIMULA 67:ää. Tunnetuimpien kielten parametrinvälitysmekanismeja käsitellään seuraavaksi. FORTRAN -kielessä käytetään inout -moodia parametrien välitykseen. Kieli ei kuitenkaan määrittele, käytetäänkö arvo-tulosvälitystä vai viitevälitystä. Ainakin vanhemmat FORTRANin versiot käyttivät yleisesti viitevälitystä, joissakin myöhemmissä versioissa on käytössä ollut arvo-tulosvälitys. FORTRANin parametrinvälityksestä seuraa, että aliohjelmassa voidaan muuttaa todellisten parametrien arvoja, ts. aliohjelma SUBROUTINE VAIHDA(A,B) INTEGER A,B,X X = A A = B B = X RETURN END vaihtaa kutsuttaessa CALL VAIHDA(EKA_MUU,TOKA_MUU) muuttujien EKA_MUU ja TOKA_MUU arvot.
C -kieli käyttää (samoin kuin C++ ja Java) aina arvovälitystä parametreille, näin ollen C - kielinen funktio void vaihda(int x,int y) int temp = x; x = y; y = temp; } ei kutsuttaessa int eka = 10, toka = 20; vaihda(eka,toka); vaihda todellisten parametriensa eka ja toka arvoja. C-kielessä, samoin kuin C++ - kielessä voidaan kuitenkin inout -semantiikka toteuttaa käyttämällä osoittimia. Kun muutetaan yllä oleva funktio muotoon kutsu void vaihda(int *x,int *y) int temp = *x; *x = *y; *y = temp; } int eka = 10, toka = 20; vaihda(&eka,&toka); vaihtaa muuttujien eka ja toka arvot. Huomaa, että tässä tapauksessa funktiolle välitetään parametrina muuttujien osoitteet, joten parametrinvälitysmekanismi on silti sama kuin ennenkin. C++ :ssa on lisäksi referenssimuuttujatyyppi, jota on käsitelty aiemmin. C++ -kielen referenssi on vakio-osoitin, jolle tehdään implisiittinen muistiosoitteeseen viittaaminen, näin ollen käyttämällä referenssimuuttujia voidaan toteuttaa viitetyyppinen parametrin välitys. Näin ollen C++ -kielessä edellinen funktio voitaisiin myös kirjoittaa
ja kutsua void vaihda(int &x,int &y) int temp = x; x = y; y = temp; } int eka = 10, toka = 20; vaihda(eka,toka); jolloin muuttujien arvot vaihtuvat. Javassa ei ole osoitintyyppiä ja sen referenssityyppi poikkeaa C++ -kielen referenssistä, joten ylläolevan kaltaista kahden primitiivistä tietotyyppiä olevan muuttujan arvoa vaihtavaa metodia ei voi Javalla kirjoittaa. C# -kielen oletusmekanismi parametrinvälitykseen on arvovälitys, mutta myös viitevälitys on mahdollinen. Tämä saadaan aikaan määrittelemällä parametri joko ref tai out -tyyppiseksi ([Arc], luku 6). Määrittelyt eroavat ainoastaan siten, että ref - tyyppisenä parametrina käytettävä muuttuja on alustettava, kun out -tyyppistä parametria ei tarvitse. Näin ollen yllämainittu vaihtofunktio kirjoitettaisiin C#:ssa esimerkiksi public void vaihda(ref int x, ref int y) int temp = x; x = y; y = temp; } Pascalissa voidaan parametrin välityssemantiikka valita. Ellei parametrille anneta välitystyyppiä, käytetään arvovälitystä. Jos taas halutaan inout -semantiikka, määritellään parametrit muuttujaparametreiksi määreellä VAR. Tällöin käytetään viittausvälitystä, ts. aliohjelma käsittelee suoraan parametrimuuttujan muistipaikkaa (ks. [Kor], kappale 6.2.4). Näin ollen Pascalin aliohjelma
PROCEDURE vaihda(x,y:integer); var temp:integer; BEGIN temp := x; x := y; y := temp; END; ei vaihda parametriensa arvoja kutsuvassa ohjelmassa, mutta PROCEDURE vaihdaoikein(var x,y:integer); var temp:integer; BEGIN temp := x; x := y; y := temp; END; vaihtaa. Samassa aliohjelmassa voi olla sekä arvo- että muuttujaparametreja. Adassa voidaan käyttää kaikkia kolmea semantiikkaa: Adassa parametrien välitys tapahtuu kopioimalla, ts. in -parametrit välitetään arvovälityksellä, out -parametrit tulosvälityksellä ja in out -parametrit arvo-tulosvälityksellä. Näin ollen Ada -versio arvojen vaihtorutiinista olisi procedure vaihda(x: in out integer, y: in out integer) is temp:integer; begin temp := x; x := y; y := temp; end vaihda; Adassa arvoparametrit ovat tyyppiä in ja tulosparametrit tyyppiä out (ks. [Kur], kappale 5.5). 3.3 Taulukkojen välittäminen parametrina Taulukkojen - erityisesti useampiulotteisten - välittäminen parametrina vaatii monissa kielissä erityisiä toimenpiteitä, koska taulukot ovat yhtenäisiä muistialueita, joissa indeksointi lasketaan dimensioiden perusteella. Usein on kuitenkin välitettävä aliohjelmalle taulukko, jonka kokoa ja dimensioita ei tunneta (yleensä on kuitenkin
tunnettava ainakin ulottuvuuksien lukumäärä). Jo aiemmin mainittiin alkuperäisen Pascal-kielen ominaisuus, että taulukon indeksirajat olivat osa taulukon tyyppiä, joten oli mahdotonta kirjoittaa aliohjelmaa, joka olisi parametrinaan ottanut erikokoisia taulukoita. Tätä varten Pascalissa voidaan nykyään yleisesti käyttää avoimia taulukoita. Tällöin aliohjelmalle annetaan parametriksi vain taulukon tyyppi ja sen indeksien oletetaan alkavan nollasta. Korkein indeksi saadaan kutsumalla funktiota HIGH(). (Ks. aiempi esimerkki) C ja C++ -kielissä yksiulotteinen taulukko voidaan antaa parametrina aliohjelmalle ilman taulukon rajoja (tällöin luonnollisesti on mahdollista, että operaatiot ylittävät taulukon rajat, ellei niitä tunneta aliohjelmassa). Moniulotteisia taulukoita ei sen sijaan voida käsitellä aliohjelmissa, ellei dimensioita tunneta ylintä lukuunottamatta. Näin ollen muodollisissa parametreissa on annettava taulukon kaikki muut dimensiot, esimerkiksi kaksiulotteinen taulukko on välitettävä seuraavasti: void matriisifunktio(float matrix [][20]) jne Tällöin parametrina voidaan välittää esimerkiksi tai float matti[10][20]; float mattix[100][20]; mutta ei taulukkoa float mattiy[20][10]; Siksi ei ole mahdollista kirjoittaa funktiota, joka parametrinaan ottaisi käsiteltäväksi mielivaltaisen kaksiulotteisen taulukon ilman muita parametreja. Tällainen yleiskäyttöinen funktio voidaan C:lläkin kirjoittaa pienellä vaivalla: Kirjoitetaan funktio void matriisifunktio(float *mat_ptr, int rows, int cols) jne
jolle annetaan parametreina taulukko, taulukon rivien ja sarakkeiden lukumäärä. Nyt ohjelmoijan omalla vastuulla on kirjoittaa indeksointi ottamalla huomioon, että taulukon alkio mat_ptr[x,y] on sama kuin *(mat_ptr + x*cols +y) koska C:ssä indeksointi tehdään ensin rivien, sitten sarakkeiden mukaan ([Ker], Appendix A 8.6.2). Javassa taulukot ovat olioita, joiden dimensiot tunnetaan aina. Itse asiassa taulukot ovat aina yksiulotteisia, mutta niiden alkiot voivat olla taulukoita, jolloin saadaan rakennettua useampiulotteisia taulukoita. Javassa voidaan välittää mielivaltaisia taulukoita parametreina aliohjelmille, joissa saadaan taulukon koko selville kysymällä length -attribuutin arvoa kullekin dimensiolle. Adassa voidaan käyttää rajoittamattomia taulukoita ja välittää niitä parametreina aliohjelmalle; taulukon koko saadaan selville range -funktiolla, soveltamalla tätä kuhunkin dimensioon erikseen. Javan ja Adan mekanismit muistuttavat siis melkoisesti toisiaan. Adassa voitaisiin kirjoittaa esimerkiksi mielivaltaisen matriisin alkioiden summan laskeva funktio seuraavasti: type MATRIISI is array (INTEGER range<>, INTEGER range<>) of FLOAT; function MATSUMMA(MATRIX: in MATRIISI) return FLOAT is SUMMA:FLOAT := 0.0; begin for RIVI in MATRIX'range(1) loop for SARA in MATRIX'range(2) loop SUMMA := SUMMA+MATRIX(RIVI,SARA); end loop; end loop; return SUMMA; end MATSUMMA; Tällöin funktiolle voidaan antaa mikä tahansa kaksiulotteinen liukulukutaulukko parametrina määrittelemällä se MATRIISI -tyyppiseksi muuttujaksi.
3.4 Aliohjelmien välittäminen parametrina Joskus ohjelmoinnissa kätevin ratkaisu olisi välittää aliohjelma parametrina toiselle aliohjelmalle, jolloin sitä voidaan kutsua ns. takaisinkutsun (callback) periaatteella. Funktionaalisessa ohjelmoinnissa tällainen aliohjelmien välittäminen on keskeisessä asemassa. Myös joissakin imperatiivisissa kielissä tämä on mahdollista. Idean toteuttamisessa on kuitenkin joitakin hankaluuksia. Tyypillinen ongelma on, tarkistetaanko parametrina saadun aliohjelman kutsuissa esiintyvien parametrien tyypit. Lisäksi parametrina välitetyn aliohjelman viiteympäristön määräytyminen on mielenkiintoinen ongelma. (Aliohjelman viiteympäristö tarkoittaa aliohjelmassa näkyvien tunnisteiden joukkoa.) Olio-ohjelmoinnissa tarve aliohjelman välittämiselle poistuu ainakin osittain, koska takaisinkutsu voidaan toteuttaa välittämällä olio, jonka metodia kutsutaan. Tyypillisesti tähän käytetään apuna rajapintoja. Esimerkiksi Javassa ei ole mahdollista välittää metodia aliohjelmaparametrina, vaan on käytettävä oliosuuntautuneita tekniikoita takaisinkutsun toteuttamiseen. Joissakin kielissä aliohjelmaparametrien käyttäminen vaihtelee toteutusten välillä. Esimerkiksi alkuperäisessä Pascalissa aliohjelmaparametreja voitiin käyttää, mutta myöhemmissä versioissa ominaisuus ei ole yleisesti käytössä. Adassa aliohjelmien välittäminen parametreina on kielletty. FORTRAN 77:ssä voidaan aliohjelman nimi välittää parametrina aliohjelmalle; tyypin tarkistuksia ei tehdä. C ja C++ -kielissä voidaan myös käyttää funktion nimeä parametrina; tällöin välitetään osoitin funktioon, koska C-kielessä (ja C++:ssa) funktion nimi toimii aina funktion osoittimena ([Ker], kappale 5.11). Koska funktion parametrit kuuluvat funktion tyyppiin, voidaan parametrien tyyppiyhteensopivuus varmistaa jo käännösaikana. Monet C-kääntäjät sallivat kuitenkin epäyhteensopivat kutsut kuitaten ne esimerkiksi varoituksella. Yleensä C++ -kääntäjät ovat tarkempia.
C-kielessä aliohjelma välitettäisiin esimerkiksi seuraavasti: void ali_yx(int x) printf("x=%d \n",x); } void cb_fun(void ali(int)) ali(-200); } Pääohjelmassa kutsuttaisiin:... cb_fun(ali_yx);... Parametrina saadun aliohjelman viiteympäristön määräytymiseen voidaan periaatteessa käyttää kolmea tapaa: 1. Pinnallinen sidonta (matala sidonta, shallow binding), 2. Syvä sidonta (deep binding), 3. Tilanteen mukainen sidonta (ad hoc binding). Pinnallisessa sidonnassa viiteympäristö on kutsuvan aliohjelman ympäristö. Tätä menetelmää suositaan joissakin dynaamiseen sidontaan perustuvissa kielissä. Staattista sidontaa käyttävät kielet suosivat yleensä syvää sidontaa, jolloin parametrina saadun funktion viiteympäristö on sama kuin välitettävän aliohjelman viiteympäristö muutenkin. Näin ollen staattinen sidonta on ylivoimaisesti yleisin malli. Tilanteen mukainen sidonta, vaikka onkin periaatteessa mahdollinen, ei tiettävästi ole käytössä missään kielessä: siinä viiteympäristön määrää aliohjelmaparametrin saavaa aliohjelmaa kutsuva aliohjelma (ks. [Seb], kappale 9.6). 3.5 Aliohjelman ylikuormittaminen ja geneeriset aliohjelmat Aliohjelman ylikuormittaminen (overloading) tarkoittaa sitä, että samannimisellä aliohjelmalla on useita määrittelyjä, jotka eroavat toisistaan parametrilistoiltaan ja -
joissakin tapauksissa - paluuarvoiltaan. Ohjelmassa pitää voida aliohjelman kutsun muodosta päätellä, mitä versiota aliohjelmasta käytetään. Yleisesti paluuarvon tyypillä ei voida erottaa aliohjelmia toisistaan. Näin ollen useimmat kielet eivät salli samannimisiä aliohjelmia, joilla on samat parametrilistat mutta eri paluuarvot. Ylikuormittaminen ei ole mahdollista FORTRANissa, mutta uudemmat kielet, kuten Pascal (ainakin jotkin versiot), Ada, Java, C ja C++ sallivat aliohjelmien ylikuormittamisen. Geneerisellä ohjelmoinnilla tarkoitetaan ohjelmointimenetelmää, joka sallii parametrisoitujen tyyppien käytön konkreettisten tyyppien asemasta. Erityisesti toteutettaessa tietorakenteita geneerinen ohjelmointi osoittaa voimansa: esimerkiksi toteutettaessa lineaarinen lista, sen käsittelyalgoritmit ovat samat riippumatta siitä, minkälaisia tietoalkioita lista sisältää. Erityisesti geneeriset aliohjelmat sallivat tyyppiriippumattomien aliohjelmien toteuttamisen, koska ne voivat ottaa erityyppisiä parametreja eri aktivointikerroilla. Joissakin yhteyksissä geneerisiä aliohjelmia kutsutaan myös polymorfisiksi aliohjelmiksi. Yleisimmässä muodossaan geneeriset aliohjelmat voidaan toteuttaa dynaamista sidontaa käyttävissä kielissä, koska niissä aliohjelmaparametrien tyyppiä ei tarvitse välttämättä etukäteen määritellä. Tavallisempi muoto on kuitenkin parametrisoitu polymorfismi, jossa käytetään parametrisoituja tyyppejä. Nämä sidotaan käännösaikaisesti johonkin konkreettiseen tyyppiin. Geneeristen aliohjelmien (ja yleensä geneerisen ohjelmoinnin) soveltaminen lisää koodin uudelleenkäytettävyyttä. Yleisimpiä geneeristä ohjelmointia tukevia kieliä ovat olleet Ada ja C++, joista molemmissa on toteutettu käännösaikainen parametrisoitu polymorfismi. C++ -kieli on rakennettu tukemaan voimakkaasti geneeristä ohjelmointia. Itse asiassa jokaiseen C++ -toteutukseen sisältyy standardimallikirjasto (Standard Template Library, STL), joka sisältää geneerisesti toteutettuja tietorakenteita ja algoritmeja. C++ - kielessä geneerisyys ilmaistaan avainsanalla template, esimerkiksi seuraava funktio lajittelee mielivaltaisia alkiota sisältävän taulukon (kunhan taulukkoalkioita voidaan vertailla operaattorilla >):
template <typename T> void lajittele(t lista[], int pituus) int i, j; T temp; for (i=0; i < pituus-1; i++) for (j=i+1; j < pituus; j++) if (lista[i] > lista[j]) temp=lista[i]; lista[i]= lista[j]; lista[j]=temp; } } Tätä funktiota voitaisiin käyttää esimerkiksi seuraavasti: double dbllist[] = -1.2, 3.4, -5.4, 9.9, 2.5}; int intlist[] = -1, 2, 3, -5, 10, 25, 11}; lajittele<double>(dbllist,5); lajittele<int>(intlist,7); Ada-kielen toteutuksen tavoin myös C++ -kielessä kääntäjä muodostaa koodin, kun mallia sovelletaan ohjelmassa konkreettiseen tyyppiin. Tarkemmin aihetta käsitellään lähteessä [Strou], luku 13. Javaan on lisätty geneerisen ohjelmoinnin tuki versioon 1.5 (ks. tarkemmin: http://docs.oracle.com/javase/tutorial/java/generics/index.html). 3.6 Aliohjelmien toteuttamisesta Lopuksi käsitellään hieman aliohjelmien toteutukseen liittyviä seikkoja eri kielissä. Ohjelmarakenne vaikuttaa olennaisella tavalla aliohjelmien toteutukseen ja siihen, miten aliohjelma voi kommunikoida muun ohjelman kanssa. Paitsi välittämällä aliohjelmalle parametreja, voidaan kommunikoida myös ei-paikallisen informaation välityksellä. Ohjelmarakenne määrää informaation näkyvyyden aliohjelmassa. Ohjelmointikielet voidaan jakaa ohjelmarakenteensa perusteella neljään ryhmään:
1. Avoimet kielet (ei rakennetta), 2. Riippumattomat kielet (sisäkkäisiä ohjelman osia ei sallita), 3. Alisteiset kielet (sisäkkäiset osat mahdollisia), 4. Rajoitetusti alisteiset kielet. Avoimia kieliä ovat BASIC ja COBOL, joissa ei ohjelmalla ole erityistä järjestettyä rakennetta. FORTRAN on esimerkki riippumattomasta kielestä; FORTRAN -ohjelmassa on pääohjelma ja joukko riippumattomia aliohjelmia, jotka eivät voi olla sisäkkäisiä. FORTRANissa globaalit muuttujat esitellään COMMON -lauseella. Itse asiassa COMMON määrittelee FORTRANissa yhteisen muistialueen, jota kukin aliohjelma voi käyttää haluamallaan tavalla. Useimmiten COMMON -lohkoja käytetään kuitenkin yhteisten, globaalien muuttujien toteuttamiseen. ALGOL -tyyppisissä kielissä ohjelma on yhtenäinen kokonaisuus, jonka osia sen aliohjelmat ovat. Lisäksi aliohjelmat voivat sisältää aliohjelmia jne. Tällaisia alisteisia kieliä ovat esimerkiksi Pascal ja Ada. C -kieli (samoin C++) on riippumattomien ja alisteisten kielten välimuoto: rajoitetusti alisteinen kieli. Tällaisessa kielessä aliohjelmia ei voi määritellä alisteisesti, ts. aliohjelmamäärittely ei voi sisältää toista aliohjelmamäärittelyä. Kuitenkin aliohjelmarakenteen yläpuolella on globaali alue, johon voidaan määritellä globaaleja muuttujia. Suurten ohjelmistojen rakentamisen perusedellytyksiä on pystyä kääntämään ohjelmiston osia kääntämättä koko ohjelmaa uudelleen. Siten tällaiseen ohjelmistokehitykseen suunniteltujen kielten tulee antaa mahdollisuus osittaiseen kääntämiseen. Yleisesti tämä voidaan tehdä kahdella tavalla: erillisellä kääntämisellä (separate compilation) ja riippumattomalla kääntämisellä (independent compilation). Erikseen käännettäviä ohjelman osia sanotaan käännösyksiköiksi (compilation units). Tavallisimmat riippumattoman kääntämisen sallivat kielet ovat C ja FORTRAN 77; riippumattomassa kääntämisessä eri käännösyksikköjen välisiä riippuvuuksia ei tarkisteta; näin ollen käännösyksiköt voidaan kääntää täysin toisistaan piittaamatta. Erityisesti FORTRAN 77:ssä jopa samassa tiedostossa sijaitsevat aliohjelmat käännetään toisistaan riippumatta. Alisteisissa kielissä riippumaton kääntäminen on
ohjelmarakenteen takia mahdotonta. Näihin kieliin on yleensä kuitenkin toteutettu erillinen kääntäminen, mikä tarkoittaa sitä, että käännösyksiköt voidaan kääntää erikseen, mutta käännöstulos riippuu kuitenkin muista käännösyksiköistä. Alkuperäinen Pascal (kuten FORTRANin ensimmäiset versiot) ei sallinut erillistä kääntämistä, vaan käännösyksikkönä oli koko ohjelma. Myöhemmissä versioissa tämä puute on korjattu. Aliohjelman kutsun ja paluun muodostamaa operaatiota kutsutaan yleensä aliohjelman linkitykseksi (subprogram linkage). Linkityksessä on toteutettava tiedonsiirto aliohjelman ja kutsuvan ohjelman välillä kaikilla niillä tavoilla, jotka kielessä ovat mahdollisia. Edelleen kutsuvan ohjelman ajonaikainen tila on talletettava, jotta kontrolli voidaan palauttaa aliohjelmasta poistuttaessa. Koska aliohjelmissa tarvitaan paikallisia muuttujia, näille on varattava muistitilaa. Päämenetelmät muistin varaamiseen ovat staattinen ja dynaaminen allokointi. Staattisessa menetelmässä kaikkien muuttujien vaatima muistitila on tiedettävä ennakolta ja se varataan jo käännösaikana. Staattinen menetelmä on suorituksen kannalta tehokas, mutta myös resursseja kuluttava, koska eri aliohjelmien paikallisia muuttujia ei voi sijoittaa samoihin muistipaikkoihin, vaikka aliohjelmia ei suoritettaisi ikinä yhtä aikaa. FORTRANissa käytettiin staattista menetelmää, mikä oli mahdollista koska dynaamisia tietoalkioita eikä rekursiivisia aliohjelmia voitu käyttää. Muistin allokointi tapahtuu kokonaisuudessaan, kun ohjelma ladataan muistiin. Yleensä nykyään käytetään dynaamista allokointia, jossa vaadittava tila varataan dynaamisesti aliohjelmapinosta. Alue, johon aliohjelmaa kutsuttaessa tietoalkiot luodaan, on nimeltään aliohjelman aktivaatiotietue (activation record). Aliohjelmaa kutsuttaessa siis aktivaatiotietue laitetaan pinoon ja poistetaan jälleen aliohjelmasta palattaessa. Tämä mahdollistaa dynaamisemman muistinkäytön suoritusajan lievän kasvamisen kustannuksella, eroa ei voida kuitenkaan pitää merkittävänä nykykoneilla. Myös FORTRAN käyttää aktivaatiotietuetta, mutta se on luonteeltaan staattinen, pinomuistia ei käytetä.
Kuva. Aktivaatiotietueen tyypillinen rakenne. Aktivaatiotietue sisältää muutakin tietoa kuin aliohjelman paikallisia muuttujia ja tilan funktion paluuarvolle, kuten ylläoleva kuva osoittaa. Luonnollisesti tarvitaan paluuosoite kutsuvaan ohjelmaan kontrollin siirtämiseksi, lisäksi on tiedettävä kutsuvan ohjelman aktivaatiotietueen sijainti, ts. pinon vanha huippuosoite eli dynaaminen linkki. Näiden tietojen avulla pino voidaan palauttaa aliohjelmakutsua edeltävään tilaan sekä jatkaa ohjelman suoritusta, kun aliohjelman suoritus päättyy. Myös rekursio voidaan toteuttaa käyttämällä dynaamisesti varattavia aktivaatiotietueita. C-kielen kaltaisissa kielissä aktivaatiotietueen toteutus on varsin suoraviivainen, koska aliohjelmat eivät voi limittyä. Tällöin aliohjelmien aktivaatiotietueiden ei tarvitse tietää mitään muiden aliohjelmien tietueista. Alla olevassa kuvassa on esimerkki C-ohjelman kutsupinosta, kun pääohjelmasta kutsutaan funktiota F, joka puolestaan kutsuu funktiota G.
Kuva. Esimerkki C-ohjelman kutsupinosta. Sen sijaan alisteisten kielten tapauksessa aktivaatiotietueen on sisällettävä informaatiota, jonka avulla aliohjelmassa voidaan käyttää muiden aliohjelmien aktivaatiotietueita. Tätä tarvitaan ei-paikallisten viittausten arvojen hakemiseksi. C- kielessä tämä ei ole ongelma, sillä ei-paikalliset viittaukset kohdistuvat aina globaaliin alueeseen, joka on sama kaikille aliohjelmille. Jos aliohjelma on kuitenkin määritelty toisen aliohjelman sisällä, voi ei-paikallinen muuttuja olla esitelty pääohjelmassa tai ylemmässä aliohjelmassa. Jälkimmäisessä tapauksessa muuttujan arvo saadaan käyttämällä ko. aliohjelman aktivaatiotietuetta. Tällaisessa tapauksessa helpoin tapa toteuttaa tarvittavat sidonnat, on tallettaa aktivaatiotietueeseen ns. näkyvyyslinkki (staattinen linkki, static link, access link), joka osoittaa pinossa viimeiseen aliohjelman sisältävän aliohjelman aktivaatiotietueeseen. Linkkiä seuraamalla löydetään oikea tunniste. Näkyvyyslinkkimenetelmä ei kuitenkaan ole kovin tehokas, mikäli ohjelman rakenne on monimutkainen. Tehokkaampaa on käyttää ns. näkyvyystauluja (display tables), mikä onkin ainoa yleisesti käytetty vaihtoehtoinen menetelmä. Tässä menetelmässä näkyvyyslinkit kootaan yhteen tauluun eikä niitä säilytetä aktivaatiotietueissa. Näkyvyystaulun ylläpito on hieman työläämpää kuin näkyvyyslinkkien, nimittäin jokaisen aliohjelmakutsun ja aliohjelmasta paluun yhteydessä näkyvyystilanne muuttuu, joten myös näkyvyystaulua on muutettava kummassakin tapauksessa. Sebestan teoksessa ([Seb], luku 10) käsitellään varsin tarkasti näkyvyyslinkkien ja näkyvyystaulujen käyttöä.
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, Richie. 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. [Seb] Sebesta, Robert W. Concepts of Programming Languages 10th edition, Pearson 2013. [Strou] Stroustrup, Bjarne. The C++ Programming Language, 3rd edition, Murray Hill 1997.