Podprogram

zaporedje ukazov, ki se jih lahko kliče z drugih mest v računalniškem programu

Podprogram[1][2][3][4] [pòtprográm] je v računalništvu zaporedje programskih ukazov, ki izvaja določeno nalogo, zbrano kot enota. Ta enota se lahko potem rabi v programih kjerkoli se mora ta določena naloga izvesti.

Podprogrami so lahko določeni znotraj programov ali ločeno v programskih knjižnicah in jih lahko uporabijo drugi programi. V različnih programskih jezikih se podprogrami lahko imenujejo pòdrutȋna,[1][5] rutȋna, fúnkcija, metóda ali procedúra. Tehnično imajo ti izrazi različne definicije in izrazje se od jezika do jezika razlikuje. Včasih se rabi tudi še splošnejši, krovni izraz klicljìva enôta (callable unit).[6]

Ime podprogram nakazuje, da se takšna enota obnaša na skoraj enak način kot računalniški program, ki se rabi kot en korak v večjem programu ali drugem podprogramu. Podprogram se velikokrat sprogramira tako, da se lahko med izvajanjem programa požene večkrat in z več mest, vključno iz drugih podprogramov, in po klicu, ko je naloga podprograma zaključena, preide nazaj (se vrne) k naslednjemu ukazu. Podporogram si je začetno zamislil John Mauchly med svojim delom na računalniku ENIAC[7] in ga predstavil na harvardskem simpoziju »Preparation of Problems for EDVAC-type Machines« januarja 1947.[8] Za formalno iznajdbo tega koncepta veljajo Maurice Vincent Wilkes, David John Wheeler in Stanley Gill. Imenovali so ga zaprti podprogram,[9][10] v nasprotju z odprtim podprogramom ali makrojem.[11] Tudi Turing je že obravnaval podprograme v članku leta 1945 o konstrukciji in predlogih za stroj ACE britanskega Narodnega fizikalnega laboratorija in je šel tako daleč, da je izumil koncept sklada povratnega naslova (return address stack).[12]

Podprogrami so močno programsko orodje[13] in skladnja mnogih programskih jezikov vsebuje podporo za njihovo pisanje in uporabo. Razumna raba podprogramov, na primer prek pristopa strukturalnega programiranja, bo velikokrat zelo omejila ceno razvoja in vzdrževanja velikega programa, ter povečala njegovo kakovost in zanesljivost.[14] Podprogrami, velikokrat zbrani v knjižnicah, so pomemben mehanizem za deljenje in prodajo programske opreme. Disciplina objektno usmerjenega programiranja temelji na objektih in metodah, ki so podprogrami, pripojeni tem objektom ali razredom objektov.

Pri prevajanju se metoda imenuje povezana koda (threaded code), izvršni program pa je v bistvu zaporedje podprogramskih klicev.

Glavni koncepti uredi

Vsebina podprograma je njegovo telo, ki je del programske kode, in ta se pri klicu podprograma izvede.

Podprogram se lahko napiše tako, da od klicočega programa pričakuje eno ali več podatkovnih vrednosti – da nadomesti njegove parametre (postavke) ali formalne parametre. Klicoči program zagotovi dejanske vrednosti teh parametrov, ki se imenujejo argumenti. Različni pogramski jeziki lahko rabijo različne dogovore za prenosne argumente:

dogovor opis splošna raba
klic po vrednosti Argument se ovrednoti in kopija vrednosti se prenese na podprogram Privzeto v večini jezikov podobnih ALGOLu po različici ALGOL 60, kot so paskal, Delphi, Simula, CPL, PL/M, Modula, Oberon, Ada in mnogi drugi. C, C++, java (sklici na objekte in polja se tudi prenašajo po vrednosti)
klic po sklicevanju Sklicevanje na argument, tipično je prenesen njegov naslov Izbirno v večini jezikov podobnih ALGOLu pa različici ALGOL 60, kot so ALGOL 68, paskal, Delphi, Simula, CPL, PL/M, Modula, Oberon, Ada in mnogi drugi. C++, Fortran, PL/I
klic po rezultatu Vrednost parametra se kopira nazaj na argument pri vračanju iz podprograma Ada – parametri OUT
klic po vrednosti-rezultatu Vrednost parametra se kopira nazaj na vhod podprograma in spet pri vračanju ALGOL, Swift – parametri in-out
klic po imenu Kot makro – nadomesti parametre z neovrednotenim izrazom argumenta, nato ovrednoti argument v kontekstu klicočega vsakokrat, ko ga klicani podprogram uporabi. ALGOL, Scala
klic po konstantni vrednosti Podobno kot klic po vrednosti, razen, da se parameter obravnava kot konstanta PL/I – parametri NONASSIGNABLE, Ada – parametri IN

Podprogramski klic ima lahko tudi stranskje učinke, ko je na primer spreminjanje podatkovnih struktur v računalniškem pomnilniku, branje ali pisanje na zunanjo napravo, tvorjenje datoteke, zaustavljanje programa ali stroja ali celo zakasnitev izvrševanja programa za določen čas. Podprogram s stranskimi učinku lahko vrne različne rezultate vsakič, ko je klican, tudi, če je klican z enakimi argumenti. Zgled je podprogram za naključna števila, ki je na voljo v mnogih jezikih, in, ki pri vsakem klicu vrne različno psevdonaključno število. Široka raba podprogramov s stranskimi učinki je značilna za imperativne jezike.

Podprogramu so lahko sprogramirani tako, da lahko za izvedbo naloge rekurzivno kličejo sami sebe na enem ali več mestih. Ta metoda omogoča neposredno implementacijo funkcij, ki so definirane z matematično indukcijo in rekurzivnimi algoritmi deli in vladaj.

Podprogram, katerega namen je izračunati funkcijo z Booleovo vrednostjo, to je odgovor na vprašanje z da ali ne, se včasih imenuje predikat. V jezikih logičnega programiranja se vsi podprogrami imenujejo predikati, ker prvenstveno določajo uspeh ali neuspeh.

Podprogram, ki ne vrača nobene vrednosti ali vrača ničelno vrednost, se včasih imenuje procedura. Procedure po navadi spreminjajo svoje argumente in so bistveni del proceduralnega programiranja.

Funkcije uredi

Funkcija je podprogram, ki vrne vrednost.[15] Glavni namen funkcij je razbiti zapletene izračune v smiselne kose in jih poimenovati.[16] Podprogram lahko vrne izračunano vrednost klicočemu (njegovo povratno vrednost), ali pa da različne vrednosti rezultatov ali izhodne parametre. Splošna raba podprogramov je res implementacija funkcij v katerih je namen podprograma zgolj izračun enega ali več rezultatov, katerih vrednosti so v celoti določene z argumenti, podanimi podprogramu. Taka zgleda sta na primer izračun logaritma števila ali determinanta matrike. V nekaterih jezikih je skladnja za podprogram, ki vrača vrednost, dejansko enaka skladnji za podprogram, ki ne vrača vrednosti, razen za odsotnost na primer stavka RETURNS. V nekaterih jezikih lahko podprogram dinamično izbere ali vrne vrednost ali je ne vrne, kar je odvisno od njegovih argumentov.

Podpora jezikov uredi

Visokonivojski programski jeziki po navadi vsebujejo določene konstrukte za:

  • razmejitev dela programa (telesa), ki tvori podprogram
  • dodelitev identifikatorja (imena) podprogramu
  • označitev imen in podatkovnih tipov njegovih parametrov in povratnih vrednosti
  • zagotovitev zasebnega imenskega dosega njegovih začasnih spremenljivk
  • istovetnost spremenljivk zunaj podprograma, ki so v njem dosegljive
  • klicanje podprograma
  • zagotovitev vrednosti njegovih parametrov
  • glavni program, ki naj vsebuje njegov naslov
  • podprogram, ki naj vsebuje naslov naslednjega ukaza za klic funkcije v glavnem programu
  • označitev povratnih vrednosti iz notranjosti njegovega telesa
  • vračanje h klicočemu programu
  • razmestitev vrednosti, ki jih vrne klic
  • upravljanje z izjemami, ki se srečajo med klicem
  • pakiranje podprogramov v modul, knjižnico, objekt ali razred

Nekateri programski jeziki, kot so paskal, Fortran, Ada in mnoga narečja BASICa, razlikujejo med funkcijami ali funkcijskimi podprogrami, ki zagotavljajo eksplicitno povratno vrednost klicočemu programu, in podprogrami ali procedurami, ki te vrednosti ne zagotavljajo. V teh jezikih so klici funkcij normalno vgrajeni v izraze – na primer funkcija sqrt se lahko kliče kot y = z + sqrt(x). Klici podprogramov se lahko obnašajo skladenjsko kot stavki – prodprogram print se lahko kliče kot if x > 0 then print(x) ali pa jih eksplicitno kličejo stavki, kot na primer CALL ali GOSUB – na primer call print(x)). Drugi jeziki, kot sta C in lisp, med funkcijami in podprogrami ne razlikujejo.

V jeziki strogega funkcionalnega programiranja, kot je Haskell, so lahko podprogrami brez stranskih učinkov, kar pomeni, da se raznolična notranja stanja programa ne bodo spremenila. Funkcije bodo vedno vrnile enak rezultat, če se kličejo večkrat z enakimi argumenti. Takšni jeziki tipično podpirajo le funkcije, ker so podprogrami, ki ne vračajo vrednosti, neuporabni vse dokler ne povzročijo stranskega učinka.

V programskih jezikih, kot so C, C++ in C#, se podprogrami lahko imenujejo preprosto funkcije, kar pa se ne sme zamenjevati s pojmoma funkcije v matematiki ali funkcionalnim programiranjem, ki sta različna koncepta.

Prevajalnik jezika bo po navadi prevedel klice podprogramov in povratne vrednosti v strojne ukaze glede na dobro definiran dogovor o klicanju, tako da se lahko podprogrami prevedejo ločeno od programov, ki jih kličejo. Zaporedja ukazov, ki odgovarja klicnim in povratnim stavkom, se imenuje uvod in sklepna beseda podprograma.

V zelo zgodnjih zbirnikih je bila podpora podprogramov omejena. Podprogrami eksplicitno niso bili ločeni drug od drugega ali od glavnega programa, in res se je lahko izvorna koda pomešala s tisto iz drugih programov. Nekateri zbirniki so ponujali preddefinirane makroje za tvorjenje zaporedij klicev in vračanj. Do 1960-ih so imeli zbirniki po navadi razvitejšo podporo tako za vrinjene kot za ločeno zbrane podprograme, ki se jih je dalo povezati skupaj.

Eden od prvih programskih jezikov, ki je podpiral uporabniško napisane podprograme, je bil FORTRAN II. Prevajalnik za IBM FORTRAN II je izšel leta 1958. Tudi ALGOL 58 in drugi zgodnji programski jeziki so podpirali proceduralno programiranje.

Prednosti uredi

Prednosti razbitja programa na podprograme so:

  • dekompozicija zapletenih programskih nalog v preprostejše korake – to je eden od dveh glavnih orodij strukturalnega programiranja poleg podatkovnih tipov
  • zmanjšanje podvojitev kode znotraj programa
  • usposobitev ponovne rabe kode prek večkratnih programov
  • ločitev velike programske naloge med različne programerje ali različne stopnje projekta
  • skrivanje podrobnosti izvedb od uporabnikov podprograma
  • izboljšanje berljivosti kode z zamenjevanjem bloka kode s klicem funkcije, kjer opisno ime funkcije služi za opis bloka kode.[17] Zaradi tega je klicna koda zgoščena in berljiva, četudi funkcija ni namenjena ponovni rabi.
  • izboljšanje sledljivosti – večina jezikov ponuja načine za dosego sledi klica, kar vsebuje imena vključenih podprogramov in mogoče še več informacij, kot so imena dototek in številke vrstic. Če se koda ne bi razstavila v podprograme, bi bilo razhroščevanje zelo oteženo.

Slabosti uredi

V primerjavi z rabo vrinjene kode klicanje podprograma v klicnem mehanizmu vsiljuje nekaj računalniške režije.

Podprogram tipično zahteva standardno vzdrževalno kodo – tako pri vhodu in pri izhodu iz funkcije (uvod in sklepna beseda funkcije), kar po navadi registre za splošni namen in povratni naslov ohranja v najmanjšem obsegu.

Zgodovina uredi

Zamisel podprograma je bila uresničena po tem, ko so računski stroji nekaj časa že obstajali. Ukazi aritmetičnih in pogojnih skokov so bili načrtovani v naprej in so se relativno malo spremenili, posebni ukazi za podprogramske klice pa so se skozi leta zelo spremenili. Najzgodnejši računalniki in mikroprocesorji, kot sta na primer Manchester Baby in RCA 1802, niso imeli enega ukaza za podprogramski klic. Podprogrami so se lahko izvedli, vendar so zahtevali programerje, ki so rabili klicno zaporedje – niz ukazov pri vsakem klicnem mestu.

Podprogrami so bili izvedeni v računalniku Z4 Konrada Zuseja leta 1945.

Leta 1945 je Alan Turing rabil izraz »skriti« (bury) in »ne skriti« (unbury) v smislu klicanja in vračanja iz podprogramov.[18][19]

Januarja 1947 je John William Mauchly predstavil splošne opombe na Simpoziju o digitalnih računskih strojih velikega obsega pod skupnim pokroviteljstvom Univerze Harvard in Urada za oborožitev VM ZDA. Tu je razpravljal o zaporednih in vzporednih operacijah ter predlagal:

...ni treba, da je zgradba stroja sploh zapletena. Ker so na voljo vse logične značilnosti, nujne za ta podprogram, je možno razviti kodni ukaz za umestitev podprogramov v pomnilnik na mesta, znana stroju, in na tak način, da se lahko preprosto kličejo v uporabo.
Z drugimi besedami, podprogram A za deljenje, podprogram B za kompleksno množenje in podprogram C za ovrednotenje standardne napake se lahko označijo kot zaporedje števil, in tako naprej skozi seznam podprogramov, potrebnih za določeni problem. ... Vsi ti podprogrami bodo potem shranjeni v stroju, in vse kar je treba narediti, je izdelati kratek sklic nanje s številom, kakor so naznačeni v kodiranju.[8]

Kay McNulty je tesno sodelovala z Johnom Mauchlyjem v skupini razvoja računalnika ENIAC in razvila zamisel za podprograme zanj, ko ga je programirala med 2. svetovno vojno.[20] Ona in drugi programerji računalnika ENIAC so uporabljali podprograme za pomoč pri izračunavanju trajektorij izstrelkov.[20]

Goldstine in von Neumann sta napisala članek, datiran 16. avgusta 1948, kjer sta obravnavala rabo podprogramov.[21]

Nekateri zelo zgodnji računalniki in mikroprocesorji, kot so IBM 1620, Intel 4004 in Intel 8008, ter mikrokrmilniki PIC so imeli enoukazni podprogramski klic, ki je uporabljal dodeljeni strojni sklad za hranjenje povratnih naslovov – takšna strojna oprema podpira le nekaj nivojev gnezdenja podprogramov, lahko pa podpira rekurzivne podprograme. Stroji pred sredino 1960-ih, kot so bili UNIVAC I, PDP-1 in IBM 1130, so tipično rabili dogovor o klicanju, ki je shranil števec ukazov v prvo pomnilniško lokacijo klicanega podprograma. To je omogočalo poljubno globoke nivoje gnezdenja podprogramov, ni pa podpiralo rekurzivnih podprogramov. PDP-11 (1970) je bil eden prvih računalnikov z ukazom podprogramskega klica prek nakopičevalnega skladovnega pomnilnika – ta gradnik podpira tako poljubno globoko gnezdenje podprogramov kot tudi rekurzivne podprograme.[22]

Podprogramske knjižnice uredi

Tudi s tako nerodnim pristopom so se podprogrami izkazali za zelo uporabne. Kot prvo so omogočili rabo iste kode v mnogih različnih programih. Poleg tega je bil v zgodnjih računalnikih pomnilnik zelo redek vir, tako da so programi omogočili prihranke velikosti programov.

Mnogi zgodnji računalniki so vnašali programske ukaze v pomnilnik z luknjanega traku. Vsak podprogram se je potem lahko pridobil z ločenim kosom traku, naloženim ali povezanim pred ali za glavnim programom (ali »glavno vrstico« (mainline)),[23] isti trak s podprogramom pa se je lahko uporabil pri mnogih različnih programih. Podobni pristop se je uporabil v računalnikih, ki so za svoj glavni vnos rabili luknjane kartice. Ime podprogramska knjižnica je izvirno pomenila knjižnico, v dobesednem smislu, saj so v njej hranili indeksirano zbirko trakov ali paketov kartic za skupno rabo.

Vračanje s posrednim skokom uredi

Da so odstranili potrebo po samospreminjajoči se kodi, so oblikovalci računalnikov zagotovili ukaz za posredni skok, kjer je bil operand, namesto, da je bil povratni naslov sam, lokacija spremenljivke ali procesorski register, ki je vseboval povratni naslov.

Na teh računalnikih je klicoči program, namesto spreminjanja povratnega skoka podprograma, hranil povratni naslov v spremenljivki, tako da, ko se je podprogram končal, je izvršil posredni skok, ki je usmerila izvršitev na lokacijo, dano s preddefinirano spremenljivko.

Skok do podprograma uredi

Druga prednost je bil ukaz za skok do podprograma, ki je kombiniral hranjenje povratnega naslova s klicočim skokom in tako zelo minimiziral režijo.

V družini osrednjih računalnikov IBM System/360 bi ukaza za prehod BAL ali BALR, oblikovana za klicanje podprogramov, na primer shranila povratni naslov v procesorski register, naveden v ukazu, z dogovorjenim registrom 14. Za vračanje je moral podprogram le izvršiti ukaz za posredni prehod (BR) prek tega registra. Če je podprogram ta register potreboval za kakšen drug namen, kot na primer za klicanje drugega programa, je shranil vsebino registra v zasebno lokacijo pomnilnika ali v registrski sklad.

V sistemih, kot je na primer vrsta 16-bitnih miniračunalnikov HP 2100, bi ukaz JSB izvedel podobno nalogo s tem, da je bil povratni naslov shranjen v pomnilniški lokaciji, ki je bila tarča prehoda. Izvršitev podprograma se je dejansko začela na naslednji pomnilniški lokaciji. V zbirnem jeziku HP 2100 bi se na primer zapisalo:

       ...
       JSB MYSUB    (klic podprograma MYSUB.)
 BB    ...          (vračanje sem, ko je MYSUB končan.)

za klic podprograma z imenom MYSUB iz glavnega programa. Koda za podprogram bi bila:

 MYSUB NOP          (hranjenje povratnega naslova za MYSUB.)
 AA    ...          (začetek telesa MYSUB.)
       ...
       JMP MYSUB,I  (vračanje na klicoči program.)

Ukaz JSB je postavil naslov ukaza NEXT (namreč BB) v lokacijo, navedeno kot njegov operand (namreč MYSUB), in potem prešel na lokacijo NEXT (namreč AA = MYSUB + 1). Podprogram se je lahko potem vrnil v glavni pogram s izvršitvijo posrednega skoka JMP MYSUB, I, ki je prehajal na lokacijo, shranjeno na lokaciji MYSUB.

Prevajalniki za Fortran in druge jezike so lahko preprosto uporabili te ukaze, ko so bili na voljo. Ta pristop je podpiral večkratne nivoje klicev – ker pa so imeli povratni naslov, parametri in povratne vrednosti podprograma vseeno dodeljene stalne pomnilniške lokacije, to ni omogočalo rekurzivnih klicev.

Sicer je bila podobna metoda rabljena v programu za preglednice Lotus 1-2-3 v zgodnjih 1980-ih za odkrivanje odvisnosti ponovnih izračunavanj v preglednici. Lokacija je bila rezervirana v vsaki celici za hranjenje povratnega naslova. Ker krožna sklicevanja za naravni vrstni red ponovnega izračunavanja niso dovoljena, je to omogočalo sprehod po drevesu brez rezerviranja prostora za sklad v pomnilniku, ki je bil zelo omejen na majhnih računalnikih, kot je mikroračunalnik IBM PC.

Klicni sklad uredi

Večina modernihi zvedb podprogramskih klicev rabi klicni sklad, posebno vrsto podatkovne strukture sklada, za izvajanje podprogramskih klicev in vračanj. Vsak podprogramski klic tvori nov vpis, imenovan okvir sklada (stack frame), na vrhu sklada. Ko se podprogram vrača, se njegov okvir sklada s sklada pobriše, njegov prostor pa se lahko uporabi za drugi podprogramske klice. Vsak okvir sklada vsebuje zasebne podatke odgovarjajočega klica, kar tipično vključujed parametre in notranje spremenljivke podprograma, ter povratni naslov.

Klicno zaporedje se lahko izvede z zaporedjem navadnih ukazov – ta pristop se še vedno rabi v arhitekturah RISC in VLIW, vendar ima mnogo tradicionalnih strojev, oblikovanih od poznih 1960-ih, za ta namen vključene posebne ukaze.

Klicni sklad se po navadi izvede kot soseedno območje pomnilnika. Ali bo dno sklada najnižji ali najvišji naslov znotraj tega območja, je poljubna izbira zasnove, tako da lahko sklad raste naprej ali nazaj v pomnilniku. Mnogo arhitektur rabi drugo možnost.

Nekatere zasnove, še posebej izvedbe jezika Forth, rabijo dva ločena sklada, enega v glavnem za nadzorne informacije, kot so povratni naslovi in števci zank, drugega pa za podatke. Prvi je bil ali je delal kot klicni sklad in je bil le posredno dostopen programerju prek drugih jezikovnih konstruktov, drugi pa je bil bolj neposredno dostopen.

Ko so prvič uvedli podprogramske klice na podlagi sklada, je bila pomembna vzpodbuda prihranek dragocenega pomnilnika. S takšno shemo prevajalniku za zasebne podatke (parametre, povratne naslove in krajevne spremenljivke) vsakega podprograma ni bilo treba rezervirati ločenega prostora v pomnilniku. V vsakem trenutku sklad vsebuje le zasebne podatke klicev, ki so trenutno dejavni – tisti, ki so bili klicani, pa se še niso vrnili. Zaradi načinov po katerih so bili programi zbrani iz knjižnic, je bilo (in še vedno je) običajno najti programe, ki so vključevali tisoče podprogramov, od katerih je samo nekaj dejavnih ob poljubnem danem trenutku. Za takšne programe je lahko mehanizem klicnega sklada prihranil pomembno količino pomnilnika. Na mehanizem klicnega sklada se res lahko gleda kot na najzgodnejšo in najpreprostejšo metodo samodejnega upravljanja s pomnilnikom.

Druga prednost metode klicnega sklada je ta, da omogoča rekurzivne podprogramske klice, ker vsak od vgnezdenih klicev istega poprograma dobi ločen primerek svojih zasebnih podatkov.

Zakasnjeno skladanje uredi

Ena od slabosti mehanizma klicnega sklada je povečana cena podprogramskega klica in njemu ustreznega vračanja. Dodatni strošek vključuje povečevanje in zmanjševanje kazalca sklada in, v nekaterih arhitekturah, preverjanje prekoračitve sklada, in dostopanje do krajevnih spremenljivk in parametrov z naslovi, relativnimi okvirjem, namesto absolutnih naslovov. Strošek se lahko uresniči v povečanem času izvajanja, povečani kompleksnosti procesorja ali oboje.

Ta režija je najočitnejša in neprijetna v listnih podprogramih ali listnih funkcijah, ki se vračajo brez, da bi sami tvorili kakšne podpogramske klice.[24][25][26] Za zmanjšanje te režije mnogi moderni prevajalniki poskušajo zakasniti rabo klicnega sklada vse dokler res ni potreben. Klic podprograma P lahko na primer shrani njegov povratni naslov in parametre v določenih procesorskih registrih in s preprostim skokom prenese nadzor telesu podprograma. Če se podprogram P vrne brez, da bi klical druge podprograme, klicni sklad sploh ni rabljen. Če mora P klicati drug podprogram Q, bo potem rabil klicni sklad za hranjenje vsebine kateregakoli registra (na primer povratnega naslova), ki bo potreben potem, ko se Q vrne.

Zgledi uredi

C in C++ uredi

V programskih jezikih C in C++ se podprogrami imenujejo funkcije, (še naprej razvrščeni kot funkcije članov, ko so povezani z razredom, ali proste funkcije,[27] kadar niso). Ta dva jezika rabita posebno ključno besedo void za naznačitev funkcije, ki ne vrača nobene vrednosti. Upoštevati je treba, da imajo lahko funkcije C/C++ stranske učinke, vključno s spreminjanjem poljubnih spremenljivk, katerih naslovi se prenašajo kot parametri. Zgledi so:

void funkcija1() { /* poljubna koda */ }

ali

void funkcija1(void) { /* poljubna koda */ }

Funkcija ne vrača vrednosti in se mora klicati kot samostojna funkcija, na primer funkcija1();. Funkcija je tudi brez argumentov, kar je pri njeni dodelitvi (definiciji) naznačeno kot funkcija1() ali eksplicitno kot funkcija1(void). Pri klicanju funkcije ključne besede void za argumente ni treba rabiti in se kliče brez, na primer kar funkcija1();.

int funkcija2() {
   return 5;
}

Ta funkcija vrača rezultat (celo število 5) in klic je lahko del izraza, na primer x + funkcija2().

char funkcija3(int stevilo) {
   char izbira[] = {'N', 'P', 'T', 'S', 'Č', 'P', 'S'};
   return izbira[stevilo];
}

Ta funkcija spremeni število med 0 in 6 v začetno črko odgovarjajočega dneva v tednu – 0 v 'N', 1 v 'P', ..., 6 v 'S'. Rezultat njenega klica se lahko dodeli spremenljivki, na primer st_dneva = funkcija3(stevilo);.

void funkcija4(int *kazalec_na_spremenljivko) {
   (*kazalec_na_spremenljivko)++;
}

Ta funkcija ne vrača vrednosti, vendar spreminja spremenljivko, katere naslov se prenaša kot parameter – kliče se kot funkcija4(&spremenljivka_za_vecanje);.

MS Small Basic uredi

zgled()                               ' klic podprograma

Sub zgled                             ' začetek podprograma
    TextWindow.WriteLine("To je zgled podprograma v jeziku MS Small Basic.")  ' vsebina podpograma
EndSub                                ' konec podprograma

V zgornjem zgledu zgled() kliče podprogram.[28] V jeziku MS Small Basic se mora za opredelitev dejanskega podprograma uporabiti ključna beseda Sub, čemur sledi ime podprograma Sub zgled. Po vsebini podprograma mora slediti ukaz za njegov konec EndSub.

Visual Basic 6 uredi

V jeziku Visual Basic 6 se podprogrami imenujejo funkcije ali sub-i (ali metode, ko so povezane z razredom). Visual Basic 6 rabi različne izraze, imenovane types, za opredelitev tega kar je preneseno kot parameter. Privzeto je neoznačena spremenljivka registrirana kot spremenljivi tip in se lahko prenaša kot ByRef (privzeto) ali kot ByVal. Kadar je funkcija ali sub označena (deklarirana), dobi označba javno (public), zasebno (private) ali prijateljsko (friend), kar določa ali se lahko dostopa zunaj modula ali projekta, kjer je bila označena.

  • po vrednosti [ByVal] – način prenašanja vrednosti argumenta k podprogramu s prenašanjem kopije vrednosti, namesto prenašanja naslova. Zaradi tega dejanske vrednosti spremenljivke podprogram, ki mu je bila vrednost prenesena, ne more spremeniti.
  • po sklicevanju [ByRef] – način prenašanja vrednosti argumenta k podprogramu s prenašanjem naslova spremenljivke, namesto prenašanja kopije njene vrednosti. To omogoča, da lahko podprogram dostopa do dejanske spremenljivke. Zaradi tega lahko podprogram, ki mu je bila vrednost prenesena, spremeni dejansko vrednost spremenljivke. Če ni drugače navedeno, se argumenti prenašajo po sklicevanju.
  • Public (izbirno) – nakazuje, da je funkcijski podprogram dostopen vsem drugim podprogramom v vseh modulih. Če se rabi v modulu, ki vsebuje izbiro Private, podprogram ni na voljo zunaj projekta.
  • Private (izbirno) – nakazuje, da je funkcijski podprogram dostopen le drugim podprogramom v modulu, kjer je označen.
  • Friend (izbirno) – se rabi le v razrednem modulu. Nakazuje, da je funkcijski podprogram viden skozi celotni projekt in neviden nadzorniku primerka objekta.
Private Function funkcija1()
    ' poljubna koda
End Function

Funkcija ne vrača vrednosti in se mora klicati kot samostojna funkcija, na primer funkcija1.

Private Function funkcija2() as Integer
    funkcija2 = 5
End Function

Ta funkcija vrača rezultat (celo število 5) in klic je lahko del izraza, na primer x + funkcija2().

Private Function funkcija3(ByVal intValue as Integer) as String
    Dim polje_znakov(6) as String
    polje_znako = Array("P", "T", "S", "Č", "P", "S", "N")
    funkcija3 = polje_znakov(intValue)
End Function

Ta funkcija pretvori število med 0 in 6 v začetno črko odgovarjajočega dneva v tednu – 0 v 'P', 1 v 'T', ..., 6 v N 'S'. Rezultat njenega klica se lahko dodeli spremenljivki, na primer st_dneva = funkcija3(stevilo).

Private Function funkcija4(ByRef intValue as Integer)
    intValue = intValue + 1
End Function

Ta funkcija ne vrača vrednosti, vendar spreminja spremenljivko, katere naslov se prenaša kot parameter – kliče se kot "funkcija4(spremenljivka_za_vecanje)".

PL/I uredi

V jeziku PL/I klicani podprogram lahko prenaša opisnik (deskriptor), ki zagotalja informacije o argumentu, kot so na primer dolžine znakovnih nizov (stringov) in meje polj. To podprogramu omogoča širšo splošnost in izločanje potrebe programerju, da takšne informacije prenaša. Privzeto PL/I prenaša argumente po sklicevanju. (Trivialni) podprogram za spreminjanje predznaka vsakega elementa dvorazsežnega polja bi izgledal kot:

  spremeni_znak: procedure(polje);
    declare polje (*,*) float;
    polje = -polje;
    end spremeni_znak;

To se lahko kliče z različnimi polji, kot sledi:

  /* prvo polje ima velikost od -5 do +10 in od 3 do 9 */
  declare polje1 (-5:10, 3:9) float;
  /* drugo polje ima velikost od 1 do 16 in od 1 do 16 */
  declare polje2 (16,16) float;
  call spremeni_znak(polje1);
  call spremeni_znak(polje2);

Python uredi

V jeziku Python se za opredelitev funkcije rabi ključna beseda def. Stavek, ki tvori telo funkcije, se mora nadaljevati ali v isti vrstici ali pa se začeti v drugi vrstici in biti zamaknjen.[29] Naslednji zgled programa izpiše »Pozdravljen, svet!«, čemur sledi »Wikipedija« v naslednji vrstici:

def preprosta_funkcija():
   print('Pozdravljen, svet!')
   print('Wikipedija')
preprosta_funkcija()

Krajevne spremenljivke, rekurzija in ponovno vstopanje uredi

Podprogram je lahko uporaben za rabo določene količine pomožnega prostora, pomnilnika, ki se rabi med njegovim izvrševanjem tega podprograma za držanje vmesnih rezultatov. Spremenljivke, shranjene v tem pomožnem prostoru, se imenujejo krajevne spremenljivke, pomožni prostor pa se imenuje aktivacijski zapis. Aktivacijski zapis ima tipični povratni naslov, ki mu pove kam nazaj mora vrniti nadzor, ko se podprogram konča.

Podprogram ima lahko poljubni število in naravo klicnih mest. Če je podprta rekurzija, lahko podprogram celo kliče samega sebe, kar povzroči, da se njegova izvršitev začasno odloži medtem ko se pojavi druga vgnezdena izvršitev istega podprograma. Rekurzija je uporabno sredstvo za poenostavitev nekaterij zapletenih algoritmov in zlom zapletenih problemov. Rekurzivni jeziki v splošnem pri vsakem klicu zagotavljajo novo kopijo krajevnih spremenljivk. Če programer želi, da vrednosti krajevnih spremenljivk med klici ostajajo enake, se v nekaterih jezikih lahko označijo kot statične, lahko pa se rabijo globalne vrednosti ali skupna območja. Tu je zgled rekurzivnega podprograma v C/C++ za iskanje Fibonaccijevih števil:

int fib(int n) {
   if (n <= 1) {
      return n;
   }
   return fib(n - 1) + fib(n - 2);
}

Zgodnji jeziki, kot je Fortran, v začetku niso podpirali rekurzije, ker so bile spremenljivke dodeljene statično, kot tudi lokacija za povratni naslov. Večina računalnikov pred poznimi 1960-imi, ko je na primer PDP-8, niso imeli podpore za strojne skladovne registre.

Sodobni jeziki po ALGOLu, kot sta PL/I in C skoraj vedno rabijo sklad, po navadi podprt z najsodobnejšimi računalniškimi nabori ukazov za zagotovitev svežih aktivacijskih zapisov za vsako izvršitev podprograma. Na ta način je vgnezdena izvršitev prosta za spreminjanje svojih krajevnih spremenljivk brez skrbi za vpliv na druge odložene izvršitve v poteku. Ker se vgnezdeni klici kopičijo, se tvori struktura klicnega sklada, ki se sestoji iz enega aktivacijskega zapisa za vsak odložen podprogram. Dejansko je ta skladovna struktura navidezno vseprisotna, aktivacijski zapisi pa se zato v splošnem imenujejo okvirji skladov.

Nekateri jeziki, kot so paskal, PL/I in Ada, tudi podpirajo vgnezdene podpograme, ki so podprogrami klicljivi le znotraj obsega zunanjega (starševskega) podprograma. Notranji podprogrami imajo dostop do krajevnih spremenljivk zunanjega podprograma, ki jih je klical. To se doseže s hranitvijo dodatnih pomenskih informacij znotraj aktivacijskega zapisa, kar se imenuje zaslon (display).

če se podprogram lahko pravilno izvrši tudi kadar je že v teku druga izvršitev istega podprograma, se tak podprogram imenuje ponovno izvedljiv (reentrant). Rekurzivni podprogrami morajo biti ponovno izvedljivi. Ponovno izvedljivi podprogrami so uporabni tudi v razmerah mnogonitnosti, ker lahko mnogokratne niti kličejo isti podprogram brez bojazni pred medsebojnim motenjem. V sistemu obdelave transakcij IBM CICS je bila delno ponovno izvedljivost rahlo manj omejevalna, vendar podobna zahteva za aplikacijske programe, ki jih je delilo več niti.

V mnogonitnem okolju v splošnem obstaja več kot en sklad. Okolje, ki v celoti podpira soprograme ali zakasnjeno izračunavanje, lahko za hranjenje svojih aktivacijskih zapisov namesto skladov rabi druge podatkovne strukture.

Preobremenjevanje uredi

V močno tipiziranih jezikih je včasih zaželeno, da obstaja več funkcij z enakim imenom, vendar delujočih na različnih podatkovnih tipih ali z različnimi profili parametrov. Funkcija za kvadratni koren je lahko na primer opredeljena tako, da deluje nad realnimi števili, kompleksnimi vrednostmi ali matrikami. Algoritem je v vsakem primeru drugačen in tudi povratni rezultat je lahko različen. Z zapisom treh ločenih funkcij z enakim imenom si programerju ni treba zapomniti različnih imen za vsak podatkovni tip. Če se naprej lahko opredeli podtip za realna števila, da se ločijo pozitivna od negativnih realna števila, se za realna števila lahko napišeta dve funkciji , ena, ki vrača realno število, kadar je paramater pozitiven, in druga, ki vrača kompleksno vrednost, kadar je negativen.

V objektno usmerjenem programiranju kadar lahko niz funkcij z enakim imenom sprejme različne profile parametrov ali parametre različnih tipov, je vsaka od njih preobremenjena.

Sledi zgled preobremenjevanja podprograma v C++:

#include <iostream>

double povrsina(double h, double w) { return h * w; }

double povrsina(double r) { return r * r * 3.14; }

int main() {
   double povrsina_pravokotnika = povrsina(3, 4);
   double povrsina_kroga = povrsina(5);

   std::cout << "Površina pravokotnika je " << povrsina_pravokotnika << std::endl;
   std::cout << "Površina kroga je " << povrsina_kroga << std::endl;
}

V tej kodi sta dve funkciji z enakim imenom povrsina(), vendar imata različne parametre. C preobremnjevanja funkcij neposredno ne podpira.

V drugem zgledu lahko podprogram zgradi objekt, ki bo sprejel smeri, in sledil svoji poti tem točkam na zaslonu. Obstaja preobilo paramatrov, ki se lahko prenesejo konstruktorju (barva sledi, začetni koordinati x in y, hitrost sledi). Če bi programer želel, da lahko konstruktor sprejme le en parameter barve, potem lahko pokliče drugi konstruktor, ki sprejme le barvo, kar izmenoma kliče konstruktor z vsemi parametri, prenesenimi v množici privzeti vrednosti za vse druge parametre (X in Y bosta v splošnem usredinjena na zaslonu ali pa postavljena v izhodišču, hitrost pa bo nastavljena na drugo vrednost glede na programerjevo izbiro).

PL/I ima atribut GENERIC za označitev rodovnega imena za množico vnosnih sklicevanj, klicanih z različnimi tipi argumentov. Na primer:

  DECLARE gen_name GENERIC(
                      name  WHEN(FIXED BINARY),
                      flame  WHEN(FLOAT),
                      pathname OTHERWISE 
                           );

Pri vsakem vnosu se lahko navedejo mnogovrstne opredelitve argumentov. Klic na »gen_name« bo klical podprogram »name«, kadar je argument tipa FIXED BINARY, »flame« kadar je tipa FLOAT in tako naprej. Če se argument ne ujema z nobeno izbiro, bo klican podprogram »pathname«.

Zaprtja uredi

Glavni članek: zaprtje (računalništvo).

Zaprtje (closure) je podprogram skupaj z vrednostmi nekaterih njegovih spremenljivk, zaobjetimi z okoljem v katerem je nastal. Zaprtja so bila pomembni gradnik programskega jezika lisp, ki ga je v poznih 1950-ih uvedel John McCarthy. Odvisno od izvedbe lahko zaprtja služijo kot mehanizem za stranske učinke.

Dogovori uredi

Za kodiranje podprogramov so razvili veliko število dogovorov. Glede na njihovo ime je mnogo razvijalcev prisvojilo pristop v katerem mora biti ime podprograma glagol, kadar naredi določeno nalogo, pridevnik, kadar naredi kakšno poizvedovanje, in samostalnik, kadar se rabi kot zamenjava za spremenljivke.

Nekateri programerji predlagajo, da naj podprogram izvede le eno nalogo, in, če podpogram izvaja več kot eno nalogo, naj se deli v druge podprograme. Sklepajo, da so podprogrami ključne komponente v vzdrževanju kode, njihova vloga v programu pa mora ostajati jasna.

Zagovorniki modularnega programiranja (modularizacije kode) se zavzemajo, da mora biti vsak podprogram najmanj odvisen od dugih delov kode. Raba globalnih spremenljivk za zagovornike tega vidika v splošnem velja za nespametno, ker dodaja tesno sklopitev med podprogramom in temi globalnimi spremenljivkami. Če takšna sklopitev ni potrebna, je njihov predlog preoblikovanje podprogramov, da lahko namesto njih sprejemajo prenesene parametre. Vendar lahko povečevanje števila parametrov, prenesenih v podprograme, vpliva na berljivost kode.

Povratne kode uredi

Poleg njegovega glavnega ali normalnega učinka mora podprogram klicočemu programu sporočiti o izjemnih pogojih, ki so se med njegovim izvajanjem mogoče pojavili. V nekaterih jezikih in programskih standardih se to velikokrat naredi prek povratne kode, celoštevilske vrednosti, ki jo podprogram postavi na kakšno standardno lokacijo in, ki zakodira normalne ali izjemne pogoje.

V družini IBM System/360, kjer je bila od podprograma zahtevana povratna koda, je bila povratna vrednost pogosto oblikovana kot mnogokratnik števila 4, tako da se je lahko uporabila kot neposredni indeks razvejitvene razpredelnice v razvejitveno razpredelnico, ki se pogosto nahaja takoj za ukazom za klicanje zaradi izogibanja dodatnim pogojnim testom, in s tem izboljšanjem učinkovitosti. V zbirnem jeziku družine IBM System/360 se na primer lahko zapiše:

           BAL  14, SUBRTN01   skok na podprogram, shranitev povratnega naslova v R14
           B    TABLE(15)      uporaba povratne vrednosti v registru 15 na indeks razvejitvene razpredelnice, 
*                              vejitev na odgovarjajoči vejitveni ukaz.
TABLE      B    OK             povratna koda =00   V REDU                }
           B    BAD            povratna koda =04   nepravilni vnos       } razvejitvena razpredelnica
           B    ERROR          povratna koda =08   nepredvideni pogoj    }

Optimizacija podprogramskih klicev uredi

Pri klicanju podprogramov obstaja velika režija izvajalnega časa, vključno s prenašanjem argumentov, vejitvijo do podprograma in vejitvijo nazaj na klicočega. Režija pogosto vključuje hranjenje in obnavljanje določenih procesorskih registrov, dodeljevanje in ponovno vračanje pomnilnika klicnega okvirja. V nekaterih jezikih vsak programski klic vsebuje tudi samodejno testiranje povratne kode podprograma ali upravljanje z izjemami, ki se lahko pojavijo. Pomembni vir režije v objektno usmerjenih jezikih je intenzivna raba dinamičnega dodeljevanja za klice metod.

Za podprogramske klice obstaja nekaj navidezno očitnih optimizacij, ki se ne morejo uporabiti, če imajo podprogrami stranske učinke. V izrazu (f(x)-1)/(f(x)+1) se mora na primer funkcija f(x) klicati dvakrat, ker dva klica lahko vrneta različna rezultata. Poleg tega mora biti vrednost x pred drugim klicem dostavljena ponovno, saj jo je lahko prvi klic spremenil. Določanje ali ima lahko podprogram stranske učinke je zelo težko – po Riceovem izreku celo neodločljivo. Čeprav so takšne optimizacije varne v čisto funkcionalnih programskih jezikih, prevajalniki tipičnega imperativnega programiranja morajo po navadi dopuščati najslabše.

Vrivanje uredi

Metoda za izločitev te režije je vrinjena razširitev (inline expansion) ali vrivanje (inlining) telesa podprograma k vsakemu klicnemu mestu, namesto vejitve k podprogramu in nazaj. Tako ne samo, da se izogne klicni režiji, ampak omogoča prevajalniku učinkovitejšo optimizacijo telesa podprograma z upoštevanjem konteksta in argumentov klica. Vstavljeno telo lahko prevajalnik optimira. Vrivanje pa bo po navadi povečalo velikost kode, razen če program ne vsebuje le en podprogramski klic.

Trop v priljubljenih medijih uredi

Izraz »podprogram« se je od 1990-ih na televiziji in v filmih rabil neštetokrat. Včasih lahko poljubni element zgodbe, ki vsebuje računalniško programiranje ali vdiranje, postavljen v sedanjost in včasih v daljno prihodnost, obudi ta koncept – velikokrat pa ni pravilno rabljen.

Glej tudi uredi

Sklici uredi

  1. 1,0 1,1 »subroutine«. Računalniški slovarček – Odsek za inteligentne sisteme, Institut "Jožef Stefan". 2022. Pridobljeno 18. februarja 2022.
  2. »podprogram«. Fran, Inštitut za slovenski jezik Frana Ramovša, ZRC SAZU. 2021. Pridobljeno 18. februarja 2022.
  3. »podprogram«. Wikislovar (v angleščini). Pridobljeno 23. februarja 2022.
  4. »podprogram«. Islovar, Slovensko društvo Informatika. 2020. Pridobljeno 25. februarja 2022..
  5. »subroutine«. Wikislovar (v angleščini). Pridobljeno 23. februarja 2022.
  6. U.S. Election Assistance Commission (2007). »Definitions of Words with Special Meanings«. Voluntary Voting System Guidelines. Arhivirano iz prvotnega spletišča dne 8. decembra 2012. Pridobljeno 14. januarja 2013.
  7. Dasgupta (2014), str. 155–.
  8. 8,0 8,1 Mauchly (1947).
  9. Wheeler (1952), str. 235.
  10. Wilkes; Wheeler; Gill (1951), str. 45, 80–91, 100.
  11. Dainith (2004).
  12. Turing (1945).
  13. Knuth (1997).
  14. Dahl; Dijkstra; Hoare (1972).
  15. Wilson; Clark (2001), str. 140.
  16. Stroustrup (2013), str. 307.
  17. Martin (2009), str. 39.
  18. Turing (1946).
  19. Carpenter; Doran (1977).
  20. 20,0 20,1 Isaacson (2014).
  21. Goldstine; von Neumann (1947), str. 163.
  22. Steele (1977).
  23. Frank (1983), str. 195.
  24. »ARM Information Center« (v angleščini). Infocenter.arm.com. Pridobljeno 29. septembra 2013.
  25. »x64 stack usage«. Microsoft Docs (v angleščini). Microsoft. 3. avgust 2021. Pridobljeno 5. avgusta 2019.
  26. »Function Types« (v angleščini). Msdn.microsoft.com. 16. november 2012. Pridobljeno 29. septembra 2013.
  27. »what is meant by a free function« (v angleščini). stackoverflow. 1. februar 2011. Pridobljeno 23. februarja 2022.
  28. »Microsoft Small Basic«. www.smallbasic.com (v angleščini). Pridobljeno 23. februarja 2022.
  29. »4. More Control Flow Tools — Python 3.9.7 documentation« (v angleščini). Pridobljeno 23. februarja 2023.

Viri uredi