Tietokoneen käytön ja ohjelmoinnin alkeet

Kurssin kotisivulle


4 Ohjelmointikielet

Tietokonetta ei voi komennella miten tahansa, vaan sille annettavat määräykset on kirjoitettava täsmällisellä tavalla aivan tiettyyn muotoon. Käyttöjärjestelmälle annettavat komennot ovat tästä yksinkertainen esimerkki. Monimutkaisen tehtävät suorittava ohjelma voi olla hyvin pitkä; laajat ohjelmistot voivat käsittää satojatuhansia rivejä ohjelmakoodia.

Tietokoneiden ohjelmointiin käytettävillä ohjelmointikielillä on omat kielioppinsa aivan kuten luonnollisillakin kielillä. Kieliopista voidaan erottaa syntaksi ja semantiikka. Syntaksi kuvaa kielen ulkoasua ja määrittelee, millaiset lauseet ovat sallittuja. Syntaksi voidaan esittää esimerkiksi Backus-Naur-formalismilla (luku 5). Kielen semantiikka kertoo sitten, mitä erilaiset lauseet tekevät.

Konekielet ja assemblerit

Ainoa kieli, jota tietokone ymmärtää, on sen oma konekieli. Konekieli on jono 2-kantaisia lukuja eli binäärilukuja, joita tietokoneen prosessori tulkitsee. Prosessorin osoiterekisteri ilmoittaa, mistä osoitteesta löytyy seuraavaksi suoritettava käsky.

Yksi käsky voi olla yhden tai useamman tavun mittainen. Useisiin käskyihin liittyy osoiteosa, joka kertoo esimerkiksi, mhin osoitteeseen siirrytään seuraavaksi tai mistä muistipaikasta haetaan luku johonkin prosessorin rekisteriin.

Ohjelman kirjoittaminen binäärilukuina olisi toivottoman hankalaa. Siksi tarvitaan kehittyneempiä kieliä, joiden esitystapa on luettavampi, mutta jotka sitten joudutaan kääntämään jollakin ohjelmalla konekielisiksi.

Konekieltä eniten muistuttavat symboliset konekielet eli assemblerit. Niissä kullekin käskylle on annettu jokin muutamasta kirjaimesta muodostuva nimi ja muistipaikkoihin voidaan yleensä viitata jollakin nimellä muistipaikan absoluuttisen numeerisen osoitteen sijastta.

Jokainen symbolisen konekielen käsky vastaa yhtä konekielistä käskyä; ainoa ero on, että se on esitetty luettavammassa muodossa. Oletetaanpa, että haluamme laskea summan

S = A + B.

Tätä vastaava symbolisen konekielen ohjelma voisi olla

   LOAD A
   ADD B
   STORE S

Tämä on vielä ymmärrettävissä, muttei kovin havainnollista.

Mutkikkaampi esimerkki voisi olla summa

S = 1 + 2 + 3 + ... + 10

Tämä voitaisiin toteuttaa seuraavantapaisella assembler-ohjelmalla:

   LOAD =0=
   STORE S
   STORE I
 SUM:  LOAD S
   ADD I
   STORE S
   LOAD I
   ADD =1=
   COMPARE =10=
   JLE SUM

Tämä vaatii jo hieman paneutumista, mutta tällä tavoin ensimmäisiä tietokoneita jouduttiin ohjelmoimaan.

Vaikka symbolinen konekieli vastaakin konekieltä hyvin suoraviivaisella tavalla, tietokone ei ymmärrä sitä. Tilanteesta riippuu esimerkiksi, mihin muistipaikkaan osoite S viittaa. Siksi ohjelma on ennen suoritusta käännettävä assembler-kääntäjällä, joka tuottaa ohjelmaa vastaavan konekielisen version.

Käskyjen (kuten LOAD, STORE) muuntaminen binääriluvuiksi on suoraviivainen toimenpide. Mutkikkaampi tehtävä on muuttujien (S, I) talletuspaikkojen ja ohjelmassa esiintyvien osoitteiden (SUM) määrittäminen ja korvaaminen numeroarvoilla. Tätä varten kääntäjä muodostaa symbolitaulun, joka kertoo symbolien numeeriset vastineet.

Korkean tason kielet

Nykyisin oikeastaan kaikki ohjelmat joitakin käyttöjärjestelmän osia lukuunottamatta kirjoitetaan jollakin korkean tason kielellä, jonka syntaksi on lähempänä matematiikan ja luonnollisen kielen merkintätapoja. Tällainen kieli edellyttää jo varsin monimutkaista kääntäjää, joka tarkistaa, että ohjelma on muodollisesti oikein ja muuntaa sen konekielelle.

Ensimmäinen korkean tason kieli oli IBM:n kehittämä Fortran, jonka ensimmäinen kääntäjä julkaistiin 1957. Kielessä voitiin käyttää matematiikasta tuttuja lausekkeita, mutta muuten se oli vielä melko alkeellinen. Kielen ainoa kontrollirakenne oli hyppykäsky johonkin osoitteeseen; hyppy voi olla joko ehdoton tai riippua jonkin muuttujan arvosta. Esimerkkimme summa voitaisiin laskea ohjelmalla

    S=0.0
    I=1
 1  S=S+I
    I=I+1
    GOTO (1,1,2) I-10
 2  ...  

Ohjelman yhteys edellä olleeseen assembler-versioon on vielä varsin hyvin nähtävissä.

Ohjelmointikielten yleinen teoria alkoi kehittyä voimakkaasti 1950-luvun lopulla. Ensimmäinen moderni ohjelmointikieli oli Algol 60, joka on ollut monien nykyisten kielten esikuvana. Kieli etääntyi itse koneesta ja tuli lähemmäs käyttäjän ongelmien ratkaisemiseen liittyviä tarpeita.

Algolilla summamme laskettaisiin lauseilla, joiden merkitys on jokseenkin ilmeinen:

    s := 0 ;
    for i := 1 step 1 until 10 do s := s+i ;

Korkean tason kielet edellyttävät jo melko mutkikasta kielen syntaksianalyysiä. Tässä merkittävässä asemassa oli MIT:n Noam Chomskyn kehittämä kielioppien luokittelu. Ohjelmointikielten kieliopeille kehitettiin esitystapoja, joiden avulla niitä vastaavien kääntäjien kirjoittaminen oli suhteellisen suoraviivaista.

Vanha korkean tason kieli on myös kaupallis-hallinnollisiin sovelluksiin kehitetty COBOL. Siinä voidaan määritellä tietueita, jotka sisältävät useita eri tietoja. Tietuetta voidaan sitten käsitellä yhtenä kokonaisuutena; se voidaan esimerkiksi kopioida yhdellä ainoalla sijoituslauseella. COBOLissa määritellään, miten monta merkkiä tai numeroa kunkin tiedon esittämiseen tarvitaan. Tämä aiheutti melkoisen urakan vuosituhannen vaihtuessa, kun monissa ohjelmissa vuosiluvulle oli varattu vain kahden numeron mittainen kenttä.

COBOLilla esimerkkisilmukka toteutettaisiin näin:

   SUM = 0.  
   PERFORM LOOP VARYING I FROM 1 TO 10.
LOOP. COMPUTE SUM = SUM + I.

Algolin jalanjäljissä alkoi kehittyä monia muitakin kieliä. Pascal kehitettiin erityisesti opetustarkoituksiin; siinä abstraktien tietotyyppien määrittely oli viety aikaisempaa paljon pitemmälle. Ohjelmoija voi määritellä esimerkiksi, että muuttujan arvojen on oltava tietyllä välillä. Yksinkertaisia muuttujia voi paketoida tietueiksi ja näitä edelleen isommiksi tietueiksi. Ohjelmoijan työn helpottaminen oli vain yksi näiden kielten tavoitteista. Samalla kiinnitettiin huomiota myös ohjelmien luotettavuuteen. Uudemmissa kielissä pystytään havaitsemaan aikaisempaa paremmin monia virheitä jo käännösvaiheessa.

Alkuperäistä Algolia seurasi teoreettisesti kaunis Algol 68, jolle ei kuitenkaan sen mutkikkuuden vuoksi juuri tehty kääntäjiä. Hieman samoin kävi Adalle, josta kaavailtiin hallinnon standardia. Vähitellen myös Fortrania alettiin muokata uusien virtausten mukaiseksi: ensin tuli Fortran 77, ja vihdoin jo nykyaikaiselta vaikuttava Fortran 90, jota ovat seuranneet F95 ja F2000. Näissä esimerkin summa voitaisiin laskea näin:

    S=0.0
    do I=1,10
       S=S+I
    end do

Edellä mainitut kielet ovat suurten yhteisöjen kompromisseja. Siksi niiden kehitys on ollut hidasta ja jotkin piirteet vaikuttavat hieman väkinäisiltä. Toisenlainen kehityslinja on ollut C-kielellä, jonka Dennis Ritchie kehitti Unix-käyttöjärjestelmäämnsä varten. Suurin osa Unixista (ja Linuxista) on edelleenkin kirjoitettu C:llä. Siksi jokaisen Unixia vakavasti harrastavan on oltava siitä ainakin jossakin määrin perillä.

Alkuperäinen C on tavallaan sekoitus korkean tason kieltä ja assembleria. Se toteutettiin aluksi PDP-11-laitteistolla, ja kielessä on monia rakenteita, jotka vastaavat kyseisen koneen assembler-käskyjä. Uudemman ANSI-standardin mukaisessa kielessä ja siitä edelleen kehitetyssä C++-kielessä abstraktiotaso on viety pitemmälle.

C:ssä edellisen esimerkin tehtävä toteutettaisiin vaikkapa näin:

   for (s=0, i=1; i<=10; i++) s += i ;

C on laiskan ohjelmoijan kieli äärimmäisen kompaktin esitystapansa vuoksi. Toisaalta tämä heikentää ohjelmien luettavuutta.

Algoritmiset ja funktionaaliset kielet

Edellä esiintyneet kielet kuvaavat tehtävän ratkaisumenetelmän eli algoritmin niin yksikäsitteisellä tavalla, että se voidaan kääntää konekieliseksi ohjelmaksi. Siksi tällaisia kieliä sanotaan algoritmisiksi kieliksi.

Algoritmisella kielellä kirjoitettu ohjelma on joukko toimenpiteitä, jotka suoritetaan tavallisesti peräkkäin siinä järjestyksessä kuin ne on ohjelmaan kirjoitettu. Tätä suoritusjärjestystä voidaan muuttaa erilaisilla kontrollirakenteille (kappale \cn.??) Ohjelman ja sen konekielisen vastineen välillä on melko yksinkertainen yhteys.

Funktionaalisissa kielissä määritellään joukko funktioita, jotka laskevat jotakin ja palauttavata arvonaan laskennan tuloksen. Ensimmäinen ja edelleenkin paljon käytetty funktionaalinen kieli on 1950-luvulla alkunsa saanut Lisp. Esimerkin summa voitaisiin Lispillä laskea funktiokutsulla

   (do ((s 0) (i 1 10)) 
       ((= i 10) s)
       (setq s (+ s i))
     )

Lisp tulee sanoista list processing language, ja sen ainoa tietorakenne on linkitetty lista. Sekä ohjelman käsittelemät muuttujat että itse ohjelma ovat samanlaisia listoja. Ohjelman kirjoittama lista voidaan myös tulkita ohjelman osaksi ja suorittaa. Ohjelma voi jopa muuttaa itseään, mikä tosin johtaa usein arvaamattomiin tilanteisiin.

Vaikka Lisp on jo varsin vanha kieli, sitä käytetään edelleen. Alkuperäinen Lisp oli erittäin suppea, mutta myöhemmin standardoitu Common Lisp sisältää paljon laajennuksia.

Lispiä käytetään paljon symbolisen matematiikan ohjelmissa. Matemaattinen lauseke voidaan esittää symbolien muodostamana listana, jota ohjelma voi käsitellä. Esimerkiksi funktion derivaatan analyyttisen lausekkeen määrittäminen on helppo ohjelmoida Lispillä.

Prolog herätti aikoinaan suurta huomiota, mutta sittemmin innostus on hiipunut. Kielessä määritellään matematiikan tapaan päättelysääntöjä. Sitten ohjelmalle voidaan esittää väitteitä, ja ohjelma päättelee, ovatko ne tosia vai eivät. Mieleen tulee ajatus "matemaattisesta todistusautomaatista", mutta mistään ihmevälineestä ei ole kysymys. Itse asiassa säännöt on kirjoitettava juuri oikeassa järjestyksessä, jotta päättely toimisi halutulla tavalla.

Olio-ohjelmointi

Olio-ohjelmointa (object-oriented programming) jatkaa kehitystä vielä abstraktimpaan ja tehtävänläheisempään suuntaan.

Olio on jokin tietorakenne, johon liittyy joukko siihen kohdistuvia funktioita. Tietorakenteen komponetteihin päästään käsiksi vain näiden funktioiden avulla, joten ulkopuoliset funktiot eivät voi sekoittaa sitä.

C-kielen laajennus C++ on tunnetuimpia oliokieliä. Myös Lispistä on laajennuksia, jotka sopivat olio-ohjelmointiin. Uudempi tulokas on Java, joka on tarkoitettu erityisesti verkkoympäristöön ja käyttöliittymiin. Java-kääntäjä tuottaa laiteriippumatonta koodia, jonka sitten laitteistosta riippuva osa tulkitsee.

Ohjelman kääntäminen

Tietokone ymmärtää vain omaa konekieltään. Kaikki muut ohjelmat on käännettävä (compile) konekielelle. Käännöksen suorittaa kääntäjäksi (compiler) kutsuttu ohjelma. Unixien mukana tulee ainakin C-kielen kääntäjä, koska sitä tarvitaan, jos käyttöjärjestelmä on käännettävä uudelleen. Muiden kielten kääntäjät voi joutua hankkimaan ja asentamaan erikseen.

Ohjelmoija kirjoittaa ohjelman lähdekoodin (source code) yleensä jollakin korkean tason kielellä. Kääntäjä lukee lähdekoodin ja tuottaa sitä vastaan konekielisen objektikoodin (object code). Unixissa objektikoodia sisältävän tiedoston nimen tarkenneosa on .o.

Objektikoodia ei vielä sellaisenaan voi suorittaa. Siinä voi esimerkiksi olla viittauksia toisissa tiedostoissa oleviin aliohjelmiin tai aliohjelmakirjastojen rutiineihin. Nämä viittaukset ulkopuolisiin olioihin on asetettava osoittamaan oikeisiin paikkoihin ja samalla on tarkistettava, että kaikki viitatut funktiot ja aliohjelmat ovat olemassa. Tämän tehtävän suorittaa linkittäjä (linker). Linkittäjä kokoaa mahdollisesti useista erillisistä objektitiedostoista suoritettavan ohjelman. Suoritettavalle ohjelmalle voidaan antaa haluttu nimi. Ellei nimeä määritellä, Unixissa nimeksi tulee a.out.

Useinkaan käännös- ja linkityskomentoja ei tarvitse antaa erikseen, sillä käännöskomento käynnistää automaattisesti sekä kääntäjän että linkittäjän.

Luvussa ?? tarkastellaan C-ohjelmien kääntämistä ja joitakin tärkeimpiä kääntäjän valitsimia. Muiden kääntäjien käyttö on yleensä samantapaista ja ne hyväksyvät samanlaiset valitsimet.

Tulkittavat kielet

Konekielellä kääntämisen ohella toinen vaihtoehto on, että ohjelma syötetään tulkille, joka tutkii ohjelmaa käsky kerrallaan ja suorittaa käskyn edellyttämät toimenpiteet. Esimerkiksi muinainen ohjelmointityyliin tuhoisasti vaikuttanut BASIC oli aluksi tällainen tulkittava kieli.

Myös Lisp ali alkuaan tulkittava kieli, mikä on varsin ymmärrettävää, kun ohjelma voi generoida lisää ohjelmaa tai jopa muuttaa itseään. Nykyisissä järjestelmissä ohjelmia voidaan myös kääntää.

Tulkkaus soveltuu interaktiivisesti käytettäville kielille. Jokainen syötetty käsky toteutetaan saman tien. Käyttöjärjestelmän komentotiedostot ovat itse asiassa tulkittavia ohjelmia. Varsinkin Unixiin on tullut koko joukko erilaisia ns. skriptikieliä, jotka tarjoavat enemmän mahdollisuuksia kuin käyttöjärjestelmän komentotulkki. Ensimmäinen oli awk, jota on seurannut suuri määärä muita, kuten perl, tcl, python, ...

Numeerisiin sovelluksiin on kehitetty mm. Matlab ja sitä muistuttavat ilmaisversiot, kuten Scilab ja Octave, IDL ja sen ilmainen versio GDL.

Ohjelman tulkinta on kätevää silloin, kun halutaan kokeilla erilaisia asioita. Tulkkia käytettäessä ei tarvitse käydä läpi monivaiheista ketjua editoi ohjelmaa -- käännä ohjelma -- suorita ohjelma.

Toisaalta ohjelman tulkinta vie aikaa. Jos ohjelmassa suoritetaan silmukka miljoona kertaa, sen koko teksti tutkitaan merkki kerrallaan miljoona kertaa ja sen perusteella päätetään, mitä pitää tehdä. Käännetyssä ohjelmassa verrataan ehkä vain kierroslaskuria ylärajaan ja suoritetaan toimenpiteet, jotka on jo tallennettu konekieliseksi ohjelmaksi. Raskaassa laskennassa tulkittava kieli voi siksi toimia hyvin paljon hitaammin kuin käännetty ohjelma. Joissakin tulkittavissa kielissä on kyllä mahdollisuus myös kääntää ohjelmia, jolloin tämä ongelma voidaan osittain välttää.

Java on tulkittavan ja käännettävän kielen välimuoto. Kääntäjä tuottaa siitä laiteriippumatonta koodia, joka sitten tulkitaan. Vaikka tämä välikieli onkin kohtalaisen tehokasta, raskaassa numeerisessa laskennassa se häviää käännettäville kielille.

Muuttujien määrittely

Luvussa 3 käsiteltiin joitakin muuttujatyyppejä. Esimerkiksi kokonais- ja reaaliluvut tarvitsevat muistista erilaisen tilan. Miten niille osataan varata sopivankokoinen tila? Tähän on periaatteessa kaksi erilaista ratkaisua.

Dynaamisessa sidonnassa muuttujan nimi voi eri aikoina viitata eri tyyppisiin olioihin. Muuttujalle ei varata aluksi mitään tilaa, tai oikeammin sille varataan tila, johon mahtuu todellisen muuttujan osoite. Kun muuttujalle joskus annetaan arvo, se asetetaan viittaamaan muuttujan talletuspaikkaan. Erityisesti Lispissä käytetään dynaamista sidontaa.

Staattisessa sidonnassa muuttujalle varataan kiinteä tila ohjelman käynnistyessä. Ohjelmakoodia tuottavat kääntäjän täytyy tietää, minkä tyyppinen muuttuja on, jotta sille osataan varata oikea määrä muistia. Monissa kielissä muuttujan tyyppi on määriteltävä ennen kuin muuttujaa voi käyttää. Ohjelmasta voi löytyä teksti

    integer k

Tästä kääntäjä tietää, että k on kokonaisluku. Fortranissa on käytössä myös implisiittinen tyypin määrittely. Sen mukaan tietyillä kirjaimilla alkavat muuttujien nimet viittaavat kokonaislukuihin, muut reaalilukuihin. Tämä on vaarallista, koska vahingossa väärin kirjoitetty muuttujan nimi ei kääntäjän mielestä ole virhe, vaan viittaa uuteen muuttujaan, jolla varataan erillinen tila. Seurauksena voi olla kummallisia tuloksia, joiden selvittäminen on erittäin hankalaa. Onneksi implisiittiset määrittelyt voidaan kieltää, ja mahdollisuutta on todella syytä käyttää.

Mitä sitten, jos muuttujalle yritetään antaa vääränlainen arvo? Oletetaan, että k on määritelty kokonaisluvuksi, ja sille yritetään sijoittaa arvo esimerkiksi lauseella

   k = 1.7

Kielestä riippuu, mitä tässä tapahtuu. Joissakin kielissä sijoituslauseen oikean puolen arvo pyöristetään lähimmäksi kokonaisluvuksi, jolloin k saa arvon 2. Fortranissa lausekkeen arvo katkaistaan eli pyöristetään aina kohti nollaa, jolloin k saa arvon 1. Joissakin kielissä sijoituslauseen molempien puolten on oltava samaa tyyppiä, joten lause on sääntöjen vastainen. Silloin lause aiheuttaa jo käännösaikana virheilmoituksen. Esimerkiksi Adassa muuttujatyyppien tarkistus on niin tiukkaa, ettei edes lauseke 1+1.5 ole sallittu, koska siinä lasketaan yhteen kaksi erityyppistä lukua. Tosin laskutoimitus voidaan määritellä niin, että kääntäjä hyväksyy sen, mutta se edellyttää, että ohjelmoija päättää, mitä tuolla lausekkeella oikeastaan tarkoitetaan.

Tyyppien tarkistuksessa on kysymys tasapainoilusta ohjelmoinnin vaivattomuuden ja ohjelman luotettavuuden välillä. Jos tarkistus on heikkoa, ohjelmoijan ei tarvitse erityisesti vaivautua muuttujatyyppien muunnosten kanssa. Silloin vaarana on kuitenkin, että ohjelmaan livahtaa vaikeasti löydettäviä virheitä, kun ohjelman oletusarvoinen toiminta ei olekaan sellaista kuin ohjelmoija ajatteli. Tiukka tyyppien tarkistus teettää ohjelmoijalla enemmän töitä, koska kaikki erityyppisten olioiden väliset toimenpiteet on määriteltävä ennen kuin niitä voi käyttää. Tässä tapauksessa monet mahdolliset virheet havaitaan jo ohjelmaa käännettäessä. Kun ohjelma sitten saadaan käännetyksi, se myös toimii suuremmalla todennäköisyydellä.

Kontrollirakenteet

Oikeastaan kaikissa algoritmisissa kielissä esiintyy samoja rakenteita, joilla ohjataan käskyjen suoritusjärjestystä. Oletetaan, että S1, S2, ... ovat kielen mitä tahansa lauseita. Seuraavissa esimerkeissä ei käytetä mitään erityistä ohjelmointikieltä. Vastaavat rakenteet löytyvät eri kielistä hieman eri näköisinä. Luvussa ?? kerrotaan, miten ne on toteutettu C-kielessä.

Peräkkäisyys. Suoritetaan ensi lause S1 ja sen jälkeen S2:

   S1 ; S2

Valinta. Jonkin ehdon perusteella päätetään, mitä tehdään. Tässä on useita mahdollisuuksia. Ensinnäkin jokin lause voidaan suorittaa, jos ehto on tosi, mutta muulloin ei tehdä mitään:

  if (ehto) then S1

Toinen mahdollisuus on, että ehdon ollessa tosi suoritetaan S1, muulloin S2:

  if (ehto) then S1 else S2

Monissa kielissä on myös tämän yleistys, jossa jonkin muuttujan arvon perusteella valitaan suoritettavaksi yksi useista vaihtoehdoista:

  case (i) of
  {
  1: S1
  2: S2
  3: S3
  }

Toisto. Lausetta suoritetaan toistuvasti, kunnes lopetusehto tulee voimaan. Toistostakin voi olla useita erilaisia muotoja. Ensinnäkin lause voidaan suorittaa jonkin muuttujan tietyillä arvoilla:

  do i=1,10 S1

Toinen mahdollisuus on, että lausetta toistetaan niin kauan, kuin ehto on voimassa:

  do while (ehto) S1

Tässä ehtoa siis tutkitaan ennen lauseen suoritusta. Jos ehto on jo heti alussa epätosi, lausetta ei suoriteta kertaakaan.

Ehtoa voidaan tutkia myös vasta lauseen suorituksen jälkeen:

  do S1 until (ehto)

Tässä tapauksessa suoritus lopetetaan, kun ehto tulee voimaan. Lause suoritetaan joka tapauksessa ainakin kerran.

Silmukka voi sisältää myös useita lauseita:

  do while (ehto)
  {
     S1 ;  S2
   }

Kaikki ovat itse asiassa erikoistapauksia n+(1/2) kierroksen silmukasta:

  do
  {
    S1
    if (ehto) exit
    S2
  }

Tässä siis ehtoa tutkitaan silmukan keskellä, ja jos ehto on tosi, silmukan suoritus lopetetaan. Tämä on luontevaa esimerkiksi seuraavanlaisessa tilanteessa:

  do
  {
    read x
    if (x==0) exit
    y=1/x
   ...
  }

Jos luettu luku on nolla, poistutaan silmukasta, ja vältetään nollalla jokaminen, joka yleensä johtaisi suorituksen päättymiseen virhetilanteeseen. Silmukassa voi olla useitakin erilaisia lopetusehtoja, joita tutkitaan suorituksen eri vaiheissa. Näin lopetusta voidaan testata paikassa, johon se luontevasti sopii, ja ohjelmasta saadaan selkeämpi kuin, jos lopetustehdo testauksen pitäisi olla vain silmukan alussa tai lopussa.

Näiden lisäksi useimmissa kielissä on hyppykäsky:

  goto osoite
  ...
  osoite:  Sn

1960-luvulla ohjelmointikielten teoria kehittyi voimakkaasti, ja samalla käytiin väittelyä eri kontrollirakenteista, erityisesti hyppykäskyistä. Niiden runsas viljely johtaa ohjelmaan, jonka toiminnan hahmottaminen on hankalaa. Yksi vaikeus on, että esimerkiksi edellä olleen esimerkin lauseeseen

  osoite: Sn

voidaan hypätä mistä tahansa ohjelman kohdasta. Jos ohjelma on pitkä, vastaavien hyppykäskyjen löytäminen on työlästä.

Pian todistettiin, että mikä tahansa ohjelma voidaan toteuttaa ilman hyppykäskyjä, pelkästään kolmen peruskontrollirakenteen, peräkkäisyyden, valinnan ja toiston avulla. Hyppykäskyjen välttäminen ei toki ole mikään itsetarkoitus, mutta selkeyden vuoksi niiden käyttö olisi syytä rajata poikkeustilanteisiin. Kun ohjelman suunnittelee kunnolla ja johdonmukaisesti esimerkiksi top-down-menetelmällä (kappale ?.??), siihen ei juuri tarvita hyppykäskyjä ja ohjelmasta tulee yksinkertaisempi ja havainnollisempi ja usein myös tehokkaampi.

Aliohjelmat ja funktiot

Kaikki ohjelmat voitaisiin kyllä toteuttaa pelkästään edellä kuvatuilla rakenteilla. Silloin niistä tulisi kuitenkin toivottoman pitkiä ja vaikeaselkoisia. Usein ohjelmassa esiintyy toimenpiteitä, joita tarvitaan monessa eri kohtaa. Tällaista ohjelmakoodia ei kannata kirjoittaa yhä uudestaan, vaan siitä tehdään aliohjelma, jota kutsutaan tarpeen mukaan.

Nimitykset aliohjelma, proseduuri ja funktio tarkoittavat eri ohjelmointikielissä hieman eri asioita. Käytännössä aliohjelmia on kuitenkin kahta perustyyppiä.

Funktio vastaa ajatusta matemaattisesta funktiosta. Se laskee jonkin arvon ja palauttaa sen kutsuvaan ohjelmaan. Tällaisen funktion kutsua voidaan käyttää esimerkiksi matemaattisissa lausekkeissa samaan tapaan kuin siniä tai logaritmia. Seuraava esimerkki on Fortranista:

  y=1+f(1.5)
  ...

  real function f(x)
  real x
  f = sqrt(sin(x))
  end function

Tässä funktio f palauttaa arvonaan argumentin x sinin neliöjuuren.

Aliohjelma sen sijaan ei palauta mitään arvoa. Jotta se ylipäänsä tekisi jotakin hyödyllistä, sillä täytyy olla jonkinlaisia sivuvaikutuksia. Se voi muuttaa argumenttejaan tai kutsuvan ohjelman muuttujia tai se voi lukea tai tulostaa jotakin tms.

Aliohjelman ja funktion argumentteja voidaan käsitellä eri tavoin. Vaikka ohjelmointikielen merkintätapa voi muistuttaa matemaattista notaatiota, sen merkitys on erilainen. Ohjelmointikielen lause tekee aina jotakin. Matematiikassa voimme kirjoittaa funktion arvon f(x) ja luottaa siihen, ettei se vaikuta muuttujan x arvoon. Ohjelman tapauksessa tilanne on mutkikkaampi.

Tarkastellaanpa funktiota

  x = 1.0
  y = f(x)
  write x, y

  real function f(x)
  real x
  x =  x/2
  f = sin(x)
  end function

Tässä funktio f muuttaa parametrinsa arvoa jakamalla sen kahdella. Mitä ohjelma mahtaa tulostaa?

Tilanne riippuu siitä, mitä kieltä tämä on, ja miten se käsittelee funktion parametreja. Parametrien välittämiseen on useita erilaisia mekanismeja, mutta kaksi yleisintä ovat arvoparametrit ja viiteparametrit.

Jos edellisen esimerkin parametri x on arvoparamtri, sen arvo lasketaan funktiota kutsuttaessa ja sen jälkeen funktio käsittelee sitä ikiomana muuttujanaan. Vaikka sen arvoa muutetaan, se ei vaikuta millään tavoin kutsuvan ohjelmaan muuttujaan. Silloin ohjelman tulostama muuttujan x arvo on edelleen 1.0.

Arvoparametrit ovat sikäli turvallisia, että funktiot ja aliohjelmat voivat tehdä parametreille mitä huvittaa, mutta se ei muuta niiden arvoja kutsuvassa ohjelmassa. Esimerkiksi C-kielessä kaikki parametrit ovat arvoparametreja.

Viiteparametri kertoo aliohjelmalle todellisen parametrin osoitteen. Silloin aliohjelma käsittelee kutsuvan ohjelman muuttujaa, jonka arvo voi muuttua. Esimerkin tapauksessa muuttujan x arvoksi tulostuisi 0.5.

Fortranissa kaikki parametrit ovat viiteparametreja. Aikaisemmin tämä oli melkoinen ongelma, sillä ohjelmoija ei voinut varmistaa, että aliohjelmna ei vahingossakaan pääse muuttamaan parametrien arvoja. Fortran 90:stä lähtien ongelmaan on ollut tarjolla ratkaisu.

Lokaalit ja globaalit muuttujat

Tarkastellaan yksinkertaista Fortran-ohjelmaa

  program koe
  real x, y, z, u
  x=1.5
  y=2.0
  u=f(x)
  write(*,*) x,y,z
  ...

  real function f(t)
  real, intent(in):: t
  real y
  y = sqrt(t)
  z = y
  f = sin(y)
  end function

Tässä sekä pääohjelmassa että funktiossa f määritellään saman niminen muuttuja y. Lisäksi pääohjelmassa on määritelty muuttuja z, jota ei ole määritelty funktiossa, mutta jota silti käytetään siellä.

Funktion muuttuja y on lokaali eli paikallinen muuttuja. Funktio voi tehdä sen kanssa mitä tahansa, mutta sillä ei ole mitään vaikutusta pääohjelman muuttujaan y. Funktiokutsun jälkeen pääohjelman muuttujan y arvo on edelleenkin 2.0.

Funktiossa muutetaan muuttujan z arvoa, mutta sellaista muuttujaa ei funktiossa ole määritelty. Kyseessä on globaali muuttuja, joka viittaa johonkin funktion ulkopuolella määriteltyyn muuttujaan. Kielestä riippuu, mihin tällainen globaali viittaus oikein kohdistuu. Tässä tapauksessa se viittaa pääohjelman muuttujaan z, jonka arvo on muuttunut, kun lause u=f(x) on suoritettu. Joskus tällainen globaalien muuttujien sorkkiminen on käytännöllistä, mutta esimerkin tapauksessa kyseessä voi olla ohjelmoijan huolimattomuus, joka johtaa vaikeasti jäljitettävään virhetilanteeseen.

Pääohjelman muuttujan x avulla välitetään tietoa funktiolle. Funktiossa f argumenttina esiintyy muuttuja nimeltä t. Tämä on funktion muodollinen parametri. Kun funktiota kutsutaan, t asetetaan osoittamaan kutsussa esiintyvän todellisen parametrin, tässä tapauksessa muuttujan x, arvoa. Jos nyt funktiossa esiintyisi lause t=t+1, se lisäisi pääohjelman muuttujaan x ykkösen. Todennäköisesti tätä ei kuitenkaan haluta. Siksi funktiossa onkin määritelty, että parametrilla t voi välittää tietoa vain sisäänpäin (in) eli kutsuvasta ohjelmasta funktioon. Jos muuttujan t arvoa yritettäisiin muuttaa, kääntäjä ilmoittaisi toimenpiteen olevan virheellinen. Fortran 77:ssä ja sitä edeltävissä Fortranin versioissa tällainen määrittely ei ollut mahdollista, joten ohjelmoijalla ei ollut mitään keinoa varmistaa, etteivät aliohjelmat ja funktiot muuttele parametrien arvoja. Tämä oli kielen yksi vakavimpia puutteita, mutta onneksi se on korjattu F90:ssä ja sitä uudemmissa versioisa.