• Nem Talált Eredményt

Függvénynevek átdefiniálása (túlterhelése)

II. fejezet - Moduláris programozás C++ nyelven

2. A függvényekről magasabb szinten

2.2. Függvénynevek átdefiniálása (túlterhelése)

else

also = kozepso + 1;

} // while return eredmeny;

}

int main() {

int v[8] = {2, 7, 10, 11, 12, 23, 29, 30};

cout << BinarisKereses(v, 8, 23)<< endl; // 5 cout << BinarisKereses(v, 8, 4)<< endl; // -1 }

Néhány további megjegyzés az beágyazott függvények használatával kapcsolatban:

• Az inline megoldás általában kisméretű, nem bonyolult függvények esetén használható hatékonyan.

• Az inline függvényeket fejállományba kell helyeznünk, amennyiben több fordítási egységben is hívjuk őket, azonban egyetlen forrásfájlba csak egyszer építhetők be.

• A beágyazott függvények ugyan gyorsabb-, azonban legtöbbször nagyobb lefordított kódot eredményeznek, hosszabb fordítási folyamat során.

• Az inline előírás csupán egy javaslat a fordítóprogram számára, amelyet az bizonyos feltételek esetén (de nem mindig!) figyelembe is vesz. A legtöbb fordító a ciklust vagy rekurzív függvényhívást tartalmazó függvények esetén figyelmen kívül hagyja az inline előírást.

2.2. Függvénynevek átdefiniálása (túlterhelése)

Első látásra úgy tűnhet, hogy a függvények széles körben való alkalmazásának útjában áll a paraméterek szigorú típusellenőrzése. Értékparaméterek esetén ez csak a mutatók, tömbök és a felhasználói típusok esetén akadály.

Az alábbi abszolút értéket meghatározó függvény tetszőleges numerikus argumentummal hívható, azonban az eredményt mindig int típusúként adja vissza:

inline int Abszolut(int x) { return x < 0 ? -x : x;

}

Néhány függvényhívás és visszaadott érték:

Abszolut(-7) 7

Abszolut(-1.2) 1

Abszolut(2.3F) 2

Abszolut('I') 73

Abszolut(L'\x807F') 32895

A fordító a 2. és 3. esetben figyelmeztet ugyan az adatvesztésre, azonban a futtatható program elkészíthető.

Nem konstans, referencia paraméterek esetén azonban sokkal erősebbek a megkötések, hiszen az Ertekadas() függvény hívásakor az első két paramétert int változóként kell megadnunk:

void Ertekadas(int & a, int & b, int c) { a = b + c;

b = c;

}

Jogos az igény, hogy egy függvényben megvalósított algoritmust különböző típusú paraméterekkel tudjuk futatni, méghozzá ugyanazt a függvénynevet használva. A névazonossághoz azért ragaszkodunk, mert általában a függvénynév utal a függvényben megvalósított tevékenységre, algoritmusra. Ezt az igényünket a C++ nyelv a függvénynevek túlterhelésével (overloading) támogatja.

A függvénynevek túlterhelése azt jelenti, hogy azonos hatókörben (ennek pontos jelentését később tisztázzuk) azonos névvel, de eltérő paraméterlistával (paraméterszignatúrával) különböző függvényeket definiálhatunk. (A paraméterszignatúra alatt a paraméterek számát és és a peramétersor elemeinek típusait.)

Az elmondottakat figyelembe véve állítsuk elő az Abszolut() függvényünk két változatát!

#include <iostream>

using namespace std;

inline int Abszolut(int x) { return x < 0 ? -x : x;

}

inline double Abszolut(double x) { return x < 0 ? -x : x;

}

int main() {

cout << Abszolut(-7) << endl; // int cout << Abszolut(-1.2) << endl; // double cout << Abszolut(2.3F) << endl; // double cout << Abszolut('I') << endl; // int cout << Abszolut(L'\x807F') << endl; // int }

Fordításkor nem jelentkezik az adatvesztésre utaló üzenet, tehát a lebegőpontos értékekkel a double típusú függvény aktivizálódik.

Ebben az esetben a függvény neve önmagában nem határozza meg egyértelműen, hogy melyik függvény fut. A fordítóprogram a hívási argumentumok szignatúráját sorban egyezteti az összes azonos nevű függvény paramétereinek szignatúrájával. Az egyeztetés eredményeként az alábbi esetek fordulhatnak elő:

• Pontosan egy illeszkedőt talál, teljes típusegyezéssel – ekkor az ennek megfelelő függvény hívását fordítja be.

• Pontosan egy illeszkedőt talál, automatikus konverziók utáni típusegyezéssel - ilyenkor is egyértelmű a megfelelő függvény kiválasztása.

• Egyetlen illeszkedőt sem talál - hibajelzést ad.

• Több egyformán illeszkedőt talál - hibajelzést ad.

Felhívjuk a figyelmet arra, hogy a megfelelő függvényváltozat kiválasztásában a függvény visszatérési értékének típusa nem játszik szerepet.

Amennyiben szükséges, például több egyformán illeszkedő függvény esetén, típuskonverziós előírással magunk is segíthetjük a megfelelő függvény kiválasztását:

int main() {

cout << Abszolut((double)-7) << endl; // double cout << Abszolut(-1.2) << endl; // double cout << Abszolut((int)2.3F) << endl; // int cout << Abszolut((float)'I') << endl; // double cout << Abszolut(L'\x807F') << endl; // int

}

unsigned int VektorOsszeg(unsigned int a[], int n) { int osszeg=0;

double VektorOsszeg(double a[], int n) { double osszeg=0;

unsigned int vu[]={1,1,2,3,5,8,13};

const int nu=sizeof(vu) / sizeof(vu[0]);

cout << "\nAz unsigned tomb elemosszege: "

<<VektorOsszeg(vu, nu);

double vd[]={1.2,2.3,3.4,4.5,5.6};

const int nd=sizeof(vd) / sizeof(vd[0]);

cout << "\nA double tomb elemosszege: "

<< VektorOsszeg(vd, nd);

}

A fordítóprogram az első híváskor a VektorOsszeg(unsigned int*, int), míg a második esetében VektorOsszeg(double*, int) szignatúrát találja illeszkedőnek. Más típusú (például int és float) tömbök esetén fordítási hibát kapunk, hiszen C++-ban erősen korlátozott a mutatók automatikus konverziója.

Felhívjuk a figyelmet arra, hogy a függvényekhez hasonló módon használható a műveletek átdefiniálásának mechanizmusa (operator overloading). Az operátorokat azonban csak felhasználó által definiált típusokkal (struct, class) lehet túlterhelni, ezért ezzel a lehetőséggel könyvünk következő részében foglalkozunk részletesen.

A fenti példákból látható, hogy további típusok bevezetése esetén szövegszerkesztési feladattá válik az újabb, ugyanazt az algoritmust megvalósító, függvényváltozatok elkészítése (blokkmásolás, szövegcsere). Ennek elkerülése érdekében az átdefiniált függvényváltozatok előállítását a C++ fordítóprogramra is rábízhatjuk, a függvénysablonok (functiontemplates) bevezetésével.

2.3. Függvénysablonok

Az előző részekben megismerkedtünk a függvényekkel, amelyek segítenek abban, hogy biztonságosabb, jól karbantartható programokat készítsünk. Bár a függvények igen hatékony és rugalmas programépítést tesznek lehetővé, bizonyos esetekben nehézségekbe ütközünk, hiszen minden paraméterhez típust kell rendelnünk. Az előző alfejezetben bemutatott túlterheléses megoldás segít a típusoktól bizonyos mértékig függetleníteni a függvényeinek. Ott azonban csak véges számú típusban gondolkodhatunk, és fejlesztői szempontból igencsak nagy problémát jelent a megismételt forráskód javítása, naprakésszé tétele.

Jogosan merül fel az igény, hogy csak egyszer kelljen elkészíteni a függvényünket, azonban akárhányszor felhasználhassuk, tetszőleges típusú argumentumokkal. A legtöbb, napjainkban használatos programozási nyelv a generikus programozás eszközeivel ad megoldást a problémánkra.

A generikus (generic) programozás egy általános programozási modell. Maga a technika olyan programkód fejlesztését jelenti, amely nem függ a program típusaitól. A forráskódban úgynevezett általánosított vagy

paraméterezett típusokat használunk. Ez az elv nagyban növeli az újrafelhasználás mértékét, hiszen típusoktól független tárolókat és algoritmusokat készíthetünk segítségével. A C++ nyelv a sablonok (templates) bevezetésével, fordítási időben valósítja meg a generikus programozást. Léteznek azonban olyan nyelvek és rendszerek is, amelyeknél ez futási időben történik (ilyenek például a Java és a С#).

2.3.1. Függvénysablonok készítése és használata

A sablonok deklarációja a template kulcsszóval indul, mely után a < és > jelek között adjuk meg a sablon paramétereit. Ezek a paraméterek elsősorban általánosított típusnevek azonban változók is szerepelhetnek közöttük. Az általánosított típusnevek előtt a class vagy a typename kulcsszavakat kell használnunk.

A függvénysablon előállításakor egy működő függvényből érdemes kiindulni, amelyben a lecserélendő típusokat általános típusokkal (TIPUS) helyettesítjük. Ezek után közölnünk kell a fordítóval, hogy mely típusokat kell majd lecserélni a függvénymintában (template<class TIPUS>). Tekintsük például az első a lépést az Abszolut() függvényünk esetén

inline TIPUS Abszolut(TIPUS x) { return x < 0 ? -x : x;

}

majd pedig a végleges forma kétféleképpen megadva:

template <class TIPUS>

inline TIPUS Abszolut(TIPUS x) { return x < 0 ? -x : x;

}

template <typename TIPUS>

inline TIPUS Abszolut(TIPUS x) { return x < 0 ? -x : x;

}

Az elkészült függvénysablonból a fordító állítja elő a szükséges függvényváltozatot, amikor először találkozik egy hívással. Felhívjuk a figyelmet arra, hogy az Abszolut() sablon csak numerikus típusokkal használható, amelyek körében értelmezettek az kisebb és az előjelváltás műveletek.

A szokásos függvényhívás esetén a fordítóprogram az argumentum típusából dönti el, hogy az Abszolut() függvény mely változatát állítja elő, és fordítja le.

cout << Abszolut(123)<< endl; // TIPUS = int cout << Abszolut(123.45)<< endl; // TIPUS = double

Típus-átalakítás segítségével az automatikus típusválasztás eredményéről magunk is gondoskodhatunk:

cout << Abszolut((float)123.45)<< endl; // TIPUS = float cout << Abszolut((int)123.45)<< endl; // TIPUS = int

Ugyanerre az eredményre jutunk, ha a függvény neve után, a < és > jelek között megadjuk a típusnevet:

cout << Abszolut<float>(123.45)<< endl; // TIPUS = float cout << Abszolut<int>(123.45)<< endl; // TIPUS = int

A függvénysablonban természetesen több, azonos típus helyettesítése is megoldható, mint ahogy azt a következő példában láthatjuk:

template <typename T>

inline T Maximum(T a, T b) { return (a>b ? a : b);

}

int main() {

int a = 12, b=23;

float c = 7.29, d = 10.2;

cout<<Maximum(a, b); // Maximum(int, int) cout<<Maximum(c, d); // Maximum(float, float)

↯ cout<<Maximum(a, c); // nem található Maximum(int, float) cout<<Maximum<int>(a,c);

// a Maximum(int, int) függvény hívódik meg konverzióval }

A különböző típusok alkalmazásához több általánosított típust kell a sablonfejben megadnunk:

template <typename T1, typename T2>

inline T1 Maximum(T1 a, T2 b) { return (a>b ? a : b);

}

int main() {

cout<<Maximum(5,4); // Maximum(int, int) cout<<Maximum(5.6,4); // Maximum(double, int) cout<<Maximum('A',66L); // Maximum(char, long) cout<<Maximum<float, double>(5.6,4);

// Maximum(float, double) cout<<Maximum<int, char>('A',66L);

// Maximum(int, char) }

2.3.2. A függvénysablon példányosítása

Amikor a C++ fordítóprogram először találkozik egy függvénysablon hívásával, az adott típussal/típusokkal elkészíti a függvény egy példányát. Minden függvénypéldány az adott függvénysablon, adott típusokkal specializált változata. A lefordított programkódban minden felhasznált típushoz pontosan egy függvénypéldány tartozik. A fenti hívásokban erre az ún. implicit példányosításra láttunk példát.

A függvénysablont explicit módon is példányosíthatjuk, amennyiben a függvény fejlécét magában foglaló template sorban konkrét típusokat adunk meg:

template inline int Abszolut<int>(int);

template inline float Abszolut(float);

template inline double Maximum(double, long);

template inline char Maximum<char, float>(char, float);

Fontos megjegyeznünk, hogy adott típus(ok)ra vonatkozó explicit példányosítás csak egyszer szerepelhet a programkódban.

Mivel a C++ fordító a függvénysablonokat fordítási időben dolgozza fel, a függvényváltozatok elkészítéséhez a sablonoknak forrásnyelven kell rendelkezésre állniuk. Ajánlott a függvénysablonokat a fejállományban elhelyezni, biztosítva azt, hogy a fejállományt csak egyszer lehessen beépíteni a C++ modulokba.

2.3.3. A függvénysablon specializálása

Vannak esetek, amikor az elkészített általános függvénysablonban definiált algoritmustól eltérő működésre van szükségünk bizonyos típusok vagy típuscsoportok esetén. Ebben az esetben magát a függvénysablont kell túlterhelnünk (specializálnunk) – explicit specializáció.

Az alábbi példában a Csere() függvénysablon a hivatkozással átadott argumentumok értékét felcseréli. Az első specializált változat a mutató argumentumok esetén nem a mutatókat, hanem a hivatkozott értékeket cseréli fel.

A másik, tovább specializált változat kimondottan C-stílusú sztringek cseréjére használható.

#include <iostream>

#include <cstring>

using namespace std;

// Az alap sablon

template< typename T > void Csere(T& a, T& b) {

// A mutatókra specializált sablon

template<typename T> void Csere(T* a, T* b) { T c(*a); template<> void Csere(char *a, char *b) { char puffer[123];

Fordító a különböző specializált változatok közül mindig a leginkább specializált (konkrét) sablont alkalmazza először a függvényhívás fordításához.

2.3.4. Néhány további függvénysablon példa

A függvénysablonok összefoglalásaként nézzünk meg néhány összetettebb példát! Kezdjük a sort a 2.2. szakasz - Függvénynevek átdefiniálása (túlterhelése)VektorOsszeg() függvényének általánosításával! A sablon segítségével tetszőleges típusú, egydimenziós, numerikus tömb elemösszegét meg tudjuk határozni:

#include <iostream>

using namespace std;

template<class TIPUS>

TIPUS VektorOsszeg(TIPUS a[], int n) { TIPUS sum = 0;

const int ni=sizeof(ai) / sizeof(ai[0]);

cout << "\nAz int tomb elemosszege: "

<<VektorOsszeg(ai,ni);

double ad[]={1.2,2.3,3.4,4.5,5.6};

const int nd=sizeof(ad) / sizeof(ad[0]);

cout << "\nA double tomb elemosszege: "

<<VektorOsszeg(ad,nd);

float af[]={3, 2, 4, 5};

const int nf=sizeof(af) / sizeof(af[0]);

cout << "\nA float tomb elemosszege: "

<<VektorOsszeg(af,nf);

long al[]={1223, 19800729, 2004102};

const int nl=sizeof(al) / sizeof(al[0]);

cout << "\nA long tomb elemosszege: "

<<VektorOsszeg(al,nl);

}

A Rendez() függvénysablon egy Tip típusú és Meret méretű egydimenziós tömb elemeit rendezi növekvő sorrendbe. A függvény a tömbön belül mozgatja az elemeket, és az eredmény is ugyanabban a tömbben áll elő.

Mivel ebben az esetben a típus paraméter (Tip) mellett egy egész értéket fogadó paraméter (Meret) is helyet kapott a deklarációban, a hívás bővített formáját kell használnunk:

Rendez<int, meret>(tomb);

A példaprogramban a Rendez() sablonból a Csere() sablont is hívjuk:

#include <iostream>

using namespace std;

template< typename T > void Csere(T& a, T& b) { T c(a);

a = b;

b = c;

}

template<class Tip, int Meret> void Rendez(Tip vektor[]) { int j;

Rendez<int, meret>(tomb);

for (int i = 0; i < meret; i++) cout << tomb[i] << " ";

cout << endl;

}

Az utolsó példánkban a Fuggveny() különböző változatainak hívásakor megjelenítjük a fordító által használt függvényváltozatokat. A specializált változatokból az általános sablont aktivizáljuk, amely a Kiir() sablon segítségével megjelenti a teljes hívás elemeit. A típusok nevét a typeid operátor által visszaadott objektum name() tagja adja vissza. A Karakter() függvénysablonnal a megjeleníthető karaktereket aposztrófok között másoljuk a kiírt szövegbe. Ez a kis példa betekintést enged a függvénysablonok használatának

template <typename T> T Karakter(T a) { return a;

}

string Karakter(char c) {

stringstream ss;

template <typename TA, typename TB>

void Kiir(TA a, TB b){

cout << "Fuggveny<" << typeid(TA).name() << ","

<< typeid(TB).name();

cout << ">(" << Karakter(a) << ", " << Karakter(b) << ")\n";

}

template <typename TA, typename TB>

void Fuggveny(TA a = 0, TB b = 0){

A C++ nyelvű program gyakran áttekinthetetlen. Ahhoz, hogy egy nagyobb program átlátható maradjon, több olyan szabályt is be kell tartanunk, amit ugyan a C++ nyelv szintaktikája nem követel meg, azonban a használhatóság igen. Az alkalmazott szabályok többsége a moduláris programozás területéről származik.

Az átláthatóság megtartásának egyik hatékony módja, ha a programunkat működési szempontból részekre bontjuk, és az egyes részeket különálló forrásfájlokban, modulokban (.cpp) valósítjuk meg (implementáljuk). A modulok közötti kapcsolatot interfész (deklarációs) állományok (.h) biztosítják. Minden modul esetén alkalmazzuk az adatrejtés elvét, mely szerint a modulokban definiált változók és függvények közül csak azokat osztjuk meg más modulokkal, amelyekre feltétlenül szükség van, míg a többi definíciót elrejtjük a külvilág elől.

A könyvünk második fejezetének célja a jól strukturált, moduláris programozást segítő C++ eszközök bemutatása. Egyik legfontosabb elemmel, a függvényekkel már megismerkedtünk, azonban még további megoldásokra is szükségünk van: