Aliohjelma (kutsutaan eri yhteyksissä myös termeillä proseduuri, funktio, metodi tai rutiini) on ohjelmoinnissa itsenäinen ohjelman osa, joka suorittaa tietyn toiminnon ja jota voidaan kutsua eri puolilta pääohjelmaa tai muista aliohjelmista. Aliohjelman suorituksen jälkeen ohjelman suoritus jatkuu kutsuvassa ohjelmassa aliohjelmakutsua seuraavasta lauseesta.
Aliohjelmien tarkoitus on yksinkertaistaa ohjelmointia: useimmat tietokonetta vaativat tehtävät ovat liian monimutkaisia kerralla ratkaistavaksi ja aliohjelmien käytöllä ongelmat voidaan pilkkoa pienempiin yksittäin ratkaistaviin palasiin.[1]
Proseduraalisessa ohjelmointikielessä aliohjelmia kutsutaan funktioiksi tai proseduureiksi. Olio-ohjelmoinnissa olion funktioita nimitetään jäsenfunktioiksi tai metodeiksi. Assemblyssä aliohjelmia kutsutaan alirutiineiksi. Joissakin yhteyksissä aliohjelmasta käytetään nimitystä kutsuttava yksikkö (engl. callable unit) usein eri nimitysten sijaan.[2]
Ohjelmoinnissa esiintyy usein tilanne, että samanlaista toimintoa tarvitaan ohjelman useassa eri kohdassa. Kirjoittamalla toiminto aliohjelmaksi voidaan se suorittaa useasta eri kohdasta. Näin säästetään työtä. Jos sama toiminto kirjoitettaisiin ohjelmaan yhä uudelleen, ohjelmakoodi pitenisi ja tulisi epäselväksi ja virheiden mahdollisuus kasvaisi. Mahdolliset korjaukset ohjelmakoodiin pitäisi tehdä useaan paikkaan. Aliohjelmat myös lisäävät ohjelman modulaarisuutta, koska ne mahdollistavat ohjelmakoodin jakamisen pienempiin itsenäisiin osiin ja parantavat siten ohjelmiston luettavuutta, testattavuutta ja ylläpidettävyyttä.
Aliohjelmaa voidaan kutsua useassa kohdassa ohjelmaa. Aliohjelman määrittelyn yhteydessä esitellään joukko aliohjelman parametreja tai argumentteja. Aliohjelmalla voi olla parametrien lisäksi paikallisia muuttujia, jotka näkyvät ja ovat käytettävissä vain aliohjelman alueella (engl. scope). Aliohjelman parametrit voivat olla muuttujia tai viittauksia muuttujiin.
Aliohjelman kutsuun on eräissä suorittimissa tuki käskytasolla: konekielinen käsky tallentaa senhetkisen ohjelmalaskurin arvon pinoon ja hyppää toiseen kohtaan ohjelmaa. Kun aliohjelma päättyy, paluuosoite haetaan pinon päältä. Kaikki ohjelmointikielet eivät tue pinoa: FORTRAN 77 ei tukenut pinoa vaan funktioilla oli oma muistialueensa argumenteille ja datalle.[3]
Järjestelmäkutsu ei ole suora funktiokutsu kuten sovelluksissa, vaan kutsun on ylitettävä ytimen ja käyttäjäavaruuden välinen jako.[4]
Tilaa jonne osoite tallennetaan kutsutaan aliohjelman aktivaatiotietueeksi. Siihen tallennetaan lisäksi aliohjelman käyttämät paikalliset muuttujat. Jos aliohjelma kutsuu itseään useaan kertaan, eli toimii rekursiivisesti, aktivaatiotietueita on useampi pinon päällä.
Varhainen kuvaus alirutiinikirjastolle on esitetty vuonna 1948 John von Neumannin ja Herman H. Goldstinen teoksessa Planning and Coding of Problems for an Electronic Computing Instrument.[5] Maurice V. Wilkes David Wheelerin ja Stanley Gillin kanssa esitteli käytännössä toimivan ratkaisun vuonna 1951 teoksessa Preparation of Programs for Electronic Digital Computers.[6]
Vuonna 1977 keskusteltiin proseduurikutsujen haitoista verrattuna goto
-käskyjen käyttöön: PDP-11 oli yksi ensimmäisiä tietokoneita, joissa oli käskytuki pinoon työntämiseen alirutiinikutsussa. Eräiden testitulosten mukaan tuon ajan hitaus aliohjelmakutsuissa johtui PL/I-kielen kääntäjän toteutuksesta, joka johti virheellisiin käytäntöihin.[7]
Suoritin käyttää hyppykäskyä (jmp
), kutsukäskyä (call
, jsr
) tai tarkoitukseen varattua haarautumiskäskyä (esim. bl
, bsr
) siirtyäkseen suoritettavaan ohjelmakohtaan.[8][9][10][11] Tätä varten suorittimelle on kerrottava suoritettavan aliohjelman osoite.[12][11]
Kun suoritin siirtyy aliohjelmaan, paluuosoite sijoitetaan pinoon jotta suoritin palaa oikeaan kohtaan aliohjelman lopussa. Pinoon voidaan lisätä useampia paluuosoitteita sisäkkäisiä kutsuja varten.[9] Paluuosoite riippuu paikasta, jossa aliohjelmaa kutsutaan, esimerkiksi jos sitä kutsutaan kahdesti peräkkäin eri parametreilla.[13] Seuraavan suoritettavan käskyn osoite voidaan sijoittaa pinoon ennen aliohjelman kutsua, jolloin se palautetaan aliohjelmasta paluun jälkeen ja suoritus jatkuu seuraavasta käskystä.[12]
Suorittimen arkkitehtuurista riippuu käskyjen toteutustapa ja käskyt voivat yhdistää suorituspaikan työntämisen pinoon sekä ehdottoman hyppykäskyn suorittamisen.[10][11] Kohde annetaan kääntäjälle aliohjelman nimenä, josta kääntäjä päättelee varsinaisen osoitteen: osoite voidaan laskea myös suorituksen aikana, jolloin voidaan käyttää uudelleensijoitettavia aliohjelmia.[11]
Aliohjelmakutsun yhteydessä suorittimen rekisterien tilat on talletettava pinoon jotta aliohjelma ei ylikirjoita säilytettävää tietoa sen suorituksen aikana. Suorittimessa on rajattu määrä rekistereitä, joita kaikki aliohjelmat voivat käyttää.[13] Vastuu rekisterin tilan säilömisessä voi olla aliohjelman kutsujalla tai kutsuttavalla aliohjelmalla, joissa on molemmissa tapauksissa etunsa ja haittansa.[13][14] Kutsukäytäntö määrittää tavan aliohjelmien kutsumiseen.[14] Suoritin voi sisältää erillisen käskyn rekisterien säilömistä varten ("push" ja "pop") tai ne voivat jäädä ohjelmoijan vastuulle: esimerkiksi MIPS-arkkitehtuurissa ei ole käskyjä tätä varten.[13] SPARC käyttää suurta määrää rekistereitä liukuvan ikkunan periaatteella, jolloin aliohjelma ei näe kaikkia rekistereitä.[15]
Suorittimella ei ole tietoa aliohjelmalle välitettävien muuttujien tyypeistä vaan se on ohjelmallisesti käsiteltävää tietoa joko käännösvaiheessa tai ajonaikaisella tyyppitarkistuksella.[13]
Esimerkki aliohjelmasta, joka saa parametriksi numeroarvon (int eli integer) nimeltä ika ja joka palauttaa boolean-arvon (true/false) sen mukaan, onko ika alle 18 vai ei.
public Boolean onkoHenkiloAlaikainen(int ika) {
if(ika<18) {
return true;
} else {
return false;
}
}
Tämän jälkeen aliohjelmaa voitaisiin kutsua esimerkiksi seuraavasti:
if(onkoHenkiloAlaikainen(12) == true) {
tulosta "Henkilö on alaikäinen!";
}
Olio-ohjelmoinnissa eräissä kieli aliohjelman (metodin) eteen kirjoitetaan halutun olion nimi seuraavasti:
if(henkilo.onkoHenkiloAlaikainen(12) == true) {
tulosta "Henkilö on alaikäinen!";
}
Assembly-tasolla aliohjelman kutsu:
main: bsr foo ... paluun jälkeen tehtävät toiminnot foo: ... aliohjelmassa tehtävät asiat rts
... jossa bsr
(branch-to-subroutine) siirtää suorituksen aliohjelmaan ja rts
(return-from-subroutine) palaa aliohjelmasta kutsun tehneeseen paikkaan. Esimerkissä ei ole mukana pinon tai parametrien käsittelyä.[16] Vastaavasti bl
(branch-and-link) käytetään ARM-suorittimella aliohjelman kutsuun, mutta paluu tapahtuu asettamalla paluuosoite ohjelmalaskuriin (PC
).[17]
Kahden numeron yhteenlasku aliohjelmassa ARM-suorittimella:[18]
AREA subrout, CODE, READONLY ; koodilohkon nimi
ENTRY ; ensimmäisenä suoritettavan käskyn merkki
start MOV r0, #10 ; parametrien asetus rekistereihin
MOV r1, #3
BL doadd ; kutsutaan alirutiinia "doadd"
stop MOV r0, #0x18 ; angel_SWIreason_ReportException
LDR r1, =0x20026 ; ADP_Stopped_ApplicationExit
SVC #0x123456 ; ARM semihosting (formerly SWI)
doadd ADD r0, r0, r1 ; alirutiinin koodi (laske yhteen rekisterien r1 ja r0 arvot, sijoita lopputulos rekisteriin r0)
BX lr ; paluu alirutiinista (katso BL)
END ; tiedoston loppu
Eräät ohjelmointikielet kuten C-kieli erottavat esittelyn (engl. declaration) ja määrittelyn tai toteutuksen (engl. definition). Esittely kertoo miten funktiota kutsutaan ilman tarvetta tietää toteutusta toisessa osassa ohjelmaa.[19][20][21][22]
void swap(int *a, int *b); /* esittely */
void swap(int *a, int *b) /* määrittely */
{
int t = *a;
*a = *b;
*b = t;
}
Aliohjelma voi käyttää paikallisia muuttujia, jotka ovat rekisterissä tai pinossa vain aliohjelman suorituksen aikana.
int laske(int a, int b)
{
int d; /* <- d voidaan sijoittaa rekisteriin tai pinoon */
d = a + b;
return d;
}
Aliohjelmat voivat myös nähdä globaaleja muuttujia ja muuttaa niitä, tai olio-ohjelmoinnissa metodit voivat käsitellä luokan jäsenmuuttujia. Näiden muuttujien saatavuus ja elinaika on eri kuin aliohjelman paikallisen muuttujan.
int d; /* globaali, säilytettävä ohjelman suorituksen ajan,
* näkyvissä kaikkialle ohjelmassa */
void laske(int a, int b)
{
d = a + b;
}
Jäsenmuuttujan elinkaari on sama kuin olion ja sen näkyvyys voi olla rajattu, jolloin sitä voi käyttää vain jäsenmetodien avulla.
class Laske
{
private:
int d; /* jäsenmuuttuja */
public:
void laske(int a, int b)
{
d = a + b;
}
int arvo() { return d; }
Laske() { d = 0; } /* konstruktori, joka vain alustaa jäsenmuuttujan arvon */
};
Yleinen tapa palauttaa aliohjelmasta tietoa on sen paluuarvon avulla. Esimerkiksi aliohjelma voi palauttaa int -tyyppisen kokonaisluvun ja sen arvona luvun 42 seuraavasti:
int anna_luku()
{
return 42;
}
Rust-kielessä funktion viimeinen lause on oletuksena myös funktion paluuarvo:[23]
fn five() -> i32 {
5
}
Eräissä kielissä voidaan antaa oletusarvot argumenteille, jolloin kutsuessa ei välttämättä tarvitse antaa arvoa jokaiselle argumentille. Esimerkki, joka antaa kahden muuttujan tulon:
int kertolasku(int a, int b = 1) { return a * b; }
void main()
{
int tulo;
tulo = kertolasku(2); /* 2 * 1 = 2*/
tulo = kertolasku(2, 1); /* 2 * 1 = 2 */
tulo = kertolasku(2, 2); /* 2 * 2 = 4 */
}
Parametreja voidaan välittää kielestä riippuen kolmella tavalla aliohjelmalle:
Osoittimen ja viitteen tapauksessa aliohjelma voi palauttaa tietoa annettuihin muuttujiin tai olioihin. Esimerkiksi funktio, joka laskee kaksi arvoa yhteen, palauttaa summan sekä merkitsee muuttujan todeksi mikäli jompikumpi parametri ylittää maksimin.
void summa(int a, int *b, bool &maksimi)
{
maksimi = false;
if (a > INT_MAX || *b > INT_MAX)
{
maksimi = true;
}
*b = a + *b;
}
/* kutsuminen */
int main()
{
bool maks = false;
int a = 1;
int b = 2;
/* ensimmäiselle parametrille välitetään arvo, toiselle muuttujan osoite ja kolmannelle viite */
summa(a, &b, maks);
}
Arvona välitetyn argumentin (by value) muuttaminen aliohjelmassa ei muuta kutsujan käyttämää arvoa, mutta viitteellä välitettyä kutsujan käyttämää arvoa voidaan muuttaa kutsutussa aliohjelmassa. Viitteen välittäminen on tehokkaampaa kuin arvon välittäminen koska tällöin ei tehdä kopiota.[25] Osoittimen välittämien (by pointer) tekee kopion osoittimen arvosta aliohjelmalle. Osoittimen arvon muuttaminen aliohjelmassa muuttaa vain aliohjelman käyttämää osoitinta.[26] Arvon välittämisessä (by value) kääntäjä tekee aliohjelmalle kopion, jonka muuttaminen aliohjelmassa näkyy vain aliohjelmalle eikä vaikuta kutsujan käyttämään arvoon.[27]
Ada-kielessä käytetään in
ja out
määrityksiä parametreille. in out
sisältää kutsujan antaman alkuarvon, jota aliohjelma voi muuttaa.[28]
Esimerkki Adan kutsutavoista:[28]
procedure Proc
(Var1 : Integer;
Var2 : out Integer;
Var3 : in out Integer)
is
begin
Var2 := Func (Var1);
Var3 := Var3 + 1;
end Proc;
C-kieli ja sen johdannaiset tukevat muuttuvaa määrää parametreja ellipsis (...
) määrittelyllä. Tällöin aliohjelmassa itsessään on oltava tuki muuttujaluettelon käsittelyyn. Tyypillinen tapaus on printf()
-funktio, jonka määrittely on muotoa:
int printf(const char *format, ...)
Käyttö:
printf("Hei maailma\n");
printf("Numero: %d\n", 42);
Eräät ohjelmointikielet tukevat ylikuormittamista (engl. overloading) (vaihtoehtoisia kutsutapoja ja toteutuksia).
Ylikuormittaminen on tarkoitettu käyttöön, kun sama operaatio tehdään eri tyyppisille olioille. Saman nimen käyttöä eri tyyppien kanssa kutsutaan ylikuormittamiseksi.[29]
Ylikuormittamisessa samalle aliohjelmalle voi olla rinnakkaisia toteutuksia eri tietotyypeillä kuten neliöjuuren laskentaan:
float sqrt(float val);
double sqrt(double val);
Metodin ylikirjoittaminen (engl. overriding tai engl. overwriting) tarkoittaa kantaluokassa olevan toteutuksen korvaamista toisella samalla nimellä ja samoilla argumenteilla perityssä luokassa.[30][31]
Inline-funktiolla tarkoitetaan funktion, jonka kutsun kääntäjä korvaa funktion sisällöllä. Kääntäjän kyvykkyyttä optimointiin ei ole määritelty, joten eri kääntäjä voi korvata kutsun eri tavoin (esimerkiksi yksinkertaistamalla laskentaa tai korvaamalla se vakioarvolla käännösaikaisella laskennalla). Määrittäminen vakioilmaisuksi (constexpr
) helpottaa käännösaikaista arviointia. inline
-avainsana ei muuta funktion semantiikkaa.[32]
Kääntäjän yleensä sallitaan olla välittämättä pyynnöstä tuottaa inline-koodia funktiokutsun sijaan. Inline-kutsu voi nopeuttaa, mutta se voi myös hidastaa ohjelman suoritusta kun ohjelman koko kasvaa liikaa, josta johtuen yksinkertaista vastausta nopeutukseen ei ole.[33]
Sisäänrakennetut funktiot (engl. built-in tai engl. intrinsic) tarkoittavat funktioita, joille kääntäjä tuottaa inline-koodia käännöksen aikana. Tämä korvaa ajonaikaisen kutsun samannimiseen funktioon dynaamisesti ladattavassa kirjastossa. Tämä kasvattaa tuotetun ohjelmakoodin määrää, mutta nopeuttaa suoritusta kun kutsut jäävät pois.[34] Joissakin tapauksissa kyseessä on laitteiston tukema erikoistunut toiminto ja lopputulos voi olla yksi tai kaksi käskyä, mutta niitä kutsutaan funktioiksi kutsuun käytetyn syntaksin vuoksi.[35]
Tietoturvan kiertävät haittaohjelmat pyrkivät muuttamaan kutsuttavaa aliohjelmaa suorittaakseen oman rutiininsa tai ne voivat muuttaa paluuosoitetta, jolloin aliohjelman paluussa suoritus siirtyy hyökkääjän omaan rutiiniin (engl. return oriented programming).[36] ASLR muuttaa osoitteita, jotta haavoittuvuuksien hyödyntäminen on vaikeampaa.
Funktionkutsumisen nopeuteen vaikuttaa suoritusaikainen yleiskustannus (engl. overhead), joka sisältää argumenttien välittämisen, haarautumisen aliohjelmiin ja palaaminen takaisin kutsujaan. Yleiskustannuksella tarkoitetaan tässä yhteydessä ylimääräistä taakkaa ja suorittamisen aikaista viivettä, joka aiheutuu huonosti optimoitujen funktiokutsujen takia. olio-orientoituneissa kielissä dynaaminen sidonta (engl. dynamic dispatch) on aina ohjelman suoritusta hidastava.[21] Usein yleiskustannus sisältää myös prosessorin rekisterien tallentamisen ja palauttamisen, sekä kutsurungon tallennus- ja vapautustoimintojen varauksen. Ohjelmointikielestä riippuen voi funktiokutsu sisältää myös automaattisen funktion testauksen paluukoodista tai poikkeusten käsittelystä, joka voi vaikuttaa ohjelman yleiskustannuksiin.
Aliohjelmien sivuvaikutuksien määrittäminen voi myös olla erittäin hankalaa Ricen teoreeman (engl. Rice's theorem) mukaan. Esimerkiksi jos funktiolla on riippuvuuksia toisista funktioista, jonka takia täytyy funktiota kutsua uudestaan voi optimoinnista aiheutua ongelmia, kuten ohjelman ajautuminen ikuiseen silmukkaan tai ohjelman rakenteesta voi tulla monimutkainen.