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.
Tartalomjegyzék
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.
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).
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.
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) |