Eljárások
Eljárások és függvények¶
Az eljárásvezérlésnek két fajtája van, az eljárásművelet és a függvényművelet, amelyből az utóbbival már megismerkedtünk.
Eljárásműveleten olyan tevékenységet értünk, amelynek alkalmazása adott argumentumokra az argumentumok értékének pontosan meghatározott megváltozását eredményezi. Minden eljárásműveletnek rögzített számú paramétere van, és minden paraméter rögzített adattípusú.
- Bemenő mód: ha a művelet végrehajtása nem változtathatja meg az adott argumentum értékét.
- Kimenő mód: ha a művelet eredménye nem függ az adott argumentum végrehajtás előtti értékétől, de az adott argumentum értéke a művelet hatására megváltozhat.
- Be- és kimenő (vegyes) mód: Ha a művelet felhasználhatja az adott argumentum végrehajtás előtti értékét és az argumentum értéke a művelet hatására meg is változhat.
Az eljárásművelet specifikációja tartalmazza:
- a művelet elnevezését,
- a paraméterek felsorolását,
- mindegyik paraméter adattípusát,
- a művelet hatásának leírását.
A függvényművelet specifikációja pedig a fentieken túl tartalmazza a függvényművelet eredménytípusát is.
Mint az látható az eljárás- és függvényműveletek között igen kicsi a különbség (ez a kis különbség viszont egyes programozási nyelveken lényeges és jelentős lehet, bár a C nyelvben éppen nem az). Éppen ezért a függvényművelet argumentumai ugyanúgy lehetnek kimenő és be- és kimenő módúak is, mint az eljárásműveletek esetén, tehát a függvényművelet végrehajtása is eredményezheti az aktuális argumentumok megváltozását.
Eljárásműveletet általános alakban P(m_1 X_1 : T_1; ...; m_n X_n : T_n)
formában jelölhetünk, ahol
P
az eljárás neve,m_i
az i. paraméter kezelési módja,X_i
az i. paraméter azonosítója,T_i
az i. paraméter adattípusa.
Az eljárás algoritmusa egy olyan szerkezeti ábrával adható meg, melynek a feje így néz ki:
Ennek az eljárásműveletnek adott A_1, ..., A_n
argumentumokra történő végrehajtását eljáráshívásnak nevezzük és a P(A_1,...,A_n)
módon jelöljük.
Ha az i. paraméter kezelési módja kimenő vagy be- és kimenő, akkor az A_i
aktuális argumentum csak változó lehet (azaz nem lehet tetszőleges kifejezés).
Mivel az eljárásműveletnek nincs visszatérési értéke, magának az eljárásműveletnek nincs értéke, így az eljáráshívás utasítás, azaz nem lehet része kifejezésnek, utána ;
-t kell tenni.
A függvényművelet általános (nem a C nyelvhez igazodó) jelölése annyiban tér el az eljárásművelet jelölésétől, hogy jelezzük benne a visszatérési érték típusát is:
A függvényművelet korábbi jelölései tehát kiegészülnek az argumentumok kezelési módjával.
Mint említettük, a C nyelvben nincs igazán nagy különbség az eljárás és a függvény között.
Valójában az eljárás egy olyan függvény C-ben, aminek a visszatérési értéke void
típusú.
Az eljárásokban nem kötelező return
utasításnak szerepelnie, de lehet, bár ekkor sem adható meg neki visszatérési érték (vagyis az utasítást rögtön le is kell zárni egy ;
-vel).
Mivel a C nyelvben lényegében csak függvényművelet és csak bemenő módú paraméterkezelés van, így a továbbiakban az egyszerűbb, C-hez igazodó jelölésmódot alkalmazzuk.
A legnagyobb különbség C-ben az eljárások és függvények között az eljáráshívás és függvényhívás esetén adódik. Míg az eljáráshívás utasítás, addig a függvényhívás kifejezés, vagyis részkifejezése lehet összetett kifejezéseknek.
Vegyes és kimenő módú argumentumok¶
A C nyelvben a függvényművelet paraméterei bemenő módúak, tehát alapvetően a függvényművelet végrehajtása az aktuális argumentumok megváltozását nem eredményezheti. A be- és kimenő, valamint kimenő módú paramétereket a pointerek segítségével magunk kell, hogy kezeljük.
Az alábbiakban egy működő megoldást mutatunk, egyelőre részletes magyarázat nélkül (majd a pointer adattípus ismertetésénél érthetővé válnak a részletek is):
- Ha az i. paramétert kimenő (vagy vegyes) módúnak szeretnénk, akkor a függvény deklarációjában
T_i X_i
helyettT_i *X_i
deklarációt, a függvénytörzsben pedigX_i
helyett mindenhol*X_i
változóhivatkozást használunk. - Továbbá a függvény meghívásakor az
A_i
argumentum helyett az&A_i
argumentumot használjuk. (Talán itt már így érthető, hogy ascanf
utasításnál miért is kellett az&
jelet használni az egyszerű típusú változók előtt. Így lettek kimenő módú paraméterek, amik ascanf
hatására értéket kaphattak.)
Példa: kamatos kamat számítása¶
-
Problémafelvetés: A szakemberek régóta figyelmeztetnek, hogy a jelenlegi nyugdíjrendszer a jelenlegi körülmények között hosszú távon nem fentartható, ezért mindenki gondoskodjon magáról. Ennek egyik módja lehet, ha valaki egy nagyobb összeget hosszabb időre leköt. Ennek a megtakarításnak a jellemzője, hogy az éves kamat minden év végén hozzáadódik a tőkéhez, és együtt kamatoznak tovább. Szeretnénk tudni, hogy ha ilyen módon lekötünk egy összeget fix éves kamatszint mellett több évre, akkor a végén mennyi pénzt tudunk majd felvenni. Kellene egy program, ami kamatos kamatot tud számolni!
-
Specifikáció: A probléma inputja a betett összeg (valós), az éves kamatláb (egész, százalékos érték) és az évek száma (egész). A kimenet az adott időre betett összeg éves alapon tőkenövelt kamatozással adott kamatlábbal számított új értéke a futamidő végén.
-
Algoritmustervezés: A kamatos-kamat számításhoz készítünk egy általános hatványozó függvényt és ezt hívjuk meg a kamatos-kamat számítás alapképletével. Ennek szerkezeti ábrája:
Felmerülhet a kérdés, hogy miért nem szorozzuk össze n-szer az x értéket. Ezt is megtehetnénk, mivel azonban az x valós, és adott esetben kicsi szám, így a valós számábrázolás miatt a számításunk pontatlan lehet. A logaritmus azonosságokat felhasználva (https://hu.wikipedia.org/wiki/Logaritmus) azonban a hatványműveletet felírhatjuk logaritmikus kifejezésként.
Ha n==0, akkor a hatvány értéke 1, vagyis x0==1 (ez x==0 esetén definíció kérdése, de 00-t halandók számára 1-nek szokás definiálni). Ha a hatványozás alapja a nulla, akkor a hatvány értéke is 0, azaz x0 (kivéve a 00 esetet).
Amennyiben x>0, az xn kifejezés felírható e(n*log_e(x)) alakban. Mivel a logaritmus függvény negatív számra nem értelmezhető, így annak abszolút értékével számolhatunk. A hatvány előjelét úgyis n paritása fogja meghatározni.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
A program implementációja tehát jelen esetben tartalmaz egy függvényt, amely célja, hogy a paraméterében kapott hatványalap és -kitevő ismeretében meghatározza az adott hatvány értéket, és ezzel az értékkel visszatérjen.
A tervezés során már láttuk, hogy ehhez a számításhoz szükségünk lesz ez exp
és a log
függvényekre, amelyeket a math.h
deklarál számunkra, és a matematikai függvénykönyvtár valósít meg.
Emiatt fontos, hogy amikor fordítjuk a programot, akkor ezt a függvény könyvtárat a -lm
kapcsolóval hozzá kell linkelni a programunkhoz.
A hatv
függvényen belül a szerkezeti ábrának megfelelően egy többszörös szelekciót valósítunk meg, ami a paraméterek függvényében meghatározza a megfelelő visszatérési értéket.
A 17. sor feltétele lehet első pillanatra egy kicsit furcsább.
Itt bitenkénti 'és' (&) műveletet végzünk.
Az n&1
kifejezés eredménye minden olyan esetben 1, amikor az egész szám utolsó bitje 1-es, és minden egyéb esetben 0.
Azaz ha a szám páratlan, akkor ennek a kifejezésnek az értéke 1, ami a logikailag igaz érték, amennyiben páros, akkor a kifejezés értéke 0, azaz logikailag hamis érték.
A program main
metódusában bekérjük a felhasználótól azokat az adatokat, amelyekre a kamatos kamatot számítaná, majd a program 37. sorában az adott paraméterekre meghívjuk, illetve alkalmazzuk a hatv
függvényt.
Az adatok beolvasásánál alkalmazzuk azt a korábban már megismert módszert, ami segítségével elvárjuk, hogy minden adat külön sorban érkezzen a felhasználótól.
Blokkstruktúra a C nyelvben¶
A C nyelvben blokkon egy {
és }
zárójelpárba zárt programrészt értünk.
Egy C program blokkjai mellérendeltségi és alárendeltségi viszonyban vannak.
Ezt a viszonyt az ide vonatkozó szabályokkal együtt blokkstruktúrának nevezzük.
A blokkstruktúra az azonosítók láthatóságát befolyásolja, de hatással lehet például a változók létezésére is.
A blokkok elhelyezkedése és sorrendje meghatározza, hogy a végrehajtás adott pontján, azaz a program egy adott utasításban mely változók használhatóak, érhetőek el. Mindezt három szabály határozza meg.
- Sorrendiségi szabály: a program egy adott pontján csak a hivatkozás helyét megelőzően már deklarált azonosítóra hivatkozhatunk. Változó-, függvény- és típus-azonosító a megjelenése helyén deklaráltnak minősül.
- Egyediségi szabály: egy adott blokkban egy azonosító csak egyszer deklarálható, nem számítva az alárendelt blokkokat.
- Láthatósági szabály: Egy B1 blokkban deklarált A azonosító akkor és csak akkor látható (hivatkozható) egy B2 blokkban, ha
- B1 megegyezik B2-vel, vagy B2 alárendeltje B1-nek és az A azonosító előbb van deklarálva, mint B2, és
- az A azonosító nincs deklarálva egyetlen olyan C blokkban sem, amely alárendeltje B1-nek és amelynek B2 alárendeltje (beleértve azt, hogy B2 megegyezik C-vel).
Pár példán keresztül nézzük meg, mit is jelentenek ezek a szabályok!
- példa az egyediségi szabályt mutatja be.
A piros nyíllal jelölt sorban egyszerre két változót deklarálunk ugyanazon a néven.
Ez probléma, hiszen ekkor a zöld nyíllal jelölt sorokban nem tudjuk, hogy adott ponton mely változóval kell dolgozni.
a
változót, illetve az f
függvényt próbáljuk hivatkozni, de ezt nem tehetjük meg, hiszen ezek csak a használat helye után lesznek deklarálva.
(Na jó, ez a példa kicsit sántít, mert sok C fordító az ANSI C szabványból adódóan bizonyos esetekben az előzetes deklaráció nélküli függvényhivatkozást is elfogadja, bár általában szól érte.)a
változó használható az alárendelt 2. blokkban, mivel a deklarációja előbb van, ráadásul nincs olyan blokk a kettő pont között, amelyben deklarálva lenne.b
változó használható a zöld nyíllal jelölt sorban.
Bár a piros nyíllal jelölt sorban is deklarálunk egy b
változót, az egy alárendelt blokkban van, így az a deklaráció a zöld nyíllal jelölt sorban nem látszódik.c
változó nem látható a 4. blokkban (amit a 3. blokkon keresztül tartalmaz az 1-es blokk), mivel a 3. blokkban a kék nyíllal jelölt sorban ezt a c
változót felüldefiniáljuk.
Mint látszik, az sem baj, hogy ez a c
változó más típussal rendelkezik, mint az elsőként deklarált c
.Azon blokkok összességét, amelyből egy a
azonosító látható, az a
azonosító hatáskörének nevezzük.
Egy azonosítót lokálisnak nevezünk egy blokkra nézve, ha az azonosító az adott blokkban van deklarálva.
Azt mondjuk, hogy egy a
azonosító globális egy B
blokkra nézve, ha nem B
-ben van deklarálva, de látható B
-ben.
A blokkstruktúra alapján látható, hogy a C nyelvben vannak úgynevezett lokális változók, sőt általában ezeket használjuk.
Látható azonban az is, hogy a programfájlban deklarált programegységek globálisak az összes függvénydeklarációra nézve, vagyis ezek minden blokkban láthatóak a deklarálásuktól kezdve az újradeklarálásukig.
Ezeket csak nagyon indokolt esetben szoktuk használni.
A gcc néha elviseli, ha egy függvényt hamarabb használunk, mint ahogyan deklarálnánk (tehát megsértjük a sorrendiségi szabályt).
A hívásból ugyanis ki tudja deríteni a paraméterek számát és típusát, a visszatérési értéket viszont ilyen esetekben int
-ként kezeli.
Az ansi C nem engedi meg a deklarációk és utasítások keveredését, tehát már a blokk elején deklarálni kell az összes változót.
A \(C^{99}\) szabvány ennél rugalmasabb.
Ha azt akarjuk, hogy a gcc az ilyen eseteket warninggal jelezze, akkor használjuk a --pedantic
kapcsolót fordításkor.
Tárolási osztályok¶
Eddig minden esetben, amikor egy változót deklaráltunk, annyi dolgunk volt, hogy megmondjuk, az adott változó milyen típussal rendelkezik. Ekkor a változó számára memória allokálódik. A globálisan deklarált változók számára "statikus" hely foglalódik a program teljes futási idejére. Ez azt jelenti, hogy az adott változó a program teljes futási ideje alatt ugyanazon a memóriaterületen található. A blokkokban lokálisan deklarált változóknak a veremben foglalódik hely az adott blokk végrehajtási idejére. Többször végrehajtva az adott blokkot, nem garantált, hogy a változó ugyanazt a memóraiterületet kapja meg, amit korábban. Ez a tárolási mód az alapértelmezett, automatikus tárolási mód, amelyet az auto kulcsszóval is jelezhetünk a deklaráció előtt.
Ha a deklaráció elé betesszük a static kulcsszót, akkor az adott változónak mindenképp statikus, azaz állandó helye lesz a program teljes futási ideje alatt, még akkor is, ha csak egy lokális változóról van szó. Ez a memóriahely már a program kezdetén allkoálódik és inicializálódik, a globális változókkal együtt. A változó értéke megmarad a blokk végrehajtása után is, és az újabb végrehajtás során ez a megőrzött érték újrafelhasználható, a változó deklarációja és inicializálása nem történik meg újra. Ez a kulcsszó a változó létezését befolyásolja, a láthatóságát viszont nem.
Amennyiben a deklaráció előtt az extern (külső) kulcsszó szerepel, akkor ezzel jelezzük a fordítónak, hogy az adott változónak nem kell helyet foglalnia, a változó deklarációja egy olyan programegységben történik meg, amelyet később, a linkelés által fog az adott programkomponens elérni.
A register változót, ha lehetséges a fordító a számítógép regiszterébe helyezi (ha nem tudja, akkor a változó a memóriában marad). Ezt a tárolási módot akkor célszerű választani, amikor egy adott változó sok számításnak az eleme, ilyenkor a program futásidejét csökkentheti, ha az adott adatot nem kell mindig a regiszterbe mozgatni. Azonban mindig fordító függő, hogy az adott változó a regiszterbe kerül, vagy sem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Futtatva a kódot látszik, hogy az ideiglenes
nevű változó minden esetben újra inicializálódik, az értéke 1 lesz, az allando
változó, amely előtt ott volt a static
módosító, az megőrzi a blokk végén levő értékét, és ez az érték lesz a kezdőértéke akkor, amikor a blokkot újra elkezdjük végrehajtani:
Kimenet
ideiglenes = 1 allando = 1
ideiglenes = 1 allando = 2
ideiglenes = 1 allando = 3
ideiglenes = 1 allando = 4
ideiglenes = 1 allando = 5
Rekurzió¶
Sokszor adódik úgy, hogy egy problémát úgy a legegyszerűbb megoldani, ha a probléma megoldását megpróbáljuk visszavezetni probléma egy egyszerűbb esetére. Az ilyenfajta megoldást rekurziónak nevezzük.
A C nyelven bármelyik függvény lehet rekurzív illetve részt vehet rekurzív függvényrendszerben (ahol a függvények nem feltétlenül közvetlenül hívják meg magukat, hanem esetleg más függvényeken keresztül).
Példa: Hanoi tornyai¶
-
Problémafelvetés: Adjunk megoldást a Hanoi tornyai játékra. Az Édouard Lucas francia matematikus által kitalált játék lényege, hogy adott három oszlop, amely egyikén egy torony van, amely egyre csökkenő átmérővel rendelkező korongok összessége. Ezt a tornyot kell úgy áthelyezni valamelyik másik üres oszlopra, hogy az alábbi szabályokat nem szegjük meg:
- egyszerre csak egy korongot lehet mozgatni,
- és nagyobb átmérőjű korong nem kerülhet kisebb korongra
-
Specifikáció: Az input a korongok száma (pozitív egész) valamint a kezdő és a cél oszlop sorszáma 1, 2 vagy 3 lehet. Feladat egy olyan szöveges leírás megadása, amely leírja azt a tevékenységsorozatot, amellyel az adott magas torony a megadott oszlopról a megnevezett cél oszlopra mozgatható.
- Algoritmustervezés:
- Az 1 magasságú torony átpakolása nem igényel előkészületet, azonnal elvégezhető, hiszen a forrás oszlopról egyszerűen átmozgathatjuk a cél oszlopra, és mindeközben nem sértünk szabályt.
- Készítsünk egy rekurzív eljárást, amelyik az N magasságú torony átpakolását visszavezeti az N-1 magasságú torony átpakolására. Ekkor az N-1 magas tornyot átmozgatjuk a segéd toronyra, a legnagyobb korongot átmozgatjuk a cél toronyra, majd a segéd toronyra helyezett N-1 magas tornyot átmozgatjuk a cél toronyra:
- Mivel az X magasságú torony, amit az egyik rúdról a másikra pakolunk mindig az X legkisebb korongból áll, a harmadik rudat akkor is használhatjuk segédrúdként, ha azon van korong, mivel ez biztosan nagyobb, mint a legnagyobb, amit mi át szeretnénk pakolni.
- Az 1 magasságú torony átpakolása nem igényel előkészületet, azonnal elvégezhető, hiszen a forrás oszlopról egyszerűen átmozgathatjuk a cél oszlopra, és mindeközben nem sértünk szabályt.
- Algoritmustervezés -- Szerkezeti ábra:
- Főprogram:
- A rekurzív függvény:
- Főprogram:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Függvényhívás végrehajtása (i386-linux)¶
Amikor egy függvényt végre akarunk hajtani, akkor érdemes tisztában lenni azzal, mi is történik a végrehajtás során a memóriában. A következő kis animáció azt mutatja, mi történik akkor, amikor egy függvényhívás kifejezést kiértékelünk.
A példa első lépése, hogy a globális változóként deklarált változókat inicializálja a main
függvényben.
Ezek a változók a statikus, most kékkel jelölt memóriaterületen allokálódtak.
Tegyük fel, hogy a felhasználó által megadott kezdőértékek rendre a 8, 8 és 4.
Amikor meghívódik az A
függvény, ezen változók, mint aktuális paraméterek kerülnek felhasználásra, értékei bemásolódnak a verembe, ahol az A
függvény formális paraméterei által hivatkozhatóakká válnak.
Az argumentumok elvileg tetszőleges sorrendben értékelődhetnek ki (a szabvány ezt nem definiálja), de a paraméterek jobbról balra haladva kerülnek a verembe. Mivel mind értékparaméter, az i-edik argumentum aktuális értéke átadódik az i-edik paraméternek, vagyis az aktuális argumentum értéke bemásolódik a paraméter számára foglalt memóriahelyre.
Ekkor a verembe bekerülnek azok a technikai információk is (pl. visszatérési cím), és a vezérlés átadódik a függvénynek.
A függvény a végrehajtása során memóriát allokál a saját lokális változói számára a veremben.
A függvény végrehajtása addig tart, amíg el nem jutunk egy return
utasításig.
Amikor a függvény végrehajtása befejeződött, akkor a függvényblokk formális paraméterei számára foglalt megórai felszabadul, a függvényhívás kifejezés felveszi a függvényben kiszámolt értéket.
A függvényből a vezérlés végrehajtása visszakerül a hívó függvényhez, amely gondoskodik a veremmutató visszaállításáról arra a pozícióra, amelyen az állt az előtt, hogy az aktuális paramétereket rátette a veremre.
Függvényhívás végrehajtása rekurzióval (i386-linux)¶
Amikor rekurzív függvényt hajtunk végre,többször egymás után, akkor ez a folyamat ismétlődik úgy, hogy a veremre pakolt értékek egyre szaporodnak. Tulajdonképpen teljesen mindegy, hogy a hívás rekurzív, vagy sem, ugyanaz történik minden egymásba ágyazott függvényhívásnál.
Blokkok végrehajtása¶
A C nyelvben nem csak függvény szinten, hanem blokk szinten lehet változókat deklarálni. Ezek tárolását a C szintén a veremben végzi. Emiatt egy blokk végrehajtásának lépései nagyon hasonlítanak a függvények végrehajtásához.
- Memória helyfoglalás a blokk változói számára.
- A blokk végrehajtása.
- Memória felszabadítása.
Függvények mellékhatása¶
Függvény mellékhatásán azt értjük, hogy a függvényhívás hatására nem csak a függvényérték számítódik ki (és a paraméterek változhatnak meg), hanem megváltozhat egy globális változó értéke is (vagy egyéb műveletek is végrehajtódnak, pl. kiíratás). Mellékhatás következménye, hogy az összeadás kommutativitása nem feltétlenül teljesül, ha a tagok függvényhívások. C-ben ugyanis nincs meghatározva, hogy két részkifejezés közül melyiket kell előbb kiértékelni, tehát az sem világos, hogy ha mindkettőben van függvényhívás, melyik hajtódik végre előbb.
A következő példában a main
függvényben a Z
változó úgy kap értéket, hogy abban egy olyan összeget kell meghatározni, amelynek tagjai az f
függvény kifejezések értékei.
Ugyanakkor az f
függvény módosítja az A
globális változó értékét, és mivel egyik tag esetében ez a változó a függvény paramétere, amely majd meghatározza közvetve a függvény visszatérési értékét, nem mindegy, hogy az f(A)
, vagy f(B)
kifejezés értékelődik ki először.
Könnyen látható, hogy amennyiben az f(A)
értékelődik ki először, annak értéke 2 lesz, miközben A
értéke is A
-re módosul. Ekkor az f(B)
kifejezés értéke 4 lesz, azaz az f(A)+f(B)
kifejezés értéke 6.
Ha előbb f(B)
értékelődik ki, akkor f(B)
értéke 3, f(A)
értéke 6, azaz f(A)+f(B)
értéke 9 lesz.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Függvények előnyei¶
- Többszörös felhasználás: Hasonló részproblémák megoldására elég egy függvényt készíteni és a különböző adatokra végrehajtatni a részalgoritmust. Így a program méretét csökkenteni lehet.
- Memória igény csökkentése: A függvények lokális változói számára csak a függvény végrehajtása idejére foglalódik memória.
- Karbantarthatóság: Függvények használatával a program áttekinthetőbb lesz, ami jelentősen megkönnyíti a későbbi módosítását.
- Modularitás: A tervezés során a részproblémák függvénnyel történő megoldása lehetővé teszi a figyelem lokalizálását.
- Programhelyesség: Függvények alkalmazása megkönnyíti a bizonyítást, a program tesztelését, a hibakeresést és javítást.