Tietokoneen käytön ja ohjelmoinnin alkeet

Kurssin kotisivulle


9 C - osa 1: perusrakenteet

C-ohjelmointikieli on kehittynyt yhdessä Unix-käyttöjärjestelmän kanssa. Se sai alkunsa työkaluna, jonka kokeneet ohjelmoijat loivat omiin tarpeisiinsa käyttöjärjestelmän ohjelmointia varten. Siksi se on pedagogisesti ongelmallinen, mutta parempien kielten kääntäjät ovat vaikeammin saatavissa. Koska Unix on kirjoitettu pääosin C:llä, C-kääntäjä tulee käyttöjärjestelmän mukana ja on heti käytettävissä. Siksi sen osaaminen on Unixin käyttäjälle varsin hyödyllistä. Tässä luvussa esitetään lyhyt johdatus C-kieleen, mutta näiden perustietojen avulla pitäisi jo pystyä kirjoittamaan ohjelmia ainakin suhteellisen yksinkertaisten tehtävien ratkaisemiseen.

Vaikka C on korkean tason kieli, sille on ominaista tietynlainen koneenläheisyys. Esimerkiksi jotkin piirteet ovat seurausta siitä, että niille oli suoranaiset vastineet käytetyn laitteiston konekielessä. Muuttujien tyyppien tarkistus ei ole kovin tiukkaa. Kielestä on karsittu mahdollisimman paljon joidenkin muiden kielten laveasanaisuutta, jotta ohjelmien kirjoittaminen olisi nopeaa. Merkintätapojen äärimmilleen viety tiiviys tekee ohjelmista helposti hyvin vaikeaselkoisia, ja C:tä onkin kritisoitu "write-only"-kielenä. Varsinkin kielen alkuperäisessä versiossa nämä ominaisuudet aiheuttivat helposti vaikeasti selvitettäviä virhetilanteita. Uudemmassa ANSI-standardin mukaisessa kielessä tilanne on hieman parempi.

Monet asiat voidaan ilmaista erittäin lyhyellä ja kryptisellä tai pitemmällä ja luettavammalla tavalla. Hyvään ohjelmointityyliin kuuluu myös ohjelman selkeys, joten lyhyys ei saa olla mikään itsetarkoitus. Aloittelevan ohjelmoijan ei kannata yrittää keksiä mahdollisimman lyhyttä ja nerokasta ratkaisua; tärkeämpää on saada aikaan luotettavasti toimiva ja ymmärrettävä ohjelma.

Esimerkkiohjelma

Kirjoitetaan seuraavanlainen ohjelma tiedostoon koe.c:

  /* triviaali ohjelma, joka tulostaa yhden luvun */
  #include <stdio.h>
  main()
  {
    int luku ;
    luku = 2 + 5*8 ;
    printf("luku = %d\n", luku) ;
  }

Merkkien /* ja */ välissä oleva teksti on kommentti. Kääntäjä ohittaa tämän osan ohjelmasta tekemättä sille yhtään mitään. Heti alusta alkaen olisi hyvä oppia lisäämään ohjelmiinsa selventäviä kommentteja. Hyvin tarpeellinen on aivan alussa oleva kommentti, joka kertoo lyhyesti, mitä ohjelma tekee. Ohjelmatiedostojen lyhyet nimet eivät yleensä kovin paljoa kerro, eikä myöhemmin muista itsekään, mikä jonkin ohjelman tarkoitus oikein on.

Toisen rivin alussa oleva merkki # kertoo, että kyseessä ei ole varsinainen ohjelman lause, vaan kääntäjän toimintaa ohjaava direktiivi. Direktiivi include antaa määräyksen lukea tiedosto stdio.h. Toiminta on aivan sama kuin jos tähän kohtaan kopioitaisiin koko tiedoston stdio.h sisältö. Tuo tiedosto sisältää syöttöön ja tulostukseen liittyvien kirjastoaliohjelmien määrittelyjä.

Tämän jälkeen tulee pääohjelma. Se on itse asiassa samanlainen kuin mikä tahansa muukin funktio, mutta sen tunnistaa nimestä main. Pääohjelman samoin kuin muidenkin funktioiden ohjelmakoodi kirjoitetaan aaltosulkujen väliin.

Seuraavaksi määritellään, että muuttuja luku on kokonaisluku (integer). Kaikki ohjelmassa esiintyvät muuttujat on määriteltävä. Muuttujan nimessä voi olla isoja ja pieniä kirjaimia, numeroita ja alaviivoja. Nimen ensimmäisenä merkkinä ei kuitenkaan saa olla numeroa. C:ssä isot ja pienet kirjaimet tulkitaan eri merkeiksi, joten luku ja Luku ovat eri muuttujia. Joissakin muissa kielissä tällaista eroa ei ole.

Määrittelyt ja lauseet päättyvät aina puolipisteeseen.

Lause luku = 2 + 5*8 on ohjelman ensimmäinen suoritettava lause. Aluksi lasketaan =-merkin oikealla puolella olevan lausekkeen arvo. Sitten lausekkeen arvo sijoitetaan muuttujaan luku. Merkki = ei tarkoita tässä yhtäsuuruutta, vaan kyseessä on sijoitusoperaattori (assignment operator). Matematiikassa luku=luku+1 on epätosi lause, mutta C-kielessä se tarkoittaa toimenpidettä, joka lisää muuttujan luku arvoa yhdellä.

Seuraava lause tulostaa muuttujan luku arvon. Tässä esiintyvä funktio printf löytyy valmiina aliohjelmakirjastosta, joten sitä ei ohjelmoijan tarvitse itse kirjoittaa. Jotta kääntäjä tietäisi, millainen funktio on kyseessä, tarvitaan alussa ollutta direktiiviä.

Funktiolla printf on ensimmäisenä parametrina merkkijono, joka ilmoittaa, miten rivi muotoillaan. Se voi sisältää tekstiä, joka tulostetaan sellaisenaan, kuten tässä luku =. Prosenttimerkki tarkoittaa, että sitä seuraa muotoin, joka määrää tulostettavan muuttujan esitystavan. Kirjain d tarkoittaa, että kyseessä on kokonaisluku. Merkkijonon lopussa oleva \n tarkoittaa rivinvaihtoa. Ilman tätä seuraava tulostus tulisi samalle riville tässä tulostetun tekstin jälkeen. Muotoilua ohjaavan merkkijonon jälkeen luetellaan tulostettavat muuttujat. Tässä tapauksessa niitä on vain yksi. Muuttujia ja merkkijonon sisällä olevia muotoimia on oltava yhtä monta.

Ohjelman kirjoitusasu on varsin vapaa. Muuttujien ja funktioiden nimien ja varattujen sanojen sisällä ei saa olla välilyöntejä, mutta muuten väleillä ja rivinvaihdoilla ei ole merkitystä. Lauseen päättyminen tunnistetaan puolipisteestä, joten lause voi jatkua usealle riville tai samalla rivillä voi olla useita lauseita. Sisennyksiä kannattaa käyttää selventämään ohjelman rakennetta. Isossa ohjelmassa voi olla tuhansia rivejä, jolloin hyvä typografia helpottaa ohjelman jäsentämistä. Kääntäjän kannalta muotoilulla sen sijaan ei ole merkitystä.

Kääntäminen

Esimerkkiohjelmamme voidaan kääntää ja linkittää Unixin komennolla

  gcc koe.c

Tässä gcc tarkoittaa GNU-C-kääntäjää, joka tulee Linuxien mukana. Muissa Unixeissa komento on tavallisesti cc.

Jos ohjelma oli kirjoitettu oikein, tuloksena syntyy suoritettava ohjelma, jonka nimi on  a.out. Se voidaan suorittaa komennolla

  a.out

Jos tästä tulee ilmoitus, ettei sellaista tiedosto löydy, nykyhakemisto ei ole hakupolussa. Silloin ohjelman suorittamiseksi on joko korjattava hakupolkua tai ilmoitettava, että tiedosto on nimenomaan nykyhakemistossa:

  ./a.out

Nimeämistapa voi tuntua erikoiselta, mutta se on perusteltavissa. Käänettäessä useita samassa hakemistossa olevia ohjelmia niistä ei synny useita tiedostoja, vaan ne tallentuvat samaan tiedostoon, jonka entinen sisältö tuhoutuu. Nimen avulla tiedostot on myös helppo etsiä eri hakemistoista ja siivota pois kuluttamasta levytilaa.

Jossakin vaiheessa ohjelma sitten toimii niin hyvin, että suoritettava tiedosto halutaan säilyttää. Sen voi nimetä uudelleen komennolla mv tai antaa nimen kääntäjän valitsimella -o:

  gcc -o koe koe.c

Nyt suoritettava ohjelma tallettuu tiedostoon koe.

Myöhemmin kappaleessa ??.?? tarkastellaan joitakin muita usein tarvittavia käännöskomennon valitsimia.

Toinen esimerkkiohjelma

Luetaan kaksi kokonaislukua ja tulostetaan niiden summa.


  #include <stdio.h>
  main()
  {
    int a, b, summa ;
    printf("anna kaksi lukua\n") ;
    scanf("%d %d", &a, &b) ;
    summa = a + b ;
    printf("summa = %d\n", summa) ;
  }

Funktio scanf lukee tekstiä näppäimistöltä. Samoin kuin funktion printf kutsussa ensimmäisenä on muotoimia sisältävä merkkijono. Muotoin d tarkoittaa, että luetaan kokonaisluku.

Funktiolle scanf välitetään tässä kolme parametria: muotoinluettelo ja kaksi luettavaa muuttujaa. Funktio ei voi muuttaa parametriensa arvoja; siksi sille ei välitetä muuttujia a ja b, vaan niiden talletuspaikkojen osoitteet.

Kolmas esimerkkiohjelma

Tehdään hieman mutkikkaampi ohjelma, joka jo tekee jotakin. Haluamme taulukoida kulmien 0, ..., 90 astetta sinit ja kosinit tietyin välein. Askelpituus kysytään käyttäjältä. Kirjoitetaan tiedostoon taulu.c seuraava ohjelma:

  /* taulukoidaan sini ja kosini kulmille 0, dx, 2dx , .. 90 */
  #include <stdio.h>
  #include <math.h>
  main()
  {
    float x, rx, dx, sinx, cosx, rad = 57.29578 ;
    printf("askelpituus asteina ") ;
    scanf("%f", &dx) ;
    for (x=0.0 ; x <= 90.0; x += dx)
    { 
      rx = x / rad ;   /* kulma radiaaneina */
      sinx = sin(rx) ; cosx = cos(rx) ;
      printf("%5.2f %8.4f %8.4f\n", x, sinx, cosx) ;
    }
  }

Ohjelmassa kutsutaan funktioita sin ja cos. Ne löytyvät vakiokirjastosta, mutta taaskin kääntäjälle on kerrottava, miten ne on määritelty. Tämä tapahtuu ottamalla mukaan matematiikkakirjaston määrittelyt tiedostosta math.h.

Ohjelman kaikki muuttujat ovat nyt reaalilukuja (float). Samassa määrittelyssä voidaan luetella useita muuttujia. Niille voidaan myös antaa alkuarvoja, kuten tässä muuttujalle rad, joka ilmoittaa, kuinka monta astetta yksi radiaani on.

Funktio scanf lukee tekstiä näppäimistöltä. Samoin kuin funktion printf kutsussa ensimmäisenä on muotoimia sisältävä merkkijono. Muotoin f tarkoittaa, että luetaan reaaliluku. Askelpituus luetaan muuttujan dx arvoksi. Muuttujaluettelossa ei kuitenkaa lue dx, vaan &dx. Tämä pieni erikoisuus johtuu siitä, että funktion parametrit ovat arvoparametreja, joten funktio ei voi muuttaa niiden arvoja. Siksi sille ei välitetäkään itse muuttujaa, vaan sen talletuspaikan osoite eli muuttujaan osoittava osoitin. Tätä käsitellään lisää myöhemmin. Toistaiseksi voimme vain tyytyä siihen, että syöttölauseissa muuttujien eteen on lisättävä tuo &-merkki.

Varsinaisen työn ohjelma tekee silmukassa, jossa muuttuja x käy läpi halutut kulmat. Silmukka voidaan toteuttaa for-lauseella, jonka muoto on

for (alkuarvot; ehto; päivitys) lause ;

Aluksi asetetaan alkuarvoja, esimerkissä muuttujan x arvoksi asetetaan nolla. Seuraavaksi tutkitaan, onko suoritusehto vielä voimassa. Jos ehto on tosi, suoritetaan lause, joka voi olla myös aaltosuluilla rajattu joukko lauseita. Tämän jälkeen suoritetaan päivitys. Esimerkissä muuttujaan x lisätään askelpituus dx. Merkintä x += dx toimii samoin kuin pitempi muoto x = x + dx. Sitten siirrytään taas tutkimaan ehtoa ja näin jatketaan, kunnes ehto ei enää ole voimassa.

Esimerkin silmukassa muuttujan x arvo kasvaa joka kierroksella askelpituuden dx verran. Lopulta se tulee suuremmaksi kuin 90, jolloin suoritus lopetetaan. Silmukassa täytyy aina olla jokin muuttuva suure, jonka arvojen perusteella suoritus voidaan lopettaa.

Mitä tapahtuisi, jos esimerkin silmukassa päivitysosassa olisikin x -= dx? Nyt muuttujan arvo pienenisi joka kierroksella eikä saavuttaisi koskaan annettua rajaa. Ohjelma jatkaisi toimintaansa ikuisestsi, kunnes se keskeytettäisiin jollakin muulla tavoin.

C:ssä kuten useimmissa ohjelmointikielissä trigonometristen funktioiden argumentti on radiaaneina. Siksi asteina annettu kulma muunnetaan ensin radiaaneiksi.

Ohjelma käännetään ja linkitetään sanomalla

  gcc -o taulu taulu.c -lm

Erona aikaisempaan on valitsin -lm. Se on linkittäjän valitsin, joka ilmoittaa, että mukaan on linkitettävä myös matematiikkakirjasto. Kokeile, mitä tapahtuu, jos jätät tämän valitsimen pois.

Suoritus sujuu sitten seuraavasti:

>./taulu
askelpituus asteina 10
 0.00   0.0000   1.0000
10.00   0.1736   0.9848
20.00   0.3420   0.9397
30.00   0.5000   0.8660
40.00   0.6428   0.7660
50.00   0.7660   0.6428
60.00   0.8660   0.5000
70.00   0.9397   0.3420
80.00   0.9848   0.1736
90.00   1.0000   0.0000
>

Yksinkertaiset muuttujat

Yksinkertaiset muuttujat voivat olla periaatteessa kolmea tyyppiä, kokonaislukuja, reaalilukuja tai merkkejä. Hieman eksoottisempia olioita ovat osoittimet, joita käsitellään myöhemmin kappaleessa 9.??.

Todellisuudessa muuttujatyyppejä on enemmän, sillä muuttujilla voi olla useita erilaisia lisämääreitä. Esimerkiksi kokonaisluvut voivat olla lyhyitä (short int), tavallisia (int), pitkiä (long int) tai etumerkittömiä (unsigned). Ikävä kyllä näiden toteutys vaihtelee eri laitteistoilla.

Reaaliluvut voivat olla yksinkertaisen tarkkuuden (float) tai kaksinkertaisen tarkkuuden (double) lukuja. C-kielen alkuperäisessä versiossa kaikki reaalilukujen laskutoimitukset laskettiin varmuuden vuoksi kaksinkertaisella tarkkuudella, mikä on tietysti tulosten kannalta hyvä asia. Tehokkuus on sitten toinen juttu. Sillä on merkitystä, jos ohjelmassa suoritetaan hyvin suuri määrä laskutoimituksia. Laitteistosta riippuu, miten kaksoistarkkuuden laskutoimitukset toteutetaan. Pahimmassa tapauksessa ne voivat olla hyvin paljon hitaampia kuin yksinkertaisen tarkkuuden laskutoimitukset. Silloin on syytä harkita, millä tavoin muuttujansa määrittelee.

Lukujen lisäksi usein tarvittava muuttujatyyppi on merkki (char). Se on muuttuja, joka voi sisältää yhden merkin merkkikoodin eli välillä 0--127 olevan kokonaisluvun. Käytännössä sille siis varataan tilaa yhden tavun verran.

Muuttujien määrittely voisi olla vaikka seuraavanlainen:

  int i, j=1, n1=100, n2 ;
  float x, y ;
  double z=1.0, w ;
  char merkki='A' ;

Joissakin kielissä totuusarvojen esittämiseen on oma muuttujatyyppinsä (logical, boolean). Sellainen muuttuja voi saada vain arvot tosi tai epätosi. C-kielessä totuusarvoja varten ei ole erillistä muuttujatyyppiä, vaan totuusarvoja esitetään kokonaisluvuilla. Muuttujan arvo 0 vastaa epätotta ja kaikki nollasta poikkeavat arvot totta.

Sijoituslause

Tavallisin toimenpide on jonkin arvon sijoittaminen muuttujaan. Tämä tapahtuu sijoitusoperaattorilla =:

  muuttuja = lauseke ;

Tästä on edellä ollut jo useita esimerkkejä. Seuraavassa kappaleessa puolestaan tarkastellaan lähemmin, millaisia lausekkeita sijoitusoperaattorin oikealla puolella voi olla.

Sijoituslause tekee tietyn toimenpiteen, mutta sillä on myös tietty arvo. Sijoituslauseen arvo on muuttujaan sijoitettavan lausekkeen arvo tai yhtäpitävästi muuttujan sisältö sijoituksen jälkeen. Tätä arvoa voidaan käyttää edelleen missä tahansa, missä voi esiintyä jokin lauseke. Niinpä seuraavanlainen rakenne on täysin kelvollinen:

   i = j = 1 ;

Nyt j=1 on sijoituslause, joka palauttaa arvon 1. Tämä arvo sijoitetaan sitten edelleen muuttujan i arvoksi. Lause toimii siten samalla tavoin kuin lauseet

   j = 1 ;
   i = j ;

Aritmeettiset lausekkeet

Muuttujista, vakioista ja funktiokutsuista voidaan muodostaa mutkikkaampia lausekkeita, jotka noudattavat suunnilleen matematiikasta tuttua merkintätapaa.

Operaattoreilla +, -, * ja / on tavanomainen tulkinta peruslaskutoimituksina. Niille pätee myös tavanomainen assosiatiivisuus: ensin lasketaan kerto- ja jakolaskut vasemmalta oikealle, sitten yhteen- ja vähennyslaskut vasemmalta oikealle. Laskentajärjestystä voidaan muuttaa sulkumerkeillä ( ) .

Potenssiinkorotukselle ei ole omaa operaattoria, vaan sitä varten on kutsuttava varusfunktiota: pow(x,y) palauttaa arvon xy.

Jakojäännös saadaan operaattorilla %. Esimerkiksi lausekkeen 5%2 arvo on 1.

Matemaattisten funktioiden kirjasto sisältää kaikki tavalliset alkeisfunktiot:

exp(x) eksponenttifuntkio, ex
log(x) luonnollinen logaritmi, ln x
log10(x) 10-kantainen logaritmi
sqrt(x) neliöjuuri
cbrt(x) kuutiojuuri (GNU-C:n laajennus)
sin(x) sin x, missä x on radiaaneina
cos(x) cos x
tan(x) tan x
asin(x) arcsin x radiaaneina, välillä [-pi/2, +pi/2]
acos(x) arccos x radiaaneina, välillä [0, pi]
atan(x) arctan x radiaaneina, välillä [-pi/2, +pi/2]
atan2(y,x) arctan y/x radiaaneina, välillä [-pi, +pi]
Tämä vastaa muunnosta suorakulmaisesta xy-koordinaatistosta napakoordinaatistoon; oikea neljännes valitaan koordinaattien  perusteella.
sinh(x) hyperbolinen sini
cosh(x) hyperbolinen kosini
tanh(x) hyperbolinen tangentti
asinh(x) hyperbolisen sinin käänteisfunktio ar sinh x
acosh(x) hyperbolisen kosinin käänteisfunktio
atanh(x) hyperbolisen tangentin käänteisfunktio

Aritmeettinen lauseke voi esiintyä sijoitusoperaattorin oikealla puolella, jolloin sen arvo lasketaan ensin ja sijoitetaan sitten vasemmalla puolella olevan muuttujan arvoksi. Se voi myös esiintyä ehto-osana ehdollisissa ja toistolauseissa, joita käsitellään myöhemmin.

Seuraavassa on joitakin esimerkkejä sallituista aritmeettisista lausekkeista

  1 + 2*3 - 8/3
  x + 2*(y-1)
  sqrt(sin(x/2)+0.5)
  

C on kehitetty laiskan ohjelmoijan tarpeisiin, joten siinä on lyhennysmerkintöjä usein toistuville toimenpiteille.

Sijoitusoperaattoria voi edeltää mikä tahansa aritmeettinen operaattori. Esimerkiksi lause

   n += 2 ;

toimii samalla tavoin kuin lause

   n = n + 2 ;

Vastaavasti lause

   n *= 2 ;

kaksinkertaistaa muuttuja n arvon.

Hyvin usein esiintyvä toimenpide on muuttujan arvon lisääminen yhdellä. Siksi sille on oma merkintänsä:

   ++ n ;

Tämä toimii samalla tavoin kuin lauseet n += 1 tai n = n + 1.

Vastaavasti muuttujan arvoa voidaan pienentää yhdellä operaattorilla --:

   -- n;

Näistä operaatioista on itse asiassa kaksi eri muotoa. Lisäysoperaattorin toinen muoto on

   n ++ ;

Tämäkin lisää muuttujan n arvoa yhdellä. Ero tulee esiin, jos lauseen arvoa käytetään johonkin. Operaatio ++ n lisää ensin muuttujan arvoa ja palauttaa arvonaan tuon lisätyn arvon, samoin kuin lause n=n+1. Sen sijaan lauseen n++ arvo on muuttujan n arvo ennen ykkösen lisäystä.

Lauseiden

   i = 1 ;
   j = i++ ;

jälkeen muuttujan i arvo on 2 ja j:n arvo 1. Sen sijaan lauseiden

   i = 1 ;
   j = ++i ;

jälkeen sekä i että k ovat 2.


Kontrollirakenteet

Kaikki ohjelmat voidaan toteuttaa kolmen perusrakenteen avulla: peräkkäisyys, valinta, toisto. Nämä kontrollirakenteet löytyvät käytännössä kaikista ohjelmointikielistä.

Peräkkäisyys

Lauseita suoritetaan peräkkäin siinä järjestyksessä kuin ne on ohjelmaan kirjoitettu. Kukin lause päätetään puolipisteeseen (;). Puolipisteen paikalla voidaan käyttää myös pilkkua:

  lause-1, lause-2 ;

Ensin suoritetaan lause-1, jonka palauttama arvo palautetaan koko rakenteen arvona. Sitten suoritetaan lause-2, mutta sen arvo heitetään menemään. Tavallisin käyttö pilkulle on toistolauseessa, jota käsitellään vähän myöhemmin.

Lauseet

   x = 1.0; y=2.0; z=-5.0;

voidaan kirjoittaa myös

   x = 1.0, y=2.0, z=-5.0;

Rakenne on joskus vaarallinen. Mitä tekee seuraava lause?

   i=1,2;

Desimaaliosan erotin on piste, ei pilkku!


Valinta

Valinta voi tapahtua yhden, kahden tai useamman vaihtoehdon välillä.

1) Yksi vaihtoehto; lause suoritetaan, jos ehto on voimassa. Lause on muotoa

  if (ehto) lause ;

Esimerkiksi lause

   if (i==1) n=0 ;

Asettaa muuttujan n arvoksi nollan, jos i on yksi. Muussa tapauksessa ei tehdä mitään.

Huomaa, että vertailuoperaattori on ==, ei =. Lause

   if (i=1) n=0 ;

on kyllä kelvollinen, mutta sen toiminta on todennäköisesti aivan muuta kuin mitä haluttiin.

2) Kaksi vaihtoehtoa:

  if (ehto) lause-1; else lause-2 ;

Jos ehto on tosi, suoritetaan lause-1, muuten lause-2.

Esimerkiksi

   if (n != 0)  y = x/n ; else y = 0.0 ;

Tässä siis jakolasku suoritetaan vain, jos jakaja on nollasta poikkeava.

3) Valinta useammasta vaihtoehdosta. Tähän on kaksi erilaista ratkaisua. Ensimmäinen mahdollisuus on ketjuttaa if .. else -rakenteita:

  if (ehto-1)  lause-1; 
  else if (ehto-2) lause-2 ;
  else if (ehto-3) lause-3 ;
  else lause-4 ;

Esimerkiksi

   if (n < 0)  k = -1 ; 
   else if (n==0)  k = 0 ;
   else k = n + 1 ;

Jos valinta tapahtuu jonkin muuttujan arvon perusteella, se voidaan toteuttaa case-rakenteella. Esimerkiksi

  switch (n) {
    case 0: k = 0 ; i = 1 ; break ;
    case 1: k = 1 ; break ;
    case 2: 3: k = 2; break ;
    default: k = 3 ; break ;
  }

Muuttujan n perusteella valitaan suoritettavat lauseet. Jos n==0, suoritetaan lauseet k = 0 ja i = 1. Suoritusta jatketaan niin pitkälle, kunnes vastaan tulee break-lause.

Break ei ole pakollinen, mutta silloin suoritus jatkuu seuraavasta lauseesta, mikä ei ehkä ole tarkoitus.

Useampi muuttujan arvo voi johtaa samaan haaraan. Edellä lause k = 2 suoritetaan, jos muuttujan n arvo on 2 tai 3.

Rakenteessa voi olla myös default-haara, johon mennään, mikäli muuttujan arvo ei ole mikään luetelluista. Default ei ole välttämätön. Silloin ei tehdä mitään, mikäli valitsimen arvo ei ole mikään luetelluista.

Valitsimena voi olla kokonaislukumuuttuja, mutta se voi olla myös mielivaltainen kokonaislukuarvoinen lauseke. Myös merkkien merkkikoodit ovat käytännössä kokonaislukuja, joten seuraavanlainenkin rakenne on mahdollinen:

   char command ;
   ...
   switch (command) {
     case '+': z = x + y ; break ;
     case '-': z = x - y ; break ;
     case '*': z = x * y ; break ;
     case '/': z = x / y ; break ;
     default: error() ; break ;
   }


Vertailuoperaattorit

Edellä esiintyi jo operaattori ==, jolla tutkitaan, ovatko kahden lausekkeen arvot samoja. Aritmeettisia lausekkeita voidaan vertailla operaattoreilla:

  x==y    yhtäsuuruus, tosi, jos x=y
  x!=y    erisuuruus, tosi, jos x ja y eri suuria
  xy     suurempi kuin, tosi, jos x>y
  x>=y    suurempi tai yhtä suuri

Muuttujat x ja y voivat olla joko kokonais- tai reaalilukuja. Vertailu on mahdollista myös merkeille. Merkkien tapauksessa esimerkiksi x > y on tosi, jos merkin x merkkikoodi kokonaislukuna on suurempi kuin merkin y merkkikoodi. Käytännössä tämä tarkoittaa, että merkki x on aakkosissa merkin y jälkeen. Skandinaavisten ääkkösten osalta tämä ei kuitenkaan pidä välttämättä paikkaansa.

Vertailuoperaatio tuottaa aina joko arvon 0 (epätosi) tai 1 (tosi). Sitä voidaan käyttää ehdollisten ja toistolauseiden suoritusta ohjaavissa ehdoissa, mutta yhtä hyvin myös kokonaislukuna aritmeettisissa lausekkeissa.

Sijoituslause voi esiintyä myös osana vertailuoperaatiota:

  if (i == j = n + m) ...  

Tässä lasketaan ensin summa n+m, sijoitetaan se muuttujan j arvoksi, ja lopuksi verrataan, ovatko i ja j yhtä suuria. p> Varoitus 1: Tämä voi johtaa vaikeasti ymmärrettävään temppuiluun. Yleensä parempi kirjoittaa esimerkiksi

  j = n + m ;
  if (i == j) ...  

Varoitus 2: Ole tarkkana operaattorien = ja == kanssa! Monissa tilanteissa kumpikin on syntaktisesti kelvollinen, joten kääntäjä ei valita asiasta, mutta toiminta on tyystin erilaista.

Varoitus 3:

  float x, y ;
  x = 1.0/3 ;
  y = 3.0/9 ;
  if (x==y) ...

Ovatko x ja y varmasti yhtäsuuria?

Reaalilukujen yhtäsuuruuden vertaaminen vaarallista. Mieluummin esimerkiksi

  if (fabs(x-y) < 1.0e-5) ...

Huom: reaaliluvun itseisarvo on fabs; abs palauttaa kokonaisluvun, joten ei kelpaa tähän tarkoitukseen.

/* luetaan kokonaislukuja; lopetetaan kun luettu luku = 0,
   tulostetaan positiivisten ja negatiivisten 
   lukujen lukumaarat */

#include <stdio.h>
main() {
 int luku, neg, pos, total ;
 total = neg = pos = 0 ;
 while(1) {  
   scanf("%d",&luku) ;
   if (luku == 0) break ;
   if (luku < 0) neg++ ;
   if (luku > 0) pos++ ;
   total++ ;
 }
 printf("%d lukua, %d negatiivista, %d positiivista\n", 
        total, neg, pos) ;
}


Loogiset operaattorit

Valinnassa tarvitaan jokin ehto, jonka perusteella päätetään, mitä tehdään. Ehto voi olla pelkkä totuusarvo (1=tosi, 0=epätosi), mutta se voi olla myös mutkikkaampi lauseke.

Totuusarvoja voidaan yhdistellä loogisilla operaattoreilla logiikan lausekalkyylin tai Boolen algebran tapaan. C-kielen loogiset operaattorit ovat

  x && y   looginen JA, tosi, jos sekä x että y ovat tosia
  x || y   looginen TAI, tosi, jos joko x tai y tai molemmat ovat tosia
  !x       negaatio, tosi, jos x on epätosi

Esimerkiksi ehto

   (i==0)||(j==0)

on tosi, jos joko i tai j on nolla tai molemmat ovat nollia.

Ehto

   (i==0)&&(j==0)

puolestaan on tosi jos ja vain jos sekä muuttujat i että j ovat nollia. Tämän negaatio

   !((i==0)&&(j==0))

on tosi, jos ainakin toinen muuttujista on nollasta poikkeava. Boolen algebran sääntöjen mukaan tämä voitaisiin kirjoittaa myös muotoihin

   !(i==0) || !(j==0))
   (i!=0) || (j!=0)

Tilanteesta riippuu, mikä näistä on kaikkein havainnollisin.

Esimerkiksi seuraavalla tavalla voidaan tutkia, onko annettu vuosi karkausvuosi:

   int vuosi, karkausvuosi, paivat ;
   vuosi = ...
   karkausvuosi = 
         ((vuosi % 4 == 0)&&(vuosi % 100 != 0)) 
           || (vuosi % 400 == 0) ;

   if (karkausvuosi) paivat = 366; 
   else paivat = 365 ;


Toisto

Toistolauseesta on kolme erilaista muotoa

  while (ehto) lause ;
  do lause1 while (ehto)  ;
  for (alustus; ehto; päivitys) lause ;

while-lause

Ensimmäisessä versiossa lausetta suoritetaan toistuvasti niin kauan kuin ehto on voimassa. Suoritusehtoa tutkitaan aina jokaisen kierroksen alussa. Jos se on epätosi jo heti alussa, silmukkaa ei suoriteta kertaakaan.

  jos ehto ei ole tosi, lopeta
  suorita lause
  jos ehto ei ole tosi, lopeta
  suorita lause
  jos ehto ei ole tosi, lopeta
  ...

Seuraavassa on esimerkki while-rakenteen käytöstä. Ohjelma laskee harmonisen sarjan summaa niin kauan kuin termit ovat tiettyä rajaa suurempia.

   #include <math.h>
   #include <stdio.h>
   main() {
     int i, n ;
     double term, sum, limit=0.001 ;
  
     sum=0 ; i=1; term=1 ;
     while (term > limit) {
       sum += term ;
       i++ ; 
       term = 1.0/i ;
     }
     printf("%d termiä, summa = %lf\n", i, sum) ;
   }

Toistolauseesta voidaan poistua break-lauseella. Sen avulla voidaan toteuttaa ns. n+1/2 kierroksen silmukka:

   while (1) {
     ...
     if (lopetus) break ;
     ...
   }

Tässä suoritusehtona on vakio 1, joka vastaa totuusarvoa tosi. Silmukkaa toistettaisiin siis loputtomiin, ellei sen sisällä olisi jokin suorituksen lopettava lause. Esimerkki: luetaan lukuja, kunnes syötetään negatiivinen luku, ja lopuksi tulostetaan lukujen summa:

   summa = 0.0 ;
   while (1) {
     scanf("%f", &x);
     if (x < 0.0) break ;
     summa += x ;
   }
   printf("summa=%f\n", summa) ;

Silmukan sisällä voi olla useita eri lopetustestejä. Lisätään toinen ehto: lopetetaan, kun summa on suurempi kuin 100:

   summa = 0.0 ;
   while (1) {
     scanf("%f", &x);
     if (x < 0.0) break ;
     summa += x ;
     if (summa > 100.0) break ;
   }
   printf("summa=%f\n", summa) ;

do-lause
Toistolauseen toinen muoto on do .. while -rakenne. Siinä suoritusehtoa testataan vasta silmukan lopussa, joten silmukka suoritetaan joka tapauksessa ainakin kerran.

  suorita lause
  jos ehto ei ole tosi, lopeta
  suorita lause
  jos ehto ei ole tosi, lopeta
  ...

   #include <math.h>
   #include <stdio.h>
   main() {
     int i, n ;
     double term, sum, limit=0.001 ;
  
     sum=0 ; i=1; term=1 ;
     do {
       sum += term ;
       i++ ; 
       term = 1.0/i ;
     } while  (term > limit) ;
     printf("%d termiä, summa = %lf\n", i, sum) ;
   }

for-lause

Kolmas muoto for-rakenne on kätevin tilanteissa, joissa silmukkaa suoritetaan jonkin muuttujan tietyillä arvoilla.

   for (i=0 ; i < 10; i++) a[i] = i ;

Tämä täyttää taulukon a luvuilla 0, 1, ..., 9.

   i=0
   jos (i < 10) jatketaan silmukkaa, muuten lopetetaan
   a[i] = i
   i = i+1
   jos (i < 10) jatketaan silmukkaa, muuten lopetetaan
   a[i] = i
   ...

Lauseen alustus-, ehto- ja päivitysosat muodostuvat kukin periaatteessa yhdestä lauseesta. Jokaisen tilalla voi kuitenkin olla myös useita pilkulla erotettuja lauseita. Varsinkin alustus- ja päivitysosassa tämä on usein käytännöllinen ratkaisu. Voimme samalla kertaa alustaa ja päivittää useita eri muuttujia:

   for (i=0, j=1 ; i < 10; i++, j *= 2) a[i] = j ;

Tämä sijoittaa taulukkoon a luvut 1, 2, 4, 8, ....

   i=0, j=1
   jos (i < 10) jatketaan silmukkaa, muuten lopetetaan
   a[0] = 1
   i = 1, j=2
   jos (i < 10) jatketaan silmukkaa, muuten lopetetaan
   a[1] = 2
   i = 2, j = 4
   jos (i < 10) jatketaan silmukkaa, muuten lopetetaan
   a[2] = 4
   ...

Alustus- ja päivitysosat voivat myös puuttua. Aikaisemmin esiintynyt ikuinen silmukka voitaisiin toteuttaa myös näin:

   for ( ; 1 ; ) {
   ...
   if (lopetus) break ;
   ...
   }

Esimerkki: Lasketaan eksponenttifunktio sarjakehitelmästä

ex = 1+x+(1/2)x2 + (1/6)x3 + ...

/* lasketaan eksponenttifunktio Taylorin sarjasta */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
main()
{
  float x, summa, termi ;
  int i, n ;

  printf("anna x ja termien maara: ") ;
  scanf("%f %d", &x, &n) ;

  /*jos virheellinen termien lkm, lopetetaan */
  if (n <= 0)
  {
    printf("termien lukumaaran oltava > 0\n") ;
    exit(1) ;
  }

  /* lasketaan Taylorin sarjan summa */
  summa = 1.0 ;
  termi = 1.0 ;

  for (i=1; i<n; i++)
  {
    termi = termi * x / i ;
    summa += termi ;
  }

  printf("exp(%f)=%f\n", x, summa) ;

}

>
>gcc exp.c -lm
>./a.out
anna x ja n: 1 10
exp(x)=2.718282
>

Esimerkki: yhtälön ratkaisu suoralla iteroinnilla.

Kirjoitetaan yhtälö muotoon $x=f(x)$, jolloin ratkaisua voidaan etsiä iteroimalla

x0 = alkuarvaus,
x1 = f(x0),
x2 = f(x1),
...

Jatketaan, kunnes peräkkäiset x-arvot eivät muutu vaaditulla tarkkuudella.

Lasketut x-arvot eivät aina suppene kohti ratkaisua. Silloin voidaan yrittää ratkaista yhtäpitävää yhtälöä x=f-1(x).

/* ratkaistaan yhtalo x=cos(x) */
#include <stdio.h>
#include <math.h>
main()
{
  float x0, x1 ;
  int n=0 ;
  printf("anna alkuarvo: ") ;
  scanf("%f", &x0) ;
  x1 =  cos(x0) ;

  while (fabs(x1-x0)>1.0e-5) 
  {
    x0 = x1 ;
    x1 = cos(x0) ;
    n++ ;
  }

  printf("x=%f, cos(x)=%f, n=%d\n", x1, cos(x1), n) ;
}

>./a.out
anna alkuarvo: 0.5
x=0.739082, cos(x)=0.739087, n=27


Taulukoiden alkeita

Esimerkiksi

    int taulu[10] ;

määrittelee taulukon, joka sisältää kymmenen kokonaislukua.

C:ssä taulukon indeksointi alkaa aina nollasta. Tätä ei voi muuttaa, vaikka monissa tapauksissa jokin muu alaraja olisi tehtävän kannalta luontevampi. Seurauksena on joskus ikävää indeksiaritmetiikkaa.

Esimerkin taulukon alkiot ovat siis

taulu[0], taulu[1], ... taulu[9];

Taulukon määrittelyssä esiintyvä luku 10 ei siis ole indeksin suurin sallittu arvo, vaan alkioiden lukumäärä.

Esimerkin taulukko voitaisiin nollata silmukalla

    for (i=0; i<10; i++) taulu[i]=0 ;

Silmukan rajojen kanssa on oltava tarkkana. Tässä lausetta toistetaan vain niin kauan kuin i on aidosti pienempi kuin 10.

Esimerkki: lasketaan lukujen 0..10 kertomat taulukkoon:

#include <stdio.h>
main() {
   int fac[11], i, luku ;
  
   fac[0]=1 ;
   for(i=1, luku=1; i<=10; i++)
   {
       luku *= i ;
       fac[i] = luku ;
   }

   for(i=0; i<=10; i++)
     printf("%d! = %d\n", i, fac[i]) ;
}

gcc fac.c
./a.out

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800


Esimerkki: lasketaan Fibonaccin sarjan alkupää taulukkoon:

#include <stdio.h>
main() {
   int fib[21], i ;
  
   fib[0]=1 ;
   fib[1]=1 ;

   for(i=2; i<=20; i++)
   {
     fib[i] = fib[i-1]+fib[i-2] ;
   }

   for(i=0; i<=20; i++)
     printf("fib(%d) = %d\n", i, fib[i]) ;
}


fib(0) = 1
fib(1) = 1
fib(2) = 2
fib(3) = 3
fib(4) = 5
fib(5) = 8
fib(6) = 13
fib(7) = 21
fib(8) = 34
fib(9) = 55
fib(10) = 89
fib(11) = 144
fib(12) = 233
fib(13) = 377
fib(14) = 610
fib(15) = 987
fib(16) = 1597
fib(17) = 2584
fib(18) = 4181
fib(19) = 6765
fib(20) = 10946

Esimerkki: sama ilman taulukkoa

#include <stdio.h>
main() {
   int f0, f1, f2, i ;
  
   f0=1 ;
   f1=1 ;

   for(i=2; i<=20; i++)
   {
     f2 = f0 + f1 ;
     printf("fib(%d) = %d\n", i, f2) ;
     f0=f1;
     f1=f2;
   }
}

fib(2) = 2
fib(3) = 3
fib(4) = 5
fib(5) = 8
fib(6) = 13
fib(7) = 21
fib(8) = 34
fib(9) = 55
fib(10) = 89
fib(11) = 144
fib(12) = 233
fib(13) = 377
fib(14) = 610
fib(15) = 987
fib(16) = 1597
fib(17) = 2584
fib(18) = 4181
fib(19) = 6765
fib(20) = 10946

Esimerkki: taulukon lajittelu nousevaan järjestykseen.

#include <stdio.h>
main() {
   int i, j, tmp,
   taulu[10] = {5, 1, 0, 3, 2, 7, 6, 9, 4, 8} ;
  
   for(i=0; i<10; i++)
   for(j=i+1; j<10; j++)
     /* jos alkiot ovat vaarassa jarjestyksessa,*/ 
     /* vaihdetaan keskenaan                    */
     if (taulu[i] > taulu[j]) 
     {  tmp = taulu[j] ;      
        taulu[j] = taulu[i] ;
        taulu[i] = tmp ;
     }

   for(i=0; i<10; i++) 
     printf(" %d", taulu[i]) ;
   printf("\n") ;

}

> gcc lajit1.c 
> ./a.out
 0 1 2 3 4 5 6 7 8 9
>



Osoittimet

Jokaisella muuttujalla on tietty talletuspaikka, jolla on yksikäsitteinen osoite. Osoitin on olio, joka sisältää tuon osoitteen ja mahdollisesti muutakin tietoa. C:n ominaisuuksien vuoksi siinä tarvitaan osoittimia enemmän kuin monissa muissa kielissä.

Tarkastellaan seuraavaa esimerkkiä:

    int k ;
    int *n ;
    k = 1 ;
    *n = 2 ;
    

Tässä k on tavallinen kokonaislukumuuttuja. Sille voidaan antaa arvo sijoituslauseella (k=1). Toinen määrittely näyttää hieman erikoiselta. Se sanoo kyllä, että *n on kokonaisluku. Niinpä tuntuisi luonnolliselta, että sillekin voidaan antaa arvo kuten ohjelmassa sijoituslauseella. Kääntäjä ei (luultavasti) valita asiasta, joten kaikki tuntuu olevan kunnossa. Kun ohjelma sitten suoritetaan, tuloksena on todennäköisesti lakoninen ilmoitus "Segmentation fault". Jotakin on siis pielessä.

Ongelma on siinä, että määrittely int *n ei varaa tilaa kokonaisluvulle, vaan osoittimelle, joka osoittaa kokonaislukuun. Määrittely ei määrittele kokonaislukua, vaan osoitintyyppisen muuttujan, jonka nimi on n. Merkintä *n tarkoittaa osoittimen n osoittaman muistipaikan sisältöä. Alussa n ei osoita mihinkään. Lausessa *n=2 yritetään sijoittaa kakkonen osoittimen n osoittamaan muistipaikkaan, jota ei oikeastaan ole olemassakaan.

Muutetaan ohjelmaa lisäämällä siihen yksi rivi:

    int k ;
    int *n ;
    k = 1 ;
    n = &k ;
    *n = 2 ;

Lisäsimme lauseen n=&k. Sen vaikutus on, että osoitin n pannaan osoittamaan muuttujan k talletuspaikkaa. Merkintä &k tarkoittaa juuri muuttujan k osoitetta. Nyt osoitin n osoittaa kelvolliseen kokonaislukumuuttujaan, ja sen arvo voidaan asettaa sijoituslauseella *n=2. Todellisuudessa tämä muuttaa muuttujan k arvoa.

Osoittimien avulla samaan muistipaikkaan voidaan viitata useilla eri nimillä. Joskus tämä on hyödyllistä, mutta voi myös johtaa vaikeaselkoisiin ohjelmiin ja virheisiin, joiden syyn selvittäminen on työn takana.

Muista aina erottaa toisistaan muuttujan arvo ja sen osoite, samoin kuin osoitin ja osoittimen osoittaman muuttujan arvo.

Osoittimien käyttökohteita:

- C:ssä välttämättömiä, jos funktion on päästävä muuttamaan argumenttina välitetyn muuttujan arvoa. Tästä on jo esiintynyt esimerkki, funktio scanf, joka sijoittaa lukemansa tiedot muuttujien arvoiksi.

- Merkkijonojen käsittely


Funktiot

Esimerkki:

  #include <stdio.h>

  int func (int x) ;

  main()
  {
    int y ;
    y = func(5) ;
    ...
  }

  int func (int x)
  {
    return 2*x + 1 ;
  }

Pääohjelmassa esiintyy funktiokutsu func(5). Kääntäjä ei voi pelkän kutsun perusteella tietää, minkä tyyppinen funktio on ja millaisia argumentteja sille pitäisi välittää. Siksi ennen pääohjelmaa on annettava funktion prototyyppi , joka sisältää funktion ja sen argumenttien määrittelyt.

Käytännössä sen voi kirjoittaa kopioimalla editorilla funktion varsinaisen määrittelyn alun sopivaan paikkaan.

Huomaa, että prototyyppi päättyy puolipisteeseen, jota varsinaisessa määrittelyssä ei ole.

Prototyyppi kertoo, että func palauttaa arvonaan kokonaisluvun ja että sille täytyy antaa argumenttina yksi kokonaisluku.

Funktion suoritettava koodi on joukko aaltosulkujen välissä olevia lauseita. Pääohjelma on sekin oikeastaan funktio.

Funktiolla voi olla nolla tai useampia argumentteja. Esimerkin funktion määrittelyssä esiintyvä x on funktion muodollinen argumentti (tai muodollinen parametri).

Funktion kutsussa esiintyvät suureet ovat todellisia argumentteja . Ne voivat olla vakioita, muuttujia tai lausekkeita.

  #include <stdio.h>

  int func (int x) ;

  main()
  {
    int y1, y2, z=1.0 ;
    y1 = func(z) ;
    y2 = func(y1+2*z) ;
    ...
  }

  int func (int x)
  {
    return 2*x + 1 ;
  }

Funktion arvo välitetään kutsuvalle ohjelmalle lauseella return. Lause päättää samalla funktion suorituksen. Se voi sijaita missä tahansa kohtaa funktion sisällä. Jos se on esimerkiksi toistolauseessa, toisto lopetetaan ja suoritus jatkuu kutsuvasta ohjelmasta.

C-kielessä ei ole erikseen funktioita, jotka laskevat jonkin arvon, ja aliohjelmia, jotka eivät palauta mitään arvoa. Funktion arvoa ei tosin tarvitse käyttää mihinkään. Esimerkissä lauseen y=func(5) tilalla voisi olla pelkästään func(5), mikä tässä tapauksessa ei kylläkään olisi mielekästä.

Jos halutaan kirjoittaa aliohjelman tapainen funktio, jonka toiminta perustuu pelkästään sivuvaikutuksiin, funktion tyypiksi voidaan määritellä void. Se on funktio, joka ei tuota mitään arvoa.

Seuraavassa funktio printmean tulostaa argumenttiensa keskiarvon, mutta ei palauta kutsuvaan ohjelmaan mitään tietoja:


  #include <stdio.h>
  void printmean (float x, float y) ;

  main()
  {
    ...
    printmean (1.0, 2.0) ;
    ...
  }

  void printmean (float x, float y)
  {
    printf("keskiarvo = %f \n", (x+y)/2) ;
  }

Funktion paikalliset muuttujat

  #include <stdio.h>
  main()
  {
    float m ;
    m = mean(1.0, 2.0) ;
    printf("%f\n", m)  ;
  }

  float mean (float x, float y)
  {
    float s ;
    s = (x+y)/2 ;
    return s ;
  }

Funktiolla func on kaksi argumenttia, x ja y.

Lisäksi funktiossa määritellään paikallinen muuttuja s. Tämä on pelkästään funktion omass käytössä oleva muuttuja, johon pääohjelma tai muut funktiot eivät pääse käsiksi.

Funktio palauttaa arvonaan argumenttiensa keskiarvon.

Esimerkkiohjelma, versio 2:

  #include <stdio.h>
  float mean (float x, float y) ;
  main()
  {
    float m, s ;
    s = 10.0 ;
    m = mean(1.0, 2.0) ;
    printf("%f %f\n", m, s)  ;
  }

  float mean (float x, float y)
  {
    float s ;
    s = (x+y)/2 ;
    return s ;
  }

Sekä pääohjelmassa että funktiossa mean määritellään muuttuja s. Ne ovat edelleen paikallisia muuttujia ja toisistaan riippumattomia. Funktion sijoituslausen s= ... muuttaa funktion paikallista muuttujaa, mutta ei vaikuta pääohjelman muuttujaan.

Esimerkkiohjelma, versio 3:

  #include <stdio.h>
  float mean (float x, float y) ;
  main()
  {
    float m ;
    m = mean(1.0, 2.0) ;
    printf("%f %f\n", m, x)  ;
  }

  float mean (float x, float y)
  {
    float s ;
    x++ ;
    s = (x+y)/2 ;
    return s ;
  }

Nyt funktiossa lisätään argumentin x arvoa yhdellä. Mikä on pääohjelman tulostama muuttujan x arvo? Todellinen argumentti on vakio 1.0; voiko x++ muuttaa vakiota??

Funktion argumentit ovat C-kielessä aina arvoparametreja . Ne ovat funktion kannalta paikallisia muuttujia. Funktiota kutsuttaessa todellisen argumentin arvo lasketaan ja sijoitetaan tämän paikallisen muuttujan arvoksi.

Funktio voi käsitellä argumentteja kuten mitä tahansa muitakin paikallisia muuttujia. Vaikka funktiossa argumentin x arvoa muutetaan lauseella x++, se ei vaikuta millään tavoin pääohjelmaan.

Esimerkkiohjelma, versio 4:

  #include <stdio.h>
  float mean (float x, float y) ;
  main()
  {
    float m, x=1.0, y=2.0 ;
    m = mean(x, y) ;
    printf("%f %f %f\n", m, x, y)  ;
  }

  float mean (float x, float y)
  {
    float s ;
    x++ ;
    s = (x+y)/2 ;
    return s ;
  }

Todelliset argumentit ovat nyt pääohjelman paikallisia muuttujia. Funktion ei voi muuttaa niiden arvoja, vaikka niillä sattumalta onkin samat nimet kuin funktion muodollisilla argumenteilla.

Muunnetaan aikaisemmin esiintynyt harmonisen sarjan summaa laskeva ohjelma niin, että summa lasketaan funktiolla. Hetken mietittyämme päätämme, että funktiolle kannattaa välittää argumenttina raja, jota suuremmat termit otetaan mukaan.

   #include <math.h>
   #include <stdio.h>
   double harmonic (double limit) ;
   main() {
      double raja ;
      printf("anna raja:") ;
      scanf("%lf", &raja) ;
      printf("%lf\n", harmonic(raja)) ;
   }

   double harmonic (double limit)
   { double sum=0.0, term=1.0 ;
     int i=1 ;
     while (term > limit) {
       sum += term ;
       i++ ; 
       term = 1.0/i ;
     }
     return sum ;
   }

   > gcc -o harmon harmon.c -lm
   > ./harmon
   > anna raja:0.1
   > 2.828968

Nyt funktiota harmonic voidaan kutsua monta kertaa eri argumentin arvoilla ja tutkia, miten summa kasvaa, kun rajaa pienennetään.

Globaalit muuttujat

  #include <stdio.h>
  float degtorad = 0.017453,
        radtodeg = 57.29578 ;

  main()
  {
    float f, x, xd ;
    x = 15.0 * degtorad ;
    f = func(x) ;
    xd = f * radtodeg ;
    ...
  }

  float func(float x)
  {
    float s ;
    s = x + 0.5 * degtorad ;
    return s ;
  }

Muuttujat degtorad ja radtodeg ovat globaaleja muuttujia .

Globaalien muuttujien määrittelyt ovat kaikkien funktioiden ulkopuolella, yleensä ohjelman alussa.

Globaalit muuttujat kaikkien funktioiden käytettävissä.

Vakioiden määrittely (kuten edellä) voidaan tehdä turvallisemmin makrojen avulla (käsitellään myöhemmin).

Globaalien muuttujien käyttö: esimerkiksi suuret tietorakenteet, joita useat funktiot käsittelevät.

Edellä ollut harmonisen sarjan laskeva funktio ei kerro kutsuvalle ohjelmalle, montako termiä sarjasta laskettiin. Termien määrä pitäisi välittää jotenkin kutsuvaan pääohjelmaan. Funktio voi kuitenkin palauttaa vain yhden arvon, joten miten sieltä saadaan ulos kaksi lukua? Tähän on useita vaihtoehtoja, ainakin kolme.

1) Funktion arvo ei olekaan luku, vaan kokonainen tietue, joka sisältää sekä sarjan summan että termien lukumäärän. Tätä käsitetllään vähän myöhemmin.

2) Termien määrän ilmoittaa globaali muuttuja, jonka sekä pääohjelma että funktio näkevät.

   #include <math.h>
   #include <stdio.h>

   int termit ;
   double harmonic (double limit) ;

   main() {
     double raja, summa ;
     printf("anna raja:") ;
     scanf("%lf", &raja) ;
     summa = harmonic(raja) ;
     printf("%lf %d\n", summa, termit) ;
   }

   double harmonic (double limit)
   { double sum=0.0, term=1.0 ;
     int i=1 ;
     while (term > limit) {
       sum += term ;
       i++ ; 
       term = 1.0/i ;
   }
   termit = i-1 ;
   return sum ;
}

Tässä termit on globaali muuttuja, jonka arvoa funktio muuttaa.

3) Funktiolle välitetään toinenkin argumentti, jonka arvona termien lukumäärä palautetaan. Jotta argumentin avulla voitaisiin välittää tietoa ulos funktiosta, joudutaan käyttämään osoittimia.

   #include <math.h>
   #include <stdio.h>
   double harmonic (double limit, int *count) ;

   main() {
      double sum ;
      int termit ;
      sum = harmonic (0.001, &termit) ;
      printf("%d termiä, summa = %lf\n", termit, sum) ;
   }

   double harmonic (double limit, int *count)
   { double sum=0.0, term=1.0 ;
     int i=1 ;
     while (term > limit) {
       sum += term ;
       i++ ; 
       term = 1.0/i ;
     }
     *count = i ;
     return sum ;
   }

Pääohjelman muuttuja termit on kokonaislukumuuttuja, siis tarkkaan ottaen tuon muuttujan eli tietyn muistipaikan sisältämä luku. Merkintä &termit puolestaan tarkoittaa tuon muistipaikan osoitetta. Funktiolle ei siis välitetä muuttujan arvoa, vaan sen talletuspaikan osoite.

Funktiossa puolestaan esiintyy merkintä *count. Määrittelyn mukaan *count on kokonaisluku, joten count on jotakin muuta. Se on tuon kokonaisluvun talletuspaikan osoite. Merkki * tarkoittaa osoittimen osoittaman muistipaikan sisältöä. *count on siis se kokonaisluku, jonka osoite on count. Merkintätapa ja kutsun eroaminen prototyypistä saattaa aiheuttaa sekaannuksia, mutta asialle ei voi mitään.

Koska funktion arhumentit ovat arvoparametreja, funktio ei pääse käsiksi alkuperäisiin arvoihin. Tämä suojamuuri kierretään niin, että funktiolle välitetäänkin muuttujan talletuspaikan osoite. Funktion ei tarvitse muuttaa tätä osoitetta, mutta se pääsee käsiksi osoitteesta löytyvän muuttujan sisältöön.

Tämä on yksi niistä C-kielelle ominaisista konekielimäisistä ratkaisuista, jotka tekevät kielestä paikoitellen varsin vaikeaselkoista.


/* lasketaan neliojuuri iteroimalla */
#include <stdio.h>
#include <math.hgt;

float mysqrt(float x) ;
float limit=0.0001 ;

main()
{
  float x, sqrtx;
  printf("anna x:") ;
  scanf("%f",&x) ;
  sqrtx = mysqrt(x) ;
  printf("sqrt(%f)=%f\n", x, sqrtx) ;
}

float mysqrt(float x) 
{ 
  float x1, x0 ;
  x0 = 1.0 ;
  while(1) {
    x1 = 0.5 * (x0 + x/x0) ;
    if (fabs(x1-x0) < limit) break ;
    x0 = x1 ; 
  }
  return x0 ;
}

> gcc -o mysqrt mysqrt.c -lm
> ./mysqrt
anna x:9.0
sqrt(9.000000)=3.000092


Taulukot argumentteina

    int taulu[10] ;

Tämä määrittelee taulukon, joka sisältää kymmenen kokonaislukua.

Hieman (tai joskus aika paljonkin) hämmentävää on, että taulukon nimi on itse asiassa osoitin taulukolle varatun tilan alkuun. Siten seuraavat sijoituslauseet toimivat täsmälleen samalla tavoin:

      taulu[5] = 1;
      *(taulu + 5) = 1;

Oikeastaan jälkimmäisestä muodosta näkee selvemmin, mistä on kysymys, ja edellistä voi pitää vain sille sovittuna merkintänä, joka muistuttaa enemmän muiden kielten merkintätapoja.

Merkintä on sikäli hämäävä, että taulu\ on osoitin, joten tekisi mieli kirjoittaa sen eteen tähti, kun tarkoitus on kajota osoittimen osoittaman muuttujan sisältöön. Niin ei kuitenkaan saa tehdä.

Tämä vaatii tarkkaavaisuutta, kun taulukoita välitetään argumentteina funktioille. Koska funktiolle välitetään vain taulukon osoite, sen sisältöä ei kopioida funktion paikalliseksi muuttujaksi, mikä merkitsee merkittävää ajansäästöä, jos taulukko on hyvin iso.

Funktion määrittelystä ei näy, että argumenttina on oikeastaan taulukko, sillä varsinainen argumentti on vain osoitin. Seuraavassa esimerkissä määritellään funktio, jolla nollataan taulukko.

    void nollaa (int *t, int n) ;
    main()
    {
      int taulu[10] ;
      nollaa (taulu, 10) ;
      ...
    }
    void nollaa (int *t, int n) 
    { int i ;  
      for (i=0; i

Funktiota kirjoitettaessa on vain tiedettävä, että t on taulukko; funktion prototyypistä se ei näy millään tavoin.

/* lajitellaan taulukko nousevaan jarjestykseen */
#include <stdio.h>
void tulosta (int *t, int n) ;
void lajittele (int *t, int n) ;

main()
{
  int taulu[10] = { 1, 0, 2, 3, 5, 2, 4, 1, 7, 3 } ;
  lajittele (taulu, 10) ;
  tulosta (taulu, 10) ;
}

void tulosta (int *t, int n) 
{ int i ;  
  for (i=0; i gcc -o lajit lajit2.c
> ./lajit
0 
1 
1 
2 
2 
3 
3 
4 
5 
7 


Makrot

Makro määrittelee, että ohjelmaa käännettäessä jokin merkkijono korvataan jollakin toisella (yleensä ohjelman osalla). Makro on siis vain lyhennysmerkintä jollekin ohjelmakoodin palaselle.

C:ssä makro määritellään kääntäjän direktiivillä #define.

Aikaisemmin esiintyi muunnos asteista radiaaneiksi. Muunnoskerroin oli tavallinen muuttuja, jolle annettiin alkuarvo.

   float rad = 57.2958 ;
   ...
   x /= rad ;

Tämä ei ole turvallinen tapa, koska muuttuja rad on tavallinen muuttuja, jonka arvoa voidaan muuttaa.

Määritellään vakion arvo direktiivillä:

  # define rad 57.2958

Tämä on oikeastaan makron määrittely. Kun kääntäjä näkee symbolin rad, se korvataan luvulla 57.2958. Nyt rad ei ole muuttuja, joten sen arvoa ei voi muuttaa ohjelmassa.

Makroja voidaan myös käyttää taulukoiden koon määrittelyyn:

  # define N 100
  ...
  float taulu[N] ;
  ...
  for (i=0; i

Kun taulukon todellinen koko ei sellaisenaan esiinny missään kohtaa ohjelmaa, kokoa voidaan helposti muuttaa vain makromäärittelyä muuttamalla.

Makroilla voi olla myös argumentteja (parametreja). Muunnokset radiaaneiksi ja asteiksi voitaisiin määritellä makroina

  # define rad(x)  x / 57.2958
  # define deg(x)  x * 57.2958

Nyt näitä voidaan käyttää kulmamuunnoksissa:

   a = sin(rad(x)) ;
   z = deg(acos(a)) ;

Makron kutsu vaikuttaa samanlaiselta kuin funktiokutsu. Makro ei kuitenkaan ole funktio, vaan pelkkä lyhennysmerkintä. Kääntäjä vain korvaa makrokutsun sille define-direktiivillä määritellyllä tekstillä. Jos nyt kirjoitetaan

   a = rad(x + y) ;

tästä generoituu ohjelma

   a = x + y / 57.2958 ;

ja tulos ei luultavasti ole toivottu. Ongelma vältetään lisäämällä määrittelyyn sulut:

  # define rad(x)  (x) / 57.2958
  # define deg(x)  (x) * 57.2958

jolloin kutsu toimii kuten pitääkin:

   a = (x + y) / 57.2958 ;

Jos aikaisemmassa lajitteluesimerkissä käsitellään vain samankokoisia taulukoita, koko voidaan määritellä makrolla:


/* lajitellaan taulukko nousevaan jarjestykseen */
#include <stdio.h>
#define N 10
void tulosta(int *t) ;
void lajittele (int *t) ;

main()
{
  int taulu[N] = { 1, 0, 2, 3, 5, 2, 4, 1, 7, 3 } ;
  lajittele (taulu) ;
  tulosta (taulu) ;
}

void tulosta (int *t) 
{ int i ;  
  for (i=0; i


Kirjastot

Funktion ohjelmakoodi voidaan kirjoittaa samaan tiedostoon pääohjelman kanssa. Se ei kuitenkaan ole välttämätöntä, vaan funktiot voivat olla myös eri tiedostoissa. Ne voidaan myös kääntää erikseen ja tallettaa käännetyt versiot eli funktioiden objektikoodi. Silloin ne vain linkitetään mukaan pääohjelmaan.

Edellä olleen varsin triviaalin esimerkin aliohjelma voitaisiin kirjoittaa omaan tiedostoonsa. Olkoon tiedoston nimi vaikka funktio.c:

  int func (int x)
  {
    return 2*x + 1 ;
  }

Tämä käännetään komennolla

   gcc -c funktio.c

Valitsin -c tarkoittaa, että ohjelma vain käännetään, mutta ei linkitetä. Objektikoodi talletetaan tiedostoon funktio.o.

Pääohjelma voisi olla kuten edelläkin, nyt vain ilman funktiota. Olkoon tämä tiedostossa paaohjelma.c:


  int func (int x) ;

  main()
  {
    int y ;
    y = func(5) ;
    ...
  }

Käännöskomennossa on ilmoitettava, että mukaan linkitetään myös funktion objektitiedosto:

   gcc  funktio.o paaohjelma.c

Tämän jälkeen tuloksena on suoritettava tiedosto a.out.

Edellä esitetty menettely kaipaa vielä parantelua. Vaikka funktio onkin eri tiedostossa, sen prototyypin täytyy olla pääaohjelman tiedostossa. Muuten kääntäjä ei voi tietää, millaine kutsuttava funktio oikein on.

Erillisessä tiedostossa voi kuitenkin olla lukuisia funktioita, joten kaikkien prototyyppien kirjoittaminen näkyviin olisi hankalaa. Siksi onkin parempi koota prototyypit omaan tiedostoonsa. Tehdään tiedosto funktio.h:

  int func (int x) ;

Tiedoston nimen tarkenne .h viittaa juuri tällaisiin tiedostoihin (h=header).

Nyt tämä tiedosto saadaan käyttöön include-direktiivillä seuraavasti:

  #include "funktio.h"
  main()
  {
    int y ;
    y = func(5) ;
    ...
  }

Aikaisemmin on esiintynyt direktiivi

  #include <stdio.h>

Tässä tiedoston nimi on kulmasuluissa, kun se äskeisessä esimerkissä on lainausmerkeissä. Merkinnöillä on tietty ero. Kulmasuluissa olevat nimet viittaavat systeemikirjastoihin, joita haetaan tietyistä ennalta määrätyistä hakemistoista ja jotka tulevat käyttöjärjestelmän mukana. Lainausmerkeissä olevat tiedostot ovat yleensä käyttäjän omia, ja niiden sijaintipaikka täytyy ilmoittaa. Esimerkin tapauksessa tiedosto on samassa hakemistossa kuin pääohjelma, mutta lainausmerkkien sisällä voi olla myös mutkikkaampi hakupolku.

Funktiokirjaston merkittävä etu on tehtävän jäsentelyssä. Kirjastsoon voidaan koota yleiskäyttöisiä funktioita, joita useat eri ohjelmat tarvitsevat. Kirjastoa voidaan kehittää muista ohjelmista erillään, kunhan vain funktioiden kutsut ja vaikutukset määritellään tarkasti. Kirjastoihin voidaan myös siirtää jo testattuja ja toimivia funktioita, joihin ei ole tarvetta kajota.

Käännökseen ja linkitykseen tarvittavat komennot voidaan koota make-tiedostoksi.


C - lisää muuttujien määrittelyä

Kaksiulotteiset taulukot

Käyttöjärjestelmän ohjelmoija tarvitsee aniharvoin useampiulotteisia taulukoita. Siksi sellaisten käsittely C:ssä on jäänyt hivenen mutkikkaaksi. Numeerisissa sovelluksissa niitä sen sijaan tarvitaan tuon tuostakin.

Kaksiulotteisen taulukon määrittely voisi olla:

    float matriisi[3][4] ;

Tämä määrittelee taulukon, jossa on kolme vaakariviä ja neljä pystysaraketta. Tämä kuitenkin toteutetaan käytännössä yksiulotteisena taulukkona, jonka neljä ensimmäistä alkiota vastaavat ensimmäistä vaakariviä, neljä seuraavaa toista riviä jne. Taulukon nimi matriisi on tässäkin tapauksessa osoitin taulukon alkuun.

Esimerkiksi seuraavat lauseet toimivat samalla tavoin:

    matriisi[2][3] = 1.0 ;
    *(matriisi + 2*4 + 3) = 1.0 ;

Taaskin ensimmäinen muoto on oikeastaan vain lyhennemerkintä jälkimmäiselle.

Seuraavassa esimerkissä lasketaan matriisitulo C=AB:

   double A[3][3], B[3][3], C[3][3], sum ;
   int i, j, k ;
   ...
   for (i=0; i<3; i++)
   for (j=0; i<3; j++)
   { sum = 0 ;
     for (k=0; k<3; k++) sum += A[i][k] * B[k][j] ;
     C[i][j] = sum ;
   }

Merkkijonot

Koska C:n yksi käyttötarve on ollut Unixin käyttöliittymän kirjoittaminen, siihen on rakennettu hyvät työkalut juuri merkkijonojen käsittelyyn. Osoittimien avulla voidaan kätevästi tutkia ja muokata merkkijonoja. Lisäksi monet usein tarvittavat toimenpiteet on toteutettu kirjastofunktioina.

Merkkijono on oikeastaan vain taulukko, joka sisältää joukon peräkkäisiä merkkejä. Määrittely voisi olla esimerkiksi

    char nimi[30] ;

Merkkijonon yksittäistä merkkiä voidaan käsitellä kuten muidenkin taulukoiden alkioita:

    nimi[0] = 'a' ;
    nimi[1] = nimi[0] ;
    if (nimi[i] == 'z') ...

Kokonaista merkkijonoa ei voi kopioida toisen arvoksi yhdellä sijoitusoperaatiolla (kuten ei taulukkoakaan). Myöskään kahden eri merkkijonon samanlaisuutta ei voi tutkia yhdellä ==-operaattorilla. Molemmat voidaan toteuttaa helposti toistolauseilla, mutta näitä toimenpiteitä tarvitaan niin usein, että niiden suorittamista varten on olemassa joukko valmiita funktioita.

Merkkijonoja käsittelevät funktiot tunnistavat merkkijonon päättymisen merkistä \ 0 eli käytännössä merkkikoodista 0. Tämä tarkoittaa siis, että muistipaikan sisältö on 0; merkin '0' merkkikkodi on jotakin muuta.

Huomaa, että yksittäinen merkki esitetään yksinkertaisissa lainausmerkeissä ('a'). Sen sijaan "a" on merkkijono, jossa on yksi alkio ja nolla lopetusmerkkinä.

Merkkijono voidaan kopioida funktiolla strcpy. Funktiolla on kaksi argumenttia, joista ensimmäinen ilmoittaa, mihin merkkijono kopioidaan, ja toinen, mistä kopioidaan. Järjestys voi tuntua omituiselta, mutta se vastaa sijoituslauseen järjestystä, jossa muutettava suure on vasemmalla puolella ja sille annettava arvo oikealla. Toinen merkkijonojen kopiointifunktio on strncpy, jossa on vielä kolmas argumentti; se kertoo, kuinka monta merkkiä kopioidaan.

Kahta merkkijonoa voidaan verrata funktillla strcmp. Jos funktion arvo on nolla, merkkijonot ovat samoja. Jos arvo on negatiivinen, ensimmäinen argumentti on toista "pienempi" eli aakkosissa toisen merkkijonon edellä. Jos arvo on positiivinen, ensimmäinen argumentti on aakkosissa toista myöhempi. Jos halutaan verrata vain tiettyä määrää merkkejä merkkijonojen alkupäästä, käytetään funktiota strncmp.

Näiden merkkijonofunktioiden käyttö näkyy seuraavasta esimerkkiohjelmasta:

    #include 

    char nimi1[30], nimi2[30], nimi3[10] ;
    int i ;
    ...
    strcpy(nimi1, "huihai") ;
    strcpy(nimi2, nimi1) ;

    strncpy(nimi3, nimi1, 5) ;
    ...
    if((i=strcmp(nimi1, nimi2))==0)
      printf("merkkijonot ovat samoja\n") ;
    else (if i < 0) 
      printf("%s on aakkosissa ennen %s\n", nimi1, nimi2) ;

    if(strncmp(nimi1, nimi3, 2)==0)
      printf("merkkijonojen alut ovat samoja\n") ;

Esimerkki osoittimien käytöstä merkkijonojen käsittelyssä.

#include 
main()
{
  char s[50], c, *p ;
  int i ;

  fgets(s,50,stdin) ;

  for(p=s; (*p==' ')&&(*p!=0); i++, p++) ;
 
  printf("%c\n",s[i]) ;

  for( ; (*p!=' ')&&(*p!=0); i++, p++) ;
  for( ; (*p==' ')&&(*p!=0); i++, p++) ;

  printf("%c\n",s[i]) ;

}


>./a.out
  abc   dfg
a
d
>

Funktio fgets lukee korkeintaan 50 merkin rivin taulukkoon s. Osoitin p osoittaa aluksi taulukon alkuun; sitä siirretään eteenpäin, kunnes se osoittaa ensimmäistä välilyönnistä eroavaa merkkiä, joka tulostetaan.

Seuraavaksi etsitään seuraava välilyönti, ja sitten taas ensimmäinen välilyönnistä eroava merkki.

Omat muuttujatyypit

Useita muuttujia voidaan paketoida tietueeksi, jota sitten voidaan käsitellä yhtenä kokonaisuutena. Seuraavassa esimerkissä määritellään tietuetyyppi, jolle annetaan nimeksi kaupunki:

    struct kaupunki 
      { 
        float pituus, leveys ;
        char nimi[20] ; 
      } ;

Tämä määrittelee vasta, millainen tietue on kyseessä, muttei varaa sille lainkaan tilaa. Määrittelyn jälkeen voidaan luoda muuttujia, jotka ovat tyyppiä kaupunki esimerkiksi seuraavilla tavoilla:

    struct kaupunki town,
                    hesa = {25.0, 60.0, "Helsinki" },
                    turku = {22.3, 60.4, "Turku" } ;
    struct kaupunki kaupungit[100] ;

Ensimmäinen näistä varaa tilan muuttujalle, muttei anna sille mitään alkuarvoa, toisessa asetetaan myös tietueen komponenttien alkuarvot luettelemalla ne samassa järjestyksessä kuin tietueen määrittelyssä. Kolmas määrittely varaa taulukon, johon mahtuu sadan kaupungin tiedot.

Tietueen arvot voidaan kopioida toiseen tietueeseen yhdellä sijoituslauseella:

    kaupungit[1] = hesa ;

Tietueen komponentteja voidaan käsitellä erikseen tavallisten muuttujien tapaan. Niihin viitataan operaattorilla ., jota seuraa komponentin nimi:

    town.pituus = 30.0 ; town.leveys = 63.0 ;
    kaupungit[2].pituus = 20.0 ;
    z = kaupungit[1].leveys ;
    

Tietuetyyppi voi esiintyä myös funktion arvona. Seuraavassa määritellään kompleksilukutyyppi ja funktio add, joka laskee argumentteina annettujen kompleksilukujen summan ja palauttaa sen arvonaan. Tämä arvo voidaan sitten sijoittaa kompleksulukutyyppiseen muuttujaan.

    struct complex { float re, im ; } ;
    struct complex z, u, v = {1.0, 0.0}  ;
    u = {0.5, 1.0} ;
    z = add(u, v) ;
    ...
    
    struct cmplx add (struct cmplx u, struct cmplx v)
    {
      struct cmplx val ;
      val.x = u.x + v.x ;
      val.y = u.y + v.y ;
      return val ;
    }

Joissakin C:n toteutuksissa kompleksityyppi on jo valmiina, jolloin ohjelma voitaisiin kirjoittaa lyhyesti:

    complex z, u, v = 1.0  ;
    u = 0.5 + I ; 
    z = u + v ;

Tyyppimuunnokset

Aritmeettisissa lausekkeissa muuttujien tyypit muunnetaan automaattisesti.

  int i ;
  float x ;
  i = 5/2 ;
  x = 5/2 ;

Jakolaskun 5/2 operandit ovat kokonaislukuja, joten tulos on myös kokonaisluku, jakolaskun kokonaisosa (2). Asiaan ei vaikuta, vaikka sijoituslauseen vasemmalla puolella olisi reaaliluku. Ohjelman jälkeen muuttujan i arvo on 2 ja muuttujan x 2.0.

Jos vaaditaan reaalilukujen jakolaskua, on kirjoitettava esimerkiksi

  x = 5.0/2 ;

Muuttujan tai vakion tyyppi voidaan muuttaa eksplisiittisesti:

  int i, j ;
  float x ;
  x = (float)i / j ;

Nyt i muunnetaan reaaliluvuksi, joten jakolasku tapahtuu reaalilukujen jakolaskuna. Varoitus. Seuraava toimii eri tavalla:

  x = (float)(i / j) ;

Tässä lasketaan ensin jakolasku kokonaisjakona, jonka tulos on kokonaisluku. Vasta osamäärä muunnetaan reaaliluvuksi. Jotta jakolasku suoritettaisiin reaalilukujen jakolaskuna, ainakin toinen operandi on muunnettava reaaliluvuksi.

Vastaavasti (int) muuntaa luvun kokonaisluvuksi. Muunnos reaaliluvusta kokonaisluvusta on katkaiseva; desimaaliosa heitetään pois ja luku pyöristetään nollaa kohti: 1.9 -> 1, -2.2 -> -2.


Syöttö ja tulostus

Funktiot scanf ja printf lukevat tiedostoa stdin eli käytännössä näppäimistöä ja tulostavat tiedostoon stdout eli kuvaruudulle.

Nämä ovat jo varsin korkean tason funktioita, sillä ne huolehtivat mm.\ lukujen muuntamisesta koneen sisäisen esitystavan ja ihmisen ymmärtämien desimaalilukujen välillä.

Alkeellisimpia funktioita ovat getchar, joka lukee seuraavan merkin ja putchar, joka tulostaa yhden merkin. Niiden avulla voitaisiin toteuttaa muut I/O-funktiot.

Kaikista näistä on myös versiot, jotka käsittelevät päätteen sijasta annettua tiedostoa.

Esimerkiksi windows-koneelta tulevissa tiedostoissa rivien erottimena on joskus merkki, joka emacsissa näkyy merkkeinä \^ M (ctrl-m). Tämän merkin merkkikkoodi 10-järjestelmässä on 13. Nämä merkit voidaan vaihtaa Unixissa käytettäviksi koviksi rivinvaihdoiksi pienellä ohjelmalla:

/* muunnetaan dos/windows-järjestelmän rivinvaihdot koviksi */
#include 
main()
{ int c ;
  while ((c=getchar()) != EOF)
  { if (c== 13) putchar('\n') ;  else  putchar(c) ; }
}

Tässä while-silmukassa luetaan merkkejä yksitellen, kunnes tiedosto päättyy. Funktio getchar palauttaa normaalisti luetun merkkikoodin, mutta tiedoston päättyessä sen arvo on jokin kokonaisluku, joka ei ole kelvollinen merkkikoodi. Ohjelmoijan ei tarvitse tietää tätä lukua, sillä siihen voi viitata valmiiksi määritellyllä vakiolla EOF (end of file). Luettaessa tekstiä näppäimistöllä, tiedoston loppu voidaan ilmoittaa näppäilemällä ctrl-D.

Jos tiedosto ei päättynyt, tulostetaan luettu merkki sellaisenaan paitsi jos merkki oli rivinvaihto, jolloin tulostetaan Unixin mukainen rivinvaihto.

Jos käännetty ohjelma on tiedostossa rivinvaihto ja muunnettava teksti tiedostossa tiedosto.win, muunnos voidaan tehdä seuraavaan tapaan:

   rivinvaihto < tiedosto.win > tiedosto.unix

Muotoimet

Funktioiden scanf ja printf argumentteja käsiteltiin aikaisemmin vain hyvin ylimalkaisesti. Ensimmäinen argumentti on merkkijono, joka ohjaa muotoilua. Merkkijono voi sisältää tekstiä, joka tulostuu sellaisenna, mutta siinä voi olla myös ohjeita muuttujien tulostamista varten eli muotoimia. Muotoimet tunnistaa niitä edeltävästä \%-merkistä. Edellä esiintyi jo kaksi muotointa, \%d ja \%f.

Tavallisimpia muotoimia ovat:

%d kokonaisluku
%ld kaksoistarkkuuden kokonaisluku (long)
%f reaaliluku
%lf kaksoistarkkuuden reaaliluku (double)
%c merkki
%s merkkijono

Heti prosenttimerkin jälkeen voi olla kokonaisluku, joka ilmoittaa, kuinka monen merkin mittainen kenttä tulostettavalle muuttujan arvolle varataan. Esimerkiksi \%5d tarkoittaa, että muuttujan arvo tulostetaan viiden merkin mittaiseen kenttään. Jos tilaa ei tarvita näin paljon, muuttuja arvo tasataan viiden merkin kentän oikeaan reunaan. Ellei luku mahdu kenttään, tilaa laajennetaan tarpeen mukaan, joten kentän leveys ilmoittaa oikeastaan vain kentän minimileveyden.

Reaalilukujen muotoimilla voidaan ilmoittaa kentän kokonaisleveyden lisäksi desimaalien määrä. Esimerkiksi \%8.2f tarkoittaa, että luku tulostetaan kahdeksan merkin mittaisen kentän oikeaan reunaan siten, että luku esitetään kahden desimaalin tarkkuudella.

Levytiedostojen käsittely

Toistaiseksi on käsitelty vain sitä, miten luetaan tiedostoa stdin ja tulostetaan tiedostoon stdout. Käyttämällä syötön ja tulostuksen uudelleenohjausta nämä tiedostot voidaan kyllä liittää mihin tahansa muuhunkin tiedostoon. Usein kuitenkin eteen tulee tilanne, jossa ohjelman on luettava tietoa useista eri tiedostosta tai tulostettava useita tiedostoja.

Kaikista edellä esiintyneistä syöttö- ja tulostusfunktioista on myös versiot, jotka operoivat mielivaltaiseen tiedostoon. Valitettavasti niiden nimet ja argumenttien järjestys ovat hämmentävän epäjohdonmukaisia. Vaikka Unixin man-komento ei olekaan käyttäjäystävällinen, tässä tapauksessa sitä kannattaa käyttää, sillä siitä näkee nopeasti, millä tavoin näitä lukuisia eri funktioita oikein käytetään.

Jotta toimenpide voisi kohdistua mielivaltaiseen tiedostoon, tiedosto on jotenkin määriteltävä. Aluksi määritellään muuttuja, jonka tyyppi on tiedosto:

   FILE *fyle ;

Tiedosto on avattava ennen kuin sitä voi käsitellä. Tiedosto avataan funktiolla fopen. Esimerkiksi

    fyle = fopen("tiedot.dat", "r") ;

panee tiedostomuuttujan fyle osoittamaan todelliseen tiedostoon tiedot.dat. Funktion toinen argumentti ilmoittaa, että tiedosto avataan lukemista (r=read) varten. Jos kyseessä olisi tulostustiedosto, toinen argumentti olisi "w".

Jos tiedoston avaaminen onnistuu, fopen palauttaa osoittimen tiedostoon. Jos avaaminen ei onnistu, funktion arvo on NULL:

    FILE *fyle ;
    ...
    fyle = fopen("tiedot.dat", "r") ;
    if (fyle == NULL) {
       fprintf(stderr, "tiedoston avaaminen ei onnistu\n") ;
       exit(1) ;
    }
    fscanf(fyle, "%d", &n) ;

Jos tässä esimerkissä tiedoston avaaminen ei onnistu (tiedostoa ei ole olemassa tai sen suojaukset estävät sen lukemisen), tulostetaan virheilmoitus. Sopiva tulostustiedosto on stderr, joka useimmiten on sama näyttö, johon stdout viittaa.

Lopuksi ohjelman suoritus päätetään kutsumalla funktiota exit, mikä on kätevä tapa keskeyttää ohjelma väkisin missä kohtaa tahansa tarvitsematta palauttaa kontrollia pääohjelmaan ja sen loppuun. Funktion argumentti on paluukoodi, jonka ohjelma palauttaa komentotulkille.

Jokainen Unixin komento palauttaa paluukoodin; nolla tarkoittaa onnistunutta suoritusta ja nollasta poikkeavat arvot jotakin virhetilannetta.

Levytiedostoa luetaan funktiolla fscanf, jonka kutsu on muuten samanlainen kuin funktion scanf kutsu, mutta ensimmäisenä argumenttina on tiedostoon viittaava osoitin. Samalla tavoin fprintf  vastaa funktiota printf.

Funktion   getchar vastineita on kaksi, jotka toimivaat periaatteessa samalla tavoin. Niiden nimet ovat epäjohdonmukaiset fgetc ja getc. Funktion putchar vadstineet puolestaan ovat fputc ja putc. Jotta epäjohdonmukaisuus ei kärsisi, niiden kutsuissa tulostettava merkki on ensimmäisenä argumenttina ja tiedostoon viittaava osoitin vasta sen jälkeen toisena argumenttina.

Seuraavassa on esimerkkejä kaikkien näiden funktioiden kutsuista.

    FILE *infile, *outfile ;
    int c ;
    float x, y ;
    ...
    infile = fopen("syote.dat", "r") ;
    outfile = fopen ("tulos.dat", "w") ;

    fscanf(infile, " %f %f", &x, &y) ;
    fprintf(outfile, " x = %f8.2  y = %f8.2\n", x,y) ;
    c = fgetc(infile) ;
    fputc(c, outfile) ;

Ohjelman päättyessä kaikki tiedostot suljetaan automaattisesti. Tiedosto voidaan myös sulkea eksplisiittisesti funktiolla close:

    fclose (outfile) ;

Tämä on käytännöllistä, jos ohjelma käsittelee useita eri tiedostoja samaan tapaan. Kun jo käsitelty tiedosto suljetaan, sama osoitin voidaan panna osoittamaan seuraavaan tiedostoon uudella fopen-funktion kutsulla.

Esimerkki: Merkkijonojen lajittelu aakkosjärjestykseen. Määritellään taulukko, johon mahtuu 100 kappaletta 80 merkin rivejä. Merkkijonoja luetaan, kunnes tiedosto loppuu (tai painetaan ctrl-D:tä).

Huom: scanf("%s"...) lopettaa merkkijonon lukemisen välilyöntiin. Koko rivi voidaan lukea funktiolla fgets. Funktion arvo on NULL tiedoston loppuessa.

#include 
#include 
#include 

main()
{
  char rivi[100][80], tmp[80] ;
  int i, j, n ;

  n = 0 ;

  /* luetaan merkkijonoja, kunnes tiedosto loppuu */
  while (fgets(rivi[n],80,stdin) != NULL) n++ ;
  printf("luettiin %d riviä\n", n) ;

  /* lajitellaan rivit              */
  /* nyt vertailu funktilla strcmp  */
  /* ja sijoitus funktilla strcpy   */
  for (i=0; i< n; i++)
    for (j=i+1; j<n; j++)
      if (strcmp(rivi[i], rivi[j]) > 0)
         { strcpy(tmp, rivi[i]) ;
           strcpy(rivi[i], rivi[j]) ;
           strcpy(rivi[j], tmp) ;
         }

  /* tulostetaan lajiteltu taulukko */
  for (i=0; i< n; i++)
    printf("%s\n", rivi[i] );

}

Binääri- ja suorasaantitiedostot

Edellä on käsitelty vain muotoiltua (formatoitua) syöttöä ja tulostusta. Esimerkiksi syötetyt luvut esitetään merkkijonoina, mutta ne muunnetaan tietokoneen ymmärtämiksi binääriluvuiksi ja tulostuksessa takaisin merkkijonoiksi.

Muotoilu on kohtalaisen mutkikas prosessi. Jos on tulostettava suuri määrä tietoa tiedostoon, joka on tarkoitettu vain toisen ohjelman luettavaksi, sitä ei kannata muotoilla, vaan tulostetaan binääriluvut sellaisinaan.

Edellä tiedostot ovat olleet peräkkäistiedostoja: tietoja luetaan ja tulostetaan tietua (kuten rivi) kerrallaan järjestyksessä.

Esimerkiksi suurta tietokantaa (kuten puhelinluetteloa) ei kannata lukea kokonaan, kun etsitään jotakin tiettyä tietuetta. Tiedosto voi olla myös suorasaantitiedosto, jolloin luku- ja kirjoitusoperaatiot voivat kohdistua mihin kohtaan tahansa. Oikea paikka ilmoitetaan hakuindeksin avulla. Jotta haku indeksin perusteella olisi mahdollista, kaikkien tietuiden on oltava saman mittaisna.


Komentorivin argumentit

Esimerkiksi Unixin komennolle cat voidaan antaa komentorivillä tulostettavan tiedoston nimi:

   cat ohjelma.c

Tässä ohjelma.c on komentoriviargumentti. Komento suorittaa itse asiassa alunperin C-kielellä kirjoitetun ohjelman, joka tulkitsee kutsun perään kirjoitetut argumentit.

Samalla tavoin argumentteja voidaan välittää mille tahansa C-ohjelmalle. Omat ohjelmat toimivat aivan samalla tavalla kuin Unixin komennot. Tämä ei yleensä ole mahdollista muissa kielissä.

Pääohjelma on samanlainen funktio kuin muutkin, ja sille voidaan välittää argumentteja. Pääohjelman määrittelyssä sillä voi olla kaksi parametria: komentorivillä annettavien argumenttien lukumäärä ja taulukko, joka sisältää nämä argumentit merkkijonoina.

Tehdään oma version cat-ohjelmasta. Olkoon se mycat.c:

   #include 
   #include 

   FILE *input ;
   main(int argc, char *argv[])
   { int c ;

     if (argc != 2)
        { fprintf(stderr,"usage: mycat file\n") ; exit(1) ; }

     input = fopen(argv[1],"r") ;
     if (input == NULL)
        { fprintf(stderr,"cannot open file %s\n", argv[1]) ; 
          exit(2) ; 
        }

     while ((c=fgetc(input)) != EOF) putchar(c) ; 
     return 0;
}


Ensimmäinen ( argc) on kokonaisluku, joka kertoo komentorivin mrkkijonojen lukumäärän. Jos luku on 1, rivillä ei ole mitään muuta kuin ohjelman nimi; jos luku on 2, rivillä on lisäksi yksi argumentti jne.

Toinen argumentti on taulukko, joka sisältää komentorivillä olevat merkkijonot. Numerointi alkaa nollasta: argumentti 0 on ohjelman nimi, 1 ensimmäinen ohjelmalle välitetty parametri jne.

Toinen argumentti voitaisiin yhtä hyvin kirjoittaa myös muotoon char **argv. Kyseessä on joukko osoittimia, jotka osoittavat komentoriville kirjoitettujen merkkijonojen alkuun: argv[1] osoittaa ensimmäisen parametrin alkuun argv[2] toisen parametrin alkuun jne.

Esimerkin tapauksessa ohjelmalle halutaan välittää täsmälleen yhden tiedoston nimi, joten argumenttien määrän on oltava 2. Ellei niin ole, annetaan virheilmoitus ja lopetetaan ohjelman suoritus kutsumalla funktiota exit. Jos Unixin komento toimii normaalisti, se palauttaa arvon 0. Nollasta poikkeava arvo tarkoittaa virhetilannetta. Tämän periaatteen mukaisesti ohjelma palauttaa ykkösen, jos sitä kutsuttiin väärällä tavalla.

Hyvä käytäntö on tulostaa ohjelman lyhyt käyttöohje, jos komentorivi on jollakin tavoin virheellinen.

Seuraaavaksi yritetään avata tiedosto, jonka nimi välitettiin argumenttina. Jos tiedoston avaaminen ei onnistu (todennäköisin syy on, ettei tiedostoa ole), ohjelman suoritus lopetetaan virheilmoitukseen ja taaskin palautetaan nollasta poikkeava paluukoodi.

Ohjelma voidaan nyt suorittaa samalla tavoin kuin mikä tahansa Unixin komento:

    ./mycat tiedosto.txt

Komentorivin argumentit ovat aina pelkkiä merkkijonoja. Jos tarvitaan numeerisia arvoja, merkkijonoja vastaavat arvot on laskettava. Tähän on valmiita varusfunktioita:

atoi laskee merkkijonon esittämän kokonaisluvun arvon
atof laskee reaaliluvun (float) arvon
atol laskee kaksoistarkkuuden (double) luvun arvon

Funktioilla on argumenttina merkkijono (tai oikeastaan osoitin merkkijonon alkuun). Niitä voi käyttää ohjelmassa muutenkin, kun on laskettava merkkijonon esittämän luvun numeerinen arvo.

Esimerkki: Ohjelma, joka muuntaa päivämääriä juliaanisiksi ja takaisin. Annettujen argumenttien perusteella päätellään, kummasta on kysymys.

Ohjelmassa käytetyistä melko mutkikkaista kaavoista ei tarvitse tässä välittää. Tärkeintä on pääohjelman rakenne.

/*-------------------------------------------------------------*/
/* conversion between ordinary (Gregorian) and Julian dates    */
/*-------------------------------------------------------------*/
#include 
#include 
#include 
#include 
#include 

/* function prototypes ----------------------------------------*/
double julian0(int y, int m, int d) ;
long int day_number(int y, int m, int d) ;
void   gregor(long int dn, int *yy, int *mm, int *dd, int *wd) ;

/*-------------------------------------------------------------*/
/* main program; use the number of arguments                   */
/* to decide what to do                                        */
/*-------------------------------------------------------------*/
main(int argc, char **argv)
{
  double jd ;
  long int dn ;
  int y, m, d, yy, mm, dd, wd ;

  if (argc==2)                              /* JD to Gregorian */
  {
    jd = atol(argv[1]) ;
    dn = (long)(jd - julian0 (1900, 1,1)) ;
    gregor (dn, &y, &m, &d, &wd) ;
  }
  else if (argc==4)                         /* Gregorian to JD */ 
  {
    y = atoi(argv[1]) ;
    m = atoi(argv[2]) ;
    d = atoi(argv[3]) ;
    jd = julian0 (y, m, d) ;
    dn = day_number(y, m, d) ;
    gregor (dn, &y, &m, &d, &wd) ;
  }
  else
  {
    fprintf(stderr,"usage: jd y m d   OR  jd julian_date\n") ;
    exit(1) ;
  }

  printf("%d.%d.%d  wd=%d  jd=%10.1lf\n", y,m,d,wd,jd) ;

}

/*-------------------------------------------------------------*/
/* standard julian date                                        */
/*-------------------------------------------------------------*/
double julian0(int y, int m, int d)
{
  if (m <= 2)
  {  y-- ; m+=12 ; }
  return(floor(365.25*y) + floor(30.6001*(m+1)) + d +
         1720994.5 + 2.0 - y / 100 + y / 400) ;
}

/*-------------------------------------------------------------*/
/* day number                                                  */
/* dn = 0 <=> 1.1.1900                                         */
/*-------------------------------------------------------------*/
long int day_number(int y, int m, int d)
{
  return(367L*(y-1900)-7*(y+(m+9)/12)/4-3*((y+(m-9)/7)/100+1)/4+
         275L*m/9+d+3309L) ;
}

/*-------------------------------------------------------------*/
/* convert day number to an ordinary date                      */
/* wd: 1=mo, 2=tu, ..., 6=sa, 0=su                             */
/*-------------------------------------------------------------*/
void gregor(long int dn, int *yy, int *mm, int *dd, int *wd)
{
  long int j, y, m, d ;
  *wd = (int)(dn % 7) ;
  j = dn+693901L ;
  y = (4*j-1)/146097L ;
  j = (4*j-1)%146097L ;
  d = j/4 ;
  j = (4*d+3)/1461L ;
  d = ((4*d+3)%1461)/4 + 1 ;
  m = (5*d-3)/153 ;
  d = ((5*d-3)%153)/5 + 1 ;
  y = y*100+j ;
  if (m < 10)
      m+=3 ;
  else
     { m-=9 ; y++ ; }

  *yy = (int)y ;
  *mm = (int)m ;
  *dd = (int)d ;
}



Make

Jos ohjelmaan linkitetään useita kirjastoja, käännöskomento voi olla mutkikas. Se voidaan kirjoittaa komentotiedostoon, mutta parempi työkalu on make-komento.

Oletetaan, että pääohjelma on tiedostossa testiohj.c:

  #include 
  #include "funktio.h"
  main()
  {
    int y ;
    y = func(5) ;
    printf("%d\n",y) ;
  }

Funktion määrittely on tiedostossa funktio.c:

  int func (int x)
  {
    return 2*x + 1 ;
  }

funktio.h on funktion prototyyppi:

  int func (int x) ;

Tehdään tiedosto, jonka nimi on makefile:

#
#  make-tiedoston esimerkki
#  paaohjelma tiedostossa testiohj.c, 
#  mukaan linkitetaan funktio tiedostosta funktio.c
#
all: funktio.o testiohj

funktio.o: funktio.c funktio.h
	gcc -c funktio.c

testiohj: testiohj.c\
        funktio.o
	gcc -o testiohj testiohj.c funktio.o

clean: 
	rm testiohj funktio.o

Makefile-tiedostossa määritellään, miten tiedostot riippuvat toisistaan. Esimerkiksi funktion objektikoodi funktio.o riippuu tiedostoista funktio.c ja funktio.h. Jos näitä muutetaan, objektitiedosto on luotava kääntämällä lähdekoodi funktio.c. Make-tiedostossa annetaan ohje (gcc-komento), miten tämä tapahtuu.

Käännös tapahtuu nyt komennolla

  make

Komento tutkii tiedostojen aikamääreitä. Makefilen komennoista suoritetaan vain ne, jotka ovat tarpeen muuttuneiden tiedostojen vuoksi.

Make-tiedostossa voi olla mitä tahansa Unixin komentoja. Tiedosto voi sisältää erilaisia nimettyjä osioita, kuten edellä all ja clean.

Esimerkin komennolla

   make clean

voidaan siivota pois turhiksi käyneet binääritiedostot.


9 C - sekalaista

Virhetilanteet

Aikaisemmin mainittiin jo ikuinen silmukka, jonka toiminta jatkuu, kunnes se katkaistaan väkivalloin. Jos ohjelma suoltaa tavaraa ruudulle, se voidaan pysäyttää Ctrl-C -näppäimellä.

Ellei tämä onnistu, ohjelma voidaan aina tappaa Unixin kill-komennolla:

  • Mennään johonkin toiseen ikkunaan, joka toimii normaalisti.
  • Komennolla ps etsitään tapettavan ohjelman prosessin numero.
  • Ohjelma tapetaan komennolla kill -9 n, missä n on prosessin numero.

Ohjelma voi myös tehdä jonkin virheellisen toiminnon, joka aiheuttaa sen keskeytymisen.

Joillakin laitteistoilla ohjelma voi keskeytyä aritmeettiseen virheeseen, kuten nollalla jakamiseen tai yritykseen laskea negatiivisen luvun logaritmia. Myös yritys sijoittaa muuttujalle niin iso arvo, ettei se mahdu muuttujalle varattuun tilaan, voi keskeyttää ohjelman. PC:ssä näin ei yleensä käy, koska niissä käytetään laajennettua aritmetiikkaa, jossa esimerkiksi nollalla jaon tulos on ääretön (Inf) ja negatiivisen luvun logaritmi epäkelpo luku (Not a Number, NaN). Sellainen on kuitenkin yleensä merkki huonosti suunnitellusta algoritmista.

Varsin tavallisia ja usein hankalasti selvitettäviä ovat virheelliset muistiviittaukset. Taulukkojen indeksejä ei normaalisti tarkisteta, joten viittaus taulukkoon voi kohdistua myös taulukon ulkopuolelle. Kyseessä on tehokkuus: tarkitus edellyttäisi, että jokaisen taulukkoviittauksen yhteyteen lisätään koodia, joka tarkistaa indeksin olevan sallituissa rajoissa. C:ssä tämä ei edes ole mahdollista osoittimien takia.

Pahimmassa tapauksessa ohjelma jatkaa toimintaansa, mutta tulokset ovat tietysti mitä sattuu. Viittaus saattaa aiheuttaa myös virhetilanteen, joka kaataa ohjelman yleensä legendaariseen ilmoitukseen "segmentation fault".

Virhetilanteiden jäljittämiseen on erilaisia ohjelmia (debugger). Niiden käyttö on usein hankalaa. Usein helpointa on lisätä ohjelmaan tulostuksia, joiden avulla nähdään, missä kohta mennään pieleen.


Strukturoitu ohjelmointi

Miten hallita laajoja ohjelmointitehtäviä?

Jaetaan tehtävä pienempiin osiin (aliohjelmiin).

Top down -menetelmä

  1. Määrittele tarkasti, mitä tietoja ohjelma saa ja mitä sen täytyy niille tehdä.
  2. Jos tehtävä on riittävän yksinkertainen, kirjoita sen suorittava ohjelmakoodi.
  3. Muuten jaa tehtävä pienempiin osiin ja määrittele tarkasti kunkin osan tehtävä ja liitäntä muuhun ohjelmaan.
  4. Suorita vaiheet 1-4 erikseen kullekin osatehtävälle.

Bottom up -menetelmä

Työkalupakkauksen idea. Ohjelma jaetaan tasoihin, joista kukin toteutetaan alempien tasojen työkalujen avulla. (vrt. Unix)

Usein käytetään molempia yhdessä: Ylimmän tason logiikka jäsennellään top-down-menetelmällä, alemmilla tasoilla käytetään olemassaolevia kirjastoaliohjelmia.

Haja-ajatuksia ohjelmointityylistä

Tietyn tehtävän suorittava ohjelma voidaan toteuttaa hyvin monella tavalla. Onko jokin ratkaisu parempi kuin toinen? Mitään ehdottomia kriteereitä ei ole, vaan ainakin osittain ne riippuvat ohjelman käyttötarkoituksesta.

Käyttäjän kannalta tärkeää on ohjelman toimivuus ja raskaissa laskennallisissa tehtävissä myös tehokkuus. Ohjelmoijan ja ohjelman ylläpidon kannalta tärkeää on ohjelman selkeys ja ymmärrettävyys. Ohjelmointityyli on kuitenkin abstraktimpi ja vaikeammin määriteltävä asia kuin toimivuus tai tehokkuus.

Ohjelman on parempi tehdä yksi asia kunnolla kuin jotenkuten vähän sitä sun tätä. Ennen ohjelmointia on määriteltävä täsmällisesti, mitä ohjelmalta vaaditaan.

Käytetystä tekniikasta riippumatta strukturoitu ohjelmointi pyrkii ohjelman pilkkomiseen pieniin, helposti hallittaviin ja riittävän yksinkertaisiin moduuleihin.

Toimivuus
Ohjelman käyttäjän kannalta tärkeintä on, että ohjelma toimii oikein ja vaatimusten mukaisesti. Toimivuuteen voi katsoa kuuluvaksi myös ohjelman käytettävyyden.

  • Ohjelman on toimittava oikein kaikilla sallituilla syöttöaineistoilla.
  • Ohjelman on ilmoitettava virheellisistä syöttötiedoista ja varoitettava arvoista, jotka ovat luultavasti järjettömiä.
  • Jokaiseen ohjelmaan liittyy erilaisia rajoituksia. Jos ohjelmaa yritetään käyttää tilanteessa, johon se ei sovellu, ohjelman on varoitettava käyttäjää.
  • Tarkistusten on oltava niin kattavia, ettei ohjelma kaadu millään syöttöaineistolla, vaan päättyy hallitusti.
  • Käytön on oltava mahdollisimman helppoa ja havainnollista.
  • Ohjelman ei pidä vaatia sellaisia tietoja, jotka se voi itse laskea tai ottaa muuten selville.
  • Syöttötietojen tulee olla käyttäjän kannalta luontevassa muodossa.

Tehokkuus
Jos ohjelma on tarkoitettu esimerkiksi suurten numeeristen tehtävien ratkaisemiseen tai pitkiin simulointeihin, sen on oltava ennen kaikkea tehokas.

Ohjelmointityylistä voi joskus joutua tinkimään tehokkuuden kustannuksella, mutta yleensä selkeästi kirjoitettu ohjelma toimii myös tehokkaasti. Oikean ratkaisumenetelmän löytämisellä on suurempi merkitys kuin olemassaoleven koodin viilaamisella ja jippoilulla.

Tehokkuutta käsitellään enemmän numeriikan kurssilla.

Aliohjelmat
Aliohjelman pitäisi olla aina ymmärrettävissä kerralla yhtenä kokonaisuutena. Mitä monimutkaisempia kontrollirakenteita aliohjelma sisältää, sitä lyhyempi sen on oltava.

Kukin aliohjelma tekee yhden täsmällisesti määritellyn toimenpiteen. Tämä ei tarkoita, että toiminnon pitäisi olla yksinkertainen, pääasia on että se voidaan määritellä yksinkertaisesti, esim. "lajittele tiedosto", "etsi osittaisdifferentiaaliyhtälön ratkaisu, joka toteuttaa annetut reunaehdot", "todista Goldbachin konjektuuri". Samaan aliohjelmaan ei pidä kasata sekalaisia toimenpiteitä, jotka eivät kuulu loogisesti yhteen.

Aliohjelmien tulee olla toisistaan mahdollisimman riippumattomia. Globaalien muuttujien avulla tulisi välittää mahdollisimman vähän tietoa. Jos aliohjelmalla on hyvin paljon parametreja, jotakin voi olla vialla.

Kontrollirakenteet
C-kielessä on paljon keinoja esittää asiat hyvin lyhyesti. Ohjelman lyhyys ei ole itsetarkoitus, vaan johtaa usein vaikeaselkoiseen koodiin.

Tärkeintä on kirjoittaa ohjelma niin, että sen toiminta on helposti ymmärrettävissä.

Kommentit
Kommenttien avulla voit helpottaa niiden henkilöiden työtä, jotka joskus joutuvat muuttamaan tai tutkimaan ohjelmaasi. Tähän joukkoon kuulut myös itse.

Jokaisen ohjelmatiedoston alussa pitäisi olla selostus siitä, mihin ohjelmaa tai tässä tiedostossa olevaa ohjelmanosaa käytetään. Ohjelmaa kehitellessään tulee helposti tehneeksi tiedostoja, joilla on sellaisia havainnollisia nimiä kuin a, test, koe3, prog tai xx. Muutamaa päivää myöhemmin ei enää itsekään muista, mitä mikin niistä tekee.

Mikäli ohjelmaan on linkitettävä muissakin tiedostoissa olevia moduleita, alkukommenteissa olisi hyvä kertoa myös käännös- ja linkitysohjeet. Hyödyksi on myös tieto, mitä syöttö- ja tulostustiedostoja ohjelma mahdollisesti tarvitsee.

Jokaisen funktion ja aliohjelman alussa tulisi olla lyhyt selostus aliohjelman toiminnasta, sen parametreista, mahdollisista globaaleista muuttujista, sivuvaikutuksista ja funktion tapauksessa sen palauttamasta arvosta.

Sekalaista
Eristä laiteriippuvat osat erillisiksi aliohjelmiksi

Testaa rajatapaukset. Varo taulukkojen indeksien ylityksiä, ja laskureiden "yhdellä pielessä" tilanteita. Tarkkaile muuttujien arvojen erikoistapauksia, ja muita erikoistilanteita, jotka usein ovat virheiden syinä.

Alusta kaikki muuttujat, älä luota, että ne ovat nollia automaattisesti.

Varo pyöristysvirheitä: 10.0 * 0.1 on tuskin koskaan 1.0! Älä vertaa liukulukujen yhtäsuuruutta.


Siis ei näin:


#include <stdio.h>
#define S 10000
#define M 2800
#define A 2000
main()
{ int i, j, carry=0, arr[M+1];
  for (i=0; i<=M; ++i) arr[i] = A;
  for (i=M; i; i-=14) 
  { int sum=0;
    for (j=i; j>0; --j) 
    { sum=sum*j+S*arr[j];
      arr[j]=sum%(j*2-1);
      sum/=(j*2-1);
    }
    printf("%04d",carry+sum/S);
    carry=sum%S;
  }
}


31415926535897932384626433832795028841971693993751
05820974944592307816406286208998628034825342117067
98214808651328230664709384460955058223172535940812
84811174502841027019385211055596446229489549303819
64428810975665933446128475648233786783165271201909
14564856692346034861045432664821339360726024914127
37245870066063155881748815209209628292540917153643
67892590360011330530548820466521384146951941511609
43305727036575959195309218611738193261179310511854
80744623799627495673518857527248912279381830119491
29833673362440656643086021394946395224737190702179
86094370277053921717629317675238467481846766940513
20005681271452635608277857713427577896091736371787
21468440901224953430146549585371050792279689258923
54201995611212902196086403441815981362977477130996
05187072113499999983729780499510597317328160963185

Esimerkki isohkon ohjelman kehittelystä

Tehdään ohjelma joka piirtää tähtikartan, jossa näkyy koko horisontin yläpuolella oleva taivas. Erillisissä tiedostoissa on luettelot tähdistä, tähdistöjä esittävistä viivoista ja teksteistä.

Päätellään, että parametreina tarvitaan ainakin paikkakunnan leveysaste ja tähtiaika.

Aloitetaan luonnostelemalla pääohjelma

main
{
   if (argc < 3) help() ;

tarkista komentoriviargumentit: 
      st=tahtiaika, leveys, mahdolliset optiot ;

muodosta syottotiedostojen nimet ;
avaa tulostustiedosto ;

   writeprologue() ;
   
   plot(st, leveys) ;

   writeepilogue() ;

}

Pääohjelma kutsuu kolmea aliohjelmaa. Kaksi sisältää vain grafiikkatiedostoon tulevia vakiotekstejä.


void writeprologue()
{
  tulostetaan grafiikkatiedoston alkukamat
}

void writeepilogue()
{
  tulostetaan grafiikkatiedoston loppukamat
}

void plot(float st, float leveys)
{
  avaa tahtiluettelo; 
  ellei ole, anna virheilmoitus ja lopeta;
  
 
   while (getstar(&r, &d, &m))
   if (m <= rajamagnitudi))
   {
     project(r, d, &xxx, &yyy);

     /* jos horisontin ylapuolella, piirretaan */
     if (xxx > -100) plotstar(xxx, yyy, m, 0) ;
   }

   plotlines();

   plottexts() ;

}

Varsinaisen työn tekee plot, joka kutsuu edelleen useita aliohjelmia:

  • getstar lukee yhden tähden tiedot; arvo on 0, kun tiedosto loppuu
  • project laskee paikan paperilla sopivan karttaprojektion avulla
  • plotstar piirtää yhden tähden
  • plotlines piirtää tähdistöjä esittävät viivat
  • plottexts tulostaa kartan tekstit

Otetaan nyt kukin näistä erikseen käsittelyyn.

Funktio getstar lukee yhden tähden tiedot luettelosta. Jos tiedosto loppuu, funktio saa arvon 0, muuten 1. Tämä on jo niin yksinkertainen toimenpide, että funktion koodi voidaan kirjoittaa valmiiksi.


int getstar(float *ra, float *dec, float *mag)
{
   if (fscanf(f_in," %f %f %f", &r,&d,&m)>0)
   {
     *ra = r ;
     *dec= d ;
     *mag= m ;
     return 1 ;
   }
   else return 0;
}

Funktio project projisoi tähden kartalle käyttämällä pallotähtitieteen kaavoja. Kahden parametrin x ja y välityksellä funktio palauttaa tähden paikan kuvan koordinaatistossa.

void project(float r, float d, float *x, float *y)
{

   d = d rad ;
   r = r * 15 rad ;

   /* ch=cos(h)=cos(sidereal time - R.A.)  ---------------*/
   ch=cosT*(cra=cos(r))+sinT*(sra=sin(r)) ;
   u =cosL*ch*(cdec=cos(d))+sinL*(sdec=sin(d)) ;
   if (u < 0) { *x = -1000.0; return;}  ;

   /* zeniittietaisyys z  --------------------------------*/
   elev = asin(u) ;
   z = halfpi-elev ;
   z = z/halfpi ;

   /* atsimuutti ------ ----------------------------------*/
   sh=sinT*cra-cosT*sra ;
   argx=sinL*ch*cdec-cosL*sdec ;
   argy=sh*cdec ;
   u=atan2(argy,argx) ;
   xxx = z*sin(u) ;   yyy = -z*cos(u) ;}

   *x = (float) xxx ; *y = (float) yyy;
}

Tämä on jo aika mutkikas, mutta jo hallittavissa. Funktiossa esiintyy useita muuttujia, joita ei ole vielä määritelty. Tässä vaiheessa huomataan, että useita niistä kannattaa määritellä globaaleiksi, jolloin ne tarvitsee laskea vain kerran ohjelman alussa.

Näin jatketaan kirjoittamalla funktioita ja täydentämällä määrittelyjä, kunnes ohjelma on valmis.