previous next Title Contents Index

8. C++- kielen muuttujista ja aliohjelmista


Mitä tässä luvussa käsitellään?

* muuttujat

* malliohjelma jossa tarvitaan välttämättä muuttujia

* osoiteoperaattori, &

* osoitin muuttujat ja epäsuora osoitus, *

* aliohjelmat, eli funktiot

* erilaiset aliohjelmien kutsumekanismit

* referenssimuuttujat eli viitemuuttujat, &

Syntaksi:

	Seuraavassa muut = muuttujan nimi, koostuu A-Z,a-z,0-9,_, ei ala 0-9
	muut.esittely:    tyyppi muut = alkuarvo;            // 0-1 x =alkuarvo
	sijoitus:         muut = lauseke;
	lukeminen, C:     scanf(format, osoite, osoite, ...) // 0-n x osoite
	lukeminen, C++    cin >> olio >> olio;               // 1-n x olio
	aliohj.esittely:  tyyppi aliohj_nimi(tyypi muut, tyyppi muut);  // 0-n x muut
	aliohj.kutsu      muut = aliohj_nimi(arvo, arvo);    // 0-1 x muut=, 0-n x arvo
	muut.osoite:      &muut
	osoitinmuut.esit: tyyppi *pMuut_nimi
	epäsuora osoitus: *osoite
	viitemuut.esit.   tyyppi &rMuut_nimi
Jatkossa puhumme C- kielestä kun tarkoitamme ominaisuuksia, jotka ovat sekä varsinaisessa C- kielessa että C++- kielessä. Jos puhumme C++- kielen ominaisuuksista, tarkoitamme ominaisuuksia joita ei ole C- kielessä. Aloittelevan lukijan kannalta tällä jaottelulla ei niinkään ole merkitystä. Merkitystä on ainoastaan silloin, jos joutuu kirjoittamaan ohjelmaa, jonka on mentävä läpi puhtaasta C- kääntäjästä. Tällainen tilanne voi tulla vastaan esimerkiksi sulautettuja järjestelmiä ohjelmoitaessa (=järjestelmät joissa tietokone on "näkymättömässä" osassa, kodinelektroniikka, "kännykät", ohjauslaitteet). Näihin ei vielä aina ole saatavilla C++- kääntäjää.

8.1 Mittakaavaohjelman suunnittelu

Satunnainen matkaaja ajelee tällä kertaa kotimaassa. Autoillessa hänellä on käytössä Suomen tiekartan GT - karttalehtiä, joiden mittakaava on 1:200000. Viivottimella hän mittaa kartalta milleinä matkan, jonka hän aikoo ajaa. Ilman matikkapäätä laskut eivät kuitenkaan suju. Siis hän tarvitsee ohjelman, jolla matkat saadaan muutettua kilometreiksi.

Millainen ohjelman toiminta voisi olla? Vaikkapa seuraavanlainen:

	C:\OMA\MATKAAJA>matka[RET]
	Lasken 1:200000 kartalta millimetreinä mitatun matkan 
	kilometreinä luonnossa.
	Anna matka millimetreinä>35[RET]
	Matka on luonnossa 7.0 km.
	C:\OMA\MATKAAJA>matka[RET]
	Lasken 1:200000 kartalta millimetreinä mitatun matkan 
	kilometreinä luonnossa.
	Anna matka millimetreinä>352[RET]
	Matka on luonnossa 70.4 km.
	C:\OMA\MATKAAJA>
Edellisessä toteutuksessa on vielä runsaasti huonoja puolia. Mikäli samalla haluttaisiin laskea useita matkoja, niin olisi kätevämpää kysellä matkoja kunnes kyllästytään laskemaan. Lisäksi olisi ehkä kiva käyttää muitakin mittakaavoja kuin 1:200000. Muutettava matka voitaisiin tarvittaessa antaa jopa ohjelman kutsussa. Voimme lisätä nämä asiat ohjelmaan myöhemmin, kunhan kykymme siihen riittävät. Toteutamme nyt kuitenkin ensin mainitun ohjelman.

8.2 Muuttujat

Ohjelmamme poikkeaa aikaisemmista esimerkeistä siinä, että nyt ohjelman sisällä tarvitaan muuttuvaa tietoa: matka millimetreinä. Tällaiset muuttuvat tiedot talletetaan ohjelmointikielissä muuttujiin. Muuttuja on koneen muistialueesta varattu tarvittavan kokoinen "muistimöhkäle", johon viitataan käytännössä muuttujan nimellä.

Kone viittaa muistipaikkaan muistipaikan osoitteella. Kääntäjäohjelman tehtävä on muuttaa muuttujien nimiä muistipaikkojen osoitteiksi. Kääntäjälle täytyy kuitenkin kertoa aluksi minkäkokoisia 'möhkäleitä' halutaan käyttää. Esimerkiksi kokonaisluku voidaan tallettaa pienempään tilaan kuin reaaliluku. Mikäli haluaisimme varata vaikkapa muuttujan, jonka nimi olisi matka_mm kokonaisluvuksi, kirjoittaisimme seuraavan C- kielisen lauseen (muuttujan esittely):

	int matka_mm; /* yksinkertaisen tarkkuuden kokonaisluku */
Pascal - kielen osaajille huomautettakoon, että Pascalissahan esittely oli päinvastoin:
	VAR matka_mm: INTEGER;
Tulos, eli matka kilometreinä voitaisiin laskea muuttujaan matka_km. Tämän muuttujan on kuitenkin oltava reaalilukutyyppinen (ks. esimerkkiajo), koska tulos voi sisältää myös desimaaliosan:
	double matka_km; /* kaksinkertaisen tarkkuuden reaaliluku */
On olemassa myös yksinkertaisen tarkkuuden reaaliluku float, mutta emme tarvitse sitä tällä kurssilla. Samoin kokonaisluvusta voidaan tehdä etumerkillinen, etumerkitön, "lyhyt" tai "kaksi kertaa isompi":
	signed int matka_km;       /* Sama kuin int matka_km  */
	unsigned int sormia;       /* Aina positiivinen       */
	short int varpaita;        /* Ei koskaan kovin montaa */
	long int valtion_velka_Mmk;/* Tarvitaan ISO arvoalue  */
int- tyyppiä ei edellä olisi pakko kirjoittaa:
	signed matka_km;       /* Sama kuin int matka_km  */
	unsigned sormia;       /* Aina positiivinen       */
	short varpaita;        /* Ei koskaan kovin montaa */
	long valtion_velka_Mmk;/* Tarvitaan ISO arvoalue  */
Muuttujan määritys voisi olla myös
	const volatile unsigned long int sadasosia; 
Tulemme kuitenkin aluksi varsin pitkään toimeen pelkästään seuraavilla tyypeillä ja niiden osoittimilla (Turbo C++:n arvoalueet):
	int      -  kokonaisluvut - 32 768 -   32 767, 16-bit, tai
	           - -2 147 483 648 - 2 147 483 647, 32-bit systeemit
	double   -  reaaliluvut n. 15 desim. - > 1.7e308
	char     -  kirjaimet, kokonaislukuina - 128 -   127 (tai 0-255)

8.2.1 Matkan laskeminen

Ohjelman käyttämä mittakaava kannattaa sijoittaa ehkä vakioksi, tällöin ainakin ohjelman muuttaminen on helpompaa. Samoin vakioksi kannattaa sijoittaa tieto siitä, paljonko yksi km on millimetreinä (1 km = 1000 m, 1 m = 1000 mm). Ohjelmastamme tulee tällöin esimerkiksi seuraavan näköinen:

c-muut\matka.cpp - mittakaavamuunnos 1:200000 kartalta

	// matka.cpp
	// Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
	// Vesa Lappalainen 18.9.1991
	#include <iostream.h>
	
	const double MITTAKAAVA = 200000.0;
	const double MM_KM      = 1000.0*1000.0;
	
	int main(void)
	{
	  int    matka_mm;
	  double matka_km;
	
	  // Ohjeet
	  cout << "Lasken 1:" << MITTAKAAVA 
	       << " kartalta millimetreinä mitatun matkan\n";
	  cout << "kilometreinä luonnossa.\n";
	
	  // Syöttöpyyntö ja vastauksen lukeminen
	  cout << "Anna matka millimetreinä>";
	  cin >> matka_mm;
	
	  // Datan käsittely
	  matka_km = matka_mm*MITTAKAAVA/MM_KM;
	
	  // Tulostus
	  cout << "Matka on luonnossa "<< matka_km << " km." << endl;
	
	  return 0;
	}
Lukija huomatkoon, että muuttujien ja vakioiden nimet on pyritty valitsemaan siten, ettei niitä tarvitse paljoa selitellä. Tästä huolimatta isommissa ohjelmissa on tapana kommentoida muuttujan esittelyn viereen muuttujan käyttötarkoitus. Mekin pyrimme tähän myöhemmin.

Tehtävä 8.55 Vakion korvaaminen

Korvaa vakioiden esittely #define makroilla. Mitä vaaroja liittyy makrojen käyttöön tässä tapauksessa.

8.2.2 Muuttujan nimeäminen

Muuttujien nimissä on sallittuja kaikki kirjaimet (a- z, A- Z) sekä numerot (0- 9) sekä alleviivausviiva (_). Muuttujan nimi ei kuitenkaan saa alkaa numerolla. Muuttujia saa esitellä (declare) useita samalla kertaa, kunhan muuttujien nimet erotetaan toisistaan pilkulla.

Muuttujan nimi ei myöskään saa olla mikään varatuista sanoista (reserwed word):

	C-kielen varatut sanat:
	auto   	break       	case      	char
	const   	continue    	default   	do
	double      	else        	enum     	extern
	float     	for         	goto	if
	int       	long        	register	return
	short    	signed      	sizeof	static
	struct    	switch      	typedef	union
	unsigned   	void        	volatile	while
	C++:ssa on lisäksi varattuja sanoja:
	asm	bool	catch	class	
	const_cast	delete	dynamic_cast	explicit 
	false	friend	inline	mutable	
	namespace	new	operator	private	
	protected 	public	__rtti	static_cast	
	template	this	throw	true	
	try	typeid	typename 	reinterpret_cast
	using	virtual	wchar_t

Tehtävä 8.56 Varatut sanat

Merkitse edelliseen taulukkoon kunkin varatun sanan viereen se, missä kohti monistetta ko. sana on selitetty.

Tehtävä 8.57 Muuttujan nimeäminen

Mitkä seuraavista ovat oikeita muuttujan esittelyjä ja mitkä niistä ovat hyviä:
int o;
int 9_kissaa;
int _9_kissaa;
double pitkä_matka;
int i, j, kissojen_maara;
int auto, pyora, juna;

8.2.3 Muuttujalle sijoittaminen =

Muuttujalle voidaan antaa ohjelman aikana uusia arvoja käyttäen joko sijoitusoperaattoria = tai aliohjelmakutsua, joka muuttaa muuttujan arvoa osoittimen välityksellä (tai ++,- - ,+=,- =,*= jne. - operaattoreilla).

Sijoitusmerkin = vasemmalle puolelle tulee muuttujan nimi ja oikealle puolelle mikä tahansa lauseke, joka tuottaa halutun tyyppisen tuloksen (arvon). Lausekkeessa voidaan käyttää mm. operaattoreita +,- ,*,/ ja funktiokutsuja. Lausekkeen suoritusjärjestykseen voidaan vaikuttaa suluilla (ja):

	kengan_koko  = 42;
	pi           = 3.14159265358979323846;
	// usein käytetään math.h:n M_PI vakiota 
	pi           = M_PI;
	pinta_ala    = leveys * pituus;
	ympyran_ala  = pi*r*r;
	hypotenuusa  = vastainen_kateetti/sin(kulma);
	matka_km     = matka_mm*MITTAKAAVA/MM_KM; 
Seuraava sijoitus on tietenkin mieletön:
	r*r = 5.0; /* MIELETÖN USEIMMISSA OHJELMOINTI KIELISSA! */	
Eli sijoituksessa tulee vasemmalla olla sen muistipaikan nimi, johon sijoitetaan ja oikealla arvo joka sijoitetaan.

Huom! C- kielessä = merkki EI ole yhtäsuuruusmerkki, vaan nimenomaan sijoitusmerkki. Yhtäsuuruusmerkki on = =.

Tehtävä 8.58 Muuttujien esittely

Esittele edellisissä sijoitus - esimerkeissä tarvittavat muuttujat.

8.2.4 Muuttujan esittely ja alkuarvon sijoittaminen

Muuttujan esittelyn (declaration) yhteydessä muuttujalle voidaan antaa myös alkuarvo (alustus, definition). Muuttujien alustaminen tietyllä arvolla on tärkeää, koska alustamattoman muuttujan arvo saattaa olla hyvinkin satunnainen. Alustamattoman muuttujan käyttö onkin jälleen eräs tyypillinen ohjelmointivirhe.
	int    kengan_koko = 32, takin_koko = 52;
	double pi = 3.14159265358979323846, r = 5.0; 

8.3 Muuttujan arvon lukeminen päätteeltä

8.3.1 scanf ja muuttujan osoite &

Muuttujalle voidaan sijoittaa uusi arvo myös C- kielen scanf- funktiolla. Funktion kutsussa on kaksi osaa: ohje- eli format - osa ja lista muuttujien osoitteista, joihin arvot sijoitetaan. Matka olisi voitu lukea myös kutsulla:
	  scanf("%d",&matka_mm); 
	
Tässä "%d" tarkoittaa, että kutsun listaosassa vastaavassa paikassa oleva muuttuja on tyyppiä d eli desimaalinen (annetaan 10- järjestelmässä) kokonaisluku.

&matka_mm tarkoittaa muuttujan matka_mm osoitetta. Funktiolle ei voida viedä parametrina itse muuttujan arvoa, koska funktio ei tämän perusteella tiedä mihin se luetun arvon sijoittaisi. Tämän takia välitämmekin muuttujan osoitteen, eli sen paikan muistissa, missä muuttujan arvo sijaitsee. Näin aliohjelma tietää sen mihin paikkaan arvo tulee sijoittaa.

Varoitus! Yleinen virhe on unohtaa &- merkki pois scanf- funktion kutsusta.

Joissakin ohjelmointikielissä (kuten Pascal tai Fortran ja osittain myös C++:ssa) ei osoitemerkkiä kirjoiteta, koska kääntäjä kääntää vastaavat kutsut siten, että parametrina viedäänkin osoite. C- kielessä on mahdollista välittää vain arvoja parametrina.

	  int pituus,leveys;
	  printf("Anna kentän pituus ja leveys metreinä >");
	  scanf("%d %d",&pituus,&leveys); 
Koska kyseessä on funktio, niin se myös palauttaa jonkin arvon. Tässä tapauksessa palautetaan kokonaislukuarvo, joka kertoo montako onnistunutta sijoitusta pystyttiin tekemään.

c-muut\ala.c - tietojen kysyminen kunnes oikein

	/* ala.c */
	/* Ohjelmalla luetaan kentän leveys ja pituus sekä tulostetaan
	   näiden perusteella kentän ala.  Tietoja kysytään kunnes
	   molemmat tulevat oikein annetuksi.
	
	   Vesa Lappalainen 18.9.1991
	*/
	#include <stdio.h>
	int main(void)
	{
	  int pituus,leveys;
	  do {
	    printf("Anna kentän pituus ja leveys metreinä >");
	    fflush(stdin); /* Poistetaan mahd. ed. kier. ylim. merkit, epästd. */
	  } while ( scanf("%d %d",&pituus,&leveys) < 2 );
	  printf("Ala on %d m2.\n",pituus*leveys);
	  return 0;
	}
format- osassa voidaan pakottaa syöttämään tiettyjä merkkejä syöttötekstin väliin:
	scanf("%d,%d",&pituus,&leveys) 
Tällöin syöttöjä saataisiin seuraavasti:

Syöttö:

syöttöjen lkm
pituus
leveys

23,34
2
23
34

23, 45
2
23
45

23 34
1
23
alkup.

23 ,34
1
23
alkup.

,34
0
alkup.
alkup.


odottaa lisää



34
1
34
alkup.

kissa
0
alkup.
alkup.

Näin voitaisiin lukea esimerkiksi mittakaava:
	  printf("Anna mittakaava muodossa 1:2000 >");
	  scanf("1:%lf",&mittakaava); 
tai jopa:
	  printf("Anna mittakaava muodossa 1:2000 >");
	  scanf("%lf:%lf",&kerroin,&mittakaava); 
Mikäli syöttö on formaattiin nähden väärää muotoa, jäävät ne muuttujat alkuperäiseen arvoonsa, jotka ovat virheellisen syöttökohdan jälkeen. Esimerkiksi
	  scanf("1:%lf",&mittakaava); 
palauttaa 0 ja jättää mittakaava - muuttujan arvon koskemattomaksi, mikäli syöttö alkaa millä tahansa muulla kuin 1:.

Tehtävä 8.59 Mittakaavan kysyminen

Muuta matka- ohjelmaa siten, että myös mittakaava kysytään käyttäjältä. Mikäli mittakaavaan vastataan pelkkä [RET] pitää mittakaavaksi tulla 1:200000.

8.3.2 Tiedon lukeminen C++:ssa, cin

C++:ssa lukeminen voidaan suorittaa C:n scanf:n lisäksi tietovirtoja käyttäen:
	cin >> matka_mm;
Valitettavasti samanlaista syötön pakottamista kuin scanf:llä ala.c - ohjelmassa oli, ei ole helppo saavuttaa. Käytännössä tällä ei ole merkitystä, koska "oikeassa" ohjelmassa syöttö tulee lähes aina ensin merkkijonoon, josta syöttötietoa sitten aletaan käsittelemään. Pilkun syöttäminen lukujen väliin (jos se on oleellista) voitaisiin tutkia seuraavasti:

c-muut\ala.cpp - tiedon kysyminen kunnes pilkku väliin

	#include <iostream.h>
	int main(void)
	{
	  int pituus,leveys;
	  char c;
	  do {  // Ohjelma on helppo saad ikuiseen silmukkaan!
	    cout << "Anna kentän pituus ja leveys metreinä >";
	    cin >> pituus >> c >> leveys;
	  } while ( c != ',' );
	  cout << "Ala on " << pituus*leveys << " m2." << endl;
	  return 0;
	}
Hyvä puoli tietovirtojen käytössä on se, ettei helposti unohtuvia osoitemerkkejä (&) tarvita. Ohjelma ei kuitenkaan toimi idioottivarmasti, mutta kuten edellä todettiin, niin lukemalla syöttörivi ensin merkkijonoon saataisiin parempi lopputulos. Tähän tekniikkaan paneudutaan vähän myöhemmin.

8.4 Osoittimet

8.4.1 Miksi osoittimet?

C- kielessä osoittimet piti opetella heti ohjelmoinnin alussa, jos halusi tehdä minkäänlaisia järkeviä aliohjelmia. C++:ssa ongelmaa voidaan kiertää viitemuuttujien (references) avulla. Mutta vaikka emme osoittimia ihan välttämättä parametrin välityksen takia enää tarvitsisikaan, opettelemme ne seuraavaksi. Käytännössä niitä kuitenkin tarvitaan, koska maailmassa on valtavasti valmista C- koodia, jonka hyödyntämiseksi osoittimia joutuu joka tapauksessa käyttämään. Lisäksi olio- ohjelmointi pääsee C++:ssa täysiin oikeuksiinsa vasta olio- osoittimien ja polymorfismin myötä. Ja kuten aiemmin todettiin, myös taulukoiden läpikäynnissä osoittimet ovat käteviä.

Edellä sanottiin, että &matka_mm on osoite muuttujaan matka_mm. Tätä voitaisiin kuvata seuraavasti:

Kuva 8.1 Muistipaikan osoite
Reaali- ja kokonaislukujen lisäksi voidaan määritellä myös muuttujia, jotka voivat saada osoite- arvoja. Tällaisia muuttujia nimitetään osoittimiksi (eng. pointer, vrt. sormet ja korttien lajittelu). Osoite- arvon saava muuttuja siis osoittaa johonkin muistipaikkaan, eli muuttujaan. Muuttujiin voidaan sijoittaa arvoja paitsi suoraan, niin myös epäsuorasti osoittimien avulla.

8.4.2 Muuttujan arvon epäsuora muuttaminen

Tutkitaanpa vaikkapa seuraavaa ohjelmaa:

c-muut\kissaoso.cpp - epäsuora osoitus

	// kissaoso.cpp
	// Mitä ohjelma tulostaa?
	#include <iostream.h>
	int main(void)
	{
	  int kissoja,koiria;
	  int *pElaimia;   // Osoitinmuuttuja kokonaislukuun. (p = pointer)
	
	  pElaimia  = &kissoja;
	  *pElaimia = 5;
	
	  pElaimia  = &koiria;
	  *pElaimia = 3;
	
	  cout << "Kissoja on " << kissoja << " ja koiria " << koiria << "." << endl;
	
	  return 0;
	}
Osoitinmuuttuja määritellään laittamalla * muuttujan nimen eteen esiteltäessä muuttujaa. Sijoituksessa
	osoitin =                // tässä tapauksessa pElaimia =
pitää oikealla puolella olla osoite- tyyppiä oleva lauseke (esim. &kissoja).

Vastaavasti muoto *osoitin ("tähdätään osoitinta pitkin") tarkoittaa sen muistipaikan sisältöä, johon muuttuja osoitin osoittaa. Olkoon meillä muisti jakaantunut käännöksen jälkeen seuraavasti:

Kuva 8.2 Epäsuora osoitus
Osoitinmuuttujia tarvitaan erityisesti aliohjelmien ja taulukoiden yhteydessä. Tämä esimerkki on itse asiassa varsin huono! Nimenomaan tällaista moninimisyyttä (aliasing), eli sama muuttuja voi muuttua useata eri kautta, tulisi välttää, koska se on omiaan tekemään ohjelmista epäluotettavia ja vaikeasti ylläpidettäviä. Järkevämmän osoite- esimerkin otamme heti kun saamme aliohjelmat kunnolla käyttöön.

Tehtävä 8.60 Sijoitus osoittimeen

Jos edellä voitaisiin sijoittaa
pElaimia = 100;
niin mitä tapahtuisi sijoituksella
*pElaimia = 10;
Entäpä jos olisi sijoitukset
pElaimia = 106;
*pElaimia = 104;
*pElaimia = 108;
Varoitus! Edellisestä tehtävästä huolimatta ÄLÄ KOSKAAN mene itse keksimään arvoja, joita sijoitat osoitinmuuttujalle. Tämä on 99.999% varma tapa saada kone kaatumaan. Käytännössä osoittimien arvot (osoitteet) ovat edellä kuvattua esimerkkiä monimutkaisempia.

8.4.3 * ja &

Osoittimen sisällön ottaminen
	*p
on sallittu silloin, kun p on esitelty osoittimeksi johonkin muuhun tyyppiin kuin void tyyppiin. Siis

c-muut\ososij.c - miten osoitinta saa käyttää

	/* ososij.c */
	int *p,i;        ...  i = *p;  /* OK jos on ollut sijoitus p = */
	double *p,d;     ...  d = *p;  /* OK jos on ollut sijoitus p = */
	void *p; int i;  ...  i = *p;  /* VÄÄRIN */
Jos otetaan muuttujan osoite
	&i
saadaan osoitin, joka on samaa tyyppiä kuin muuttuja. Siis
	int i,*p;        p = &i;  /* OK */
	double *p,d;     p = &d;  /* OK */
	void *p; int i;  p = &i;  /* OK, koska void osoittimelle saa
	                                 sijoittaa minkä tahansa osoittimen */
Seuraavat ovat oikein, mutta niitä täytyy varoa:
	double *p; int i; 
	  p = &i;  /* VAARALLINEN */
	  i = *p;  /* OK, paikassa p oleva double muuttuu int */
Jos i ja j on esitelty vaikkapa int i,j;, ei seuraavassa ole mieltä:
	int i,j;
	j = *i; /* VÄÄRIN, koska i ei ole osoitin! */

Tehtävä 8.61 *&

Tästä tehtävästä huolimatta ei alla olevan kaltaisia sotkuja tule käyttää!
Olkoon muuttujien esittelyt kuten ohjelmassa kissaoso.c. Missä seuraavista tapauksissa tulee sijoitetuksi 5 muuttujalle kissa (*&i tarkoittaa *(&i)):
/* ososotku.c */
/* a */ *&kissoja = 5;
/* b */ &*kissoja = 5;
/* c */ *&*&*&*&kissoja = 5;
/* d */ &*pElaimia = &kissoja; *&*pElaimia = 5;
/* e */ *&pElaimia = kissoja; *pElaimia = 5;

8.4.4 NULL- osoitin

Yleensä osoittimelle ei saa sijoittaa mitään vakioarvoa. Kuitenkin eräs vakioarvo, NULL, muodostaa poikkeuksen. Jokaisen kunnollisen ohjelman tulisi aina ennen osoittimen käyttöä tarkistaa ettei osoittimen arvo ole NULL.

NULL on varattu tarkoittamaan, ettei osoittimella ole laillista osoitteena toimivaa arvoa. Yleensä vakio NULL on 0, mutta tähän ei saa liiaksi luottaa. Kuitenkin taataan, että jos p on NULL- osoitin, niin ehtolause

	if ( p ) ...
	/* on sama kuin */
	if ( p != NULL ) ... 
Erityisesti monet C- kirjaston funktioista palauttavat NULL arvoja, mikäli hommat eivät menneet niinkuin piti (vrt. malloc, fopen jne.).

8.5 Aliohjelmat (funktiot)

Eräs ohjelmoinnin tärkeimmistä rakenteista on aliohjelma. C- kielessä kaikkia eri tyyppisiä aliohjelmia nimitetään funktioiksi; joissakin muissa kielissä eri tyyppejä erotetaan eri nimille. Aliohjelmaa käytetään seuraavissa tapauksissa:
1.
Haluttu tehtävä on valmiiksi jonkun toisen kirjoittamana aliohjelmana esimerkiksi standardikirjastossa.
2.
Haluttua tehtävää suoritetaan usein liki samanlaisena joko samassa ohjelmassa tai jossain toisessa ohjelmassa.
3.
Haluttu tehtävä muodostaa selvän kokonaisuuden, jonka toiminta on ilmaistavissa muutamalla sanalla riittävän selkeästi (= aliohjelman nimi).
4.
Haluttua tehtävää ei juuri sillä hetkellä osata tai viitsitä ohjelmoida. Tällöin määritellään millainen aliohjelma tarvitaan ja kirjoitetaan tarvittavaan kohtaan pelkkä aliohjelman kutsu. Itse aliohjelma voidaan aluksi toteuttaa varsin triviaalina ja korjata myöhemmin tekemään sen varsinainen tehtävä.
5.
Rakenne saadaan selkeämmän näköiseksi.

8.5.1 Parametriton aliohjelma

Aliohjelma esitellään vastaavasti kuin pääohjelmakin. Esimerkiksi satunnaisen matkaajan mittakaavaohjelmassa (tässä puhdas C- kielinen versio) voisimme kirjoittaa käyttöohjeet omaksi aliohjelmakseen:

c-muut\matka_a1.c - ohjeet aliohjelmaksi

	#include <stdio.h>
	
	#define MITTAKAAVA 200000.0
	#define MM_KM      (1000.0*1000.0)
	
	void ohjeet(void)
	{
	  printf("Lasken 1:%3.0lf kartalta millimetreinä mitatun matkan\n",MITTAKAAVA);
	  printf("kilometreinä luonnossa.\n");
	}
	
	int main(void)
	{
	  int    matka_mm;
	  double matka_km;
	
	  ohjeet();
	
	  printf("Anna matka millimetreinä>");
	  scanf("%d",&matka_mm);
	
	  matka_km = matka_mm*MITTAKAAVA/MM_KM;
	
	  printf("Matka on luonnossa %1.1lf km.\n",matka_km);
	
	  return 0;
	}

Tämän etu on siinä, että saimme pääohjelman selkeämmän näköiseksi.

8.5.2 Funktiot ja parametrit

Voisimme jatkaa pääohjelman selkeyttämistä. Tavoite voisi olla aluksi vaikkapa kirjoittaa pääohjelma muotoon:
	  ohjeet();
	  kysy_matka(&matka_mm);
	  matka_km = mittakaava_muunnos(matka_mm);
	  tulosta_matka(matka_km);
	  return 0; 
Tällainen pääohjelma tuskin tarvitsisi paljoakaan kommentteja.

Edellä on käytetty neljän eri tyypin aliohjelmia (funktioita)

1.
ohjeet();- parametriton aliohjelma
2.
kysy_matka(&matka_mm); - aliohjelma joka palauttaa tuloksen parametrissaan (vrt. esim scanf).
3.
mittakaava_muunnos(matka_mm); - funktio, joka palauttaa tuloksen nimessään
4.
tulosta_matka(matka_km); - aliohjelma, jolle vain viedään arvo, mutta mitään arvoa ei palauteta
Valmis ohjelma, jossa myös aliohjelmat on esitelty, näyttäisi seuraavalta (rivien numerointi on myöhemmin esitettävää pöytätestiä varten):

c-muut\matka_a2.cpp - erilaisia funktioita

	// matka_a2.cpp
	// Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
	// Vesa Lappalainen 4.1.1997
	#include <iostream.h>
	
	const double MITTAKAAVA = 200000.0;
	const double MM_KM      = 1000.0*1000.0;
	
	void ohjeet(void)
	{
	  cout << "Lasken 1:" << MITTAKAAVA 
	       << " kartalta millimetreinä mitatun matkan\n";
	  cout << "kilometreinä luonnossa.\n";
	}
	
	void kysy_matka(int *pMatka_mm)
	{
	  int mm;
	  cout << "Anna matka millimetreinä>";
	  cin >> mm;
	  *pMatka_mm = mm;
	}
	
	double mittakaava_muunnos(int matka_mm)
	{
	  return matka_mm*MITTAKAAVA/MM_KM;
	}
	
	void tulosta_matka(double matka_km)
	{
	  cout << "Matka on luonnossa "<< matka_km << " km." << endl;
	}
	
	
	int main(void)
	{
	  int    matka_mm;
	  double matka_km;
	
	  ohjeet();
	  kysy_matka(&matka_mm);
	  matka_km = mittakaava_muunnos(matka_mm);
	  tulosta_matka(matka_km);
	  return 0;
	}
Edellä olevasta huomataan, että aliohjelmat jotka eivät palauta mitään arvoa nimessään, esitellään void- tyyppisiksi.

mittakaava_muunnos on reaaliluvun palauttava funktio, joten se esitellään double - tyyppiseksi.

Seuraavaksi pöytätestaamme ohjelmamme toiminnan:

main

kysy_matka
mi..muunnos
tulosta

lause
matka_mm
matka_km
pMatka_mm
mm
matka_mm
tulos
matka_km
tulostus
40 ohjeet()
??
??






9-13 cout<<







Lasken 1:200000
41 kysy_mat


&matka_mm





16-18 int m



??




19 cout <<







Anna matka ...
20 cin>>mm



352




21 *pMatka
352

<==o





42 matka_km




352



26 return





70.4


42 matka_km

70.4






43 tulosta






70.4

29-31 cout<







Matka on luo..
44 return 0;








Mikäli kukin aliohjelma olisi testattu erikseen, riittäisi meille pelkkä pääohjelman testi:

main


lause
matka_mm
matka_km
tulostus
40 ohjeet()
??
??
Lasken 1:200000
41 kysy_mat
352

Anna matka ..
42 matka_km

70.4

43 tulosta


Matka on luo..
44 return 0;



Tämä on testaustapa, johon tulisi pyrkiä. Isossa ohjelmassa ei ole enää mitään järkeä testata sitä jokainen aliohjelma kerrallaan. Koodiin liitettyjen aliohjelmien tulee olla testattuja kukin erillisinä ja lopullinen testi on vain viimeisimmän mallin mukainen!

Tehtävä 8.62 matka_a2.c

Kirjoita matka_a2.cpp:stä C- versio (tietovirrat => printf/scanf)

8.5.3 Parametrin nimi kutsussa ja esittelyssä

Huomattakoon, ettei parametrien nimillä aliohjelmien esittelyissä ja kutsuissa ole mitään tekemistä keskenään. Nimi voi olla joko sama tai eri nimi. Parametrien idea on nimenomaan siinä, että samaa aliohjelmaa voidaan kutsua eri muuttujien tai mahdollisesti vakioiden tai lausekkeiden arvoilla. Esimerkiksi nyt kirjoitettua tulosta_matka aliohjelmaa voitaisiin kutsua myös seuraavasti:

c-muut\matka_a3.cpp - erilaisia tapoja kutsua funktiota

	// matka_a3.cpp
	#include <iostream.h>
	
	void tulosta_matka(double matka_km)
	{
	  cout << "Matka on luonnossa "<< matka_km << " km." << endl;
	}
	
	int main(void)
	{
	  double d = 50.2;
	  tulosta_matka(d);         // eri niminen muuttuja
	  tulosta_matka(30.7);      // vakio 
	  tulosta_matka(d+20.8);    // lauseke
	  tulosta_matka(2*d-30.0);  // lauseke 
	  return 0;
	}
Edellä aliohjelman kutsut voidaan tulkita seuraaviksi sijoituksiksi aliohjelman tulosta_matka muuttujaan matka_km:
	matka_km = d;
	matka_km = 30.7;
	matka_km = d+20.8;
	matka_km = 2*d- 30.0
Aliohjelma jouduttiin edellä vielä kirjoittamaan uudestaan (käytännössä kopioimaan edellisestä ohjelmasta), mutta myöhemmin opimme miten aliohjelmia voidaan kirjastoida standardikirjastojen tapaan (ks. moduuleihin jako), jolloin kerran kirjoitettua aliohjelmaa ei enää koskaan tarvitse kirjoittaa uudestaan (eikä kopioida).

8.5.4 Osoitteen välittäminen

Aliohjelma kysy_matka joutuu palauttamaan kysytyn matkan arvon kutsuvalle ohjelmalle. Tämän takia aliohjelmalle ei viedä matkan arvoa parametrina, vaan sen paikan osoite, johon aliohjelman tulos täytyy laittaa:
	kysy_matka(&matka_mm) 
Vastaavasti parametri esitellään osoitin- tyyppiseksi aliohjelman esittelyssä:
	void kysy_matka(int *pMatka_mm) 
Jatkossa pitää muistaa, että tämän aliohjelman pMatka_mm on osoite sinne, minne tulos pitää laittaa. Nimeämmekin tässä opiskelun alkuvaiheessa osoitinmuuttujat aina alkamaan pienellä p- kirjaimella (p=pointer). Tällaista nimeämistapaa, jossa muuttujan alkukirjaimilla kuvataan muuttujan tyyppi, sanotaan unkarilaiseksi nimeämistavaksi. Jotkut ohjelmoinnin opettajat vastustavat nimeämistapaa, mutta suuria Windows- ohjelmia lukiessa täytyy todeta että on todella mukava tietää mitä tyyppiä mikäkin muuttuja on.

Aliohjelmassa on aluksi yksinkertaisuuden vuoksi esitelty aliohjelman paikallinen muuttuja mm, johon päätteeltä saatava arvo ensin luetaan. Jotta myös pääohjelma saisi tietää tästä, pitää suorittaa epäsuora osoitus ("tähdätä osoitinta pitkin"):

	*pMatka_mm = mm;
"Tosiohjelmoija" ei tällaista apumuuttujaa kirjoittaisi, vaan lyhentäisi aliohjelman suoraan muotoon:
	void kysy_matka(int *pMatka_mm)
	{
	  cout << "Anna matka millimetreinä>";
	  cin >> *pMatka_mm;
	}
Vinkki
"Tähtäämisoperaation" muistaa ehkä helpoimmin, jos ajattelee että muuttujan nimi onkin *pMatka_mm eikä pMatka_mm.

Lyhennetty C- versio samasta aliohjelmasta olisi:

	void kysy_matka(int *pMatka_mm)
	{
	  printf("Anna matka millimetreinä>");
	  scanf("%d",pMatka_mm);
	}
Aikaisemmin olemme tottuneet kirjoittamaan scanf- funktioon
	scanf("%d",&matka_mm); 
Nyt kuitenkin pMatka_mm on valmiiksi osoite, jolloin &- merkki täytyy jättää pois kutsusta. Toisaalta voisimme ajatella, että muuttujan nimi onkin *pMatka_mm (koska se on niin esitelty) ja tällöin scanf:n kutsu olisi
	scanf("%d",&*pMatka_mm); /* ==scanf("%d",pMatka_mm); */
On siis erittäin tärkeää ymmärtää milloin on kyse osoitteesta ja milloin arvosta.

8.5.5 Viitemuuttujat (referenssimuuttujat, &)

C++ tarjoaa vielä oman uuden tavan välittää muuttuvia parametreja: viitemuuttujat, eli referenssimuuttujat (reference). Varsinainen mekanismi on täsmälleen sama kuin parametrin välittäminen osoitteiden avullakin. Vain syntaksi on erilainen - aloittelijalle jopa helpompi käyttää:

Aliohjelma olisi muotoa:

c-muut\matka_ar.cpp - parametri referenssinä

	void kysy_matka(int &rMatka_mm) 
	{
	  int mm;
	  cout << "Anna matka millimetreinä>";
	  cin >> mm;
	  rMatka_mm = mm;
	}
tai lyhyemmässä muodossa:
	void kysy_matka(int &rMatka_mm)
	{
	  cout << "Anna matka millimetreinä>";
	  cin >> rMatka_mm;
	}
Tässä esittelyllä
	int &rMatka_mm
esitellään referenssimuuttuja rMatka_mm, eli muuttuja joka referoi johonkin toiseen muuttujaan aina kun siihen viitataan. Näin sijoitus
	rMatka_mm = mm;
tarkoittaakin, että tulos sijoitetaan kutsuneen ohjelman vastaavalle muuttujalle.

Huonona (tai jonkin mielestä hyvänä) puolena viitemuuttujista voitaisiin pitää sitä, että itse kutsu pääohjelmasta täytyy nyt olla muodossa:

	kysy_matka(matka_mm);                 // HUOM!
Miksi pitäisin tätä huonona? Siksi, ettei kutsusta nyt näe suoran aikooko aliohjelma muuttaa muuttujan matka_mm arvoa vaiko ei.

Hyvänä puolena on taas se, että aliohjelma voidaan muuttaa viitteitä käyttäväksi muuttamatta itse kutsuvaa ohjelmaa. Tämä tulee kyseeseen sitten kun välitämme parametreinä "isoja" oliota, jolloin on edullisempaa vain viitata olioon, kuin kuljettaa mukana koko olio.

Vielä yksi huono (tai joidenkin mielestä hyvä) puoli viitemuuttujissa on se, ettei niiden viittaamaa paikkaa voida muuttaa muuta kuin alustuksessa (esim. aliohjelman kutsun yhteydessä). Näin osoittimia tarvitaan vielä tilanteissa, joissa tietorakenteita pitää käydä läpi.

8.5.6 & - merkillä monta eri merkitystä

Vielä eräs inhottava puoli on se, että samalle merkille & (eikä tämä ole edes ainoa kuormitettu symboli) on otettu useita eri merkityksiä riippuen siitä missä yhteydessä merkki esiintyy:
1.
Osoiteoperaattori: Jos & on jo olemassa olevan muuttujan edessä, otetaan muuttujan osoite. - scanf("%d",&matka_mm);
2.
Referenssin esittely: Jos & on muuttujan esittelyn yhteydessä, esitellään viitemuuttuja (referenssi) - int &rMatka_mm
3.
Bittitason AND- operaattori: Jos & esiintyy kahden lausekkeen välissä, on kyseessä bittitason JA- operaattori: parillinen = luku & 0xfffe
4.
Looginen AND- operaattori: Jos & esiintyy 2-kertaisena kahden lausekkeen välissä, on kyseessä looginen JA- operaattori:
if ( kello < 23 && 0 < rahaa ) ...
Jos selviämme tästä &- sekamelskasta, selviämme lopuistakin ongelmista C++:n kanssa.

Yleensä AND operaatiot eivät aiheuta sekaannusta, mutta jokin muistisääntö tarvitaan siihen milloin kirjoitetaan * ja milloin &. Olkoon se vaikka: Vinkki

Tähdet taivaalla

1.
Jos kyseessä on osoiteparametrin välitys tulee * ylös, koska tähdet ovat taivaalla ja niitä osoitetaan, tällöin &- merkit tulevat alas!
2.
Jos kyseessä on parametrin välitys referenssin avulla, ei tarvitse tähtäillä, joten tähtiä ei tule ja jokainen merkki siirtyy pykälän ylöspäin, eli & merkit ylös ja alhaalle ei jää mitään!
3.
Tavallisessa arvoparametrin välityksessä ei tarvita mitään ihmemerkkejä, mutta ei saada ihmeitä aikaankaan!
Kertaamme "problematiikkaa" vähän myöhemmin, kun katsomme tarkemmin mitä aliohjelmakutsu oikein tarkoittaa.

8.5.7 Nimessään arvon palauttavat funktiot

Funktion arvo palautetaan return - lauseessa. Jokaisessa ei- void - tyyppiseksi esitellyssä funktiossa tulee olla vähintään yksi return - lause. void- tyyppisessäkin voi olla return- lause. Tarvittaessa return- lauseita voi olla useampiakin:
	int suurempi(int a, int b)
	{
	  if ( a >= b ) return a;
	  return b;
	}
Kun return - lause tulee vastaan, lopetetaan HETI funktion suoritus. Tällöin myöhemmin olevilla lauseilla ei ole mitään merkitystä. Näin ollen useat return- lauseet ovat mielekkäitä vain ehdollisissa rakenteissa. Siis seuraavassa ei olisi mitään mieltä:
	int hopo(int a) 	
	{
	  int i;
	  return 5;  /* Palauttaa aina 5!!! */
	  i = 3 + a;
	  return i+2;
	}
return- lausetta ei saa sotkea arvojen palauttamiseen osoitteen avulla. Esimerkiksi:

c-muut\funjaoso.c - sivuvaikutuksellinen funktio

	/* funjaoso.c */
	#include <stdio.h>
	
	int tupla_plus_yksi(int *pArvo)
	{
	  int vanha=*pArvo;     /* Alkuperäinen arvo talteen              */
	  *pArvo = *pArvo + 1;  /* Kutsun muuttuja muuttui nyt! eli j = 4 */
	  return 2*vanha;       /* Arvo palautui nimessä nyt!   eli i = 6 */
	}
	
	int main(void)
	{
	  int i,j=3;
	  i = tupla_plus_yksi(&j);
	  printf("i = %d, j = %d\n",i,j);
	  return 0;
	} 

Tehtävä 8.63 Funktio ja osoitin

Mitä pääohjelma funjaoso tulostaisi jos aliohjelma olisikin ollut:
	int tupla_plus_yksi(int *pArvo)
	{
	  *pArvo = *pArvo + 1;  
	  return 2**pArvo;     
	}

Tehtävä 8.64 Funktio ja referenssi

Kirjoita edellisestä tehtävästä molemmista tupla_plus_yksi - versioista viitemuuttujia käyttävä versio!

8.5.8 Ketjutettu kutsu

Koska funktio- aliohjelma palauttaa valmiiksi arvon, voitaisiin matka_a2.cpp:n pääohjelma kirjoittaa myös muodossa:
	  ohjeet();
	  kysy_matka(&matka_mm);
	  tulosta_matka(mittakaava_muunnos(matka_mm));
	  return 0; 
Funktioita käytetään silloin, kun aliohjelman tehtävänä on palauttaa vain yksi täsmällinen arvo. Tyypillisiä math.h- kirjaston funktioita on esim (suluissa olevat eivät ole standardin funktioita):
	(abs)   acos   asin   atan   atan2     atof  (cabs)  ceil 
	cos     cosh   exp    fabs   floor     fmod  frexp   (hypot)
	(labs)  ldexp  log    log10  (matherr) modf  (poly)   pow 
	(pow10) sin    sinh   sqrt   tan       tanh 
Funktioita käytetään kuten matematiikassa on totuttu:
	c = sqrt(a*a+b*b) + asin((sin(alpha)+0.2)/2.0); 
kysy_matka ja kysy_mittakaava voitaisiin kirjoittaa myös funktioiksi, ja tällöin niitä voitaisiin kutsua esim. seuraavasti:
	matka_km = kysy_matka()*kysy_mittakaava()/MM_KM; 
Vaarana olisi kuitenkin se, ettei voida olla aivan varmoja kumpiko funktiosta kysy_matka vai kysy_mittakaava suoritettaisiin ensin ja tämä saattaisi aiheuttaa joissakin tilanteissa yllätyksiä.

Tämän vuoksi pyrimmekin kirjoittamaan funktioiksi vain sellaiset aliohjelmat, jotka palauttavat täsmälleen yhden arvon ja jotka eivät ota muuta informaatiota ympäristöstä kuin sen mitä niille parametrina välitetään. Eli tavoitteena on se, että funktioiden kutsuminen lausekkeen osana olisi turvallista.

Muut aliohjelmat kirjoitamme siten, että arvot palautetaan osoitteen avulla. Hyvin yleinen C- tapa on kuitenkin palauttaa tällaisenkin aliohjelman onnistumista kuvaava arvo funktion nimessä (vrt. esim. scanf).

Tehtävä 8.65 math.h

Katso Turbo- C:n Help- toiminnon avulla kunkin math.h- kirjaston funktion parametrien määrä ja tyyppi sekä se mitä kukin todella tekee.

Tehtävä 8.66 Funktiot

Kirjoita edellä mainitut kysy_matka ja kysy_mittakaava nimessään arvon palauttavina funktioina.

Tehtävä 8.67 Ympyrän ala ja pallon tilavuus

Kirjoita funktiot, jotka palauttavat r- säteisen ympyrän pinta- alan ja r- säteisen pallon tilavuuden.
Kirjoita pääohjelma, jossa pinta- ala ja tilavuus - funktiot testataan.

Tehtävä 8.68 Pääohjelma yhtenä funktiokutsuna

Jatka edellä mainittua ketjuttamista siten, että koko pääohjelma on vain yksi lauseke (ohjeet- kutsu saa olla oma rivinsä jos haluat). Tosin tämä on C- hakkerismia eikä mikään tavoite helposti luettavalta ohjelmalta. Itse asiassa hyvä kääntäjä tekee automaattisesti tämän kaltaista optimointia (mitä muka voitiin säästää?).

8.5.9 Aliohjelmien testaaminen

Kun uusi aliohjelma kirjoitetaan, kannattaa sen testaamista varten kirjoittaa hyvin lyhyt testi- pääohjelma.

Esimerkiksi kerhon jäsenrekisterin päämenun tulostamista varten voisimme kirjoittaa aliohjelman nimeltä paamenu. Tämä päämenu voitaisiin sitten testata vaikkapa seuraavalla testipääohjelmalla:

c-muut\menutest.cpp - päämenun testaus

	// menutest.cpp 
	#include "paamenu.cpp" // HUOM! "Oikeasti" aliohjemia ei INCLUDEta 
	                       // vaan paamenu.h ja tehdään projekti tai MAKEFILE! 
	int main(void)
	{
	  paamenu(10);
	  return 0;
	}
Tiedostot paamenu.h ja paamenu.cpp, joissa aliohjelman prototyyppi ja itse päämenu esiteltäisiin, voisivat olla esimerkiksi:

c-muut\paamenu.h - päämenun otsikkotiedosto

	/* paamenu.h */
	#ifndef PAAMENU_H
	#define PAAMENU_H
	void paamenu(int jasenia);
	#endif /* PAAMENU_H */

c-muut\paamenu.cpp - päämenun totetutus

	// paamenu.cpp 
	#include <iostream.h>
	#include "paamenu.h"
	
	void paamenu(int jasenia)
	{
	  cout << "\n\n\n\n\n";
	  cout << "Jäsenrekisteri\n";
	  cout << "==============\n";
	  cout << "\n";
	  cout << "Kerhossa on " << jasenia << " jäsentä.\n";
	  cout << "\n";
	  cout << "Valitse:\n";
	  cout << "   ?  = avustus\n";
	  cout << "   0  = lopetus\n";
	  cout << "   1  = lisää uusi jäsen\n";
	  cout << "   2  = etsi jäsenen tiedot\n";
	  cout << "   3  = tulosteet\n";
	  cout << "   4  = tietojen korjailu\n";
	  cout << "   5  = päivitä jäsenmaksuja" << endl;
	  cout << "   :";
	}
Huomattakoon, että aliohjelma voitaisiin kirjoittaa myös seuraavasti (miksi?):

c-muut\paamenu2.cpp - toteutus vähillä cout-kutsuilla

	// paamenu2.cpp 
	#include <iostream.h>
	#include "paamenu.h"
	
	void paamenu(int jasenia)
	{
	  cout << "\n\n\n\n\n"
	  "Jäsenrekisteri\n"
	  "==============\n"
	  "\n"
	  "Kerhossa on " << jasenia << " jäsentä.\n"
	  "\n"
	  "Valitse:\n"
	  "   ?  = avustus\n"
	  "   0  = lopetus\n"
	  "   1  = lisää uusi jäsen\n"
	  "   2  = etsi jäsenen tiedot\n"
	  "   3  = tulosteet\n"
	  "   4  = tietojen korjailu\n"
	  "   5  = päivitä jäsenmaksuja" << endl <<
	  "   :";
	}
Voidaan kirjoittaa jopa (miksi):

c-muut\paamenu3.cpp - rivin jatkaminen

	#include <iostream.h>
	#include "paamenu.h"
	
	void paamenu(int jasenia)
	{
	cout << "\n\n\n\n\n\
	Jäsenrekisteri\n\
	==============\n\
	\n\
	Kerhossa on " << jasenia << " jäsentä.\n\
	\n\
	Valitse:\n\
	   ?  = avustus\n\
	   0  = lopetus\n\
	   1  = lisää uusi jäsen\n\
	   2  = etsi jäsenen tiedot\n\
	   3  = tulosteet\n\
	   4  = tietojen korjailu\n\
	   5  = päivitä jäsenmaksuja" << endl << "\
	   :";
	}
Jatkossa kommentoimme aliohjelmia enemmän, mutta nyt olemme jättäneet kommentit pois, jotta ohjelma olisi mahdollisimman lyhyt.

Huomattakoon, että aliohjelma on saatu kopioiduksi suoraan aikaisemmasta ohjelman suunnitelmasta lisäämällä vain kunkin rivin alkuun cout <<"ja loppuun \n";. Tällaiset toimenpiteet voidaan automatisoida tekstinkäsittelyn avulla.

8.6 Lisää aliohjelmista

8.6.1 Useita parametreja

Kaikissa edellisissä esimerkeissämme meillä on ollut vain 0 tai yksi parametria välitettävänä aliohjelmaan. Käytännössä usein tarvitsemme useampia parametreja. Esimerkiksi edellisessä paamenu- aliohjelmassa pitäisi oikeastaan tulostaa myös kerhon nimi. Emme vielä kuitenkaan osaa käsitellä merkkijonoja, joten palaamme tähän ongelmaan myöhemmin.

Ottakaamme esimerkiksi mittakaava_muunnos - funktio. Mikäli ohjelma haluttaisiin muuttaa siten, että myös mittakaavaa olisi mahdollista muuttaa, pitäisi myös mittakaava voida välittää muunnos- aliohjelmalle parametrina. Kutsussa tämä voisi näyttää esim. tältä:

	  matka_km = mittakaava_muunnos(10000.0,32); 
Vastaavasti funktio- esittelyssä täytyisi olla kaksi parametria:
	double mittakaava_muunnos(double mittakaava,int matka_mm)
	{
	  return matka_mm*mittakaava/MM_KM;
	}
Kun kutsu suoritetaan, välitetään aliohjelmalle parametrit siinä järjestyksessä, missä ne on esitelty. Voitaisiin siis kuvitella aliohjelmakutsun aiheuttavan sijoitukset aliohjelman parametrimuuttujiin:
	mittakaava = 10000.0;
	matka_mm   = 32; 
Jos kutsu on muotoa
	  matka_km = mittakaava_muunnos(MITTAKAAVA,matka_mm); 
kuvitellaan sijoitukset
	mittakaava = MITTAKAAVA; /* Ohjelman vakio                */
	matka_mm   = matka_mm;   /* Pääohjelman muuttuja matka_mm */
Siis vaikka kutsussa ja esittelyssä esiintyykin sama nimi, ei nimien samuudella ole muuta tekemistä kuin mahdollisesti se, että turha on väkisinkään keksiä lyhennettyjä huonoja nimiä, jos kerran on hyvä nimi keksitty kuvaamaan jotakin asiaa.

Parametreista osa, ei yhtään tai kaikki voivat olla myös osoitteita tai referenssejä.

Huom! Vaikka kaikilla aliohjelman parametreilla olisikin sama tyyppi, täytyy jokaisen parametrin tyyppi mainita silti erikseen:

	double nelion_ala(double korkeus, double leveys) 

Tehtävä 8.69 Toisen asteen yhtälön juuri

Kirjoita funktio root_1(a,b,c), joka palauttaa jomman kumman toisen asteen yhtälön ax2+bx+c=0 juurista (oletetaan tällä kertaa, että a<>0 ja D = b2- 4ac >= 0. Miksi oletetaan?).

Tehtävä 8.70 Toisen asteen polynomi, root_1

Kirjoita funktio root_1 joka palauttaa toisen asteen polynomin P(x) = ax2+bx+c arvon (muista viedä parametrina myös a,b ja c).

Tehtävä 8.71 root_1 testaus

Kirjoita pääohjelma, jolla voidaan testata root_1 - aliohjelma (jotenkin myös se, että tulos toteuttaa yhtälön).

8.6.2 Muuttujien lokaalisuus

Kukin aliohjelma muodostaa oman kokonaisuutensa. Edellä olleissa esimerkeissä aliohjelmat eivät tiedä ulkomaailmasta mitään muuta, kuin sen, mitä niille tuodaan parametreina kutsun yhteydessä.

Vastaavasti ulkomaailma ei tiedä mitään aliohjelman omista muuttujista. Näitä aliohjelman lokaaleja muuttujia on esim. seuraavassa:

	void kysy_matka(int *pMatka_mm)
	{
	  int mm;
	  printf("Anna matka millimetreinä>");
	  scanf("%d",&mm);
	  *pMatka_mm = mm;
	}
	pMatka_mm   -  aliohjelman parametrimuuttuja (tässä tapauksessa osoitinmuuttuja).
	mm          -  aliohjelman lokaali apumuuttuja matkan lukemiseksi.

Yleensäkin C- kielessä lausesulut { ja } muodostavat lohkon, jonka ulkopuolelle mikään lohkon sisällä määritelty muuttuja tai tyyppimääritys ei näy. Näkyvyysalueesta käytetään englanninkielisessä kirjallisuudessa nimitystä scope. Lokaaleilla muuttujilla voi olla vastaava nimi, joka on jo aiemmin esiintynyt jossakin toisessa yhteydessä. Lohkon sisällä käytetään sitä määrittelyä, joka esiintyy lohkossa:

c-muut\lokaali.c - lokaalien muuttujien näkyvyys

	#include <stdio.h>             	
	int main(void)
	{
	   char ch='A';
	   printf("Kirjain %c",ch);
	   {
	     int ch = 5;
	     printf(" kokonaisluku %d",ch);
	     {
	       double ch = 4.5;
	       printf(" reaaliluku %5.2lf\n",ch);
	     }
	   }
	   return 0;
	}
Saman tunnuksen käyttäminen eri tarkoituksissa on kuitenkin kaikkea muuta kuin hyvää ohjelmointia.

Tehtävä 8.72 Eri nimet

Korjaa edellinen ohjelma siten, että kullakin erityyppisellä muuttujalla on eri nimi.

8.6.3 auto ja register

Varattu sana auto tarkoittaa, että muuttujasta tehdään automaattinen muuttuja. Oletuksena jokainen lokaali muuttuja, joka ei ole esitelty static - määrityksellä, on automaattinen muuttuja. Tämän vuoksi auto- sanaa harvoin käytetään. Muuttujan automaattisuus tarkoittaa sitä, että kun muuttujan määrittelylohkosta poistutaan, tuhotaan muuttuja samalla.
	{
	  int mm;        /* sama kirjoitetaanko näin */
	  auto int mm;   /* vai näin                 */

Joskus kääntäjän työn helpottamiseksi voidaan kääntäjälle ehdottaa jonkin lokaalin muuttujan sijoittamista prosessorin (CPU) rekisteriin ja näin saadaan muuttujan käyttö nopeammaksi. Tämä tehdään varatulla sanalla register.

c-muut\register.c - muuttujat prosessorin rekistereihin

	#include <stdio.h>
	int main(void)
	{
	  register int i;
	  for (i=0; i<4; i++) printf("i=%d\n",i);
	  /* Seuraava ei toimi koska rekisteristä ei saada osoitetta */
	/*  printf("i:n osoite on %p\n",&i); */
	  return 0;
	}

8.6.4 Parametrinvälitysmekanismi

Ainoa C- kielen tuntema parametrinvälitysmekanismi on parametrien välittäminen arvoina. Tämä tarkoittaa sitä, että aliohjelma saa käyttöönsä vain (luku)arvoja, ei muuta. Olkoon meillä esimerkiksi ongelmana tehdä aliohjelma, jolle viedään parametreina tunnit ja minuutit sekä niihin lisättävä minuuttimäärä. Jos ensimmäinen yritys olisi seuraava:

c-muut\aikalisa.cpp - yritys lisätä arvoja

	#include <iostream.h>
	void lisaa(int h, int m, int lisa_min) 	
	{
	  int yht_min = h*60 + m + lisa_min;
	  h = yht_min / 60;
	  m = yht_min % 60;
	}
	
	void tulosta(int h, int m)
	{
	  cout << h << ":" << m << endl;
	}
	
	int main(void)
	{
	  int h=12,m=15;
	  lisaa(h,m,55);
	  tulosta(h,m);
	  return 0;
	}
Tämä ei tietenkään toimisi! Hyvä kääntäjä jopa varoittaisi että:
	Warn :  aikalisa.cpp(8,2):'m' is assigned a value that is never used
	Warn :  aikalisa.cpp(7,2):'h' is assigned a value that is never used
Mutta miksi ohjelma ei toimisi? Seuraavan selityksen voi ehkä ohittaa ensimmäisellä lukukerralla. Tutkitaanpa tarkemmin mitä aliohjelmakutsussa oikein tapahtuu. Oikaisemme seuraavassa hieman joissakin kohdissa liian tekniikan kiertämiseksi, mutta emme kovin paljoa. Katsotaanpa ensin miten kääntäjä kääntäisi aliohjelmakutsun (Borland C++ 5.1, 32-bittinen käännös, rekisterimuuttujat kielletty jottei optimointi tekisi konekielisestä ohjelmasta liian monimutkaista):
	lisaa(h,m,55);
	
	muistiosoite assembler         selitys
	-------------------------------------------------------------------------
	004010F9     push 0x37           pinoon 55
	004010FB     push [ebp-0x08]     pinoon m:n arvo 
	004010FE     push [ebp-0x04]     pinoon h:n arvo
	00401101     call lisaa          mennään aliohjelmaan lisää
	00401106     add esp,0x0c        poistetaan pinosta 12 tavua (3 x int)
Kun saavutaan aliohjelmaan lisaa, on pino siis seuraavan näköinen:
	muistiosoite  sisältö          selitys
	------------------------------------------------------------------------
	064FDEC       00401106 <-ESP   paluuosoite kun aliohjelma on suoritettu
	064FDF0       0000000C         h:n arvo, eli 12
	064FDF4       0000000F         m:n arvo, eli 15
	064FDF8       00000037         lisa_min, eli 55
Eli aliohjelmaan saavuttaessa aliohjelmalla on käytössään vain arvot 12,15 ja 55. Näitä se käyttää tässä järjestyksessä omien parametriensa arvoina, eli m,h,lisa_min.

Muutetaanpa ohjelmaan parametrin välitys osoitteiden avulla:

c-muut\aikalis2.cpp - parametrin välitys osoittimilla

	#include <iostream.h>
	
	void lisaa(int *ph, int *pm, int lisa_min)
	{
	  int yht_min = *ph * 60 + *pm + lisa_min;
	  *ph = yht_min / 60;
	  *pm = yht_min % 60;
	}
	
	void tulosta(int h, int m)
	{
	  cout << h << ":" << m << endl;
	}
	
	int main(void)
	{
	  int h=12,m=15;
	  lisaa(&h,&m,55);
	  tulosta(h,m);
	  return 0;
	}
Nyt ohjelma toimii ja tulostaa 13:10 kuten pitääkin. Eikä kääntäjäkään anna varoituksia. Mitä nyt tapahtuu ohjelman sisällä? Aliohjelmakutsusta seuraa:
	lisaa(&h,&m,55);
	
	muistiosoite assembler         selitys
	-------------------------------------------------------------------------
	0040111D     push 0x37           pinoon 55
	0040111F     lea eax,[ebp-0x08]  m:osoite rekisteriin eax
	00401122     push eax            ja tämä pinoon (0064FDFC) 
	00401123     lea edx,[ebp-0x04]  h:n osoite rekisteriin edx
	00401126     push edx            ja tämä pinoon (0064FE00)
	00401127     call lisaa          mennään aliohjelmaan lisää
	0040112C     add esp,0x0c        poistetaan pinosta 12 tavua (2 x int)
Pino on aliohjelmaan saavuttaessa seuraavan näköinen
	muistiosoite assembler         selitys
	------------------------------------------------------------------------
	0064FDEC      0040112C <-ESP   paluuosoite kun aliohjelma on suoritettu
	0064FDF0      0064FE00         h:n osoite,  eli ph:n arvo
	0064FDF4      0064FDFC         m:n osoite,  eli pm:n arvo
	0064FDF8      00000037         lisa_min, eli 55
Aliohjelman alussa olevien lauseiden
	muistiosoite assembler         selitys
	-------------------------------------------------------------------------
	0040107C     push ebp            pinoon talteen rekisterin ebp arvo
	0040107D     mov  ebp,esp        ebp:hn pinon pinnan osoite
	0040107F     push ecx            pinoon tilaa yhdelle kokonaisluvulle (min)
suorittamisen jälkeen pino näyttää seuraavalta
	muistiosoite  sisältö          selitys
	------------------------------------------------------------------------
	0064FDE4  -04 00000000 <-ESP   aliohjelman oma tila, eli min-muuttuja
	0064FDE8  +00 0064FE04 <-EBP   vanha EBP:n arvo, johon EBP nyt osoittaa
	0064FDEC  +04 0040112C         paluuosoite kun aliohjelma on suoritettu
	0064FDF0  +08 0064FE00         h:n osoite,  eli ph:n arvo
	0064FDF4  +0C 0064FDFC         m:n osoite,  eli pm:n arvo
	0064FDF8  +10 00000037         lisa_min, eli 55
Esimerkiksi minuuteille sijoitus kääntyisi seuraavasti:
	*pm = yht_min % 60;
	
	muistiosoite assembler         selitys
	-------------------------------------------------------------------------
	004010A1     mov  eax,[ebp-0x04] eax:ään yht_min muuttujan arvo        
	004010A4     mov  ecx,0x0000003c ecx:ään 60 jakajaksi          
	004010A9     cdq                 konvertoidaan eax 64 bitiksi edx:eax      
	004010AA     idiv ecx            jaetaan edx:eax/ecx:llä tulos eax, jakoj. edx
	004010AC     mov  eax,[ebp+0x0c] eax:ään m:n osoite (eli pm:n arvo)
	004010AF     mov  [eax],edx      sinne muistipaikkaan, jonne eax asoittaa,
	                                 eli 0064FDFC, eli pääohjelman m:ään kopioidaan
	                                 edx, eli 10.  HUOM!  Pääohjelman m:n arvo
	                                 muuttui juuri tällä hetkellä!
	}                                aliohjelmasta poistuminen
	004010B1     pop  ecx            pinosta pois aliohjelman omat muuttujat
	004010B2     pop  ebp            alkuperäinen ebp:n arvo talteen
	004010B3     ret                 ja paluu osoitteeseen joko on pinon päällä nyt
	                                 eli 0040112C, eli pääohjelmaan call-lauseen
	                                 jälkeen
HUOM! Kääntäjä ei ole optimoivillakaan asetuksilla huomannut, että sekä h:n että m:n sijoitus saataisiin samasta jakolaskusta, koska toisessa tarvitaan kokonaisosaa ja toisessa jakojäännöstä, jotka molemmat saadaan samalla kertaa idiv operaatiossa. Huonoa kääntäjän kannalta, mutta ilahduttavaa että hyvälle assembler- ohjelmoijallekin jää vielä käyttöä.

Takaisin asiaan. Nyt siis aliohjelmalla oli pelkkien 12,15,55 arvojen sijasta käytössä osoitteet pääohjelman h:hon ja m:ään sekä arvo 55. Näin aliohjelma pystyi muuttamaan kutsuneen ohjelman muuttujien arvoja. Sama kuvana ennen *pm- sijoitusta:

Kuva 8.3 Sijoitus aliohjelmasta kutsuvan ohjelman muuttujaan

Tehtävä 8.73 aikalis3.cpp

Kirjoita aikalis2.cpp:stä viitemuuttujia käyttävä versio. Sisäisesti ohjelma kääntyy täsmälleen samanlaiseksi kuin osoittimilla, eli osoittimet ja viitemuuttujat ovat sisäisesti todellakin sama asia.

Tehtävä 8.74 Muotoilu?

Kokeilepa lisätä aikaan esimerkiksi 50 min. Mitä tulostuu? Miten vian voisi korjata?

Tehtävä 8.75 Tiedon lukeminen

Kirjoita aliohjelma lue, joka kysyy ja lukee arvon kellonajalle, syöttö muodossa 12:15.

8.6.5 Aliohjelmien kirjoittaminen

Aliohjelmien kirjoittaminen kannattaa aina aloittaa aliohjelmakutsun kirjoittamisesta. Näin voidaan suunnitella mitä parametreja ja missä järjestyksessä aliohjelmalle viedään. Näinhän teimme mittakaava- ohjelmassakin.

8.6.6 Globaalit muuttujat

Muuttujat voidaan esitellä myös globaaleiksi. Mikäli muuttujat esitellään kaikkien ohjelman lausesulkujen ulkopuolella, näkyvät muuttujat kaikille niille lohkoille, jotka on esitelty muuttujan esittelyn jälkeen. Seuraava ohjelma on kaikkea muuta kuin hyvän ohjelmointitavan mukainen, mutta pöytätestaamme sen siitä huolimatta:

c-muut\alisotku.c - parametrin välitystä

	/* 01 */ /* alisotku.c */
	/* 02 */ /* Mitä ohjelma tulostaa?? */
	/* 03 */ #include <stdio.h>
	/* 04 */
	/* 05 */ int a,b,c;
	/* 06 */
	/* 07 */ void ali_1(int *a, int b)
	/* 08 */ {
	/* 09 */   int d;
	/* 10 */   d  = *a;
	/* 11 */   c  = b + 3;
	/* 12 */   b  = d -  1;
	/* 13 */   *a = c -  5;
	/* 14 */ }
	/* 15 */
	/* 16 */ void ali_2(int *a, int *b)
	/* 17 */ {
	/* 18 */   int c;
	/* 19 */   c  = *a + *b;
	/* 20 */   *a = 9 -  c;
	/* 21 */   *b = 32;
	/* 22 */ }
	/* 23 */
	/* 24 */ int main(void)
	/* 25 */ {
	/* 26 */   int d;
	/* 27 */   a=1; b=2; c=3; d=4;
	/* 28 */   ali_1(&d,c);
	/* 29 */   ali_2(&b,&a);
	/* 30 */   ali_1(&c,3+d);
	/* 31 */   printf("%d %d %d %d\n",a,b,c,d);
	/* 32 */   return 0;
	/* 33 */ }
Seuraavassa g.c täytyy tulkita: globaali muuttuja c ja m.d: main- funktion muuttuja d.

globaalit 

main

ali_1

ali_2

laskuja

           

   

   

   

    

*   

   

   

*   

*   

   


   lause  

 a 

 b 

 c 

  d 

  a 

 b 

 d 

  a 

  b 

 c 


27 a=1; b=2

 1 

 2 

 3 

  4 

    

   

   

    

    

   


28 ali_1(&d

   

   

   

    

&m.d

 3 

 ? 

    

    

   

ali_1(&m.d,3)

10 d = *a; 

   

   

   

    

    

   

 4 

    

    

   

= m.d = 4

11 c = b+3;

   

   

 6 

    

    

   

   

    

    

   

= 3+3 = 6

12 b = d-1;

   

   

   

    

    

 3 

   

    

    

   

= 4-1 = 3

13 *a= c-5;

   

   

   

  1 

 <-o

   

   

    

    

   

m.d = g.c-5 = 6-5 = 1

29 ali_2(&b

   

   

   

    

    

   

   

&g.b

&g.a

 ? 

ali_2(&g.b,&g.a)

19 c =*a+*b

   

   

   

    

    

   

   

    

    

 3 

= g.b+g.a = 2+1 = 3

20 *a= 9-c;

   

 6 

   

    

    

   

   

<-o

    

   

g.b = 9-3 = 6 

21 *b= 32; 

32 

   

   

    

    

   

   

    

<-o

   

g.a = 32

30 ali_1(&c

   

   

   

    

&g.c

 4 

 ? 

    

    

   

ali_1(&g.c,3+1)

10 d = *a; 

   

   

   

    

    

   

 6 

    

    

   

= g.c = 6

11 c = b+3;

   

   

 7 

    

    

   

   

    

    

   

= 4+3 = 7

12 b = d-1;

   

   

   

    

    

 5 

   

    

    

   

= 6-1 = 5

13 *a= c-5;

   

   

 2 

    

 <-o

   

   

    

    

   

g.c = c-5 = 7-5 = 2

31 printf("

   

   

   

    

    

   

   

    

    

   

Tulostus: 32 6 2 1

32 return 0

   

   

   

    

    

   

   

    

    

   

          ========= 

Globaaleiden muuttujien käyttöä tulee ohjelmoinnissa välttää. Tuskin mistään on tullut yhtä paljon ohjelmointivirheitä, kuin vahingossa muutetuista globaaleista muuttujista!

Tehtävä 8.76 Muuttujien näkyvyys

Pöytätestaa seuraava ohjelma:

c-muut\alisotk2.cpp - parametrin välitystä

	/* 01 */ /* alisotk2.c */ /* Mitä ohjelma tulostaa?? */
	/* 02 */ #include <stdio.h>
	/* 03 */
	/* 04 */ int b,c;
	/* 05 */
	/* 06 */ void s_1(int *a, int b)
	/* 07 */ {
	/* 08 */   int d;
	/* 09 */   d  = *a;
	/* 10 */   c  = b + 3;
	/* 11 */   b  = d -  1;
	/* 12 */   *a = c -  5;
	/* 13 */ }
	/* 14 */
	/* 15 */ void a_2(int *a, int &b)
	/* 16 */ {
	/* 17 */   c  = *a + b;
	/* 18 */   { int c; c = b;
	/* 19 */   *a = 8 * c; }
	/* 20 */   b = 175;
	/* 21 */ }
	/* 22 */
	/* 23 */ int main(void)
	/* 24 */ {
	/* 25 */   int a,d;
	/* 26 */   a=4; b=3; c=2; d=1;
	/* 27 */   s_1(&b,c);
	/* 28 */   a_2(&d,a);
	/* 29 */   s_1(&d,3+d);
	/* 30 */   printf("%d %d %d %d\n",a,b,c,d);
	/* 31 */   return 0;
	/* 31 */ }


previous next Title Contents Index