Kirjautuminen

Haku

Tehtävät

Oppaat: C++-ohjelmointi: Osa 5 - Funktiot

  1. Osa 1 - Johdanto
  2. Osa 2 - Vakiot, muuttujat ja perustietotyypit
  3. Osa 3 - Ehdot, silmukat ja poikkeukset
  4. Osa 4 - Rakenteet, taulukot ja merkkijonot
  5. Osa 5 - Funktiot
  6. Osa 6 - Esittelyt, määrittelyt ja elinajat
  7. Osa 7 - Viittaukset, osoittimet ja dynaaminen muisti
  8. Osa 8 - Mallit
  9. Osa 9 - Luokkien perusteet
  10. Osa 10 - Luokkien erikoiset jäsenet
  11. Osa 11 - Luokkien perintä
  12. Liite 1 - C++-kehitysympäristöt
  13. Liite 2 - Valmis C++-kääntäjäpaketti

Kirjoittaja: Metabolix (2009).

Funktiot ovat väistämätön osa C++-kieltä, koska niihin kirjoitetaan käytännössä kaikki ohjelman toiminnallinen koodi. Yksi funktio on jo kokonaisuudessaan esitelty: jokainen esimerkkiohjelma on sisältänyt main-funktion, joka tunnetaan myös nimellä pääohjelma. Muita funktioita taas toisinaan nimitetään aliohjelmiksi, joskin yleensä käytetään vain sanaa funktio tai myöhemmin luokkien yhteydessä sanaa metodi. Funktioita kutsutaan tietyillä argumenteilla, joiden arvot tulevat funktion parametreiksi. Funktiot voivat suorittaa toimintoja tai laskea ja palauttaa jonkin arvon.

Funktion osat

Funktiossa on neljä tärkeää osaa: paluuarvon tyyppi, funktion nimi, parametrit sekä varsinainen funktion sisältämä koodi. Paluuarvon tyyppi voi olla mikä tahansa muuttujatyyppi tai luokka, tai se voi olla void eli tyhjä. Arvo palautetaan return-lauseella, kuten seuraavassa esimerkissä. Funktioiden nimiin taas pätevät samat säännöt kuin muuttujien nimiin: ne voivat sisältää merkkejä A–Z, a–z, 0–9 ja _. Parametreja on nolla tai enemmän. Ne näyttävät normaaleilta muuttujilta ja funktion kannalta ovatkin sitä, mutta niiden alkuarvot annetaan yleensä argumentteina funktion ulkopuolelta.

Yksinkertainen funktio voisi näyttää tältä:

// paluuarvon tyyppi, float
// funktion nimi, funktio
// parametrit, const float parametri ja const float toinen
float funktio(const float parametri, const float toinen) {
	// return-lauseella palautetaan arvo
	return (parametri + toinen) / 2;
}

Tämä funktio ottaa kaksi float-tyyppistä argumenttia, jotka sijoitetaan const float -tyyppisiin parametreihin parametri ja toinen. Parametrit on määritelty vakioiksi, koska niitä ei ole tässä funktiossa tarpeen muuttaa. Funktio laskee parametriensa keskiarvon ja palauttaa sen.

Esitetty funktio on ohjelmointitavoiltaan huono, koska sen nimi ei kerro, mitä se tekee, ja parametritkin on nimetty yhtä huonosti. Vastaava funktio näyttäisi järkevämmin kirjoitettuna tältä:

float keskiarvo(const float a, const float b) {
	return (a + b) / 2;
}

Tässä tapauksessa parametrien nimien ei tarvitse olla kummempia, koska on selvää, että funktio nimeltä keskiarvo laskee näiden lukujen keskiarvon. Jos taas parametreilla olisi omat, erilaiset merkityksensä, ne olisi hyvä nimetä tarkemmin.

Funktion paluuarvo

Funktion sisällä oleva return-rivi lopettaa funktion suorituksen ja palauttaa määrätyn arvon. Tämä arvo päätyy sinne, mistä funktiota on kutsuttu. Funktion paluuarvoa voi käyttää koodissa aivan kuten mitä tahansa vakion, muuttujan tai laskulausekkeen arvoa. Äskeistä keskiarvo-funktiota voisi siis hyödyntää vaikkapa näin:

#include <iostream>

float keskiarvo(const float a, const float b) {
	return (a + b) / 2;
}

int main() {
	float l1, l2;
	std::cout << "Anna kaksi lukua omilla riveillään: " << std::endl;
	std::cin >> l1 >> l2;
	// Funktiokutsussa l1 ja l2 ovat argumentteja,
	// niiden arvot päätyvät funktion parametreille.
	std::cout << "Lukujen keskiarvo on " << keskiarvo(l1, l2) << std::endl;
}

Jos funktion paluuarvon tyyppi on void, sillä ei ole lainkaan paluuarvoa eikä se myöskään tarvitse return-lausetta. Tällaisille funktioille on silti käyttöä, koska ne voivat silti tehdä jotain, vaikkapa piirtää kuvia ruudulle. Funktioiden tärkeä tarkoitus onkin eristää ohjelmasta selkeitä kokonaisuuksia.

Funktion suoritus päättyy return-lauseeseen myös silloin, kun tämä lause tulee vastaan kesken funktion. Asettelemalla koodin sopivasti voi siis säästyä funktiossa pitkiltä else-lohkoilta ja ylimääräisiltä sisennyksiltä. Seuraavan esimerkin else-lohko on tosin niin lyhyt, ettei asialla ole sen kohdalla juuri merkitystä.

// Tämä funktio palauttaa annetuista arvoista pienemmän.
int pienempi_A(const int a, const int b) {
	if (a < b) {
		// Ehdon toteutuessa suoritus päättyy tähän ja palautetaan a.
		return a;
	} else {
		// Muuten palautetaan b:n arvo.
		return b;
	}
}
// Tämä funktio toimii täsmälleen samalla tavalla.
int pienempi_B(const int a, const int b) {
	if (a < b) {
		// Ehdon toteutuessa suoritus loppuu tähän.
		return a;
	}
	// Jälkimmäiseen return-lauseeseen päästään vain silloin,
	// kun ensimmäistä ei suoriteta.
	return b;
}
// Tällä kertaa funktiota voi lyhentää vielä ?:-rakenteella.
int pienempi_C(const int a, const int b) {
	return a < b ? a : b;
}

Merkittävä poikkeus paluuarvon suhteen on main-funktio: Sen paluuarvon tyyppi on int, joten loogisesti sen pitäisi palauttaa arvo. Silti C++:n standardin mukaan main-funktion lopussa ei välttämättä tarvitse olla return-lausetta, ja jos se puuttuu, kääntäjä lisää automaattisesti lauseen return 0. Tämä poikkeus koskee vain main-funktiota; muiden kohdalla return-lauseen puuttuminen tarkoittaa, että paluuarvo on määrittelemätön – se voi olla siis mitä tahansa satunnaista, mikä on ohjelman toiminnan kannalta ikävä asia.

Kuormitus

C++ sallii funktioiden (yli)kuormittamisen (engl. overloading) eli sen, että koodissa on monta samannimistä funktiota. Näiden funktioiden täytyy erota toisistaan parametriensa osalta, jotta olisi selvää, mitä funktiota pitää kutsua. Esimerkiksi nämä kaksi funktiota tulostavat kumpikin parametriensa keskiarvon:

void tulosta_keskiarvo(const float a, const float b) {
	std::cout << (a + b) / 2 << std::endl;
}
void tulosta_keskiarvo(const float a, const float b, const float c) {
	std::cout << (a + b + c) / 3 << std::endl;
}

Tässä tapauksessa kääntäjän on helppo tietää, kumpaa funktiota ohjelmoija haluaa käyttää, koska parametrien määrä on eri. Ero voisi olla myös parametrien tyypeissä. Kuormittamisen ansiosta esimerkiksi tulostukseen käytetty <<-operaattori toimii kaikenlaisilla tietotyypeillä – tekstillä, kokonaisluvuilla ja liukuluvuilla.

Oletusargumentit

Funktioille voi asettaa myös oletusargumentteja, jolloin osan argumenteista voi funktiokutsussa jättää antamatta. Keskiarvofunktion kanssa tämä ei tule kysymykseen – oletusarvohan vaikuttaisi keskiarvoon! Sen sijaan summa- ja tulofunktioissa oletusarvot toimivat oikein hyvin:

// Tämän funktion kaikilla argumenteilla on oletusarvona 0.
// Jos siis argumentteja ei erikseen anneta, parametrien arvoksi tulee 0.
int summa(const int a = 0, const int b = 0, const int c = 0) {
	return a + b + c;
}
// Tälle funktiolle täytyy antaa vähintään yksi argumentti.
// Kahden muun oletusarvona on 1, jotta kertolaskusta saadaan oikea tulos.
int tulo(const int a, const int b = 1, const int c = 1) {
	return a * b * c;
}

Nyt summafunktiolle voi antaa valinnan mukaan 0–3 argumenttia, ja loput ovat oletusarvoisesti nollia eivätkä siksi muuta summaa. Vastaavasti tulofunktion argumentit ovat oletusarvoisesti ykkösiä eivätkä vaikuta kertolaskun tulokseen.

summa();        // 0 + 0 + 0 = 0
summa(1);       // 1 + 0 + 0 = 1
summa(1, 2, 4); // 1 + 2 + 4 = 7
tulo(6);        // 6 * 1 * 1 = 6
tulo(2, 3, 0);  // 2 * 3 * 0 = 0

Annetut argumentit menevät aina funktion ensimmäisille parametreille, koska kääntäjällä ei ole luotettavaa tapaa arvata, mille muillekaan ne pitäisi laittaa. Siksi funktiolle ei myöskään voi määritellä oletusparametreja vain keskelle parametrilistaa, vaan ne pitää määritellä kaikille lopuille parameteille.

Inline-funktiot

Funktion kutsuminen ja funktiosta takaisin palaaminen vievät hieman aikaa – eivät kovin paljon, mutta jos funktio on hyvin lyhyt ja sitä kutsutaan todella usein, tämäkin aika alkaa merkitä jotain. Tällaisessa tilanteessa kääntäjälle voi antaa vihjeen, että olisi kätevää kopioida funktion toiminnot suoraan kutsupaikalle niin, että erityistä funktiokutsua ei tulisikaan. Vihje annetaan sanalla inline.

On hyvä muistaa, että inline ei aina ole hyväksi. Jos funktio sisältää vain muutaman vertailun ja yksinkertaisen laskun, tästä voi olla hyötyä. Jos sen sijaan funktiossa tehdään pitkiä ja mutkikkaita toimituksia, tällaisesta optimoinnista ei ole mitään hyötyä, ja lisäksi tuotetun ohjelmatiedoston koko voi kasvaa huomattavasti, kun yksi pitkä funktio on turhaan kopioitu moneen kohtaan ohjelmassa.

Seuraavassa esimerkissä pieni funktio on määritelty inline-funktioksi, jolloin kääntäjä voi halutessaan optimoida sen operaatiot muun koodin sekaan.

#include <iostream>

// Tämä funktio tekee melko pienen operaation,
// joten tämän voi hyvin määritellä inlineksi.
inline int pienin(int a, int b, int c) {
	if (a < b) {
		// a < b, joten b ei voi olla pienin.
		// Verrataan a:ta ja c:tä.
		if (a < c) {
			return a;
		}
		return c;
	}
	// a < b ei ollut tosi, joten a ei ole pienin.
	// Verrataan b:ta ja c:tä.
	if (b < c) {
		return b;
	}
	return c;
}

int main() {
	int l1 = 0, l2 = 0, l3 = 0;
	std::cout << "Anna kolme kokonaislukua, esimerkiksi 13 74 32: ";
	std::cin >> l1 >> l2 >> l3;

	// Käytetään inline-funktiota aivan kuten mitä tahansa muutakin.
	// Kääntäjä voi halutessaan kopioida pienin-funktion toimenpiteet tähän
	// ja korvata koodista muuttujat (a, b, c) argumenteilla (l1, l2, l3).
	// Näin säästyy hieman aikaa, kun koneen ei tarvitse hypätä toiseen
	// kohtaan ohjelmassa vaan funktio on kopioitu tänne. (Kun funktiota
	// kutsutaan vain kerran, tällä säästöllä ei ole kerta kaikkiaan mitään
	// merkitystä.)
	std::cout << "Pienin on " << pienin(l1, l2, l3) << "." << std::endl;
}

Inline-funktioihin palataan uudestaan luokkien yhteydessä, jolloin niillä on käytännöllisempiä sovelluksia.

Funktiot ja poikkeukset

Funktioiden yhteydessä on joskus tapana kertoa, mitä poikkeuksia niistä voi heittää. Tämä tapahtuu lisäämällä funktion esittelyn perään poikkeusmäärittely, joka sisältää sallittujen poikkeustyyppien nimet. Muunlaisen poikkeuksen heittäminen ulos funktiosta johtaa tällöin ohjelman väkivaltaiseen sulkeutumiseen riippumatta siitä, olisiko funktion kutsuja valmis nappaamaan poikkeuksen.

Harvemmin esiintyvä erikoisuus on, että funktion rungon voi korvata suoraan try-lohkolla, kuten seuraavassa koodissa on tehty.

#include <iostream>

// Funktio poikkeusmäärittelyn kanssa: sallitaan char, int, float ja double.
void heittaja(char p) throw(char, int, float, double) {
	// c: heitetään char
	if (p == 'c') throw 'a';
	// i: heitetään int
	if (p == 'i') throw 112358;
	// f: heitetään float
	if (p == 'f') throw 1.23f;
	// Muuten heitetään double-luku.
	throw 3.14;
}

// Välikäsi nappaa, mitä heittäjä heittää, ja heittää ne edelleen kutsujalle.
// Tällä kertaa heitettäväksi sallitaan vain char, int ja float!
// Nyt funktion runko onkin korvattu pelkällä try-lohkolla.
// Funktio kutsuu heittäjää, sieppaa char-, int- ja double-poikkeukset,
// tulostaa niistä ilmoituksen ja heittää ne edelleen. Kuitenkin float-poikkeus
// jätetään sieppaamatta, jolloin se lentää suoraan kutsuvalle funktiolle.
// Lisäksi doublen heittäminen johtaa ohjelman sulkeutumiseen, koska sen
// heittämistä tästä funktiosta ei ole sallittu.
void valikasi(char p) throw(char, int, float)
try {
	heittaja(p);
} catch (char c) {
	std::cout << "Siepattiin char, " << c << ", heitetään..." << std::endl;
	throw;
} catch (int i) {
	std::cout << "Siepattiin int, " << i << ", heitetään..." << std::endl;
	throw;
// } catch (float f) { // Jätetään float sieppaamatta, jollon se lehtää ohi
} catch (double d) {
	std::cout << "Siepattiin double, " << d << ", heitetään..." << std::endl;
	// Yritetään heitetään double eteenpäin; funktio saa heittää vain
	// char-, int- ja float-poikkeuksia, joten ohjelman suoritus päättyy
	// nyt virheeseen.
	throw;
}

int main() {
	char p[4] = {'c', 'i', 'f', 'd'};
	for (int i = 0; i < 4; ++i) {
		try {
			std::cout << "valikasi('" << p[i] << "');" << std::endl;
			valikasi(p[i]);
		} catch (char c) {
			std::cout << "Tuli char: " << c << std::endl;
		} catch (int i) {
			std::cout << "Tuli int: " << i << std::endl;
		} catch (float f) {
			// Koska välikäsi ei sieppaa float-poikkeuksia, ne
			// saadaan ensimmäisen kerran kiinni vasta täällä.
			std::cout << "Tuli float: " << f << std::endl;
		} catch (double d) {
			// Tänne ei koskaan päästä, koska välikäsi ei saa
			// heittää kuin char-, int- ja float-poikkeuksia.
			std::cout << "Tuli double: " << d << std::endl;
		}
		std::cout << std::endl;
	}
}

Ohjelman argumentit

Tähän asti main-funktioiden parametrilistat ovat olleet aivan tyhjiä. Myös main-funktiolle voi määritellä tietyt parametrit, joiden avulla pääsee käsiksi ohjelman käynnistyksen yhteydessä annettuihin, tekstimuotoisiin argumentteihin. Ensimmäinen parametri kertoo komentorivillä annettujen arvojen määrän, toinen taas on osoitin itse arvoihin, jotka ovat C-tyylisiä merkkijonoja eli merkkitaulukoita, joiden lopussa on nollamerkki. Osoittimia käsitellään lisää opassarjan myöhemmissä osissa.

Seuraava esimerkkiohjelma tulostaa kaikki komentoriviparametrit. Jos järjestelmä antaa parametreja, ensimmäinen näistä on aina ohjelman ajamiseen käytetty komento, esimerkiksi "ohjelma.exe".

#include <iostream>

int main(int param_lkm, char** param_teksti) {
	for (int i = 0; i < param_lkm; ++i) {
		std::cout << i << ": " << param_teksti[i] << std::endl;
	}
}

Ohjelman voi ajaa esimerkiksi tällaisella komennolla:
./ohjelma.exe kas "parametrit ovat hauskoja"
Tällöin ohjelma tulostaa seuraavat rivit:

0: ./ohjelma.exe
1: kas
2: parametrit ovat hauskoja

Kommentit

punppis [16.07.2009 00:02:21]

#

Muuten hyvä opas, mutta tuo poikkeuskohta menee yli hilseen. En tajua yhtään mitä siinä yritetään ajaa takaa. Ohjelma kaatuu kun yritän kääntää koodia g++:lla.

Mikä hyöty tästä try/catch/throw -härdellistä on? Käytännön esimerkki kiiitos.

fronty [14.09.2009 23:26:05]

#

Käytännön esimerkiksi vaikkapa päivämääräluokka. Kun luo uuden olion tähän luokkaan, ctorilla annetaan päivä, kuukausi ja vuosi. Koska esimerkiksi 30.2. ei ole kelvollinen päivä, ctor tarkistaa aina päivän kelvollisuuden, ja jos päivämäärä ei ole kelvollinen, se heittää keskeytyksen. Jos kirjoittaa ohjelmaa, jolle käyttäjän täytyy syöttää päivämäärä, voi ohjelma napata sen päivämääräluokan ctorin heittämän keskeytyksen ja pyytää käyttäjää syöttämään kelvollinen päivämäärä.

Kotitehtävä nro 1: Toteuta tällainen luokka.

Jokotai [18.09.2010 18:25:41]

#

Tärkein tarkoitus kuitenkin try/catch/throwille on virheiden metsästys

Vähän myöhässä :)

tkok [28.07.2011 20:22:53]

#

Jokotai kirjoitti:

Tärkein tarkoitus kuitenkin try/catch/throwille on virheiden metsästys

Metsästyksen lisäksi niitä voi käyttää myös virhesietoisuuden lisäämiseksi?

Metabolix [28.07.2011 20:35:01]

#

punppis kirjoitti:

Muuten hyvä opas, mutta tuo poikkeuskohta menee yli hilseen. ... Mikä hyöty tästä try/catch/throw -härdellistä on? Käytännön esimerkki kiiitos.

Poikkeuksien perusasiat käsiteltiin jo aiemmassa osassa, jossa on myös käytännöllisempi esimerkki (virhetilanteen käsittely). Tässä osassa esitellään vain funktioihin liittyviä erityispiirteitä, lähinnä throw-määrettä. (Oikeasti poikkeuksissa käytetään yleensä perustietotyyppien sijaan std::exception-luokasta periytyviä olioita. Niihin varmaankin palataan myöhemmin.)

punppis kirjoitti:

Ohjelma kaatuu kun yritän kääntää koodia g++:lla.

Jos viitsisit lukea sen kommentit, tietäisit, että sen kuuluukin lopuksi kaatua.

Kirjoita kommentti

Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.

Muista lukea kirjoitusohjeet.
Tietoa sivustosta