* malliohjelma jossa tarvitaan välttämättä muuttujia
* osoiteoperaattori, &
* osoitin muuttujat ja epäsuora osoitus, *
* aliohjelmat, eli funktiot
* erilaiset aliohjelmien kutsumekanismit
* referenssimuuttujat eli viitemuuttujat, &
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_nimiJatkossa 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ää.
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.
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)
// 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.
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
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 = =.
int kengan_koko = 32, takin_koko = 52; double pi = 3.14159265358979323846, r = 5.0;
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.
/* 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.
|
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:.
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:
#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.
Edellä sanottiin, että &matka_mm on osoite muuttujaan matka_mm. Tätä voitaisiin kuvata seuraavasti:
// 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:
*pon sallittu silloin, kun p on esitelty osoittimeksi johonkin muuhun tyyppiin kuin void tyyppiin. Siis
/* 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
&isaadaan 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! */
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.).
#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.
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)
// 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;
|
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;
|
// 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.0Aliohjelma 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).
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
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.
Aliohjelma olisi muotoa:
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_mmesitellää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.
Yleensä AND operaatiot eivät aiheuta sekaannusta, mutta jokin
muistisääntö tarvitaan siihen milloin kirjoitetaan * ja milloin
&. Olkoon se vaikka:
Vinkki
Tähdet taivaalla
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:
/* 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; }
int tupla_plus_yksi(int *pArvo) { *pArvo = *pArvo + 1; return 2**pArvo; }
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 tanhFunktioita 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).
Esimerkiksi kerhon jäsenrekisterin päämenun tulostamista varten voisimme kirjoittaa aliohjelman nimeltä paamenu. Tämä päämenu voitaisiin sitten testata vaikkapa seuraavalla testipääohjelmalla:
// 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:
/* paamenu.h */ #ifndef PAAMENU_H #define PAAMENU_H void paamenu(int jasenia); #endif /* PAAMENU_H */
// 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?):
// 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):
#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.
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)
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:
#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.
{ 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.
#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; }
#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 usedMutta 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 55Eli 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:
#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 55Aliohjelman 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 55Esimerkiksi 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älkeenHUOM! 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:
/* 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 |
|
|
|
|
|
|
|
|
|
|
========= |
/* 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 */ }