C alkeet
Mureakuha
Varsinaisissa C-käsikirjoissa asioita selostetaan usein niin perusteellisesti, että aloittelevalle ohjelmoijalle olennaisen erottaminen epäolennaisesta voi tuottaa ongelmia. Tästä tekstistä on tarkoituksella jätetty pois paljon yksityiskohtia, koska niitä ei tarvita yksinkertaisten C-ohjelmien toteutuksessa.
C-ohjelman yleisrakenne
C-ohjelma koostuu varsinaisesta C-koodista ja ns. C-esikääntäjälle annettavista ohjeista, joita kutsutaan direktiiveiksi ja makroiksi. Esikääntäjä muokkaa ohjeiden perusteella alkuperäistä C-koodia. Kaikki esikääntäjän ohjeet alkavat symbolilla # (risuaita).
C-ohjelma voi sisältyä yhteen ainoaan tiedostoon tai jakautua usemman tiedoston kesken. Oletamme seuraavassa, ellei muuta kerrota, että ohjelma sisältyy yhteen tiedostoon.
C-koodi koostuu määrittelyistä ja lauseista. Määrittelyissä kuvataan käytettävät muuttujat, tyypit ja funktiot. Lauseissa kerrotaan, mitä ohjelman tulee varsinaisesti tehdä. Yleisperiaate on se, että jokainen muuttuja, tyyppi ja funktio on määriteltävä ennen, kuin sitä voidaan käyttää ohjelmassa.
Seuraavassa on pieni esimerkkiohjelma, joka havainnollistaa yllä selitettyjä asioita
#include <stdio.h> /* 1 */ #define PII 3.14159 /* 2 */ void laske(float r, float *ala, float *tilavuus) /* 3 */ { *ala = 4 * PII * r * r; *tilavuus = 4 * PII * r * r * r / 3; } int main(void) /* 4 */ { float pallon_sade, pallon_ala, pallon_tilavuus; printf("Anna pallon säde: "); scanf("%f", &pallon_sade); if (pallon_sade < 0) printf("Säde ei voi olla negatiivinen. \n"); else { laske(pallon_sade, &pallon_ala, &pallon_tilavuus); printf("Ala: %f \n", pallon_ala); printf("Tilavuus: %f \n", pallon_tilavuus); } return 0; }
Ohjelman alussa on kaksi direktiiviä (kommentit 1 ja 2), joista ensimmäinen ilmoittaa, että ohjelmassa tarvitaan standardikirjastossa stdio.h määriteltyjä käsitteitä ja toinen määrittelee luvulle 3.14159 tekstimuotoisen synonyymin PII. Näitä seuraa kahden funktion, laske ja main, määrittelyt.
Ensimmäisessä funktiossa (kommentti 3) määritellään parametrit (tai argumentit) r, ala ja tilavuus, jotka kaikki ovat tyypiltään reaalilukuja (float). Jälkimmäisessä funktiossa (kommentti 4) määritellään paikalliset muuttujat pallon_sade, pallon_ala ja pallon_tilavuus, jotka ovat samoin tyypiltään reaalilukuja. Määrittelyjä seuraa lauseosa kummassakin funktiossa. Ohjelmassa on lisäksi paljon yksityiskohtia, joita selitetään myöhemmin tarkemmin. C-kielessä ei ole rajoitettu sitä, miten määrittelyt ja lauseet sijoitetaan ohjelmatekstin riveille. Samalle riville saa kirjoittaa useita asioita ja tyhjiä rivejä saa käyttää. Tekstin saa aloittaa ja lopettaa mihin sarakkeeseen tahansa. Poikkeuksena tähän ovat esikääntäjän ohjeet, joiden tulee olla omalla rivillään. Hyvään ohjelmointityyliin kuuluu kuitenkin se, että ohjelma on ulkoasultaan siisti ja jonkin yhtenäisen periaatteen mukaan sisennetty vasemmasta laidasta.
Tunnukset
Tunnus on ohjelmassa määritelty nimi jollekin asialle, kuten muuttujalle, tyypille tai funktiolle. Tunnuksia koskevat seuraavat säännöt.
- Tunnuksen tulee alkaa kirjaimella tai alleviivausmerkillä _
- Tunnus saa sisältää kirjaimia, numeroita tai alleviivausmerkkejä _
- Kirjaimilla tarkoitetaan tässä englanninkielisiä aakkosia A..Z
- Tunnus kirjoitetaan aina yhteen
- Tunnuksen nimen tulee kuvata jotenkin sen käyttötarkoitusta
Näiden sääntöjen mukaan sallittuja tunnuksia ovat mm. Test_value, value_4, x, patarouva ja LOPPUTULOS. Tunnus Test-value ei ole kelvollinen, koska siinä on laiton väliviivamerkki. Tunnus 4_roses on laiton, koska se alkaa numerolla, ja tunnus OPPILAAN NIMI sisältää puolestaan kaksi sanaa, eikä siten ole sallittu. Sen sijaan tunnus OPPILAAN_NIMI olisi sallittu.
Tunnus voi olla miten pitkä tahansa, mutta kääntäjästä riippuen vain N ensimmäistä merkkiä ovat merkitseviä. ANSI-standardin mukaan N on vähintään 6, käytännössä yleensä vähintään 31. Käytännössä tämä tarkoittaa sitä, että jos erotuskynnys N olisi vaikkapa 8 merkkiä, niin tunnukset Taulukonalku ja Taulukonloppu olisivat ohjelmassa samaa asiaa merkitseviä tunnuksia, mitä ohjelmoija ei ehkä ole tarkoittanut.
Pienet ja suuret kirjaimet käsitetään C-kielisissä tunnuksissa eri merkeiksi. Näin ollen tunnukset jussi, Jussi ja JUSSI ovat eri tunnuksia. Hyvään ohjelmointityyliin kuuluu kuitenkin se, että ei käytä hyväkseen tätä mahdollisuutta, koska se vaikeuttaa ohjelman ymmärtämistä.
C-kielessä on lisäksi 32 varattua sanaa, joita ei saa käyttää tunnuksina. Näitä ovat eri ohjelmarakenteissa kiinteästi esiintyvät sanat, kuten while, if, else, for, int, float, char... (huomaa, että varatut sanat kirjoitetaan aina pienellä).
On olemassa joukko sääntöjä, milloin kahdelle eri C-ohjelman käsitteelle saa käyttää samaa tunnusta. Seuraava yleissääntö on riittävä useimpiin tilanteisiin: Samassa funktiossa ei tule määritellä kahta samannimistä tunnusta.
Tietotyypit
Jokaisella ohjelman käsittelemällä tietoalkiolla on tyyppi, joka kertoo, miten kyseinen tietokoneen muistissa oleva tieto on tulkittava. Sama bittiyhdistelmä voi tarkoittaa esim. kokonaislukua, reaalilukua tai merkkijonoa. Tässä tekstissä käsittelemme vain yksinkertaisia tyyppejä, joissa jokainen tietoalkio on ohjelman kannalta yksittäinen atominen kokonaisuus. Rakenteisia tyyppejä ovat taulukot ja tietueet, joita käsitellään luentokurssin puolivälissä.
Perustietotyyppejä ovat kokonaisluvut, reaaliluvut ja merkkitieto. Näillä on useita eri alityyppejä sen mukaan, mikä on kyseisen tyypin fyysinen esitysmuoto tietokoneessa.
Kokonaislukutyyppejä C-kielessä esittävät tyypit int, long int ja short int. Ne eroavat toisistaan siten, että niiden esittämät lukualueet ovat erilaiset. Tavallisesti short int tarkoittaa 16 bitin kokonaislukuja, jolloin lukualue on -32768..32767, ja long int 32 bitin kokonaislukuja, jolloin lukualue on -2147483648.. 2147483647. Tyyppi int voi tarkoittaa kumpaa tahansa, ja voi vaihdella C-järjestelmästä toiseen. Siitä syystä on aina syytä käyttää tyyppiä long int, jos muuttujan arvo mahdollisesti menee alueen -32768..32767 ulkopuolelle.
Määritteet short int ja long int voidaan esittää myös lyhemmässä muodossa short ja long. Esimerkiksi seuraavat määrittelyt ovat merkitykseltään samoja.
short int laskuri; short laskuri;
Reaalilukutyyppejä ovat float ja double, joista edellinen tarkoittaa tyypillisesti 32-bitin reaalilukuja ja jälkimmäinen 64 bitin (kaksoistarkkuuden) reaalilukuja. Tyyppien arvoalueet voivat vaihdella riippuen tietokoneesta ja C-järjestelmästä, mutta yleisesti arvoalue on vähintään 10-37..10+37 ja yksinkertaisen tarkkuuden luvut esitetään vähintään 6 numeron ja kaksoistarkkuuden luvut vähintään 10 numeron tarkkuudella. Käytännössä käytetään yleensä tyyppiä double.
Reaalilukuvakiot esitetään C-kielessä aina käyttäen desimaalipistettä, esim. 3.14159. Lukuun voi liittyä myös eksponenttiosa, joka alkaa kirjaimella 'e' tai 'E' ja jota seuraa kymmenen potenssi, jolla luvun alussa oleva desimaalinen mantissa kerrotaan. Esimerkiksi luku 1.045E2 tarkoittaa lukua 1.045 kertaa 10^2 eli lukua 104.5. Reaalilukuvakiot ovat oletusarvoisesti tyyppiä double.
Merkkityyppi char esittää yksittäisiä merkkejä tietokoneessa käytetyn merkkikoodin, esim. ASCII-koodin mukaisesti. Jokaista koodissa esitettyä merkkiä vastaa jokin kokonaisluku, jonka arvo tallennetaan muistiin, kun halutaan esittää ko. merkkiä. Esimerkiksi ns. 7-bittisessä ASCII-koodissa kirjainta 'A' vastaa koodinumero 65, numeromerkkiä '8' koodi 56 ja merkkiä '+' koodi 43. Kaikkia koodiarvoja ei vastaa jokin näkyvä merkki.
Merkkityyppi char vastaa tietokoneen muistissa yleensä tavun käsitettä. Sitä voidaan käyttää myös pienten kokonaislukujen esittämiseen, jos tiedetään, että lukuarvot ovat välillä 0-127.
Merkkivakiot esitetään C-ohjelmassa yksinkertaisten lainausmerkkien sisällä, esim. 'A', '='. Joillekin tärkeille merkeille, joille ei ole olemassa näkyvää vastinsymbolia, on oma esitysmuotonsa. Esimerkiksi '\n' tarkoittaa rivinvaihtomerkkiä ja '\t' tabulointimerkkiä.
Merkkijonovakiot ovat useista merkeistä koostuvia kokonaisuuksia, joita käsitellään yhtenä vakiona. Ne erotetaan koodista kaksoislainausmerkeillä, esimerkiksi: "Otsikko".
Kommentointi
/* ... */ on yleinen C:n kommentti. Varsinaisesti C kieleen ei kuulu //-kommenttimerkki mutta nykyään ei ole olemassa ainuttakaan kääntäjää joka ei sitä ymmärtäisi joten sitä voi myös käyttää. Esimerkiksi
int main(void) // tämä teksti on kommenttia { int sade, tilavuus; /* tämä on c-kielen kommentti */ printf("Oma maa mansikka jne."); return 0; }
Muuttujien määrittelyt
Muuttuja määritellään siten, että ensin esitetään sen tyyppi ja sitten muuttujalle annettava tunnus. Määrittely päättyy puolipisteeseen.
int luku; float keskiarvo; char merkki;
Useita samantyyppisiä muuttujia voi määritellä samassa määrittelyssä, kun niiden tunnukset erotetaan pilkuilla:
short int laskuri, i; long int luku, arvo, tulos; double x,y,z;
Eräissä tilanteissa C-järjestelmä asettaa automaattisesti muuttujille alkuarvoksi nollan. Muulloin muuttujan alkuarvo voi olla mitä tahansa. Tästä syystä hyvään ohjelmointityyliin kuuluu se, että ohjelmoija huolehtii aina itse alkuarvon asettamisesta. Alustus voi tapahtua joko määrittelyn yhteydessä tai ohjelman lauseosassa ennen, kuin muuttujan arvoa käytetään mihinkään.
Jos muuttuja alustetaan määrittelynsä yhteydessä, tämä tapahtuu asettamalla määrittelyssä muuttujan tunnuksen jälkeen yhtäsuuruusmerkki ja haluttu alkuarvo. Esimerkki:
long luku = 10; float x = 0.0, y = 0.0; char merkki = 'A';
Lauseet ja lausekkeet
Lause on ohjelmassa oleva toimintaohje, joka määrittelee, mitä tietyssä tilanteessa tulee tapahtua. Lauseke on kaava, jonka arvon laskeminen tapahtuu tietyssä järjestyksessä. Lausekkeita käytetään monien C-kielen lauseiden osina, mutta niistä voidaan myös muodostaa itsenäisiä lauseita asettamalla niiden perään puolipiste.
Sijoituslause
Sijoituslausetta käytetään muuttujien arvojen vaihtamiseen, esimerkiksi:
weight = 90;
Em. sijoitus luetaan "muuttuja weight saa arvon 90" ja se EI siis tarkoita vertailua "weight on yhtäsuuri kuin 90".
Sijoituslauseen yleinen muoto on:
<muuttuja> = <lauseke>;
missä <lauseke> voi olla monimutkainen kombinaatio muuttujista, operaattoreista ym. operaatioista, esimerkiksi
x = (a + b) * sin(c);
Huomaa lauseen lopussa oleva puolipiste.
Symboli '=' on operaattori, joka sijoittaa oikealla puolellaan olevan lausekkeen arvon vasemmalla puolellaan olevan muuttujan arvoksi. Tästä syystä alla oleva ilmaus on sallittu, vaikka se näyttää matemaattisesti epätodelta. C-kielessä seuraava merkintä merkitsee, että otetaan muuttujan vanha arvo, lisätään siihen 1 ja sijoitetaan tulos muuttujan weight uudeksi arvoksi.
weight = weight + 1;
Alla oleva ilmaus on kielletty, koska 90 on vakio.
90 = weight;
Vastaavasti kielletty on seuraava tilanne, koska arvo on määritelty vakioksi:
#define arvo 1 arvo = 0;
Samassa sijoituslauseessa voidaan tehdä useita peräkkäisiä sijoituksia, esimerkiksi:
a = b = c = 0;
Sijoitukset tehdään tässä tilanteessa oikealta vasemmalle.
Yksinkertainen syöttö ja tulostus
Muuttujien ja vakioiden arvoja voidaan tulostaa päätteelle (kuvaruudulle) käyttäen funktiota printf, joka löytyy standardikirjastosta stdio.h. Tätä varten tulee ohjelman alussa olla määrittely
#include <stdio.h>
Funktion printf yleinen muoto on seuraava:
printf("formaatti", arvo_1, ..., arvo_n);
Argumentit arvo_1, arvo_2, ... ovat tulostettavien muuttujien, vakioiden tai lausekkeiden arvoja. Formaatti on tulostusta ohjaava merkkijono, joka määrittää, miten argumenttien arvot tulostuvat. Usein formaatti sisältää myös tekstiä, joka selittää tulostuksen sisältöä, esimerkiksi:
printf("Muuttujien arvot ovat %d ja %f \n", luku, keskiarvo);
Formaatissa olevat tulostuksen ohjauskomennot alkavat merkillä %. Sitä seuraava ohjauskirjain määrittää, minkä tyyppisestä tulosteesta on kysymys. Eräitä vaihtoehtoja ovat:
- %d kokonaisluku
- %ld long int tyyppinen kokonaisluku
- %c merkki
- %s merkkijono
- %f reaaliluku ilman eksponenttiosaa
- %e reaaliluku eksponenttiosan kanssa
- %g reaaliluku joko eksponenttiosan kanssa tai ilman sitä riippuen luvun arvosta
%-merkin ja ohjauskirjaimen välissä voi olla kokonaisluku, joka kertoo kuinka leveään kenttään (kuinka monen merkin levyiselle alueelle) tulostus sijoittuu. Tulostus tulee oletuksena kentän oikeaan laitaan, eli jos kentän leveys ylittää tulosteen pituuden, tulee kentän alkuun välilyöntejä. Jos pituusmäärettä ei ole, tulostus vie juuri sen verran merkkejä kuin on tarpeen luvun tai muun tulosteen esittämiseen. Esimerkkejä:
printf("Suureen arvo on %d yksikköä \n", luku); printf("Suureen arvo on %5d yksikköä \n", luku); printf("Suureen arvo on %10d yksikköä \n", luku);
Jos muuttujalla luku on arvo 123, tulostuu näkyviin
Suureen arvo on 123 yksikköä Suureen arvo on 123 yksikköä Suureen arvo on 123 yksikköä
Tulostettaessa reaalilukuja on usein tarpeen määritellä myös tulostettavien desimaalien määrä. Se kerrotaan kentän pituuden jälkeen muodossa .d . Esimerkkejä:
printf("Suureen arvo on %f yksikköä \n", arvo); printf("Suureen arvo on %5.1f yksikköä \n", arvo); printf("Suureen arvo on %10.2e yksikköä \n", arvo);
Jos muuttujan arvo arvo on 12.345, tulostuu näkyviin.
Suureen arvo on 12.345000 yksikköä Suureen arvo on 12.3 yksikköä Suureen arvo on 1.23e+01 yksikköä
Tulostettavien argumenttien tyyppien tulee vastata määriteltyjä tulostustyyppejä. Jos näin ei ole, voi tulostua näkyviin mitä tahansa tai ohjelma saattaa keskeytyä tai käyttäytyä muuten kummallisesti.
Muuttujien arvo voidaan lukea päätteeltä (näppäimistöltä) käyttäen standardi-funktiota scanf, mikä on määritelty C-kielen kirjastossa stdio.h. Tämän funktion muoto on sama kuin printf-funktiolla paitsi, että scanf joutuu myös muuttamaan argumenttiensa arvoja. Siksi välitetäänkin argumenttien osoitteet:
scanf("formaatti", &muuttuja1, ..., &muuttuja1);
Siis jokaisen muuttujan edessä, jolle aiotaan lukea arvo, tulee olla merkki &, mikä kertoo, että kyseessä on muuttujan muistiosoite eikä muuttujan arvo. Kaikkien argumenttien tulee olla muuttujia. Vakioille tai lausekkeille ei voida lukea uutta arvoa. Formaatissa annetun tyyppikirjaimen ja muuttujan tyypin tulee vastata toisiaan.
Esimerkki:
int luku; float arvo; char ch; scanf("%d %f %c", &luku, &arvo, &ch);
Väärin:
int i; scanf("%d", i);
Scanf-funktion formaatissa voidaan myös määritellä syötteen muotoa hieman samaan tapaan kuin printf-funktiossakin. Yksinkertaisissa ohjelmissa riittää kuitenkin se, että annetaan vain muuttujan tyyppiä vastaava ohjauskirjain.
Scanf lukee syötevirtaa (päätteeltä annettavia merkkejä), kunnes se on saanut kaikille muuttujille arvon. Syötettävät numeeriset argumentit erotetaan toisistaan välilyönneillä tai rivinvaihdoilla. Syötettäessä merkkitietoa scanf lukee kuitenkin heti seuraavan merkin muuttujan arvoksi. Scanf-funktio palauttaa arvonaan niiden muuttujien lukumäärän, joille onnistuttiin lukemaan arvo. Tätä tietoa voi käyttää hyväksi tutkittaessa, tapahtuiko syötteissä jokin virhe.
Jos luettava tiedosto loppuu (päätteellä annetaan ctrl-D) ennen, kuin yhtää tietoa on pystytty lukemaan, scanf palauttaa stdio.h-kirjastossa määritellyn vakioarvon EOF (= tiedoston loppu).
Sekä printf- että scanf-funktioiden formaatit tarjoavat paljon erilaisia mahdollisuuksia syötön ja tulostuksen tarkkaan ohjaamiseen. Tässä on käsitelty näistä mahdollisuuksista vain pieni osa.
Lausekkeet
Operaattorit määrittävät, millä tavalla lausekkeen operandien, kuten vakioiden ja muuttujien arvoja yhdistellään lausekkeen arvon laskemisessa. C-kielessä on hyvin runsaasti operaattoreita, mutta seuraavassa käsitellään vain tärkeimpiä, joita ovat aritmeettiset operaattorit, vertailuoperaattorit ja loogiset operaattorit.
Aritmeettisia operaattoreita ovat
- + yhteenlasku
- - vähennyslasku
- * kertolasku
- / jakolasku
- % jakojäännös (vain kokonaislukujaossa)
Esimerkki:
weight = density * volume;
Kokonaislukujen jakolaskussa on tuloksena kokonaislukuosamäärä, ei desimaalilukua. Siten lausekkeen 3 / 2 arvona on 1, kun taas lausekkeen 3.0 / 2.0 arvona on 1.5.
Eri operaattoreilla on määrätty prioriteetti: operaatiot *, / ja % ovat samanarvoisia ja ne suoritetaan ennen + ja - -operaatioita. Esimerkiksi sijoituksessa
weight = 2 + 5 * 6;
weight saa arvon 32 eikä 42, koska kertolasku lasketaan ensin. Jos lausekkeessa on useita samanarvoisia operaatiota, ne suoritetaan vasemmalta oikealle (jollei muuta määritellä). Esimerkiksi sijoituksessa
weight = 11 * 2 % 3;
weight saa arvon 1 eikä 22.
Yhteen- ja vähennyslaskuoperaattoreita voidaan käyttää myös yksioperandisena, etumerkin luonteisena operaattorina. Täten sijoitus
negweight = - weight;
tarkoittaa samaa kuin
negweight = 0 - weight;
Oletuslaskujärjestyksestä voidaan poiketa käyttämällä sulkumerkkejä ( ), esimerkiksi näin:
weight = (2+5) * 6;
Vertailuoperaattoreita käytetään tiedon yhtäsuuruuden ja erisuuruuden toteamiseen, jolloin tuloksena on looginen totuusarvo. Näitä ovat:
- a == b tosi, jos a yhtäsuuri kuin b
- a != b tosi, a erisuuri kuin b
- a < b tosi, jos a pienempi kuin b
- a > b tosi, jos a suurempi kuin b
- a <= b tosi, jos a pienempi tai yhtäsuuri kuin b
- a >= b tosi, jos a suurempi tai yhtäsuuri kuin b
Näissä a ja b voivat olla mielivaltaisia lausekkeita, kunhan niiden vertailu on mielekästä. Huomaa, että kahdesta merkistä koostuvat operaattorien symbolit kirjoitetaan yhteen.
Totuusarvoisia lausekkeita voidaan yhdistellä monimutkaisemmiksi lausekkeiksi käyttämällä loogisia operaattoreita EI, JA sekä TAI, jotka C-kielessä ilmaistaan seuraavasti (tässä a ja b voivat olla mielivaltaisia totuusarvoisia lausekkeita.):
- ! a tosi, jos a on epätosi
- a && b tosi, jos a ja b ovat tosia
- a || b tosi, jos a tai b on tosi
Esimerkkejä:
Tutkitaan, onko muuttuja x välillä [0 ... 10)
(x >= 0) && (x < 10)
Tutkitaan, onko muuttuja x välin (0, 1) ulkopuolella
(x <= 0) || (x >= 1)
Sama asia voidaan ilmaista myös seuraavasti:
! ((x > 0) && (x < 1))
Huom. C-kielessä ei ole omaa totuusarvoa esittävää tietotyyppiä, kuten Pascalin tietotyyppi Boolean. C-ohjelmissa arvo 0 (ja nollaosoitin) tulkitaan epätodeksi ja kaikki muut arvot todeksi.
Aritmeettisten, vertailu- ja loogisten operaattorien keskinäinen prioriteettijärjestys on seuraava:
- !
- aritmeettiset operaattorit
- <, >, <=, >=
- ==, !=
- &&
- ||
C:ssä on paljon muitakin operaattoreita ja eri prioriteettitasoja on paljon, siksi kannattaa käyttää sulkuja. Esimerkiksi:
a < b || a == 5 && average(i,j,k) < 12
on samanarvoinen lauseke kuin
(a < b) || ((a == 5) && (average(i,j,k) < 12))
mutta jälkimmäisen ymmärtäminen on huomattavasti helpompaa.
Lauseke lasketaan pääsääntöisesti vasemmalta oikealle ja laskenta lopetetaan heti, kun tulos voidaan todeta varmuudella. Esimerkiksi seuraavassa lausekkeessa ei tapahdu nollalla jakoa, jos b:n arvo on 0, koska osalausekkeen (b!=0) arvo on tällöin epätosi ja silloin koko lausekkeen arvo on aina epätosi.
(b != 0) && (a/b > 20)
Hieman tyyppimuunnoksista
Usein aritmeettisissa lausekkeissa esiintyy eri tyyppisiä operandeja, kuten kokonaislukuja ja reaalilukuja tai eri pituisiksi määriteltyjä kokonaislukuja. C-kieli määrittää, mikä tulee tällöin koko lausekkeen arvon tyypiksi.
Perussääntö on se, että sijoituslauseen oikea puoli lasketaan ensin valmiiksi ja lausekkeen arvo muunnetaan sitten vasemman puolen edellyttämään muotoon. Tarkastellaan esimerkkeinä seuraavia sijoituksia eri muuttujiin:
double d; long i; char c; float f; d = 0.123456789; i = d;
Tapauksessa, jossa kokonaislukumuuttujaan sijoitetaan reaalilukumuuttujan tai -lausekkeen arvo, desimaaliosa yksinkertaisesti katkaistaan pois, eli muuttuja i saa arvon 5. Jos tulos ei mahdu kokonaisluvun lukualueeseen, tapahtuu virhe.
c = '0';
i = c;
Tässä sijoitetaan merkkimuuttujan arvo kokonaislukumuuttujaan ja tulokseksi tulee merkin koodiarvo, esimerkiksi käytettäessä ASCII-koodia muuttuja i saa arvokseen 48. Tällaista merkki- ja kokonaislukutiedon yhdistelyä tulee kuitenkin välttää.
f = d; /* f=0.123456 */
Sijoitettaessa kaksoistarkkuuden lukua yksinkertaisen tarkkuuden muuttujaan osa merkitsevistä numeroista katoaa.
i = 100;
f = i;
Sijoitettaessa kokonaislukuarvo reaalilukumuuttujaan, lukuarvo muunnetaan reaaliluvuksi, eli muuttuja f saa arvon 100.0. Osa merkitsevistä numeroista voi silloinkin kadota:
long i; i = 123456789; f = i; /* f = 1.234567E9 */
Lausekkeiden tuottaman arvon tyyppi määräytyy seuraavien sääntöjen mukaan:
1. Tyypit char ja short int muunnetaan ensin int-muotoon. Esim. jos c1 ja c2 ovat tyyppiä short, niin lausekkeen c1 + c2 tyyppi on int.
2. Jos molemmat operandit ovat samaa tyyppiä niin tämä on myös tuloksen tyyppi. Esim. int + int tuottaa tulokseksi tyypin int, float + float => float.
3. Jos operandit ovat erityyppisiä, arvoalueeltaan pienempi tyyppi muunnetaan ennen operaatiota suuremmaksi tyypiksi. Esim. int + float => float + float => float, tai int + long => long + long => long.
Ohjelmoija voi myös itse määritellä lausekkeen tyypin. Tämä tapahtuu asettamalla halutun tyypin nimi suluissa lausekkeen tai sen osan eteen. Esimerkkejä:
double ratio; ratio = 5 / 2;
Tulos on 2.0, koska kokonaisjako tuottaa arvon 2 ja se muutetaan reaaliluvuksi.
ratio = (double) 5 / 2;
Tulos on 2.5, koska tyypin muunnos suoritetaan ennen jakolaskua. Laskutoimitus ja muunnos tapahtuu seuraavasti:
(double) 5 / 2 = ((double) 5) / 2 = 5.0 / 2 = 2.5;
Seuraavassa lasketaan reaalukujakolaskussa osamäärä:
ratio = (int) 5.0 / (int) 2.0;
Kumpikin reaaliluku muutetaan ensin kokonaisluvuksi ja sitten tehdään kokonaislukujako, jonka tulos on 2. Tämä sijoitetaan sitten reaalilukumuuttujaan, jonka arvoksi tulee 2.0.
Valintalauseet
Valintalauseet ovat lauseita, joilla voidaan valita haluttu toimenpide kahdesta tai useammasta toiminnosta. C-kielessä on kaksi valintarakennetta, if-else-rakenne ja switch-rakenne, joista käsittelemme tässä vain edellistä.
If-lauseen yleinen muoto on esitetty alla. Jos suluissa oleva lauseke on tosi, niin suoritetaan lause1, muuten suoritetaan lause2. Huomaa puolipiste lauseen 1 lopussa ja sulut lausekkeen ympärillä.
if (lauseke) lause1; else lause2;
Esimerkki:
if (x < 0) printf("Muuttujan arvo ei saa olla negatiivinen \n"); else laske_suureen_arvo(x);
Puhuttaessa if-rakenteesta käytetään yleisesti seuraavia ilmauksia rakenteen eri osista: "if-lauseen ehto", "if-haara" ja "else-haara". If-lauseen yksinkertaisemmassa muodossa else-haara puuttuu.
if (x < 0) x = -x;
Jos if-tai else-haarassa halutaan suorittaa useita lauseita, ne kootaan yhteen lohkosulkeilla {}:
if (lauseke) { lause_1; ... lause_n; }
Jos ohjelman suoritus etenee tähän haaraan, suoritetaan kaikki ko. haarassa oleva lauseet. Ne voivat sisältää uusia if-lauseita, funktiokutsuja, jne. Ts. hyvinkin monimutkaisten lauseiden rakentaminen on mahdollista. Esimerkki:
if (x < 0) { printf("Virhe. Muuttujan arvo on negatiivinen. \n"); virhelaskuri = virhelaskuri + 1; } else { if (x < 10) kasittele_pieni_arvo(x); else kasittele_suuri_arvo(x); laskuri = laskuri + 1; }
Huomaa, että lohkosulkujen jälkeen ei tule puolipistettä.
Jos if-haarassa on toinen if-lause, johon kuuluu myös else-haara, on periaatteena se, että else-haara vastaa aina lähinnä edeltävää if-haaraa. Esimerkki:
if (x < 1000) if (x < 0) printf("x on negatiivinen. \n"); else printf("x on välillä 0-1000. \n");
Jos else-haaran tulee liittyä nimenomaan ulompaan if-lauseeseen, sisempi lause täytyy laittaa lohkosulkeiden sisään:
if (x< 1000) { if (x < 0) printf("x on negatiivinen. \n"); } else printf("x on vähintään 1000. \n");
Laittamalla else-haaraan uusi if-lause, voidaan muodostaa ns. ketjutettuja if-lauseita, jotka soveltuvat vaikkapa aineiston luokitteluun:
if (luku < 0) negatiiviset = negatiiviset + 1; else if (luku < 10) yksinumeroiset = yksinumeroiset + 1; else if (luku < 100) kaksinumeroiset = kaksinumeroiset + 1; else suuret = suuret + 1;
Toistolauseet
Toistolauseet suorittavat samaa toimintoa uudelleen niin kauan, kuin toistoa kontrolloiva ehto on voimassa. C-kielessä on kolme eri rakennetta, joiden avulla toistolause voidaan rakentaa.
while-lause toistaa lausetta niin kauan, kuin ehtolauseke on tosi. Lauseen yleinen muoto on seuraava (huomaa sulut lausekkeen ympärillä):
while(lauseke) lause;
Jos toistettavia lauseita on useita, ne kootaan lohkosulkujen sisään.
Esimerkki: Seuraava koodi laskee syöterivin pituuden. Rivi päättyy rivinvaihtomerkkiin '\n' ja standardifunktio getchar palauttaa seuraavan merkin rivillä (huom, getchar palauttaa tyypiä int olevan tiedon).
int count = 0; int ch = ' '; ch = getchar(); while (ch != '\n') { ch = getchar(); count = count + 1; }
Jos while-lauseen lauseke on heti alussa epätosi, niin lausetta ei suoriteta lainkaan. Esimerkiksi seuraava rakenne varmistaa, että käyttäjän antama luku on positiivinen. Jos annettu luku on heti positiivinen, ei silmukkaa suoriteta lainkaan.
int luku; printf("Anna uusi luku. \n"); scanf("%d", &luku); while (luku < 0) { printf("Luku ei saa olla negatiivinen. \n"); printf("Anna uusi luku. \n"); scanf("%d", &luku); }
Seuraava ohjelma laskee N:n ensimmäisen neliöluvun summan:
#include <stdio.h> int main(void) { int n, i, total = 0; printf("Anna n:"); scanf("%d", &n); i = n; while (i > 0) { total = total + i * i; i = i - 1; } printf("%d:n ensimmäisen neliöluvun summa: %d \n", n, total); return 0; }
do-lause on hyvin samantapainen kuin while-lause. Siinä silmukan jatkamisehto testataan vasta silmukan lopussa, eli silmukka suoritetaan aina vähintään kerran. Do-lauseen yleinen muoto on seuraava (huomaa sulut lausekkeen ympärillä):
do lause while (lauseke);
Silmukkaa suoritetaan niin kauan, kuin lausekkeen arvo on tosi. Esimerkki:
int luku; do { printf("Anna uusi luku. \n"); scanf("%d", &luku); } while (luku < 0);
Myös for-lause on toiminta-ajatukseltaan samantapainen kuin while-lause. Se on vain hieman kompaktimpi esitysmuodoltaan.
for(lauseke1; lauseke2; lauseke3) lause;
Ennen toistoa lasketaan lausekkeen 1 arvo, joka voi sisältää esimerkiksi tarvittavan kierroslaskurin alustuksen. Toistettavaa lausetta suoritetaan niin kauan kuin lausekkeen 2 määrittämä ehto on voimassa ja jokaisen toiston jälkeen lasketaan lausekkeen 3 arvo. Käytännössä for-lause vastaa siis seuraavaa while-lausetta.
lauseke1; while(lauseke2) { lause; lauseke3; }
For-lausetta käyttämällä rivin pituuden laskeminen näyttää seuraavalta. Huomaa, että laskuri count alustetaan nyt arvoon -1, koska silmukka suoritetaan aina vähintään kerran
int count; int ch = ' '; for (count = -1; ch != '\n'; count = count + 1) ch = getchar();
Ohjelma neliölukujen summan laskemiseen.
#include <stdio.h> int main(void) { int n, i, total = 0; printf("Anna n: "); scanf("%d", &n); for (i = n; i > 0; i = i - 1) total = total + i*i; printf("%d:n ensimmäisen neliöluvun summa: %d \n", n, total); return 0; }
For-lauseen lausekkeet voidaan jättää pois, jos ne ovat tarpeettomia. Edellisen ohjelman silmukka voitaisiin kirjoittaa myös näin.
printf("Anna n: "); scanf("%d", &n); i = n; for (; i > 0; i = i -1) total = total + i*i;
Seuraava ohjelma tulostaa muunnoksen Celcius-asteista Fahrenheit-asteiksi.
#include <stdio.h> int main(void) { int alku, loppu, c_aste, f_aste; printf("Tulostan lämpötilamuunnosasteikon \n"); printf("Anna muunnosasteikon ala- ja ylärajat. \n"); scanf("%d%d", &alku, &loppu); printf("Celsius Fahrenheit \n\n"); for (c_aste = alku; c_aste <= loppu; c_aste = c_aste+1) { f_aste = (int) (9.0 / 5 * c_aste + 32); printf("%d \t %d \n", c_aste, f_aste); } return 0; }
Lohkot
Edellä on jo puhuttu lohkosulkeista {}, joiden sisälle voidaan koota useampia lauseita yhdeksi kokonaisuudeksi. Tälläisesta asiasta käytetään usein termiä koottu lause. Lauseiden lisäksi lohko voi sisältää myös uusia lohkoja ja muuttujien määrittelyjä. Viimeksi mainittu on mielekästä, jos muuttujia tarvitaan vain tämän lohkon sisällä. Seuraava ohjelma tekee muunnokset Celcius-ja Fahrenheit-asteiden välillä molempiin suuntiin.
#include <stdio.h> int main(void) { int muunnos; printf("Lämpotilamuunnoasteikko Celcius-asteista \n"); printf("Fahrenheit-asteiksi tai päinvastoin.\n"); printf("Kumpi muunnos tehdään 1) C->F vai 2) F->C). \n"); printf("Vastaa 1 tai 2: "); scanf("%d", &muunnos); if (!((muunnos ==1) || (muunnos == 2))) printf("Annoit väärän muunnoksen. \n"); else { int alku, loppu, c_aste, f_aste; printf("Anna muunnosasteikon ala- ja ylärajat. \n"); scanf("%d%d", &alku, &loppu); if (muunnos == 1) { printf("Celsius Fahrenheit \n\n"); for (c_aste = alku; c_aste <= loppu; c_aste = c_aste+1) { f_aste = (int) (9.0 / 5 * c_aste + 32); printf("%d \t %d \n", c_aste, f_aste); } /* for */ } /* if */ else { printf("Fahrenheit Celcius \n\n"); for (f_aste = alku; f_aste <= loppu; f_aste = f_aste+1) { c_aste = (int) (5 * (f_aste - 32) / 9.0); printf("%d \t %d \n", f_aste, c_aste); } /* for */ } /* else */ } /* else */ return 0; } /* main */
Huomaa, että lohkojen lopussa ei ole puolipistettä. Huomaa myös lohkojen lopussa olevat kommentit, jotka helpottavat lohkojen erottumista toisistaan. Niitä kannattaa käyttää.
Osoitinmuuttujat
Kutakin ohjelman muuttujaa vastaa jokin osoite tietokoneen muistissa, johon muuttujan sisältämä data on tallennettu. Eräissä tilanteissa on tarpeen saada selville muuttujan osoite, ja tätä varten käytetään merkintää &muuttuja. Symboli & on operaattori, joka palauttaa sitä seuraavan muuttujan muistiosoitteen. Esimerkiksi, kun luetaan ohjelman muuttujille arvoja, välitetään scanf-funktiolle kyseisten muuttujien osoitteet, joihin scanf sitten tallettaa luetut data-arvot. Huomaa, että &-operaattorin ja muuttujanimen väliin EI tule välilyöntiä.
Seuraava kutsu lukee arvot muuttujille luku ja arvo.
scanf("%d%d", &luku, &arvo);
Muuttujan tyyppinä voi olla myös muistiosoite, joka osoittaa tietyn tyyppiseen muuttujaan. Tällöin muuttujan määrittelyssä muuttujan tunnusta edeltää symboli '*'. Seuraavassa määritellään kaksi muuttujaa luku ja p, joista edellinen on tavallinen kokonaislukumuuttuja ja jälkimmäinen on muuttuja, johon voidaan tallentaa kokonaislukumuuttujan muistiosoite.
int luku, *p;
Seuraavalla sijoituksella muuttujan luku muistiosoite voidaan tallettaa muuttujaan p:
p = &luku;
Kun halutaan viitata osoitinmuuttujan osoittaman muistialueen sisältöön, käytetään merkintää *muuttuja. Symboli * on operaattori, joka kertoo, että sitä seuraavan osoitinmuuttujan sisällön asemasta otetaankin data, joka on talletettu ko. osoitteeseen. Esimerkissämme sijoitamme ensin muuttujaan luku arvon 3.
luku = 3;
Sen jälkeen voimme käsitellä luku-muuttujan sisältöä myös muuttujan p avulla, koska p sisältää saman muistipaikan osoitteen kuin johon tunnus luku viittaa. Sijoitus
*p = 4;
saa aikaan sen, että ko. muistipaikan sisällöksi tulee 4 (ja samalla siis muuttujan luku arvoksi tulee 4).
Funktiot
Funktio on itsenäinen ohjelmakoodin yksikko, joka on suunniteltu määrätyn tehtävän suorittamiseen. Funktio saa tarvitsemansa tiedot argumentteinaan ja palauttaa yleensä yhden arvon.
Funktiot selkeyttävät ohjelmaa ja vähentävät ohjelman kirjoittamiseen kuluvaa aikaa, koska jokin monta kertaa tarvittava toimenpide tarvitse toteuttaa vain yhden kerran, funktiona. Sitä voidaan sitten kutsua useita kertoja ohjelman eri kohdista eri argumenttien arvoilla. Usein silloinkin, kun tehtävä halutaan suorittaa vain kerran, kannattaa sitä varten kirjoittaa funktio, koska tämä selkeyttää ohjelman rakennetta. Funktiot tekevät ohjelmasta modulaarisen ja helpottavat sen lukemista, muuttamista ja korjaamista. Pääohjelman tulisi toimia lähinnä sisällysluettelona, joka sisältää määrittelyjä ja funktiokutsuja.
Funktion rakenne
Funktio koostuu otsikosta ja lohkosta. Otsikko sisältää seuraavat tiedot: funktion palauttaman arvon tyyppi, funktion tunnus, argumenttien tyypit ja tunnukset.
Funktion lohkossa voi määritellä funktion sisäisiä muuttujia, joihin voi viitata vain funktion omassa lauseosassa. Tällaisten, ns. paikallisten muuttujien arvot tuhoutuvat kutsukertojen välillä, eli näihin sisäisiin muuttujiin ei voi jättää mitään tietoa "varastoon" odottamaan funktion seuraavaa kutsukertaa.
Lohko sisältää funktion toiminnan määrittävät lauseet. Niissä määritellään myös, minkä arvon funktio palauttaa kutsujalleen. Funktion muodollinen määrittelytapa on seuraava:
<tyyppi> <funktion_tunnus> ( <arg1 tyyppi> <arg1 tunnus>, <arg2 tyyppi>...) { <paikallisten muuttujien määrittelyt>; <lauseita>; return <lauseke>; }
Funktion palauttama arvo on return-komentoa seuraavan lausekkeen arvo. return on tavallisesti funktion viimeinen lause, mutta se voi sijaita muuallakin. Samassa funktiossa voi olla myös useita eri return-lauseita, jolloin funktion toiminta voi päättyä useammassa paikassa.
Funktion kutsuminen
Funktiota kutsutaan muualta ohjelmasta muodossa
<funktion_tunnus> (arg1, arg2, ...)
jossa arg1, arg2... ovat funktiolle välitettäviä argumentteja. Esimerkiksi:
muuttuja = funktio(arg1, arg2);
Argumenttien sisältämät tiedot tai muistiosoitteet siirtyvät funktion omaan käyttöön kutsun ajaksi. Kun funktio on suorittanut tehtävänsä, sen palauttama arvo (return-lausetta seuraavan lausekkeen arvo) palautetaan kutsukohtaan, jossa sitä voidaan käyttää laskennassa mukana. Muun ohjelman suoritus jatkuu kutsukohdasta eteenpäin.
Funktion pitää olla määritelty ennen, kuin sitä kutsutaan. Tämän vuoksi kannattaa kirjoittaa kaikki funktiot ennen pääohjelmaa. Ne kannattaa kirjoittaa kuitenkin järkevässä järjestyksessä eli siten, että ensiksi kutsuttava funktio on ensimmäisenä. Tällöin suuremmatkin ohjelmat pysyvät helpommin hahmotettavina. Suuret ohjelmat jaetaan useisiin tiedostoihin, ja tällöin funktioiden määrittelyissä tarvitaan usein ns. prototyyppejä. Näistä enemmän oppikirjassa.
Funktion kutsussa argumentit on annettava samassa järjestyksessä, kuin ne on määritelty funktion otsikossa. Lisäksi niiden tyyppien on oltava samat kuin otsikossa.
Esimerkkiohjelma ilman funktioita:
#include <stdio.h> int main(void) { int a, b, erotus; printf("Anna kaksi lukua: "); scanf("%d %d", &a, &b); erotus = a - b; printf("%d - %d = %d\n", a, b, erotus); return 0; }
Sama funktion avulla:
#include <stdio.h> int laske(int c, int d) { int ero; ero = c - d; return ero; /* arvon palautus pääohjelmalle */ } int main(void) { int a, b, erotus; printf("Anna kaksi lukua: "); scanf("%d %d", &a, &b); erotus = laske(a, b); /* HUOM ei: laske(b, a); */ printf("%d - %d = %d\n", a, b, erotus); return 0; }
Proseduuri
Proseduureista puhutaan silloin, kun funktio ei palauta mitään arvoa, jolloin sen tyyppi on void. C-kielessä ei ole varsinaisia proseduureja toisin kuin monessa muussa ohjelmointikielessä, vaan C:ssä on proseduurimaisia funktioita.
Mikäli funktio ei palauta mitään arvoa, sitä voidaan kutsua suoraan omana lauseenaan:
funktio2(arg1, arg2);
Esimerkki:
#include <stdio.h> int laske(int c, int d) { return (c - d); } void tulosta(int yksi, int kaksi, int yhteensa) { printf("%d - %d = %d\n", yksi, kaksi, yhteensa); /* ei palauteta mitään arvoa */ } int main(void) { int a, b, erotus; printf("Anna kaksi lukua: "); scanf("%d %d", &a, &b); erotus = laske(a, b); /* HUOM ei: laske(b, a); */ tulosta(a, b, erotus); /* HUOM argumentit vain tässä järj.*/ return 0; }
Muuttujien näkyvyys
Paikalliset muuttujat näkyvät ainoastaan funktion sisällä, eikä niihin voida viitata missään muualla. Tästä seuraa, että toisessa funktiossa voidaan määritellä samanniminen ja saman- tai erityyppinen muuttuja, mutta sillä ei ole mitään tekemistä toisen funktion samannimisen paikallisen muuttujan kanssa.
Funktion paikallisen muuttujan arvo ei säily kahden kutsukerran välillä muuttumattomana. Esimerkiksi, jos haluat tarkkailla, montako kertaa jotakin funktiota kutsutaan, älä laita laskuria itse tutkittavaan funktioon vaan sitä kutsuvaan funktioon.
Jos funktio fun1 kutsuu funktiota fun2, ei fun1:ssä voida viitata fun2:en muuttujiin tai päinvastoin. Mikäli fun1:sta halutaan välittää informaatiota fun2:lle, on käytettävä argumenttivälitystä.
Globaalit muuttujat määritellään ohjelmatiedostossa funktioiden ulkopuolella, useimmiten tiedoston alussa. Niihin voidaan viitata suoraan kaikista ohjelmatiedoston funktioista ja funktioissa voidaan muuttaa niiden arvoja. Hyvään ohjelmointityyliin kuuluu, että käytetään mahdollisimman vähän globaaleja muuttujia, koska niiden runsas käyttö vaikeuttaa ohjelmakoodin ymmärtämistä.
Argumenttien välitys
Funktion argumentteja (=parametreja) voidaan välittää joko arvoparametreina tai muuttuja-parametreina. Edellisessä tapauksessa välittyy vain funktion kutsussa olevan argumenttilausekkeen - tai muuttujan arvo (itse asiassa kopio siitä) funktion otsikossa määritellyn muodollisen parametrin arvoksi. Sitä voidaan tällöin muuttaa funktion sisällä ilman, että kutsuvassa ohjelmassa olevat arvot muuttuvat. Esimerkki:
int summa(int a, int b) { a = a + b; return (a); }
Kutsuttaessa tätä funktiota seuraavalla tavalla, säilyvät muuttujien x ja y arvot ennallaan, koska argumentin a arvon muutos kohdistuu x:n arvon kopioon.
luku = summa (x, y);
Haluttaessa muuttaa kutsuvan ohjelman muuttujien arvoja, välitetään argumentteina muuttujien arvojen sijasta muuttujien osoitteet. Tällöin funktion otsikossa määritelty muodollinen parametri viittaa samaan muistipaikkaan kuin todellinen argumentti ja edellisen arvon muutos vaikuttaa suoraan jälkimmäisen arvoon. Esimerkiksi scanf-funktio menettelee juuri näin.
Kun halutaan sallia tiedon siirtäminen muuttujaparametrien avulla, asetetaan funktion määrittelyssä nämä parametrit osoitinmuuttujiksi. Funktion kutsussa käytetään silloin muuttujien osoitteita. Esimerkki:
#include <stdio.h> void kysy(int *luku1, int *luku2) { int a, b; printf("Anna kaksi lukua: "); scanf("%d %d", &a, &b); *luku1 = a; *luku2 = b; } int laske(int c, int d) { return (c - d); } void tulosta(int yksi, int kaksi, int yhteensa) { printf("%d - %d = %d\n", yksi, kaksi, yhteensa); } int main(void) { int a, b, erotus; kysy(&a, &b); erotus = laske(a, b); tulosta(a, b, erotus); return 0; }
Tämän C-ohjelman pääohjelma on lähinnä sisällysluettelo ja kaikki muu tapahtuu funktioissa. Eli tämä on esimerkki hyvästä C-ohjelmasta, vaikka ohjelma ei teekään juuri mitään järkevää.
Kopio lisenssistä (englanniksi) löytyy täältä.
Alkuperäinen (c) Petteri Hämäläinen
