1/29. Standard Template Library. Template ek

Hasonló dokumentumok
C++ Standard Template Library (STL)

Algoritmusok és adatszerkezetek gyakorlat 06 Adatszerkezetek

Fejlett programozási nyelvek C++ Iterátorok

XIII. STL. Tároló Bejáró Algoritmus. XIII.1 A vector #include <vector> #include <vector> #include <algorithm> using namespace std;

Pénzügyi algoritmusok

1. Alapok. Programozás II

STL. Algoritmus. Iterátor. Tároló. Elsődleges komponensek: Tárolók Algoritmusok Bejárók

Programozás C++ -ban

Programozási technológia

Programozás C++ -ban 2007/4

Algoritmusok és adatszerkezetek gyakorlat 07

Programozás alapjai C nyelv 8. gyakorlat. Mutatók és címek (ism.) Indirekció (ism)

Programozás II. 4. Dr. Iványi Péter

1. Template (sablon) 1.1. Függvénysablon Függvénysablon példányosítás Osztálysablon

Mutatók és címek (ism.) Programozás alapjai C nyelv 8. gyakorlat. Indirekció (ism) Néhány dolog érthetőbb (ism.) Változók a memóriában

Programozás II. 2. Dr. Iványi Péter

A programozás alapjai előadás. [<struktúra változó azonosítók>] ; Dinamikus adatszerkezetek:

Tartalom Keresés és rendezés. Vektoralgoritmusok. 1. fejezet. Keresés adatvektorban. A programozás alapjai I.

1. Mi a fejállományok szerepe C és C++ nyelvben és hogyan használjuk őket? 2. Milyen alapvető változókat használhatunk a C és C++ nyelvben?

Globális operátor overloading

Keresés és rendezés. A programozás alapjai I. Hálózati Rendszerek és Szolgáltatások Tanszék Farkas Balázs, Fiala Péter, Vitéz András, Zsóka Zoltán

Programozás alapjai II. (7. ea) C++ Speciális adatszerkezetek. Tömbök. Kiegészítő anyag: speciális adatszerkezetek

Egyirányban láncolt lista

Programozás II gyakorlat. 8. Operátor túlterhelés

Speciális adatszerkezetek. Programozás alapjai II. (8. ea) C++ Tömbök. Tömbök/2. N dimenziós tömb. Nagyméretű ritka tömbök

Bevezetés a Programozásba II 12. előadás. Adatszerkezetek alkalmazása (Standard Template Library)

.Net adatstruktúrák. Készítette: Major Péter

Programozás alapjai II. (7. ea) C++

Adatszerkezetek Adatszerkezet fogalma. Az értékhalmaz struktúrája

Elemi adatszerkezetek

117. AA Megoldó Alfréd AA 117.

A lista eleme. mutató rész. adat rész. Listaelem létrehozása. Node Deklarálás. Létrehozás. Az elemet nekünk kell bef zni a listába

Generikus osztályok, gyűjtemények és algoritmusok

OOP: Java 11.Gy: Enumok, beágyazott osztályok. 13/1 B ITv: MAN

Generikus Típusok, Kollekciók

A C++ Standard Template Library rövid összefoglalás

500. AA Megoldó Alfréd AA 500.

Dinamikus csatolású függvénykönyvtár készítése és használata Plugin-szerű betöltés Egyszeű C++ osztályok készítése

500. CC Megoldó Alfréd CC 500.

STL gyakorlat C++ Izsó Tamás május 9. Izsó Tamás STL gyakorlat/ 1

C++ programozási nyelv Konstruktorok-destruktorok

Programozás I gyakorlat

Adatszerkezetek 2. Dr. Iványi Péter

Bevezetés a programozásba 2

Programozás II. 2. gyakorlat Áttérés C-ről C++-ra

PROGRAMOZÁSI NYELVEK - CPP. GYAKORLAT JEGYZET

C++ programozási nyelv

Miről lesz ma szó? A PROGAMOZÁS ALAPJAI 1. Dinamikus adatszerkezetek. Dinamikus adatszerkezetek. Önhivatkozó struktúrák. Önhivatkozó struktúrák

3. Osztályok II. Programozás II

Programozás. C++ típusok, operátorok. Fodor Attila

Algoritmuselmélet. 2-3 fák. Katona Gyula Y. Számítástudományi és Információelméleti Tanszék Budapesti Műszaki és Gazdaságtudományi Egyetem. 8.

Programozás I. 3. gyakorlat. Szegedi Tudományegyetem Természettudományi és Informatikai Kar

A feladat lényege egy felhasználói típusnak a zsák típusnak a megvalósítása.

Pénzügyi algoritmusok

C# nyelv alapjai. Krizsán Zoltán 1. Objektumorientált programozás C# alapokon tananyag. Általános Informatikai Tanszék Miskolci Egyetem

Programozás C és C++ -ban

C programozási nyelv Pointerek, tömbök, pointer aritmetika

B-fa. Felépítés, alapvető műveletek. Programozás II. előadás. Szénási Sándor.

Láncolt listák. Egyszerű, rendezett és speciális láncolt listák. Programozás II. előadás. Szénási Sándor

Bevezetés a programozásba I.

Programozás alapjai. 10. előadás

Programozás C++ -ban 2007/7

Java II. I A Java programozási nyelv alapelemei

Bevezetés, a C++ osztályok. Pere László

Szövegek C++ -ban, a string osztály

Programozási nyelvek Java

Bevezetés a Programozásba II 11. előadás. Adatszerkezetek megvalósítása. Adatszerkezetek megvalósítása Adatszerkezetek

PROGRAMOZÁSI NYELVEK - CPP. GYAKORLAT JEGYZET

List<String> l1 = new ArrayList<String>(); List<Object> l2 = l1; // error

Felhasználó által definiált adattípus

Informatika terméktervezőknek

Bevezetés a programozásba Előadás: Tagfüggvények, osztály, objektum

Programozás II gyakorlat. 6. Polimorfizmus

Mutatók és mutató-aritmetika C-ben március 19.

Láncolt Listák. Adat1 Adat2 Adat3 ø. Adat1 Adat2 ø Adat3

Programozás alapjai. 5. előadás

és az instanceof operátor

Programozás II. 3. gyakorlat Objektum Orientáltság C++-ban

Algoritmizálás + kódolás C++ nyelven és Pascalban

Java VIII. Az interfacei. és az instanceof operátor. Az interfészről általában. Interfészek JAVA-ban. Krizsán Zoltán

OOP #14 (referencia-elv)

500. DD Megoldó Alfréd DD 500.

Programozási Nyelvek (C++) Összefoglaló

A számítástudomány alapjai. Katona Gyula Y. Számítástudományi és Információelméleti Tanszék Budapesti Műszaki és Gazdaságtudományi Egyetem

5. Gyakorlat. struct diak {

Programozási nyelvek Java

Pénzügyi algoritmusok

Gelle Kitti Algoritmusok és adatszerkezetek gyakorlat - 07 Hasítótáblák

Láncolt listák. PPT 2007/2008 tavasz.

1. Öröklés Rétegelés Nyilvános öröklés - isa reláció Korlátozó öröklődés - has-a reláció

félstatikus adatszerkezetek: verem, várakozási sor, hasítótábla dinamikus adatszerkezetek: lineáris lista, fa, hálózat

C# Nyelvi Elemei. Tóth Zsolt. Miskolci Egyetem. Tóth Zsolt (Miskolci Egyetem) C# Nyelvi Elemei / 18

Bevezetés a programozásba II. 5. Előadás: Másoló konstruktor, túlterhelés, operátorok

10. előadás Speciális többágú fák

Statikus adattagok. Statikus adattag inicializálása. Speciális adattagok és tagfüggvények. Általános Informatikai Tanszék

Challenge Accepted:C++ Standard Template Library

Programozás C++ -ban

A szemantikus elemzés helye. A szemantikus elemzés feladatai. A szemantikus elemzés feladatai. Deklarációk és láthatósági szabályok

Alprogramok, paraméterátadás

Dinamikus láncolt lista 4. GYAKORLAT

Átírás:

A cikksorozat célja, hogy felkeltse az érdeklődést az STL (Standard Template Library) iránt, ami egy egyszerűen használható template alapú algoritmus és adatszerkezet gyűjtemény. Az STL segítségével gyorsabb, áttekinthetőbb, és bugmentesebb programot lehet írni C++ nyelven. Ma már szinten minden C++ fordító támogatja a különféle STL verziókat. A cikksorozat célja, hogy felkeltse az érdeklődést az STL (Standard Template Library) iránt, ami egy egyszerűen használható template alapú algoritmus és adatszerkezet gyűjtemény. Az STL segítségével gyorsabb, áttekinthetőbb, és bug mentesebb programot lehet írni C++ nyelven. Ma már szinten minden C++ fordító támogatja a különféle STL verziókat. A sorozat elsősorban gyakorlati oldaláról közelít az STL hez, bemutatja a főbb tárolók használatát, és érint egy két lényeges algoritmust is, valamint a cikksorozat végén gyakorlati példákat ad a használatukra. A cikkben szereplő példákat a GNU C++ 2.95.3 5 (g++ 3 SGI STL) illetve a free Borland C++ 5.5 ös fordítóval (Rogue Wave STL) teszteltem. Standard Template Library A szoftverfejlesztés során általában nem új algoritmusokat és adatszerkezeteket kell kitalálni, hanem a már meglévők közül kell valamilyen szempontból optimálist kiválasztani. Többféle cél alapján optimalizálhatunk a tervezés során, például lehetséges memóriára, sebességre, fejlesztési időre, egyszerűségre, robosztusságra és így tovább. Sokszor, egyszerűbb esetekben azonban a lustaság (a jó programozó lusta : )), vagy az ismereteink szabják meg a kiválasztott adatszerkezetet, illetve algoritmust. Például egy buborékrendezést (bubble sort) bármelyikünk kapásból meg tudna írni, azonban, az általános esetekre optimálisabb, gyorsrendezést (quicksort) már nem mindegyikünk tudná egyből hibátlanul implementálni. Vagy könnyebben használunk egy fix méretű tömböt, semmint egy dinamikusan növekedő illetve zsugorodó tömböt, esetleg láncolt listát, hogy csak az egyszerűbb lehetőségeket említsem, és a fix méretű tömbbel esetleg buffer overrun típusú hibákat rakhatunk egy programban. A bonyolultabb adatszerkezetek és algoritmusok használatának elősegítésében a fejlesztőeszközök is nyújtanak valamilyen segítséget. Például már a K&R féle C ben is könyvtári függvény volt a qsort, bsearch illetve a lsearch, amelyek paraméterezése azonban a C nyelv szépségeihez és lehetőségeihez illően eléggé trükkös mutató konverziókat tesz szükségessé és ezzel kevésbé gyakorlott C programozók számára hibalehetőségeket teremt. Template ek A C++ nyelv e problémák megoldására a generikus programozás lehetőségét nyújtja, a templatek segítségével, amelyek leegyszerűsítve felfoghatók úgy is, mint típusokkal és értékekkel paraméterezett típusok és eljárások. Példaképpen az egyik legegyszerűbb templatet nézzük meg. Biztos mindnyájan találkoztunk már a min makróval, melyekkel két érték közül tudjuk a kisebbet kiválasztani. #define min(a, b) ((a)<(b)? (a) : (b)) Azonban e makró használata során nem kellő odafigyelés esetén, a szöveghelyettesítés miatt, esetleg könnyen írhatunk olyan kódot, ami nem azt teszi, mint aminek látszik. Például mi lesz az a, a b és a c változó értéke? int a=1, b=2, c=min(a++,--b); E mellékhatást ki tudjuk kerülni, ha függvényként írnánk meg, azonban így annyi függvényt kéne 1/29

írni, ahány típusra szeretnénk használni a függvényt. Ebben nyújthat segítséget a template ként implementált min függvény, aminek definíciója: template <class T> const T& min(const T& a, const T& b) return (a<b? a : b); Egy kis magyarázat: a paramétereket azért referenciaként veszi át, mivel így kimarad a copy konstruktor hívása, és ezért referencia a visszatérési típus is. A const pedig arra szolgál, hogy használni lehessen a függvény hívásának helyén csak konstansként elérhető változókra is copy konstruktor hívása nélkül. Ez a template egyedül azt követeli meg a típustól, hogy értelmezve legyen rá a kisebb operátor, azaz olyan típusokra tudjuk használni, ami vagy beépítve tartalmazza ezt, mint például az int, double stb., vagy létezik publikus kisebb operátora, amelyik ráadásul const típusú. Azaz például a következő struktúrára szintén lehet használni a min template függvényt: struct Dummy int a; int b; bool operator < (const Dummy& O) const return (a<o.a (a==o.a && b<o.b)); // operator < ; Vagy például két változó értékét kicserélő függvény template alapú implementációja: template <class T> void swap(t& a, T& b) T c = a; // copy constructor a = b; // operator = b = c; // operator =... int a=1, b=2;... swap(a, b); Ez a template azt követeli meg a típustól, hogy jól működő copy konstruktora, destruktora és egyenlőség operátora legyen, más elvárása nincsen. Jó template eket nem túl könnyű írni, mivel ha az ember nem ismeri jól a C++ nyelvet, akkor esetleg olyan dolgokat is megkövetelhet, ami valójában az adott template működése szempontjából felesleges lenne, vagy esetleg nem lesz hatékony a generált kód. Példaképpen nézzük meg, hogy milyen plusz metódusokat igényelnek az alábbi, nem túl hatékony implementációk: template <class T> T minworst(t a, T b) // copy constructor 2* 2/29

T RetVal; // constructor if (a<b) // operator < RetVal = a; // operator = else RetVal = b; // operator = return RetVal; // copy constructor // destructor 3* template <class T> T minbad(t a, T b) // copy constructor 2* return ((a<b)? a : b); // operator<, copy constructor // destructor 2* Az STL Az STL azaz a Standard Template Library egy a templatekre épülő adatszerkezet és algoritmus gyűjtemény. Többféle implementációjával lehet találkozni és különféle C++ fordítók alatt működik. Ebből adódóan az egyszer megszerzett tudás és gyakorlat nagy valószínűséggel használható lesz egy másik C++ fordító alatt is. Mivel jól megtervezett és felépített a könyvtár, ezért a kezdeti nehézségeken túljutva könnyen tudjuk használni az esetleg még nem használt új funkciókat is. A különféle implementációk, ahogy már megszokhattuk, a lehetőségeket nem mind implementálják, vagy esetleg plusz lehetőségeket is tartalmaznak. Például a C++ Builderben található (a free Borland C++ 5.5 is ugyanezt tartalmazza) mellékelt Rogue Wave féle STL nem tartalmazza a hash alapú tárolókat, de például lehetőség van a memóriafoglalás paraméterezésére define okon keresztül. (Csak elvileg, mert gyakorlatilag pont ebből a részből hiányzik egy ";" karakter, azaz egy kis patch kell hozzá). Az STL t több részre fel lehet osztani: Tárolók Iterátorok Algoritmusok E cikk keretében elsősorban a különféle tárolókkal és a hozzájuk kapcsolódó iterátorokkal fogok foglalkozni. Minden egyes esetben gyakorlati példákon szemléltetve használatukat. Az ismertetésre kerülő tárolók: vector deque list set, multiset map, multimap Minden tárolókban tárolt típussal szemben követelmény a megfelelő copy konstruktor, a destruktor, az operator =, valamint a reference és dereference operátor. 3/29

Iterátorokról dióhéjban Az iterátorok szerepe a különféle tárolók adataihoz történő hozzáférés biztosítása. Fajtájukat tekintve léteznek egyirányú, kétirányú, konstans, nem konstans, normál/fordított irányú, stb. iterátorok. Az STL ben megtalálható iterátorfajtákat nem ismertetem részletesen, de a példák során bemutatom használatukat. Az iterátoroknak általában létezik konstans és nem konstans, valamint normál és fordított irányú verziójuk is. A példákban szereplő iterátorok főbb operátorai: Operátor Funkció ++ a következő elemre lép az előző elemre lép * az iterátor által "mutatott" adatot adja vissza == azonos e a két iterátor!= különböző e a két iterátor A véletlen elérésűeknél értelmezett még az összeadás, kivonás hasonlóan a mutatókhoz, illetve némelyiknél az operator >, amivel szintén az adattagot lehet elérni. Ezek alapján egy általános, bármely tároló tartalmát kiíró template et már könnyű írni, feltéve, hogy az adattagra értelmezett a kiírás. Az alábbi függvény szépsége az, hogy akár egy pointer párra is meghívható. #include <iostream> #include <typeinfo> using namespace std; template <class T> void printall(const T& beg, const T& end) cout<<typeid(t).name()<<endl; for (T i = beg; i!=end; i++) cout<<*i<<endl; main() int tomb[] = 1,2,3,4,5,6; printall(tomb+0, tomb+sizeof(tomb)/sizeof(tomb[0])); Közvetlen elérésű iterátorral rendelkező tárolók vector A vector osztály egy dinamikusan nyújtózkodó, illetve zsugorodó C típusú tömbnek fogható fel. Indexelése is ehhez hasonlóan 0 tól a mérete (size()) mínusz egyig terjed. Előnyösen használható bármikor a dinamikusan foglalt memória helyett, ha a jellemző művelet a tömb végéhez új elemek hozzáadása illetve elvétele, és fontos a direkt elérés. A memória felszabadításról az osztály illetve a fordító együttesen gondoskodik, azaz kivétel (exception) fellépte esetén is biztosan felszabadítja a lefoglalt memóriát (nincs több memory leak). A memória allokációs algoritmusa általában mivel 4/29

egyes STL verziónkban esetleg külön szabályozható mindig duplázza a lefoglalt memóriát, ha a méret növelésére van szükség, hogy ezzel is minimalizálja a növelésből adódó másolás overhead jét, valamint némely verzióban egy minimális területből indul ki, azaz ha csak egy elemet tartalmaz a vector, akkor is legalább lefoglal memóriát n darabnak, hogy ezzel is csökkentse a kezdeti másolások számát. A memóriafoglalást manuálisan is lehet szabályozni, a reserve() függvénnyel. A vector iterátora valójában egy mutató az elemre, a vector tartalmát a begin() és end() iterátor közötti folytonos memória területen lehet elérni. Létezik egy specializált vector osztály is, a vector<bool>, amelynek memória kezelése kihasználja, hogy egy bool érték ábrázolható egy biten is, és ezért a lefoglalt memória is sokkal kisebb lesz. Erre azonban már nem igaz az, hogy az iterátora egy pointer. Néhány példa a vector template használatára. #include <iostream> #include <vector> #include <algorithm> using namespace std; template <class T> void printall(const T& beg, const T& end) for (T i = beg; i!=end; i++) cout<<*i<<' '; cout<<endl; const char HW[] = "Hello world!\n"; main() // üres vector vector<char> vc1; // vector a Hello world! -al vector<char> vc2(hw+0, HW+sizeof(HW)/sizeof(HW[0])); // kiírás printall(vc2.begin(), vc2.end()); // sorbarendezése sort(vc2.begin(), vc2.end()); // kiírás printall(vc2.begin(), vc2.end()); // "!Hdellloorw" // keresés rendezett tömbben >= -re vector<char>::iterator pc; pc = lower_bound(vc2.begin(), vc2.end(), 'm'); if (pc!=vc2.end()) cout<<"found:'"<<*pc<<"'"<<endl; else cout<<"not found"<<endl; // 100 méretű vector vector<int> vi(5); // feltöltése 5/29

for (int i=0; i<5; i++) vi[i] = 9-i; // hozzáadás for (int i=5; i<10; i++) vi.push_back(9-i); printall(vi.begin(), vi.end()); // "9876543210" // egy részének sorbarendezése sort(vi.begin()+5, vi.end()); printall(vi.begin(), vi.end()); // "9876501234" deque A deque hasonlít a vectorhoz, annyi különbséggel, hogy az implementációja miatt mindkét végén, azaz az elején és a végén is, hatékony mind a beszúrás, mind a törlés művelet. A fenti példában a vector t kicserélve deque ra szintén működő kódot kapunk. Ebből is látható, hogyha az igények nőnek, akkor egy helyesen megírt kódot elég könnyen tovább lehet fejleszteni egy nagyobb tudású tárolóra. Egy lényeges különbsége van: mivel a deque egy körkörös buffert tartalmaz az implementáció során, ezért az iterátora nem egy pointer típus, azaz ha valahol szükség van erre, ott csak a vectort lehet használni. A list template egy kétirányú láncolt listás adatábrázolást valósít meg. Ebből adódnak az előnyei és a hátrányai is. Az elemeket csak szekvenciálisan lehet elérni, azaz ha szükség van az n edik elemre, akkor ehhez először le kell kérjük az elsőt, majd n 1 szer a következőre kell lépnünk, hogy elérjük az n ediket. A törlés és a beszúrás költsége konstans, mivel nincs szükség soha a listában szereplő értékek másolására, csak a mutatók átállítására. Visszacsatolás Az előző cikkben bennmaradt egy pontatlanság, amit gondolom néhányan észre is vettek. A deque implementációja fix méretű lefoglalt pufferekből áll. Egy deque lehetséges állapotát az első ábra szemlélteti. 1. ábra 2. ábra A deque ban középen elhelyezkedő pufferek mindig teljesen fel vannak töltve, így a pointer 6/29

aritmetika gyors lehet. Ha a végére vagy az elejére kell új elemeket hozzáadni, akkor legrosszabb esetben is csak egy új puffert kell lefoglalni, és a pointer táblában kell másolást végrehajtani. Ha törlési műveletet végzünk a legelején, illetve legvégén, akkor legrosszabb esetben el kell dobni a legutolsó, illetve legelső blokkot. Ha pedig se nem az elejére, se nem a végére vonatkozik a művelet, akkor a meglévő adat egy részét kell másolni a közelebb lévő végéhez, amit a második ábra szemléltet. Látható, hogy a törlés után a piros színű adat "előtt" lévő adatok "feljebb" másolódtak, míg a sárga színű adat esetén a "mögötte" lévő adatokat "lejjebb" másolódtak. Ebből is látható, hogy optimálisabb lehet a deque használata a legtöbb esetben, a vector osztály helyett. Sőt a memória allokáció / sebesség viszonya is kedvezőbb lehet, mivel a vector vagy duplázza mindig a méretét, vagy fix mérettel növeli. Ha duplázza, akkor elég nagy adatterület mehet veszendőbe, ha már nagy méretű a vector, ha pedig fix mérettel növeli, akkor pedig lassú lehet a folyamatos növelése a vectornak a sok másolás miatt. Az STL val foglalkozó dokumentációk általában megemlítik, hogy ha nincs igény arra, hogy az iterátor castolható legyen egy mutatóra, akkor mindig a deque t célszerű használni a vector helyett. Előnyként jelentkezik, hogy a deque iterátora nem válik érvénytelenné akkor, ha csak az elején illetve a végén végzünk műveleteket, és nem töröljük az iterátor által címzett adatot, míg a vector a memória újrafoglalás miatt ezt nem tudja biztosítani. list A list template egy kétirányú láncolt listás adatábrázolást valósít meg. Ebből adódnak az előnyei és a hátrányai is. Az elemeket csak szekvenciálisan lehet elérni, azaz ha szükség van az n edik elemre, akkor ehhez először le kell kérjük az elsőt, majd n 1 szer a következőre kell lépnünk, hogy elérjük az n ediket. A törlés és a beszúrás költsége konstans, mivel nincs szükség soha a listában szereplő értékek másolására, csak a mutatók átállítására. Egy list elemének járulékos memóriaigénye két mutató egy a lista előző és egy a következő elemére. Ennek az adatszerkezetnek megvan az az előnye, hogy az iterátorok nem válnak érvénytelenné a beszúrás/törlés hatására, nem úgy, mint a vector illetve a deque esetén. A STL tartalmaz egy sort metódust is a listhez, mivel a generikus sort algoritmus véletlen elérésű iterátort vár, illetve néhány hasznos függvényt rendezett listákra. A vectoros példaprogram list alapú implementációja: #include <iostream> #include <list> #include <algorithm> #include <functional> using namespace std; const char HW[] = "Hello world!\n"; main() // üres list list<char> lc1; // lista a Hello world! -al list<char> lc2(hw+0, HW+sizeof(HW)/sizeof(HW[0])); 7/29

// kiírás copy(lc2.begin(), lc2.end(), ostream_iterator<char,char>(cout," ")); cout<<endl; // sorbarendezése lc2.sort(); // kiírás copy(lc2.begin(), lc2.end(), ostream_iterator<char,char>(cout," ")); // "!Hdellloorw" cout<<endl; // keresés rendezett list-ben >= -re (nem kisebbre) list<char>::iterator pc; pc = find_if(lc2.begin(), lc2.end(), bind1st(less<char>(), 'm')); if (pc!=lc2.end()) cout<<"found:'"<<*pc<<"'"<<endl; else cout<<"not found"<<endl; // 5 méret? list list<int> li(5), li1; list<int>::iterator pi; // feltöltése pi=li.begin(); for (unsigned int i=0; i<li.size(); i++, pi++) *pi = 9-i; pi = li.end(); pi--; // hozzáadás for (int i=5; i<10; i++) li.push_back(9-i); copy(li.begin(), li.end(), ostream_iterator<int,char>(cout," ")); // "9876543210" cout<<endl; // egy részének sorbarendezése // 1. kivágom a sorbarendezendő részt li1.splice(li1.end(), li, ++pi, li.end()); copy(li.begin(), li.end(), ostream_iterator<int,char>(cout," ")); // "98765" cout<<endl; copy(li1.begin(), li1.end(), ostream_iterator<int,char>(cout," ")); // "43210" cout<<endl; // 2. sorbarendezem a kivágottat li1.sort(); copy(li1.begin(), li1.end(), ostream_iterator<int,char>(cout," ")); // "01234" cout<<endl; // 3. visszaillesztem a végére li.splice(li.end(), li1); 8/29

copy(li.begin(), li.end(), ostream_iterator<int,char>(cout," ")); // "9876501234" cout<<endl; Amint látható ez a megoldás már egy fokkal bonyolultabb, és kevésbé hatékony, mint a vector alapú implementáció. Például az 'm' betűnél nagyobb egyenlő keresése itt lineáris kereséssel működik, szemben a lower_bound ban implementált bináris kereséssel. Kevés elemszám esetén e kettő sebessége között azonban minimális a különbség. Ezért is fontos, hogy ismerjük a lehetőségeket, és azok közül ki tudjuk választani, a célnak leginkább megfelelőt. Nincs általánosan jó megoldás. Ha például egy olyan struktúrát akarunk tárolni, aminek a copy constructora időigényes művelet, és nem biztosítható, hogy nem kell törölni illetve beszúrni a tároló közepére, akkor célszerűbb lehet kerülni a vector és a deque tárolót. Azonban ha kevés elem van, akkor célszerűbb lehet az egyszerűbb, rövidebben kódolható, ergo hibamentesebb megoldást választani, függetlenül esetleg az implementáló adatszerkezet bonyolultságára. stack, queue A stack (LIFO), queue (FIFO) nem külön adatszerkezetek, hanem csak egy viselkedés implementálása más adatszerkezetek segítségével. Alapértelmezett esetben mindkettő a deque t használja az implementáció során, azonban el lehet ettől térni. A queue esetén nem célszerű használni a vectort, mivel utóbbi használata esetén nem túl hatékony a pop művelet, ami a vector elejéről töröl elemet, és ennek hatására a vector teljes tartalma másolásra kerül. A stack illetve a queue főbb műveletei: Művelet jelentés stack esetén jelentése queue esetén bool empty() const üres e üres e unsigned size() hány elem van benne const hány elem van benne T& top() a legutoljára beletett elem referenciáját adja vissza push(const T&) egy új elemet helyez el egy új elemet helyez el void pop() a legutoljára beletett elemet a legrégebben beletett elemet eltávolítja eltávolítja T& front() a legrégebben beletett elem referenciáját adja vissza T& back() a legutoljára beletett elem referenciáját adja vissza Az alábbi egyszerű kifejezés kiértékelő szemlélteti a stack egy lehetséges használatát (lásd még 9/29

http://www.prog.hu/cikkek/prog_langs/algoritmusok/exprs/index.htm): #include <iostream> #include <vector> #include <deque> #include <stack> #include <Exception> using namespace std; class ExprEvalError : public exception private: vector<char> Error; public: ExprEvalError(const char *in_error) while (*in_error) Error.push_back(*in_Error++); Error.push_back('\0'); const char * what () const return Error.begin(); ; class ExprEval private: enum TOperations opadd, opsub, opmul, opdiv, opneg, opvalue ; deque<long int> Values; deque<toperations> Ops; void Expr(const char* &expr, const int Level); void skipspaces(const char* &expr) while (*expr==' ' *expr=='\n' *expr=='\r' *expr=='\t') expr++; public: 10/29

ExprEval(const char* expr); long int operator() () const; ; // a Level jelzi a precedenciát void ExprEval::Expr(const char* &expr, const int Level) skipspaces(expr); switch (Level) case 0: // '+' '-' Expr(expr, Level+1); while (*expr=='+' *expr=='-') TOperations op = (*expr++=='+'? opadd : opsub); Expr(expr, Level+1); Ops.push_back(op); case 1: // '*' '/' Expr(expr, Level+1); while (*expr=='/' *expr=='*') TOperations op = (*expr++=='/'? opdiv : opmul); Expr(expr, Level+1); Ops.push_back(op); case 2: // '-' if (*expr=='-') expr++; Expr(expr, Level+1); Ops.push_back(opNeg); else Expr(expr, Level+1); case 3: // '(' Expr ')', ertek if (*expr=='(') Expr(++expr, 0); if (*expr++!=')') throw ExprEvalError("')' missing in expression"); else // parse number only [0..9]+ form if (*expr=='\0') 11/29

throw ExprEvalError("Not terminated correctly," " or empty expression"); long int Value = 0; while (*expr>='0' && *expr<='9') // not too perfect Value = Value*10 + (*expr++ - '0'); Values.push_back(Value); Ops.push_back(opValue); skipspaces(expr); ExprEval::ExprEval(const char* expr) Expr(expr, 0); if (*expr!='\0') char * ErrorStr = "Unexpected character ' ' in expression."; char * c = ErrorStr; while (*c!='\'') c++; c++; *c = *expr; throw ExprEvalError(ErrorStr); long int ExprEval::operator()() const stack<long int> EvalStack; long int V2; deque<long int>::const_iterator vi = Values.begin(); for (deque<toperations>::const_iterator opi = Ops.begin(); opi!= Ops.end(); opi++) switch (*opi) case opadd: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() += V2; case opsub: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() -= V2; case opmul: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() *= V2; case opdiv: V2 = EvalStack.top(); EvalStack.pop(); 12/29

EvalStack.top() /= V2; case opneg: EvalStack.top() *= -1; case opvalue: EvalStack.push(*vi++); return EvalStack.top(); main() try cout << ExprEval("1 + 20 / 3 + -(60-59) * 7")() << endl; catch (ExprEvalError &e) cerr << e.what() << endl; cout << endl; A következő fejezetben az asszociatív tárolókat fogom bemutatni, és azok segítségével kibővítem a fenti kifejezés kiértékelőt, változók függvények kezelésével. A számítástechnikában nagyon fontos szerepet töltenek be a hierarchikus adatszerkezetek, melyeknek egyik talán legismertebb reprezentációja a könyvtárszerkezet. Az STL ben többféle hierarchikus adatszerkezet található, ezek közül ismertet néhányat ez a cikk. Bevezetés A számítástechnikában nagyon fontos szerepet töltenek be a hierarchikus adatszerkezetek. Egyik talán legismertebb reprezentációja a könyvtárszerkezet, amely a tároló eszközökön található erőforrások elérését könnyíti meg. Vagy például az adatbázis kezelők működésében is kitüntetett szerepe van a B fa néven ismert indexszerkezetnek, amelyik szintén egy hierarchikus adatszerkezet. Ebben a cikkben csak a rendezett bináris fával fogunk találkozni, amelynek az a jellemzője, hogy egy elemnek maximum kettő "gyereke" lehet, és valamilyen összefüggés fent áll a "szülő" és a "gyereke" i között. A bináris fák nagyon jól használhatók keresésre és rendezésre, mivel a legtöbb művelet futási idejének felső korlátja arányos a fa mélységével, ami ideális esetben lg(n), ahol n a fában található elemek száma. priority_queue A priority_queue egy speciális adatszerkezeten, a kupacon (heap) alapuló template. A kupac egy olyan bináris fa, amelyik egy tömbben van ábrázolva. Egy lehetséges állapotot a következő táblázat illetve ábra szemlélteti. 13/29

A kupac rendező elve az, hogy a szülő elem kisebb egyenlő (vagy nagyobb egyenlő) a gyerekeinél. Egy elem indexéből a gyerekeinek indexe, illetve a szülejének az indexe egyszerűen és gyorsan számolható: #define LEFTCHILD(i) ((i)*2+1) #define RIGHTCHILD(i) (((i)+1)*2) #define PARENT(i) (((i)-1)/2) A tömb mindig teljesen kitöltött, azaz üres hely nincs benne, ebből adódóan a fa mélysége n elem esetén lg(n). Előnye ennek a szerkezetnek, hogy bármely tömb lineáris időben kupaccá rendezhető, illetve a beszúrás és törlés művelet időigénye a fa mélységével arányos, ami lg(n). A priority_queue a beszúrás során a beszúrandó elemet elhelyezi a tömb végén, majd "felutaztatja" a helyére, törlés során, a törölt elem helyére rakja legutolsót, és a tömb méretének csökkentése után, "leutaztatja" az elemet a helyére. A "fel " illetve "leutazás" során az aktuális elem illetve gyerekei közül kiválasztja a minimális elemet (maximális elemet), és ha ez nem a szülő, akkor cserét hajt végre, és folytatja tovább ezt az algoritmust a cserében részt vevő ágon. Az implementáláshoz használható alap struktúrák, a vector és a deque. A metódusai megegyeznek a stack metódusaival. A jellemző felhasználási területe a prioritás alapú ütemezés, azaz egy általános programban nem túl gyakori a használata : ). vörös fekete fa (red black tree) A bináris fák egy nagy csoportját alkotják a bináris keresőfák. A bináris keresőfa rendező elve, hogy a szülőnél kisebb (vagy nem nagyobb, vagy nem kisebb, vagy nagyobb) elemek mind a baloldali részfában találhatók, így elég egyszerű maximum a fa mélységének megfelelő lépésszámú feladat egy elem megkeresése. Egy új elem beszúrása szintén a fa mélységével arányos lépésszámban elvégezhető. Először meg kell keresni a helyét, majd a feltételnek megfelelő üres helyre felfűzni. A törlés egy picivel bonyolultabb, de természetesen ennek a lépésszáma is a fa mélységével arányos, azaz miután megvan a törlendő elem, meg kell nézni, hogy van e bal illetve jobboldali részfája. Ha egyik sincs, akkor törölhető. Ha csak baloldali, vagy jobboldali van, akkor a gyereke kerül a helyére. Egyébként pedig a jobboldali gyerekének kell a legkisebb elemét megkeresni, és a törölt helyére áthelyezni. Egy lehetséges bináris keresőfát ábrázol a következő ábra. 14/29

A bináris keresőfának azonban van egy problémás tulajdonsága, könnyen előfordulhat, hogy "elfajul", azaz egy szülő baloldali részfájának a mélysége jóval nagyobb, vagy kisebb lesz, mint a jobboldalié. Sőt az is előfordulhat, hogy egy bináris keresőfa átváltozik láncolt listává, azaz ha például csak baloldali, illetve jobboldali ágak vannak benne. Ez történik például akkor, ha rendezett adatokat szúrunk egy üres fába. Ennek elkerülésére több megoldás is született, ezek közül az egyik a vörös fekete fa. Az alapműveletek nem változtak, csak kiegészültek elfajulást megelőző (kiegyensúlyozó) kóddal. A vörös fekete fa rendezőelve a következő: Minden elemnek van színe, ami vagy vörös, vagy fekete. Az üres mutató (NULL) színe fekete. Bármely vörös elem mindkét gyerekének a színe fekete. Bármely elemtől induló, üres mutatóig vezető úton a fekete csúcsok száma azonos. Ha egy fára igazak ezek a szabályok, akkor a teljesül az, hogy a fa magassága maximum kétszerese az ideális fa magasságának, ebből következően a beszúrás, törlés, keresés lépésszáma arányos lg(n) el. Az STL a vörös fekete fákat használja a rendezett asszociatív tárolók (map, multimap, set, multiset) megvalósítására. A vörös fekete fa egy eleménél a fa fenntartásához szükséges járulékos memória igény (overhead) három mutató kettő a gyerekek felé, illetve egy a szülő felé illetve egy színinformációt hordozó változó. Ez az x86 os platformon 32 bites rendszert használva 16 byte. set, multiset A set egymástól különböző objektumok tárolására, rendezésére és ezek között keresésre szolgál. A tárolt típusra vagy értelmezve kell lennie a kisebb operátornak, vagy egy összehasonlító objektumot kell átadni a konstruktorban. A set/multiset legfontosabb függvényei: Művelet Jelentése bool empty() const üres e unsigned size() hány elem van benne const pair<iterator, bool> insert(const T&) új elemet ad hozzá, és visszatér az új elemre egy iterátorral és igazzal, illetve ha nem történt hozzáadás, akkor hamissal és a megtalált elemre egy iterátorral, ami set esetén fordulhat elő akkor, ha már volt benne ilyen elem 15/29

size_type erase(const T&) kitörli a megadott értékű elemeket, és visszaadja, hány törlés történt void erase(iterator) kitörli a megadott elemet void clear() iterator find(const T&) const size_type count(const T&) const pair<iterator, iterator> equal_range(const T&) const iterator begin() const kiürítés megkeresi a kért elemet, multiset esetén a legkisebb ilyet megszámolja, hogy hány adott értékű elem található visszaadja a megadott értékű elemnél nagyobb egyenlő (lower_bound), nagyobb (upper_bound) intervallumot egy iterátort ad a legelső mivel rendezett a set, ezért a legkisebb elemre iterator end() const egy iterátort ad vissza a legutolsó elem utáni pozicióra A multiset abban különbözik a set től, hogy azonos értékű (azaz amelyik se nem kisebb, se nem nagyobb) elemből több is lehet benne. Például set esetén a count vagy nullát, vagy egyet ad vissza, míg multiset esetén vagy nullát, vagy nagyobb, mint nullát. map, multimap A map kulcsok, és a kulcsokhoz hozzátartozó adatok tárolására szolgál. A kulcsra ugyanaz vonatkozik, mint ami a set et alkotó adatokra, míg kulcshoz tartozó adat tetszőleges típus lehet, azonban természetesen erre is jól kell működnie a copy kontruktornak, egyenlőség operátornak és a destruktornak. Egy map felfogható úgy is, és a belső megvalósítása valójában ennek megfelelő, mint egy kulcsból és adatból álló pár (megvalósításban pair<key, Value>), amire értelmezve van az összehasonlítás operátor, ami a kulcs részt hasonlítja össze. Annyi a különbség, hogy a keresési műveleteket nem az összetett struktúrával kell meghívni, hanem csak a kulccsal. A map/multimap legfontosabb függvényei hasonlóak a set/multiset nél megismertekhez (a kulcs típusa K, az értéké V, a kulcs és érték összetett adat típusa KV :), amire van egy typedef e is a map nek value_type néven): Művelet bool empty() const unsigned size() const pair<iterator, bool> insert(const KV&) V& operator[] Jelentése üres e hány elem van benne új kulcs és érték párt ad hozzá, és visszatér az új elemre egy iterátorral és igazzal, illetve, ha nem történt hozzáadás, hamissal és a megtalált elemre egy iterátorral, ami map esetén fordulhat elő akkor, ha már volt benne ilyen kulcsú elem csak map esetén! megkeresi a megadott kulcsú elemet, és 16/29

(const K&) size_type erase(const K&) void erase(iterator) void clear() iterator find(const K&) visszaadja a hozzátartozó értéket; ha nem talál ilyet, akkor beilleszt egy új elemet a map be aminek a kulcsa a megadott, az értéke pedig a default konstruktor által adott érték (azaz kell hozzá a paraméterek nélküli konstruktor) kitörli a megadott kulcsú elemeket, és visszaadja, hány törlés történt kitörli a megadott elemet kiürítés const megkeresi a kért kulcsú elemet, multimap esetén a legkisebb ilyet size_type const megszámolja, hogy hány elem kulcsa egyezik meg a count(const K&) megadott kulcs értékével pair<iterator, const visszaadja a megadott kulcsú elemnél nagyobb egyenlő iterator> (lower_bound), nagyobb (upper_bound) intervallumot equal_range(con st K&) iterator begin() const egy iterátort ad a legelső mivel rendezett a map, ezért a legkisebb elemre iterator end() const egy iterátort ad vissza a legutolsó elem utáni pozicióra A map és multimap között annyi a különbség, hogy a map ben a kulcs egyedi, míg a multimap ben nem. Példák az asszociatív template k használatára A megismert asszociatív template k nagyon hasznosak tudnak lenni a mindennapi programozási munka során. Például adatbázis kezelő program írása közben biztos néhányunkban felvetődött már, hogy jó lenne az adatok egy statikus, vagy nagyon ritkán változó részét kliens (legalábbis az adatbázis kiszolgáló szempontjából) oldalon is tárolni (cache elni), és ezzel gyorsítani az alkalmazás futását. Nézzünk egy példát egy adatbázis egy táblájának a kliens oldali cache elésére: Az adatbázis táblája mondjuk, tartalmazza az országok kódját, nevét, és az ország telefonos előtagját, a területét, és az egy főre jutó GDP t, azaz a következő SQL script feleljen meg neki: create table country ( kod varchar(3) not null primary key, nev varchar(30) not null unique, telprefix varchar(10), terulet integer, GDP_p_fo numeric(15,2) -- oracle esetén number(15,2) ) Kliens oldalon tároljuk a teljes táblát, a nem kitöltött adatok helyett legyen üres string és nulla, és adjunk lehetőséget a kód és név szerinti elérésre, rendezett lista lekérése név, kód és GDP alapján. 17/29

Az ennek megfelelő struktúra és osztály interface: struct TCountryData string kod; string nev; string telprefix; int terulet; double GDP_p_fo; ; class TCountryCache... public: void InsorUpd(const TCountryData &Data); void Delete(const string &kod); // ha nem talált, akkor false bool Find(const string &kod, TCountryData &Data) const; // ha nem talált, akkor false bool FindByNev(const string &nev, TCountryData &Data) const; vector<tcountrydata> GetListByKod() const; vector<tcountrydata> GetListByNev() const; vector<tcountrydata> GetListByGDP() const; ; A definíció alapján szükséges egy tároló, ami tárolja az adatokat. Azt, hogy miben tároljuk ezt a struktúrát (map vagy set), inkább a kényelem szabja meg. Ha map et használunk, akkor az ország kódját kétszer tároljuk, vagy az eltárolt struktúrában nem tároljuk a kódot, és így két struktúrára van szükség (mivel nem használhatjuk a TCountryData t), de ezért cserében egyszerű keresés az adatok között kód alapján. Ha set et használunk, akkor csak egy struktúra van, amire vagy megírjuk a <operátort, vagy írunk hozzá egy kód alapján összehasonlító osztályt, de keresés esetén mindig egy teljes struktúrával kell keresni. A kód alapján, akár map, akár a set template t használjuk is, már könnyen és hatékonyan tudunk keresni, mivel ez a kulcsa a tároló struktúrának. Azonban szükség van a név alapján történő keresésre is. Erre szintén több lehetőség is létezik. Lehet a lineáris keresést használni a tároló template n, ami nem túl hatékony, de ha kevés adat van, akkor megfelelő, sőt kevés ország (kb. 10 20) esetén az egész problémát célszerűbb így megoldani, azaz deque/list/vector template segítségével tárolni az országok információit, és lineáris keresést használni. A másik kézenfekvő megoldás, hogy egy map et (ha nem egyedi az index, akkor multimap et) használunk, aminek a kulcsa az indexnek a mezőiből áll, és az adat része pedig valamilyen elérési információ a tárolt adatra. Ez lehet egy iterator (16 byte), lehet egy mutató (4 byte), de akár lehet az adat kulcsa is. Válasszuk most a mutatót használó megoldást, bár ennél a tesztpéldánál bármelyik jó lenne, mivel nincs sok adat, de sok adat esetén a járulékos memória használat (overhead) már számíthat. Ezek után nézzünk példát a map alapú implementációra: class TCountryCache private: typedef map<string, TCountryData> TData; 18/29

typedef map<string, const TCountryData*> TNevIdx; TData Data; TNevIdx NevIdx; // a GDP alapján történő rendezés használja struct GDPComp bool operator() (const TCountryData& x, const TCountryData &y) const return x.gdp_p_fo < y.gdp_p_fo; ; public:... void TCountryCache::InsorUpd(const TCountryData &AData) pair<tdata::iterator, bool> ir; ir = Data.insert(TData::value_type(AData.kod, AData)); if (!ir.second) // módosítás // a régi érték törlése az indexből NevIdx.erase(ir.first->second.nev); // a tárolóban az adat felülírása ir.first->second = AData; // beszúrás az indexbe is NevIdx.insert(TNevIdx::value_type(AData.nev, &(ir.first- >second))); void TCountryCache::Delete(const string &kod) TData::iterator i = Data.find(kod); if (i!= Data.end()) NevIdx.erase(i->second.nev); Data.erase(i); bool TCountryCache::Find(const string &kod, TCountryData &AData) const TData::const_iterator i = Data.find(kod); if (i!= Data.end()) AData = i->second; return (i!= Data.end()); 19/29

bool TCountryCache::FindByNev(const string &nev, TCountryData &AData) const TNevIdx::const_iterator i = NevIdx.find(nev); if (i!= NevIdx.end()) AData = *(i->second); return (i!= NevIdx.end()); vector<tcountrydata> TCountryCache::GetListByKod() const vector<tcountrydata> RetVal; RetVal.reserve(Data.size()); for (TData::const_iterator i = Data.begin(); i!= Data.end(); i+ +) RetVal.push_back(i->second); return RetVal; vector<tcountrydata> TCountryCache::GetListByNev() const vector<tcountrydata> RetVal; RetVal.reserve(Data.size()); for (TNevIdx::const_iterator i = NevIdx.begin(); i!= NevIdx.end(); i++) RetVal.push_back(*(i->second)); return RetVal; vector<tcountrydata> TCountryCache::GetListByGDP() const vector<tcountrydata> RetVal = GetListByKod(); sort(retval.begin(), RetVal.end(), GDPComp()); return RetVal; Vagy ugyanaz példa, de az adattároláshoz a set template-t használva (csak a különböző függvények szerepelnek): class TCountryCache private: struct TCountryDatakodComp bool operator() (const TCountryData& x, const TCountryData &y) const return x.kod < y.kod; ; typedef set<tcountrydata, TCountryDatakodComp> TData;... void TCountryCache::InsorUpd(const TCountryData &AData) 20/29

pair<tdata::iterator, bool> ir; ir = Data.insert(AData); if (!ir.second) // módosítás // a régi érték törlése az indexből NevIdx.erase(ir.first->nev); // a tárolóban az adat felülírása // az SGI féle STL-ben a set iterátora mindig const, szemben // például a Rogue Wave féle STL-el const_cast<tcountrydata&>(*(ir.first)) = AData; // beszúrás az indexbe is NevIdx.insert(TNevIdx::value_type(AData.nev, &(*(ir.first)))); void TCountryCache::Delete(const string &kod) TCountryData tmp; tmp.kod = kod; TData::iterator i = Data.find(tmp); if (i!= Data.end()) NevIdx.erase(i->nev); Data.erase(i); bool TCountryCache::Find(const string &kod, TCountryData &AData) const TCountryData tmp; tmp.kod = kod; TData::const_iterator i = Data.find(tmp); if (i!= Data.end()) AData = *i; return (i!= Data.end());... vector<tcountrydata> TCountryCache::GetListByKod() const vector<tcountrydata> RetVal; RetVal.reserve(Data.size()); for (TData::const_iterator i = Data.begin(); i!= Data.end(); i+ +) RetVal.push_back(*i); return RetVal;... Egy másik érdekes kis felhasználási lehetőség, ha sok string et akarunk tárolni, és az adatok között sok az ismétlődés, akkor memóriát lehet megtakarítani, ha a referencia számolt string osztályra 21/29

alapozva írunk egy kis segéd osztályt, amelyik eltárolja az ismétlődő stringeket. Példaképpen nézzük meg az alábbi program memóriafoglalását mindkét esetben (gcc vel fordítva, win32 platformon a memóriafoglalás aránya 1132 kb/10228 kb). #include <iostream> #include <string> #include <vector> #include <set> using namespace std; // implement singleton pattern class stringref private: set<string> strings; stringref() public: static const string& get(const string& s); ; const string& stringref::get(const string& s) static stringref sr; return *(sr.strings.insert(s).first); string getstring(int n, bool useref) string s; for (int i=0; i<n; i++) s+="a"; return (useref? stringref::get(s) : s); main(int argc) vector<string> strings(100000); if (argc==1) cout << "Using stringref::get." << endl; else cout << "Not using stringref::get." << endl; cout << "Press Enter key to continue" << endl; cin.ignore(); cout << "Running..." << endl; for (unsigned i=0; i<strings.size(); i++) strings[i] = getstring(i%100, argc==1); cout << "Test completed. Press Enter key to exit" << endl; cin.ignore(); 22/29

Végül a múltkori cikkben megígért kifejezés kiértékelő függvény, kibővítve változó kezeléssel és egy illetve két operandust kezelő függvényekkel. #include <iostream> #include <vector> #include <deque> #include <stack> #include <map> #include <Exception> using namespace std; class ExprEvalError : public exception private: vector<char> Error; public: ExprEvalError(const char *in_err1, const char *in_err2 = NULL) while (*in_err1) Error.push_back(*in_Err1++); while (in_err2 && *in_err2) Error.push_back(*in_Err2++); Error.push_back('\0'); const char * what () const return Error.begin(); ; typedef long int (*func1)(long int); typedef long int (*func2)(long int, long int); class ExprEval private: enum TOperations opadd, opsub, opmul, opdiv, opneg, opvalue, opvar, opfunc1, opfunc2 ; 23/29

union TOpData long int Value; func1 f1; func2 f2; long int* PVar; ; struct TOperation TOperations Op; TOpData Data; TOperation(TOperations AOp) Op = AOp; Data.Value = 0; ; typedef map<string, long int> tvars; typedef map<string, func1> tfunc1s; typedef map<string, func2> tfunc2s; deque<toperation> Ops; tvars vars; tfunc1s func1s; tfunc2s func2s; locale loc; void Expr(const char* &expr, const int Level); void ExprCmpx(const char* &expr); void skipspaces(const char* &expr) while (isspace(*expr, loc)) expr++; public: // a következő függvények törlik a parse-olt kifejezést void definefunc1(const string& name, func1 f); void definefunc2(const string& name, func2 f); void definevar(const string& name, long int val=0); void clear(); void parse(const char* expr); long int setvar(const string& name, long int val); long int operator() () const; ; // a Level jelzi a precedenciát void ExprEval::Expr(const char* &expr, const int Level) skipspaces(expr); 24/29

switch (Level) case 0: // '+' '-' Expr(expr, Level+1); while (*expr=='+' *expr=='-') TOperations op = (*expr++=='+'? opadd : opsub); Expr(expr, Level+1); Ops.push_back(op); case 1: // '*' '/' Expr(expr, Level+1); while (*expr=='/' *expr=='*') TOperations op = (*expr++=='/'? opdiv : opmul); Expr(expr, Level+1); Ops.push_back(op); case 2: // '-' if (*expr=='-') expr++; Expr(expr, Level+1); Ops.push_back(opNeg); else Expr(expr, Level+1); case 3: // '(' Expr ')', ertek if (*expr=='(') Expr(++expr, 0); if (*expr++!=')') throw ExprEvalError("')' missing in expression"); else // parse funcs, vars and numbers (only [0..9]+) ExprCmpx(expr); skipspaces(expr); void ExprEval::ExprCmpx(const char* &expr) if (*expr=='\0') throw ExprEvalError("Not terminated correctly," 25/29

" or empty expression"); TOperation Op(opValue); if (isdigit(*expr, loc)) Op.Data.Value = 0; while (isdigit(*expr, loc)) // not too perfect Op.Data.Value = Op.Data.Value*10 + (*expr++ - '0'); else string name; while (isalnum(*expr, loc)) name += *expr++; skipspaces(expr); if (*expr=='(') // functions Expr(++expr, 0); if (*expr == ',') // func/2 Expr(++expr, 0); if (*expr++!=')') throw ExprEvalError("')' missing in expression"); Op.Op = opfunc2; tfunc2s::iterator i = func2s.find(name); if (i == func2s.end()) throw ExprEvalError("Unknown function/2:", name.c_str()); Op.Data.f2 = i->second; else // func/1 if (*expr++!=')') throw ExprEvalError("')' missing in expression"); Op.Op = opfunc1; tfunc1s::iterator i = func1s.find(name); if (i == func1s.end()) throw ExprEvalError("Unknown function/1:", name.c_str()); Op.Data.f1 = i->second; else // variable Op.Op = opvar; tvars::iterator i = vars.find(name); if (i == vars.end()) throw ExprEvalError("Unknown variable:", name.c_str()); 26/29

Op.Data.PVar = &(i->second); Ops.push_back(Op); void ExprEval::parse(const char* expr) Ops.clear(); Expr(expr, 0); if (*expr!='\0') throw ExprEvalError("Unexpected character in expression:", expr); long int ExprEval::operator()() const stack<long int> EvalStack; long int V1; long int V2; if (Ops.size()==0) throw ExprEvalError("Empty expression"); for (deque<toperation>::const_iterator opi = Ops.begin(); opi!= Ops.end(); opi++) switch (opi->op) case opadd: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() += V2; case opsub: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() -= V2; case opmul: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() *= V2; case opdiv: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() /= V2; case opneg: EvalStack.top() *= -1; case opvalue: EvalStack.push(opi->Data.Value); 27/29

case opfunc1: EvalStack.top() = opi->data.f1(evalstack.top()); case opfunc2: V2 = EvalStack.top(); EvalStack.pop(); EvalStack.top() = opi->data.f2(evalstack.top(), V2); case opvar: EvalStack.push(*opi->Data.PVar); return EvalStack.top(); void ExprEval::definefunc1(const string& name, func1 f) if (!func1s.insert(tfunc1s::value_type(name, f)).second) throw ExprEvalError("Function/1 already exists:", name.c_str()); Ops.clear(); void ExprEval::definefunc2(const string& name, func2 f) if (!func2s.insert(tfunc2s::value_type(name, f)).second) throw ExprEvalError("Function/2 already exists:", name.c_str()); Ops.clear(); void ExprEval::definevar(const string& name, long int val) if (!vars.insert(tvars::value_type(name, val)).second) throw ExprEvalError("Variable already exists:", name.c_str()); Ops.clear(); void ExprEval::clear() Ops.clear(); func1s.clear(); func2s.clear(); vars.clear(); long int ExprEval::setvar(const string& name, long int val) long int RetVal; tvars::iterator i = vars.find(name); 28/29

if (i == vars.end()) throw ExprEvalError("Variable doesn't exist:", name.c_str()); RetVal = i->second; i->second = val; return RetVal; long int myabs(long int a) if (a<0) return -a; else return 0; long int mymul(long int a, long int b) return a*b; main() try ExprEval ee; ee.definevar("a"); ee.definefunc1("abs", myabs); ee.definefunc1("mul", mymul); ee.setvar("a", 2); ee.parse("a + abs(20 / 3) - abs(mul(-(60-59), 8))"); cout << ee() << endl; ee.setvar("a", 5); cout << ee() << endl; catch (ExprEvalError &e) cerr << e.what() << endl; cout << endl; Látható, hogy az STL segítségével a feladatra lehetett koncentrálni (kifejezés elemzés és kiértékelés), és a járulékos kódmennyisége minimális szinten volt tartható. Ha megnézzük a változó és függvénykezelő kódot, akkor látható, hogy milyen egyszerű volt a map segítségével megoldani ezt a feladatot, mindössze néhány sor az egész. Szinte fel sem vetődik, hogy helyes e program memória kezelése, azaz például mindig felszabadul e a lefoglalt memória, mivel erről a fordító és az STL együttesen gondoskodik. A mutató konverziók miatti hibalehetőségek száma is minimális, ami egy hasonló feladatot ellátó c programról már nem feltétlen mondható el. 29/29