Függvények használata; tömbök függvényekben; a korábbi programozási tételes feladatok újrafogalmazása függvényekkel.
{esszé}
Fogalmazza meg, hogy a mi a különbség a függvény formális és aktuális paraméterei között! Adjon példát egy függvény fejsorára, és ugyanennek a függvénynek a hívására! A híváshoz ne felejtse el leírni a megfelelő paraméterek deklarációját!
{/esszé}
A függvény programozásbeli fogalmának lényege a következő: egy olyan viszonylagosan zárt, önálló programegység, amely a bemenetéül szolgáló adatok felhasználásával előállítja a kimenetéül meghatározott halmazba tartozó értéket.
A bemenetéül szolgáló adatokat általában az argumentumában soroljuk föl, amelyek neve, sorrendje és típusa kötött. A kimenetét alkotó értékek halmazának a függvény ún. értéktípusa felel meg.
A viszonylagos zártsága arra utal, hogy általában csak a paraméterein keresztül tartja a kapcsolatot a környezetével. Bizonyos kényelmi okok miatt lesz példa arra, hogy egy függvény a program paramétereit közvetlenül, azaz nem a paraméterezés útján éri el.
Az önálló jelző pedig azt fejezi ki, hogy jól meghatározható az a (rész)feladat, amelyet meg kell oldania, így minden olyan programba változatlanul beilleszthető, amelyben ez a részfeladat fölvetődik. Így válik hasznos eszközévé a felülről lefelé történő programtervezésnek.
A függvények a programozás során (is) kétféle szituációban bukkan(hat)nak föl: amikor fölhasználjuk, és amikor definiáljuk azokat. Amikor fölhasználjuk, akkor a paraméterei azok az objektumok, amelyekkel dolgoznia kell. Ezeket a paramétereket aktuális paramétereknek nevezik. Amikor definiáljuk a függvényszámítást, akkor a paraméterek azt írják le, hogy milyen módon játszanak szerepet a számításban. Ezek az ún. formális paraméterek.
A specifikációban a függvényekre hivatkozás nem különbözik a matematikában megszokottól. A definiáláshoz a specifikációban egy külön rész szolgál, amelyben először az adott függvény értelmezési tartományát és értékkészletét adjuk meg (ez az ún. szignatúra), majd a leképezés szabályát. Egy korábbi gyakorlatból idézzük az alábbi specifikációrészletet:
…
Utófeltétel
PrímN∈[10N-1+1 .. 10N-1] és (PrímN Mod 10)=1 és Prím?(PrímN) és …
Definíció
Prim?:Egész→Logikai
Prím?(x):= ∀i∈[2..Gyök(x)]: nem (i | x)
Algoritmizálás közben a hivatkozás a szokásnak megfelelően történik, a definiáláshoz viszont egy új struktogramot kell szentelni, amelyben két újdonság van:
1) a fejsor – a függvény nevén felül – felsorolja az értelmezési tartományt megtestesítő paraméterek lokális neveit, azaz amikkel a függvény törzsén belül lehet rájuk hivatkozni;
2) a függvény értékét egy, a függvény nevével megegyező nevű változó képviseli, a neki adott érték lesz a függvény végső (visszaadott) értéke.
A függvénydefiniálásra egy struktogram-példa:
A C++ kódban a hivatkozás az eddigiekhez hasonlóan egyszerű: függvényNév(paraméterek). A definiáláshoz alapelv, hogy a fordítóprogramnak már az előtt ismernie kell a függvényhívás szintaktikáját, azaz a függvény szignatúráját, mielőtt használnánk, hivatkoznánk rá. Természetesen a fordításhoz a függvény működését leíró törzs is kell, de azt szabad halogatni, a forrásprogram végére hagyni. Így a javasolt programszerkezet az alábbi:
#include <iostream> using namespace std; … fv-szignatúrák … int main() { … main-törzs … return 0; } … fv-definiciók …
A fv-szignatúra szintaxisa a deklarációkéra emlékeztet; újdonság a paraméterezés jelölése:
típ fv-azonosító(formális paraméterezés);
Jegyezzük azonban meg, hogy a paraméterezés még akkor sem hagyható el, ha valójában nincs paramétere, azaz a zárójelek nem hagyhatók el!
A fv-definíció szintaxisa – nem meglepő módon – nagyban emlékeztet a már megismert main() függvény szintaxisára:
típ fv-azonosító(formális paraméterezés) { … a fv-törzs … //megegyezik a main-törzs szintaxisával return fv-érték; //fv-érték: egy kifejezés, amely a visszaadandó //értéket képviseli; típusa: típ }
Mindennél többet mond egy konkrét kód-példa:
#include <iostream> using namespace std; bool PrimE(int x); int main() { … if (PrimE(N)) { … } … return 0; } bool PrimE(int x) { int i=2; while (i*i<=x && i%x!=0) { ++i; } return i*i>x; }
Megjegyzések a konkrét kódhoz:
A függvényeknek van egy olyan fajtája, amely nem úgy ad vissza értéket, mint egy függvény, hanem paraméterei közt van olyan, amely értékét módosítja. Ezeket eljárásfüggvényeknek (vagy egyszerűen: eljárásoknak) nevezik. Ekkor a típusaként a void (üres, mentes jelentésű) szót kell megadni. A return utasításra ekkor nincs szükség.
Kétféle számunkra érdekes paraméterátadási módszer van: az érték szerinti, illetve a hivatkozás szerinti.
Az érték szerinti paraméterátadás lényege: a formális paraméterből keletkezett lokális változóba másolódik a híváskor az aktuális paraméter értéke, így ennek a törzsön belüli megváltozása nincs hatással az aktuális paraméterre.
A hivatkozás szerinti paraméterátadás lényege: a formális paraméterbe az aktuális paraméter címe (rá való hivatkozás) kerül, a lokális néven is elérhetővé válik. Így értelemszerűen akár meg változtatni is képes azt.
A függvény szignatúrájában jelölni kell, hogy a paraméterátadás melyik fajtáját írjuk elő. Hogy ezt miként kell tükrözni a C++ nyelvben, írjuk le alább.
Skalárváltozók esetén az érték szerinti: típus formális-paraméter; a hivatkozás szerinti: típus &formális-paraméter.
int max(int x, int y)
vagy ugyanez eljárásfüggvényként:
void max(int x, int y, int &maxxy)
Tömbváltozók esetén az érték szerinti: const típus formális-paraméter[]; a hivatkozás szerinti: típus formális-paraméter[]. Vegyük észre a C++ filozófiaváltását: tömböknél a hivatkozás szerintit tekinti alapvetőbbnek (⇒egyszerűbben leírhatónak).
void ki_int_tomb(const int x[], int n) void be_int_tomb(int x[], int &n, int maxN)
Az animáció bemutatja a függvények használatát.
Készítsük el azt az eljárást, amely beolvas egy min..max közötti int számot! Ha a max<min, az jelentse azt, hogy max=+∞! Az eljárás szignatúrája az alábbi:
void be_int(string kerdes, int &n, int min, int max, string uzenet)
Bemenet
kérdés,üzenet:Szöveg, min,max:Egész
Kimenet
n:Egész
Előfeltétel
−
Utófeltétel
∃i>0: n=cini és (min≤max → n∈[min..max]) és
(min>max → n≥min) és
∀j∈[1..i): coutj=üzenet
Magyarázat az utófeltételhez: A ∃i: n=cini azt fejezi ki, hogy előbb-utóbb (pontosabban az i. próbálkozásra) a billentyűzetre kerül számként értelmezhető jelsorozat, amely teljesíti a határok által megfogalmazott feltételeket. Az addigi elvárást fejezi ki a ∀j∈[1..i): coutj=üzenet feltételrész, amit így olvashatunk: az első helyes inputig (az i. próbálkozást megelőzően) a konzol outputra a megadott (hiba-)üzenet kerül.
//beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet) { bool hiba; string tmp; do { if (max>=min) { cout << kerdes << " (" << min << ".." << max << "):"; cin >> n; hiba=cin.fail() || n<min || n>max; } else { cout << kerdes << " (" << min << "..):"; cin >> n; hiba=cin.fail() || n<min; } if (hiba) { cout << uzenet << endl; cin.clear(); getline(cin,tmp,'\n'); } }while(hiba); }
Készítsük el azt az eljárást, amely megcserél két int értéket! Az eljárás szignatúrája:
void csere_int(int &a, int &b)
Bemenet
a,b:Egész
Kimenet
a,b:Egész
Előfeltétel
−
Utófeltétel
a’=b és b’=a
Éltünk a korábban bevezetett struktogram egyszerűsítési lehetőséggel, amely során több értékadást is egyetlen dobozba sűrítettünk.
//csere: a<=>b void csere_int(int &a, int &b) { int c=a; a=b; b=c; }
Építsük össze az előbbi két eljárást egy próbaprogramba amely csak pozitív számokat fogad el!
Bemenet
A,B:Egész
Kimenet
A,B:Egész
Előfeltétel
−
Utófeltétel
A’=B és B’=A
Banálisan egyszerű: a nem algoritmizálandó beolvasáson és kiíráson túl a csereeljárás meghívását tartalmazza, valamint a felhasznált két eljárás struktogramját. Ezért nem részletezzük.
#include <iostream> using namespace std; //beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet); //csere: a<=>b void csere_int(int &a, int &b); int main() { int A,B; be_int("Kérem az 'A'-t (pozitív egész):",A,1,0,"Nem jó szám!"); be_int("Kérem a 'B'-t (pozitív egész):",B,1,0,"Nem jó szám!"); csere_int(A,B); cout << "Az 'A':" << A << endl; cout << "A 'B':" << B << endl; return 0; } //beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet) { bool hiba; string tmp; do { if (max>=min) { cout << kerdes << " (" << min << ".." << max << "):"; cin >> n; hiba=cin.fail() || n<min || n>max; } else { cout << kerdes << " (" << min << "..):"; cin >> n; hiba=cin.fail() || n<min; } if (hiba) { cout << uzenet << endl; cin.clear(); getline(cin,tmp,'\n'); } }while(hiba); } //csere: a<=>b void csere_int(int &a, int &b) { int c=a; a=b; b=c; }
Egy korábbi leckében a következő feladat került szóba, amelyet ismét előveszünk, de az „újdonságok” kedvéért:
Határozzuk meg az első 1-re végződő N jegyű prímszámot! Oldjuk meg most függvények felhasználásával!
Ezt a specifikációt készítettük akkor:
Bemenet
N:Egész
Kimenet
PrímN:Egész
Előfeltétel
N>1 [ilyen garantáltan létezik]
Utófeltétel
PrímN∈[10N-1+1 .. 10N-1] és (PrímN Mod 10)=1 és Prím?(PrímN) és
∀pm∈[10N-1+1 .. 10N-1]: (pm Mod 10)=1 és Prím?(pm) → pm≥PrímN
Definíció
Prim?:Egész→Logikai
Prím?(x):= ∀i∈[2..Gyök(x)]: nem (i | x)
Az akkori algoritmusok alapján az alábbiakat alkothatjuk meg. Csak a legfelsőbb szintű, lényegi algoritmusban (PrímKeresés-ben) van elenyésző változás. A változást épp a függvénnyé alakítás okozza.
A kiinduló algoritmus | → | A függvényesített algoritmus |
---|---|---|
→ |
Változatlanul hagyhatjuk a Prím?() függvényt:
A kódoláskor felhasználtuk a korábban elkészített egész-beolvasó eljárást és a PrimE() függvényt. Az algoritmus alapján kódoltuk a PrimKereses() függvényt, és egy később is felhasználható, billentyűlenyomásra várakoztató eljárást is készítettünk.
#include <iostream> #include <windows.h> using namespace std; //beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet); //n-jegyű prímet keres: int PrimKereses(int n); //eldönti: x prímszám-e int PrimE(int x); //billentyű-lenyomásra várakozik void billreVar(); int main() { int N; //a jegyek száma int PrimN;//N jegyű prím be_int("A jegyek száma (>1):",N,2,0,"Nem jó számhossz!"); PrimN=PrimKereses(N); cout << "Az első 1-re végződő " << N << "-jegyű prím:"
<< PrimN << endl; billreVar(); return 0; } //beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet) { bool hiba; string tmp; do { if (max>=min) { cout << kerdes << " (" << min << ".." << max << "):"; cin >> n; hiba=cin.fail() || n<min || n>max; } else { cout << kerdes << " (" << min << "..):"; cin >> n; hiba=cin.fail() || n<min; } if (hiba) { cout << uzenet << endl; cin.clear(); getline(cin,tmp,'\n'); } } while(hiba); } //n-jegyű prímet keres: int PrimKereses(int n) { //10^(n-1) (n>1): int tn=1; for (int i=1;i<n;++i) { tn*=10; } int i=tn+1; while (!PrimE(i)) { i+=10; } return i; } //eldönti: x prímszám-e int PrimE(int x) { int i=2; while (i*i<=x && i%x!=0) { ++i; } return i*i>x; } //billentyű-lenyomásra várakozik void billreVar() { system("pause");//windows esetében! }
Írjuk meg azt az eljáráspárt, amely beolvas, illetve kiír egy N elemű tömböt! A tömb legyen statikusan deklarálva. Szignatúráik legyenek az alábbiak:
void be_int_tomb(int x[], int &n, int maxN) void ki_int_tomb(const int x[], int n)
Majd ezek kipróbálására a következő feladatú eljárást is készítsük el: a beolvasott elemek sorrendjének a megfordítsa! Használjuk föl a korábbi csere(…) eljárást is! A megfordítást végezze az alábbi alprogram (a paraméterezés „megálmodása” is a feladat része):
void megfordit_int_tomb( ??? , ??? )
Adja meg egy természetes szám prímtényezős felbontását! A programban használjuk az előbbiekben létrehozott eljárásokat, valamint egy újat, amely egy tömbben sorolja föl a prímtényezőket!
Válasszon ki az eddigi gyakorlatokon elkészített programok közül néhányat, és írja át ezeket a fentiek szellemében úgy, hogy a beolvasást, a lényegi tevékenységet és a kiírást függvényekkel valósítja meg!