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() {
// koodia
// ...
return 0;
}
..jota kutsutaan automaattisesti kun ohjelman suoritus alkaa. Samoin ohjelman suoritus päättyy kun
main
-funktion suoritus päättyy. Pätevän oloinen funktio siis, mutta main
-funktion lisäksi ohjelmassamme voi tietysti olla muitakin funktiota, joko itse luomiamme tai valmiita kirjastojen tarjoamia. Tarkastellaan yleistä C-kielisen ohjelman rakennetta:
// 1. Esikääntäjäkäskyt jotka ohjaavat ohjelman käännösprosessia
#include <stdio.h> // valmis kirjasto mukaan ohjelmaan
#define PI 3.14159 // vakion asetus
// 2. Ohjelmamme omien (globaalien) 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: "); // Tulosta kysymys
scanf("%f", &ympyran_sade); // Lue liukuluku talteen muuttujaan
ympyran_ala = laske_ala(ympyran_sade);
printf("Ympyrän pinta-ala on: %.2f\n", ympyran_ala);
// Funktion palautusarvon asetus
return 0;
}
// 4. Muut omat 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.
Hox! Esimerkissä
main
-funktio on kuvattu ilman parametrejä. Itseasiassa sille voidaan myös antaa (työasemissa) argumentteja ohjelman ulkopuolelta, esimerkkinä avattavan tiedoston nimi komentoriviltä ohjelmaa käynnistäessä. Komentoriviparametrejä ei sulautettujen ohjelmoinnissa yleisesti käytetä ja siksi ne ohitetaan tässä.. Modulaarisuudesta¶
Kurssilla näin heti alusta ajattelemme C-kielistä ohjelmaamme modulaarisesti koodilohkoina, jotka on toteutettu funktioina. Toisinsanoen, C-kielinen ohjelma rakentuu funktiokutsuista, joille annamme niiden vaatimat argumentit ja otamme talteen palautusarvon, joka taas voidaan käsitellä / välittää seuraavalle funktiolle.
Funktion toteutus tehdään koodilohkon sisälle, jossa määritellään sen sisäiset muuttujat, toiminnallisuus ja palautusarvo(t). Koodilohkot siis rajaavat jonkin toiminnallisuuden ja C-kielessä merkitään aaltosulkeilla.
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;
}
Esimerkissä yllä pääfunktiossa omaa funktiota
laske_ala
kutsutaan koodista esimerkiksi ao. mukaisesti. Nyt funktion argumentiksi tulee vain yksi liukuluku muuttujasta ympyran_sade
(jonka olemme scanf-funktiolla kysyneet käyttäjältä, no scanf:n käytöstä lisää myöhemmin). Funktion suorituksen jälkeen sen palautusarvo otetaan main-funktiossa talteen muuttujaan ympyran_ala
. Ilman palautusarvon talteenottoa emme tietäisi mikä on ympyrän ala. ...
ympyran_ala = laske_ala(ympyran_sade);
...
C-kielen funktiossa voidaan esitellä ja alustaa sen sisäisiä (paikallisia) muuttujia. Nämä sisäiset muuttujat ovat olemassa vain funktion (koodilohkon) sisällä ja niiden esittelyt tehdään yleensä funktion koodin alussa (tässä eri C-standardit käyttäytyvät hieman eri tavoin, joten varminta on esitellä ne alussa). Kuten yllä muuttuja
pinta_ala
funktion laske_ala
. Näin voimme palastella ohjelman toiminnallisuutta vielä myös muistin tasolla. Hetken päästä selviää, miksi tämä on sulautetuissa varsin tärkeää..Näin funktio on erotettavissa omaksi kokonaisuudekseen ja voidaan kapseloida / piilottaa sen toteutuksen detaljit ja kommunikoida muiden ohjelman osien kanssa argumenttien ja palautusarvojen kautta.
Itseasiassa, kaikki C-kielen harjoitustehtävätkin tulevat olemaan tyyliin "Laadi funktio, joka..".
Funktion esittely¶
Funktion esittely ohjelmassamme tehdään sen prototyypin kautta, Prototyyppi on yleisesti muotoa:
paluuarvon_tyyppi
funktion_nimi
( muuttujatyyppi argumentti1, muuttujatyyppi argumentti2, ...
)Funktion argumentti taas on muuttujan esittely funktion käyttöön. Argumentti on siis tapa välittää haluttua tietoa funktiolle sitä kutsuvasta ohjelmasta. C-kielessä esitellyt muuttujat ovat funktion käytössä sen sisäisinä muuttujina:
muuttujatyyppi
muuttujan_nimi
float laske_ala(float sade); // Funktion prototyyppi
Esittelyä luetaan seuraavasti. Funktion nimi on
laske_ala
, se ottaa sisäänsä argumentin float sade
, ts liukulukumuuttujan. Funktio palauttaa arvon tyyppiä float
, joten otamme palautusarvon talteen sen tyyppiseen muuttujaan. Muistetaan puolipiste kaikkien C-kielen lauseiden loppuun.Argumentit voivat olla mitä vain C-kielen muuttujatyyppejä (int, long, double, float, char, struct, 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ä. C-kielessä ei voida antaa argumentille oletusarvoa, kuten joissain muissa ohjelmointikielissä. Jos argumentti ei ole samaa tyyppiä kuin prototyypissä määritelty, kääntäjä tekee muuttujatyypin muunnoksen ja siinä voi käydä huonosti (giljotiini / muunnossäännöt).
Funktion palautusarvo¶
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);
. 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. Nyt
main
-funktio on erikoistapaus, koska sitä kutsuu käyttöjärjestelmä. Ohjelman päätyttyä on hyvä kertoa käyttöjärjestelmälle että suoritus onnistui. Tässä syystä main-funktiot palauttavan nollan
viimeisenä käskynään, joka siis kertoo, että ohjelman suoritus onnistui virheittä. Yleensä muut main-funktion palautusarvot tarkottavat virhettä ohjelman suorituksessa!int main() {
...
// Funktion palautusarvon asetus
return 0;
}
Funktion kutsuminen¶
C-kielen ominaisuus on, että argumenttien osalta kääntäjä tekee niiden arvoista sisäisen kopion ja sijoittaa sen vastaavaan esittelyssä annettuun sisäiseen muuttujaan. Näin funktio pääsee siis käsittelemään tietoa koodilohkonsa ulkopuolelta. Huomataan, että itse muuttujaa ei välitetä funktiolle, vaan sen arvo!
...
ympyran_ala = laske_ala(ympyran_sade);
}
// funktio käyttää sisäistä sade-muuttujaa
// jonka arvo saatiin ympyran_sade-muuttujasta
float laske_ala(float sade) {
// Funktion sisäisten muuttujien esittely (ja alustus)
float pinta_ala = 0.0;
// Funktion toiminnallisuus
pinta_ala = PI * sade * sade;
// Muuttujan pinta_ala arvo kopioituu
// main-funktion muuttujaan ympyran_ala
return pinta_ala;
}
Myöskin tässä C-kieli tekee tarvittaessa tyyppikonversion käyttäen giljotiinia.
Nyt, funktiokutsussa voisi olla esimerkiksi muuttujan tilalta vaikka toinen funktiokutsu, jonka palautusarvo välitettäisiin kutsuttavalle funktiolle argumenttina. Näin C-kielessä voidaan ketjuttaa funktioita.
void kutsu_minua(int16_t a);
int16_t kutsu_viela_kerran(int16_t b),
int16_t palauta_jotain(void);
int main(){
...
// ok, mutta hankala debugata..
kutsu_minua(kutsu_viela_kerran(palauta_jotain()));
...
// Myös ehtolauseessa
if (palauta_jotain() > 0) {
kutsu_minua(x);
}
...
}
Funktion kutsumisessa on laiteläheisessä ohjelmoinnissa oleellista huomioitavaa.
Koska funktion kutsuissa tehdään argumenteistä kopiot omiin funktion paikallisiin muuttujiin, voi käydä niinkin, että laitteen vähäinen muisti loppuu (ja ohjelma kaatuu käyttäjälle mystiseen virheeseen).. Tyypillisesti näin voi käydä jos argumenttina on taulukko, jota funktion pitäisi käsitellä ja joka siis (vahingossa) yritetään kopioida funktion sisäiseen taulukkoon toiseen kertaan. Tällaiseen tilanteeseen voi törmätä esimerkiksi kerätessä anturidataa taulukkoon, sen käsittelyä varten, kurssin harjoitustyössä..
Miten sitten taulukkoja voidaan välittää funktioille? Tähän palaamme myöhemmin, kun esittelemme vielä uusia muuttujatyyppejä 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, joka on C-kielessä oleellista. Kääntäjä luottaa tässä ohjelmoijaan, että funktion koodilohko tulee vastaan jossain kohti ohjelmistoprojektiamme. Jos ei funktion määrittelyä löydy mistään, kääntäminen loppuu virheilmoitukseen..
float laske_ala(float sade);
int main() {
...
// ok, uskotaan että funktion toteutus tulee perässä
ympyran_ala = laske_ala(ympyran_sade);
...
}
Esimerkissä yllä, jo ennen main-funktiota toteutusta meillä oli oman funktiomme prototyyppi. Tämä mahdollistaa sen, että main-funktiossa voimme siis kutsua
laske_ala
-funktiota, ilman että kääntäjä edestietäisi mitä funktio tekee! Kääntäjä tarkistaa tässä kohden vain funktiokutsun syntaksin, eli että kutsumme funktioita oikein nimen ja argumenttien osalta. 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, yo. ohjelmakoodissa alussa oleva
#include <stdio.h>
-esikääntäjän lause hakee meille käyttämiemme scanf- ja printf-funktioiden prototyypit. Ja näin saamme ne käyttöön ohjelmassa "ilmaiseksi" ennen niiden kääntämistä. Näiden funktioiden toteutukset ovat osa C-kielen valmiita standardoituja kirjastoja ja ne ovat usein toteutettu laitteellemme ja käännetty valmiiksi, joten niitä ei pääse itse 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 kaikkien funktio-määritysten ulkopuolella, jolloin se on elossa aina ja käytettävissä kaikissa koodimoduulin (lue: C-kielessä kooditiedoston) funktioissa. Globaaleja muuttujia voidaan myös välittää (laajemmassa ohjelmassa) koodimoduleista toiseen, kuten myöhemmin opimme.
Esimerkki muuttujien käytöstä.
// Ohjelman sisäisten funktioiden esittely prototyypin avulla
void tulosta_muuttujat(float olen_argumentti);
// Globaalin muuttujan esittely ja alustus
float ou_jes_olen_globaali = 3.14;
int main() {
float olen_paikallinen_main = 3.14;
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_muuttujat(olen_paikallinen_main);
}
void tulosta_muuttujat(float olen_argumentti) {
float olen_paikalllinen = 3.14;
printf("Tulostetaan funktiossa argumentti: %f\n", olen_argumentti);
printf("Tulostetaan funktiossa paikallinen muuttuja: %f\n", olen_paikallinen );
// 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?