5. fejezet - Alprogramok, vezérlésátadás


A strukturált programozási nyelvek egyik alapeleme az alprogram. Ezekből kétféle van, az eljárás és a függvény. Utóbbit általában akkor használjuk, ha ki akarunk számolni valamilyen értéket, mondjuk egy szám szinuszát, eljárást pedig akkor ha valamit végre akarunk hajtani.

Persze vannak olyan nyelvek, amik nem strukturáltak, például az első Fortran. Ott alprogramok helyett ugróutasításokat használnak (pl. goto). Egyesek szerint az ilyen ugróutasításoktól spagetti lesz a kód, és ezért ki kéne őket gyomlálni a prognyelvekből. Szerintem viszont nem, azoknak is van haszna (és amilyen kódokat látok mostanában nekem senki ne merje szidni a goto-t).



Nem kell messzire menni, hogy példát találjunk, hiszen a main függvény is egy függvény. Persze bonyolult lenne az élet, ha minden kódot ebbe kéne írni, sőt meglehetősen redundáns is lenne. A szoftverfejlesztés egyik aspektusa az újrafelhasználhatóság, azaz olyan egységekre kell lebontani a programot, amiket később újra fel tudunk használni. Írjuk meg az előző fejezetben látott lnko -t függvényként!

CODE
int lnko(int a, int b) { if( a == 0 ) return b; if( b == 0 ) return a; while( a != b ) { if( a > 0 ? a > b : a < b ) a -= b; else b -= a; } return a; }

Elsőre nem is tűnik bonyolultnak. Amikor definiálunk egy függvényt, az alábbi minta szerint járunk el:

<visszatérő érték típusa> <név>(<paraméterek>) { ... }

A minta nem teljes, mást is meg lehet még adni, de azok most nem érdekesek. A kapcsos zárójelek közötti részt a függvény törzsének, az azok előtti részt a függvény fejlécének nevezzük. Amikor meghívunk egy függvényt, akkor a program végrehajtása a hívás helyéről átugrik a függvény törzsére. Többek között ezt hívják vezérlésátadásnak. A függvényből visszaadható a vezérlés a return <visszatérő érték> utasítással.

Ha egy függvénynek void a visszatérési értéke (vagyis nincs), akkor azt eljárásnak tekintem, ugyanis C/C++ -ban nincs külön nyelvi elem az eljárásra. Ilyenkor nem kötelező megadni a return utasítást, csak ha valami miatt korábban vissza akarsz térni (például hiba történt).

A függvény meghívása (nem ebédre) a következőképpen néz ki:

CODE
int main() { std::cout << lnko(8, 6) << "\n"; std::cout << lnko(65, 35) << "\n"; std::cout << lnko(127, 121) << "\n"; std::cout << lnko(-6, -2) << "\n"; return 0; }

A függvény fejlécében megadott paramétereket formális paramétereknek, a híváskor megadottakat pedig aktuális paramétereknek hívják. Azt, hogy ezek hogyan feleltetődnek meg egymásnak paraméterátadásnak hívjuk. A formális paraméterek a függvényre nézve lokálisak, tehát a befejezéskor megsemmisülnek.


Kezdjük a legegyszerűbbel, az érték szerinti paraméterátadással. Ilyenkor az aktuális paraméterek értékei bemásolódnak a formális paraméterekbe, tehát előbbieket a függvény biztosan nem fogja megváltoztatni. Például a fenti lnko függvény is ilyet használ. Rögtön látjuk ennek a hátrányát is: mi van, ha a paraméter valami nagy adatszerkezet? Nem lenne túl célszerű másolgatni, hacsak nem akarjuk elnyerni a leglassabb program címet. Más nyelvekben: in vagy byval.

Ennek a kiküszöbölésére van a cím szerinti paraméterátadás. Erről még nem tudjuk, hogyan működik, hiszen nem írtam még pointerekről. Röviden arról van szó, hogy az aktuális paramétereknek a memóriacíme adódik át, tehát a függvény közvetetten ugyan, de az aktuális paraméterekkel dolgozik és akár meg is változtathatja azokat. Más nyelvekben: ref vagy byref.

Az sem túl örömteli, hogy csak egy visszatérési érték lehet, ha többet akarunk akkor vagy összefogjuk őket egy rekordba (későbbi fejezet), vagy eredmény szerinti paraméterátadást használunk. Ilyenkor az aktuális paraméter értékét a függvény nem kapja meg, csak a dolga végeztével az eredményt belemásolja. Rögtön következik ebből, hogy ebben az esetben az aktuális paraméter balérték kell legyen. Más nyelvekben: out.

Ennek egy rokona az érték-eredmény szerinti paraméterátadás, amikor meg is kapja az értéket és módosítja is. Más nyelvekben: in out.

Az eddigieknél az aktuális paraméter egyszer értékelődött ki, a függvény meghívásakor. De vannak olyan nyelvek, amikben magát a kifejezést lehet átadni és az minden hivatkozáskor ki fog értékelődni. Ezt név szerinti paraméterátadásnak hívjuk.

C++ -ban csak érték és cím szerinti paraméterátadás van, a többi viszont szimulálható az utóbbival.

A paramétereknek adhatunk alapértelmezett értéket azzal a feltétellel, hogy a fordító meg tudja majd feleltetni nekik az aktuális paramétereket. Tehát a default értékkel ellátott paramétereket a paraméterlista végén célszerű elhelyezni. Még egy megkötés, hogy a default paraméter nem hivatkozhat a paraméterlista többi elemére. A példában számoljuk ki egy vektor (1,2 vagy 3 dimenziós) hossznégyzetét (mivel gyököt vonni még nem tudunk).

CODE
float squared_length(float x, float y = 0, float z = 0) { return x * x + y * y + z * z; } int main() { std::cout << squared_length(5) << " " << squared_length(5, 6) << " " << squared_length(5, 6, 7) << "\n"; return 0; } // output: 25 61 110

Ha a függvény hívásakor nem adunk meg minden paramétert, akkor azok helyébe az alapértelmezettek helyettesítődnek be.


Természetesen semmi akadálya nincs annak, hogy egy függvény önmagát hívja. Vigyázni kell azonban, hogy egyrészt ne hívja magát a végtelenségig, másrészt ne fogyassza el a végrehajtási verem memóriáját (később).
Írjunk egy olyan függvényt, ami kiszámolja egy szám faktoriálisát! Nullára valószínüleg mindenki ki tudja számolni, mert arra egy lesz. Egy tetszőleges számra megkaphatjuk úgy, hogy kiszámoljuk az eggyel kisebb szám faktoriálisát, majd azt megszorozzuk a számmal. Tehát a függvény az alábbi módon néz ki:

CODE
unsigned int factorial(unsigned int n) { if( n == 0 ) return 1; return factorial(n - 1) * n; }

Sajnos mivel csak 32 bit van, ezért már 20! -t se lehet így kiszámolni. Vannak nyelvek amikben viszont ki lehet, például a Prolog (az ugyanis stringben tárolja a számokat).


A függvényeket nem csak a nevük azonosítja, hanem a nevük és a paraméterlistájuk együtt. Ez azt jelenti, hogy definiálhatunk két ugyanolyan nevű függvényt, ha a paraméterlistájuk eltérő. Ezt túlterhelésnek nevezik.

CODE
void foo() { std::cout << "foo\n"; } void foo(int i) { std::cout << "foo " << i << "\n"; }

Amivel nem lehet túlterhelni az a visszatérési érték. Tehát ha két függvény csak a visszatérési érték típusában különbözik, arra hibát fogsz kapni. Az osztályoknál majd lesznek konstans függvények is (amik nem változtatják meg az objektum állapotát). Az ilyen konstansággal is túl lehet terhelni.


Tegyük fel, hogy van sok .cpp fájlunk, amik mind használni akarnak valamilyen függvényt. Mondjuk az lnko függvény legyen az lnko.cpp-ben, és ezen kívül legyen egy a.cpp és b.cpp, mindkettő hívja be az lnko-t (emlékeztető: az #include direktíva a megadott fájlt bemásolja az aktuálisba).

CODE
// a.cpp #include "lnko.cpp" // b.cpp #include "lnko.cpp"

Fordításkor az alábbi hibát fogjuk kapni:

multiple definition of `lnko(int, int)`

Ez így nyilván nem járható út. Többek között erre találták ki azt a módszert, hogy szétválasszuk a függvény specifikációját (fejléc) az implementációjától. Ehhez a C/C++ a fejléc fájlokkal (header) nyújt segítséget. Ezek .h -ra végződnek és például függvények fejléceit tartalmazzák. Konvencionálisan minden header fájl egy ellenőrzéssel kezdődik, hogy ha egy fordítási egység többször is behívja, akkor ne legyenek névütközések. Az lnko a következőképpen néz ki szétválasztva:

CODE
// lnko.h #ifndef LNKO_H #define LNKO_H int lnko(int a, int b); #endif // lnko.cpp #include "lnko.h" int lnko(int a, int b) { // ... }

Ez a bizonyos ellenőrzés egy újabb előfordító direktíva; valójában egy feltételes fordítás. Megnézi, hogy definiálva van-e már az LNKO_H nevű makró, ha igen, akkor nem kell bemásolni még egyszer a specifikációkat. Néhány fordító megenged más módszereket is, de ez a szabványos megoldás, úgyhogy ezt fogom használni.


A C-vel való kompatibilitás miatt C++ -ban is megtartották a goto utasítást. Ez egy olyan utasítás amit sosem akarsz használni, de néha mégis hasznos. Tekintsük például az alábbi kódot:

CODE
bool valami() { // erőforrások kreálása if( hiba1 ) { // erőforrások felszabadítása return false; } if( hiba2 ) { // erőforrások felszabadítása return false; } // ... return true; }

Nem túl kényelmes kód, képzeljük el, hogy valamit kifelejtettünk a felszabadításból és írhatjuk át mind a kétszáz helyen. Ilyenkor muszáj a tiltott eszközhöz nyúlni az élet kényelmesebbé tételéhez:

CODE
bool valami() { // erőforrások kreálása if( hiba1 ) goto felszabadit; if( hiba2 ) goto felszabadit; felszabadit: // létező erőforrások felszabadítása // vigyázz mert ez így mindenképpen lefut! return error; }

A goto utasítás egy címkét vár, ahová a vezérlést átdobja. A címke az adott scope-ban kell hogy legyen definiálva, tehát például függvények között nem lehet ugrálni vele. Azért ahol lehet kerüld el.


A függvények a már korábban említett dekompozíció eszközei, szokták ezt moduláris dekompozíciónak is hívni. Ennek során a bonyolult feladatot lebontjuk kisebb feladatokra, majd ezeket külön programegységekben (modulokban) valósítjuk meg. A procedurális nyelvekben az ehhez felhasznált programegységek az alprogramok, míg az objektum-orientált nyelvekben az osztályok.

Függvényekről még bőven lesz szó, ebben a fejezetben csak a továbbhaladáshoz szükséges alapvető dolgokat tárgyaltam meg.


Höfö:

  • Írd meg az lkkt-t függvény formájában!
  • Válaszd szét az lnko és az lkkt specifikációját és implementációját!
    A fájlok neve legyen myfunctions.h és myfunctions.cpp

Irodalomjegyzék

http://en.cppreference.com/w/cpp
http://www.cplusplus.com/
http://www.parashift.com/c++-faq-lite/ (ezt különösen ajánlott elolvasni)
http://aszt.inf.elte.hu/~gsd/halado_cpp/ (ezt is)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!