Funktiot C-kielessä¶
Osaamistavoitteet: Tiedät miten C-kielinen ohjelma rakentuu ja osaat käyttää funktioita C-ohjelmassa.
Ensimmäinen C-ohjelma¶
Kuten tähän asti olemme johdantomateriaalista aavistelleet, jokaisessa C-ohjelmassa on on aina vähintään yksi funktio:
int main() {
...
return 0;
}
..jota kutsutaan automaattisesti kun ohjelman suoritus alkaa. Ohjelman suoritus päättyy kun
main
-funktion suoritus päättyy. main
-funktion lisäksi ohjelmassamme voi tietysti olla muitakin funktiota, joko itse luomiamme tai valmiita kirjastojen tarjoamia. Tässä
main
-funktio on kuvattu ilman parametrejä. Itseasiassa sille voidaan myös antaa argumentteja ohjelman ulkopuolelta, vaikkapa komentoriviltä, kun käynnistämme ohjelman. Tarkastellaan yleistä C-kielisen ohjelman rakennetta:
// 1. Esikääntäjäkäskyt jotka ohjaavat ohjelman käännösprosessia
#include <stdio.h>
#define PI 3.14159
// 2. Ohjelman sisäisten muuttujien ja funktioiden esittely
float laske_ala(float sade); // Funktion prototyyppi
// 3. Pääfunktion toteutus
int main() {
// Funktion sisäisten muuttujien esittely (ja alustus)
float ympyran_ala = 0.0, ympyran_sade = 0.0;
// Funktion toiminnallisuus
printf("Anna ympyrän säde: ");
scanf("%f", &ympyran_sade);
ympyran_ala = laske_ala(ympyran_sade);
printf("Ympyrän pinta-ala on: %.2f\n", ympyran_ala);
// Funktion palautusarvon asetus
return 0;
}
// 4. Muut funktiot
// Oma funktio laske_ala
// Argumentit: sade, käyttäjän antama ympyrän säde
float laske_ala(float sade) {
// Funktion sisäisten muuttujien esittely (ja alustus)
float pinta_ala = 0.0;
// Funktion toiminnallisuus
pinta_ala = PI * sade * sade;
// Funktion palautusarvon asetus
return pinta_ala;
}
Koodin kommenteissa on kuvattu siis yleinen rakenne:
- Esikääntäjäkäskyt joiden avulla liitetään ohjelmaan valmiita kirjastoja. Myös vakioiden määrittely voidaan tehdä esikääntäjäkäskyillä. Näistä lisää myöhemmin.
- Ohjelmassa otettiin käyttöön standardoitu C-kirjaston
stdio
, joka tarjoaa käyttöömme funktioita syötteen ja tulostuksen käsittelyyn. Siis näyttö ja näppäimistö, mutta myös paljon muutakin. - Ohjelman (moduulin) omat sisäiset esittelyt: funktioiden prototyypit ja muuttujat. Tässä lohkossa tehdyt esittelyt ovat käytössä koko moduulissa.
- Ohjelmassamme esiteltiin prototyyppi
float laske_ala(float sade);
- Ohjelman toiminnallisuus: pääohjelma:
main
- Ohjelman toiminnallisuus: omat funktiot.
- Tässä funktion
laske_ala
toteutus.
Modulaarisuudesta¶
Funktion toteutus tehdään koodilohkon sisälle, jossa määritellään sen sisäiset muuttujat, toiminnallisuus ja palautusarvo(t). Koodilohkoja C-kielessä merkitään aaltosulkeilla. Kurssilla heti alusta ajattelemme ohjelmaamme modulaarisesti koodilohkoina funktioiden kautta. Kaikki harjoitustehtävätkin tulevat olemaan "Laadi funktio, joka..".
Funktiossa voidaan esitellä ja alustaa sen sisäisiä (paikallisia) muuttujia. Kuten yllä muuttuja
pinta_ala
funktion laske_ala
. Sisäiset muuttujat ovat olemassa vain funktion sisällä ja niiden esittelyt tehdään yleensä funktion koodin alussa, mutta tässä eri C-standardit käyttäytyvät hieman eri tavoin, joten varminta on esitellä ne alussa. Esimerkissä yllä pääfunktiossa omaa funktiota
laske_ala
kutsutaan lauseella ympyran_ala = laske_ala(ympyran_sade);
Nyt funktion argumentiksi tuli vain yksi liukuluku muuttujasta ympyran_sade
, jonka olemme scanf
-lauseella kysyneet käyttäjältä (scanf:n käytöstä lisää myöhemmin). Funktion suorituksen jälkeen sen palautusarvo otetaan main-funktiossa talteen muuttujaan ympyran_ala
. Näin funktio on erotettavissa omaksi kokonaisuudekseen, piilottaa toteutuksen detaljit ja kommunikoi muiden ohjelman osien kanssa argumenttien ja palautusarvojen kautta.
Funktion esittely¶
Funktion esittely, ts. prototyyppi, on yleisesti muotoa:
paluuarvon_tyyppi funktion_nimi(argumentti1, argumentti2, ...)
Funktion argumentti taas on muuttujan esittely. Argumentteina esitellyt muuttujat ovat funktion käytössä sen sisäisinä muuttujina:
muuttujatyyppi muuttujan_nimi
Esimerkki. Funktion laske_ala esittely.
uint64_t laske_kertoma(uint32_t luku);
Esittelyä luetaan seuraavasti. Funktion nimi on
laske_kertoma
, se ottaa sisäänsä argumentin uint32_t luku
. Funktio palauttaa arvon tyyppiä uint64_t
. Muistetaan puolipiste kaikkien C-kielen lauseiden loppuun.Argumentit voivat olla mitä vain C-kielen muuttujatyyppejä (int, long, double, float, char, jne ja näiden taulukko), riippuen tietenkin mitä funktion halutaan tekevän. Argumentit voivat olla myös erityyppisiä samassa funktiossa. Jos argumentteja on useita, ne erotellaan pilkulla. Erona joihinkin muihin kieliin, kun funktiota kutsutaan, sille pitää täsmälleen oikea määrä argumentteja oikeassa tyypissä. Jos argumentti ei ole samaa tyyppiä kuin prototyypissä määritelty, kääntäjä tekee muunnoksen ja siinä voi käydä huonosti (giljotiini / muunnossäännöt).
C-kielessä funktio voi
return
-lauseella palauttaa vain yhden arvon, joka myös voi olla mitä tahansa c-kielen muuttujatyyppiä paitsi taulukko (tästä lisää myöhemmin). Näin saamme välitettyä funktion tuloksen ja ottaa sen talteen muuttujaan,sitä kutsuneelle funktiolle. Esimerkkikoodissa yllä funktio laske_ala
palauttaa sisäisen (ts. paikallisen, engl. local) muuttujan pinta_ala
arvon. Arvosta "ottaa kopin" main-funktiossa muuttuja ympyran_ala = laske_ala(ympyran_sade);
. Koska
main
-funktiota kutsuu käyttöjärjestelmä, paluuarvo 0 kertoo sille, että ohjelman suoritus onnistui virheittä. Yleensä muut main-funktion palautusarvot tarkottavat virhettä ohjelman suorituksessa. Mikäli funktio ei ota argumentteja tai ei palauta arvoa, käytetään selvyyden vuoksi niiden paikalla esittelyssä sanaa
void
. void en_ole_palauttamassa(uint8_t x);
int32_t en_tarvi_argumenttia(void);
void antakaa_mun_olla_rauhassa(void);
void
-sana ei ole pakollinen tällaisissa funktion esittelyissä, mutta kääntäjät olettavat sitten funktioista asioita.. esimerkiksi jos funktiolle ei ole paluuarvoa merkitty eikä käytetä void
ia, kääntäjä olettaa sen palauttavan int-tyypin kokonaisluvun. Siksi on syytä aina käyttää void-sanaa näissä tilanteissa. Funktion kutsuminen¶
Argumenttien osalta kääntäjä tekee annetusta argumentin arvosta kopion ja sijoittaa sen vastaavaan esittelyssä annettuun muuttujaan. Kutsussa käytettävillä muuttujien nimillä ei ole merkitystä funktion toiminnan kannalta, vain arvo välitetään funktiolle.
Alla argumentin arvot siis ovat funktiossa käytettävissä sen sisäisessä muuttujassa nimeltä
x_koord
. // Prototyyppi
void kutsu_minua(int16_t x_koord);
...
// Pääohjelma
...
int16_t x = 27;
kutsu_minua(x); // x_koord = x
kutsu_minua(27); // x_koord = 27
...
Myöskin tässä C-kieli tekee tarvittaessa tyyppikonversion käyttäen giljotiinia.
Nyt, funktiokutsussa voisi olla esimerkiksi muuttujan tilalta vaikka toinen funktiokutsu, jolloin tämän palauttama arvo välitettäisiin kutsuttavalle funktiolle! Nä'in c-kielessä voidaan käyttää ikääkuin sisäkkäisiä funktioita.
void kutsu_minua(int16_t x_koord);
int16_t palauta_jotain(void);
int main(){
int16_t x = 27;
...
kutsu_minua(palauta_jotain());
...
if (palauta_jotain() > 0) {
kutsu_minua(x);
}
...
}
Funktion kutsumisessa on laiteläheisessä ohjelmoinnissa huomioitavaa. Koska funktion kutsuissa tehdään argumenteistä kopiot omiin funktion paikallisiin muuttujiin, voi käydä niinkin, että muisti loppuu (ja ohjelma kaatuu käyttäjälle mystiseen virheeseen).. Tyypillisesti näin voi käydä jos argumenttina on iso taulukko, joka funktion pitäisi käsitellä. Myöhemmin materiaalissa esittelemme keinoja, miten isojen taulukkojen ongelmilta voidaan välttyä, mm. käytttämällä globaaleja ja osoitin-muuttujia.
Funktion prototyyppi¶
Prototyyppi, ts. funktion esittely, kertoo kääntäjälle millaisen arvon funktio palauttaa ja millaisia argumentteja se haluaa. Tämä mahdollistaa funktion käytön koodissa ennenkuin sen koodi on käännetty. Kääntäjä luottaa tässä ohjelmoijaan, että funktion koodi tulee vastaan jossain kohti ohjelmistoprojektia. Jos ei funktion määrittelyä löydy, kääntäminen loppuu virheilmoitukseen..
Esimerkissä, ennen main-funktiota meillä oli funktion prototyyppi
float laske_ala(float sade);
. Nyt main-funktiossa voimme siis kutsua laske_ala
-funktiota, ilman että kääntäjä tietää mitä funktio tekee. Kääntäjä tarkistaa tässäkohden vain syntaksin, eli että kutsumme oikein. Tosin, otetaan takaisin sen verran, että jos funktion
laske_ala
määrittely olisi ollut ohjelmassa ennen sen käyttöä main-funktiossa, emme tarvitsisi prototyyppiä. Tästä seuraa kuitenkin asioita, joista lisää kirjasto-materiaalissa.. Kurssilla tästälähin kirjoittaessamme uusia funktioita muistamme aina niiden esittelyn prototyypillä ensin! (Harjoitustehtävät edellyttävät prototyyppiä.)Ja, itseasiassa koodissa oleva
#include <stdio.h>
-esikääntäjän lause hakee meille käyttämiemme scanf- ja printf-funktioiden esittelyt. Niiden toteutukset on usein tehty valmiiksi binääreina, joten niitä ei pääse käpistelemään.. no tästä lisää kirjastojen yhteydessä.Uusia muuttujatyyppejä¶
Yllä todettiin, että funktion sisällä määritellyt paikalliset muuttujat ovat olemassa vain siinä funktiossa. Tämä koskee myös main-funktiota, sen muutoin erikoislaadusta huolimatta. Opimme juuri, että muuttujia voidaan välittää argumenttien ja paluuarvojen kautta funktioiden välillä, mutta tämän lisäksi voimme C-kielessä esitellä globaaleja ja staattisia muuttujia.
Globaali muuttuja määritellään funktio-määritysten ulkopuolella, jolloin se on elossa aina ja käytettävissä kaikissa koodimoduulin funktioissa. Globaaleja muuttujia voidaan myös välittää laajemman ohjelman koodimoduleista toisiin, mutta tässä on aloittelevalle ohjelmoijalle monta hasardin paikkaa.. joten emme kurssilla mene siihen.
Esimerkki globaaliin muuttujan käytöstä.
#include <stdio.h>
// Ohjelman sisäisten funktioiden esittely prototyypin avulla
void tulosta_muuttuja(void);
float ou_jes_olen_globaali = 3.14; // Globaalin muuttujan esittely ja alustus
// Pääfunktion toteutus
int main() {
printf("Tulostetaan main:ssa globaali muuttuja: %f\n", ou_jes_olen_globaali );
ou_jes_olen_globaali = 2.71828; // Muuttujaa voidaan käsitellä kuten muitakin muuttujia
tulosta_muuttuja();
}
void tulosta_muuttuja(void) {
// // Globaalia muuttujaa ei tarvitse antaa argumenttina
printf("Tulostetaan funktiossa globaali muuttuja: %f\n", ou_jes_olen_globaali );
}
Globaalien muuttujien käyttämiseen yleisesti on kaksi syytä: niiden elinaika ja käyttöalue on laajempi, ja ne ovat kätevämpiä käyttää jos funktioilla on paljon argumentteja tai paluuarvoja. Laiteläheisessä ohjelmoinnissa käytetään usein globaaleja muuttujia juuri välittämään tietoa ohjelman eri osien (ts. funktioiden) välillä. Tällöin pienestä ohjelmasta saadaan suoraviivainen ja toisaalta muistin säästämiseksi, kun ohjelmassa ei tarvitse välittää tietoa funktion argumentteina eikä näinollen tehdä niistä kopiota.
Kaikkia ohjelman muuttujia kuitenkaan ei kannata tehdä globaaleina, koska niiden vaikutus tosiaan on globaali ohjelmassamme. Saatetaan aiheuttaa turhia riippuvuuksia eri funktioiden välille ja laajemmissa ohjelmissa koodiin voi ilmestyä aika vaikeaselkoisia ja vaikeasti jäljitettäviä virheitä. Puhumattakaan useista koodimoduleista koostuvista ohjelmista..
Staattinen muuttuja alustetaan kerran ja se säilyttää myös arvonsa funktiokutsujen välillä. Tällöin muuttujan esittelyssä käytetään sanaa
static
. void laskuri(void) {
static uint64_t lukumaara=0;
lukumaara++;
}
Nyt muuttujan
lukumaara
arvo lähtee nollasta ja kasvaa yhdellä aina kun laskuri()
-funktiota kutsutaan. Vaikka muuttujan arvo säilyy, se ei ole käytettävissä muualla kuin funktion sisällä. Huomataan, että oikeastaan myös globaalit muuttujat ovat koodimodulin (ts. tiedoston) sisäisiä staattisia muuttujia.. ja static-määreellä on muitakin käyttötapoja, mutta emme mene niihin tällä kurssilla.
Lopuksi¶
Koodaustyyliin liittyen, funktion koko (ohjelmalauseiden määrä) on syytä pitää kohtuullisena. Nyrkkisääntönä, jos funktion rivimäärä ei mahdu kerralla ruudulle, voi miettiä funktion jakamista useisiin pienempiin funktioihin. Tämä maksaa vaivan ohjelman ylläpidettävyyden ja lukukelpoisuuden myötä.
Funktioilla pelaaminen pääsee itseasiassa vauhtiin vasta standardi- ja oheislaitteiden kirjastoihin tutustuessa sekä omia ohjelmia tehdessä!
Anna palautetta
Kommentteja materiaalista?