Funktiot C-kielessä¶
Osaamistavoitteet: Opiskelija tietää millainen on C-kielisen ohjelman rakenne ja osaa käyttää funktioita C-kielisessä ohjelmassa.
Kuten olemme jo johdantomateriaalista ounastelleet, jokaisessa C-kielisessä ohjelmassa on on aina vähintään yksi funktio, nimeltään
main
. Tätä funktiota käyttöjärjestelmä (tai firmware) kutsuu automaattisesti, kun ohjelman suoritus alkaa. Itseasiassa, C-kielisen ohjelman suoritus kestää tasan niin kauan kuin main-funktion suoritus. Mutta tästä lisää laiteläheisessä osuudessa..Kurssilla ajattelemme C-kielistä ohjelmaa modulaarisesti eri itsenäisten toiminnallisuuksien kokoelmana. Jokaisella toiminnallisuudella/tehtävällä on omat syötteet/parametrit, toiminnallisuus ja vaste/tulokset. Modulaarisessa ohjelmoinnissa tehtävät toteutetaan erillisinä moduuleina / aliohjelmina eli C-kielessä funktioina. Näin ohjelman toimintalogiikka perustuu funktioiden tulosten ja argumenttien välittämiseen muuttujina funktiolta toiselle. Esimerkiksi mittaustulosten lukeminen lämpötila-anturilta sulautetussa järjestelmässä:
- Alusta ja kalibroi lämpötila-anturi
- Kysy mittaustulos anturilta
- Tulosta mittaustulos näytölle
Hox! Kurssilla kaikki C-kielen harjoitustehtävätkin tulevat olemaan erilaisten funktioiden toteutuksia.
Ensimmäinen C-ohjelma¶
Tarkastellaan aluksi yleisesti C-kielisen ohjelman rakennetta. Nyt ei vielä tarvitse tietää mitä kaikkea allaolevassa koodissa tapahtuu, vaan hahmottaa C-ohjelman rakenteen neljä oleellista osaa.
/**************************************
* Ensimmäinen c-ohjelma
**************************************/
/*
* Esikääntäjäkäskyt
*/
// Kirjasto stdio mukaan ohjelmaan
#include <stdio.h>
// Vakion PI esittely
#define PI 3.14159
/*
* Ohjelman sisäisten funktioiden ja muuttujien esittely
*/
// Esitellään oma funktio laske_ala prototyypin avulla
float laske_ala(float sade);
/*
* Pääohjelman main toteutus
*/
int main() {
// Pääohjelman sisäisten muuttujien esittely (ja alustus)
float ympyran_ala = 0.0, ympyran_sade = 0.0;
// Pääohjelman 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);
// Pääohjelman (funktion) palautusarvo
return 0;
}
/*
* Funktio: laske_ala, laskee ympyrän alan annetusta säteestä
* Argumentit: sade, 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 palautusarvo
return pinta_ala;
}
Katsotaanpas siis mistä osista C-kielinen ohjelma rakentuu:
1. Esikääntäjäkäskyt ohjaavat ohjelman käännösprosessia ja niillä on useita eri tarkoituksia. Tässä vaiheessa tarvitsee tietää:
1. Esikääntäjäkäskyt ohjaavat ohjelman käännösprosessia ja niillä on useita eri tarkoituksia. Tässä vaiheessa tarvitsee tietää:
- Voidaan liittää C-ohjelmaan omia ja kääntäjäympäristön valmiita kirjastoja.
- Esimerkissä käytetään C-kirjastoa stdio, joka tarjoaa funktioita syötteen ja tulostuksen käsittelyyn.
- Voidaan määritellä vakiota ja makroja ohjelmiin.
- Esimerkissä luotiin vakio PI ohjelman käyttöön.
2. Ohjelman sisäisten funktioiden ja muuttujien esittely prototyypin avulla. Prototyyppi kertoo ohjelmalle millaisia funktiota sen käytössä on.
- Esimerkin prototyyppi
float laske_ala(float sade);
kertoo että käytössä on laske_ala-niminen funktio ja sen että se ottaa argumenteikseen ja palauttaa liukulukuja. - Itseasiassa kun liitämme kirjastoja C-ohjelmaan, usein niiden toteutuksista liitetään vain funktioiden prototyypit, vakiot, ym määritykset. Tästä lisää kohta..
3. Pääohjelman
main
-funktion toteutus.- Esimerkissä pääfunktiossa kysytään käyttäjältä ympyrän säteen ja lasketaan funktion laske_ala avulla sen ala.
4. Ohjelmassa esiteltyjen sisäisten funktioiden toiminnallisuus.
- Esimerkissä funktion
laske_ala
-toteutus eli ympyrän alan laskeminen, kun säde annetaan.
Funktion prototyyppi¶
Funktion esittely C-kielisessä ohjelmassa tehdään sen prototyypin kautta. Prototyyppi on yleisesti muotoa:
paluuarvon_tyyppi
, minkätyyppisen paluuarvon funktio palauttaa suorituksen loputtua.funktion_nimi
, nimi millä funktiota kutsutaan ohjelmassa.( muuttujatyyppi1 muuttuja1, muuttujatyyppi2 muuttuja2, ...)
, näillä määrityksillä funktio kertoo minkätyyppisiä parametrejä se ottaa vastaan ja millä muuttujanimellä niitä voidaan funktiossa käsitellä.
Esimerkki. Funktio nimeltään
laske_ala
palauttaa float-tyypin liukuluvun ja ottaa sisäänsä float-tyypin liukuluvun (muuttujassa sade). // Esitellään oma funktio laske_ala prototyypin avulla
float laske_ala(float sade);
Miksi prototyyppi?¶
Okei, mutta miksi tarvitaan prototyyppiä? Menemättä syvälle kääntäjien maailmaan, todetaan että C-kielessä prototyyppi mahdollistaa funktion käytön ohjelmassa ennenkuin sen koodi on käännetty. Esimerkissä siis
main
-funktiossa voidaan jo käyttää laske_ala
-funktiota ennenkuin sen koodi on tullut kääntäjää vastaan, kun se käy läpi C-kooditiedostoa. Kääntäjä siis luottaa tässä ohjelmoijan lupaukseen, että prototyypin mukainen funktion toteutus tulee vielä joskus käännösprosessin aikana vastaan. Kääntäjä tarkistaa prototyypin löydettyään vain funktiokutsun syntaksin, eli että kutsutaan funktioita oikein nimen ja parametrien osalta.
Tästälähin, kurssilla, luodessamme funktioita muistamme ensin aina esittelyn prototyypillä! Jos toteutusta ei löydy ohjelman kooditiedostoista tai mukaan otetuista kirjastoista, kääntäminen loppuu virheilmoitukseen. Hyvin käyttäytyvät kääntäjätkin lopettavat koko käännösprosessin ja varoittavat jos prototyyppi ohjelmasta puuttuu..
Koodimesimerkissä yllä:
// prototyyppi lupaa että tällainen funktio löytyy
float laske_ala(float sade);
// Prototyypin avulla voidaan funktiota käyttää
int main() {
ympyran_ala = laske_ala(ympyran_sade);
}
// itse funktion toteutus
float laske_ala(float sade) {
float pinta_ala = 0.0;
pinta_ala = PI * sade * sade;
return pinta_ala;
}
Palataan vielä esikääntäjäkäskyihin sen verran, että itseasiassa käsky
#include <stdio.h>
hakee ohjelmaamme käyttämiemme scanf
- ja printf
-funktioiden prototyypit. Näin saamme vastaavasti tuotua valmiita kirjastofunktioita käyttöön ohjelmassamme prototyypin avulla.Hox! C-kielen harjoitustehtävätkin edellyttävät prototyyppiä tehtävävastauksessa..
Funktion parametrit¶
Funktion parametrit ovat funktion sisäisten muuttujien esittelyjä sekä funktion palautusarvon tyypin määrittely. Parametri, ja sen funktiolle välittyvä arvo eli argumentti, on siis tapa välittää tietoa funktiolle sitä kutsuvasta koodista ja ulos funktiosta sitä kutsuvaan koodiin.
- Parametrit voivat olla mitä vain C-kielen standardoituja muuttujatyyppejä
int, long, double, float, char, struct
sekä johdettuja muuttujatyyppejä ja taulukkoja. - Jos parametrejä on useita, ne erotetaan pilkulla.
- Funktio voi ottaa sisäänsä erityyppisiä parametrejä.
Esimerkkikoodissa yllä esitellään funktio niin, että se ottaa sisäänsä yhden liukuluku-tyyppisen argumentin.
float laske_ala(float sade);
Mikäli funktio ei ota argumentteja tai ei palauta arvoa, käytetään selvyyden vuoksi niiden paikalla esittelyssä varattua sanaa
void
. void en_ole_palauttamassa(uint8_t x, uint8_t y);
int32_t en_tarvi_argumenttia(void);
void haluun_olla_rauhassa(void);
Sinällään
void
-sana ei ole pakollinen tällaisissa funktion esittelyissä, mutta kääntäjät tällöin olettavat funktioista asioita.. esimerkiksi ilman void
ia kääntäjä voi olettaa sen palauttavan int-tyypin kokonaisluvun ja tästä voi seurata varoituksia / virheilmoituksia. Selvyyden vuoksi on syytä aina käyttää void
-sanaa tarpeen mukaan. Hox! Esimerkissä
main
-funktio on toteutettu ohjelmaan ilman parametrien määrityksiä. Jossain kehitysympäristöissä työasemaohjelmoinnissa törmäätte main-funktioihin, joille on määritelty parametrit. Nämä sisältävät ohjelmalle käynnistäessä annetut komentoriviparametrit. Tämä on työasemalla ihan ok, mutta sulautetuissa järjestelmissä tämä ei ole yleisesti käytössä ja siksi sitä ei kurssiesimerkeissä näy.Funktion toteutus¶
Funktion toteutus tehdään modulaarisesti oman koodilohkonsa sisälle, joka C-kielessä merkitään/rajataan aaltosulkeilla omaksi toiminnalliseksi kokonaisuudekseen. (Pythonin syntaksissa aaltosulkeita vastaava rajaus tehtiin tabulaattorin avulla.)
/*
* Funktio: laske_ala, laskee ympyrän alan annetusta säteestä
* Argumentit: sade, 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 palautusarvo
return pinta_ala;
}
Funktion toteutus jakaantuu sekin kolmeen osaan:
- Funktiossa yleensä esitellään sisäisiä, ts. paikallisia, muuttujia.
- Paikalliset muuttujat ovat olemassa vain funktion sisällä sen suorituksen ajan ja ne katoavat kun suoritus loppuu.
- Yllä esimerkissä siis muuttuja
pinta_ala
on paikallinen muuttuja. - C-kielen nykyinen standardi sallii kyllä muuttujien esitellyn keskellä koodia, mutta edelleen aloittelevan ohjelmoijan on selkeintä on esitellä muuttujat kootusti funktion toteutuksen alussa.
- Funktion toteutus C-kielen lauseina, jotka päättyvät puolipisteeseen.
- Funktion palautusarvon asetus.
- Esimerkissä palautetaan siis paikallisen muuttujan
pinta_ala
:n arvo.
Funktion toteutus paketoidaan aaltosulkujen sisään.
Funktion palautusarvo¶
C-kielessä funktio voi
return
-lauseella palauttaa vain yhden arvon, joka on tyypillisesti funktion tulos ja/tai tieto siitä miten funktion suoritus onnistui. Palautusarvo voi olla mitä tahansa C-kielen muuttujatyyppiä, paitsi taulukko (palataan tähän). Esimerkissä yllä funktio
laske_ala
palauttaa sisäisen muuttujan pinta_ala
arvon, jonka main
-funktiossa ottaa talteen sen sisäinen muuttuja ympyran_ala
. int main() {
float ympyran_ala = 0.0;
ympyran_ala = laske_ala(ympyran_sade);
}
C-kielessä
main
-funktio on erikoistapaus, koska sitä kutsuu käyttöjärjestelmä / firmware, ja sen suorituksen loputtua myös ohjelman suoritus loppuu. Näinollen sen palautusarvolla voidaan käyttöjärjestelmälle kertoa tietoa mitä ohjelman suorituksessa tapahtui, esimerkiksi menikö kaikki ok funktion suorituksessa vai tapahtuiko jokin virhe. Tässä syystä
main
-funktiot yleensä palauttavat numeroarvon viimeisenä käskynään. Tässä sovitusti nolla
kertoo, että ohjelman suoritus onnistui virheittä ja muut palautusarvot tarkottavat jotain virhettä suorituksessa, joka ei välttämättä siis kaatanut ohjelmaa. Tällaiset virhekoodit on käyttöjärjestelmän / firmwaren määrittelemiä ja niihin ei kurssilla mennä syvemmälti.int main() {
...
// homma ok
return 0;
}
Funktion kutsuminen¶
C-kielen syntaksissa funktiota kutsutaan, kuten muissakin kielissä, nimellä ja antamalla parametrien arvot.
int main() {
float ympyran_sade = 2.45;
ympyran_ala = laske_ala(ympyran_sade);
}
C-kielessä pitää funktiokutsussa olla tarkempi kuin joissain muissa kielissä.
- Funktiolle pitää kutsuessa antaa täsmälleen oikea määrä argumentteja oikeassa tyypissä. Jos muuttujat eivät ole haluttuja tyyppiä, kääntäjäparka joutuu itse tekemään muunnoksen tyypistä toiseen ja tulos voi olla muuta kuin toivottu.
- C-kielessä ei voi antaa parametreille oletusarvoa.
Lisäksi, C-kielen funktiokutsussa voisi olla esimerkiksi muuttujan tilalta vaikka toinen funktiokutsu, jonka palautusarvo näin ollen välitettäisiin kutsuttavalle funktiolle argumenttina. C-kielessä voidaan siis ketjuttaa funktioita.
kutsu_minua(kutsutaan_kaveria(hei_palauta_jotain(eiku_palauta_sina())));
Sulautetut funktiot¶
Sulautettuja järjestelmiä ohjelmoidessa meidän täytyy tietää hieman lisää funktioiden sielunelämästä ollaksemme tehokkaita ohjelmoijia.
C-kielen ominaisuus on, että kääntäjä tekee funktion 'argumenteistä funktion sisäisen muuttujan, jonne argumentin arvo kopioituu. Tässä onkin iso sudenkuoppa, koska laitteissa on usein vain vähäinen määrä muistia. Tällöin muisti on (valitettavan) helppoa käyttää loppuun ohjelman ajonaikana, jolloin tyypillisesti ohjelma kaatuu muistiosoitusvirheeseen. Työasemaohjelmoinnissa tämä asia ei ole niin kriittinen, koska muistia on gigatavuja vs muutama kilotavu sulautetussa laitteessa.
Tarkastellaan asiaa esimerkin kautta. Alla siis meillä on
main
-funktiossa muuttuja ympyran_sade
, joka annetaan laske_ala
-funktiolle parametriksi paikalliseen muuttujaan sade
. Näin ollen argumentin arvo kopioituu eli on ohjelman ajon aikana tallessa kahdessa eri muuttujassa. int main() {
...
float ympyran_sade = 2.45;
ympyran_ala = laske_ala(ympyran_sade);
...
}
float laske_ala(float sade) {
...
}
Okei no tässä ei nyt ole isompaa hätää, koska liukulukumuuttuja tyyppiä
float
tarvitsee vain 4 tavua muistia.Mutta dramatisoidaanpa asiaa esittelemällä funktio
laheta_viesti
, joka ottaa argumentikseen kahden kilotavun kokoisen taulukon. // funktion prototyyppi
void laheta_viesti(char viesti[2048]);
...
// esitellään iiiiiso taulukko
char viesti_kotiin[2048];
...
// kutsutaan funktiota
laheta_viesti(viesti_kotiin);
...
Nyt funktiossa
laheta_viesti
tehtäisiin siis 2048 merkin taulukosta kopio paikalliseen yhtä suureen taulukkoon ja muistia kuluisi yhteensä 4096 tavua, tilapäisesti aina kun funktiota kutsutaan. Vaara piilee juuri tässä, eli jossain vaiheessa ohjelman suortista meillä saattaisi olla riittävästi muistia ohjelman käytössä, mutta sen edetessä jossain toisaalla muistia ei enää olekaan jäljellä. Tällainen hiljaiseen virhe on hankala koska sen syy ei ole käyttäjälle (..tai ohjelmoijalle) ilmeinen.Auts.. no mites sitten taulukkoja tai isoja tietomääriä voidaan välittää funktioille? Palaamme hetken päästä kun esittelemme uusia muuttujatyyppejä ja vielä myöhemmässä materiaalissa tulevat tutuiksi osoitinmuuttujat.
Uusia muuttujatyyppejä¶
Yllä todettiin, että funktion sisällä määritellyt paikalliset muuttujat ovat olemassa vain siinä funktiossa missä ne esitellään sen suorituksen ajan. 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¶
Globaali muuttuja määritellään kaikkien funktio-määritysten ulkopuolella, jolloin se on käytettävissä kaikkialla koodimoduulin/tiedoston funktioissa ihan samoin kun funktioiden paikallset muuttujat, sillä erolla ettei globaalia muuttujaa tarvitse esitellä funktion toteutuksen alussa.
Esimerkki globaalin muuttujan käytöstä.
/*
* Ohjelman sisäisten muuttujien ja funktioiden esittely
*/
float ou_jes_olen_globaali = 3.14;
/*
* Pääohjelman main toteutus
*/
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;
tulosta_muuttujat(olen_paikallinen_main);
return 0;
}
/*
* Funktio: tulosta_muuttujat
*/
void tulosta_muuttujat(float olen_argumentti) {
float olen_paikallinen = 3.14;
printf("Tulostetaan funktiossa argumentti: %f\n", olen_argumentti);
printf("Tulostetaan funktiossa paikallinen muuttuja: %f\n", olen_paikallinen );
printf("Tulostetaan funktiossa globaali muuttuja: %f\n", ou_jes_olen_globaali );
}
Hox! Jos globaali muuttuja esitellään uudestaan funktion toteutuksessa, se itseasiassa yliajaa globaalin muuttujan esittelyn funktion sisällä ja luo uuden paikallisen samannimisen paikallisen muuttujan.. hups.
Globaalien muuttujien käyttämiseen yleisesti on kaksi syytä:
- Ne ovat elossa koko ohjelman suorituksen ajan ja niiden käyttöalue ohjelmakoodissa on laajempi, ts. kaikki modulin funktiot.
- Niitä voidaan käyttää funktioiden parametrien ja paluuarvon sijaan säästämään muistia. Sulautettujen ohjelmoinnissa kätevää välittää tietoa ohjelman eri funktioiden välillä, ilman että argumenteistä tehtäisiin kopioita jatkuvalla syötöllä.
- Tässä onkin yksi keino käsitellä taulukkoja sulautetussa C-ohjelmassa, kun tehdään niistä globaaleja, niin ei tarvitse välittää niitä funktion parametreinä.
Tämän seurauksena tosin virheiden jäljittäminen ohjelmassa voi vaikeutua, koska no.. ne ovat käytettävissä todellakin kaikkialla ohjelmassa ja sikäli hyönteisten jäljitys vaikeutuu. Globaalit muuttujat myös aiheuttavat riippuvuuksia toiminnallisuuksien (funktioiden) välille, joten ohjelman modulaarisuudesta voidaan joutua tinkimään ja koodiin voi ilmestyä vaikeaselkoisia bugeja..
Lopuksi vielä tiedoksi, että globaaleja muuttujia voidaan myös välittää laajemmissa C-ohjelmissa koodimoduleista toiseen, mutta tähän liittyy isoja sudenkuoppia, joten emme käsittele asiaa tarkemmin..
Staattinen muuttuja¶
Staattinen muuttuja on myös hyödyllinen jossain tapauksissa. Ideana on että muuttuja alustetaan kerran ja se säilyttää arvonsa funktiokutsujen välillä. Tällöin muuttujan esittelyssä käytetään määrettä
static
. Esimerkkinä funktion paikallisen muuttujan käyttö laskurina.
void laskuri(void) {
static uint64_t lukumaara=0;
lukumaara++;
}
Funktiossa muuttujan
lukumaara
arvo lähtee alustusarvosta nollasta ja kasvaa yhdellä aina kun funktiota kutsutaan. Vaikka muuttujan arvo säilyy, se on edelleen vain paikallinen muuttuja eikä käytettävissä funktion ulkopuolella.Tiedoksi, että
static
-määreellä on muitakin käyttötapoja, mutta emme mene niihin 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 pienempiin funktioihin. Tämä maksaa loppupeleissä vaivan ohjelman virheiden metsästyksen, koodin ylläpidettävyyden ja lukukelpoisuuden myötä.
Funktioilla pelaaminen pääsee itseasiassa vauhtiin vasta C-kielen standardi- ja oheislaitteiden kirjastoihin tutustuessa sekä omia ohjelmia tehdessä!
Anna palautetta
Kommentteja materiaalista?