C++ haladó leckék 17. – Saját függvények

Saját függvények, avagy lustaság fél egészség

Programjaink írásakor sokszor futunk bele olyan helyzetbe, hogy bizonyos kód részeket többször kell használni. Gondoljunk csak arra, hogy ha egy tömbbel dolgozunk, azt nyilván szeretnénk kiíratni. Utána mondjuk rendezni szeretnénk, de szintén szeretnénk kiíratni, hogy lássuk az eredményt. A kiíratás igaz ugyan, hogy csak egy gépelési feladat, de akkor is újra meg kell írni egymástól függetlenül kétszer. És ha csak egyszer írnánk meg, de akárhányszor használhatnánk? A saját függvények pontosan erre valók. Visszautalnék a függvények leckére, anélkül ne is olvass tovább. Ha az a témakör tiszta, akkor lássunk neki.

Nézzük meg, hogy néz ki egy saját függvény. Tegyük fel, szükségünk van egy függvényre, amely kiírja egy számtani sorozat első 15 elemét egymás mellé szóközzel elválasztva. Egy számtani sorozathoz szükség van a sorozat első elemére és a differenciára (az elemek közötti különbségre).

void szamtani( int a1, int d )
{
    int elem = a1;

    for( int i = 0; i < 15; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}

Lássuk akkor a kiemelt részek magyarázatát:

  • 1 – A függvényünk nem eredményt állít elő, hanem egy kiíratást hajt végre, kiírja a sorozat elemeit, ezért a függvény típusa void.
  • 3 – Létrehozok egy elem nevű változót, ami a sorozat aktuális elemét fogja tárolni, ennek kezdőértéke a kapott a1, a sorozat első eleme lesz.
  • 5 – Indítok egy ciklust, ami 15x végrehajt valamit.
  • 7 – Először kiírja az aktuális elemet (ami első esetben a kezdőérték lesz),
  • 8 – utána kiszámítja a következő értéket. Természetesen így a ciklus végén az elem már a 16. elemet tárolja, de azt már nem írja ki.
  • 10 – Egy sordobás a kiíratás után, hogy a következő esetleges kiíratás ne ennek a sornak a végén kezdődjön.

Ha mondjuk ehhez hasonló a feladat, de nem kiírni kell a sorozat első 15 elemét, hanem esetleg azok összegére van szükségünk, akkor egy kis módosítással ez is megoldható. Természetesen a számtani sorozatoknál ismert összegképlettel is megadható lenne az első n elem összege, de most tekintsünk el ettől, és lássuk be, így sem túl bonyolult.

void szamtani( int a1, int d )
{
    int elem = a1;
    int osszeg = 0;

    for( int i = 0; i << 15; i++ )
    {
        osszeg += elem;
        elem += d;
    }
    return osszeg;
}
  • 4 – Az összegzéshez szükség van egy nullázott összeg változóra.
  • 8 – Az elemeket pedig összeadjuk az összeg változóba.

Lássunk akkor mást. Írjunk függvényt, mely a kapott 3 egész számról eldönti, hogy szerkeszthető-e belőlük háromszög.

bool haromszog( int a, int b, int c )
{
    if( a + b > c && a + c > b && b + c > a )
    {
        return true;
    }
    else
    {
        return false;
    }
}
  • 1 – A függvény egy logikai eredményt ad, mert vagy szerkeszthető, vagy nem.
  • 5 – Ha bármely két oldal összege nagyobb, mint a harmadik oldal, akkor igen,
  • 9 – egyébként nem szerkeszthető.

Ez a feladat is megoldható rövidebben, legalábbis a feltételvizsgálatos része. Gondoljunk arra, hogy a return true sor csak akkor hajtható végre, ha az if()-ben megadott feltétel igaz. Vagyis ha a feltétel igaz, akkor az már önmagában lehetne visszatérési érték. Mi változik akkor ha ezt észrevesszük?

bool haromszog( int a, int b, int c )
{
    return (a + b > c && a + c > b && b + c > a);
}

Azért jóval rövidebb lett, nem? Az összetett kifejezés körüli zárójel is elhagyható lenne, mert a tényleges visszatérés (return) előtt kiértékelődik a kifejezés, és annak az egyetlen eredménye kerül vissza a return utasítással a függvényt hívó részhez a vezérlés.

A függvények túlterhelhetőek. Ez azt jelenti, hogy azonos nevű, de különböző szignatúrájú függvények is készülhetnek, ha a szükség úgy kívánja. Bővítsük ki az egyik előző feladatot. Ha emlékszel, készítettünk egy függvényt, amely egy sorozatnak kiírja az első 15 elemét. Készítsünk olyan függvényt is, amely egy sorozat tetszőleges, általunk megadott darabnyi elemét írja ki. Ezt úgy oldjuk meg, hogy a már megírt 15 elemet kiíró függvényt ne bántsuk, készítsük el külön függvényben az általános megoldást azonos néven, és csak a szignatúrában legyen különbség.

void szamtani( int a1, int d )
{
    int elem = a1;

    for( int i = 0; i < 15; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}

void szamtani( int a1, int d, int n )
{
    int elem = a1;

    for( int i = 0; i < n; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}

A két változtatás az azonos nevű függvényben a következő:

  • 13 – A függvény kapott paraméterei között utolsóként megjelent egy n is, ami az elemszámot jelenti, hogy hány elemét szeretnéd kiírni a sorozatnak.
  • 17 – Ez az n pedig megjelenik a ciklusban is, mert ennyi darab elemet szeretnénk kiírni, tehát ez lesz a határ a ciklus futásának darabszámában.

Van egy nagyon fontos elv a programozáskor, amelyet jó, ha az ember megtanul, mert ezzel nagyon-nagyon sok gondtól kímélheti meg magát. Ez az elv elsődlegesen nem középiskolai tanulmányaink során kap szerepet, hanem a majdani esetleges programozói munkánkban. Ott is főleg akkor, amikor nem egyedül, hanem csapatban kell dolgozni, az elv a következő:

Logikát nem duplikálunk!

Vagyis, az azonos logikát használó programrészek minden további nélkül hivatkozhatnak egymásra, sőt, ez elvárt követelmény!

Ha jobban megnézed, a fenti két függvény a túlterhelés miatt nagyon hasonló kódrészleteket tartalmaz. Amire nekünk függvény szinten szükségünk van:

  • egy függvény, mely egy számtani sorozat első 15 elemét írja ki egymás mellé, az első elem és differencia ismeretében
  • egy függvény, mely egy számtani sorozat első n elemét írja ki egymás mellé, az első elem és differencia ismeretében

Még a megfogalmazásuk is egyetlen helyen tér el. A másik különbség, hogy a második függvény vár egy 3. paramétert is, ahol a sorozat kiíratni szánt elemszámát adjuk meg. Akkor hogy lehet kiszedni a logika duplázását?

void szamtani( int a1, int d )
{
    szamtani( a1, d, 15 );
}

void szamtani( int a1, int d, int n )
{
    int elem = a1;

    for( int i = 0; i < n; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}

A két függvény megoldása közül a második az általános megoldás, az első specifikus, ami konkrét darabszám kiírására vonatkozik. Itt csak a sorozat első elemét és differenciáját adom meg, a kívánt fix darabszámot kézzel adom meg. Továbbpasszolom a feladatot az általános megoldásnak úgy, hogy a végére odaírom a kiíratni szánt elemek számát. A feladat logikai megoldása így csak egyszer szerepel. Nem is olyan nehéz igaz? Egyszerűen meghívjuk az első függvényből a másodikat a szükséges fix értékkel. A két kiemelt sorban ezt láthatod.

Nyilván nem az volt a célom, hogy 7 sorral rövidebb legyen a kódom. A lényege az, hogy az egyetlen helyen szereplő logikában könnyebb hibát javítani, vagy akár változtatni is. Mi van ha az elemeket nem szóközzel elválasztva egy sorba, hanem egymás alá külön sorokba kellene írni? Akkor egy helyen kell módosítani. Nyilván ha olyat szeretnénk megváltoztatni, ami csak az egyik fajta megoldásra jellemző, akkor a két megoldást külön kell szedni. Valljuk be, ez azért nem túl életszerű.

Kipróbáltad? Nem működik… Többek között azzal van gondja, hogy a szamtani() nevű függvénynek túl sok paramétere van. 3-at kap, de csak kettőt kérne. Hogy mi van? Világosan ott van a második függvényben, hogy 3 paramétert vár.

A probléma a két függvény sorrendjével van. Ha két függvény közül az egyik használna egy másikat, akkor ezt csakis abban az esetben teheti meg, ha tud a másikról. Ez technikailag azt jelenti, hogy használat előtt meg kell ismernie az adott függvény szignatúráját.

  1. mi rögzítettük a használni kívánt függvény szignatúráját még a használat előtt
  2. a használni kívánt függvény előtte van a forráskódban, tehát “látja”

Formailag a két eset a következő:

Előre megadott szignatúra:

#include <iostream>

using namespace std;

void szamtani( int, int, int );

void szamtani( int a1, int d )
{
    szamtani(a1,d,15);
}

void szamtani( int a1, int d, int n )
{
    int elem = a1;

    for( int i = 0; i < n; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}


int main()
{
    szamtani( 2,6 );

    return 0;
}
  • 5 – A kód elején megadjuk annak a függvénynek a szignatúráját, amelyiket egy másik függvény úgy szeretne használni, hogy az nem előtte, hanem mögötte található, tehát számára a szignatúra nélkül ismeretlen lenne.
  • 9 – Itt pedig ténylegesen használjuk a mögötte lévő, de ismert szignatúrájú függvényt.

Két függvény megfelelő sorrendje:

#include <iostream>

using namespace std;

void szamtani( int a1, int d, int n )
{
    int elem = a1;

    for( int i = 0; i < n; i++ )
    {
        cout << elem << " ";
        elem += d;
    }
    cout << endl;
}

void szamtani( int a1, int d )
{
    szamtani(a1,d,15);
}


int main()
{
    szamtani( 2,6 );

    return 0;
}

Itt a hátul lévő függvény számára nem ismeretlen az előtte lévő, így előre megadott szignatúra nélkül használhatja. A szignatúrát egyébként a programon belül bárhol megadhatjuk. Amire oda kell figyelni:

  • Mindenképp valamilyen függvényen kívüli terület legyen
  • Mindenképp azelőtt adjuk meg a szignatúrát, hogy szükség lenne arra a függvényre.

A függvényeket nem csak megírni, használni is kell, erre a fent már említett függvények leckében láthatsz példákat, de azért álljon itt is egy összetett feladat.

Komplex feladat:

Természetesen a programunk bármilyen részét kiszervezhetjük, például az átláthatóság miatt. Kétségtelen, hogy az egyszer használatos kódokat nem feltétlenül logikus minden áron függvényekben elhelyezni. Ennek ellenére most gyakorlásképp egy tömbhöz kapcsolódó alap algoritmusokkal megoldható feladatsort valósítsunk meg függvényekként.

  1. Adott egy 10 elemű tömb, töltsük fel értékekkel az [5;10] intervallumból.
  2. Írjuk ki az elemeit egymás mellé szóközzel elválasztva.
  3. Írjuk ki a tömb elemeinek összegét
  4. Írjuk ki a tömbben lévő páratlan számok darabszámát
  5. Írjuk ki a tömb legnagyobb elemét
  6. Rendezzük, majd írjuk ki a tömb elemeit növekvő sorrendben

Készítsünk függvényeket, melyek kiírja a tömb elemeit egymás mellé szóközzel elválasztva, kiszámítja az adott tömb elemeinek összegét, megadja a benne lévő páratlan számok darabszámát, és rendezi az adott tömböt növekvő sorrendbe. Lássuk akkor a komplett megoldást

#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;


void feltolt( int szamok[], int meret )
{
    srand(time(0));

    for( int i = 0; i < meret; i++ )
    {
        szamok[i] = rand() % 6 + 5;
    }
}

void kiir( int szamok[], int meret )
{
    for( int i = 0; i < meret; i++ )
    {
        cout << szamok[i] << " ";
    }
    cout << endl;
}


int osszeg(int szamok[], int meret )
{
    int ossz = 0;
    for( int i = 0; i < meret; i++ )
    {
        ossz = ossz + szamok[i];
    }
    return ossz;
}

int ptdb(int tomb[], int meret )
{
    int db = 0;
    int ossz = 0;
    for( int i = 0; i < meret; i++ )
    {
        if( tomb[i] % 2 != 0 )
        {
            db++;
        }
    }
    return db;
}

int tmax( int tomb[], int meret )
{
    int max = 0;
    for( int i = 0; i < meret; i++ )
    {
        if( tomb[i] > tomb[max] )
        {
            max = i;
        }
    }
    return max;
}

void tombrendez(int tomb[], int meret)
{
    int csere;
    for( int i = 0; i < meret-1; i++ )
    {
        for( int j = i+1; j < meret; j++ )
        {
            if( tomb[i] > tomb[j] )
            {
                csere = tomb[i];
                tomb[i] = tomb[j];
                tomb[j] = csere;
            }
        }
    }
}


int main()
{
    int meret = 10;
    int tomb[meret];

// 1. feladat
    feltolt(tomb,meret);

// 2. feladat
    kiir(tomb,meret);


// 3. feladat
    int osszeg = osszeg( tomb, meret );
    cout << "A tombben levo szamok osszege: " << osszeg << endl;

// 4. feladat
    cout << "A tombben levo paratlan szamok darabszama: " <<
            ptdb(tomb,meret) << endl;

// 5. feladat
    int max = tmax( tomb, meret );
    cout << "A tomb legnagyobb eleme: " <<
            tomb[ max ] << endl;

// 6. feladat
    tombrendez(tomb,meret);
    kiir(tomb,meret);

    return 0;
}
  • 2-3 – szokásos dolgok a véletlenszám sorsoláshoz
  • 8-16 – a tömb feltöltése
  • 18-25 – a tömb elemeinek kiírása a megfelelő módon
  • 28-36 – a tömb elemeinek összeadása
  • 38-50 – a tömb páratlan elemeinek megszámlálása
  • 52-63 – a tömb legnagyobb elemének megkeresése (a helyét)
  • 65-80 – a tömb elemeinek növekvő rendezése
  • 89, 92, 96-97, 100-101, 104-106, 109-110 – a feladatok megoldásához szükséges megfelelő függvények meghívása, és azok eredményének esetleges felhasználása.

A paraméter átadás két típusa

A függvények paraméterei arra szolgálnak, hogy azok segítségével, vagy pontosan azokon hajtsunk végre valami műveletet. A kettő között fontos különbség van.

  1. A számtani sorozatos példa esetén a függvénynek meg kellett adni a kiírandó sorozat fő adatait, de nem konkrétan az eredetileg átadott számokat akartuk megváltoztatni, hanem azok segítségével hajtottunk végre egy feladatot. Ebben az esetben a függvény hívásakor átadott számoknak az értékét használtuk fel a feladat megoldásához. Ha az átadott paraméterekkel a függvény bármilyen változtatást végrehajt, az a hívás helyén lévő értékeket nem változtatja meg. Ezt nevezzük érték szerinti paraméter átadásnak.
  2. A komplex feladatban a tömb feltöltésekor vagy rendezésekor az eredeti main() függvényben lévő tömböt akartuk megváltoztatni. Láthatod, hogy az 1-es feladat esetén a feltöltő függvénynek odaadjuk a main()-ben létrehozott tömböt, amit fel szeretnénk tölteni. Itt azonban az történik, hogy az átadott tömböt a függvény úgy módosítja, hogy az az eredeti helyén is megváltozik, nem csak a feltölt függvényben. Ezt nevezzük cím szerinti paraméter átadásnak. A tömb, mint összetett adattípus a címével kerül átadásra bármilyen függvénynek, ami ha megváltoztatja a kapott tömböt, az valójában az eredeti tömböt változtatja meg.

Megjegyzések:

  • Ha valamilyen változót szeretnék átadni egy függvénynek, akkor az átadás-átvétel folyamatában csak annyi a követelmény, hogy az átadott változó típusa meg kell egyezzen az átvett változó típusával. Erre láthatsz példát a például az osszeg függvény hívásakor. A tomb-ot adom át, és szamok néven veszem át az egészeket tartalmazó tömböt. Az esetleges név egyezés (lásd tmax függvény) semmilyen hatással nincs arra, hogy van-e a függvénynek visszahatása a hívott oldalon lévő változóra. Ez attól függ, hogy egyszerű, vagy összetett adattípus a paraméter.
  • A void (visszatérési érték nélküli) függvényekben a return elhagyható, de szükség szerint használhatod. A függvényben a return utasítás megszakítja a függvény futását, és a vezérlés az őt hívó kódrészhez kerül.
  • Az esetleges saját függvényeket célszerű a main () elé írni, hogy a használatuk miatt ne kelljen feltétlenül mindegyik szignatúráját kiírni a kód elejére. Ha a függvények egymást is használják, akkor a szignatúra indokolt lehet, de ez is kikerülhető, ha előbb írod meg azokat a függvényeket, amelyeket a többi használni szeretne.

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez az oldal az Akismet szolgáltatást használja a spam csökkentésére. Ismerje meg a hozzászólás adatainak feldolgozását .