Bevezetés

Ez a bevezetés áttekintést ad a C++ programozási nyelv fõ fogalmairól, tulajdonságairól és standard (szabvány) könyvtáráról, valamint bemutatja a könyv szerkezetét és elmagyarázza azt a megközelítést, amelyet a nyelv lehetõségeinek és azok használatának leírásánál alkalmaztunk. Ezenkívül a bevezetõ fejezetek némi háttérinformációt is adnak a C++-ról, annak felépítésérõl és felhasználásáról.

Fejezetek 1. Megjegyzések az olvasóhoz 2. Kirándulás a C++-ban 3. Kirándulás a standard könyvtárban

1
Megjegyzések az olvasóhoz
„Szólt a Rozmár: Van ám elég, mirõl mesélni jó: ...” (L. Carroll – ford. Tótfalusi István) A könyv szerkezete • Hogyan tanuljuk a C++-t? • A C++ jellemzõi • Hatékonyság és szerkezet • Filozófiai megjegyzés • Történeti megjegyzés • Mire használjuk a C++-t? • C és C++ • Javaslatok C programozóknak • Gondolatok a C++ programozásról • Tanácsok • Hivatkozások

1.1. A könyv szerkezete
A könyv hat részbõl áll: Bevezetés: Elsõ rész: Második rész: Az 1–3. fejezetek áttekintik a C++ nyelvet, az általa támogatott fõ programozási stílusokat, és a C++ standard könyvtárát. A 4–9. fejezetek oktató jellegû bevezetést adnak a C++ beépített típusairól és az alapszolgáltatásokról, melyekkel ezekbõl programot építhetünk. A 10–15. fejezetek bevezetést adnak az objektumorientált és az általánosított programozásba a C++ használatával.

4

Bevezetés

Harmadik rész: A 16–22. fejezetek bemutatják a C++ standard könyvtárát. Negyedik rész: A 23–25. fejezetek tervezési és szoftverfejlesztési kérdéseket tárgyalnak. Függelékek: Az A–E függelékek a nyelv technikai részleteit tartalmazzák. Az 1. fejezet áttekintést ad a könyvrõl, néhány ötletet ad, hogyan használjuk, valamint háttérinformációkat szolgáltat a C++-ról és annak használatáról. Az olvasó bátran átfuthat rajta, elolvashatja, ami érdekesnek látszik, és visszatérhet ide, miután a könyv más részeit elolvasta. A 2. és 3. fejezet áttekinti a C++ programozási nyelv és a standard könyvtár fõ fogalmait és nyelvi alaptulajdonságait, megmutatva, mit lehet kifejezni a teljes C++ nyelvvel. Ha semmi mást nem tesznek, e fejezetek meg kell gyõzzék az olvasót, hogy a C++ nem (csupán) C, és hogy a C++ hosszú utat tett meg e könyv elsõ és második kiadása óta. A 2. fejezet magas szinten ismertet meg a C++-szal. A figyelmet azokra a nyelvi tulajdonságokra irányítja, melyek támogatják az elvont adatábrázolást, illetve az objektumorientált és az általánosított programozást. A 3. fejezet a standard könyvtár alapelveibe és fõ szolgáltatásaiba vezet be, ami lehetõvé teszi, hogy a szerzõ a standard könyvtár szolgáltatásait használhassa a következõ fejezetekben, valamint az olvasónak is lehetõséget ad, hogy könyvtári szolgáltatásokat használjon a gyakorlatokhoz és ne kelljen közvetlenül a beépített, alacsony szintû tulajdonságokra hagyatkoznia. A bevezetõ fejezetek egy, a könyv folyamán általánosan használt eljárás példáját adják: ahhoz, hogy egy módszert vagy tulajdonságot még közvetlenebb és valószerûbb módon vizsgálhassunk, alkalmanként elõször röviden bemutatunk egy fogalmat, majd késõbb behatóbban tárgyaljuk azt. Ez a megközelítés lehetõvé teszi, hogy konkrét példákat mutassunk be, mielõtt egy témát általánosabban tárgyalnánk. A könyv felépítése így tükrözi azt a megfigyelést, hogy rendszerint úgy tanulunk a legjobban, ha a konkréttól haladunk az elvont felé – még ott is, ahol visszatekintve az elvont egyszerûnek és magától értetõdõnek látszik. Az I. rész a C++-nak azt a részhalmazát írja le, mely a C-ben vagy a Pascalban követett hagyományos programozási stílusokat támogatja. Tárgyalja a C++ programokban szereplõ alapvetõ típusokat, kifejezéseket, vezérlési szerkezeteket. A modularitást, mint a névterek, forrásfájlok és a kivételkezelés által támogatott tulajdonságot, szintén tárgyalja. Feltételezzük, hogy az olvasónak már ismerõsek az I. fejezetben használt alapvetõ programozási fogalmak, így például bemutatjuk a C++ lehetõségeit a rekurzió és iteráció kifejezésére, de nem sokáig magyarázzuk, milyen hasznosak ezek. A II. rész a C++ új típusok létrehozását és használatát segítõ szolgáltatásait írja le. Itt (10. és 12. fejezet) mutatjuk be a konkrét és absztrakt osztályokat (felületeket), az operátor-túlterheléssel (11. fejezet), a többalakúsággal (polimorfizmussal) és az osztályhierarchiák hasz-

1. Megjegyzések az olvasóhoz

5

nálatával (12. és 15. fejezet) együtt. A 13. fejezet a sablonokat (template) mutatja be, vagyis a C++ lehetõségeit a típus- és függvénycsaládok létrehozására, valamint szemlélteti a tárolók elõállítására (pl. listák), valamint az általánosított (generikus) programozás támogatására használt alapvetõ eljárásokat. A 14. fejezet a kivételkezelést, a hibakezelési módszereket tárgyalja és a hibatûrés biztosításához ad irányelveket. Feltételezzük, hogy az olvasó az objektumorientált és az általánosított programozást nem ismeri jól, illetve hasznát látná egy magyarázatnak, hogyan támogatja a C++ a fõ elvonatkoztatási (absztrakciós) eljárásokat. Így tehát nemcsak bemutatjuk az elvonatkoztatási módszereket támogató nyelvi tulajdonságokat, hanem magukat az eljárásokat is elmagyarázzuk. A IV. rész ebben az irányban halad tovább. A III. rész a C++ standard könyvtárát mutatja be. Célja: megértetni, hogyan használjuk a könyvtárat; általános tervezési és programozási módszereket szemléltetni és megmutatni, hogyan bõvítsük a könyvtárat. A könyvtár gondoskodik tárolókról (konténerek – list, vector, map, 18. és 19. fejezet), szabványos algoritmusokról (sort, find, merge, 18. és 19. fejezet), karakterlánc-típusokról és -mûveletekrõl (20. fejezet), a bemenet és kimenet kezelésérõl (input/output, 21. fejezet), valamint a számokkal végzett mûveletek („numerikus számítás”) támogatásáról (22. fejezet). A IV. rész olyan kérdéseket vizsgál, melyek akkor merülnek fel, amikor nagy szoftverrendszerek tervezésénél és kivitelezésénél a C++-t használjuk. A 23. fejezet tervezési és vezetési kérdésekkel foglalkozik. A 24. fejezet a C++ programozási nyelv és a tervezési kérdések kapcsolatát vizsgálja, míg a 25. fejezet az osztályok használatát mutatja be a tervezésben. Az „A” függelék a C++ nyelvtana, néhány jegyzettel. A „B” függelék a C és a C++ közti és a szabványos C++ (más néven ISO C++, ANSI C++) illetve az azt megelõzõ C++-változatok közti rokonságot vizsgálja. A „C” függelék néhány nyelvtechnikai példát mutat be, A „D” függelék pedig a kulturális eltérések kezelését támogató standard könyvtárbeli elemeket mutatja be. Az „E” függelék a standard könyvtár kivételkezelésel kapcsolatos garanciáit és követelményeit tárgyalja.

1.1.1. Példák és hivatkozások
Könyvünk az algoritmusok írása helyett a program felépítésére fekteti a hangsúlyt. Következésképpen elkerüli a ravasz vagy nehezebben érthetõ algoritmusokat. Egy egyszerû eljárás alkalmasabb az egyes fogalmak vagy a programszerkezet egy szempontjának szemléltetésére. Például Shell rendezést használ, ahol a valódi kódban jobb lenne gyorsrendezést (quicksort) használni. Gyakran jó gyakorlat lehet a kód újraírása egy alkalmasabb algoritmussal. A valódi kódban általában jobb egy könyvtári függvény hívása, mint a könyvben használt, a nyelvi tulajdonságok szemléltetésére használt kód.

6

Bevezetés

A tankönyvi példák szükségszerûen egyoldalú képet adnak a programfejlesztésrõl. Tisztázva és egyszerûsítve a példákat a felmerült bonyolultságok eltûnnek. Nincs, ami helyettesítené a valódi programok írását, ha benyomást akarunk kapni, igazából milyen is a programozás és egy programozási nyelv. Ez a könyv a nyelvi tulajdonságokra és az alapvetõ eljárásokra összpontosít, amelyekbõl minden program összetevõdik, valamint az összeépítés szabályaira. A példák megválasztása tükrözi fordítóprogramokkal, alapkönyvtárakkal, szimulációkkal jellemezhetõ hátteremet. A példák egyszerûsített változatai a valódi kódban találhatóknak. Egyszerûsítésre van szükség, hogy a programozási nyelv és a tervezés lényeges szempontjai el ne vesszenek a részletekben. Nincs „ügyes” példa, amelynek nincs megfelelõje a valódi kódban. Ahol csak lehetséges, a „C” függelékben lévõ nyelvtechnikai példákat olyan alakra hoztam, ahol a változók x és y, a típusok A és B, a függvények f() és g() nevûek. A kódpéldákban az azonosítókhoz változó szélességû betûket használunk. Például:
#include<iostream> int main() { std::cout << "Helló, világ!\n"; }

Elsõ látásra ez „természetellenesnek” tûnhet a programozók számára, akik hozzászoktak, hogy a kód állandó szélességû betûkkel jelenik meg. A változó szélességû betûket általában jobbnak tartják szöveghez, mint az állandó szélességût. A változó szélességû betûk használata azt is lehetõvé teszi, hogy a kódban kevesebb legyen a logikátlan sortörés. Ezenkívül saját kísérleteim azt mutatják, hogy a legtöbb ember kis idõ elteltével könnyebben olvashatónak tartja az új stílust. Ahol lehetséges, a C++ nyelv és könyvtár tulajdonságait a kézikönyvek száraz bemutatási módja helyett a felhasználási környezetben mutatjuk be. A bemutatott nyelvi tulajdonságok és leírásuk részletessége a szerzõ nézetét tükrözik, aki a legfontosabb kérdésnek a következõt tartja: mi szükséges a C++ hatékony használatához? A nyelv teljes leírása – a könnyebb megközelítés céljából jegyzetekkel ellátva – a The Annotated C++ Language Standard címû kézikönyvben található, mely Andrew Koenig és a szerzõ mûve. Logikusan kellene hogy legyen egy másik kézikönyv is, a The Annotated C++ Standard Library. Mivel azonban mind az idõ, mind írási kapacitásom véges, nem tudom megígérni, hogy elkészül. A könyv egyes részeire való hivatkozások §2.3.4 (2. fejezet, 3.szakasz, 4. bekezdés), §B.5.6 („B” függelék, 5.6. bekezdés és §6.[10](6. fejezet, 10. gyakorlat) alakban jelennek meg. A dõlt betûket kiemelésre használjuk (pl. „egy karakterlánc-literál nem fogadható el”), fon-

1. Megjegyzések az olvasóhoz

7

tos fogalmak elsõ megjelenésénél (pl. többalakúság), a C++ nyelv egyes szimbólumainál (pl. for utasítás), az azonosítóknál és kulcsszavaknál, illetve a kódpéldákban lévõ megjegyzéseknél.

1.1.2. Gyakorlatok
Az egyes fejezetek végén gyakorlatok találhatók. A gyakorlatok fõleg az „írj egy programot” típusba sorolhatók. Mindig annyi kódot írjunk, ami elég ahhoz, hogy a megoldás fordítható és korlátozott körülmények között futtatható legyen. A gyakorlatok nehézségben jelentõsen eltérõek, ezért becsült nehézségi fokukat megjelöltük. A nehézség hatványozottan nõ, tehát ha egy (*1) gyakorlat 10 percet igényel, egy (*2) gyakorlat egy órába, míg egy (*3) egy napba kerülhet. Egy program megírása és ellenõrzése inkább függ az ember tapasztaltságától, mint magától a gyakorlattól.

1.1.3. Megjegyzés az egyes C++-változatokhoz
A könyvben használt nyelv „tiszta C++”, ahogyan a C++ szabványban leírták [C++, 1998]. Ezért a példáknak futniuk kell minden C++-változaton. A könyvben szereplõ nagyobb programrészleteket több környezetben is kipróbáltuk, azok a példák azonban, melyek a C++-ba csak nemrégiben beépített tulajdonságokat használnak fel, nem mindenhol fordíthatók le. (Azt nem érdemes megemlíteni, mely változatokon mely példákat nem sikerült lefordítani. Az ilyen információk hamar elavulnak, mert a megvalósításon igyekvõ programozók keményen dolgoznak azon, hogy nyelvi változataik helyesen fogadjanak el minden C++ tulajdonságot.) A „B” függelékben javaslatok találhatók, hogyan birkózzunk meg a régi C++ fordítókkal és a C fordítókra írott kóddal.

1.2. Hogyan tanuljuk a C++-t?
A C++ tanulásakor a legfontosabb, hogy a fogalmakra összpontosítsunk és ne vesszünk el a részletekben. A programozási nyelvek tanulásának célja az, hogy jobb programozóvá váljunk; vagyis hatékonyabbak legyünk új rendszerek tervezésénél, megvalósításánál és régi rendszerek karbantartásánál. Ehhez sokkal fontosabb a programozási és tervezési módszerek felfedezése, mint a részletek megértése; az utóbbi idõvel és gyakorlattal megszerezhetõ.

8

Bevezetés

A C++ sokféle programozási stílust támogat. Ezek mind az erõs statikus típusellenõrzésen alapulnak és legtöbbjük a magas elvonatkoztatási szint elérésére és a programozó elképzeléseinek közvetlen leképezésére irányul. Minden stílus el tudja érni a célját, miközben hatékony marad futási idõ és helyfoglalás tekintetében. Egy más nyelvet (mondjuk C, Fortran, Smalltalk, Lisp, ML, Ada, Eiffel, Pascal vagy Modula-2) használó programozó észre kell hogy vegye, hogy a C++ elõnyeinek kiaknázásához idõt kell szánnia a C++ programozási stílusok és módszerek megtanulására és megemésztésére. Ugyanez érvényes azon programozókra is, akik a C++ egy régebbi, kevésbé kifejezõképes változatát használták. Ha gondolkodás nélkül alkalmazzuk az egyik nyelvben hatékony eljárást egy másik nyelvben, rendszerint nehézkes, gyenge teljesítményû és nehezen módosítható kódot kapunk. Az ilyen kód írása is csalódást okoz, mivel minden sor kód és minden fordítási hiba arra emlékeztet, hogy a nyelv, amit használunk, más, mint „a régi nyelv”. Írhatunk Fortran, C, Smalltalk stb. stílusban bármely nyelven, de ez egy más filozófiájú nyelvben nem lesz sem kellemes, sem gazdaságos. Minden nyelv gazdag forrása lehet az ötleteknek, hogyan írjunk C++ programot. Az ötleteket azonban a C++ általános szerkezetéhez és típusrendszeréhez kell igazítani, hogy hatékony legyen az eltérõ környezetben. Egy nyelv alaptípusai felett csak pürroszi gyõzelmet arathatunk. A C++ támogatja a fokozatos tanulást. Az, hogy hogyan közelítsünk egy új nyelv tanulásához, attól függ, mit tudunk már és mit akarunk még megtanulni. Nem létezik egyetlen megközelítés sem, amely mindenkinek jó lenne. A szerzõ feltételezi, hogy az olvasó azért tanulja a C++-t, hogy jobb programozó és tervezõ legyen. Vagyis nem egyszerûen egy új nyelvtant akar megtanulni, mellyel a régi megszokott módon végzi a dolgokat, hanem új és jobb rendszerépítési módszereket akar elsajátítani. Ezt fokozatosan kell csinálni, mert minden új képesség megszerzése idõt és gyakorlást igényel. Gondoljuk meg, mennyi idõbe kerülne jól megtanulni egy új természetes nyelvet vagy megtanulni jól játszani egy hangszeren. Könnyen és gyorsan lehetünk jobb rendszertervezõk, de nem annyival könnyebben és gyorsabban, mint ahogy azt a legtöbben szeretnénk. Következésképpen a C++-t – gyakran valódi rendszerek építésére – már azelõtt használni fogjuk, mielõtt megértenénk minden nyelvi tulajdonságot és eljárást. A C++ – azáltal, hogy több programozási modellt is támogat (2. fejezet) – különbözõ szintû szakértelem esetén is támogatja a termékeny programozást. Minden új programozási stílus újabb eszközt ad eszköztárunkhoz, de mindegyik magában is hatékony és mindegyik fokozza a programozói hatékonyságot. A C++-t úgy alkották meg, hogy a fogalmakat nagyjából sorban egymás után tanulhassuk meg és eközben gyakorlati haszonra tehessünk szert. Ez fontos, mert a haszon a kifejtett erõfeszítéssel arányos.

1. Megjegyzések az olvasóhoz

9

A folytatódó vita során – kell-e C-t tanulni a C++-szal való ismerkedés elõtt – szilárd meggyõzõdésemmé vált, hogy legjobb közvetlenül a C++-ra áttérni. A C++ biztonságosabb, kifejezõbb, csökkenti annak szükségét, hogy a figyelmet alacsonyszintû eljárásokra irányítsuk. Könnyebb a C-ben a magasabb szintû lehetõségek hiányát pótló trükkösebb részeket megtanulni, ha elõbb megismertük a C és a C++ közös részhalmazát és a C++ által közvetlenül támogatott magasabb szintû eljárásokat. A „B” függelék vezérfonalat ad azoknak a programozóknak, akik a C++ ismeretében váltanak a C-re, például azért, hogy régebbi kódot kezeljenek. Több egymástól függetlenül fejlesztett és terjesztett C++-változat létezik. Gazdag választék kapható eszköztárakból, könyvtárakból, programfejlesztõ környezetekbõl is. Rengeteg tankönyv, kézikönyv, folyóirat, elektronikus hirdetõtábla, konferencia, tanfolyam áll rendelkezésünkre a C++ legfrissebb fejlesztéseirõl, használatáról, segédeszközeirõl, könyvtárairól, megvalósításairól és így tovább. Ha az olvasó komolyan akarja a C++-t használni, tanácsos az ilyen források között is böngészni. Mindegyiknek megvan a saját nézõpontja, elfogultsága, ezért használjunk legalább kettõt közülük. Például lásd [Barton,1994], [Booch,1994], [Henricson, 1997], [Koenig, 1997], [Martin, 1995].

1.3. A C++ jellemzõi
Az egyszerûség fontos tervezési feltétel volt; ahol választani lehetett, hogy a nyelvet vagy a fordítót egyszerûsítsük-e, az elõbbit választottuk. Mindenesetre nagy súlyt fektettünk arra, hogy megmaradjon a C-vel való összeegyeztethetõség, ami eleve kizárta a C nyelvtan kisöprését. A C++-nak nincsenek beépített magasszintû adattípusai, sem magasszintû alapmûveletei. A C++-ban például nincs mátrixtípus inverzió operátorral, karakterlánc-típus összefûzõ mûvelettel. Ha a felhasználónak ilyen típusra van szüksége, magában a nyelvben definíálhat ilyet. Alapjában véve a C++-ban a legelemibb programozási tevékenység az általános célú vagy alkalmazásfüggõ típusok létrehozása. Egy jól megtervezett felhasználói típus a beépített típusoktól csak abban különbözik, milyen módon határozták meg, abban nem, hogyan használják. A III. részben leírt standard könyvtár számos példát ad az ilyen típusokra és használatukra. A felhasználó szempontjából kevés a különbség egy beépített és egy standard könyvtárbeli típus között.

10

Bevezetés

A C++-ban kerültük az olyan tulajdonságokat, melyek akkor is a futási idõ növekedését vagy a tár túlterhelését okoznák, ha nem használjuk azokat. Nem megengedettek például azok a szerkezetek, melyek „háztartási információ” tárolását tennék szükségessé minden objektumban, így ha a felhasználó például két 16 bites mennyiségbõl álló szerkezetet ad meg, az egy 32 bites regiszterbe tökéletesen belefér. A C++-t hagyományos fordítási és futási környezetben való használatra tervezték, vagyis a UNIX rendszer C programozási környezetére. Szerencsére a C++ sohasem volt a UNIX-ra korlátozva, a UNIX-ot és a C-t csupán modellként használtuk a nyelv, a könyvtárak, a fordítók, a szerkesztõk, a futtatási környezetek stb. rokonsága alapján. Ez a minimális modell segítette a C++ sikeres elterjedését lényegében minden számítógépes platformon. Jó okai vannak azonban a C++ használatának olyan környezetekben, melyek jelentõsen nagyobb támogatásról gondoskodnak. Az olyan szolgáltatások, mint a dinamikus betöltés, a fokozatos fordítás vagy a típusmeghatározások adatbázisa, anélkül is jól használhatók, hogy befolyásolnák a nyelvet. A C++ típusellenõrzési és adatrejtési tulajdonságai a programok fordítási idõ alatti elemzésére támaszkodnak, hogy elkerüljék a véletlen adatsérüléseket. Nem gondoskodnak titkosításról vagy az olyan személyek elleni védelemrõl, akik szándékosan megszegik a szabályokat. Viszont szabadon használhatók és nem járnak a futási idõ vagy a szükséges tárhely növekedésével. Az alapelv az, hogy ahhoz, hogy egy nyelvi tulajdonság hasznos legyen, nemcsak elegánsnak, hanem valódi programon belül is elhelyezhetõnek kell lennie. A C++ jellemzõinek rendszerezett és részletes leírását lásd [Stroustrup, 1994].

1.3.1. Hatékonyság és szerkezet
A C++-t a C programozási nyelvbõl fejlesztettük ki és – néhány kivételtõl eltekintve – a C-t, mint részhalmazt, megtartotta. Az alapnyelvet, a C++ C részhalmazát, úgy terveztük, hogy nagyon szoros megfelelés van típusai, mûveletei, utasításai, és a számítógépek által közvetlenül kezelhetõ objektumok (számok, karakterek és címek) között. A new, delete, typeid, dynamic_cast és throw operátorok és – a try blokk – kivételével, az egyes C++ kifejezések és utasítások nem kívánnak futási idejû támogatást. A C++ ugyanolyan függvényhívási és visszatérési módokat használhat, mint a C – vagy még hatékonyabbakat. Amikor még az ilyen, viszonylag hatékony eljárások is túl költségesek, a C++ függvényt a fordítóval kifejtethetjük helyben (inline kód), így élvezhetjük a függvények használatának kényelmét, a futási idõ növelése nélkül.

1. Megjegyzések az olvasóhoz

11

A C egyik eredeti célja az assembly kód helyettesítése volt a legigényesebb rendszerprogramozási feladatokban. Amikor a C++-t terveztük, vigyáztunk, ne legyen megalkuvás e téren. A C és a C++ közti különbség elsõsorban a típusokra és adatszerkezetekre fektetett súly mértékében van. A C kifejezõ és elnézõ. A C++ még kifejezõbb. Ezért a jobb kifejezõképességért cserébe azonban nagyobb figyelmet kell fordítanunk az objektumok típusára. A fordító az objektumok típusának ismeretében helyesen tudja kezelni a kifejezéseket akkor is, ha egyébként kínos precizitással kellett volna megadni a mûveleteket. Az objektumok típusának ismerete arra is képessé teszi a fordítót, hogy olyan hibákat fedjen fel, melyek máskülönben egészen a tesztelésig vagy még tovább megmaradnának. Vegyük észre, hogy a típusrendszer használata függvényparaméterek ellenõrzésére – az adatok véletlen sérüléstõl való megvédésére, új típusok vagy operátorok elõállítására és így tovább – a C++-ban nem növeli a futási idõt vagy a szükséges helyet. A C++-ban a szerkezetre fektetett hangsúly tükrözi a C megtervezése óta megírt programok „súlygyarapodását”. Egy kis – mondjuk 1000 soros – programot megírhatunk „nyers erõvel”, még akkor is, ha felrúgjuk a jó stílus minden szabályát. Nagyobb programoknál ez egyszerûen nincs így. Ha egy 100 000 soros programnak rossz a felépítése, azt fogjuk találni, hogy ugyanolyan gyorsan keletkeznek az újabb hibák, mint ahogy a régieket eltávolítjuk. A C++-t úgy terveztük, hogy lehetõvé tegye nagyobb programok ésszerû módon való felépítését, így egyetlen személy is sokkal nagyobb kódmennyiséggel képes megbirkózni. Ezenkívül célkitûzés volt, hogy egy átlagos sornyi C++ kód sokkal többet fejezzen ki, mint egy átlagos Pascal vagy C kódsor. A C++ mostanra megmutatta, hogy túl is teljesíti ezeket a célkitûzéseket. Nem minden kódrészlet lehet jól szerkesztett, hardverfüggetlen vagy könnyen olvasható. A C++-nak vannak tulajdonságai, melyeket arra szántak, hogy közvetlen és hatékony módon kezelhessük a hardver szolgáltatásait, anélkül, hogy a biztonságra vagy az érthetõségre káros hatással lennénk. Vannak olyan lehetõségei is, melyekkel az ilyen kód elegáns és biztonságos felületek mögé rejthetõ. A C++ nagyobb programokhoz való használata természetszerûen elvezet a C++ nyelv programozócsoportok általi használatához. A C++ által a modularitásra, az erõsen típusos felületekre és a rugalmasságra fektetetett hangsúly itt fizetõdik ki. A C++-nak éppen olyan jól kiegyensúlyozott szolgáltatásai vannak nagy programok írására, mint bármely nyelvnek. Ahogy nagyobbak lesznek a programok, a fejlesztésükkel és fenntartásukkal, módosításukkal kapcsolatos problémák a „nyelvi probléma” jellegtõl az eszközök és a kezelés általánosabb problémái felé mozdulnak el. A IV. rész ilyen jellegû kérdéseket is tárgyal.

12

Bevezetés

Könyvünk kiemeli az általános célú szolgáltatások, típusok és könyvtárak készítésének módjait. Ezek éppúgy szolgálják a kis programok íróit, mint a nagy programokéit. Ezen túlmenõen, mivel minden bonyolultabb program sok, félig-meddig független részbõl áll, az ilyen részek írásához szükséges módszerek ismerete jó szolgálatot tesz minden alkalmazásprogramozónak. Az olvasó azt gondolhatja, a részletesebb típusszerkezetek használata nagyobb forrásprogramhoz vezet. A C++ esetében ez nem így van. Egy C++ program, amely függvényparaméter-típusokat vezet be vagy osztályokat használ, rendszerint kissé rövidebb, mint a vele egyenértékû C program, amely nem használja e lehetõségeket. Ott, ahol könyvtárakat használnak, egy C++ program sokkal rövidebb lesz, mint a megfelelõ C program, feltéve természetesen, hogy készíthetõ mûködõképes C-beli megfelelõ.

1.3.2. Filozófiai megjegyzés
A programozási nyelvek két rokon célt szolgálnak: a programozónak részben eszközt adnak, amellyel végrehajtható mûveleteket adhat meg, ugyanakkor egy sereg fogódzót is rendelkezésére bocsátanak, amikor arról gondolkodik, mit lehet tenni. Az elsõ cél ideális esetben „gépközeli” nyelvet kíván, amellyel a számítógép minden fontos oldala egyszerûen és hatékonyan kezelhetõ, a programozó számára ésszerû, kézenfekvõ módon. A C nyelvet elsõsorban ebben a szellemben tervezték. A második cél viszont olyan nyelvet követel meg, mely „közel van a megoldandó problémához”, hogy a megoldás közvetlenül és tömören kifejezhetõ legyen. A nyelv, melyben gondolkodunk/programozunk és a problémák, megoldások, melyeket el tudunk képzelni, szoros kapcsolatban állnak egymással. Ezért a nyelvi tulajdonságok megszorítása azzal a szándékkal, hogy kiküszöböljük a programozói hibákat, a legjobb esetben is veszélyes. A természetes nyelvekhez hasonlóan nagy elõnye van annak, ha az ember legalább két nyelvet ismer. A nyelv ellátja a programozót a megfelelõ eszközökkel, ha azonban ezek nem megfelelõek a feladathoz, egyszerûen figyelmen kívül hagyjuk azokat. A jó tervezés és hibamentesség nem biztosítható csupán az egyedi nyelvi tulajdonságok jelenlétével vagy távollétével. A típusrendszer különösen összetettebb feladatok esetében jelent segítséget. A C++ osztályai valóban erõs eszköznek bizonyultak.

1. Megjegyzések az olvasóhoz

13

1.4. Történeti megjegyzés
A szerzõ alkotta meg a C++-t, írta meg elsõ definícióit, és készítette el elsõ változatát. Megválasztotta és megfogalmazta a C++ tervezési feltételeit, megtervezte fõ szolgáltatásait, és õ volt a felelõs a C++ szabványügyi bizottságban a bõvítési javaslatok feldolgozásáért. Világos, hogy a C++ sokat köszönhet a C-nek [Kernighan, 1978]. A C néhány, a típusellenõrzés terén tapasztalt hiányosságát kivéve megmaradt, részhalmazként (lásd „B” függelék). Ugyancsak megmaradt az a C-beli szándék, hogy olyan szolgáltatásokra fektessen hangsúlyt, melyek elég alacsony szintûek ahhoz, hogy megbirkózzanak a legigényesebb rendszerprogramozási feladatokkal is. A C a maga részérõl sokat köszönhet õsének, a BCPL-nek [Richards, 1980]; a BCPL // megjegyzés-formátuma (újra) be is került a C++-ba. A C++ másik fontos forrása a Simula67 volt [Dahl, 1970] [Dahl, 1972]; az osztály fogalmát (a származtatott osztályokkal és virtuális függvényekkel) innen vettem át. A C++ operátor-túlterhelési lehetõsége és a deklarációk szabad elhelyezése az utasítások között az Algol68-ra emlékeztet [Woodward, 1974]. A könyv eredeti kiadása óta a nyelv kiterjedt felülvizsgálatokon és finomításokon ment keresztül. A felülvizsgálatok fõ területe a túlterhelés feloldása, az összeszerkesztési és tárkezelési lehetõségek voltak. Ezenkívül számos kisebb változtatás történt a C-vel való kompatibilitás növelésére. Számos általánosítás és néhány nagy bõvítés is belekerült: ezek a többszörös öröklés, a static és const tagfüggvények, a protected tagok, a sablonok, a kivételkezelés, a futási idejû típusazonosítás és a névterek. E bõvítések és felülvizsgálatok átfogó feladata a C++ olyan nyelvvé fejlesztése volt, mellyel jobban lehet könyvtárakat írni és használni. A C++ fejlõdésének leírását lásd [Stroustrup, 1994]. A sablonok (template) bevezetésének elsõdleges célja a statikus típusú tárolók (konténerek – list, vector, map) és azok hatékony használatának (általánosított vagy generikus programozás) támogatása, valamint a makrók és explicit típuskényszerítések (casting) szükségének csökkentése volt. Inspirációt az Ada általánosító eszközei (mind azok erõsségei, illetve gyengeségei), valamint részben a Clu paraméteres moduljai szolgáltattak. Hasonlóan, a C++ kivételkezelési eljárásainak elõdjei is többé-kevésbé az Ada [Ichbiah, 1979], a Clu [Liskov, 1979] és az ML [Wikstrm, 1987]. Az 1985-1995 között bevezetett egyéb fejlesztések – többszörös öröklés, tisztán virtuális függvények és névterek – viszont nem annyira más nyelvekbõl merített ötletek alapján születtek, inkább a C++ használatának tapasztalataiból leszûrt általánosítások eredményei. A nyelv korábbi változatait (összefoglaló néven az osztályokkal bõvített C-t [Stroustrup, 1994]) 1980 óta használják. Kifejlesztésében eredetileg szerepet játszott, hogy olyan eseményvezérelt szimulációkat szerettem volna írni, melyekhez a Simula67 ideális lett volna,

14

Bevezetés

ha eléggé hatékony. Az „osztályokkal bõvített C” igazi területét a nagy programok jelentették, ahol a lehetõ leggyorsabbnak kell lenni és a lehetõ legkevesebb helyet foglalni. Az elsõ változatokból még hiányzott az operátor-túlterhelés, valamint hiányoztak a referenciák, a virtuális függvények, a sablonok, a kivételek és sok egyéb. A C++-t nem kísérleti körülmények között elõször 1983-ban használták. A C++ nevet Rick Mascitti adta a nyelvnek az említett év nyarán. A név kifejezi mindazt a forradalmi újítást, amit az új nyelv a C-hez képest hozott: a ++ a C növelõ mûveleti jele. (A „C+”-t is használják, de az egy másik, független nyelv.) A C utasításformáit jól ismerõk rámutathatnak, hogy a „C++” kifejezés nem olyan „erõs”, mint a „++C”. Mindazonáltal a nyelv neve nem is D, hiszen a C-nek csupán bõvítésérõl van szó, amely az ott felmerült problémák elhárításához az eredeti nyelv szolgáltatásai közül egyet sem vet el. A C++ név más megközelítésû elemzéséhez lásd [Orwell, 1949, függelék]. A C++ megalkotásának fõ oka azonban az volt, hogy barátaimmal együtt nem szerettünk volna assembly, C vagy más modern, magas szintû nyelven programozni. Csak annyit akartunk elérni, hogy könnyebben és élvezetesebben írhassunk jól használható programokat. Kezdetben nem vetettük papírra rendszerezetten a fejlesztési terveket: egyszerre terveztünk, dokumentáltunk és alkottunk. Nem volt „C++ projekt” vagy „C++ tervezõbizottság”. A C++ a felhasználók tapasztalatai és a barátaimmal, munkatársaimmal folytatott viták során fejlõdött ki. A C++ késõbbi robbanásszerû elterjedése szükségszerûen változásokat hozott magával. Valamikor 1987-ben nyilvánvalóvá vált, hogy a C++ hivatalos szabványosítása immár elkerülhetetlen és haladéktalanul meg kell kezdenünk az ilyen irányú munka elõkészítését [Stroustrup, 1994]. Folyamatosan próbáltuk tartani a kapcsolatot mind hagyományos, mind elektronikus levélben, illetve személyesen, konferenciákat tartva a különbözõ C++ fordítók készítõivel és a nyelv fõ felhasználóival. Ebben a munkában nagy segítséget nyújtott az AT&T Bell Laboratories, lehetõvé téve, hogy vázlataimat és a C++ hivatkozási kézikönyv újabb és újabb változatait megoszthassam a fejlesztõkkel és felhasználókkal. Segítségük nem alábecsülendõ, ha tudjuk, hogy az említettek nagy része olyan vállalatoknál dolgozott, amelyek az AT&T vetélytársainak tekinthetõk. Egy kevésbé „felvilágosult” cég komoly problémákat okozhatott volna és a nyelv „tájszólásokra” töredezését idézte volna elõ, pusztán azáltal, hogy nem tesz semmit. Szerencsére a tucatnyi cégnél dolgozó mintegy száz közremûködõ elolvasta és megjegyzésekkel látta el a vázlatokat, melyekbõl az általánosan elfogadott hivatkozási kézikönyv és a szabványos ANSI C++ alapdokumentuma megszületett. A munkát segítõk neve megtalálható a The Annotated C++ Reference Manual-ban [Ellis, 1989]. Végül az ANSI X3J16 bizottsága a Hewlett-Packard kezdeményezésére 1989 decemberében összeült, 1991 júniusában pedig már

1. Megjegyzések az olvasóhoz

15

annak örülhettünk, hogy az ANSI (az amerikai nemzeti szabvány) C++ az ISO (nemzetközi) C++ szabványosítási kezdeményezés részévé vált. 1990-tõl ezek a szabványügyi bizottságok váltak a nyelv fejlesztésének és pontos körülhatárolásának fõ fórumaivá. Magam mindvégig részt vettem e bizottságok munkájában; a bõvítményekkel foglalkozó munkacsoport elnökeként közvetlenül feleltem a C++-t érintõ lényegbevágó módosítási javaslatok és az új szolgáltatások bevezetését szorgalmazó kérelmek elbírálásáért. Az elsõ szabványvázlat 1995 áprilisában került a nagyközönség elé, a végleges ISO C++ szabványt (ISO/IEC 14882) pedig 1998-ban fogadták el. A könyvben bemutatott kulcsfontosságú osztályok némelyike a C++-szal párhuzamosan fejlõdött. A complex, vector és stack osztályokat például az operátor-túlterhelési eljárásokkal egyidõben dolgoztam ki. A karakterlánc- és listaosztályokat (string, list) Jonathan Shopironak köszönhetjük (azért én is közremûködtem). Jonathan hasonló osztályai voltak az elsõk, amelyeket egy könyvtár részeként széles körben használtak; ezekbõl a régi kísérletekbõl fejlesztettük ki a C++ standard könyvtárának string osztályát. A [Stroustrup, 1987] és a §12.7[11] által leírt task könyvtár egyike volt az „osztályokkal bõvített C” nyelven elõször írt programoknak. (A könyvtárat és a kapcsolódó osztályokat én írtam a Simula stílusú szimulációk támogatásához.) A könyvtárat késõbb Jonathan Shopiro átdolgozta, és még ma is használják. Az elsõ kiadásban leírt stream könyvtárat én terveztem és készítettem el, Jerry Schwarz pedig Andrew Koenig formázó eljárása (§21.4.6) és más ötletek felhasználásával az e könyv 21. fejezetében bemutatandó iostreams könyvtárrá alakította. A szabványosítás során a könyvtár további finomításon esett át; a munka dandárját Jerry Schwarz, Nathan Myers és Norihiro Kumagai végezték. A sablonok lehetõségeit az Andrew Koenig, Alex Stepanov, személyem és mások által tervezett vector, map, list és sort sablonok alapján dolgoztuk ki. Alex Stepanovnak a sablonokkal történõ általánosított programozás terén végzett munkája emellett elvezetett a tárolók bevezetéséhez és a C++ standard könyvtárának egyes algoritmusaihoz is (§16.3, 17. fejezet, 18. fejezet §19.2). A számokkal végzett mûveletek valarray könyvtára (22. fejezet) nagyrészt Kent Budge munkája.

1.5. A C++ használata
A C++-t programozók százezrei használják, lényegében minden alkalmazási területen. Ezt a használatot támogatja tucatnyi független megvalósítás, többszáz könyvtár és tankönyv, számos mûszaki folyóirat, konferencia, és számtalan konzultáns. Oktatás és képzés minden szinten, széles körben elérhetõ.

16

Bevezetés

A régebbi alkalmazások erõsen a rendszerprogramozás felé hajlottak. Több nagy operációs rendszer íródott C++-ban: [Campbell, 1987] [Rozier, 1988] [Hamilton, 1993] [Berg, 1995] [Parrington, 1995] és sokan mások kulcsfontosságú részeket írtak. A szerzõ lényegesnek tekinti a C++ engedmény nélküli gépközeliségét, ami lehetõvé teszi, hogy C++-ban írhassunk eszközmeghajtókat és más olyan programokat, melyek valósidejû, közvetlen hardverkezelésre támaszkodnak. Az ilyen kódban a mûködés kiszámíthatósága legalább annyira fontos, mint a sebesség és gyakran így van az eredményül kapott rendszer tömörségével is. A C++-t úgy terveztük, hogy minden nyelvi tulajdonság használható legyen a komoly idõbeli és helyfoglalásbeli megszorításoknak kitett kódban is. [Stroustrup, 1994, §4.5]. A legtöbb programban vannak kódrészletek, melyek létfontosságúak az elfogadható teljesítmény tekintetében. A kód nagyobb részét azonban nem ilyen részek alkotják. A legtöbb kódnál a módosíthatóság, a könnyû bõvíthetõség és tesztelhetõség a kulcskérdés. A C++ ilyen téren nyújtott támogatása vezetett el széleskörû használatához ott, ahol kötelezõ a megbízhatóság, és ahol az idõ haladtával jelentõsen változnak a követelmények. Példaként a bankok, a kereskedelem, a biztosítási szféra, a távközlés és a katonai alkalmazások szolgálhatnak. Az USA távolsági telefonrendszere évek óta a C++-ra támaszkodik és minden 800-as hívást (vagyis olyan hívást, ahol a hívott fél fizet) C++ program irányít [Kamath, 1993]. Számos ilyen program nagy méretû és hosszú életû. Ennek eredményképpen a stabilitás, a kompatibilitás és a méretezhetõség állandó szempontok a C++ fejlesztésében. Nem szokatlanok a millió soros C++ programok. A C-hez hasonlóan a C++-t sem kifejezetten számokkal végzett mûveletekhez tervezték. Mindazonáltal sok számtani, tudományos és mérnöki számítást írtak C++-ban. Ennek fõ oka, hogy a számokkal való hagyományos munkát gyakran grafikával és olyan számításokkal kell párosítani, melyek a hagyományos Fortran mintába nem illeszkedõ adatszerkezetekre támaszkodnak [Budge, 1992] [Barton, 1994]. A grafika és a felhasználói felület olyan területek, ahol erõsen használják a C++-t. Bárki, aki akár egy Apple Macintosht, akár egy Windowst futtató PC-t használt, közvetve a C++-t használta, mert e rendszerek elsõdleges felhasználói felületeit C++ programok alkotják. Ezenkívül a UNIX-ban az X-et támogató legnépszerûbb könyvtárak némelyike is C++-ban íródott. Ilyenformán a C++ közösen választott nyelve annak a hatalmas számú alkalmazásnak, ahol a felhasználói felület kiemelt fontosságú. Mindezen szempontok mellett lehet, hogy a C++ legnagyobb erõssége az a képessége, hogy hatékonyan használható olyan programokhoz, melyek többféle alkalmazási területen igényelnek munkát. Egyszerû olyan alkalmazást találni, melyben LAN és WAN hálózatot, számokkal végzett mûveleteket, grafikát, felhasználói kölcsönhatást és adatbázis-hozzáférést használunk. Az ilyen alkalmazási területeket régebben különállóknak tekintették és általában különálló fejlesztõközösségek szolgálták ki, többféle programozási nyelvet használva.

1. Megjegyzések az olvasóhoz

17

A C++-t széles körben használják oktatásra és kutatásra. Ez néhány embert meglepett, akik – helyesen – rámutattak, hogy a C++ nem a legkisebb és legtisztább nyelv, amelyet valaha terveztek. Mindazonáltal a C++ ♦ elég tiszta ahhoz, hogy az alapfogalmakat sikeresen tanítsuk, ♦ elég valószerû, hatékony és rugalmas az igényes projektekhez is, ♦ elérhetõ olyan szervezetek és együttmûködõ csoportok számára, melyek eltérõ fejlesztési és végrehajtási környezetekre támaszkodnak, ♦ elég érthetõ ahhoz, hogy bonyolult fogalmak és módszerek tanításának hordozója legyen és ♦ elég „kereskedelmi”, hogy segítse a tanultak gyakorlatban való felhasználását. A C++ olyan nyelv, mellyel gyarapodhatunk.

1.6. C és C++
A C++ alapnyelvének a C nyelvet választottuk, mert ♦ ♦ ♦ ♦ sokoldalú, tömör, és viszonylag alacsony szintû, megfelel a legtöbb rendszerprogramozási feladatra, mindenütt és mindenen fut, és illeszkedik a UNIX programozási környezetbe.

A C-nek megvannak a hibái, de egy újonnan készített nyelvnek is lennének, a C problémáit pedig már ismerjük. Nagy jelentõsége van, hogy C-vel való munka vezetett el a hasznos (bár nehézkes) eszközzé váló „osztályokkal bõvített C”-hez, amikor elõször gondoltunk a C bõvítésére Simula-szerû osztályokkal. Ahogy szélesebb körben kezdték használni a C++-t és az általa nyújtott, a C lehetõségeit felülmúló képességek jelentõsebbek lettek, újra és újra felmerült a kérdés, megtartsuk-e a két nyelv összeegyeztethetõségét. Világos, hogy néhány probléma elkerülhetõ lett volna, ha némelyik C örökséget elutasítjuk (lásd pl. [Sethi, 1981]). Ezt nem tettük meg, a következõk miatt:

18

Bevezetés

1. Több millió sornyi C kód van, mely élvezheti a C++ elõnyeit, feltéve, hogy szükségtelen a C-rõl C++-ra való teljes átírás. 2. Több millió sornyi C-ben írt könyvtári függvény és eszközillesztõ kód van, melyet C++ programokból/programokban használni lehet, feltéve, hogy a C++ program összeszerkeszthetõ és formailag összeegyeztethetõ a C programmal. 3. Programozók százezrei léteznek, akik ismerik a C-t és ezért csak a C++ új tulajdonságait kell megtanulniuk, vagyis nem kell az alapokkal kezdeniük. 4. A C++-t és a C-t ugyanazok, ugyanazokon a rendszereken fogják évekig használni, tehát a különbségek vagy nagyon nagyok, vagy nagyon kicsik lesznek, hogy a hibák és a keveredés lehetõsége a lehetõ legkisebbre csökkenjen. A C++-t felülvizsgáltuk, hogy biztosítsuk, hogy azon szerkezetek, melyek mind a C-ben, mind a C++-ban megengedettek, mindkét nyelvben ugyanazt jelentsék (§B.2). A C nyelv maga is fejlõdött, részben a C++ fejlesztésének hatására [Rosler, 1984]. Az ANSI C szabvány [C,1990] a függvénydeklarációk formai követelményeit az „osztályokkal bõvített C”-bõl vette át. Az átvétel mindkét irányban elõfordul: a void* mutatótípust például az ANSI C-hez találták ki, de elõször a C++-ban valósították meg. Mint ahogy e könyv elsõ kiadásában megígértük, a C++-t felülvizsgáltuk, hogy eltávolítsuk az indokolatlan eltéréseket, így a C++ ma jobban illeszkedik a C-hez, mint eredetileg. Az elképzelés az volt, hogy a C++ olyan közel legyen az ANSI C-hez, amennyire csak lehetséges – de ne közelebb [Koenig, 1989]. A száz százalékos megfelelõség soha nem volt cél, mivel ez megalkuvást jelentene a típusbiztonságban, valamint a felhasználói és beépített típusok zökkenésmentes egyeztetésében. A C tudása nem elõfeltétele a C++ megtanulásának. A C programozás sok olyan módszer és trükk használatára biztat, melyeket a C++ nyelvi tulajdonságai szükségtelenné tettek. Az explicit típuskényszerítés például ritkábban szükséges a C++-ban, mint a C-ben (§1.6.1). A jó C programok azonban hajlanak a C++ programok felé. A Kernighan és Ritchie féle A C programozási nyelv (Mûszaki könyvkiadó, második kiadás, 1994) [Kernighan,1988] címû kötetben például minden program C++ program. Bármilyen statikus típusokkal rendelkezõ nyelvben szerzett tapasztalat segítséget jelent a C++ tanulásánál.

1.6.1. Javaslatok C programozóknak
Minél jobban ismeri valaki a C-t, annál nehezebbnek látja annak elkerülését, hogy C stílusban írjon C++ programot, lemondva ezáltal a C++ elõnyeirõl. Kérjük, vessen az olvasó egy pillantást a „B” függelékre, mely leírja a C és a C++ közti különbségeket. Íme néhány terület, ahol a C++ fejlettebb, mint a C:

1. Megjegyzések az olvasóhoz

19

1. A C++-ban a makrókra majdnem soha sincs szükség. A névvel ellátott állandók meghatározására használjunk konstanst (const) (§5.4) vagy felsorolást (enum) (§4.8), a függvényhívás okozta többletterhelés elkerülésére helyben kifejtett függvényeket (§7.1.1), a függvény- és típuscsaládok leírására sablonokat (13. fejezet), a névütközések elkerülésére pedig névtereket (§8.2). 2. Ne vezessünk be egy változót, mielõtt szükség van rá, így annak azonnal kezdõértéket is adhatunk. Deklaráció bárhol lehet, ahol utasítás lehet (§6.3.1), így for utasítások (§6.3.3) és elágazások feltételeiben (§6.3.2.1) is. 3. Ne használjunk malloc()-ot, a new operátor (§6.2.6) ugyanazt jobban elvégzi. A realloc() helyett próbáljuk meg a vector-t (§3.8). 4. Próbáljuk elkerülni a void* mutatókkal való számításokat, az uniókat és típuskonverziókat (típusátalakításokat), kivéve, ha valamely függvény vagy osztály megvalósításának mélyén találhatók. A legtöbb esetben a típuskonverzió a tervezési hiba jele. Ha feltétlenül erre van szükség, az „új cast-ok” (§6.2.7) egyikét próbáljuk használni szándékunk pontosabb leírásához. 5. Csökkentsük a lehetõ legkevesebbre a tömbök és a C stílusú karakterláncok használatát. A C++ standard könyvtárának string (§3.5) és vector (§3.7.1) osztályai a hagyományos C stílushoz képest gyakrabban használhatók a programozás egyszerûbbé tételére. Általában ne próbáljunk magunk építeni olyat, ami megvan a standard könyvtárban. Ahhoz, hogy eleget tegyünk a C szerkesztési szabályainak, a C++ függvényeket úgy kell megadnunk, hogy szerkesztésük C módú legyen. (§9.2.4). A legfontosabb, hogy úgy próbáljunk egy programot elképzelni, mint egymással kölcsönhatásban lévõ fogalmakat, melyeket osztályok és objektumok képviselnek, nem pedig úgy, mint egy halom adatszerkezetet, a bitekkel zsonglõrködõ függvényekkel.

1.6.2. Javaslatok C++ programozóknak
Sokan már egy évtized óta használják a C++-t. Még többen használják egyetlen környezetben és tanultak meg együtt élni a korai fordítók és elsõ generációs könyvtárak miatti korlátozásokkal. Ami a tapasztalt C++ programozók figyelmét gyakran elkerüli, nem is annyira az új eszközök megjelenése, mint inkább ezen eszközök kapcsolatainak változása, ami alapjaiban új programozási módszereket követel meg. Más szóval, amire annak idején nem gondoltunk vagy haszontalannak tartottunk, ma már kiváló módszerré válhatott, de ezekre csak az alapok újragondolásával találunk rá.

20

Bevezetés

Olvassuk át a fejezeteket sorban. Ha már ismerjük a fejezet tartalmát, gondolatban ismételjük át. Ha még nem ismerjük, valami olyat is megtanulhatunk, amire eredetileg nem számítottunk. Én magam elég sokat tanultam e könyv megírásából, és az a gyanúm, hogy kevés C++ programozó ismeri az összes itt bemutatott összes eszközt és eljárást. Ahhoz, hogy helyesen használjunk egy nyelvet, behatóan kell ismernünk annak eszközeit, módszereit. Felépítése és példái alapján ez a könyv megfelelõ rálátást biztosít.

1.7. Programozási megfontolások a C++-ban
A programtervezést ideális esetben három fokozatban közelítjük meg. Elõször tisztán érthetõvé tesszük a problémát (elemzés, analízis), ezután azonosítjuk a fõ fogalmakat, melyek egy megoldásban szerepelnek (tervezés), végül a megoldást egy programban fejezzük ki (programozás). A probléma részletei és a megoldás fogalmai azonban gyakran csak akkor válnak tisztán érthetõvé, amikor egy elfogadhatóan futtatható programban akarjuk kifejezni azokat. Ez az, ahol számít, milyen programozási nyelvet választunk. A legtöbb alkalmazásban vannak fogalmak, melyeket nem könnyû a kapcsolódó adatok nélkül az alaptípusok egyikével vagy függvénnyel ábrázolni. Ha adott egy ilyen fogalom, hozzunk létre egy osztályt, amely a programban képviselni fogja. A C++ osztályai típusok, melyek meghatározzák, hogyan viselkednek az osztályba tartozó objektumok, hogyan jönnek létre, hogyan kezelhetõk és hogyan szûnnek meg. Az osztály leírhatja azt is, hogyan jelennek meg az objektumok, bár a programtervezés korai szakaszában ez nem szükségszerûen fõ szempont. Jó programok írásánál az a legfontosabb, hogy úgy hozzunk létre osztályokat, hogy mindegyikük egyetlen fogalmat, tisztán ábrázoljon. Ez általában azt jelenti, hogy a következõ kérdésekre kell összpontosítani: Hogyan hozzuk létre az osztály objektumait? Másolhatók-e és/vagy megsemmisíthetõk-e az osztály objektumai? Milyen mûveletek alkalmazhatók az objektumokra? Ha nincsenek jó válaszok e kérdésekre, az a legvalószínûbb, hogy a fogalom nem „tiszta”. Ekkor jó ötlet, ha tovább gondolkodunk a problémán és annak javasolt megoldásán, ahelyett, hogy azonnal elkezdenénk a kód kidolgozását. A legkönnyebben kezelhetõ fogalmak azok, amelyeknek hagyományos matematikai megfogalmazásuk van: mindenfajta számok, halmazok, geometriai alakzatok stb. A szövegközpontú bemenet és kimenet, a karakterláncok, az alaptárolók, az ezekre a tárolókra alkalmazható alap-algoritmusok, valamint néhány matematikai osztály a C++ standard könyvtárának részét képezik (3. fejezet, §16.1.2). Ezenkívül elképesztõ választékban léteznek könyvtárak, melyek általános és részterületekre szakosodott elemeket támogatnak.

1. Megjegyzések az olvasóhoz

21

Az egyes fogalmak (és a hozzájuk kapcsolódó elemek) nem légüres térben léteznek, mindig rokonfogalmak csoportjába tartoznak. Az osztályok közti kapcsolatok szervezése egy programon belül – vagyis az egy megoldásban szereplõ különbözõ elemek közti pontos kapcsolatok meghatározása – gyakran nehezebb, mint az egyes osztályokat kijelölni. Jobb, ha az eredmény nem rendetlenség, melyben minden osztály függ minden másiktól. Vegyünk két osztályt: A-t és B-t. Az olyan kapcsolatok, mint az „A hív B-beli függvényeket”, „A létrehoz B-ket” és „A-nak van egy B tagja” ritkán okoznak nagy problémát, míg az olyanok, mint az „A használ B-beli adatot” rendszerint kiküszöbölhetõk. Az összetettség kezelésének egyik legerõsebb eszköze a hierarchikus rendezés, vagyis a rokon elemek faszerkezetbe szervezése, ahol a fa gyökere a legáltalánosabb elem. A C++-ban a származtatott osztályok ilyen fastruktúrákat képviselnek. Egy program gyakran úgy szervezhetõ, mint fák halmaza, vagy mint osztályok irányított körmentes gráfja. Vagyis a programozó néhány alaposztályt hoz létre, melyekhez saját származtatott osztályaik halmaza tartozik. Az elemek legáltalánosabb változatának (a bázisosztálynak) a kezelését végzõ mûveletek meghatározására a virtuális függvényeket (§2.5.5, §12.2.6) használhatjuk. Szükség esetén ezen mûveletek megvalósítása az egyedi esetekben (a származtatott osztályoknál) finomítható. Néha még az irányított körmentes gráf sem látszik kielégítõnek a programelemek szervezésére; egyes elemek kölcsönös összefüggése öröklöttnek tûnik. Ilyen esetben megpróbáljuk a ciklikus függõségeket behatárolni, hogy azok ne befolyásolják a program átfogó rendszerét. Ha nem tudjuk kiküszöbölni vagy behatárolni az ilyen kölcsönös függéseket, valószínû, hogy olyan gondban vagyunk, melybõl nincs programozási nyelv, amely kisegítene. Hacsak ki nem tudunk eszelni könnyen megállapítható kapcsolatokat az alapfogalmak között, valószínû, hogy a program kezelhetetlenné válik. A függõségi gráfok kibogozásának egyik eszköze a felület (interfész) tiszta elkülönítése a megvalósítástól (implementáció). A C++ erre szolgáló legfontosabb eszközei az absztrakt osztályok (§2.5.4, §12.3). A közös tulajdonságok kifejezésének másik formája a sablon (template, §2.7, 13. fejezet). Az osztálysablonok osztályok családját írják le. Egy listasablon például a „T elemek listáját” határozza meg, ahol „T” bármilyen típus lehet. A sablon tehát azt adja meg, hogyan hozhatunk létre egy típust egy másik típus, mint paraméter átadásával. A legszokásosabb sablonok az olyan tárolóosztályok, mint a listák, tömbök és asszociatív tömbök, valamint az ilyen tárolókat használó alap-algoritmusok. Rendszerint hiba, ha egy osztály és a vele kapcsolatos függvények paraméterezését öröklést használó típussal fejezzük ki. A legjobb sablonokat használni.

22

Bevezetés

Emlékeztetünk arra, hogy sok programozási feladat egyszerûen és tisztán elvégezhetõ elemi típusok, adatszerkezetek, világos függvények és néhány könyvtári osztály segítségével. Az új típusok leírásában szereplõ teljes apparátust nem szabad használni, kivéve, ha valóban szükség van rá. A „Hogyan írjunk C++-ban jó programot?” nagyon hasonlít a „Hogyan írjunk jó prózát?” kérdésre. Két válasz van: „Tudnunk kell, mit akarunk mondani” és „Gyakoroljunk. Színleljük a jó írást.” Mind a kettõ éppúgy helytálló a C++, mint bármely természetes nyelv esetében – és tanácsukat éppolyan nehéz követni.

1.8. Tanácsok
Íme néhány „szabály”, amelyet figyelembe vehetünk a C++ tanulásakor. Ahogy jártasabbak leszünk, továbbfejleszthetjük ezeket saját programfajtáinkhoz, programozási stílusunkhoz illeszkedõen. A szabályok szándékosan nagyon egyszerûek, így nélkülözik a részleteket. Ne vegyük õket túlzottan komolyan: a jó programok írásához elsõsorban intelligencia, ízlés, türelem kell. Ezeket nem fogjuk elsõre elsajátítani. Kísérletezzünk! [1] Amikor programozunk, valamilyen probléma megoldására született ötleteink konkrét megvalósítását hozzuk létre. Tükrözze a program szerkezete olyan közvetlenül ezeket az ötleteket, amennyire csak lehetséges: a) Ha valamire úgy gondolunk, mint külön ötletre, tegyük osztállyá. b) Ha különálló egyedként gondolunk rá, tegyük egy osztály objektumává. c) Ha két osztálynak van közös felülete, tegyük ezt a felületet absztrakt osztállyá. d) Ha két osztály megvalósításában van valami közös, tegyük bázisosztállyá e közös tulajdonságokat. e) Ha egy osztály objektumok tárolója, tegyük sablonná. f) Ha egy függvény egy tároló számára való algoritmust valósít meg, tegyük függvénysablonná, mely egy tárolócsalád algoritmusát írja le. g) Ha osztályok, sablonok stb. egy halmazán belül logikai rokonság van, tegyük azokat közös névtérbe. [2] Ha olyan osztályt hozunk létre, amely nem matematikai egyedet ír le (mint egy mátrix vagy komplex szám) vagy nem alacsonyszintû típust (mint egy láncolt lista) a) ne használjunk globális adatokat (használjunk tagokat), b) ne használjunk globális függvényeket,

1. Megjegyzések az olvasóhoz

23

c) ne használjunk nyilvános adattagokat, d) ne használjunk „barát” (friend) függvényeket, kivéve a) vagy c) elkerülésére, e) ne tegyünk egy osztályba típusazonosító mezõket, használjunk inkább virtuális függvényeket, f) ne használjunk helyben kifejtett függvényeket, kivéve ha jelentõs optimalizálásról van szó. Egyedi és részletesebb gyakorlati szabályokat az egyes fejezetek „Tanácsok” részében találhatunk. Emlékeztetjük az olvasót, hogy ezek a tanácsok csak útmutatásul szolgálnak, nem megváltoztathatatlan törvények. A tanácsokat csak ott kövessük, ahol értelme van. Nincs pótszere az intelligenciának, a tapasztalatnak, a józan észnek és a jó ízlésnek. A „soha ne tegyük ezt” alakú szabályokat haszontalannak tekintem. Következésképpen a legtöbb tanácsot javaslatként fogalmaztam meg; azt írtam le, mit tegyünk. A „negatív javaslatokat” pedig nem úgy kell érteni, mint tiltásokat: nem tudok a C++ olyan fõ tulajdonságáról, melyet ne láttam volna jól felhasználni. A „Tanácsok” nem tartalmaznak magyarázatokat. Helyette minden tanács mellett hivatkozás található a könyv megfelelõ részére. Ahol negatív tanács szerepel, a hivatkozott rész rendszerint alternatív javaslatot tartalmaz.

1.8.1. Hivatkozások
Barton, 1994 Berg, 1995 Booch, 1994 Budge, 1992 John J. Barton and Lee R. Nackman: Scientific and Engineering C++. AddisonWesley. Reading, Mass. 1994. ISBN 1-201-53393-6. William Berg, Marshall Cline, and Mike Girou: Lessons Learned from the OS/400 OO Project. CACM. Vol. 38 No. 10. October 1995. Grady Booch: Object-Oriented Analysis and Design. Benjamin/Cummings. Menlo Park, Calif. 1994. ISBN 0-8053-5340-2. Kent Budge, J. S. Perry, an A. C. Robinson: High-Performance Scientific Computation using C++. Proc. USENIX C++Conference. Portland, Oregon. August 1992. X3 Secretariat: Standard - The C Language. X3J11/90-013. ISO Standard ISO/IEC 9899. Computer and Business Equipment Manufacturers Association. Washington, DC, USA. X+ Secretariat: International Standard- The C++ Language. X3J16-14882. Information Technology Council (NSITC). Washington, DC, USA. Roy Campbell, et al.: The Design of a Multirocessor Operating System. Proc. USENIX C++ Conference. Santa Fe, New Mexico. November 1987. James O. Coplien and Douglas C. Schmidt (editors): Pattern Languages of Program Design. Addison-Wesley. Reading, Mass. 1995. ISBN 1-201-60734-4.

C, 1990

C++, 1998 Campbell, 1987 Coplien, 1995

24

Bevezetés

O-J. Dahl, B. Myrhaug, and K. Nygaard: SIMULA Common Base Language. Norwegian Computing Center S-22. Oslo, Norway. 1970. Dahl, 1972 O-J. Dahl, and C. A. R. Hoare: Hierarchical Program Consturction in Structured Programming. Academic Press, New York. 1972. Ellis, 1989 Margaret A. Ellis and Bjarne Stroustrup: The Annotated C++ Reference Manual. Addison-Wesley. Reading, Mass. 1990. ISBN 0-201-51459-1. Gamma, 1995 Erich Gamma, et al.: Design Patterns. Addison-Wesley. Reading, Mass. 1995. ISBN 0-201-63361-2. Goldberg, 1983 A. Goldberg and D. Robson: SMALLTALK- 80 - The Language and Its Implementation. Addison-Wesley. Reading, Mass. 1983. Griswold, 1970 R. E. Griswold, et al.: The Snobol4 Programming Language. Prentice-Hall. Englewood Cliffs, New Jersey. 1970. Griswold, 1983 R. E. Grisswold and M. T. Griswold: The ICON Programming Language. PrenticeHall. Englewood Cliffs, New Jersey. 1983. Hamilton, 1993 G. Hamilton and P. Kougiouris: The Spring Nucleus: A Microkernel for Objects. Proc. 1993 Summer USENIX Conference. USENIX. Henricson, 1997 Mats Henricson and Erik Nyquist: Industrial Strenght C++: Rules and Recommendations. Prentice-Hall. Englewood Cliffs, New Jersey. 1997. ISBN 0-13120965-5. Ichbiah, 1979 Jean D. Ichbiah, et al.: Rationale for the Design of the ADA Programming Language. SIGPLAN Notices. Vol. 14 No. 6. June 1979. Kamath, 1993 Yogeesh H. Kamath, Ruth E. Smilan, and Jean G. Smith: Reaping Benefits with Object-Oriented Technology. AT&T Technical Journal. Vol. 72 No. 5. September/October 1993. Kernighan, 1978 Brian W. Kernighan and Dennis M. Ritchie: The C Programming Language. Prentice-Hall. Englewood Cliffs, New Jersey. 1978. Kernighan, 1988 Brian W. Kernighan and Dennis M. Ritchie: The C Programming Language (Second Edition). Prentice-Hall. Enlewood Cliffs, New Jersey. 1988. ISBN 0-13110362-8. Koenig, 1989 Andrew Koenig and Bjarne Stroustrup: C++: As close to C as possible - but no closer. The C++ Report. Vol. 1 No. 7. July 1989. Koenig, 1997 Andrew Koenig and Barbara Moo: Ruminations on C++. Addison Wesley Longman. Reading, Mass. 1997. ISBN 1-201-42339-1. Knuth, 1968 Donald Knuth: The Art of Computer Programming. Addison-Wesley. Reading, Mass. Liskowv, 1979 Barbara Liskov et al.: Clu Reference Manual. MIT/LCS/TR-225. MIT Cambridge. Mass. 1979. Martin, 1995 Robert C. Martin: Designing Object-Oriented C++ Applications Using the Booch Method. Prentice-Hall. Englewood Cliffs, New Jersey. 1995. ISBN 0-13-203837-4. Orwell, 1949 George Orwell: 1984. Secker and Warburg. London. 1949.

Dahl, 1970

1. Megjegyzések az olvasóhoz

25

Parrington, 1995 Graham Parrington et al.: The Design and Implementation of Arjuna. Computer Systems. Vol. 8 No. 3. Summer 1995. Richards, 1980 Martin Richards and Colin Whitby-Strevens: BCPL - The Language and Its Compiler. Cambridge University Press, Cambridge. England. 1980. ISBN 0-52121965-5. Rosler, 1984 L. Rosler: The Evolution of C - Past and Future. AT&T Bell Laboratories Technical Journal. Vol. 63 No. 8. Part 2. October 1984. Rozier, 1988 M. Rozier, et al.: CHORUS Distributed Operating Systems. Computing Systems. Vol. 1 no. 4. Fall 1988. Sethi, 1981 Ravi Sethi: Uniform Syntax for Type Expressions and Declarations. Software Practice & Experience. Vol. 11. 1981. Stepanov, 1994 Alexander Stepanov and Meng Lee: The Standard Template Library. HP Labs Technical Report HPL-94-34 (R. 1). August, 1994. Stroustrup, 1986 Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley. Reading, Mass. 1986. ISBN 0-201-12078-X. Stroustrup, 1987 Bjarne Stroustrup and Jonathan Shopiro: A Set of C Classes for Co-Routine Style Programming. Proc. USENIX C++ conference. Santa Fe, New Mexico. November 1987. Stroustrup, 1991 Bjarne Stroustrup: The C++ Programming Language (Second Edition) AddisonWesley. Reading, Mass. 1991. ISBN 0-201-53992-6. Strostrup, 1994 Bjarne Stroustrup: The Design and Evolution of C++. Addison-Wesley. Reading, Mass. 1994. ISBN 0-201-54330-3. Tarjan, 1983 Robert E. Tarjan: Data Structures and Network Algorithms. Society for Industrial and Applied Mathematics. Philadelphia, Penn. 1983. ISBN 0-898-71187-8. Unicode, 1996 The Unicode Consortium: The Unicode Standard, Version 2.0. Addison-Wesley Developers Press. Reading, Mass. 1996. ISBN 0-201-48345-9. UNIX, 1985 UNIX Time-Sharing System: Programmer's Manual. Research Version, Tenth Edition. AT&T Bell Laboratories, Murray Hill, New Jersey. February 1985. Wilson, 1996 Gregory V. Wilson and Paul Lu (editors): Parallel Progrmming Using C++. The MIT Press. Cambridge. Mass. 1996. ISBN 0-262-73118-5. Wikström, 1987 Ake Wikström: Functional Programming Using ML. Prentice-Hall. Englewood Cliffs, New Jersey. 1987. Woodward, 1974 P. M. Woodward and S. G. Bond: Algol 68-R Users Guide. Her Majesty's Stationery Office. London. England. 1974.

Mi a C++? A C++ általános célú programozási nyelv.1. támogatja az objektumorientált programozást. II. rész – ford. valamint az általánosított programozást.2 Kirándulás a C++-ban „Az elsõ tennivalónk: öljünk meg minden törvénytudót” (Shakespeare: VI. . Henrik. Németh László) Mi a C++? • Programozási megközelítések • Eljárásközpontú programozás • Modularitás • Külön fordítás • Kivételkezelés • Elvont adatábrázolás • Felhasználói típusok • Konkrét típusok • Absztrakt típusok • Virtuális függvények • Objektumorientált programozás • Általánosított programozás • Tárolók • Algoritmusok • Nyelv és programozás • Tanácsok 2. melynek fõ alkalmazási területe a rendszerprogramozás és ♦ ♦ ♦ ♦ egy jobbfajta C. támogatja az elvont adatábrázolást.

melyek által az adott stílus használata kényelmes (könnyû. Mindenesetre. tegye meg magának azt a szívességet.2. Késõbb mindenre részletes magyarázatot kap. mint a könyvtárak és programozási környezetek. mit jelentenek a fentiek. ha hiányzik az átfogó szemléletünk a nyelvrõl és használatának alapvetõ módszereirõl. mivel ezek a nyelvek nem támogatják közvetlenül az említett megközelítéseket. melyek védelmet adnak a stílustól való akaratlan eltérés ellen. Itt fontos megkülönböztetnünk két fogalmat: egy nyelvrõl akkor mondjuk. hogy késõbb visszatér rájuk. A típusellenõrzés erre a legkézenfekvõbb példa. Az egyes programozási módok támogatása nem csak az adott megközelítés közvetlen használatát lehetõvé tévõ nyelvi szolgáltatások magától értetõdõ formájában rejlik. Ha az olvasó túl elnagyoltnak találja e fejezet némelyik részének tárgyalásmódját. ha kivételes erõfeszítés vagy ügyesség kell az ilyen programok írásához. anélkül. biztonságos és hatékony) lesz. Lehet strukturált programot írni Fortran77-ben és objektumközpontút C-ben. amely az objektumokat középpontba helyezõ programozási stílust támogató eljárásokról gondoskodik. olyan programozási nyelvet kell hogy jelentsen. hogy az adott megközelítést használjuk. A nyelvi tulajdonságok részletes megértése – még ha a nyelv összes tulajdonságáé is – nem ellensúlyozhatja azt. de a kétértelmûség észlelése és a futási idejû ellenõrzések szintén a programozási módok nyelvi támogatásához tartoznak. A támogatás hiányzik. egyszerûen ugorja át és lépjen tovább.28 Bevezetés Ez a fejezet elmagyarázza. de szükségtelenül nehezen. ha átugrik részeket a fejezetben. Programozási megközelítések Az objektumorientált (objektumközpontú) programozás egy programozási mód – a „jó” programok írása közben felmerülõ sereg probléma megoldásának egy megközelítése (paradigma). további támogatást adnak az egyes megközelítési módokhoz. Ha az „objektumorientált programozási nyelv” szakkifejezés egyáltalán jelent valamit. A nyelven kívüli szolgáltatások. nem pedig a C++ programozás elkezdéséhez szükséges részletes információt adni az olvasónak. ha olyan szolgáltatásai vannak. hogy belemenne a nyelv meghatározásának finomabb részleteibe. . ekkor a nyelv csupán megengedi. hanem a fordítási/futási idõbeni ellenõrzések finomabb formáiban. hogy támogat egy programozási stílust. 2. Célja általános áttekintést adni a C++-ról és használatának fõ módszereirõl.

Sok példa van ennek az ellenkezõjére is. csak azért. A következõ kettõ a minimalizmus gondolatának kifejezése. A hangsúly a tervezési megközelítéseken és a programok szerkezeti felépítésén van. ha kifejezetten használja azt egy program írásához. A nyelvi tulajdonságok bemutatása nem teljes.2. hanem inkább az. A tulajdonságokat egymással párosítva is lehessen használni. mégpedig az e megszorítások mellett támogatott hagyományos C programozási módszereken kívül. és mindegyik egy bevált tervezési módot tükröz. A fentiek közül az elsõ elv az esztétikához és a logikához való folyamodás. . hogy a meglévõ tulajdonságok elegendõek-e a kívánt programozási stílusok támogatására a kívánt alkalmazási területeken. hogy minden felhasználóra egyetlen programozási stílust kényszerítsen. A bemutatás egy sor programozási eljárással folytatódik. az nem fáj”. 2. hogy olyan megoldást adjanak. 4. A fontos kérdés nem annyira az. Ennek kívánalmai a következõk: 1. A C++-t úgy terveztük. Ezen a szinten sokkal fontosabb. melyek nem igénylik azokat.és „speciális célú” tulajdonság. Minden tulajdonság tisztán és „elegánsan” a nyelv szerves része legyen. Az egyes tulajdonságok megvalósítása nem okozhat jelentõs többletterhelést olyan programoknál. Minden megközelítés az elõdjére épül. hogy az elvont adatábrázolást. melyhez egyébként külön nyelvi tulajdonságok lennének szükségesek. 5. mindegyik hozzátesz valamit a C++ programozók eszköztárához. amelyek a másikban nem találhatók meg. A következõkben néhány programozási stílust és az azokat támogató fõbb tulajdonságokat vesszük számba. Kirándulás a C++-ban 29 Egy nyelv nem szükségszerûen jobb. hogyan. mint egy másik. A felhasználónak csak akkor kell tudnia a nyelv valamely részhalmazáról. mint hogy megértsük. 3. melyek az eljárásközpontú (procedurális) programozástól elvezetnek az objektumorientált programozásban használt osztályhierarchiáig és a sablonokat használó általánosított (generikus) programozásig. hogy fogalmat kapjunk arról. az utolsó kettõ pedig így összesíthetõ: „amirõl nem tudunk. nem a nyelvi részleteken. mert olyan tulajdonságokkal rendelkezik. illetve az objektumorientált és az általánosított programozást támogassa. milyen tulajdonságai vannak egy nyelvnek. A lehetõ legkevesebb legyen az ál. mit lehet megtenni C++-t használva. Nem arra szolgál.

A „jó stílus” jellegzetes példája az alábbi négyzetgyök-függvény. A középpontban az eljárás áll – a kívánt számításhoz szükséges algoritmus.30 Bevezetés 2.3. . itt a függvény törzsének kezdetét és a végét jelzik.) tárgyalásával. rutinok. A void kulcsszó jelzi. Magukat az algoritmusokat függvényhívásokkal és más nyelvi szolgáltatások használatával írjuk meg. A kettõs törtvonal // egy megjegyzés (comment) kezdete. Ezt egy jól érthetõ matematikai számítással éri el: double sqrt(double arg) { // a négyzetgyök kiszámításának kódja } void f() { double root2 = sqrt(2). makrók stb.. hogy rendet teremtsünk az eljárások labirintusában. A következõ alpontok vázlatos képet adnak a C++ legalapvetõbb szolgáltatásairól a számítások kifejezéséhez. Az e gondolkodásmóddal kapcsolatos irodalom tele van a paraméterátadás és a különbözõ paraméterfajták megkülönböztetési módjainak (eljárások. mely a sor végéig tart. } A kapcsos zárójelek a C++-ban valamilyen csoportba foglalást fejeznek ki. hogy az f függvény nem ad vissza értéket. // . Eljárásközpontú programozás Az eredeti programozási alapelv a következõ: Döntsd el. A nyelvek ezt az alapelvet függvényparaméterek átadásával és a függvények által visszaadott értékekkel támogatják. Programszervezési szempontból a függvényeket arra használjuk.. a függvény visszaadja az eredményt. Átadva egy kétszeres pontosságú lebegõpontos paramétert. mely eljárásokra van szükséged és használd azokhoz a lehetõ legjobb algoritmusokat.

A típus egy név vagy kifejezés megfelelõ használatát határozza meg.3. mely a programba egy nevet vezet be. Változók és aritmetika Minden névnek és kifejezésnek típusa van. például 1. vagy 1216 // kétszeres pontosságú lebegõpontos szám.és kétoperandusú is lehet // szorzás // osztás // maradékképzés Ugyanígy az összehasonlító mûveletek is: == != < > <= >= // egyenlõ // nem egyenlõ // kisebb // nagyobb // kisebb vagy egyenlõ // nagyobb vagy egyenlõ Értékadásokban és aritmetikai mûveletekben a C++ az alaptípusok között elvégez minden értelmes átalakítást. Például: bool char int double // logikai típus. Az aritmetikai mûveletek e típusok bármilyen párosítására használhatók: + * / % // összeadás vagy elõjel.és kétoperandusú is lehet // kivonás vagy elõjel. lehetséges értékei: true (igaz) és false (hamis) // karakter. egy. vagy '9' // egész érték.2. melyek közvetlen megfelelõi bizonyos hardverszolgáltatásoknak. egy. 42. hogy inch típusa int (vagyis inch egy egész típusú változó). A fenti deklaráció például azt adja meg. például 'a'.14 vagy 299793. így azokat egymással tetszés szerint keverhetjük: . Ehhez a névhez egy típust rendel.0 A char változók természetes mérete egy karakter mérete az adott gépen (rendesen egy bájt). Kirándulás a C++-ban 31 2.1. 'z'. A C++ több alaptípussal rendelkezik. A deklaráció olyan utasítás. például 3. amely meghatározza a végrehajtható mûveleteket: int inch. az int változóké az adott gépen mûködõ egész típusú aritmetikához igazodik (rendszerint egy gépi szó).

A >> („olvasd be”) a bemenet mûveleti jele. // kérdés kiírása // válasz beolvasása } A << („tedd bele”) mûveleti jelet kimeneti operátorként használtuk. 2. char answer = 0. return false. char answer = 0. cin >> answer.2. int i = 7. d = d+i. cin >> answer. i = d*i. } // értéket vissza nem adó függvény // lebegõpontos szám kezdeti értékadása // egész kezdeti értékadása // összeg értékadása // szorzat értékadása Itt = az értékadó mûvelet jele és == az egyenlõséget teszteli. mely a felhasználótól választ kér és a választól függõ logikai értéket ad vissza: bool accept() { cout << "Do you want to proceed (y or n)?\n". A \n karakter a kiírt karakterlánc végén új sort jelent. ha egy 'n' választ is számításba veszünk: bool accept2() { cout << "Do you want to proceed (y or n)?\n". if (answer == 'y') return true. Íme egy egyszerû függvény. milyen bemenet fogadható el és ez a beolvasás célpontja. mint a C-ben. a cout a szabványos kimeneti adatfolyam.3. A >> jobb oldalán álló kifejezés határozza meg. A példa kissé javítható. a cin a szabványos bemenõ adatfolyam. // kérdés kiírása // válasz beolvasása . Elágazások és ciklusok A C++ az elágazások és ciklusok kifejezésére rendelkezik a hagyományos utasításkészlettel.32 Bevezetés void some_function() { double d = 2.2.

Kirándulás a C++-ban 33 } switch (answer) { case 'y': return true. // nemleges válasznak veszi return false. } A switch utasítás egy értéket ellenõriz. cin >> answer.\n" .\n". while (tries < 4) { cout << "Do you want to proceed (y or n)?\n". default: cout << "Sorry. és ha az érték egyikkel sem egyezik. . állandók halmazát alapul véve. A case konstansoknak különállóknak kell lenniük. case 'n': return false. amíg a feltétele hamis nem lesz. a vezérlés a default címkére kerül. // kérdés kiírása // válasz beolvasása } } cout << "I'll take that for a no. Kevés programot írnak ciklusok nélkül.\n". } A while utasítás addig hajtódik végre. switch (answer) { case 'y': return true. I don't understand that. default: cout << "I'll take that for a no. // nemleges válasznak veszi return false. char answer = 0. // nem érti a választ tries = tries + 1. Esetünkben szeretnénk lehetõséget adni a felhasználónak néhány próbálkozásra: bool accept3() { int tries = 1. case 'n': return false. A programozónak nem kell alapértelmezésrõl (default) gondoskodnia.2.

A mutató változó a megfelelõ típusú objektum címét tartalmazhatja: p = &v[3]. v[0]…v[9]. és növeljük meg i-t”.4. Ha egész típusú változóra alkalmazzuk. Mutatók és tömbök Egy tömböt így határozhatunk meg: char v[10]. int v2[10]. míg a * jelentése „mutatója” (pointer to). // 10 karakterbõl álló tömb Egy mutatót így: char* p. amíg i kisebb. 2. i<10. Minden tömbnek 0 az alsó határa. tehát v-nek tíz eleme van. // . a ++ növelõ mûveleti jel az értéket egyszerûen eggyel növeli. Az egymással rokon eljárásokat az általuk kezelt adatokkal együtt gyakran modul-nak nevezzük.34 Bevezetés 2. Lássuk.3. hogyan másolhatjuk át egy tömb tíz elemét egy másik tömbbe: void another_function() { int v1[10]. az i-edik elemet másoljuk át. // p a v negyedik elemére mutat A „címe” jelentéssel bíró operátor az egyoperandusú &.3. for (int i=0.. Moduláris programozás Az évek során a programtervezés súlypontja az eljárások felõl az adatszervezés irányába tolódott el. A megközelítés alapelve ez lesz: . ++i) v1[i]=v2[i]. // mutató karakterre A deklarációkban a [ ] jelentése „tömbje” (array of). } A for utasítás így olvasható: „állítsuk i-t 0-ra. mint 10. Egyebek mellett ez a programok nagyobb méretében tükrözõdik..

Ahol az eljárások nincsenek az adatokkal egy csoportban. Biztosítani kell. char v[max_size]. Biztosítani kell a verem elsõ használat elõtti elõkészítését (inicializálását). Az egyes modulokon belüli eljárásokra a „jó eljárások” tervezésének módja is alkalmazható. . } // felület void f() { Stack::push('c'). E nevek máshol történõ használata nem lesz befolyással erre a programrészre és nem fog zavart okozni. int top = 0. mely modulokra van szükség és oszd fel a programot úgy. hogy a verem megjelenítése (pl. A megoldandó fõ problémák: 1. az elemek tömbje) csak ezen a felhasználói felületen keresztül legyen hozzáférhetõ. az eljárásközpontú programozás megfelel a célnak. Kirándulás a C++-ban 35 Döntsd el. 2. A C++ egymással rokon adatok. A Stack kifejtése lehet a program külön fordítható része: namespace Stack { // megvalósítás const int max_size = 200. Egy Stack modul felhasználói felülete például így adható meg és használható: namespace Stack { void push(char). } A Stack:: minõsítés azt jelzi. hogy a push() és a pop() a Stack névtérhez tartoznak. Gondoskodni kell a verem felhasználói felületérõl (pl. 3.2. A modulra a legközönségesebb példa egy verem (stack) létrehozása. függvények stb. if (Stack::pop() != 'c') error("lehetetlen"). különálló névterekbe való csoportosítására ad lehetõséget. Ez az adatrejtés elve. push() és pop() függvények). hogy az adatokat modulokba helyezed. char pop().

A fenti Stack modul a verem egy ábrázolásmódja. A következõkben többféle vermet használunk a különbözõ programozói stílusok szemléltetésére. char pop().36 Bevezetés } void push(char c) { /* túlcsordulás ellenõrzése és c behelyezése */ } char pop() { /* alulcsordulás ellenõrzése és a legfelsõ elem kiemelése */ } A Stack modul fontos jellemzõje.4. Ezt arra használhatjuk.1.h" // a felület beépítése void f() { Stack::push('c'). 2.2. Ennek következtében a namespace Stack { void push(char). } . if (Stack::pop() != 'c') error("impossible"). } // felület a stack. hogy a verem egy tömbbel van megvalósítva.). A felhasználónak nem kell tudnia. a felhasználók pedig ezt az úgynevezett fejállományt (header) beépítik (#include): #include "stack. a megvalósítás pedig anélkül módosítható. nevei szintén modulba helyezhetõk.h nevû fájlba kerül. Mivel az adat csak egyike az „elrejtendõ” dolgoknak. hogy hatással lenne a felhasználói kódra. az adatrejtés elve az információrejtés elvévé terjeszthetõ ki. melyek egy modul felületét írják le. jellemzõen egy fájlba írjuk. Külön fordítás A C++ támogatja a C külön fordítási elvét. hogy egy programot részben független részekre bontsunk. Azokat a deklarációkat. Következésképpen a C++ bármilyen deklaráció elhelyezését megengedi egy névtérben (§8. típusok stb. melynek neve a használatot tükrözi. hogy a felhasználói kódot a Stack::push()-t és a Stack::pop()-ot megvalósító kód elszigeteli a Stack adatábrázolásától. vagyis a függvények.

.h" verem használata stack.h" verem megvalósítása A külön fordítás követelmény minden valódi (tényleges használatra szánt) program esetében. nyelvi tulajdonságok által ábrázoljuk. A legjobb. inkább annak módja. és 9. fejezet).h" namespace Stack { const int max_size = 200. a külön fordítás használata nem nyelvi követelmény. char v[max_size].h: veremfelület user. nem csak a moduláris felépítésûeknél (mint pl. int top = 0.c).h-ban megadott veremfelületet. de a két fájl egyébként független és külön-külön lefordítható.c: #include "stack.c fájlokban lévõ kód közösen használja a stack. ha a modularitást a lehetõ legnagyobb mértékig fokozzuk.c: #include "stack. hogy a fordítónak segítsünk az egységesség és következetesség biztosításában. A user. majd külön-külön hatékonyan fordítható fájlokon keresztül valósítjuk meg (8. } // a felület beépítése // ábrázolás void Stack::push(char c) { /* túlcsordulás ellenõrzése és c behelyezése */ } char Stack::pop() { /* alulcsordulás ellenõrzése és a legfelsõ elem kiemelése */ } A felhasználói kód egy harmadik fájlba kerül (user. Pontosabban. A program részei a következõképpen ábrázolhatók: stack. a Stack). hogyan lehet egy adott nyelvi megvalósítás elõnyeit a legjobban kihasználni.2.c és a stack. Kirándulás a C++-ban 37 Ahhoz. a Stack modult megvalósító fájl szintén tartalmazza a felületet: #include "stack.

38 Bevezetés 2. A felhasználó majd megteszi a megfelelõ lépést: namespace Stack { void push(char). Mit kell tennünk. nem attól.2. Például: void f() { // . ami ahhoz szükséges.. a felhasználó pedig nem mindig észleli a hibát (ha így lenne. hogy visszajussunk a hívó függvény környezetéhez. // c behelyezése } A throw a vezérlést a Stack::Overflow típusú kivételkezelõnek adja át. a hibakezelést a modulok szintjén kell elvégeznünk. valamilyen függvényben. a túlcsordulás nem történne meg).. hogy a felhasználó mit akar tenni ilyen esetben. A helyreállítási tevékenység a mûveletet kezdeményezõ modultól függ. A kérdés. Így a throw úgy mûködik. Ehhez „visszatekerjük” a függvényhívási vermet. miközben megkísérelte a mûvelet végrehajtását. // felület // túlcsordulást ábrázoló típus Túlcsordulás észlelésekor a Stack::Push() meghívhat egy kivételkezelõ kódot. try { // a kivételekkel az alább meghatározott kezelõ foglalkozik . mely közvetve vagy közvetlenül meghívta a Stack::Push()-t. amikor túl sok karaktert próbálunk push()sal egy verembe rakni? A verem modul írója nem tudja. A megoldás: a Stack írója kell. vagyis „egy Overflow kivételt dobhat”: void Stack::push(char c) { if (top == max_size) throw Overflow().4. A programok növekedésével – különösen kiterjedt könyvtárhasználat esetén – a hibák (vagy általánosabban: a „kivételes események”) kezelési szabványai egyre fontosabbá válnak. mit kell tennie. Vegyük megint a Stack példát. char pop(). amelyik észlelte a hibát. hogy tudatában legyen a túlcsordulás veszélyének és ezután az (ismeretlen) felhasználóval tudatnia kell ezt. melyik modul felelõs az adott hiba kezeléséért? A hibát észlelõ modul gyakran nem tudja. mint egy többszintû return. Kivételkezelés Ha egy programot modulokra bontunk. } class Overflow { }.

14..2. részletek és példák: §8. 2. A while ciklus örökké ismétlõdne. void destroy(stack s). hogy tisztán kifejezzünk összetett rendszereket. Az alábbiakban a modulok használatának egyik módjával foglalkozunk (felhasználói típusok létrehozása). az alábbi felülettel: namespace Stack { struct Rep. majd megmutatjuk. ezért ha valamelyik Stack::push() hívás egy throw-t vált ki. } catch (Stack::Overflow) { // hoppá: verem-túlcsordulás. A kivételkezelõ eljárások használata szabályosabbá és olvashatóbbá teheti a hibakezelõ kódot. 2.5. a megfelelõ mûvelet végrehajtása } // . „E” függelék.5. E könyv minden tervezési vizsgálatában ez marad a középpontban. // a verem szerkezetének meghatározása máshol található // új verem létrehozása // s törlése ..1. fejezet. hogyan küzdjünk le problémákat a felhasználói típusok közvetlen létrehozása révén. stack create(). Kirándulás a C++-ban 39 } while (true) Stack::push('c'). Ha például sok vermet akarunk – az elõbbi Stack modulban található egyetlen helyett – megadhatunk egy veremkezelõt. Az elõzõekben leírt alakú modulok azonban nem elegendõek ahhoz. Elvont adatábrázolás A modularitás alapvetõ szempont minden sikeres nagy programnál.3. typedef Rep& stack. a vezérlés a Stack::Overflow-t kezelõ catch részhez kerül. További tárgyalás. Típusokat leíró modulok A modulokkal való programozás elvezet az összes azonos típusú adatnak egyetlen típuskezelõ modul általi központosított kezeléséhez.

Amíg a felületet változatlanul hagyjuk. A Stack::stack mûködése nagyon hasonlít egy beépített típuséhoz: struct Bad_pop { }. hogy a vermet saját Stack::stack-jével azonosítjuk és a további részleteket a felhasználó elõl elrejtjük. Egy megvalósítás például elõre lefoglalhatna néhány verempéldányt és a Stack::create() egy nem használt példányra való hivatkozást adna át. char pop(stack s). Fontos. // ábrázolás . struct Rep. Stack::push(s2.'k'). ha úgy döntünk. if (Stack::pop(s1) != 'c') throw Bad_pop(). így a Stack::create() újra használhatja azt: namespace Stack { const int max_size = 200.40 Bevezetés void push(stack s. char c). hogy átírjuk a Stack-et. if (Stack::pop(s2) != 'k') throw Bad_pop(). Az ötlet az. hogy Rep egy típus neve.5-ben). Stack::destroy(s2). Stack::destroy(s1). typedef Rep& stack. Stack::stack s2 = Stack::create(). a felhasználó nem fogja észrevenni. // c behelyezése az s verembe // s legfelsõ elemének kiemelése } A következõ deklaráció azt mondja. Stack::push(s1. void f() { Stack::stack s1 = Stack::create(). Az alábbi deklaráció a stack nevet adja egy „Rep referenciának” (részletek §5. Ezután a Stack::destroy() egy ábrázolást „nem használt”-ként jelölhet meg. hogy a felhasználónak nem szükséges tudnia. de késõbbre hagyja a típus meghatározását (§5.'c'). hogyan tesszük ezt.7). // új verem létrehozása // még egy verem létrehozása } Ezt a Stack-et többféleképpen megvalósíthatnánk.

char c) { /* s túlcsordulásának ellenõrzése és c behelyezése */ } char Stack::pop(stack s) { /* s alulcsordulásának ellenõrzése és a legfelsõ elem kiemelése */ } Stack::stack Stack::create() { // használaton kívüli Rep kiválasztása. rá mutató hivatkozás visszaadása } void Stack::destroy(stack s) { /* s megjelölése nem használtként */ } Amit tettünk. Rep stacks[max]. az ábrázoló típus becsomagolása felületi függvények készletébe. Az. // vermek maximális száma // elõre lefoglalt verempéldányok // used[i] igaz. Ha például egy jobban kidolgozott adatszerkezetet választottunk volna a verem azonosítására.2. melyeket az adott megvalósító típushoz hozzáférést adó modul határozott meg. elõkészítése. a Stack::create() és a Stack::destroy() függvény ellenõrzi. hogyan mutattuk be a Stack-et ábrázoló típust a verem felhasználóinak. Jelentõs probléma. hogy azok a felhasználói típusok. }. mint a beépített típusok. a Stack::stack-ek értékadási és elõkészítési (inicializálási) szabályai drámai módon megváltoztak volna (ami néha valóban kívánatos lehet). hogyan adtuk meg ezeket a felületi függvényeket. Azt például. hogy az ilyen „mûtípusoknak” a felhasználók részére való bemutatása az ábrázoló típus részleteitõl függõen nagyon változó lehet – a felhasználókat viszont el kell szigetelni az ábrázoló típus ismeretétõl. hogy a kényelmes vermek szolgáltatásának problémáját egyszerûen áttettük a Stack modulból a Stack::stack ábrázoló típusba. hogy mikor használható egy Stack::Rep. részben attól függ. int top. részben attól. nem a szokásos nyelvi szabályok. Ez gyakran kevesebb az ideálisnál. részben pedig magától az ábrázoló típustól. használtként // megjelölése. const int max = 16. mint azok. } typedef Rep& stack. bool used[max]. Kirándulás a C++-ban 41 struct Rep { char v[max_size]. nem úgy viselkednek. . Ez azonban azt mutatja. Még lényegesebb. és kisebb vagy más támogatást élveznek. ha stacks[i] használatban van void Stack::push(stack s. hogy az eredményül kapott „stack típus” hogyan viselkedik.

vagyis a re és az im csak a complex osztály bevezetésekor megadott függvények által hozzáférhetõ.42 Bevezetés 2. Az olyan aritmetikai típusok. A szerzõ inkább a felhasználói típus (user-defined type) megnevezést kedveli. Az ábrázolás privát (private). // .. A programozási megközelítés most ez lesz: Döntsd el. complex). friend complex operator*(complex. az ilyen valóban elvont egyedek konkrét példányai lennének. friend bool operator==(complex. Ha adva volna ilyen. mely típusokra van szükség és mindegyikhez biztosíts teljes mûveletkészletet. } friend complex operator+(complex.5. ADT) nevezzük.0) // kétoperandusú // egyoperandusú // egyenlõ // nem egyenlõ }. ahol egy típusból egy példánynál többre nincs szükség.. közönséges példái a felhasználói típusnak. Vegyük az alábbi kódot: class complex { double re. Az ilyen típusokat gyakran elvont vagy absztrakt adattípusoknak (abstract data type. amiket itt típusoknak nevezünk. im=i. complex).2. } complex() { re = im = 0. Az elvont adattípus kifejezõbb meghatározásához „absztrakt” matematikai leírás kellene. friend bool operator!=(complex. azok. friend complex operator-(complex). // complex létrehozása két skalárból // complex létrehozása egy skalárból // alapértelmezett complex: (0. hogy engedi. } complex(double r) { re=r. Felhasználói típusok A C++ ezt a problémát úgy küzdi le. complex). mint a beépített típusok. complex). hogy a felhasználó közvetlenül adjon meg típusokat. friend complex operator-(complex. Ott. melyek közel úgy viselkednek. friend complex operator/(complex. public: complex(double r. mint a racionális és komplex számok. complex). im=0. double i) { re=r. A complex osztály (vagyis felhasználói típus) deklarációja egy komplex számot és a rajta végrehajtható mûveletek halmazát ábrázolja. complex). elegendõ a modulokat használó adatrejtési stílus. im. Ezeket az alábbi módon adhatjuk meg: .

a konstruktor. A complex osztály három konstruktort tartalmaz. A c!=b jelentése például operator!=(c.5. class Overflow { }.3). 2.a).im+a2. class Bad_size { }.2.im). complex a2) { return complex(a1. Egyikük egy double-ból csinál complex-et. // . A konstruktor írja le az osztály egy objektumának elõkészítési-létrehozási módját.re. az 1/a jelentése operator/ (complex(1). Stack(int s). Konkrét típusok Felhasználói típusok változatos igények kielégítésére készíthetõk. hogy kissé valósághûbbé tegyük a példát.. Vegyünk egy felhasználói veremtípust a complex típus soraival együtt. int top. complex c = a+b*complex(1.a1.3. if (c != b) c = -(b/a)+2*b. A complex osztály így használható: void f(complex z) { complex a = 2.b). a harmadik alapértelmezett érték alapján. ~Stack().3. ez a Stack típus paraméterként elemei számát kapja meg: class Stack { char* v. Kirándulás a C++-ban 43 complex operator+(complex a1..re+a2. // kivétel // kivétel // kivétel // konstruktor // destruktor . A legtöbb – de nem minden – modul jobban kifejezhetõ felhasználói típusként. egy másik egy double párból. Ahhoz. } Az a tagfüggvény. complex b = 1/a. } A fordító a komplex számokhoz kapcsolt mûveleti jeleket megfelelõ függvényhívásokká alakítja. melynek neve megegyezik az osztályéval. int max_size. public: class Underflow { }.2.

v = new char[s]. // "||" jelentése "vagy" max_size = s. kupac – vagy dinamikus tár) a new operátor használatával. // . mint az int vagy a char beépített típusok. vonatkozó szabályoknak engedelmeskedik. Stack* s_ptr = new Stack(20). void push(char c). ahogy a beépített típusú változókat szokták. Ez a függvény gondoskodik a kezdeti értékadásról. A destruktor takarít.push('b'). megadhatjuk a konstruktor ellentétét. } // destruktor // elemek törlése. valahányszor létrehozzuk az osztály egy példányát. másolásra stb. hatókörre.. if (s<0 || 10000<s) throw Bad_size(). // 10 elemet tárolni képes globális verem // hivatkozás a Stack veremre // lokális verem i számú elemmel // mutató a szabad tárban levõ Stack-re } Ez a Stack típus ugyanolyan névadásra.2. amikor az osztály egy objektuma kikerül a hatókörbõl.44 Bevezetés }. hely felszabadítása újrafelhasználás céljára (§6. Ehhez lefoglal némi helyet a szabad tárból (heap – halom. s_var1. s_var2. A felhasználók a vermeket ugyanúgy hozzák létre és használják. s_ref. Például: Stack s_var1(10).push('a'). Ha bármilyen „takarításra” van szükség. . char pop(). A Stack(int) konstruktor meghívódik. int i) { Stack s_var2(i).. a destruktort: Stack::Stack(int s) // konstruktor { top = 0. s_ptr->push('d'). void f(Stack& s_ref.push('c'). // az elemek szabad tárba helyezése } Stack::~Stack() { delete[ ] v.6) A konstruktor egy új Stack változót hoz létre. felszabadítva a tárat. Az egész a Stack-ek felhasználóinak beavatkozása nélkül történik. élettartamra.

Kirándulás a C++-ban 45 Természetszerûen a push() és pop() tagfüggvényeket valahol szintén meg kell adni: void Stack::push(char c) { if (top == max_size) throw Overflow(). Absztrakt típusok Amikor a Stackrõl. amit be kellene építeni (#include) a vermeket használó programrészbe. de jelen van. melyek nem változnak gyakran.1. Nevezetesen egy típusból nem lehetnek valódi lokális (helyi) változóink. ahol a felület tökéletesebben elszigeteli a felhasználót a megvalósítás részleteitõl. Ha bármilyen jelentõs változást szenved. Az ábrázolás privát. Ha azonban teljesen el akarjuk szigetelni az adott verem felhasználóját a megvalósítás változásaitól. } A complex és Stack típusokat konkrét típusnak nevezzük. top = top + 1. Ezt az árat kell fizetni. Az ábrázolás nem válik el a felhasználói felülettõl. Ekkor a megoldás leválasztani a felületet az ábrázolásról és lemondani a valódi lokális változókról. v[top] = c.4. // kivétel // kivétel . ha nem tudjuk a típus ábrázolásának méretét. a felhasználó újra le kell. ezért csak a tagfüggvényeken keresztül hozzáférhetõ. egy tulajdonságot elvesztettünk. } char Stack::pop() { if (top == 0) throw Underflow(). class Overflow { }. ez elfogadható és gyakran ideális. és ahol lokális változók gondoskodnak a szükséges tisztaságról és hatékonyságról. return v[top]. a legutolsó Stack nem elegendõ.1) által megvalósított „mûtípusról” áttértünk egy saját típusra (§2. hanem része annak. top = top . mint a beépítettek.5. 2. Azon típusoknál. mint egy modul (§2. ellentétben az absztrakt típusokkal.3).5.5.2. Elõször határozzuk meg a felületet: class Stack { public: class Underflow { }. hogy a konkrét típusok pontosan ugyanúgy viselkedjenek. hogy fordítsa.

} Vegyük észre. hogyan használja f() a Stack felületet.46 Bevezetés }.Ezt a Stack-et így használhatnánk: void f(Stack& s_ref) { s_ref. public: Array_stack(int s). valamilyen másik függvény kell létrehozzon egy objektumot. A virtual szó a Simulában és a C++-ban azt jelenti. mely más osztályoknak felületet ad. „megvalósítja …-t”. int max_size. A „:public” olvasható úgy. virtual void push(char c) = 0. if (s_ref.push('c'). vagy „…altípusa …-nak”. virtual char pop() = 0.pop() != 'c') throw Bad_pop(). // Array_stack megvalósítja Stack-et }. Nem meglepõ. mely tartalmazza a push() és pop() függvényeket. gyakran többalakú (polimorf) típusnak nevezzük. Egy Stack-bõl származtatott osztály a Stack felületet valósítja meg. A furcsa =0 kifejezés azt mondja. hogy „az adott osztályból származtatott osztályban késõbb felülírható”. mely a megvalósítás ismeretének teljes hiányában egy Stacket akar használni. Az f() függvény részére. mint „származtatva …-ból”. a megvalósítás mikéntjérõl mit sem tudva. hogy a verembõl származtatott osztálynak meg kell határoznia a függvényt. int top. char pop(). Az olyan osztályt. amelyen az f() mûveletet hajthat végre: void g() { . void push(char c). ~Array_stack(). Ilyenformán a Stack felületként szolgál bármilyen osztály részére. amit kihagytunk a Stack felületbõl: class Array_stack : public Stack { char* p. hogy a megvalósítás a konkrét Stack osztályból mindent tartalmazhat.

Egy függvény létre tud hozni egy List_stack-et és f() használhatja azt: void h() { List_stack ls. hogy ezt feloldhassuk. } 2. } char pop(). mint lc elsõ elemét.front(). a List_stack::pop()-ot kell meghívni. } // az elsõ elem lekérése // az elsõ elem eltávolítása Itt az ábrázolás egy karakterlista.3) }. hogy futási idõben mely függvényt kell meghívni. amely táblázat függvényekre hivatkozó mutatókat tartalmaz. Mivel f() nem tud az Array_stack-ekrõl.pop_front(). public: List_stack() { } // List_stack megvalósítja Stack-et // (standard könyvtárbeli) karakterlista (§3. az lc.front() pedig lc elsõ elemére utal. lc. Virtuális függvények Hogyan történik az f()-en belüli s_ref. A fordítóknál szokásos eljárás egy virtuális függvény nevének egy táblázat valamely sorszámértékévé alakítása. f(as).5.7. A táblázatot „virtuális . amikor g()-bõl.pop_front hívás eltávolítja az elsõ elemet.push_front(c) beteszi c-t. csak a Stack felületet ismeri. char List_stack::pop() { char x = lc. ugyanolyan jól fog mûködni a Stack egy másik megvalósításával is: class List_stack : public Stack { list<char> lc. az Array_stack::pop()-ot.2. void push(char c) { lc. a Stack objektumnak információt kell tartalmaznia arról. az lc.5.pop() hívás feloldása a megfelelõ függvénydefiníció hívására? Amikor h()-ból hívjuk f()-et. return x. f(ls). Az lc. Kirándulás a C++-ban 47 } Array_stack as(200). Ahhoz.push_front(c).

6. 2. Ez grafikusan így ábrázolható: Array_stack objektum: p max_size top List_stack objektum: lc vtbl: Array_stack::push() Array_stack::pop() vtbl: List_stack::push() List_stack::pop() A vtbl-ben lévõ függvények lehetõvé teszik. Nincs mód arra. 2. hogyan lehet azt megoldani osztályhierarchiák használatával. ha de- . mint a „normális függvényhívás”. hogy új felhasználáshoz igazítsuk. hogy az objektumot akkor is helyesen használjuk. mely azonosítja az osztály virtuális függvényeit.6. Ha egy fekete dobozt létrehozunk. A felhasználói típusok azonban önmagukban nem elég rugalmasak ahhoz. E részben elõször egyszerû felhasználói típusokkal mutatunk be egy problémát. Problémák a konkrét típusokkal A konkrét típusok – a modulokban megadott „mûtípusokhoz” hasonlóan – egyfajta „fekete dobozt” írnak le. Ez a virtuális hívási eljárás lényegében ugyanolyan hatékonnyá tehetõ.48 Bevezetés függvénytáblának” vagy egyszerûen vtbl-nek szokás nevezni. az nem lép igazi kölcsönhatásba a program többi részével. valamint egy-egy vtbl minden osztályhoz. hogy kiszolgálják igényeinket. A hívónak mindössze a vtbl helyét kell tudnia a Stack-en belül. ha a hívó nem ismeri annak méretét és adatainak elrendezését.1. Többlet helyszükséglete: a virtuális függvényeket tartalmazó osztály minden objektumában egy-egy mutató. Objektumorientált programozás Az elvont adatábrázolás a jó tervezéshez alapfontosságú. kivéve. majd megmutatjuk. illetve a virtuális függvények sorszámát. a könyvben pedig a tervezés végig központi kérdés marad. Minden virtuális függvényeket tartalmazó osztálynak saját vtbl-je van.

hogy az olyan mûveletek számára. Egy alakzatot az alábbi módon adhatunk meg: enum Kind { circle. A draw() függvényt így adhatnánk meg: void Shape::draw() { switch (k) { case circle: // kör rajzolása break.. illetve végét jelöli. háromszögeket és négyzeteket kell támogatnia. // . (A Pascal-szerû nyelvekben egy változó rekordtípust használhatnánk. */ }... void rotate(int). mint a draw() (rajzolás) vagy a rotate() (forgatás) meghatározhatóvá tegyük. hogy léteznek az alábbiak: class Point { /* . de komoly rugalmatlansághoz is vezethet.8) class Shape { Kind k. Color col. Ez a helyzet ideális is lehet. class Color { /* . Point center. square }. Tegyük fel. // felsorolás (§4.. .. triangle.2. milyen fajta alakzattal van dolguk. Kirándulás a C++-ban 49 finícióját módosítjuk.. public: void draw(). // típusmezõ A k típusazonosító mezõ azért szükséges. // . }. A jelölés többsoros megjegyzésekhez is használható. case triangle: // háromszög rajzolása break. Vegyük például egy grafikus rendszerben használni kívánt Shape (Alakzat) típus meghatározását... */ }. Tegyük fel azt is. k címkével). hogy pillanatnyilag a rendszernek köröket. A /* és */ egy megjegyzés kezdetét.

valahányszor a rendszerhez egy új alakzatot adunk.. Elõször létrehozunk egy osztályt. milyen alakzatfajták léteznek..2. /* . } Ez azonban „rendetlenség”. Mivel egy új alakzat hozzáadása magával vonja minden fontos alakzat-mûvelet kódjának módosítását.. Ha új alakzatot hozunk létre. mely minden alakzat általános tulajdonságait leírja: class Shape { Point center. egy körrajzoló függvénnyel lehet megrajzolni stb. támogatják az objektumközpontúságot. A megoldásról a Simulából kölcsönzött öröklés gondoskodik. melynek sugara van.. } virtual void draw() = 0. rajzolhatóság stb.. az ilyen munka nagy ügyességet kíván és hibákat vihet be a más (régebbi) alakzatokat kezelõ kódba. Ezért az ilyen függvénynél mindig növekszik a kód. E megkülönböztetés kifejezése és elõnyeinek kihasználása az objektumorientált programozás lényege. A rendszerhez nem adhatunk új alakzatot. . Osztályhierarchiák A probléma az. A függvényeknek – mint a draw() – tudniuk kell arról. }. Azok a nyelvek. // . más nyelvek nem.. // . } void move(Point to) { center = to. hacsak hozzá nem férünk minden mûvelet forráskódjához. Az egyes alakzat-ábrázolások kiválasztását komolyan megbéníthatja az a követelmény. 2. minden mûveletet meg kell vizsgálni és (lehetõség szerint) módosítani kell azokat. Color col.). melyek e megkülönböztetés kifejezését és használatát lehetõvé tévõ szerkezetekkel rendelkeznek. hogy nincs megkülönböztetés az egyes alakzatok általános tulajdonságai (szín.6.50 Bevezetés } case square: // négyzet rajzolása break. melyet az általános Shape típus leírása képvisel. public: Point where() { return center. hogy az ábrázolásoknak (legalább is néhánynak) illeszkednie kell abba a – jellemzõen rögzített méretû – keretbe. */ draw().) és egy adott alakzatfajta tulajdonságai közt. (A kör például olyan alakzat. virtual void rotate(int angle) = 0.

bázisosztálya (alaposztálya.A programozási megközelítés itt a következõ: Döntsd el. üres függvény }. mennyire alkalmazható egy problémára az objektumorientált megközelítés. int angle) // v elemeinek elforgatása angle szöggel { for (int i = 0. Más szóhasználat szerint a Circle és a Shape alosztály (subclass)..2. hogy örökli (inherit) a bázisosztály tagjait. biztosíts mindegyikhez teljes mûveletkészletet. Némely területen.és származtatott osztályok használatát általában öröklésként említjük. azokat a függvényeket. Kirándulás a C++-ban 51 Akárcsak a §2. } Egy konkrét alakzat meghatározásához meg kell mondanunk. base). melyek alakzatokra hivatkozó mutatókból álló vektorokat kezelnek: void rotate_all(vector<Shape*>& v. illetve fõosztály (superclass). elegendõ az elvont adatábrázolás. A C++-ban a Circle osztályról azt mondjuk. i<v. Ahol nincs ilyen közös tulajdonság. virtuálisként (virtual) vezetjük be. hogy a Circle osztály õse ill. de a konkrét megvalósítás még nem ismert. */ } void rotate(int) {} // igen. például az interaktív grafikában.5. A származtatott osztályról azt mondjuk. a Shape osztályról pedig azt. ++i) v[i]->rotate(angle). világos.4 absztrakt Stack típusában.. public: void draw() { /* . az öröklés segítségével pedig határold körül pontosan a közös tulajdonságokat. A fenti meghatározás alapján már írhatunk általános függvényeket. ezért a bázis. mely osztályokra van szükséged.size(). öröklés és virtuális függvények használatával kiaknázható közösség mértéke mutatja. hogy alakzatról van szó és meg kell határoznunk konkrét tulajdonságait (beleértve a virtuális függvényeket is): class Circle : public Shape { int radius. hogy az objektumközpontúságnak óriási le- . melyeknél a hívási felület meghatározható. A típusok közti. hogy a Shape osztályból származik (derived).

így az itt felsorolt irányelvek is inkább egymást kiegészítõ. míg a modulok osztályokat és függvényeket.52 Bevezetés hetõségei vannak. kölcsönösen támogató jellegûek. alig látszik több lehetõség. Az egyes típusok közös tulajdonságait megtalálni nem egyszerû. hanem kiegészítik egymást (§12. Az osztályok és modulok például függvényeket tartalmaznak.1987] és [Booch. amelyeket egy közös bázisosztályban kihasználhatnánk. Még általánosabban. Következésképpen függetlenül kell ábrázolni is. Osztályokat lehet kifejezetten más típusok építõkockáiként tervezni. A kihasználható közösség mértékét befolyásolja a rendszer tervezési módja. a létezõ osztályokat pedig meg lehet vizsgálni. A programozási irányelv a következõ: Döntsd el. ha sablont (template) hozunk létre belõle és a konkrét char típus helyett sablonparamétert használunk. hogy minél több típussal és adatszerkezettel mûködjenek.4) nem kölcsönösen kizárják. amikor a rendszerkövetelményeket leírjuk – aktívan kell keresnünk a közös tulajdonságokat.5). Általánosított programozás Ha valakinek egy verem kell. így az objektumközpontúság támogatásához szükséges szolgáltatások feleslegesnek tûnnek.6-ban.7. Az osztályhierarchiák és az absztrakt osztályok (§2. 2. mint a klasszikus aritmetikai típusoknál és az azokon alapuló számításoknál. A tapasztalt tervezõ sokféle megközelítést használ – ahogy a szükség parancsolja. mely algoritmusokra van szükség. 1994] a §23. független a karakter fogalmától. 2.5. Tárolók Egy karakterverem-típust általánosíthatunk. mint az elvont adatábrázolás.1. ha egy algoritmus az ábrázolástól függetlenül és logikai torzulás nélkül kifejezhetõ. Más területeken. nem feltétlenül karaktereket tartalmazó veremre van szüksége. Az objektumorientált programozás konkrét programozási nyelvi szerkezetek nélkül való elemzésére irányuló kísérleteket lásd [Kerr. mutatnak-e olyan hasonlóságokat. Például: . Amikor egy rendszert tervezünk – és akkor is. és úgy lásd el azokat paraméterekkel. akkor így is kell tenni. A verem általános fogalom.7.

~Stack(). a vermet az alábbi módon használhatjuk: Stack<char> sc(200). A template<class T> elõtag T-t az utána következõ deklaráció paraméterévé teszi.2)). top = top + 1. // konstruktor // destruktor }.2)) throw Bad_pop().push(complex(1. if (scplx.pop() != complex(1. void push(T). // verem 200 karakter számára // verem 30 komplex szám részére // verem 45. } Ha a definíciók adottak. if (sc.1. scplx. class Overflow { }. void f() { sc. Kirándulás a C++-ban 53 template<class T> class Stack { T* v. Stack<complex> scplx(30).push('c'). top = top . } template<class T> T Stack<T>::pop() { if (top == 0) throw Underflow(). int max_size.2. v[top] = c. T pop(). Stack(int s). return v[top].pop() != 'c') throw Bad_pop(). Hasonlóképpen adhatjuk meg a tagfüggvényeket is: template<class T> void Stack<T>::push(T c) { if (top == max_size) throw Overflow(). egészekbõl álló lista számára } . Stack< list<int> > sli(45). int top. public: class Underflow { }.

A lényeg. általában container class-nak vagy egyszerûen tárolónak (konténernek) hívjuk. Tegyük fel. fej.. vektorokat. hogy egy tárolót anélkül használjunk. anélkül. mégpedig olyat. A bejáró (iterator) valamely elemre hivatkozik és gondoskodik arról a mûveletrõl. melynek hatására legközelebb a sorozat soron következõ elemére fog hivatkozni. Az egyik megoldás.8) vettünk át.7. sablonokként adhatunk meg listákat.. amely valamilyen típusú elemek gyûjteményét tartalmazza. A „vége” fizikai ábrázolása lehet egy „õr” (sentinel) elem. amely megengedi. hogy a sorozat számos módon ábrázolható. milyen fajta tárolóról van szó.2. A sorozat vége ugyancsak egy bejáró. fejezetek).. hogy vektorokat. A sorozatnak van egy kezdete és egy vége. Ismét használhatjuk tehát az általánosított (generikus) programozás irányelveit algoritmusok tárolók általi paraméterezésére. amelyet a C++ standard könyvtárában a tárolók és nem numerikus algoritmusok megközelítésébõl (18. és 18. melyet egy konkrét sort függvény elfogad. hogy pontosan tudnánk. asszociatív tömböket (map) és így tovább. . mely a sorozat utolsó elemén túlra hivatkozik. így listákkal és tömbökkel is. 17. de a felhasználók sajátokat is írhatnak (3. Konvertálni nem akarunk egyetlen adott adatszerkezetre sem. másolni és átkutatni. Íme a sorozat fogalmának grafikus ábrázolása: Kezdet Vég elemek: . hogy minden egyes tárolóra megírnánk a sort(). de elképzelhetõ más is. §3. copy() és search() függvényeket. ezért találnunk kell egy általános módot a tárolók leírására. listákat és tömböket akarunk rendezni. A sablonoknak fordítási idõben van jelentõségük. a sorozatokra összpontosít és azokat bejárókkal ( iterator) kezeli. tehát használatuk a „kézzel írott” kódhoz képest nem növeli a futási idõt. 2. Az olyan osztályt. Általánosított algoritmusok A C++ standard könyvtára többféle tárolóról gondoskodik.54 Bevezetés Hasonló módon.

begin()).&vc1[200]. Minden standard könyvtárbeli tároló (17. alacsonyszintû tömb és mutató típusai rendelkeznek a megfelelõ mûveletekkel: char vc1[200].. vc2 elsõ elemétõl kezdõdõen. a másodiknál pedig a ++ növelõ mûveleti jelet. void g(vector<complex>& vc.begin()).&vc2[0]). Ha az alapötletet megértettük. // hivatkozott elemek másolása ++to. copy(lc. Ezt azért tesszük.&ac[200]. In too_far.vc. class Out> void copy(In from. Fej. az In és Out jelöli. az alábbi módon írhatunk kódot: template<class In.3) támogatja ezt a bejáró. } . §16. Kirándulás a C++-ban 55 Az olyan mûveletekhez. Out to) { while (from != too_far) { *to = *from.lc. list<complex>& lc) { copy(&ac[0]. // 200 karakter tömbje // 500 karakter tömbje void f() { copy(&vc1[0]. A fentieket adottnak tekintve. A C++ beépített.lc. // következõ forrás } } Ez átmásol bármilyen tárolót.2. char vc2[500]. A forrás és a cél típusait egyetlen paraméter helyett két sablonparaméter. } Ez vc1-et elsõ elemétõl az utolsóig vc2-be másolja. Például: complex ac[200].(iterator) és sorozatjelölést. mint „egy bejáró által férjünk hozzá egy elemhez” és„ a bejáró hivatkozzon a következõ elemre” szükségünk van valamilyen szabványos jelölésre. // következõ cél ++from. mert gyakran akarunk másolni egy fajta tárolóból egy másik fajtába.end().begin(). az elsõ helyén kézenfekvõ választás a * dereferencia (hivatkozó vagy mutató) operátort használni. amelyre a formai követelmények betartásával bejárót adhatunk meg.

Ami egy feladatra tökéletes. Amikor tehát az Olvasó a következõ fejezeteket olvassa.1. Valójában ez nem is lenne ideális. ha az egyes programozási módszerek és más tulajdonságok környezetében tekintjük. A nyelvi tulajdonságok egy sereg programozási stílus és módszer támogatására valók.8. hogy jó építõeszköz legyen a rendszerek széles választékához és a fogalmak széles körét közvetlenül kifejezhessük vele. A C++-t ezért úgy terveztük. hogy képesek legyünk együttesen használni a nyelv szolgáltatásait.7. a list-et pedig a vector-ba. Valójában egy általános célú programozási nyelv nem is lehet minden feladatra tökéletes. hogy a C++ részletekbe menõ vizsgálatának igazi célja az. [3] A programozási módszerekre összpontosítsunk. [2] Jó programok írásához nem kell ismernünk a C++ minden részletét. hogy jó eszközként szolgáljon nagyszerû rendszerek építéséhez. nem az összes nyelvi tulajdonság minden részletre kiterjedõ megértése. egészséges tervezési környezetben. gyakran komoly fogyatékosságokat mutathat egy másiknál. §2.9. ne a nyelvi tulajdonságokra. kérjük. Csak akkor lesz jelentékeny. §2. 2. Egy szabványos tárolónál a begin() a bejáró (iterator). Tanácsok [1] Ne essünk kétségbe! Idõvel minden kitisztul.56 Bevezetés Itt a tömböt a list-be másoljuk. Szerencsére egy programozási nyelvnek nem kell tökéletesnek lennie ahhoz.1. . 2. amely az elsõ elemre mutat. §1. Egyetlen nyelvi tulajdonság önmagában nem túl érdekes. A gyakorlati programozásnál kevés az elõnye. Nem mindent lehet közvetlenül kifejezni egy nyelv beépített tulajdonságait felhasználva. jó programozási stílusban. Utóirat Egyetlen programozási nyelv sem tökéletes. mivel az egy területen való tökéletesség magával vonja a szakosodást. Következésképpen egy nyelv megtanulásánál a feladat a nyelv sajátos és természetes stílusainak elsajátítására való összpontosítás. ha ismerjük a legrejtettebb nyelvi tulajdonságokat vagy ha a legtöbb tulajdonságot kihasználjuk. amire csak használják. emlékezzen arra.

mely csak a puszta programnyelven íródik. Bevezetés Nincs olyan jelentõs program. hogy a következõ fejezetekben jobb .1. mikor a tudatlanság azonnali?” (Hobbes) Szabványos könyvtárak • Kimenet • Karakterláncok • Bemenet • Vektorok • Tartományellenõrzés • Listák • Asszociatív tömbök • Tárolók (áttekintés) • Algoritmusok • Bejárók • Bemeneti/kimeneti bejárók • Bejárások és predikátumok • Tagfüggvényeket használó algoritmusok • Algoritmusok (áttekintés) • Komplex számok • Vektoraritmetika • A standard könyvtár (áttekintés) • Tanácsok 3. Bemutat olyan hasznos könyvtári típusokat. fejezet folytatásaként ez a fejezet gyors körutazást tesz a fõ könyvtári szolgáltatásokban. mint a string. vector.3 Kirándulás a standard könyvtárban „Minek vesztegessük az idõt tanulásra. Elõször a nyelvet támogató könyvtárakat fejlesztik ki. Ez lehetõvé teszi. list és map. A 2. valamint használatuk legáltalánosabb módjait. mit lehet a C++ és standard könyvtárának segítségével megtenni. hogy fogalmat adjon. ezek képezik a további munka alapját.

1. világ!: #include <iostream> int main() { std::cout << "Helló. és megértsük a leghasznosabb könyvtári szolgáltatások legegyszerûbb használatát. ha a részleteket nem érti tökéletesen. kiterjedt lehetõségeket – ezt azonban a gyakorlatokra hagytuk. hogy legyen egy main() nevû függvény. úgy. amelyek a szabványos fejlesztési és/vagy futtatási környezeteket támogatják. A programok jellemzõen valamilyen kimenetet állítanak elõ. a legtöbb programfejlesztõ környezetet alapkönyvtárakkal (foundation library) is ellátták. fedezze fel a legtöbb rendszerben meglévõ. amely kiírja: Helló. Hasonlóképpen. A 2. mi következik. Minden C++ programban kell. E fejezet célja. Ha a main() nem nulla értéket ad vissza.2. ne zavarja. ha van ilyen. fejezethez hasonlóan bátorítani akarom az olvasót. Ha nincs visszatérési érték. világ!\n". kivéve a külön megjelölteket. világ! A legkisebb C++ program: int main() { } A program megadja a main nevû függvényt. Ilyeneket nem fogunk leírni. a rendszer a sikeres befejezést jelzõ értéket kap vissza. A main() által visszaadott int érték. A C++ standard könyvtárán kívül a legtöbb megvalósítás a felhasználó és a program közti párbeszédre grafikus felhasználói felületeket is kínál. Íme egy program. az hibát jelent. és megõrizni a példák „hordozhatóságát” (más rendszerekre való átültetésének lehetõségét). melynek nincsenek paraméterei és nem tesz semmit. a program visszatérési értéke „a rendszerhez”. melyeket gyakran GUI-knak vagy ablakozó rendszernek neveznek. hogy megízleljük. A standard könyvtárat részletesebben a §16. Természetesen biztatjuk az olvasót. 3.58 Bevezetés példákat és gyakorlatokat adjak az olvasónak. ne kedvetlenítse el. Az e könyvben leírt standard könyvtárbeli szolgáltatások minden teljes C++-változat részét képezik.2 mutatja be. } . Helló. A program e függvény végrehajtásával indul. ahogy a szabványban szerepel. A szándékunk a C++ önálló leírását adni.

3. Esetünkben az \n az új sor jele. világ!\n" értelmetlen volna. Használatukhoz az std:: elõtagot alkalmazhatjuk: std::string s = "Négy láb jó. illetve a szükséges #include <fejállomány>-okat. világ!\n” karakterliterál a szabványos kimeneti adatfolyamba. A << („tedd bele”) kimeneti operátor második operandusát beírja az elsõbe. tehát a kiírt „Helló.5. két láb rossz!". Kirándulás a standard könyvtárban 59 Az #include <iostream> utasítja a fordítót.4.6 és a 16. nem valamilyen más cout-ét. Ez egyértelmûen a standard cout használatát írja elõ. az std::cout-ba íródik. amint a §3. Ezenkívül vagy az std:: elõtagot kell használni. Az itt közölt programrészletek fordításához és futtatásához a megfelelõ fejállományokat be kell építeni (#include. Az egyszerûség kedvéért a példákban ritkán írjuk ki az std:: elõtagot. Ezért írtunk std::cout-ot cout helyett. A standard könyvtár névtere A standard könyvtár az std névtérhez (§2. A karakterliterál egy " " jelek közé zárt karaktersorozat. A benne szereplõ \ (backslash. std::list<std::string> slogans. hogy illessze be az iostream-ben található adatfolyam-bemeneti és -kimeneti szolgáltatások deklarációját a forráskódba. vagy globálissá kell tenni minden nevet az std névtérbõl (§8. az <iostream>-hez hasonló szabványos fejállomány által gondoskodhatunk: #include<string> #include<list> Ez rendelkezésre bocsátja a szabványos string-et és list-et.2) tartozik. Ebben az esetben a „Helló. fordított perjel) az utána következõ karakterrel valamilyen egyedi karaktert jelöl.7. §8.3): . világ!” szöveget sortörés követi. 3. A standard könyvtár minden szolgáltatásának beépítésérõl valamilyen. E deklarációk nélkül az alábbi kifejezés std::cout << "Helló. §8.3. fejezetben szereplõ felsorolásokban szerepelnek).2.

// a szabványos karakterlánc-szolgáltatások elérhetõvé tétele // std nevek elérhetõvé tétele az std:: elõtag nélkül // rendben: a string jelentése std::string string s = "A tudatlanság erény!". } A különbözõ típusú kimenetek természetesen párosíthatók: void h(int i) { cout << "i értéke ". cout << '\n'. Mindazonáltal. vagy egy magyarázat része (hogyan határozható meg az adott szabványos szolgáltatás). azt vagy a szabvány ajánlja. cout << i. } Ugyanezt teszi az alábbi kód is: void g() { int i = 10. void f() { cout << 10.4. a nyelvi és könyvtári tulajdonságokat illusztráló programrészletek rövidre fogása érdekében elhagytuk az ismétlõdõ #include-okat és std:: minõsítéseket. de felhasználói típushoz is könnyen megadhatjuk. } . Általában szegényes ízlésre vall egy névtérbõl minden nevet a globális névtérbe helyezni. cout << i. Kimenet Az iostream könyvtár minden beépített típusra meghatároz kimenetet. A következõ kód például az 1 karaktert a 0 karakterrel követve a szabványos kimeneti adatfolyamba helyezi. Alapértelmezésben a cout-ra kerülõ kimeneti értékek karaktersorozatra alakítódnak át. ha tehát egy nevet használunk onnan. 3. E könyvben majdnem kizárólag a standard könyvtárat használjuk.60 Bevezetés #include<string> using namespace std.

Karakterláncok A standard könyvtár gondoskodik a string (karakterlánc) típusról. Az adatfolyamok részletes magyarázata a 21. a kimenet a következõ lesz: i értéke 10 A karakterkonstans egy karakter. amikor több rokon tételt kell kiírni. hogy kiegészítse a korábban használt karakterliterálokat. } cout << s3. egyszeres idézõjelek közé zárva. Kirándulás a standard könyvtárban 61 Ha i értéke 10. } Ez egyenértékû h()-val. Szerencsére maguk a kimeneti kifejezések eredményei felhasználhatók további kimenetekhez: void h2(int i) { cout << "i értéke " << i << '\n'.5. 3. void m1() { string s3 = s1 + ". string s2 = "világ". ilyen például az összefûzés : string s1 = "Helló". . cout << 'b'. } A fenti kód kimenete például abc lesz.Az ember hamar belefárad a kimeneti adatfolyam nevének ismétlésébe. " + s2 + "!\n". hanem karakterként íródnak ki: void k() { cout << 'a'.3. hogy a karakterkonstansok nem számértékként. Vegyük észre. fejezetben található. A string típus egy sereg hasznos karakterlánc-mûveletet biztosít. cout << 'c'.

Ezt a += mûvelet közvetlenül támogatja: void m2(string& s1. Például: string name = "Niels Stroustrup".substr(6.. de az utóbbit elõnyben részesítjük. mert tömörebb és valószínûleg hatékonyabban valósítható meg. string& s2) { s1 = s1 + '\n'. . void m3() { string s = name.5..replace(0. } // s = "Stroustrup" // a név új értéke "Nicholas Stroustrup" lesz A substr() mûvelet egy olyan karakterláncot ad vissza. fejezet írja le. Az elsõ paraméter egy. mely a paramétereivel megadott részlánc másolata.. //varázsszó // varázslás megkezdése } A standard könyvtár string osztályát a 20.Természetesen a karakterláncok összehasonlíthatók egymással és literálokkal is: string incantation. világ! A karakterláncok összeadása összefûzést jelent. Sok alkalmazásban az összefûzés legáltalánosabb formája valamit egy karakterlánc végéhez fûzni. a karakterlánc egy adott helyére mutató sorszám. // sortörés } A lánc végéhez való hozzáadás két módja egyenértékû. void respond(const string& answer) { if (answer == incantation) { } else if (answer == "yes") { // . // sortörés s2 += '\n'. A karakterláncokhoz karakterliterálokat és karaktereket adhatunk.62 Bevezetés Az s3 kezdeti értéke itt a következõ karaktersorozat (új sorral követve): Helló.. } // ."Nicholas"). Ez más hasznos tulajdonságai mellett lehetõvé teszi a részláncok (substring) kezelését is.10). name.

cin >> i.7). amelyet helyettesít. Vegyük észre.c_str()). // egész szám beolvasása i-be // kétszeres pontosságú lebegõpontos szám beolvasása d-be } .C stílusú karakterláncok A C stílusú karakterlánc egy nulla karakterrel végzõdõ karaktertömb (§5. például 1234-et olvas be a szabványos bemenetrõl az i egész változóba és egy lebegõpontos számot.name. 5 hosszúságú részlánc a Niels.3. mint az a részlánc. hogy felhasználói típusokkal is meg tudjon birkózni. Az alábbi kód egy számot.8) például az alábbi módon írathatjuk ki: void f() { printf("name: %s\n". A >> jobb oldalán álló típus határozza meg. Kirándulás a standard könyvtárban 63 a második a kívánt részlánc hossza.34e5-öt ír a kétszeres pontosságú.A replace() mûvelet a karakterlánc egy részét helyettesíti egy másik karakterlánccal. milyen bemenet fogadható el és mi a beolvasó mûvelet célpontja.2). ez helyettesítõdik a Nicholas-szal. Ebben az esetben a 0-val induló. s a Stroustrup értéket kapja. lebegõpontos d változóba: void f() { int i. Meg fogjuk mutatni.5. A name-et a printf() kiíró C-függvénnyel (§21.1. Bemenet A standard könyvtár bemenetre az istreams-et ajánlja.6. A name végsõ értéke tehát Nicholas Stroustrup. mondjuk 12.5. Az ostreams-hez hasonlóan az istreams is a beépített típusok karaktersorozatként történõ ábrázolásával dolgozik és könnyen bõvíthetõ. A >> („olvasd be”) mûveleti jelet bemeneti operátorként használjuk.3. A C stílusú karakterláncokat kezelõ függvények meghívásához képesnek kell lennünk egy string értékének C stílusú karakterlánc formában való kinyerésére. A c_str() függvény ezt teszi (§20. a cin a szabványos bemeneti adatfolyam. hogy egy C stílusú karakterláncot könnyen bevihetünk egy string-be. hogy a helyettesítõ karakterláncnak nem kell ugyanolyan méretûnek lennie. cin >> d. } 3. Mivel a sorszámozás (az index) 0-tól indul. 3. double d.

A switch utasítás egy értéket hasonlít össze állandókkal.54. break. float x. Ennek kényelmes módja egy string -be való helyezés: int main() { string str.64 Bevezetés A következõ példa hüvelykrõl centiméterre és centiméterrõl hüvelykre alakít. Ha az ellenõrzött érték egyikkel sem egyezik. default: in = cm = 0. char ch = 0. cm = x. case 'c': in = x/factor. break. } } // lebegõpontos szám beolvasása // mértékegység beolvasása // inch (hüvelyk) // 1 hüvelyk 2. . cin >> ch. A programozónak nem kell szükségszerûen errõl az alapértelmezett lehetõségrõl gondoskodnia. cin >> x. cm. cout << "Írja be a hosszúságot: ". switch (ch) { case 'i': in = x. break. cm = x*factor. Gyakran akarunk karaktersorozatot olvasni. melynek végén egy karakter jelzi az egységet (centiméter vagy hüvelyk). a vezérlés a default-ot választja. Bemenetként egy számot kap. A break utasítások a switch utasításból való kilépésre valók.54 cm-rel egyenlõ // cm cout << in << " in = " << cm << " cm\n". A case konstansoknak egymástól különbözniük kell. in. A program válaszul kiadja a másik egységnek megfelelõ értéket: int main() { const float factor = 2.

Erik! A getline() függvény segítségével egész sort is beolvashatunk: int main() { string str. cout << "Helló.2). } E programmal az alábbi bemenet Erik a Véreskezû a kívánt kimenetet eredményezi: Helló.str). cout << "Írja be a nevét!\n". " << str << "!\n". cout << "Helló. getline(cin. tehát ha beírjuk a hírhedt király nevét Erik a Véreskezû a válasz marad Helló. Kirándulás a standard könyvtárban 65 } cout << "Írja be a nevét!\n". " << str << "!\n". Ha begépeljük a következõt Erik a válasz az alábbi lesz: Helló. cin >> str. például egy szóköz fejezi be.3. Erik a Véreskezû! . Erik! Alapértelmezés szerint az olvasást egy „üreshely” (whitespace) karakter (§5.5.

hogy rugalmasan bõvítik a tartalmukat azzal. Az olyan osztályt. int number. }. melynek fõ célja objektumok tárolása. Vektor Sok C programozó számára alkalmas kiindulásnak látszana egy beépített (név. ha kisebbet.7. 3. amely neveket és telefonszámokat tárol. amit beviszünk. Tárolók Sok számítás jár különbözõ objektumformákból álló gyûjtemények (collection) létrehozásával és kezelésével.number << '\n'. Ez az a fajta program. Entry phone_book[1000].1. void print_entry(int i) // egyszerû használat { cout << phone_book[i]. konténer) nevezzük. Adott feladathoz megfelelõ tárolókról gondoskodni és ezeket támogatni bármilyen program építésénél nagy fontossággal bír. Mindkét esetben alacsonyszintû tárkezelõ kódot kell írnunk. majd a karakterlánc kiíratása. helyet pazarolunk. } A beépített tömbök mérete azonban rögzített.7. tehát ha néhány megabájtnyi pontosvesszõt adunk meg. a tömb túl fog csordulni. A standard könyvtár a vector típust (§16.66 Bevezetés A szabványos karakterláncoknak megvan az a szép tulajdonságuk. a program valóban több oldalnyi pontosvesszõt ad vissza – hacsak gépünk vagy az operációs rendszer valamilyen kritikus erõforrása elõbb el nem fogy. amely megoldja a fentieket: . A standard könyvtár leghasznosabb tárolóinak bemutatására nézzünk meg egy egyszerû programot. Ha nagy méretet választunk. amelynek változatai az eltérõ hátterû emberek számára is „egyszerûnek és maguktól értetõdõnek” tûnnek. általánosan tárolónak (container.3) bocsátja rendelkezésre. 3.name << ' ' << phone_book[i]. Egy egyszerû példa karakterek karakterláncba helyezése.vagy szám-) párokból álló tömb: struct Entry { string name.

a fordító majdnem biztos. Tehát f()-ben az elõkészítés (inicializálás) és értékadás után v és v2 is egy-egy külön másolatot tartalmaz a phone_book-ban lévõ minden egyes Entry-rõl. } A vector size() tagfüggvénye megadja az elemek számát. void print_entry(int i) // egyszerû használat. Ez nagyon különbözik a beépített tömbök bevezetésétõl: vector<Entry> book(1000)..3. Ahol a másolás nem kívánatos. // . hogy hibaüzenetet ad. Egyetlen vector<Entry> típusú objektumot hoztunk létre. A vector egy példánya objektum. mint a tömbnél { cout << phone_book[i]. Ha egy vektor sok elemet tartalmaz.name << ' ' << phone_book[i]. } A vector-ral való értékadás az elemek másolásával jár. ahol egy vector deklarálásában ()-t értettünk. v = v2. az ilyen ártatlannak látszó értékadások megengedhetetlenül „költségesek”. Kirándulás a standard könyvtárban 67 vector<Entry> phone_book(1000). Vegyük észre a () zárójelek használatát a phone_book definíciójában. vector<Entry> books[1000].size()+n).resize(phone_book. . melynek megadtuk a kezdeti méretét. referenciákat (hivatkozásokat) vagy mutatókat kell használni. amikor a vector-t használni próbáljuk.number << '\n'. // vektor 1000 elemmel // 1000 üres vektor Ha hibásan [ ] –t (szögletes zárójelet) használnánk ott. melynek értéket adhatunk: void f(vector<Entry>& v) { vector<Entry> v2 = phone_book.. } void add_entries(int n) // méret növelése n-nel { phone_book.

mely out_of_range típusú kivételt vált ki. Vec néven.3..number << '\n'.7. mint a vector. } . Ez nem kívánatos. hogy a tartományon kívüli hozzáféréseket elkapjuk: Vec<Entry> phone_book(1000).68 Bevezetés 3. Például: void f() { int i = phone_book[1001]. Visszatérve a nevek és telefonszámok tárolásának problémájához. mint a vektornál { cout << phone_book[i].12. } // tartományellenõrzés // tartományellenõrzés }. // . fejezet tárgyalja. hogy out_of_range típusú kivételt vált ki.3).3 és a 14. ha egy index kifut a tartományából. biztosítva.number.2.. most már használhatjuk a Vec-et.name << ' ' << phone_book[i]. ha paramétere kifut a vector tartományából (§16.3. A Vec olyan. ezért a soron következõ fejezetekben a vector egy egyszerû tartományellenõrzõ átalakítását fogjuk használni. Az itteni definíció azonban elegendõ a könyv példáihoz: template<class T> class Vec : public vector<T> { public: Vec() : vector<T>() { } Vec(int s) : vector<T>(s) { } T& operator[ ](int i) { return at(i). Az at(i) egy vector indexmûvelet. mint hogy hibát okoz. azzal a különbséggel. §8. void print_entry(int i) // egyszerû használat. A Vec-hez hasonló típusok megvalósítási módjait és a kivételek hatékony használatát §11.3). } const T& operator[ ](int i) const { return at(i). } // az 1001 kívül esik a tartományon A kezdeti értékadás valószínûleg inkább valamilyen véletlenszerû értéket tesz i-be. Tartományellenõrzés A standard könyvtárbeli vector alapértelmezés szerint nem gondoskodik tartományellenõrzésrõl (§16.

A kivételek okozta meglepetések csökkentésének egyik módja. .2. Amikor listát használunk. a cerr szabványos hibakimeneti adatfolyamon hibajelzés jelenik meg (§21. tehát ha elmulasztunk elkapni egy kivételt. i<10000.) { cerr << "ismeretlen kivétel\n". adott értékû elemet keresve. Ehelyett átkutathatjuk a listát.3. Lista A telefonkönyv-bejegyzések beszúrása és törlése általánosabb lehet. } A kivétel „dobása” majd elkapása akkor történik. a program meghatározott módon befejezõdik. Ha a felhasználó nem kapja el ezt a fajta kivételt. } Ez gondoskodik az alapértelmezett kivételkezelõkrõl. ezért egy egyszerû telefonkönyv ábrázolására egy lista jobban megfelelne. ha a main() törzsében egy try blokkot hozunk létre: int main() try { // saját kód } catch (out_of_range) { cerr << "tartományhiba\n". Kirándulás a standard könyvtárban 69 A tartományon kívüli hozzáférés kivételt fog kiváltani. i++) print_entry(i).1). nem folytatja futását és nem vált ki meghatározatlan hibát.. } catch (out_of_range) { cout << "tartományhiba\n". az elemekhez nem sorszám alapján szeretnénk hozzáférni. melyet a felhasználó elkaphat: void f() { try { } for (int i = 0.3. 3. mint egy vektor: list<Entry> phone_book. } catch (.7.. amikor a phone_book[i]-re i==1000 értékkel történik hozzáférési kísérlet. ahogy a vektorok esetében általában tesszük.

melyek egy bejárót (iterátort) adnak vissza az elsõ. A map egy értékpár-tároló. A felhasználónak nem kell tudnia. A típus a tároló leírásának része és név szerint lehet hivatkozni rá. i != phone_book.8): void print_entry(const string& s) { typedef list<Entry>::const_iterator LI. A standard könyvtár nevezetesen a map típust biztosítja erre a feladatra (§17. Például: map<string. list<Entry>::iterator i) { phone_book.push_front(e). a törlést és az érték szerinti keresést.2).70 Bevezetés Ehhez kihasználjuk azt a tényt. // rövidítés referenciával if (s == e. illetve az utolsó utáni elemre (§16.push_back(e).1). a következõ elem ++i lesz. Ha nincs szükségünk egy tárolóelem módosítására. míg az s-t megtaláljuk vagy elérünk a lista végéhez. ++i) { Entry& e = *i. Minden standard könyvtárbeli tároló tartalmazza a begin() és end() függvényeket. pontosan milyen típusú egy szabványos tároló bejárója. return. // hozzáadás a lista elejéhez phone_book. // hozzáadás az ‘i' által mutatott elem elé } 3.begin().4.1) használjuk.4. és addig folytatódik.insert(i. for (LI i = phone_book. .vagy számpárokból álló listához keresõ kódot írni valójában igen fáradságos munka. ami nekünk kell.7. Az i változó a *i elemre hivatkozik.end(). // hozzáadás a lista végéhez phone_book. Más adatszerkezetek közvetlenül támogatják a beszúrást. Ha adott egy i bejáró. a const_iterator az a típus. Ezenkívül a sorban történõ keresés – a legrövidebb listák kivételével – nagyon rossz hatékonyságú. Asszociatív tömbök Egy név. Elemek hozzáadása egy list-hez igen könnyû: void add_entry(const Entry& e.int> phone_book.name << ' ' << e. } } } Az s keresése a lista elejénél kezdõdik.e).number << '\n'.3. hogy a list egy sorozat (§3. Különben a sima iterator típust (§16.3.name) { cout << e.

hogy a programozók olyan tárolót válasszanak. ami lehetõvé teszi.4. A vektorokat indexelni „olcsó” és könnyû. azzal a kivétellel.3) Érték szerint rendezett sor (§17.2) .3.3) Halmaz (§17.3) Kétirányú láncolt lista (§17. mapped type) megfelelõ értékét adja vissza: void print_entry(const string& s) { if (int i = phone_book[s]) cout << s << ' ' << i << '\n'. Szabványos tárolók Az asszociatív tömb. Itt feltételezzük. Mindegyiknek megvannak az erõs és gyenge oldalai. } Ha nem talál illeszkedést az s kulcsra. A map emlékeztet egy (kulcs–érték) párokból álló listára.2. A map alapértéke int típusra 0. A lista tulajdonságai ezzel pontosan ellentétesek. hogy a 0 nem érvényes telefonszám. a map a második típus (az érték. a phone_book egy alapértéket ad vissza.3) Halmaz.3. vagyis a hozzárendelt típus.4) Asszociatív tömb (§17.3.érték> Multimap<kulcs.7.4. A standard könyvtár rendelkezik a legáltalánosabb és leghasználhatóbb tárolótípusokkal. hogy értékei a kulcs szerinti kereséshez a legmegfelelõbbek. Ha elsõ típusával (a kulccsal. mely az adott alkalmazás igényeit a legjobban kiszolgálja: Szabványos tárolók összefoglalása Vector<T> list<T> Queue<T> Stack<T> Deque<T> Priority_queue<T> set<T> Multiset<T> Map<kulcs. a lista és a vektor mind használható telefonkönyv ábrázolására.2) Verem (§17. key) indexeljük.1) Kétvégû sor (§17.3.érték> Változó hosszúságú vektor (§16. melyben egy kulcs többször elõfordulhat (§17. Kirándulás a standard könyvtárban 71 Más környezetekben a map mint asszociatív tömb vagy szótár szerepel.4.1) Asszociatív tömb. 3.2) Sor (§17.2. melyben egy érték többször is elõfordulhat (§17.4.5. Másrészt két eleme közé egyet beszúrni költségesebb lehet.

részhalmazokat vonunk ki belõlük. hogy a programozók új tárolótípusokat készítsenek. ami éppen a vector összes elemét jelenti.6) ennek egy példája. } A szabványos algoritmusok leírását a 18. fejezet bemutatja. A Vec tartományellenõrzéssel ellátott vektor (§3.end(). mint az elemek hozzáadása és eltávolítása.72 Bevezetés A szabványos tárolókat §16. elemeket távolítunk el. Az alapmûveletek általában mindenfajta tárolóra alkalmazhatók. melyek az elsõ. mely visszaadja az elemek a számát.begin(). A következõ kódrészlet például egy vector-t rendez és minden egyedi vector elem másolatát egy list-be teszi: void f(vector<Entry>& ve. hogyan lehet egy hash_map-et a szerkezethez hozzátenni. A tárolók az std névtérhez tartoznak.2).begin(). önmagukban nem túl hasznosak. fejállományokban szerepel.Ez a jelölésbeli és jelentésbeli egységesség lehetõvé teszi. Az ilyen sorozatok bejáró-párokkal ábrázolhatók. unique_copy(ve. <list>.end()-ig. Algoritmusok Az adatszerkezetek. ve. illetve az utolsó utáni elemet adják meg.end()).8. mint a list vagy a vector. Az algoritmusok elemek sorozatával mûködnek (§2. Íráshoz csak az elsõ írandó elemet szükséges megadni. és minden tárolónak van size() tagfüggvénye. az alábbit írhatnánk: . objektumokat keresünk bennük és így tovább. fejezet mutatja be. melyek a szabványos típusokhoz nagyon hasonló módon használhatók.3 és a 17.begin()-tõl ve. A push_back() például – meglehetõsen hatékony módon – egyaránt használható elemeknek egy vektor vagy lista végéhez fûzésére. 3. A példában a sort() rendezi a sorozatot.ve. Használatukhoz olyan alapvetõ hozzáférési mûveletekre van szükség. hogy az egyes tárolótípusoktól függetlenül adjunk meg algoritmusokat. A 17.ve.le. fejezetben találjuk.2. Rendezzük.7.begin()). <map> stb.7. leírásuk a <vector>. A szabványos tárolók és alapmûveleteik jelölés szempontjából hasonlóak. A tárolófelületek egységes volta emellett lehetõvé teszi azt is. Emellett ritkán használunk egy tárolót pusztán tárolásra. §16. a kezdõ elemet követõ elemek felülíródnak. kiíratjuk. list<Entry>& le) { sort(ve. Emiatt a standard könyvtár az általános tárolótípusokon kívül biztosítja a tárolók legáltalánosabb eljárásait is. Ha több mint egy elemet írunk. továbbá a mûveletek jelentése a különbözõ tárolókra nézve egyforma. Ha az új elemeket egy tároló végéhez kívánnánk adni.

az hibákhoz vezethet: void f(vector<Entry>& ve.begin().1. } // hozzáfûzés le-hez A back_inserter() elemeket ad egy tároló végéhez. list<Entry>& le) { sort(ve. i = find(i+1. while (i != s.end()) { ++n. string::const_iterator i = find(s. char c) // c elõfordulásainak megszámlálása s-ben { int n = 0. mi történik a count egyszerû meghívásakor: . copy(ve.end().3.end().end().c). list<Entry>& le) { copy(ve. Ha a find-ot használjuk.s. Kirándulás a standard könyvtárban 73 void f(vector<Entry>& ve. Ha a hozzáfûzéskor elfelejtjük a back_inserter()-t használni. hogy helyet csináljon részükre (§19.begin().ve. C stílusú realloc()-ot használó tárkezelést (§16.ve.8.3.le. } return n.end()).le). A szabványos tárolók és a back_inserter()-ek kiküszöbölik a hibalehetõséget jelentõ.2.c).back_inserter(le)).s. Ezenkívül sok algoritmus ad vissza bejárókat.ve. } A find algoritmus valamely érték egy sorozatban való elsõ elõfordulására vagy a sorozat utolsó utáni elemére mutató bejárót ad vissza.begin(). Lássuk.end(). A find szabványos algoritmus például egy sorozatban keres egy értéket és azt a bejárót adja vissza. a legjobb példa a begin() és az end().begin().ve. bõvítve a tárolót.ve.5). megszámlálhatjuk valamely karakter elõfordulásait egy karakterláncban: int count(const string& s. Bejárók használata Amikor elõször találkozunk egy tárolóval.begin(). unique_copy(ve. megkaphatjuk néhány hasznos elemére hivatkozó bejáróját (iterátorát).end()). amely a megtalált elemre mutat.le. copy(ve. } // hiba: le nem bejáró // rossz: túlír a végén // elemek felülírása 3.begin().end().4).end().begin()).

} Ez mûködik.end()-re. ++i.val). string s) { int i1 = count(lc. A ciklusban i+1-gyel kezdjük a keresést. így mondhatjuk: void f(list<complex>& lc.13. vagyis eggyel a megtalált 'a' után. // az elõbb megtalált elem átugrása i = find(i.'a'). Ezt a count() hívást grafikusan így ábrázolhatnánk: M a r y h a d a l i t t l e l a m b A nyilak az i kezdeti.5 int n = 0.end() feltétel és kilépünk a ciklusból. lásd §C. int a_count = count(m.end()) { ++n. } return n. } A find() elsõ hívása megtalálja 'a'-t a Mary-ben. // typename.v. Ezután folytatjuk a ciklust és megtaláljuk a másik három 'a'-t.3)).74 Bevezetés void f() { string m = "Mary had a little lamb". így beléptünk a ciklusba."Diogenész").'x'). A bejáró tehát e karakterre mutat és nem az s.begin().end(). Következésképpen ugyanilyen módon általánosíthatnánk a count() függvényt: template<class C.v. int i2 = count(vc. Természetesen a find algoritmus minden szabványos tárolón egyformán fog mûködni. class T> int count(const C& v. T val) { typename C::const_iterator i = find(v. A find() ekkor elér a sorozat végéhez és az s.complex(1. int i3 = count(s. while (i != v. } . vector<string>& vc.end()-et adja vissza.end().val). közbensõ és végsõ értékeit mutatják. így nem teljesül az i!=s.

mivel az nagyon ésszerû hivatkozási mód a vector elemeire: bejáró: vektor: p P i e t H e i n .begin().begin(). melyeket kiszolgálnak.end().3.lc. int i3 = count(s. melyre az adott tárolótípusnál feladata ellátásához szüksége van. hogy a számlálást beépített tömbre használjuk és azt is. } // 'z'-k a tömbben // 'z'-k a tömb elsõ felében 3. Az elemek elõfordulásainak megszámlálása annyira általános és hasznos. Egy vector bejárója például minden bizonnyal egy közönséges mutató. Azonban sok különbözõ típusuk létezik.'x'). Ezek a bejárótípusok olyan különbözõk lehetnek.2. hogy a standard könyvtár tartalmazza ezt a mûveletet. Hogy teljesen általános legyen a megoldás. string s) { int i1 = count(lc.end()."Diogenész").&cs[sz]. mint a tárolók és azok az egyedi igények. int i2 = count(vs.3)).complex(1. } A sorozat használata megengedi.&cs[sz/2].8.'z').s. Bejárótípusok Valójában mik is azok a bejárók (iterátorok)? Minden bejáró valamilyen típusú objektum. mert a bejárónak azt az információt kell tárolnia. hogy egy tároló valamely részét számláljuk: void g(char cs[ ]. int i2 = count(&cs[0]. vector<string>& vs.vs.'z'). Kirándulás a standard könyvtárban 75 A count sablont azonban nem kell definiálnunk. a standard könyvtári count paraméterként tároló helyett egy sorozatot kap: void f(list<complex>& lc.begin(). int sz) { int i1 = count(&cs[0].end().

Saját bejáró-típusait minden tároló ismeri és azokat az egyezményes iterator és const_iterator neveken rendelkezésre bocsátja. . amely néhány. Hasonlóképpen * azt az elemet adja meg. Továbbá. a felhasználó ritkán kell. mely a következõ elemre hivatkozik. mint egy egyszerû mutató a listában tárolt elemre. cs.2. mint a vector-ra hivatkozó mutató.76 Bevezetés Egy vector-bejáró megvalósítható úgy is. Például a list<Entry>::iterator a list<Entry> általános bejárótípusa. melyre a bejáró hivatkozik. az elõzõekhez hasonló egyszerû szabálynak eleget tesz (§19. Valójában bejáró lehet bármely objektum..1). meg egy sorszám: bejáró: (kezdet == p. pozició == 3) vektor: P i e t H e i n Az ilyen bejárók használata tartományellenõrzésre is lehetõséget ad (§19.3). A lista-bejáró tehát inkább a lista valamely csomópontjára hivatkozó mutató: bejáró: p lista: csomópont cs. . cs. mivel egy ilyen elem általában nem tudja. elemek: P i e t Ami közös minden bejárónál.. A listák bejáróinak valamivel bonyolultabbnak kell lenniük. az a jelentésük és a mûveleteik elnevezése. hol van a lista következõ tagja. A ++ alkalmazása bármely bejáróra például olyan bejárót ad. hogy ismerje egy adott bejárótípusát. Ritkán kell aggódnunk az adott típus meghatározása miatt.

Bemeneti és kimeneti bejárók A bejárók fogalma általános és hasznos a tárolók elemeibõl álló sorozatok kezelésénél. ami hamarosan magától értetõdõ lesz – ha eddig még nem lenne az. ahol elemek sorozatát találjuk. Például: int main() { *oo = "Helló. hogy a bemeneti adatfolyamot úgy kezeljük. Hasonlóképpen az istream_iterator olyasvalami.3. Megadhatunk például egy bejárót. és értékek sorozatát írjuk a kimeneti adatfolyamba is. A bemeneti adatfolyamok is értékek sorozatából állnak.8.3. Az értékadás *oo-nak azt jelenti. Következésképpen a bejárók hasznosan alkalmazhatók a bemenetnél és kimenetnél is. // jelentése cout << "Helló. *oo = "világ!\n". " ++oo. . A ++oo célja: utánozni egy tömbbe mutatón keresztül történõ írást. // jelentése cout << "világ!\n" } Ez egy újabb mód szabványos üzenetek kiírására a szabványos kimenetre. A tárolók azonban nem az egyetlen helyet jelentik. Itt is meg kell adnunk a használni kívánt adatfolyamot és a várt értékek típusát: istream_iterator<string> ii(cin). hogy a kimenetet úgy kezeljük. Az alapértelmezett istream_iterator a következõ: istream_iterator<string> eos. Mivel a bejárók mindig párokban ábrázolnak egy sorozatot. A szerzõ elsõnek nem ezt a módot választaná erre az egyszerû feladatra. mely a cout szabványos kimeneti adatfolyamra hivatkozik: ostream_iterator<string> oo(cout). melyik adatfolyamot használjuk és milyen típusú objektumokat írunk bele. Kirándulás a standard könyvtárban 77 3. hogy az értékadó adatot a cout-ra írjuk ki. de ez az eszköz alkalmas arra. mint egy csak olvasható tárolót. Egy ostream_iterator létrehozásához meg kell határoznunk. mint egy csak írható tárolót. a bemenet végének jelzéséhez egy istream_iterator-ról kell gondoskodni. ". ami lehetõvé teszi.

// a kettõzött értékek elvetése // hibaállapot visszaadása (§3.és célfájl nevének beolvasása // bemeneti adatfolyam (c_str().begin().3. vector<string> b(ii.eof() || !os.3) } Az ifstream egy istream.begin(). Az ostream_iterator második paramétere a kimeneti értékeket elválasztó jel. istream_iterator<string> eos.eos). Jellemzõen algoritmusok paramétereiként szolgálnak. majd az eredményt egy másik fájlba írja: int main() { string from. ifstream is(from."\n"). melynek a bemenetrõl adunk kezdõértéket // az átmeneti tár (b) rendezése ofstream os(to. sort(b. // a forrás.5. ++ii. // kimeneti bejáró az adatfolyam számára unique_copy(b.b.oo). cin >> from >> to. // b tartalmának a kimenetre másolása. mely egy fájlból olvas. mely szintén egy fájlhoz kapcsolható.c_str()).1 és §20. istream_iterator<string> ii(is). . } cout << s1 << ' ' << s2 << '\n'.end()). lásd §3. mely egy fájlhoz kapcsolható.end(). Írjunk például egy egyszerû programot. to.3.c_str()). §21. // kimeneti adatfolyam ostream_iterator<string> oo(os. string s2 = *ii. Valójában az istream_iterator-okat és ostream_ iterator-okat nem közvetlen használatra találták ki. az ofstream pedig egy ostream.2. világot!”-ot a bemenetrõl és kiírathatnánk az alábbi módon: int main() { string s1 = *ii.78 Bevezetés Most újra beolvashatnánk a „Helló.7) // bemeneti bejáró az adatfolyam számára // bemenet-ellenõrzés // b egy vektor. az olvasott adatokat rendezi. return !is. a kétszer szereplõ elemeket eltávolítja.b.

szeretnénk az összegyûjtött adatokat a kimenetre küldeni. mely a bemenetrõl szavakat olvas és feljegyzi elõfordulásuk gyakoriságát.record). Tegyük fel. hogy egy adott függvényt a sorozat minden egyes elemére meghívjunk. A pair elsõ eleme const string. Következésképpen szeretnénk meghívni az alábbi függvényt void print(const pair<const string.eos. ezért a standard könyvtár módot ad arra.int> histogram.8. Bejárások és predikátumok A bejárók lehetõvé teszik. A map (string.3. A ciklusok megírása azonban fáradságos lehet. for_each(ii. Kirándulás a standard könyvtárban 79 3. hogy ciklusokat írjunk egy sorozat bejárására.second << '\n'.4.int>& r) { cout << r.end(). nem sima string. // "s'' gyakoriságának rögzítése } Ha beolvastuk a bemenetet.histogram.first << ' ' << r. A karakterláncok és a hozzájuk tartozó gyakoriságok kézenfekvõ ábrázolása egy map-pel történhet: map<string.begin(). mert minden map kulcs konstans. A fõprogram tehát a következõ: int main() { istream_iterator<string> ii(cin). második elemét second-nak nevezzük).int) párokból álló sorozat. Az egyes karakterláncok gyakoriságának feljegyzésére természetes mûvelet a következõ: void record(const string& s) { histogram[s]++. hogy írunk egy programot.print). } a map minden elemére (a párok (pair) elsõ elemét first-nek. } . for_each(histogram. istream_iterator<string> eos.

hogy nem kell rendeznünk a map-et ahhoz.end(). hogy a kért elemet megtalálta.gt_42). Például meg akarjuk keresni egy map elsõ 42-nél nagyobb értékét: bool gt_42(const pair<const string. jelezvén. mint 42: void g(const map<string. } Az olyan függvényeket.gt_42). vezérlõfüggvény. hogy meg kell keresni valamit egy tárolóban. melyek továbbiak alkotására használhatók (§18. mely egy bizonyos követelménynek felel meg. } Máskor megszámlálhatnánk azon szavakat. Ennek az ötletnek egy általánosabb változata olyan elemet keres.m. // . A standard könyvtár néhány hasznos predikátumot is biztosít. predicate) nevezzük. } void f(map<string. predikátumnak („állítmány”. ahányszor a predikátuma true. melyet az algoritmus szándékolt tevékenységének elvégzéséhez felhasznál.5.end(). Ezek minden elemre meghívódnak és logikai értéket adnak vissza.. melyek gyakorisága nagyobb.int>& r) { return r.begin().. A map rendezve tárolja az elemeket és a ciklus is (növekvõ) sorrendben járja végig a map-et.int>& m) { int c42 = count_if(m. amíg a predikátuma true-t nem ad vissza. A find algoritmus (§18.2) kényelmes módot ad egy adott érték megkeresésére. Hasonló módon a count_if() annyit számlál. ahelyett. valamint olyan sablonokat..80 Bevezetés Vegyük észre.begin(). . melyet az algoritmus vezérlésére használunk. hogy minden elemen végrehajtanánk egy feladatot.int>& m) { typedef map<string.int>::const_iterator MI.second>42. MI i = find_if(m.. hogy a kimenet rendezett legyen.m.2). mint a gt_42(). A find_if() például addig keres. Sok programozási feladat szól arról.4. // .

2) paraméterként egy tagfüggvény mutatóját kapja (§15. Kirándulás a standard könyvtárban 81 3.8. Például §3. és sokkal inkább a hivatkozott objektum egy tagfüggvényét szeretnénk meghívni.begin(). a mutatót paraméterként átadva. hogy a szabványos algoritmusokat többalakú (polimorf) objektumok tárolóira használjuk.8. a bemenetrõl beolvasott karakterláncra. .end(). mert megengedi. nem pedig egy globális függvényt. Tegyük fel.begin().3.4-ben a for_each(ii. } void f(list<Shape*>& sh) { for_each(sh. mely meghívja a tagfüggvényt: void draw(Shape* p) { p->draw(). A példa kezelésére egyszerûen egy nem tag függvényt írunk.eos. hogy a Shape::draw() tagfüggvényt akarjuk meghívni egy list<Shape>* elemeire.4. } A standard könyvtári mem_fun() sablon (§18. A mem_fun(&Shape::draw) eredménye egy Shape* paramétert kap és visszaadja.sh.record). amit a tag osztályára hivatkozó mutatón keresztül hívhatunk meg. meghívja a record()-ot minden egyes. Tagfüggvényeket használó algoritmusok Sok algoritmus alkalmaz függvényt egy sorozat elemeire.5) és valami olyasmit hoz létre.sh. Gyakran mutatók tárolóival van dolgunk. A mem_fun() azért fontos.5.end(). } A módszert így általánosíthatjuk: void g(list<Shape*>& sh) { for_each(sh.draw).4. amit a Shape::draw() visszaad.mem_fun(&Shape::draw)).

A standard könyvtár algoritmusai Mi az algoritmus? Általános meghatározása szerint „szabályok véges halmaza. A C++ standard könyvtárának viszonylatában az algoritmus elemek sorozatán mûveleteket végzõ sablonok (template-ek) halmaza. §1.1) Rendezd az elemeket (§18. bemenet.6.1968. A standard könyvtár több tucat algoritmust tartalmaz.1].7.3) Helyettesítsd be az elemet új értékkel (§18.9.2) Számláld meg az elem elõfordulásait (§18. Matematika A C-hez hasonlóan a C++ nyelvet sem elsõsorban számokkal végzett mûveletekre tervezték. kimenet. meghatározottság.2) Fésüld össze a rendezett sorozatokat (§18.5.7. Az algoritmusok az std névtérhez tartoznak. leírásuk az <algorithm> fejállományban szerepel.6.2) Keresd meg a predikátumra az elsõ illeszkedést (§18.5.1) Keresd meg a paraméterek elsõ elõfordulását (§18.7.3) 3.6.1) Keresd meg az összes egyezõ értékû elemet (§18.5.4) Másold az elemeket (§18. mely adott problémahalmaz megoldásához mûveletek sorozatát határozza meg és öt fontos jellemzõje van: végesség. melyeket különösen hasznosnak találtam: Válogatott szabványos algoritmusok for_each() find() find_if() count() count_if() replace() replace_if() copy() unique_copy() sort() equal_range() merge() Hívd meg a függvényt minden elemre (§18.3) Számláld meg az illeszkedéseket a predikátumra (§18.82 Bevezetés 3.5. Mindemellett rengeteg numerikus munkát végeztek C++-ban és ez tükrözõdik a standard könyvtárban is.6.4) Helyettesítsd be a predikátumra illeszkedõ elemet új értékkel (§18. . Íme néhány.8.5. hatékonyság” [Knuth.1) Másold a csak egyszer szereplõ elemeket (§18.6.

1-ben leírt vector-t általános értéktárolásra tervezték. Kirándulás a standard könyvtárban 83 3. a kétszeres pontosságú (double) stb. Emiatt a standard könyvtárban megtaláljuk a valarray nevû vektort is.2).2-ben leírt complex osztály alakjában.. fl = pow(1/fl. T& operator[ ](size_t). int).5.2. A szokásos aritmetikai mûveletek és a leggyakrabban használt matematikai függvények komplex számokkal is mûködnek: // szabványos exponenciális függvény a <complex> sablonból: template<class C> complex<C> pow(const complex<C>&. de az általánosság és rugalmasság eleve kizár olyan optimalizálásokat. // .. Az egyszeres pontosságú lebegõpontos (float).. }. // .5. db += fl*3.1.. bejárók és algoritmusok szerkezetébe. ugyanakkor nem támogatja a matematikai vektormûveleteket. mely kevésbé általános és a számmûveletekhez jobban megfelel: template<class T> class valarray { // . // . Ilyen mûveleteket könnyen be lehetett volna építeni a vector-ba. 3.9..7. Komplex számok A standard könyvtár a komplex számok egy típuscsaládját tartalmazza.. a §2. }. void f(complex<float> fl.3. skalárokat tartalmazó komplex számok támogatására a standard könyvtárbeli complex egy sablon: template<class scalar> class complex { public: complex(scalar re. } Részletesebben lásd §22. complex<double> db) { complex<long double> ld = fl+sqrt(db). scalar im). melyeket komolyabb.9.. kellõen rugalmas és illeszkedik a tárolók. . Vektoraritmetika A §3.. számokkal végzett munkánál gyakran lényegesnek tekintünk.

4. 3.84 Bevezetés A size_t elõjel nélküli egész típus.fejezet. fejezet.. 2. A standard könyvtár szolgáltatásai A standard könyvtár szolgáltatásait az alábbi módon osztályozhatjuk: 1. Alapszintû numerikus támogatás A standard könyvtár a lebegõpontos típusokhoz természetesen tartalmazza a leggyakoribb matematikai függvényeket (log(). void f(valarray<double>& a1.2. Tárolók (vector. melyet a nyelv tömbök indexelésére használ.1.3. a2 += a1*3.14. 3. lásd §16. lásd §16. lásd 16.. . list és map) és tárolókat használó algoritmusok (általános bejárások.3). tárlefoglalás és futási idejû típusinformáció).. A szokásos aritmetikai mûveleteket és a leggyakoribb matematikai függvényeket megírták a valarray-kre is: // szabványos abszolútérték-függvény a <valarray> sablonból: template<class T> valarray<T> abs(const valarray<T>&).. rendezések és összefésülések) rendszere. a = abs(a). valarray<double>& a2) { valarray<double> a = a1*3. 17. és 19.9. lásd 20. A szabványos C könyvtár (nagyon csekély módosításokkal.1.14+a2/a1. 18. Ezenkívül azonban tartalmaz olyan osztályokat is. pow() és cos().4 3. és 21. } Részletesebben lásd: §22. // .3.10. double d = a2[7].2).2. melyek beépített típusok tulajdonságait – például egy float kitevõjének lehetséges legnagyobb értékét –írják le (lásd §22. Karakterláncok és bemeneti/kimeneti adatfolyamok (nemzetközi karakterkészlet és nyelvi támogatással). a típusrendszer megsértésének elkerülésére). lásd §2. Alapvetõ futási idejû támogatás (pl.

és hogy könnyen megtanulható-e a használata. Számokkal végzett mûveletek támogatása (komplex számok és vektorok aritmetikai mûveletekkel).7. [3] Amikor választhatunk.7. §3. §3. Értsük meg. és ezeket azonnal mûködtethetik is a szabványos tárolókkal és algoritmusokkal együtt.7.4.value>-t a T[ ]-vel szemben. hogy nem jelent-e jelentõs többletterhelést ugyanennek a szolgáltatásnak valamely egyszerûbb változatához viszonyítva.2. Kirándulás a standard könyvtárban 85 5. §3. A C++ standard könyvtára tehát az alapvetõ adatszerkezeteket és az azokon alkalmazható alapvetõ algoritmusokat tartalmazza. . [9] Részesítsük elõnyben a vector<T>-t. [4] Ne gondoljuk. hogy valamilyen módon használta-e már majdnem minden C++ programozó (kezdõk és szakértõk egyaránt). hogy a standard könyvtár mindenre ideális. valamint az optimalizálást megkönnyítõ szerkezetek. lásd 22. Minden algoritmus átalakítás nélkül mûködik minden tárolóra. §3. Annak fõ feltétele. abban az értelemben. hogyan teszik.7.5.3. §3. az volt. [8] Ha kétségeink vannak. mit tesznek könyvtáraink. §3. [5] Ne felejtsük el beépíteni (#include) a felhasznált szolgáltatások fejállományait.3.3.11. 3. hogy a standard könyvtár szolgáltatásai az std névtérhez tartoznak. használjunk tartományellenõrzõ vektort (mint a Vec). BLAS-szerû és általánosított szeletek.6. Ez az egyezményesen STLnek (Standard Template Library. fejezet. és milyen áron teszik. [7] Használjunk string-et char* helyett. §3. 1994] nevezett váz bõvíthetõ. a list<T>-t és a map<key. részesítsük elõnyben a standard könyvtárat más könyvtárakkal szemben. hogy a felhasználók a könyvtár részeként megadottakon kívül könnyen készíthetnek saját tárolókat és algoritmusokat. hogy általános alakban megadható-e.1. §3. Tanácsok [1] Ne találjunk fel a melegvizet – használjunk könyvtárakat. szabványos sablonkönyvtár) [Stepanov. [2] Ne higgyünk a csodákban. [6] Ne felejtsük el. hogy egy osztály bekerülhet-e a könyvtárba.3.

[11] Használjunk vektoron push_back()-et a realloc() tömbre való alkalmazása helyett. §3. §3.8. használjunk push_back()-et vagy back_inserter()-t.86 Bevezetés [10] Amikor elemeket teszünk egy tárolóba.8.3. . §3.2. [12] Az általános kivételeket a main()-ben kapjuk el.7.7. §3.

6. 5. A C++-nak a C nyelvre visszautaló része a hagyományos programozási stílusok támogatásával együtt kerül bemutatásra. tömbök és struktúrák Kifejezések és utasítások Függvények Névterek és kivételek Forrásfájlok és programok . Típusok és deklarációk Mutatók. valamint ez a rész tárgyalja azokat az alapvetõ eszközöket is. Fejezetek 4. 9. amelyekkel programokat hozhatunk létre.Elsõ rész Alapok Ez a rész a C++ beépített típusait és azokat az alapvetõ lehetõségeket írja le. 7. amelyekkel C++ programot hozhatunk létre logikai és fizikai elemekbõl. 8.

.

Parkinson) Típusok • Alaptípusok • Logikai típusok • Karakterek • Karakterliterálok • Egészek • Egész literálok • Lebegõpontos típusok • Lebegõpontos literálok • Méretek • void • Felsoroló típusok • Deklarációk • Nevek • Hatókörök • Kezdeti értékadás • Objektumok • typedef-ek • Tanácsok • Gyakorlatok .4 Típusok és deklarációk „Ne fogadj el semmit. ami nem tökéletes!” (ismeretlen szerzõ) „A tökéletesség csak az összeomlás pontján érhetõ el.N.” (C.

kezdõértéke 7 // f egész paramétert váró és lebegõpontos számot visszaadó függvény deklarációk már értelmessé teszik a fenti példát. y és f neveket megfelelõen definiálni kell. a + (összeadás) és a () (függvényhívás) rendre értelmezettek.1. nem feltétlenül végeznek hasznos dolgokat. A terjedelmesebb és valósághûbb példák a késõbbi fejezetekben kerülnek sorra. így meg lehet hívni a megfelelõ paraméterrel. értékül lehet adni. Például a float x. azaz a programozónak meg kell adnia. Ez a fejezet egyszerûen csak azokat az alapelemeket írja le. és f nevû egyedek léteznek és olyan típusúak. . de fõleg azért. A példák csak a nyelv tulajdonságait szemléltetik.90 Alapok 4. amelynek egy int paramétere van. és késõbb visszatér. amelyre a név hivatkozik) és ezek a mûveletek mit jelentenek. hogy megértse a részleteket. int y = 7. ha csak átnézi. // x lebegõpontos változó // y egész típusú változó. A többi fejezet megértéséhez azonban nem szükséges teljesen átlátni ennek a fejezetnek minden apró részletét.9) mutatja be. hogy megértse a fontosabb fogalmakat.1. amint szükséges. amikor már többet ismertettünk a C++-ból. float f(int). milyen mûveleteket lehet végrehajtani a néven (azaz az egyeden. amelyekre az = (értékadás). a velük járó elnevezéseket és formai követelményeket. y. Ez a típus határozza meg. amelyekbõl a C++ programok létrehozhatók. az x. hogy ezek az x. Ez a fejezet az alapvetõ típusokat (§4. hogy valódi C++ programot készíthessünk. ahhoz is. Másfelõl f-et olyan függvényként határoztuk meg. hogy el tudjuk olvasni a mások által írt kódot. Hogy ez értelmes legyen valamely C++ programban.1) és deklarációkat (§4. kifejezést. Ismernünk kell ezeket az elemeket. Mivel y-t int-ként adtuk meg. A C++ programokban minden névnek (azonosítónak) van típusa. Típusok Vegyük az x = y+f(2). Következésképp az olvasó jobban teszi. használni lehet aritmetikai kifejezésekben stb.

2 Tömbtípusok (mint a char[ ]) §5. felsoroló típusokat adott értékhalmazok jelölésére (enum) Ezekbõl a típusokból más típusokat is létrehozhatunk. hogy a számítógép bájtokat biztosít a karakterek tárolásához. melyet az információ hiányának jelzésére használunk. az integrális és lebegõpontos típusokat pedig közösen aritmetikai típusoknak. mint az alaptípusok.3 §4. A többi típust beépített típusnak nevezzük. léteznek alkalmas egyedek lebegõpontos számításokhoz és címek számára. Típusok és deklarációk 91 4.4. a pontosságot.6). hogy hivatkozhassunk ezekre az egyedekre.1. fejezet) felhasználói adattípusokként emlegetjük. . és a számítási értéktartományt (§4.2 §4. lehetõvé téve a programozónak. A C++ alaptípusai a mutatókkal és tömbökkel együtt az adott nyelvi megvalósítástól függetlenül biztosítják ezeket a gépszintû fogalmakat a programozó számára.4 §4. amelyek megfelelnek a számítógép leggyakoribb tárolási egységeinek és adattárolási módszereinek: §4. gépi szót az egészek tárolására és az azokkal való számolásra.5 Referencia-típusok (mint a double&) §5. Azt feltételezzük. elõzetes bevezetés nélkül nem állnak rendelkezésre. hogy kiválaszthassa a felhasznált tár nagyságát.7 Adatszerkezetek és osztályok (10.5 Logikai típus (bool) Karaktertípusok (mint a char) Egész típusok (mint az int) Lebegõpontos típusok (mint a double) Továbbá a felhasználó megadhat: §4. Ezek a következõk: §5.1 Mutatótípusok (mint az int*) §5. A felsoroló típusokat és az osztályokat (10. mert a felhasználónak kell azokat meghatároznia. Az integrális és lebegõpontos típusok többfajta mérettel adottak. fejezet) A logikai. Alaptípusok A C++ rendelkezik azokkal az alaptípusokkal.7 void típus is.1. karakter.és egész típusokat együtt integrális típusoknak nevezzük.8 és létezik a §4.

Logikai típusok A logikai típusoknak (bool). lebegõpontos értékekhez pedig double-t. máskülönben hamis. két értéke lehet: true vagy false (igaz. bool greater(int a. akkor b1 igaz lesz. int b) { bool b1 = a==b. a false-é pedig 0. így b igaz // int(true) értéke 1. a 0 false lesz... == egyenlõségvizsgálat Ha a és b értéke ugyanaz. A többi alaptípus hatékonysági és más egyedi célokra használatos. a nem nulla egészek pedig true értéket kapnak. int b) { return a>b. amíg ilyen igények fel nem merülnek. de a régi C és C++ kódok olvasásához ismernünk kell õket. karakterekhez char-t. 4. illetve hamis). egészekhez int-et.2. így legjobb azokat elkerülni addig. } // = értékadás. Ugyanígy az egészek is logikai típusúvá alakíthatók: a nem nulla egészek true. // bool(7) igaz. A bool gyakran használatos olyan függvény visszatérési értékeként. így i értéke 1 Aritmetikai és logikai kifejezésekben a logikai típusok egésszé alakulnak.92 Alapok A legtöbb programban a logikai értékekhez egyszerûen bool-t használhatunk. } Egész számra alakítva a true értéke 1 lesz. A logikai típusokat logikai mûveletek eredményének kifejezésére használjuk: void f(int a. // . int i = true. az aritmetikai és logikai egész-mûveletek az átalakított értékeken hajtódnak végre. . a 0 false logikai értékké alakulnak: bool b = 7. Ha az eredmény visszaalakul logikai típusúvá. amely valamilyen feltételt ellenõriz (predikátum): bool is_open(File*).

Karaktertípusok A char típusú változók egy karaktert tárolhatnak az adott nyelvi megvalósítás karakterkészletébõl: char ch = 'a'. Sok probléma származik abból. §21. §D). Ennél fontosabb kérdés. §C3.3. ez azonban a könyv keretein túlmutat. hogy az adott C++-változat karakterkészlete tartalmazza a decimális számjegyeket. A nem nulla mutatók true. Típusok és deklarációk 93 void g() { bool a = true. vagy hogy minden karakter rendelkezésre áll. az angol ábécé 26 betûjét és néhány általános írásjelet.7.5). így a billentyûzeten megjelenõ karaktereket tartalmazza. így x igaz // a|b értéke 1. bool b = true. mint az angolban (a legtöbb európai nyelvben több van). Jelentõsen eltérnek azok a karakterkészletek.2.2. és azok is. bár számos helyen említésre kerül (§20. Nem biztos. így 256 különbözõ értéket tárolhat. hogyan programozzunk többnyelvû. Biztosan feltehetõ. hogy az ábécé betûi összefüggõek (az EBCDIC lyukat hagy az „i” és a „j” között). // a+b értéke 2. hogy nincs több alfabetikus karakter. például ASCII.6. 4. a „nulla mutatók” false értékûek lesznek. Itt azonban csak az érdekel minket. hogy ezeket a karakterkészleteket csak részben szabványosították (§C. hogy ezek a különbségek hogyan befolyásolják a C++ szabályait.3.4. amelyek természetes nyelveket támogatnak. bool x = a+b. így y igaz } Logikai típusúvá mutatókat is alakíthatunk (§C.3). amelyek másféleképpen támogatják ugyanazt a természetes nyelvet. A karakterkészlet jellemzõen az ISO-646 egy változata. több karakterkészletes környezetben. A char típus általában 8 bites. hogy egy 8 bites karakterkészletben nincs 127-nél több karakter (néhány karakterkészlet 255 karaktert biztosít). bool y = a|b. ami a C++ kód írásához szüksé- .

amely a begépelt karakter egész értékét mutatja meg: #include <iostream> int main() { char c. Szerencsére csak a 0-127 tartományon kívüli értékekben lehet különbség és a leggyakoribb karakterek a tartományon belül vannak.3. Jegyezzük meg. amelyekre a kérdés biztosan megválaszolható: a signed char-t (elõjeles karakter).1. §C. mint 0-tól 255-ig vagy -127-tõl 127-ig terjedõ értékeket. nem pedig beépített típus. felvet egy kérdést: a char elõjeles vagy elõjel nélküli? A 8 bites bájton ábrázolt 256 értéket úgy lehet értelmezni. Íme egy kis program.3). Lásd még a §C.3. melyiket választja egy sima char esetében (§C.2) is. Mérete az adott C++-változattól függ. így alkalmazhatóak rájuk az aritmetikai és logikai mûveletek (§6. vagyis típus-álnév. A C-ben a wchar_t egy typedef (§4. A különös név még a C-bõl maradt meg. Az a lehetõség. amely legalább 0-tól 255-ig tud értékeket tárolni.4-et arra az esetre.1).1).9. Azok a 0-127 tartományon túli értékek. hogy karaktert egésszé lehet alakítani.7). nehezen felderíthetõ „hordozhatósági” problémákat okozhatnak. a „b”-é például az ASCII karakterkészletben 98. amelyeket egy sima char tárol. Az _t toldalék a szabványos typedef-ektõl való megkülönböztetést segíti. std::cout << "A(z) ' " << c << " ' értéke " << int(c) << '\n'.3. amely legalább a -127 és127 közti értékeket képes tárolni és az unsigned char (elõjel nélküli karakter) típust. §C. std::cin >> c. Sajnos az adott fordítóprogram dönti el. A nagyobb karakterkészletek – például a Unicode – karaktereinek tárolására a wchar_t áll rendelkezésünkre. } Az int(c) jelölés a c karakter egész értékét adja. amely önálló típus. Amikor csak lehetséges.7 és §C. hogy a szükséges legnagyobb karakterkészletet tárolhassa (lásd §21. vagy ha char típusú változókban szeretnénk egészeket tárolni.3.94 Alapok ges (néhány nemzeti karakterkészlet nem biztosítja a { } [ ] | \ karaktereket. hogy a karaktertípusok integrális típusok (§4. . de elég nagy ahhoz. Minden karakternek van egy egész értéke.1. ha többféle char típus szükséges.4). A C++ azonban ad két olyan típust. nem szabad semmit feltételeznünk az objektumok ábrázolásáról és ez az általános szabály a karakterekre is vonatkozik.

általában meghiúsulnak a mögöttes konverziós szabályok miatt (§C. A „széles” karakterliterálok L’ab’ alakúak. ha decimális jelölés helyett karakterliterálokat használunk.6. melyet gyakran karakterkonstansnak is hívnak. escape karakterként. egy egyszeres idézõjelek közé zárt karakter. A program hordozhatóságát javítja. A signed int típusok csak világosabban kifejezett szinonimái a nekik megfelelõ sima int típusoknak. például 'a' és '0'. az unsigned az unsigned int. ezek a \ fordított perjelt használják ún.3. ahol az egyszeres idézõjelek között lévõ karakterek számát és jelentését az adott C++-megvalósítás a wchar_t típushoz igazítja. §C.2. A karakterliterálok típusa char. Azok a próbálkozások. melyek értéke annak a számítógépnek a karakterkészletében lévõ karakter egész értéke. Ha például ASCII karakterkészlettel rendelkezõ számítógépet használunk.2-t. és unsigned int. Hasonlóan. A sima char-ral ellentétben a sima int-ek mindig elõjelesek. illetve long int.1). mivel a széles karakterliterálok típusa wchar_t.4.6.1. Karakterliterálok A karakterliterál. „sima” int . Valójában szimbolikus konstansok (jelképes állandók). Például a \n az új sort. amin a C++ program fut. Egy long intre lehet sima long-ként hivatkozni. Az egészek három méretben adottak: short int. signed int. 4. hogy a változót unsigned-ként adják meg. Az escape karakterekrõl részletesebben lásd §C. hogy egy vagy több bitet nyerjünk pozitív egészek tárolásához. a short a short int. .1. Az elõjel nélküli (unsigned) egész típusok olyan felhasználásra ideálisak. amely úgy kezeli a tárat.3. mint egy bittömböt. a '0' értéke 48 lesz. Szinte soha nem jó ötlet az elõjel nélküli típust használni int helyett. amelyek úgy kísérlik meg biztosítani valamilyen érték nem negatív voltát. a \t pedig a vízszintes tabulátort (behúzást) jelenti.4. Típusok és deklarációk 95 4. Néhány karakternek szintén van szabványos neve. Egész típusok A char-hoz hasonlóan az egész típusok is háromfélék: „sima” int. a signed pedig a signed int szinonimája.

Ha a literál nullával kezdõdik és számjeggyel folytatódik.4). a fordító egy olyan egész literált ad. Lebegõpontos típusok A lebegõpontos típusok lebegõpontos (valós) számokat ábrázolnak. ahogy elvárjuk tõlük: 7 1234 976 12345678901234567890 A fordítóprogramnak figyelmeztetnie kell olyan literálok esetében. oktális.1. Egy olyan gépen például. Egész literálok Az egész literálok négyféle alakban fordulnak elõ: decimális.96 Alapok 4. Jó ötlet korlátozni a nem maguktól értetõdõ állandók használatát néhány. double (kétszeres pontosságú). Ha nincs megadva utótag. 0xffff a -1 negatív decimális szám lesz. 12-t. illetve nagybetûs megfelelõik rendre 10-et. 13-at. 14et és 15-öt jelentenek. 4. ha ezekkel a jelölésekkel valódi számokat fejezünk ki. akkor ez 65 535 lett volna. d. Az oktális és hexadecimális jelölés leginkább bitminták kifejezésénél hasznos. c. Például 3 egy int. Meglepetéseket okozhat. 11-et. hexadecimális és karakterliterálként. Hasonlóan. megjegyzésekkel megfelelõen ellátott const (§5. 3U egy unsigned int és 3L egy long int. az L utótag használatos a long literálokhoz.4.5. A nullával kezdõdõ és x-szel folytatódó (0x) literálok hexadecimális (16-os számrendszerbeli) számok. amelynek típusa megfelel az értéknek és a megvalósítás egész-méreteinek (§C. .8) kezdeti értékadására. e és f betûk. b. Az egészekhez hasonlóan ezek is háromfajta méretûek lehetnek: float (egyszeres pontosságú). és long double (kiterjesztett pontosságú). A decimális literálok a leginkább használatosak és úgy néznek ki. ahol az int egy kettes komplemensû 16 bites egészként van ábrázolva. oktális (8-as számrendszerbeli) számról van szó: decimális: oktális: hexadecimális: 0 0x0 2 02 0x2 63 077 0x3f 83 0123 0x53 Az a. Az U utótag használatával elõjel nélküli (unsigned) literálokat adhatunk meg. amelyek túl hosszúak az ábrázoláshoz. Ha több bitet használtunk volna az egész ábrázolására.4) vagy felsoroló típusú (§4.

23 . vagy használjunk double-t és reméljük a legjobbakat.4. 4.9e-3f Ha long double típusú lebegõpontos literált szeretnénk megadni. A megfelelõ pontosság kiválasztása egy olyan problémánál.0L 2. ha a lebegõpontos literálok az ábrázoláshoz képest túl nagyok. akkor azt az f vagy F utótag használatával tehetjük meg: 3.43 e-21 például nem lebegõpontos literál. hanem négy különálló nyelvi egység (ami formai hibát okoz): 65.997925L 2. használjuk az l vagy L utótagot: 3.43 e 21 Ha float típusú lebegõpontos literált akarunk megadni. Méretek A C++ alaptípusainak néhány jellemzõje.14159265f 2. Típusok és deklarációk 97 Az egyszeres. Rámutatok ezekre a függõségekre és gyakran ajánlom. szánjunk idõt a megtanulására.2). hogy hatásukat csökkentsük.997925F 2. kétszeres és kiterjesztett pontosság pontos jelentése az adott C++-változattól függ.0f 2. a lebegõpontos számítások mély megértését követeli meg. Lebegõpontos literálok Alapértelmezés szerint a lebegõpontos literálok double típusúak. ahol fontos a választás. mint például az int mérete. A 65. Íme néhány lebegõpontos literál: 1. akik különbözõ rendszereken programoznak vagy többféle fordí- .2e10 1.6.23e-15 Jegyezzük meg.14159265L 2. hogy szóköz nem fordulhat elõ egy lebegõpontos literál közepén. a C++ adott megvalósításától függ (§C. kérjünk tanácsot. A fordítónak itt is figyelmeztetnie kell.9e-3L 4. hogy kerüljük õket vagy tegyünk lépéseket annak érdekében.0 1. 1.5.1. Miért kellene ezzel foglalkozni? Azok. Ha nem értünk a lebegõpontos aritmetikához.23 1.23 0.

hogy a char legalább 8. mint a mostani. amikor egy programot megírnunk. A megvalósításból eredõ nyelvi sajátosságok hatását viszonylag könnyû korlátozni. A C++ objektumainak mérete mindig a char méretének többszöröse. több elõjel nélküli. A programokat továbbá gyakran újra kell fordítani más fordítókkal ugyanarra a rendszerre és még a kedvenc fordítónk késõbbi változata is másképpen csinálhat néhány dolgot. Azok. Ez beszûkült látásmód. hogy idõt pazaroljanak nehezen megfogható programhibák megtalálására és kijavítására. Az alaptípusok méretére vonatkozóan a következõk garantáltak: 1 ≡ sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) 1 ≤ sizeof(bool) ≤ sizeof(long) sizeof(char) ≤ sizeof(wchar_t) ≤ sizeof(long) sizeof(float) ≤ sizeof(double) ≤ sizeof(long double) sizeof(N) ≡ sizeof(signed N) ≡ sizeof(unsigned N) A fentiekben N lehet char. Sokkal egyszerûbb ismerni és korlátozni az adott fordító használatának hatását. a short legalább 16. hogy a hardver jellemzõit megfelelõen kihasználhassa. általában azért teszik ezt. rákényszerülnének arra. továbbá biztosított. mert csak egy rendszert használnak és úgy érzik. megengedhetik maguknak azt a hozzáállást. int vagy long int. hogy átviszik más rendszerre. Egy objektum méretét a sizeof operátorral kaphatjuk meg (§6. hogy nem érdekli õket a hordozhatóság. hogy ez lehetõséget ad a programozónak. miszerint „a nyelv az. akkor valószínû. Annak. mint megpróbálni késõbb kibogozni a problémát. Az alábbi ábra az alaptípusok egy lehetséges halmazát és egy minta-karakterláncot mutat: . a rendszerfüggõ könyvtárakét azonban sokkal nehezebb. és több lebegõpontos típus van. általában könnyen kiválaszthatjuk például a megfelelõ egész típust egy adott változó számára. a long pedig legalább 32 bites.98 Alapok tót használnak. hogy lehetõleg csak a standard könyvtár elemeit használjuk. így a char mérete 1. Sok gépen jelentõs különbségek vannak a memóriaigényben. amit a fordítóm megvalósít”.2). A char a gép karakterkészletébõl egy karaktert tárolhat. az az oka. Ha egy program sikeres. a memória hozzáférési idejében. Az egyik módszer az lehet. és valakinek meg kell találnia és ki kell javítania a megvalósítás sajátosságaiból adódó problémákat. short int. kénytelenek törõdni ezzel. mert ha nem tennék. akik azt állítják. és a többfajta alaptípussal való számolási sebességben. Ha ismerjük a gépet. hogy több egész. igazán hordozható kódot írni azonban sokkal nehezebb.

2). ez jellemzõen egy 8 bites bájt. . Legjobb. Hasonlóan.2. hogy a karakterek tárolására és kezelésére egy adott számítógépen a legmegfelelõbb legyen. ahol a char 32 bites.4. az értékek úgy alakítódnak át. Nagyobb programok készítéséhez az automatikus konverziókat részletesebben meg kell értenünk. Például vannak olyan gépek.6).5 cm egy bájt) egy megabájt memória körülbelül öt kilométernyire lógna ki a jobb oldalon. world!\0 Ugyanilyen méretarányban (0. Például: #include <limits> #include <iostream> int main() { std::cout << "A legnagyobb lebegõpontos szám == " << std::numeric_limits<float>::max() << ". Nem bölcs dolog többet feltételezni. Típusok és deklarációk 99 char: bool: short: int: int*: double: char[14]: 'a' 1 756 100000000 &c1 1234567e34 Hello.6. hogy ne legyen adatvesztés (§C. a következõ fejezetek olvasásához ugyanakkor ez nem szükséges. akkor v érték T típusúvá alakítása megõrzi az értéket és nincs probléma. az adott C++változat egyedi tulajdonságait megtalálhatjuk a <limits> fejállományban (§22. } Az alaptípusok értékadásokban és kifejezésekben szabadon párosíthatók. Ha szükségünk van rá. Ahol lehetséges. ez általában egy 4 bájtos (32 bites) gépi szó.6). amikor a konverziók nem értékõrzõk (§C. A char típust az adott nyelvi változatnak úgy kell megválasztania. hogy képesek legyünk értelmezni a mások által írt kódot. a char elõjeles == " << std::numeric_limits<char>::is_signed << '\n'. ha elkerüljük azokat az eseteket. Ha v érték pontosan ábrázolható egy T típusú változóban. fõleg azért. az int típusnak a legmegfelelõbbnek kell lennie az egészek tárolására és kezelésére.

Következésképpen a void „látszólagos visszatérési típus”-ként használatos. 4. A felsoroló konstansok típusa a felsorolási típus lesz. amely felhasználó által meghatározott értékeket tartalmaz.8. AUTO. . meg kell határozni visszatérési értékének típusát is. Az alábbi kód például három egész állandót ad meg – ezeket felsoroló konstansoknak nevezzük – és értékeket rendel hozzájuk: enum { ASM. vagy akkor. amikor egy mutató ismeretlen típusú objektumra mutat: void x. BREAK }. BREAK }. AUTO==1. BREAK==2. Minden felsorolás önálló típus. de csak egy bonyolultabb típus részeként lehet használni. Meghatározása után az egész típushoz hasonlóan használható. void f(). A felsoroló típusok tagjaiként névvel rendelkezõ egész konstansokat adhatunk meg. Alapértelmezés szerint az állandók 0-tól növekvõen kapnak értékeket. Vagy arra használjuk. Logikailag elvárható lenne.6) Amikor egy függvényt bevezetünk. Az AUTO például keyword típusú. hogy a visszatérési típus elhagyásával jelezzük.3) // ismeretlen típusú objektumra hivatkozó mutató (§5. // hiba: nincsenek void objektumok // az f függvény nem ad vissza értéket (§7. hogy meghatározzuk. annak jelölésére. A felsoroló típusnak lehet neve is: enum keyword { ASM. Felsoroló típusok A felsoroló típus (enumeration) olyan típus. hogy egy függvény nem ad vissza értéket.100 Alapok 4. void* pv. Void A void formája alapján alaptípus. nincsenek void típusú objektumok. AUTO. hogy a függvény nem ad vissza értéket. Ez viszont a nyelvtant („A” függelék) kevésbé szabályossá tenné és ütközne a C-beli gyakorlattal. így ASM==0. a függvény nem ad vissza értéket.7.

mind a fordítónak utalunk a változó tervezett használatára: void f(keyword key) { switch (key) { case ASM: // valamit csinálunk break.5. flag f2 = flag(5). a 2 legközelebbi.1. ha negatív. a konverzió eredménye nem meghatározott: enum flag { x=1. Például: enum e1 { dark. z=4. e=8 }. azoknál nagyobb hatványánál eggyel kisebb értékig. max = 1000000 }. Ha a legkisebb felsoroló konstans nem negatív. enum e2 { a = 3. light }. a legtöbb egész érték ugyanis nem ábrázolható egy adott felsoroló típusban. // tartomány: 0:15 // típushiba: 5 nem flag típusú // rendben: flag(5) flag típusú és annak tartományán belüli // rendben: flag(12) flag típusú és annak tartományán belüli // meghatározatlan: 99 nem esik a flag tartományán belülre Az utolsó értékadás mutatja. // tartomány: 0:1 // tartomány: 0:15 // tartomány: -1048576:1048575 Egy integrális típus értékét meghatározott módon felsoroló típusúvá alakíthatjuk. Típusok és deklarációk 101 Ha keyword típusú változót adunk meg sima int helyett. amely tartalmazhatja a felsoroló konstansok értékét. . a tagoknál kisebb negatív hatványával. flag f1 = 5. } } A fordító figyelmeztetést adhat. y=2. miért nincs automatikus konverzió egészrõl felsoroló típusra. az értékhalmaz 0-val kezdõdik. case BREAK: // valamit csinálunk break. flag f3 = flag(z|e). Ha az érték nem esik a felsoroló típus értékhalmazába. A felsoroló típus értékhalmaza összes tagjának értékét tartalmazza. A felsoroló konstans a kezdeti értékadáskor integrális típusú (§4. mind a felhasználónak. mert a három keyword típusú értékbõl csak kettõt kezeltünk.1) konstans kifejezéssel (§C. felkerekítve a 2 legközelebbi.) is megadható. enum e3 { min = -10. b = 9 }.4. Ez a szabály azt a legkisebb bitmezõt adja meg. flag f4 = flag(99).

A deklarációk sokféleségét a következõ példák szemléltetik: char ch. "õsz". A felsoroló típus felhasználói típus.3). struct Date { int d.102 Alapok A felsoroló típusok értékhalmazának fogalma különbözik a Pascal nyelvcsaládba tartozó nyelvek felsoroló típusainak fogalmától. "nyár". így a felhasználók a felsorolásra saját mûveleteket adhatnak meg. ahol sizeof(int)==4. enum Beer { Carlsberg.2). a név miféle egyedre hivatkozik.2.9. "tél" }. Deklarációk A C++ programokban a neveket (azonosítókat) használat elõtt be kell vezetnünk. extern int error_number. hogy megmondjuk a fordítónak. amelyek arra építenek. A felsoroló típusok sizeof-ja egy olyan integrális típus sizeof-ja. char* name = "Natasa". } double sqrt(double). azaz meg kell határoznunk típusukat. int count = 1. hiszen akkor nem lehetne intként vagy unsigned int-ként ábrázolni. Thor }. int day(Date* p) { return p->d. amely képes a felsoroló típus értékhalmazát tárolni és nem nagyobb sizeof(int)-nél. } . 4. char* season[ ] = { "tavasz".1415926535897932385. const double pi = 3. Alapértelmezés szerint a felsoroló típusok aritmetikai mûveletek esetében egésszé alakítódnak (§6. Például sizeof(e1) lehet 1 vagy talán 4. Tuborg. }. namespace NS { int a. A C-ben és a C++-ban azonban hosszú hagyománya van azoknak a bitkezelõ mûveleteknek. m. y. template<class T> T abs(T a) { return a<0 ? -a : a. string s. de nem lehet 8 egy olyan gépen. hogy a felsoroló típusok tagjain kívüli értékek jól körülhatároltak. struct User. } typedef complex<short> Point. például a ++ és << operátorokkal (§11.

azaz meg is határozza azt az egyedet. A Point esetében az egyed a complex<short> típus. */ } int error_number = 1. amelyekre hivatkoznak. struct User. extern int error_number.3-ban). a pi állandónál a 3.. // hiba: újbóli definíció // hiba: nem megfelelõ típus A következõben viszont egy sincs (az extern használatáról lásd §9.. Így a következõ részletben két hiba van: int count.2. a Date-nél egy új típus. hogy a típus hogy nézzen ki. Típusok és deklarációk 103 Amint a példákból látható. amelyre a név hivatkozik. amelyet változóként használunk (vagyis ezt a memóriaterületet fogjuk lefoglalni). Ugyanakkor a nevet többször is deklarálhatunk (bevezethetjük). struct User { /* . mint hogy egyszerûen egy nevet kapcsol össze a név típusával. és a User típus egy másik deklarációjának kell megadnia. Az sqrt függvény kódját (törzsét) más deklarációkkal kell meghatározni. így a Point a complex<short> szinonimája lesz. A fenti deklarációk közül csak a double sqrt(double). a day-nél a meghatározott függvény. A C++ programokban minden név számára mindig pontosan egy definíció (meghatározás) létezhet (az #include hatásait lásd §9. deklarációk nem definíciók is egyben: azaz máshol kell definiálni (meghatározni) azokat az egyedeket. int count. */ }. extern short error_number. ..4. az int típusú error_number változó számára az error_number egy másik deklarációjának kell lefoglalnia a memóriát.. extern int error_number. hogy egyezzen a hivatkozott egyed típusában. a deklaráció többet is jelenthet annál. A ch esetében például ez az egyed a megfelelõ memóriaterület. A fenti deklarációk többsége definíció is. Például: double sqrt(double d) { /* .1415926535897932385 érték. extern int error_number.2): extern int error_number. Egy egyed minden deklarációja meg kell.

7.) Minden deklaráció.9.5. függvények és állandók esetében ez az „érték” nem változik. nem a típusra jellemzõ tulajdonságát határozza meg. A függvény. A leggyakoribb deklarátor-operátorok a következõk (§A. és egy – szintén nem kötelezõ – kezdõérték-adó kifejezésbõl. // . lásd §4.2). }.9. char* name = "Bjarne". typedef complex<short> Point. (Arról. a deklarátor a *kings[ ].és névtér-meghatározásokat kivéve a deklaráció pontosvesszõre végzõdik: char* kings[ ] = { "Antigónusz".1.2.1415926535897932385. Itt az alaptípus char. A deklarációk szerkezete A deklarációk négy részbõl állnak: egy nem kötelezõ minõsítõbõl. hogy hogyan és mikor kap egy változó alapértelmezett értéket.6) és az extern (§9.4.5-öt és §10. int day(Date* p) { return p->d.5. } A definíciók közül csak az alábbi nem határoz meg értéket: char ch.104 Alapok Néhány definíció valamilyen „értéket” is meghatároz a megadott egyedeknek: struct Date { int d. y. és a bevezetett elem néhány. Típusok. "Ptolemaiosz" }. sablonok. a kezdõérték-adó rész pedig az ={…}. egy alaptípusból..2-t. count = 2.. mint a virtual (§2.1): . Nem konstans adattípusok esetében a kezdeti értéket késõbb módosíthatjuk: void f() { int count = 1. amely értéket határoz meg. 4. A deklarátor (declarator) egy névbõl és néhány nem kötelezõ operátorból áll. "Szeleukusz". egy deklarátorból. egyben definíciónak is minõsül. A minõsítõ (specifier) egy kulcsszó. string s. m. name = "Marian". } const double pi = 3. §12.

a [ ] és a () pedig utótag operátorok. int v[10]. NEM int* y // int x és int* q // int v[10] és int* pv A fentihez hasonló szerkezetek rontják a program olvashatóságát. így a * elõtag-. *pv. Következésképpen a *kings[ ] egy valamire hivatkozó mutatókból álló vektor. int b) { return (a>b) ? a : b.9. hogy kifejezésekben is használhatók legyenek (§6. és zárójeleket kell használnunk. hogy a típus int. ha olyasmit akarunk kifejezni. // int* p és int y. Két egészet például így vezethetünk be: int x.1 példáit). y. ha nincs típus megadva (§B. mint „… függvényre hivatkozó mutató” (lásd az §5. // int x és int y. azt feltételezve. } unsigned ui. int x. // hiba: nincs típus // hiba: nincs visszatérési típus // rendben: 'unsigned' jelentése 'unsigned int' // rendben: 'long' jelentése 'long int' A szabványos C++ ebben eltér a C és a C++ régebbi változataitól. Típusok és deklarációk 105 * *const & [] () mutató konstans mutató referencia tömb függvény elõtag elõtag elõtag utótag utótag Használatuk egyszerû lenne. Több név bevezetése Egyetlen deklarációban több nevet is megadhatunk. hogy az operátorok csak egyes nevekre vonatkoznak. amelyek megengedték az elsõ két példát. A deklaráció ekkor vesszõvel elválasztott deklarációk listáját tartalmazza.2. . ha mindegyikük elõtagként (prefix) vagy mindegyikük utótagként (postfix) használt operátor volna. gt(int a. y. Az utótagként használt operátorok több megkötéssel járnak. Jegyezzük meg. Teljes részletességgel lásd a nyelvtant az „A” függelékben. hogy a típus nem hagyható el a deklarációból: const c = 7. A *. az ugyanabban a deklarációban szereplõ további nevekre nem: int* p. a [ ] és a () operátorokat azonban arra tervezték. 4. Ez az „implicit int” szabály sok félreértés és nehezen megfogható hiba forrása volt.2). long li. *q.2). ezért kerülendõk. Jegyezzük meg.4. mint az elõtagként használtak.

Így a var10 és nem a var név (amit a 10-es szám követ). mint x. Néhány futási idejû környezet ugyancsak szükségessé teszi. Példák a nevekre: hello DEFINED var0 ez_egy_szokatlanul_hosszú_név foO bAr u_name LoPatko var1 CLASS _class ___ És néhány példa olyan karaktersorozatokra. mindig a leghosszabb olyan sorozatot keresi.3. Az _ (aláhúzás) karaktert betûnek tekintjük. Window_with_border. amelyek csak a kezdõbetûben térnek el. l1 és ll nem szerencsés választás. mint a new és az int. amelyek nem használhatók azonosítóként: 012 fizetes. Nevek A név (azonosító) betûk és számok sorozatából áll. Az elsõ karakternek betûnek kell lennie. Hasznos dolog viszonylag rövidnek hagyni a gyakran használt neveket. az elseif is egy név. az igazán hosszúakat . Az osztályok (10. Következésképpen azonosítónak a l0. A bõvítések (például a $ engedélyezése egy névben) nem hordozható programokat eredményeznek. A fordítóprogram írója azonban a megvalósítás egyes részeire nincs befolyással (konkrétan a szerkesztõprogramra. A kis. mint vector. a kis L (l) és az egyes (1) szintén. nem használhatók felhasználói egyedek neveként. A kód viszont érthetõbb lesz. amelyek csak kicsit különböznek. nem pedig az else.106 Alapok 4. ez pedig határokat szab.9.esedekes egy bolond foo~bar $sys . amely kiadhat egy nevet. és Department_number. a linker-re). A C++ kulcsszavai („A” függelék). Például a nagybetûs o (O) és a nulla (0) nehezen megkülönböztethetõ. hagyományos stílusú nevük van. A legjobb elkerülni azokat a neveket. lO. így ezeket nem szabadna használni alkalmazói programokban.és nagybetûket a nyelv megkülönbözteti. hogy kibõvítsük vagy megszorítsuk az azonosítóban elfogadható karakterkészletet. fejezet) és a névterek (§8. amit az if kulcsszó követ. Hasonlóan.name class if 3var Az aláhúzással kezdõdõ nevek a nyelvi megvalósítás és a futási idejû környezet egyedi eszközei számára vannak fenntartva. ha a kis hatókörben használt neveknek rövid.2) használhatók arra. hogy a hatókörök kicsik maradjanak. A nagy hatókörû nevek lehetõleg hosszúak és érthetõek legyenek. Amikor a fordító olvassa a programot. de nem bölcs dolog olyan neveket választani. i és p. így a Count és a count különbözõ nevek. A C++ nem korlátozza a névben használható karakterek számát.

még akkor is. A phone_book (telefonkönyv) például jobb. ha a telefonszámokat listában (§3. nehéz elérni a következetességet. x = 2. hogy az egyed jelentésére és ne annak megvalósítására utaljanak. amelyek nem típusnevek (például Shape és current_token). A függvényeken belül megadott nevek esetében (ezeket gyakran lokális vagy helyi névnek hívjuk) ez a hatókör a deklaráció helyétõl annak a blokknak a végéig tart. 4. amelyben a deklaráció szerepel. Akárhogy is. mivel a programokat általában különbözõ forrásokból vett részletek alkotják és számos különbözõ ésszerû stílus használatos bennük.7) tároljuk. Hatókörök A deklaráció a megadott nevet egy hatókörbe (scope) vezeti be. x = 1. ha függvényen. A blokk olyan kódrész. ha az azonosítóban szét akarjuk választani a szavakat. mint a number_list (számok listája). hogy egy másik egyedre hivatkozzon egy blokkon belül. Például írjuk nagy kezdõbetûvel a nem standard könyvtárbeli felhasználói típusokat és kisbetûvel azokat a neveket. A jó nevek megválasztása is egyfajta mûvészet. Egy nevet globálisnak nevezünk. Válasszuk meg úgy a neveket. Legyünk következetesek a rövidítések és betûszavak használatában is. // globális x // a lokális x elfedi a globális x-et // értékadás a lokális x-nek // elfedi az elsõ lokális x-et // értékadás a második lokális x-nek . A blokkból való kilépés után a név visszanyeri elõzõ jelentését: int x. A globális nevek hatóköre a bevezetés pontjától annak a fájlnak a végéig terjed. például HACK) és használjunk aláhúzást.4.2) kívül bevezetett. Továbbá használjunk csupa nagybetût makrók esetében (ha makrókat kell használnunk. A blokkokban szereplõ névdeklarációk a körülvevõ blokkban lévõ deklarációkat és a globális neveket elfedhetik. azaz egy nevet újra meg lehet adni úgy. { } int x. Típusok és deklarációk 107 pedig megtartani a kevésbé gyakran használatos egyedeknek. amelyet a { } kapcsos zárójelek határolnak. Próbáljunk következetes elnevezési stílust fenntartani. void f() { int x. fejezet) vagy névtéren (§8. amelyben a deklaráció szerepel.4. osztályon (10. azaz a nevet csak a programszöveg meghatározott részében lehet használni.9.

csak butaság. akkor maga keresi a bajt. Ez azt jelenti. void f3() { int x = x. x = 2. A kódot olvasónak azonban könnyen elkerüli a figyelmét. hogy egy név többször szerepel. Az elfedett globális nevekre a :: hatókörjelzõ használatával hivatkozhatunk: int x. } // a globális x elfedése // értékadás a globális x-nek // értékadás a lokális x-nek Elfedett lokális név használatára nincs mód. mint i és x. hogy egy nevet saját kezdõértékének meghatározására is használhatunk: int x.108 Alapok } x = 3. azaz a teljes deklarátor után és a kezdeti érték(ek)et megadó rész elõtt. void f2() { int x = 1. A név hatóköre a név deklarációjának pontjától kezdõdik. Egy jó fordítóprogram figyelmeztetést ad. A nevek elfedése elkerülhetetlen nagy programok írásakor. // értékadás az elsõ lokális x-nek // a globális x címének felhasználása int* p = &x.. Mivel az ilyen hibák viszonylag ritkán fordulnak elõ. nagyon nehezen lehet azokat megtalálni. ::x = 2. . Következésképpen a névelfedések számát a lehetõ legkisebbre kell csökkenteni. mielõtt értékét beállítottuk volna (lásd §5.9[9]). // .. Ha valaki olyan neveket használ globális változóként vagy egy hosszabb függvény lokális változójaként. ha egy változót azelõtt használunk. } // perverz: kezdeti értékadás x-nek saját maga (nem meghatározott) értékével Ez nem tiltott.

2.9. mert x-et ugyanabban a hatókörben kétszer adtuk meg: void f5(int x) { int x. vagy helyi statikus objektumok (§7. double d.'' A lokális változóknak (ezeket néha automatikus objektumoknak nevezzük) és a szabad tárban létrehozott objektumoknak (dinamikus vagy „heap” objektumok) alapértelmezés szerint nincs kezdõértékük: void f() { int x. y = x. mint ha a függvény legkülsõ blokkjában lennének megadva. 4. névtér (§8. y = 22 A függvényparaméterek neveit úgy tekintjük. int x = 22.4) (melyeket együttesen statikus objektumoknak nevezünk) a megfelelõ típus 0 értékét kapják kezdõértékül: int a. void f4() { int y = x. } // perverz: // a globális x felhasználása. a globális (§4. érdemes figyelnünk rá. §10.9.. akkor ez határozza meg az objektum kezdeti értékét.4. } // x értéke nem meghatározott .1.2). // .2. y = 11 // a lokális x felhasználása.4). Kezdeti értékadás Ha egy objektumhoz kezdõérték-adó kifejezést adunk meg. // jelentése "int a = 0.. } // hiba Ilyen hiba gyakran elõfordul.0.'' // jelentése "double d = 0. így az alábbi hibás.5. Ha nincs megadva ilyen. Típusok és deklarációk 109 Egy névvel egy blokkban két különbözõ objektumra a :: operátor használata nélkül is hivatkozhatunk: int x = 11.

Objektumok és balértékek Névvel nem rendelkezõ „változókat” is lefoglalhatunk és használhatunk. Jegyezzük meg.110 Alapok A tömbök és struktúrák tagjai alapértelmezés szerint kapnak kezdõértéket.2. *p[a+10]=7).5). A bonyolultabb objektumoknak egynél több értékre van szükségük a kezdeti értékadáshoz.2. amely egy objektumra hivatkozik. A tömbelemeknek és a nem statikus struktúrák vagy osztályok tagjainak az élettartamát az az objektum határozza meg. és értéket is adhatunk nekik furcsa kifejezésekkel (pl. hogy az adott szerkezet statikus-e vagy sem. A balérték (lvalue) szót eredetileg arra alkották. Az ilyen objektumokat automatikus objektumoknak nevezzük.2).4. ami egy értékadás bal oldalán szerepelhet”. A felhasználói típusokhoz magunk is megadhatunk alapértelmezett kezdõértéket (§10. és a program befejeztéig „élnek”(10.4.3) fogalmával. hogy „valami a memóriában”. A globális és névtér-hatókörben bevezetett objektumok és a függvényekben vagy osztályokban megadott static objektumok (csak) egyszer jönnek létre és kapnak kezdeti értéket.2).9). §5. // tömb kezdeti értékadása // függvény stílusú kezdeti értékadás (konstruktorral) // függvény-deklaráció 4.8).2. Azaz. egy balérték hivatkozhat állandóra (const) is (§5. A new és delete operátorokkal olyan objektumok hozhatók létre.7) ezt egy { } zárójelek által határolt listával érhetjük el.3). amikor a neve a hatókörön kívülre kerül (§10.1. Az objektumnak ezt az egyszerû és alacsony szintû fogalmát nem szabad összetéveszteni az osztályobjektumok és többalakú (polimorf típusú) objektumok (§15.1.4. hogy a deklarációkban szereplõ () üres zárójelek jelentése mindig „függvény”: int a[ ] = { 1. A konstruktorral rendelkezõ felhasználói típusoknál a függvény stílusú paraméterlisták használatosak (§2. hogy a következõt jelentse: „valami. Az ilyen objektumokat statikus objektumoknak nevezzük. és akkor szûnik meg. A tömbök és struktúrák C típusú feltöltésekor (§5. egy függvényben bevezetett változó akkor jön létre. §10. 2 }. amelynek részei. a bal oldali érték („balérték”) pedig egy olyan kifejezés.4. függetlenül attól.4. amikor definíciójához érkezünk. . A nem const-ként megadott balértéket szokás módosítható balértéknek (modifiable lvalue) is nevezni. Hacsak a programozó másképp nem rendelkezik (§7.6. Point z(1. Nem minden balérték lehet azonban az értékadás bal oldalán. Ez az objektum legegyszerûbb és legalapvetõbb fogalma.9. §10. az objektum egy folytonos tárterület.5. int f(). amelyek élettartama közvetlenül szabályozható (§6. Következésképp el kellene neveznünk azt.4).6).2.2.

melyeknek szinonimái. typedef short int16. Ha most az int32-t használjuk.4. használják a felsoroló típusokat (§4. ezért megadhatjuk a szinonimáját.7. Például az unsigned char túlságosan hosszú az igazán gyakori használatra. hogy egy típushoz való közvetlen hozzáférést egy helyre korlátozunk: typedef int int32. fejezet). programunkat átvihetjük egy olyan gépre. hogy a kódban egyszer szereplõ int32-t most másképp határozzuk meg: typedef long int32. a típus számára új nevet hoz létre. A typedef másik használata az. .9. ahol a sizeof(int) 2-vel egyenlõ. Typedef Az a deklaráció. Típusok és deklarációk 111 4. az uchar-t: typedef unsigned char uchar. nem egy adott típusú változót: typedef char* Pchar. amikor egy viszonylag nagy egészre van szükségünk. char* p3 = p1. // p1 és p2 típusa char* Az így megadott. a typedef-ek inkább más típusok szinonimái. mint önálló típusok. amit a typedef kulcsszó elõz meg. Azok. Pchar p1. akik ugyanolyan jelentéssel vagy ábrázolással rendelkezõ önálló típusokat szeretnének. Végezetül. gyakran typedef-nek (áltípus) nevezett név kényelmes rövidítés lehet egy nehezen használható név helyett. Következésképpen a typedef-ek szabadon felcserélhetõk azokkal a típusokkal. p2.8) vagy az osztályokat (10. úgy.

9.1.2. új típusok definiálására használjunk felsoroló típusokat és osztályokat. [5] Kerüljük a hasonlónak látszó neveket.112 Alapok 4. hogy a névválasztás inkább a jelentésre. §C. §4. [6] Elnevezési stílusunk legyen következetes. §4. § 4. §4.3.9.6.9.9. [10] Emlékezzünk arra.9. §4. §4. [8] Ha a beépített típus.3. [14] Részesítsük elõnyben a sima int-et a short int-tel vagy a long int-tel szemben. [4] A gyakori és helyi nevek legyenek rövidek. [11] Kerüljük a karakterek számértékével kapcsolatos szükségtelen feltételezéseket. §4.3.6. . [19] Legyünk óvatosak az elõjelesrõl elõjel nélkülire és unsigned-ról signed-ra való átalakítással.1. [21] Legyünk óvatosak a kisebb típusokra való átalakításokkal (például int-rõl char-ra).9. §4.7 [9] A typedef-ekkel típusok szinonimáit adjuk meg.9.6. hogy minden deklarációban szükséges a típus megadása (nincs „implicit int”).2.6. §4. [12] Kerüljük az egészek méretével kapcsolatos szükségtelen feltételezéseket.3.7.4. amelyet egy érték ábrázolására használunk. §4. [15] Részesítsük elõnyben a double-t a float-tal vagy a long double-lal szemben.6. [17] Kerüljük az objektumok méretével kapcsolatos szükségtelen feltételezéseket.6. §4.2.10. mintsem a megvalósításra utaljon. [18] Kerüljük az elõjel nélküli aritmetikát. a nem helyi és ritkán használt nevek hosszabbak.9. §4.9. §4. Tanácsok [1] A hatókörök legyenek kicsik.1. § C. §C.3. [16] Részesítsük elõnyben a sima char-t a signed char-ral és az unsigned char-ral szemben.3.6. [3] Deklarációnként (csak) egy nevet adjunk meg. [7] Figyeljünk arra. megváltozhat.4. §4.2. § C.6. §4. §C. §4.9.6.2.6. [2] Ne használjuk ugyanazt a nevet egy hatókörben és az azt körülvevõ hatókörben is. [13] Kerüljük a szükségtelen feltételezéseket a lebegõpontos típusok értékkészletével kapcsolatban is. használjunk typedef-et.6. így a típus számára beszédes nevet adhatunk.5. [20] Legyünk óvatosak a lebegõpontos típusról egészre való átalakítással. §4.3.4.

valamint a hozzájuk tartozó egész értékeket. írjunk hozzá definíciót. amit a C++ programokban használhatunk a rendszerünkben? Mi a leghosszabb külsõ név. (*1) Mi a leghosszabb lokális név.11. olvassuk el §B.1-et. ami nem az. double. Csináljuk meg ugyanezt hexadecimális jelöléssel. . long double és unsigned? 6. long. Rajzoljuk meg az ábrát kedvenc C++változatunk típusaira is. írjunk olyan deklarációt.5) Írjunk programot. 4.4. (*2) Rajzoljunk ábrát az egész és alaptípusokról. Ha a deklaráció definíció is. float. (*2) Futtassuk le a „Helló. Használjuk a sizeof operátort. Gyakorlatok 1.3. amely kiírja az alaptípusok. (*2) Mi a rendszerünkön a legnagyobb és legkisebb értéke a következõ típusoknak: char. 3. Végezzük el ugyanezt a többi kiírható karakterre is. short. 2. ahol egy típus egy másik típusra mutat. (*1) §4. int. (*1. világ!” programot (§3. Típusok és deklarációk 113 4.9 minden deklarációjára végezzük el a következõket: ha a deklaráció nem definíció. amely kiírja az 'a'…'z' betûket és a '0'…'9' számjegyeket. ahogy kellene.5) Írjunk programot. Ha a program fordítása nem úgy sikerül.2). (*1. 5. ha az elsõ minden értéke minden szabványos megvalósításban ábrázolható a másik típus értékeként. amit a C++ programokban használhatunk a rendszerünkben? Van-e megszorítás a nevekben használható karakterekre? 7. néhány szabadon választott mutatótípus és néhány szabadon választott felsoroló típus méretét.

1. // a p a c címét tárolja .5 Mutatók. T* a „T-re hivatkozó mutató” típus lesz.” (Tom Paine) Mutatók • Nulla • Tömbök • Karakterliterálok • Tömbre hivatkozó mutatók • Konstansok • Mutatók és konstansok • Referenciák • void* • Struktúrák • Tanácsok • Gyakorlatok 5. hogy nehéz õket szétválasztani. tömbök és struktúrák „A fenséges és a nevetséges gyakran annyira összefüggnek. Például: char c = 'a'. char* p = &c. azaz egy T* típusú változó egy T típusú objektum címét tartalmazhatja. Mutatók Ha T egy típus.

Az indirekció jele az elõtagként használt egyoperandusú * : char c = 'a'. E mûveletet indirekciónak (közvetett használatnak.1) használhatunk. Ahhoz. // mutató egészre // mutató char-ra hivatkozó mutatóra // egészre hivatkozó mutatók 15 elemû tömbje // char* paraméterû függvényre hivatkozó mutató. Azok. A mutatón végezhetõ alapvetõ mûvelet a „dereferencia”.6).1-et a deklarációk formai követelményeire vonatkozóan. char* p = &c. és az „A” függeléket a teljes nyelvtannal kapcsolatban.4) vagy struktúrákban levõ bitmezõket (§C. amin a program fut. a függvényekre hivatkozó mutatók pedig végtelenül hasznosak. mint a char (§4. amely számára önállóan memóriát foglalhatunk és amelyre beépített típusú mutatóval hivatkozhatunk. char** ppc.7 pontban tárgyaljuk. következésképp a legkisebb objektum. // a p a c címét tárolja // c2 == 'a' A p által mutatott változó c. ezeket a §7.9. olyan hardverrel rendelkeznek. int* ap[15]. amellyel a bájtokat gépi szavakból nyerik ki. char c2 = *p.2. hogy közvetlen kapcsolatot teremtsenek annak a gépnek a címzési eljárásaival. hogy a bool legalább annyi helyet foglal.3).116 Alapok Ábrával: p: &c c: 'a' Sajnos a tömbökre és függvényekre hivatkozó mutatók esetében bonyolultabb jelölés szükséges: int* pi.A tömbelemekre hivatkozó mutatókon aritmetikai mûveleteket is végezhetünk (§5. egészre hivatkozó mutatót ad vissza Lásd §4. int* f(char*). a char.8. hogy a kisebb értékeket tömörebben lehessen tárolni. A mutatók célja. Másrészrõl kevés gép tud közvetlenül egy bitet megcímezni. amelyek erre nem képesek. A legtöbb gép bájtokat címez meg. . hivatkozásnak) is hívják. Jegyezzük meg. azaz a mutató által mutatott objektumra való hivatkozás. a c-ben tárolt érték 'a'. int (*fp)(char*). egészet ad vissza // char* paraméterû függvény. így c2 értéke 'a' lesz. logikai operátorokat (§6.

1. hogy a NULL–t ott is használni lehessen. // három lebegõpontos számból álló tömb: v[0].5. // d2 olyan tömb. vagy „tagra hivatkozó mutató” típusú konstansként is használható. Az elemek sorszámozása 0-tól size-1-ig terjed: float v[3]. A nullát általában (de nem szükségszerûen) a megfelelõ méretû „csupa nulla” bitminta jelöli. amely számára a 0 címmel foglalnánk helyet.1.3) köszönhetõen a 0 integrális (§4. használjunk vektort (§3. hogy a mutató nem hivatkozik objektumra. mutató. v[2] // karakterre hivatkozó mutatók 32 elemû tömbje: a[0] .Nincs olyan objektum. konstans kifejezés kell.7. Nulla A nulla (0) az int típusba tartozik. A C++ szigorúbb típusellenõrzése miatt az ilyen NULL makrók helyett használjuk a sima 0-t.3): void f(int i) { int v1[i].. lebegõpontos.2. muszáj a NULL-t megadnunk.A C-ben a nulla mutatót (nullpointer) szokás a NULL makróval jelölni. 5. Tömbök Ha T egy típus. ahol állandóra van szükség. v[1]. char* a[32]. Következésképpen a 0 mutató-literálként viselkedik. a[31] A tömb elemeinek száma. vector<int> v2(i). azt jelölve. Ha úgy érezzük.5). A típust a környezet dönti el. tegyük azt az alábbi módon: const int NULL = 0 .2. hogy a NULL-t véletlenül újra definiáljuk és biztosítja. hogy legyen (§C. 20 egészbõl álló tömböt tartalmaz .1). amely 10 darab.1. A szabványos konverzióknak (§C. A const minõsítõ megakadályozza. a T[size] a „size darab T típusú elembõl álló tömb” típus lesz.6. Ha változó méretre van szükségünk. tömbök és struktúrák 117 5.1. Mutatók. } // hiba: a tömb mérete nem konstans kifejezés // rendben A többdimenziós tömbök úgy ábrázolódnak. mint tömbökbõl álló tömbök: int d2[10][20]. ez kevesebb problémához vezet. §16. mérete vagy dimenziója.

2.1. mert a . char v4[3] = { 'a'. a tömb maradék elemeire 0 lesz feltételezve: int v5[8] = { 1. // hiba: tömböt nem lehet értékül adni } . 4 }. 'd'. 2. 0 }. Amikor egy tömböt úgy adunk meg. 0.2) és nem megengedett konstans kifejezésekben (§C.118 Alapok A más nyelvekben tömbök méretének meghatározására használt vesszõ jelölés fordítási hibákat eredményez.2. de kezdõértékeket biztosítunk.7 pontban tárgyaljuk. a kezdõérték-listában nem szerepelhet annál több elem. 3. 4 . 0. 2. (vesszõ) mûveletsorozatot jelzõ operátor (§6. char v2[ ] = { 'a'. Például próbáljuk ki ezt: int bad[5. 'c'. Az elõzõ kód egyenértékû a következõvel: int v5[ ] = { 1. a fordítóprogram a tömb méretét a kezdõérték-lista elemeinek megszámlálásával számítja ki. 0. Tömbök feltöltése A tömböknek értékekbõl álló listákkal adhatunk kezdõértéket: int v1[ ] = { 1. 3. // hiba: túl sok kezdõérték // rendben Ha a kezdõérték túl kevés elemet ad meg. 2.5).2]. 0 }. 0 }. Következésképp v1 és v2 típusa rendre int[4] és char[4] lesz. 'b'. ha kerüljük õket. 'b'. 0 }. // hiba: konstans kifejezésben nem lehet vesszõ A többdimenziós tömböket a §C. 0 }. hogy a kezdõérték-lista nem helyettesíthetõ és nem bírálható felül tömbértékadással: void f() { v4 = { 'c'. 4 }. 'b'. 5. hogy a méretét nem határozzuk meg. Alacsonyszintû kódon kívül a legjobb. mert ez hibának számít: char v3[2] = { 'a'. Ha a méretet megadjuk. 3. Jegyezzük meg.

2. Az ilyen karakterliterálokat azonban hiba ilyen mutatón keresztül módosítani.2) is feltölthetjük. mert a karakterliterál típusa a C és a C++ korábbi változataiban char* volt. melynek értéke 0: sizeof("Bohr")==5 A karakterliterálok típusa „megfelelõ számú const (állandó) karakterbõl álló tömb”. hogy a nyelv adott változata jelentõsen optimalizálhassa a karakterliterálok tárolásának és hozzáférésének módját. tömb helyett használjunk vector-t (§16. hogy a karakterliterálok állandók. . // hiba: értékadás konstansnak. 5. hogy mennyire szereznek érvényt ennek a szabálynak. így a "Bohr" típusa const char[5] lesz. nemcsak magától értetõdõ. az eredmény nem meghatározott } Az effajta hibát általában nem lehet a futási idõig kideríteni. a '\0' null karakterre végzõdik.5.3-at. A karakterliterálokat egy char*-nak is értékül adhatjuk.2. Mutatók. p[4] = 'e'.4). Ez azért megengedett.2. A karaktertömböket kényelmi okokból karakterliterálokkal (§5. tömbök és struktúrák 119 Ha ilyen értékadásra van szükségünk. így ez szükséges ahhoz. (Lásd még §B. hogy millió sornyi C és C++ kód érvényes maradjon.) Az.3) vagy valarray-t (§22. és a nyelv egyes megvalósításai is különböznek abban. void f() { char* p = "Platón".2. Karakterliterálok A karakterliterál egy macskakörmökkel határolt karaktersorozat: "Ez egy karakterlánc" Egy karakterliterál a látszólagosnál eggyel több karaktert tartalmaz. hanem azt is lehetõvé teszi.

const char* q = "Herakleitosz". . A nem grafikus karakterek jelölésére használt fordított perjel (§C. így egy függvény visszatérési értékeként biztonságosan megadható: const char* error_message(int i) { // .3. return "tartományhiba". amit biztosan módosíthatunk.120 Alapok Ha olyan karakterláncot szeretnénk. Üres karakterláncot a "" szomszédos macskaköröm-párral írhatunk le (típusa const char[1])..2) szintén használható egy karakterlánc belsejében.. karaktereit egy tömbbe kell másolnunk: void f() { char p[ ] = "Zénón". Ez lehetõvé teszi az idézõjel (" ) és a fordított perjel „escape” karakter (\) karakterláncon belüli ábrázolását is.. p[0] = 'R'. } // p egy 6 karakterbõl álló tömb // rendben A karakterliterál tárolási helye nem változik (statikus). void g() { if (p == q) cout << "Egyezik!\n". az adott nyelvi változattól függ (§C. hogy két egyforma karakterliterál egyetlen memóriaterületen tárolódik-e. hogy a mutatókra alkalmazott == a címeket (a mutató értékeket) hasonlítja össze. } // az eredmény az adott C++-változattól függ Jegyezzük meg. Az. nem azokat az értékeket.1): const char* p = "Herakleitosz". // . melyekre a mutatók hivatkoznak. } A range_error-t tartalmazó memóriaterület tartalma nem törlõdik az error_message() meghívása után.. Az '\n' (új sor) karakter ezek közül messze a leggyakrabban használt: cout<<"csengõ az üzenet végén\a\n".

§C3. amely a tömb elsõ elemére mutat: int v[ ] = { 1.3). amely alert-ként is ismert. Az L elõtagú karakterláncok – mint amilyen az L"angst" – „széles” karakterekbõl (wide char) állnak (§4. 4 }. int* p1 = v.4. // mutató a kezdõelemre int* p3 = &v[4]. Egy tömb nevét úgy is használhatjuk.1). hogy a programszöveg szebb legyen: char alpha[ ] = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ". A karakterláncokban „igazi” sortörés nem szerepelhet: "Ez nem karakterlánc hanem formai hiba" A hosszú láncok „üreshely” (whitespace) karakterekkel széttördelhetõk. tömbök és struktúrák 121 Az '\a' karakter az ASCII BEL (csengõ). // mutató az "utolsó utáni" elemre . A fordítóprogram összefûzi a szomszédos láncokat. Tömbökre hivatkozó mutatók A C++-ban a tömbök és mutatók között szoros kapcsolat áll fenn.5. de a legtöbb program nem feltételezi. "Jens"-ként fogják kezelni (§20. 5. mint a strcpy() és a strlen(). 3. // mutató a kezdõelemre (automatikus konverzió) int* p2 = &v[0]. A "Jens\000Munk" karakterláncot például az olyan standard könyvtárbeli függvények. kiírása pedig valamilyen hangjelzést eredményez. hogy utána is vannak karakterek. mint egy mutatót. A null karaktert elvileg a karakterláncok belsejében is használhatnánk. 2. típusuk const wchar_t[ ]. Mutatók.3.3. így az alpha egyetlen karakterlánccal is megadható lett volna: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".

char* p = v. Szerencsére mutatóról tömbre való átalakítás nem végezhetõ sem automatikusan. így az „eggyel a kezdõelem elõtti elem” egyszerûen értelmetlen lesz. ezért az ilyesmi kerülendõ. ha a mutatót eggyel a tömb vége utáni elemre állítjuk. vagyis nincs mód olyan függvény bevezetésre. Mivel azonban egy ilyen mutató ténylegesen már nem mutat egy tömb elemére.3). // a <string. Nem meghatározott. strlen(v). // hiba: a tömbnek nem adható érték } A standard könyvtár strlen() függvényének mindkét híváskor ugyanaz az érték adódik át. Az a bökkenõ. hogy a tömb mérete elvész a függvény számára.2. így a strlen(p) . hogy mi történik. sem definiált módon. hogy a null karakter jelzi a karakterlánc végét. amikor egy tömb kezdõeleme elõtt levõ elem címét vesszük. §18. A C standard könyvtárában levõ más függvényekhez hasonlóan – amelyek karakterre hivatkozó mutatókat kapnak paraméterként – az strlen() is arra számít. nem szabad írásra vagy olvasásra használni.h> fejállományból void f() { char v[ ] = "Annemarie". Egyes számítógépeken a tömbök gyakran a gép címzési határain kerülnek lefoglalásra. hogy az automatikus konverziót lehetetlen elkerülni. // char[ ] automatikus átalakítása char*-gá strlen(p). A függvénynek azonban valahogy meg kell határoznia a tömb méretét.122 Alapok Ábrával: p1 p2 p3 v: 1 2 3 4 Az biztosan mûködik.7. A tömbnevek automatikus (implicit) átalakítása mutatóvá széleskörûen használatos a C stílusú kódokban szereplõ függvényhívásoknál: extern "C" int strlen(const char*). Ez sok algoritmus számára fontos (§2. hogy értelmes mûveleteket hajthasson végre rajta. A tömbparaméter automatikus mutatóvá alakítása azt jelenti. amelynek meghívásakor a v tömb átmásolódik. // char[ ] automatikus átalakítása char*-gá v = p.

1. -. 18. } Az elõtagként használt * indirekció operátor egy mutató-hivatkozást old fel. A hozzáférés egy tömbre hivatkozó mutatóval. Ez meglehetõsen alacsony szintû megoldás.3. Hajtsuk végre a következõt: #include <iostream> int main () { int vi[10]. ++ vagy -. a ++ pedig úgy növeli a mutatót. } Ez egyenértékû a mutatóval történõ bejárással: void fp(char v[ ]) { for (char* p = v. i++) use(v[i]). Íme egy példa egy karakterlánc bejárására index használatával: void fi(char v[ ]) { for (int i = 0.aritmetikai mûveleti jeleket mutatókra alkalmazzuk. mint p egész értéke. így *p a p által mutatott karakter lesz. . Nincs olyan eredendõ ok. nem beleértve a null karakter végzõdést . hogy egy T típusú objektumokból álló tömb elemére mutat. A programozók logikai és esztétikai alapon választhatnak a változatok között. fejezet). tömbök és struktúrák 123 a p karaktereinek a 0 null karakter végzõdésig számolt mennyiségét jelenti. így p+1 a tömb következõ elemét jelzi. amiért az egyik változat gyorsabb lenne a másiknál.8. short vs[10]. Tömbök bejárása Sok algoritmus lényege a tömbökhöz és más hasonló adattípusokhoz való hatékony és „elegáns” hozzáférés (lásd §3. Ha a +. hogy az a tömb következõ elemére hivatkozzon.9[8]-at). 5. hogy p+1 egész értéke sizeof(T)-vel nagyobb lesz. Ez arra utal. v[i]!=0.5.) és string (20. Amikor egy T* típusú p mutatóra alkalmazunk egy aritmetikai operátort. p++) use(*p). Mutatók. az eredmény a mutatók által hivatkozott objektumok típusától függ. A standard könyvtárban lévõ vector (§16. *p!=0. A modern fordítóprogramoknak ugyanazt a kódot kell létrehozniuk mindkét példa esetében (lásd §5.3. illetve egy indexszel vagy egy elemre hivatkozó mutatóval valósítható meg. fejezet) esetében nincs ilyen probléma. akkor p-rõl feltételezzük. p-1 pedig az elõzõ elemre mutat.

hexadecimális jelölését használva): 0x7fffaef0 0x7fffaef4 0x7fffaedc 0x7fffaede Ez azt mutatja. sizeof(int) pedig 4. amelyre az eredeti mutató. std::cout << &vs[0] << ' ' << &vs[1] << '\n'. int v2[10]. int* p1 = v2+2. hogy a tömb elemeinek száma is tárolódik a tömbbel együtt. Ez azt jelenti. int i1 = &v1[5]-&v1[3]. // i1 = 2 // meghatározhatatlan eredmény // p1 = &v2[2] // *p2 nem meghatározott } A bonyolult mutatóaritmetika rendszerint szükségtelen. Amikor kivonunk egy mutatót egy másikból. hogy ahhoz. vagy nem eggyel a tömb mögé.124 Alapok } std::cout << &vi[0] << ' ' << &vi[1] << '\n'. az eredmény mindkét esetben egy mutató érték lesz. ha mindkét mutató ugyanannak a tömbnek az elemeire mutat (bár a nyelvben nincs gyors mód annak ellenõrzésére. Nincs értelme mutatókat összeadni. Mutatókat csak akkor vonhatunk ki egymásból definiált módon. és ez nem is megengedett. A tömbök nem önleírók. az eredményül kapott mutató érték felhasználása kiszámíthatatlan eredményhez vezethet: void f() { int v1[10]. hogy valóban arra mutatnak). hogy bejárjunk egy tömböt. az eredmény a két mutató között lévõ tömbelemek száma (egy egész típusú érték) lesz. ezért legjobb elkerülni. Ekkor a következõt kapjuk (a mutatók értékének alapértelmezés szerinti. A mutatókhoz egész értéket is adhatunk és ki is vonhatunk belõle egészet. Ha ez az érték nem ugyanannak a tömbnek egy elemére mutat. mert nem biztos. int i2 = &v1[5]-&v2[3]. valahogy meg kell adnunk a tömb elemeinek számát: . int* p2 = v2-2. amely nem tartalmaz a karakterláncokéhoz hasonló végzõdést. hogy sizeof(short) az adott megvalósításban 2.

for (int i=0. 5. } Jegyezzük meg. 3. hogy lehetõségünk legyen annak kifejezésére. mint a kódban közvetlenül elhelyezett literálok. A legtöbb függvényparamétert csak olvassuk. A const kulcsszó hozzáadható egy objektum deklarációjához. hogy hatókörén belül értéke nem fog megváltozni: void f() { model = 200. hogy egy érték nem változik meg közvetlenül.5. i++) use(v2[i]). 4 }. v[2]++. i<size. } // hiba // hiba . jelezve.1).7. const int x. azaz a felhasználói állandó fogalmát. Konstansok A C++ felkínálja a const. const int N = 7. Mutatók. Mivel egy állandónak késõbb nem lehet értéket adni. i<N. nem írjuk. A tömb ezen fogalma eredendõen alacsony szintû. char v2[N]. i++) use(v[i]). const int v[ ] = { 1. 2. unsigned int size) { for (int i=0. hogy egy értéket mutatón keresztül érünk el. kezdeti értékadást kell végeznünk: const int model = 90. Gyakori.4. hogy a legtöbb C++-változat a tömbök esetében nem végez indexhatárellenõrzést. Fejlettebb tömbfogalmat osztályok használatával valósíthatunk meg (§3. Sok objektumnak a létrehozás után már nem változik meg az értéke. // a model állandó // a v[i] állandó // hiba: nincs kezdeti értékadás Ha valamit const-ként határozunk meg. hogy az objektumot állandóként határozzuk meg. A szimbolikus konstansok (jelképes állandók) könnyebben módosítható kódhoz vezetnek. Ez számos esetben hasznos lehet. az biztosíték arra. de az értéket nem változtatjuk meg. tömbök és struktúrák 125 void fp(char v[ ].

így azokat konstans kifejezésekben felhasználhatjuk.126 Alapok Jegyezzük meg. Az extern kulcsszó azt jelöli. úgy. const int* p = &c2. } Attól függõen. mert a fordítóprogram nem tudja eldönteni. hogy a konstansokból álló tömböt csak olvasható memóriába tesszük. const int c3 = my_f(3). A konstansokból álló tömböknek általában szükséges helyet foglalni. // c3 értéke fordításkor nem ismert // c4 értéke fordításkor nem ismert // c2 számára tárterületet kell foglalni Ekkor a fordítóprogram ismeri c1 és c2 értékét. hogyan használhatunk egy objektumot. Sok gépen azonban még ebben az esetben is növelhetjük a hatékonyságot.1). Az állandók kezdeti értéke például gyakran (de nem mindig) egy konstans kifejezés (§C. hogyan kell az állandó számára helyet foglalni: void g(const X* p) { // itt *p nem módosítható } void h() { X val. ha a fordítóprogram tud az állandó minden használatáról. Mivel c2 címét használjuk. számos módon kihasználhatja egy objektum állandó mivoltát.. Mivel a c3 és c4 értékek fordítási idõben nem ismertek (ha csak ebben a fordítási egységben levõ információkat használjuk fel. amikor az állandó értéke fordítási idõben ismert és számára nem szükséges tárat foglalni. . de nem határozza meg. // a val módosítható g(&val). lásd §9. c2-nek is helyet kell foglalni. // ..2). nem kell tárhelyet sem lefoglalnia számára: const int c1 = 1. Továbbá. hogy a fordítóprogram mennyire okos. hogy a const kulcsszó módosítja a típust és megszorítást ad arra. A c1 konstans példa arra az egyszerû és gyakori esetre. const int c2 = 2. mely tömbelemekre hivatkoznak a kifejezések.5). ami fordítási idõben kiértékelhetõ. hogy a c4-et máshol definiáltuk (§9. c3-nak és c4-nek tárhelyet kell foglalni. extern const int c4.

akkor az. Ha a feltevéseket megjegyzésekkel megfelelõen ellátott szimbolikus konstansokként valósítjuk meg. case b: } } // .24 pedig a dán korona és az amerikai dollár közötti keresztárfolyamot jelölheti. Azt. hogy a const milyen módon használható osztályok tagfüggvényeivel. és érvénytelenné válnak. a 6.5. hogy const helyett felsoroló konstansokat (§4. nagyon nehezen tudja megtalálni és megérteni azokat.2.. a programot nehéz lesz átnézni. aki a programot karbantartja. Ha ezeket az értékeket numerikus állandóként hagyjuk a kódban. hogy a megfelelõ módosításkor az állandó minden egyes elõfordulását kicseréljük. . a programmal kapcsolatos feltételezést jelölnek.6 és §10. Ezeket az állandókat gyakran nem veszik észre. A numerikus konstansok rendszerint valamilyen. void f(int i) { switch (i) { case a: // .. a kódban ismétlõdik. Ilyen esetekben gyakori.. A 4 például egy egészben lévõ bájtok számát. hogy elkerüljük a kódban a „mágikus számokat”. const int b = 99.7 pontokban tárgyaljuk. Mutatók.. A szimbolikus konstansok használata viszont lokálissá teszi az információt. tömbök és struktúrák 127 A const-okat gyakran használjuk tömbök indexhatáraként és case címkéknél is: const int a = 42. a 128 a bemenet átmeneti tárba helyezéséhez (puffereléséhez) szükséges karakterek számát.2. int v[max]. Ha egy numerikus állandó. a §10.8) használunk. amikor a programot átviszik más rendszerre vagy ha más változások aláássák az általuk kifejezett feltételezéseket. például egy tömb mérete. const int max = 128. minimálisra csökkenthetjük az ilyen jellegû karbantartási problémákat. A szimbolikus konstansokat rendszeresen használnunk kellene arra.

pc = p. amely egy karakter-konstansra (char const) mutat”.1. Nincs azonban const* deklarátor-operátor.128 Alapok 5. Egy objektum. Mutatók és konstansok A mutatók használatakor két objektummal kapcsolatos dologról van szó: magáról a mutatóról és az általa mutatott objektumról. Ahhoz. cp = p. amikor mutatón keresztül férünk hozzá. hogy egy mutató-paramétert const-ként adunk meg. ha az ilyen deklarációkat jobbról balra olvassuk ki. ha más módon férünk hozzá. const char* q). hogy módosítsa a mutató által mutatott objektumot: char* strcpy(char* p. hogy módosítható lesz akkor. és ne az általa mutatott objektumot vezessük be. és nem a mutatót határozza meg állandóként. Ez különösen hasznos a függvényparaméterek esetében. cpc = p. const char* pc = s. pc[3] = 'g'. a függvénynek megtiltjuk. amely állandó akkor. amely egy karakterre (char) mutat” és „pc2 egy mutató.4. cp[3] = 'a'. Ha a mutató deklarációját a const szó elõzi meg. a *const deklarátort kell használnunk a sima * helyett: void f1(char* p) { char s[ ] = "Gorm". hogy állandóként egy mutatót. akkor az az objektumot. lehet. char *const cp = s. így a * elõtt szereplõ const kulcsszót az alaptípus részének tekintjük: char *const cp. // konstans mutató karakterre // mutató kostans karakterre // mutató kostans karakterre Általában segítséget jelent. cpc[3] = 'a'. // *q nem módosítható . Azzal. char const* pc. // mutató állandóra // hiba: pc állandóra mutat // rendben // konstans mutató // rendben // hiba: cp konstans // konstans mutató állandóra // hiba: cpc állandóra mutat // hiba: cpc konstans } A *const deklarátorjelzõ teszi állandóvá a mutatót. const char* pc2. Például: „cp egy konstans (const) mutató. const char *const cpc = s.

int x = r.1). } // rendben // rendben // hiba: kezdeti értékadás int*-nak const int*-gal // kísérlet c értékének módosítására A const-ra hivatkozó mutatókkal kapcsolatos megszorításokat meghatározott (explicit) típuskonverzióval küszöbölhetjük ki (§10. *p3 = 7. mert ebbõl még semmi rossz nem következik. int& r2. hogy a referencia valaminek a neve legyen (azaz tartozzon hozzá objektum).2. } r = 2. const int c = 2. tömbök és struktúrák 129 Egy változó címét értékül adhatjuk egy konstansra hivatkozó mutatónak. const int* p2 = &a. a hivatkozás célpontját már létrehozáskor meg kell határoznunk: int i = 1. // r és i itt ugyanarra az int-re hivatkoznak // x = 1 // i = 2 Azt biztosítandó.5. int& r1 = i. 5. hogy az objektum értéke megváltozzon: void f4() { int a = 1. const int* p1 = &c.2. Mutatók.4. Konstans címét azonban nem lehet értékül adni egy nem konstans mutatónak. Az ilyen hivatkozásokat általában függvények és különösen túlterhelt operátorok (11. mert ezzel megengednénk. int& r = i. int* p3 = &c. // rendben: r1 kapott kezdõértéket // hiba: kezdeti értékadás hiányzik // rendben: r3 máshol kap kezdõértéket .1 és §15.5. Az X& jelölés jelentése „referencia X-re”. Lássunk egy példát: void f() { int i = 1.7. fejezet) paramétereinek és visszatérési értékeinek megadására használjuk. extern int& r3. Referenciák A referencia (hivatkozás) egy objektum „álneve” (alias).

int& rr = ii. Következésképpen a referenciák értéke már nem módosítható a kezdeti értékadás után. Például: void g() { int ii = 0. amely a referencia számára futási idõben szükségtelenné teszi tárterület lefoglalását. } // ii növelése eggyel // pp az ii-re mutat Ez nem helytelen. amelyet mutatóként kezelhetnénk: pp: &ii rr: ii: 1 Egyes fordítóprogramok olyan optimalizációt alkalmazhatnak. Egy const T& esetében ez nem szükséges (sem balértéknek. amelyre kezdetben beállítottuk azokat. lásd 4. amely minden egyes használatakor automatikusan feloldja a mutató-hivatkozást. míg el nem felejtjük. A látszat ellenére a referencián nem hajtódik végre egyetlen mûvelet sem. A referencia magától értetõdõ módon megvalósítható (konstans) mutatóként is. sem T típusúnak nem kell lennie).6). amelynek címére hivatkozhatunk. A referencia kezdeti értékadása magától értetõdõ. hogy legyen. int* pp = &rr. ha a kezdõérték egy balérték (vagyis egy olyan objektum. mindig arra az objektumra fognak hivatkozni. Egy „sima” T& kezdõértéke T típusú balérték kell. rr++. mindaddig. Az rr által jelölt objektumra hivatkozó mutatót &rr-rel kaphatjuk meg. a ++ egy int-re hajtódik végre (ami itt ii). hogy nem olyan objektumok. Nem származhat baj abból. ha így gondolunk a referenciákra. helyette az alábbiak történnek: . de rr++ nem az rr értékét növeli.9.130 Alapok A referencia kezdeti értékadása nagyban különbözik a késõbbi értékadástól.

mert a változók esetében nagy hibalehetõségeket rejt magában egy ideiglenes változó bevezetése. tömbök és struktúrák 131 1.5. a változónak való értékadás ugyanis a – nemsokára megszûnõ – ideiglenes tárterületnek adna értéket. hogy a program olvasható maradjon. ami szerencsés. A referenciákat olyan függvényparaméterek megadására is használhatjuk. Vegyük a következõ példát: double& dr = 1. } . Ha azt szeretnénk. végül ez az ideiglenes változó lesz a kezdõérték. } // x = 2 A paraméterátadás a kezdeti értékadáshoz hasonló. ha elkerüljük az olyan függvényeket. } void f() { int x = 1. mert ezek gyakran függvényparaméterként játszanak fontos szerepet (§11. A konstansok és változók hivatkozásait azért különböztetjük meg. melyeken keresztül a függvény módosíthatja a neki átadott objektum értékét: void increment(int& aa) { aa++. 3. // elõször létrehozunk egy ideiglenes változót a jobb oldali // értékkel // majd ezt használjuk a cdr kezdeti értékadására A referencia kezdõértékét tároló ideiglenes változó a referencia hatókörének végéig marad fenn. Elõször T-re történõ automatikus típuskonverzió megy végbe. így az increment meghívásakor az aa paraméter az x másik neve lesz. legjobb. amelyek módosítják paramétereiket. // hiba: balértékre van szükség // rendben A második a következõképpen értelmezhetõ: double temp = double(1). } void incr(int* p) { (*p)++.6-ot). Mutatók. increment(x). const double& cdr = temp. const double& cdr = 1. A konstansok hivatkozásaival nincs ilyen probléma. ha szükséges (lásd §C. aztán a kapott érték egy T típusú ideiglenes változóba kerül. 2.6). Ehelyett meghatározhatjuk a függvény által visszaadandó értéket vagy mutató paramétert adhatunk neki: int next(int p) { return p+1.

val.size()-1]. 0 }. Az alapötlet az. } // Pair hozzáadása a végéhez (§3. Következésképpen a „sima” referenciaparamétereket csak olyan esetekben használjuk.132 Alapok void g() { int x = 1. i < pairs.size(). increment(x). amikor a függvény neve határozottan utal arra. i++) if (s == pairs[i]. visszaadjuk az értékét. Rövidítsük le a példát és használjunk egy nagyon egyszerû (persze nem túl hatékony) megvalósítást: vector<Pair> pairs. Elõször határozzuk meg a Pair adatszerkezetet: struct Pair { string name. } // x = 2 // x = 3 // x = 4 Az increment(x) jelölés az olvasónak semmit sem árul el arról. ha nem. x = next(x). Adjunk meg például egy egyszerû asszociatív tömböt.name) return pairs[i]. hogy a string-hez tartozik egy lebegõpontos érték. megkeressük s-t.val. */ { for (int i = 0. Ez a bonyolultabb felhasználói típusok tervezésekor lehet igazán hasznos. ha megtaláltuk. double val. Pair p = { s. hogy az x értéke módosul. amely egy Pair-bõl álló adatszerkezetet vet össze különbözõ karakterláncokkal. amelyek egy értékadás bal és jobb oldalán egyaránt szerepelhetnek.3) return pairs[pairs. . double& value(const string& s) /* Vesszük Pair-ek egy halmazát. hogy ezek módosulnak. incr(&x). ellentétben az x=next(x) és incr(&x) jelölésekkel.push_back(p). Könnyû elkészíteni a value() függvényt. pairs.7. A referenciákat olyan függvények megadására is használhatjuk. }. új Pair-t készítünk és visszaadjuk az alapértelmezett 0-át.

Végül kiírja az eredményül kapott táblázatot.8. aztán növeli a hozzá tartozó számlálót. Mutatók. Ha a bemenet például a következõ: aa bb bb aa aa bb aa aa akkor a program eredménye az alábbi: aa: 5 bb: 3 Ezt már könnyû úgy tovább finomítani. Adott karakterlánccal összevetve.). // az egyes szavak elõfordulásának megszámlálása a bemeneten while (cin>>buf) value(buf)++.6.6. ++p) cout << p->name << ": " << p->val << '\n'. Void-ra hivatkozó mutatók Bármilyen típusú objektumra hivatkozó mutatót értékül lehet adni egy void* típusú változónak. a void* típusú változókat össze lehet hasonlítani. és az erre vonatkozó referenciát adja vissza: int main() { string buf. } A while ciklus minden esetben beolvas egy szót a cin szabványos bemenetrõl és a buf karakterláncba helyezi (§3. 5.5. egy void* típusú változót értékül lehet adni egy másik void* típusúnak. mint egy lebegõpontos értékekbõl álló tömböt.begin(). A többi mûvelet nem lenne . hogy egyenlõek-e vagy sem.4.) [ ] indexelõ operátorral. tömbök és struktúrák 133 Ezt a függvényt úgy foghatjuk fel. amit karakterláncok indexelnek. for (vector<Pair>::const_iterator p = pairs. amelyben a bemenetrõl kapott karakterláncok és azok elõfordulásának száma szerepel. ha a standard könyvtár map (§17. egy void* típusú változót pedig meghatározott módon más típusúvá lehet alakítani. a value() a megfelelõ lebegõpontos objektumot (nem pedig annak értékét) találja meg. p!=pairs. Még könnyebb a dolgunk.) típusát használjuk. hogy valódi asszociatív tömböt kapjunk.1.end(). ehhez egy sablon osztályt kell használnunk a túlterhelt (§11.

rejtsük típusbiztos felület mögé (§13.134 Alapok biztonságos.4. Ha így van.) és a tagokra hivatkozó mutatókat (§15. A gép például feltételezheti. ezért a többi mûvelet fordítási idejû hibát eredményez.2). hogy valójában miféle objektumra hivatkozik egy ilyen mutató. *pv. // n bájt lefoglalása saját tárterületen A rendszer magasabb szintjein lévõ void* típusú mutatókat gyanakvással kell figyelnünk. következésképp a használt static_cast jelölést is szándékosan csúnyának tervezték. ahol az igazi hardver-erõforrásokat kezelik. Ahhoz. mert tervezési hibát jelezhetnek. A függvényekre hivatkozó mutatókat (§7. jellemzõen a rendszer legalsó szintjén helyezkednek el. amelyek nem feltételeznek semmit az objektumok típusáról. . amely olyan típusra konvertálódik (cast). A void* elsõdlegesen arra használatos. §24.5. át kell konvertálnunk azt adott típusú mutatóvá: void f(int* pi) { void* pv = pi. Például: void* my_alloc(size_t n). hogy minden double 8 bájtos memóriahatáron jön létre. } Általában nem biztonságos olyan mutatót használni. amely különbözik a mutató által elõzõleg hivatkozott típustól. amely nem így helyezkedett el a memóriában. mert a fordítóprogram nem tudja. // rendben: int* automatikus konvertálása void*-gá // hiba: nem lehet void*-ra hivatkozni // hiba: void* nem növelhetõ (a mutatott objektum mérete ismeretlen) // explicit visszaalakítás int*-ra // hiba // hiba // nem biztonságos int* pi2 = static_cast<int*>(pv). valamint arra. Azok a függvények. pv++. Az ilyen jellegû explicit típuskényszerítés eredendõen csúnya és nem biztonságos. Ha a void*-ot optimalizálásra használjuk. double* pd1 = pv. Ahhoz.5) nem adhatjuk értékül void* típusú változónak. ha a pi egy olyan int-re mutatott. hogy egy void* típusú változót használhassunk. double* pd2 = pi. amelyek void* típusú mutatókat használnak. hogy függvények nem típusos objektumokat adjanak vissza. akkor furcsa mûködés származhat abból.7. explicit típuskonverziót kell alkalmaznunk. double* pd3 = static_cast<double*>(pv). hogy mutatókat adjunk át olyan függvényeknek. hogy ilyen objektumokat használjunk.

Mutatók. ami eggyel több. az "NJ" három karakterbõl áll. ahol pontosvesszõt kell tenni a kapcsos zárójel után. char* town. 7974 }. Vegyük észre a pontosvesszõt a definíció végén. ezért sokan hajlamosak elfelejteni. mint ami a jd. Vegyük észre. jd. és az egyes tagokra a . a struct-ok (adatszerkezetek. struktúrák) majdnem tetszõleges típusúakból: struct address { char* name. (pont. Ez egyike azon kevés helyeknek a C++-ban.number = 61.5. char* street. Mivel a karakterláncok a '\0' karakterre végzõdnek. // "Jim Dandy" // 61 // "South St" // "New Providence" // 'N' 'J' // 7974 A fenti kód egy address (cím) nevû új típust hoz létre. long zip. "New Providence".3) használni. } A tömbök kezdeti értékadására használt jelölés a struktúra-típusú változók feltöltésére is használható: address jd = { "Jim Dandy". tömbök és struktúrák 135 5.state-et nem lehetett volna az "NJ" karakterlánccal feltölteni.'J'}.name = "Jim Dandy". tagkiválasztó) operátorral hivatkozhatunk: void f() { address jd.7.2.state-be belefér. Az address típusú változókat pontosan úgy adhatjuk meg. long int number. Ennél azonban rendszerint jobb megoldás konstruktorokat (§10. jd. hogy a jd. mint más változókat. }. "South St". char state[2]. 61. amely levelek küldéséhez szükséges címzési adatokat tartalmaz. {'N'. . Struktúrák A tömbök azonos típusú elemekbõl állnak.

mint az összehasonlítás (== és !=). de a felhasználó megadhat ilyeneket (11. return prev. Ennek az az oka. A struktúra-típusú objektumok mérete nem feltétlenül a tagok méretének összege. . Az egészek például gyakran gépi szóhatárokon jönnek létre. ha egyszerûen méret szerint rendezzük a struktúra tagjait (a legnagyobb tag lesz az elsõ). Számos gépen a sizeof(address) például 24. vagy eleve hatékonyabban kezeli az így létrehozott objektumokat. nem pedig 22. } Ha p egy mutató. Az elpazarolt helyet a lehetõ legkevesebbre csökkenthetjük. A struktúra-típusú objektumokat értékül adhatjuk. és csak akkor méret szerint. ahogy az elvárható lenne.m-mel. ha bizonyítottan szükség van optimalizálásra. hogy sok gép igényli bizonyos típusú objektumok elhelyezését a felépítéstõl függõ memóriahatárokra. ha olvashatóság szerint rendezzük sorba a tagokat. } Más lehetséges mûveletek. address set_current(address next) { address prev = current. current = next. hogy az ilyen gépeken az objektumok jól illesztettek. és visszaadhatjuk függvények visszatérési értékeként is: address current. akkor p->m egyenértékû (*p). A legjobb azonban az. nem meghatározottak. fejezet).136 Alapok A struktúrák objektumaira gyakran hivatkozunk mutatókon keresztül a -> (struktúra-mutató) operátorral: void print_addr(address* p) { cout << p->name << '\n' << p->number << ' ' << p->street << '\n' << p->town << '\n' << p->state[0] << p->state[1] << ' ' << p->zip << '\n'. Ez a struktúrákon belül „lyukakat” eredményez. Ezt úgy mondjuk. átadhatjuk függvényparaméterként.

Link* successor. S f(). }. // késõbb meghatározandó A List elsõ deklarációja nélkül a List használata a Link deklarációjában formai hibát okozott volna. // 'S' valamilyen típus neve . Link* suc. }.5. feltéve. extern S a. hogy ez a használat nem igényli egy tag nevének vagy a struktúra méretének ismeretét: class S. amely a típus neve: struct List. struct Link { Link* pre. A struktúra teljes deklarációjának végéig viszont nem adhatunk meg újabb ilyen típusú objektumokat: struct No_good { No_good member. }. }. Két (vagy több) struktúra-típus kölcsönös hivatkozásához adjunk meg például egy nevet. Link* member_of. nem csak a teljes deklaráció után: struct Link { Link* previous. mert a fordítóprogram nem képes eldönteni a No_good méretét. void g(S). S* h(S*). A struktúra-típus neve a típus meghatározása elõtt is felhasználható. ahol elõször megjelenik. Mutatók. // hiba: rekurzív definíció Ez azért hibás. tömbök és struktúrák 137 Egy típus neve rögtön felhasználható attól a ponttól. struct List { Link* head.

g(a).. ezekkel elkerülhetjük a kétértelmûséget. S* q = h(p). akkor is.1. fejezet) egyszerû formája. 5. int stat(char* name.. // hiba: S nem definiált. union (§C. ha nem terheljük túl a neveket. p->m = 7. ha tagjaik ugyanazok: struct S1 { int a. S2 y = x.8) kulcsszavak is használhatók. a tag neve nem ismert } A struct az osztály (10. hacsak meg nem adjuk az S típusát: void k(S* p) { S a. a helyfoglaláshoz méret kell // hiba: S nem definiált. az adatszerkezetre pedig a struct elõtaggal kell hivatkoznunk. */ }. A fenti két típus különbözõ. struct S2 { int a. q->m = 7. Egyenértékû típusok Két struktúra mindig különbözõ típusú. paraméter átadásához méret kell // hiba: S nem definiált.2) és enum (§4.8. Ebben az esetben a „sima” stat név a nem-struktúra neve. }.138 Alapok A fenti deklarációk közül azonban sok nem használható. A C történetére visszanyúló okok miatt ugyanazzal a névvel és ugyanabban a hatókörben megadhatunk egy struct-ot és egy nem struktúra jellegû típust is: struct stat { /* . Elõtagként a class. érték visszaadásához méret kell // hiba: S nem definiált. a tag neve nem ismert // rendben: a mutatók számára foglalható hely és át is adhatók // hiba: S nem definiált. // hiba: nem megfelelõ típus . f(). így S1 x. A legjobb azonban. struct stat* buf).7. }.

Tanácsok [1] [2] [3] [4] [5] [6] [7] [8] Kerüljük a nem magától értetõdõ mutató-aritmetikát. egészre hivatkozó konstans mutató.3.1.1. Kerüljük a kódban a nem magától értetõdõ literálokat („mágikus számokat”).6. Használjuk a vector-t és a valarray-t a beépített (C stílusú) tömbök helyett. Gyakorlatok 1.3. karakterláncokból álló tömbre hivatkozó mutató. Használjunk 0-át NULL helyett. konstans egészre hivatkozó mutató. hogy ne írjunk egy tömb indexhatárán túlra. 2.2.1. // hiba: nem megfelelõ típus Minden struct-nak egyértelmû meghatározása kell. §5.1. Használjunk a lehetõ legkevesebb egyszerû referencia-paramétert. int*. hogy legyen a programban (§9.3.3. 10 egészbõl álló tömb referenciája. (*1. §5. Használjunk string-et nulla végzõdésû karaktertömbök helyett. (*1) Vezessük be a következõket: karakterre hivatkozó mutató. 5. Az alacsonyszintû kódot kivéve kerüljük a void*-ot. Ügyeljünk arra.9.8. ezért S1 x. 10 egészbõl álló tömb.5. §5. és void* mutatótípusokra vonatkozó megszorítások a mi rendszerünkön? Lehetne-e például egy int*-nak furcsa értéke? Segítség: illesztés. §5. konstans egész. §5.5) Mik a char*. tömbök és struktúrák 139 A struktúra-típusok az alaptípusoktól is különböznek. 5. Mindegyiknek adjunk kezdeti értéket. §5. §5. karakterre hivatkozó mutatóra hivatkozó mutató. §4. Használjunk helyettük jelképes állandókat. .4.8. §5. Mutatók. int i = x.3.5.).

140 Alapok 3. (*1) Adjunk meg egy karakterláncokból álló tömböt.3. (*1. karakterre hivatkozó mutatóra hivatkozó mutató.5) Készítsünk egy táblázatot. 7 elemû. ami egy dátummal ad kezdõértéket a Date-nek. egészre hivatkozó mutató. Adjuk át a tömböt egy függvénynek. Az "ab" pár például kétszer szerepel az "xabaacbaxabb"-ben. (*1. amely megszámolja egy betûpár elõfordulásait egy karakterláncban. Ne írjuk ki kétszer ugyanazt a szót. 5. g(char&) és h(const char&) függvényeket. Hívjuk meg õket az 'a'. ami ugyanezt csinálja egy nulla végû karaktertömbben (vagyis egy C stílusú karakterláncban).5) Találjunk példát. 7 elemû. egészre hivatkozó mutatókat tartalmazó 7 elemû tömbökbõl álló 8 elemû tömb. uc és sc paraméterekkel. 11. Csináljuk meg mindezt kétszer: egyszer használjunk karaktertömböt a nevek és egy tömböt a napok számára. amely kiírja a karakterláncokat. (*1) Használjunk typedef-et a következõk meghatározására: unsigned char. 10. (*2) Írjunk olyan függvényt. ahol az egyes adatszerkezetek a hónap nevét és a benne levõ napok számát tárolják. hogy megnézzük. mielõtt kiírná azokat. ahol a karakterláncok a hónapok neveit tartalmazzák. . hol lenne értelme egy nevet a saját kezdõértékében használni. const unsigned char. (*1. 4. Írjuk ki a beolvasott szavakat. (*2) Olvassuk be a bemenetrõl szavak egy sorozatát. ahol c char. Mely hívások megengedettek? Mely hívásoknál vezet be a fordítóprogram ideiglenes változót? 7.5) Adjunk meg egy Date struktúrát dátumok ábrázolásához. karaktertömbökre hivatkozó mutató. (*1. Írjuk ki a táblázatot. A bemenetet lezáró szóként használjuk a Quit-et. hogy rendezze a szavakat. ami Date-eket olvas be a bemenetrõl. egészre hivatkozó mutatókból álló tömbre hivatkozó mutató. ami Date-eket ír a kimenetre. Írjunk egy másik swap-et is. uc unsigned char és sc signed char típusú. amely két egészt cserél fel. nézzük meg. (*1) Írjunk egy swap nevû függvényt.5) Mi az str tömb mérete a következõ példában? char str[ ] = "rövid karakterlánc". Mi a "rövid karakterlánc" hossza? 6. Ha különbözõ mértékû optimalizálást lehet használni. 9. 12. amely a hónapok neveibõl és napjaik számából áll. melynek paraméterei int& típusúak. másodszor használjunk struktúrákból álló tömböt. 8. 49. (*2) Futtassunk le néhány tesztet.1). c. 3300. olyat. és olyat. 13. (*1) Készítsük el az f(char). Írjuk ki ezeket. hat-e és hogyan hat ez a létrehozott kód minõségére. Módosítsuk a programot. a fordítóprogram tényleg egyenértékû kódot hoz-e létre a mutatók használatával és az indexeléssel való tömbbejáráshoz (§5. és egy másikat. egészre hivatkozó mutatókból álló tömb. Írjunk olyan függvényt. Használjunk int* típust a paraméterek típusaként.

nem hagyhatjuk figyelmen kívül a hatékonyságot. Knuth) „Másrészrõl.” (John Bentley) „Asztali számológép” példa • Bemenet • Parancssori paraméterek • Kifejezések (áttekintés) • Logikai és összehasonlító operátorok • Növelés és csökkentés • Szabad tár • Meghatározott típuskonverziók • Utasítások (áttekintés) • Deklarációk • Elágazó utasítások • Deklarációk a feltételekben • Ciklusutasítások • A hírhedt goto • Megjegyzések és behúzás • Tanácsok • Gyakorlatok .” (D.6 Kifejezések és utasítások „Az idõ elõtti optimalizálás minden rossz gyökere.

A mûveleti jeleket a számok között (infix operátorként) kell megadni. Egy asztali számológép A kifejezéseket és utasításokat egy asztali számológép példáján keresztül mutatjuk be. a szimbólumtábla tartalmazza a nem változó adatokat és a vezérlõ kezeli a kezdeti értékadást. A számológépet számos szolgáltatással bõvíthetjük.142 Alapok 6.5 a bemenet elsõ sorának. A számológép négy fõ részbõl áll: egy elemzõbõl (parser). a 19. és a legtöbb szolgáltatás csak a kódot növelné. egy adatbeviteli függvénybõl.635 a bemenet második sorának eredménye.6[20]). hogy még hasznosabbá tegyük (§6.1. hogy további betekintést nyújtana a C++ használatába. 6. Valójában ez egy miniatûr fordítóprogram. egy szimbólumtáblából és egy vezérlõbõl. A felhasználó változókat is megadhat. amelyben az elemzõ végzi a szintaktikai elemzést (vagyis a nyelvi utasítások formai elemzését).5 19. Az elemzõ Íme a számológép által elfogadott nyelvtan: program: END expr_list END expression PRINT expression PRINT expr_list // END a bevitel vége expr_list: // PRINT a pontosvesszõ .1.1.635 A 2.5 area = pi * r * r A számológép program az alábbiakat fogja kiírni (pi elõre meghatározott): 2. A bemenet legyen a következõ: r = 2. anélkül. a kimenetet és a hibákat. A számológép a négy aritmetikai alapmûveletet hajtja végre lebegõpontos értékeken. az adatbeviteli függvény kezeli a bemenetet és végzi a lexikai elemzést (vagyis a nyelvi elemek értelmezését). de a kód így is elég hosszú lesz.

DIV='/'. A lezáró szimbólumokat (például az END. /.term term term: term / primary term * primary primary NUMBER NAME NAME = expression . +. Kifejezések és utasítások 143 expression: expression + term expression . A curr_tok változó típusa Token_value felsoroló típus: enum Token_value NAME. mint a C++. és az = . PLUS='+'. LP='('. NUMBER. a módszer hatékony is.'. Egy olyan nyelvben. PRINT='. . hogy bemenetet kapjon. term() és prim() szintaktikai elemzõ függvények ismerik fel. nevek és a *. Az utolsó get_token() hívás eredménye a curr_tok globális változóban található. a program kifejezések sorozata. ASSIGN='='. + és -) a get_token() lexikai elemzõ. Az elemzõ a get_token() függvényt használja arra. A nyelvtan minden szabályára adunk egy függvényt. akár két operandusú) operátorok. RP=')' Token_value curr_tok = PRINT. a kifejezés kiértékelõdik – egy igazi fordítóprogram esetében ezen a ponton történhetne a kód létrehozása. { NUMBER. amelyben a függvényhívások viszonylag „kis költségûek". MUL='*'.primary ( expression ) primary: Más szóval. a nem lezáró szimbólumokat pedig az expr(). népszerû és lényegretörõ.6. amely más függvényeket hív meg. amelyeket pontosvesszõk választanak el egymástól. felülrõl lefelé haladó eljárás. Az általunk használt szintaktikai elemzés módszerét rendszerint rekurzív leszállásnak (recursive descent) nevezik. Ha egy (rész)kifejezés minkét operandusa ismert. A kifejezések alapelemei a számok. MINUS='-'. END.(akár egy. . A neveket nem kell használat elõtt definiálni. }.

melynek értékét felsoroló konstansként már használjuk. A függvény kiértékeli a „saját” kifejezését és visszaadja az értékét. akik hibakeresõt (debugger) használnak. hogy meg kell-e hívnia a get_token()-t a következõ szimbólum beolvasásához. mert a curr_tok ezt az értéket fogja felvenni. case MINUS: left -= term(true). for (. Így alapállapotban „indítjuk el a rendszert”. Én pedig nem tudok olyan karakterkészletrõl. amely elemeket (term) keres az összeadáshoz vagy kivonáshoz: double expr(bool get) { double left = term(get). a lehetõ legkisebbre csökkentjük annak az esélyét. A switch utasítás azt vizsgálja meg. . default: return left. break. A függvény egyetlen ciklusból áll. amelyben van olyan kiírható karakter.. break. amely jelzi. melynek egész értéke egy számjegyû. amíg bemenetként olyan karaktert nem adunk meg. Egy nagyobb program magasabb szintû függvényeihez hasonló módon más függvényeket hív meg a feladat elvégzéséhez. és segítheti azokat.2) paramétere. } // összeadás és kivonás // "örökké" (végtelen ciklus) } Ez a függvény önmagában nem csinál túl sokat. Minden elemzõ függvénynek van egy logikai (bool) (§4.144 Alapok Az. A programozónak nem kötelezõ megadnia a default részt. Az expr() függvény kezeli az összeadást és kivonást. hogy minden szimbólumot (token) a karakterének megfelelõ egész értékkel jelölünk. hogy hibák forduljanak elõ és egyedi indítókódra sincs szükségünk. A break-kel a switch utasításból léphetünk ki. A case címkéket követõ konstansoknak különbözniük kell egymástól. Ez a módszer addig mûködik.) switch (curr_tok) { case PLUS: left += term(true). a default címke választódik ki. miután a számológép kiértékelt egy kifejezést és kiírta annak értékét. Azért választottam a curr_tok kezdeti értékeként a PRINT-et. kényelmes és hatékony megoldás. hogy a switch kulcsszó után zárójelben megadott feltétel értéke megegyezik-e a konstansok valamelyikével. Ha a vizsgált érték nem egyezik egyik case címkével sem.

Ez a for utasítás (§6. Az összeadás és kivonás kezelésére a += és -= operátorokat használjuk. akkor x@=y jelentése x=x@y. fejezet tárgyalja. így a + = 1 nyelvtanilag hibás a + és az = közötti szóköz miatt. |. hogyan építsünk fel programot modulokból.3) végletes formája. ami meghívja a prim()-et. hogy végtelen ciklust írjunk. VAGY. A 8. hogy mindent csak egyszer és használat elõtt adunk meg. azzal a különbséggel.3. Használhatnánk a left=left+term(true) és left=left-term(true) formát is. A term() függvény ugyanolyan módon kezeli a szorzást és osztást. Minden értékadó operátor önálló nyelvi egység. ) jelölés megszokott módja annak. helyette használhatjuk a while(true) szerkezetet is. a program jelentése nem változna.2 foglalja össze. hanem közvetlenebbül is fejezik ki a kívánt mûveletet. . A furcsa for( . A számológép példa deklarációit – egy kivétellel – úgy rendezhetjük sorba. A prim() meghatározása elõtti deklaráció erre való. A mûveleti jeleket és jelentésüket §6. A kivétel az expr(). &. amíg nem talál a +-tól és . A következõ kétoperandusú mûveletekhez léteznek értékadó operátorok: + * / % & | ^ << >> Így a következõ értékadó operátorok lehetségesek: = += -= *= /= %= &= |= ^= <<= >>= A % a moduló vagy maradékképzõ operátor. illetve kizáró VAGY operátorok. ami pedig ismét meghívja az expr()-et. Kifejezések és utasítások 145 Figyeljük meg. A left+=term(true) és a left-=term(true) azonban nemcsak rövidebbek. ami meghívja a term()-et. úgy mondhatjuk ki.-tól különbözõ jelet. hogy x csak egyszer értékelõdik ki. << és >> pedig a balra és jobbra léptetõ operátorok. és a 9. amikor is a default címke utáni return utasítás hajtódik végre. ahogy azt a nyelvtanban meghatároztuk. hogy „örökké” (forever). Ha @ egy bináris (kétoperandusú) operátor. hogy a 2-3+4-hez hasonló kifejezések (2-3)+4-ként értékelõdnek ki. Ezt a kört valahol meg kell szakítanunk. és ^ a bitenkénti ÉS. A switch utasítás végrehajtása addig ismétlõdik. double expr(bool). mint ahogy az expr() kezeli az összeadást és kivonást: .6.

amely az elemi szimbólumokat kezeli. kivéve azt. } return error("Nullával nem lehet osztani").2. Az error() függvényt a §6.1). return v. // szorzás és osztás case DIV: if (double d = prim(true)) { left /= d. // elemi szimbólumok kezelése switch (curr_tok) { case NUMBER: // lebegõpontos konstans { double v = number_value. Következésképpen a left/=d osztás és értékadás csak akkor megy végbe. A nullával való osztás nem meghatározott és rendszerint végzetes hibát okoz. meghívjuk az error()-t. double prim(bool get) { if (get) get_token(). break. A d változót pontosan azon a ponton vezetjük be a programba. Egy feltételben bevezetett név hatóköre a feltétel által vezérelt utasítás.3. default: } } return left. az eredményezett érték pedig a feltétel értéke (§6. és rögtön kezdeti értéket is adunk neki.. és ha igen. } . hogy mivel már lejjebb értünk a hívási hierarchiában. string string_value. for (. némi valódi munkát kell végezni és nincs szükség ciklusra: double number_value. ha d nem nulla.) switch (curr_tok) { case MUL: left *= prim(true).146 Alapok double term(bool get) { double left = prim(get).1. Ezért osztás elõtt megnézzük. nagyban hasonlít az expr()-re és a term()-re. break. ahol az szükséges.4-ben ismertetjük. A prim() függvény. get_token(). hogy a nevezõ 0 -e.

case LP: { double e = expr(true). if (get_token() == ASSIGN) v = expr(true). ahogy az utolsó beolvasott NUMBER-t a number_value tárolja.double> table. get_token(). A szimbólumtábla egy map (§3. Ideális esetben egy nyelvi egység (lexikai szimbólum) két részbõl áll: egy értékbõl. if (curr_tok != RP) return error(") szükséges"). ami a karakterlánchoz tartozik. } default: return error("elemi szimbólum szükséges"). visszaadjuk az értékét. A get_token() bemeneti eljárás elhelyezi az értéket a number_value globális változóban.7. hogy a nevet értékül kell-e adnia vagy csak egyszerûen be kell olvasnia. hogy az utolsó beolvasott NUMBER értékét tárolja. Kifejezések és utasítások 147 case NAME: { double& v = table[string_value]. Azaz. return v. . Itt is ez történt.1): map<string. amely meghatározza a szimbólum fajtáját (ebben a programban ez a Token_value) és (ha szükséges) a token értékébõl. §17. meg kell néznie. Itt csak egy egyszerû curr_tok változó szerepel. hiba esetén viszont segítheti a felhasználót. Globális változó használata a kódban gyakran jelenti. amikor a table-t egy karakterlánccal indexeljük. E kétes szerepû globális változó kiküszöbölését is a feladatok közé tûzzük ki (§6. } } Amikor egy NUMBER-t (azaz egy egész vagy lebegõpontos literált) találunk.4. Tegyük fel. így a number_value globális változó szükséges ahhoz. A számológép a számításhoz minden helyes bemenetnél használatba veszi az elsõ számot. Mindkét esetben a szimbólumtáblához fordul. mielõtt egy másikat olvasna be.4. // ')' lenyelése return e. az eredményül kapott érték az a double lesz. az utolsó beolvasott NAME karakterláncot a string_value tartalmazza. hogy a felhasználó a következõket írja be: radius = 6378.6[21]). ha mentjük az értéket és helyesen kiírjuk. } case MINUS: // egyoperandusú mínusz return -prim(true). Hasonlóan ahhoz. Mielõtt a számológép bármit kezdene egy névvel. hogy a program szerkezete nem kristálytiszta – valamiféle optimalizációt alkalmaztak rá.388. A number_value értékét nem feltétlenül szükséges a v lokális változóba menteni a get_token() meghívása elõtt.6.

majd visszaadjuk a beolvasott token-t ábrázoló Token_value értéket. azaz szóköz. Nem feltétlenül mindennapi feladat alacsonyszintû bemeneti eljárásokat írni.388. Ezek a szimbólumok késõbb a magasabb szintû eljárások bemeneti egységei lesznek. hogy úgy viselkedjen. switch (ch) { case 0: return curr_tok=END. amely komoly terhet ró a felhasználóra. // értékadás és visszatérés .. expr() kiszámolja az átadandó értéket v = 6378. ha megpróbáljuk a felhasználót rákényszeríteni. A v referenciát használjuk arra. hogy beolvasunk egy karaktert. Kellemetlen dolog (jogosan). hogy karaktereket olvasson be és magasabb szintû szimbólumokat hozzon létre belõlük. új sor stb. Itt az alacsonyszintû beolvasást a get_token() végzi. szokásaival és viszonylag véletlenszerû hibáival. hogy az olvasási mûvelet sikerült-e: Token_value get_token() { char ch = 0. hogy az a gép számára megfelelõbb legyen. A bemeneti függvény A bemenet beolvasása gyakran a program legrendezetlenebb része. // . cin>>ch.2.148 Alapok Ekkor a számológép az alábbiakat hajtja végre: double& v = table["radius"]. hogy eldöntsük. és ellenõrzik. amíg az expr() a bemeneti karakterekbõl kiszámítja a 6378. A kezdeti utasítások beolvassák az elsõ nem „üreshely” (whitespace. de jóval használhatóbb változatra.388 értéket. 6. Az ötlet az.) karaktert ch-ba. Ez azért van így. Sok rendszer erre a célra szabványos függvényeket nyújt.. Egy alacsonyszintû beolvasó eljárás feladata az. Ezután ezt módosítom egy kevésbé elegáns. tabulátor. milyen szimbólumot kell létrehozni. felhasználjuk arra. Két lépésben építem fel a get_token()-t.1. Elõször egy megtévesztõen egyszerû változatot készítek. mert a programnak egy emberrel kell társalognia és meg kell birkóznia annak szeszélyeivel. hogy a radius-hoz tartozó double értékre hivatkozzunk.

Az értékadás egy operátor. cin >> number_value.': '*': '/': '+': '-': '(': ')': '=': return curr_tok=Token_value(ch). Az. return curr_tok=NUMBER. a kód magától értetõdõ. Ez megengedi. Nézzünk meg néhány esetet külön-külön. Következésképpen ch==0 a bemenet végét jelzi. mert ez az elrendezés nehezebben olvasható. majd a konstanst a number_value változóba helyezzük. Elõször a kezdõ karaktert (számjegyet vagy pontot) visszatesszük a cin-be. A case címkéket függõleges helyett vízszintesen egy kupacba tenni általában nem jó ötlet. de elfelejtené módosítani a másikat. majd a változót ugyanabban az utasításban adjam vissza.putback(ch). Kifejezések és utasítások 149 Alapértelmezés szerint a >> operátor átugorja az üreshely karaktereket és ch értékét változatlanul hagyja. . Mivel a >> mûveleti jel a lebegõpontos konstansokat szabályszerûen egy double típusú változóba olvassa. A számokat így kezeljük: case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '. hogy a curr_tok változónak az END-et adjam értékül. az értékadás eredménye pedig annak a változónak az értéke. hogy a programozó megváltoztatná az egyiket. lehet. ha a bemeneti mûvelet nem sikerül. megkönnyíti a kód késõbbi módosítását. hogy egyszerûen visszaadjuk az értéküket: case case case case case case case case '.6. Ha az értékadást és a visszaadott értéket különválasztanánk a kódban. melynek értéket adunk. A kifejezések . hogy egy utasítást használunk kettõ helyett.': cin. mielõtt a teljes függvénnyel foglalkoznánk. végzõdését. a zárójeleket és az operátorokat úgy kezeljük. Fárasztó lenne azonban minden számjegyet külön sorba írni.

return curr_tok=PRINT.2) használjuk arra. Íme a teljes bemeneti függvény: Token_value get_token() { char ch = 0.': '*': '/': '+': '-': '(': ')': '=': return curr_tok=Token_value(ch). cin >> number_value. NAME =. hogy ne kelljen minden karaktert felsorolnunk.1. amíg üreshelyet nem talál. Ez nem ideális megoldás. mint különbözõ case címkéket.3-ban. switch (ch) { case 0: return curr_tok=END.150 Alapok A neveket hasonlóan kezeljük: default: // NAME. A standard könyvtárban levõ isalpha() függvényt (§20.putback(ch). cin>>ch. vagy hiba if (isalpha(ch)) { cin. } error("rossz szimbólum"). melyek a nevet operandusként használják. case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.4. ezért erre a problémára még visszatérünk §6. return curr_tok=NUMBER.putback(ch). Következésképpen a felhasználónak szóközzel kell befejeznie az adott nevet azon operátorok elõtt. . cin>>string_value. A karakterláncok (ebben az esetben a string_value) >> mûvelete addig olvassa a láncot. return curr_tok=NAME. case case case case case case case case '.': cin.

hogy a ciklusmag mindig legalább egyszer végrehajtódik.1. Elõször is. return curr_tok=NAME. amit az = operátor és a 7-es szám követ. Fárasztó emlékezni arra. do { A do utasítást használjuk. kivéve. // NAME. NAME =. ha ki akarjuk íratni az értékét.8). Alacsonyszintû bemenet Ha a számológépet úgy használjuk. Például x=7 egy azonosító. Mindkét problémát úgy oldjuk meg. az „új sor” karaktert azonosként kezeljük a kifejezés végét jelzõ pontosvesszõvel: Token_value get_token() { char ch.get(ch)) return curr_tok = END. 6. hogy csak üreshellyel lehet egy nevet befejezni. ahogy az eddigiekben leírtuk. amely egyenértékû a while utasítással. } error("rossz szimbólum"). fény derül néhány kényelmetlen dologra.6. switch (ch) { case '.': case '\n': return curr_tok=PRINT. cin>>string_value.get(ch) beolvas egy karaktert a szabványos bemeneti adatfolyamból ch-ba. Alapértelmezés szerint a get() nem ugorja át az üreshelyeket . return curr_tok=PRINT. hogy a get_token()-ben a típussal kapcsolatos alapértelmezett bemeneti mûveleteket olyan kódra cseréljük. hogy pontosvesszõt kell tennünk egy kifejezés után. } while (ch!='\n' && isspace(ch)). vagy hiba Egy operátor átalakítása az operátornak megfelelõ szimbólumra magától értetõdõ. A cin. és nagyon bosszantó tud lenni. amely egyenként olvassa be a karaktereket. és nem x. Kifejezések és utasítások 151 default: } } if (isalpha(ch)) { cin.putback(ch). // üreshelyek átugrása az '\n' kivételével if(!cin.3. mivel az operátorok token_value értékét az operátor egész értékeként határoztuk meg (§4.

4. Hasonló függvényekkel nézhetjük meg. így az isspace() használata sokkal gyorsabb.152 Alapok úgy. hogy eldöntsük. amit az okoz. ahogy a >> mûvelet teszi. A vizsgálatot táblázatban való keresésként valósítjuk meg. cerr << "hiba: " << s << '\n'. 6. ha nem olvasható be karakter a cin-bõl. és visszatér: int no_of_errors. Az error függvény egyszerûen megszámolja a hibákat. miféle nyelvi egység jön. return 1.get(ch) && isalnum(ch)) string_value. amíg olyan karaktert nem találunk. betû (isalpha()). hogy olyan programokat hozzunk létre. amíg üreshelyeket nem talál. hogy a kódnak csak egyes helyi érvényességû részeit módosítjuk. úgy oldjuk meg.get(ch)) ellenõrzés sikertelen. mint az egyes üreshely karakterek vizsgálata. hogy a >> addig olvassa a karakterláncot. NAME=.push_back(ch).2) szabványos vizsgálatát. A problémát. melyek javítását.1. mert a get() igazat ad vissza. más esetben nullát. a következõ karaktert arra használjuk. hogy egy karakter számjegy (isdigit()). cin. ha sikeres. } . double error(const string& s) { no_of_errors++. Szerencsére mindkét javítás elvégezhetõ úgy.4. vagy hiba if (isalpha(ch)) { string_value = ch. ami nem szám és nem betû: default: // NAME. az isspace(c) nem nulla értéket ad vissza. Fontos tervezési cél. esetleg betû vagy szám-e (isalnum()). return curr_tok=PRINT. return curr_tok=NAME.putback(ch). Hibakezelés Mivel a program ennyire egyszerû. Az if(!cin. a hibakezeléssel nem kell komolyabban törõdnünk. kiír egy hibaüzenetet. A ! (NEM) operátort azért használjuk. while (cin. } error("rossz szimbólum"). A standard könyvtár isspace() függvénye végzi az üreshelyek (§20. Miután átugrottuk az üreshelyeket. fejlesztését helyi módosításokkal intézhetjük. hogy befejezzük a számológép mûködését. hogy egyszerre egy karaktert olvasunk be. ebben az esetben END-et adunk vissza. Ha c üreshely.

majd befejezi a programot. cout << expr(false) << '\n'. ami akkor lenne hasznos. más esetben nem nullát (§3.1). Kivételek használatával elegánsabb hibakezelõ eljárások készíthetõk (lásd §8. milyen ésszerû módon folytathatná mûködését. Kifejezések és utasítások 153 A cerr egy átmeneti tárba nem helyezett („nem pufferelt”) kimeneti adatfolyam. Azért adunk vissza értéket. amely nem valószínû. hogy elindítsuk a mûködést. melynek visszatérési értéke az exit() paramétere lesz (§9. amely rendszerint hibajelzésre használatos (§21. fejezet). Ezt tehetjük meg az exit() meghívásával.2. amely elõször rendbe rakja az adatfolyamokat és hasonló dolgokat.3 és 14.2). if (curr_tok == PRINT) continue.4. hogy további hibákat okozna. // elõre megadott nevek beillesztése Hagyomány szerint a main() 0-át kell.1.6.[19]). A program futásának gyakran be kell fejezõdne. while (cin) { get_token().1). if (curr_tok == END) break. Ha a get_token() nyomon követte volna a sorok számát. az error() tájékoztathatta volna a felhasználót a hiba pontos helyérõl. mert nincs megadva. már csak a vezérlõ kódra van szükségünk ahhoz. hogy visszaadjon.5. A hibák számának visszaadásával ezt szépen megold- . egy 150 soros számológépnek éppen megfelel. ha a számológépet nem interaktívan használnánk (§6. A vezérlõ Miután a program minden részlete a helyére került. Ebben az egyszerû példában ezt a main() végzi el: int main() { table["pi"] = 3.1415926535897932385.1. vagy olyan értéket kellene visszaadnunk. de amit most csináltunk. table["e"] = 2. Ezen egyszerû számológép esetében az utóbbi megoldás megfelelõ. mert a hibák jellemzõen valamilyen kifejezés kiértékelése közben történnek.7182818284590452354. } } return no_of_errors. így vagy teljesen abba kellene hagynunk a kiértékelést. 6. miután hiba történt. ha a program hiba nélkül ér véget.6.

A fõ ciklus feladata. Ebben az esetben az egyetlen szükséges elõkészítés az.. . if (curr_tok == PRINT) continue. A PRINT (azaz '\n' és '. cout << expr(false) << '\n'. } megegyezik a következõvel: while (cin) { // . Ezt a következõ sor oldja meg: cout << expr(false) << '\n'. amellyel dolgozhat. az END vizsgálata pedig arról gondoskodik. hogy nem kell meghívnia a get_token()-t ahhoz. A continue utasítás egyenértékû azzal. } 6. így ebben az esetben while (cin) { // . ha hiba történik a bemeneti adatfolyammal. stb. A false paraméter mondja meg az expr()-nek. hogy a program befejezõdik. Fejállományok A számológép a standard könyvtár eszközeit használja..') vizsgálata megkönnyíti az expr() dolgát az üres kifejezések kezelésében. hogy a ciklusból megfelelõen lépjünk ki. ha a get_token() a fájl végéhez ér. A cin ciklusonként egyszeri ellenõrzése biztosítja. A break utasítás a legközelebbi körülvevõ switch utasításból vagy ciklusból (azaz for. if (curr_tok != PRINT) cout << expr(false) << '\n'. hogy a ciklus legvégére ugrunk. hogy a szimbólumtáblába bele kell tennünk az elõre megadott neveket. while vagy do utasításból) lép ki... hogy egy újabb szimbólumot kapjon.6.154 Alapok juk. Ezért a megfelelõ fejállományokat (header) be kell építenünk (#include). hogy beolvassa a kifejezéseket és kiírja a választ.1. hogy befejezzük a programot: #include<iostream> #include<string> #include<map> #include<cctype> // bemenet/kimenet // karakterláncok // asszociatív tömb // isalpha().

így argv[argc]==0.1934 Ekkor a paraméterek értéke a következõ: argc: argv: 2 0 "dc" "150/1.4. A 8. Parancssori paraméterek Miután a programot megírtam és kipróbáltam. ezért argv típusa char*[argc+1] lesz. függvényeket stb. a másik a paraméterekbõl álló tömb. fejezet tárgyalja. A paraméterek listáját a null karakter zárja le. §9. A program a main() (§3. milyen módon lehet ezt a számológépet a névterek használatával modulokba szervezni és hogyan lehet forrásfájlokra bontani. Én az utóbbit választottam.2.7. A szabványos fejállományoknak számos rendszeren .1. hogy elõször el kell indítani a programot. hogy az általuk nyújtott neveket felhasználhassuk.h kiterjesztésû fájl megfelelõjük van.1. és a globális névtérbe is behelyezik azokat (§9. Kifejezések és utasítások 155 Ezen fejállományok mindegyike az std névtérben nyújt szolgáltatásokat. hogy ne keverjem össze a kifejezések tárgyalását a modularitás kérdéskörével. végül ki kell lépni. és a 9.6.. §9. így argc értéke mindig legalább 1. 6. kényelmetlennek találtam. jónéhány billentyûleütést megtakaríthatnánk.4.3.) meghívásával kezdõdik.1934" . vagy az std:: minõsítõt kell használnunk. a paraméterek (argumentumok) számát adja meg. aztán be kell gépelni a kifejezéseket. így ahhoz.2. vagy a globális névtérbe kell helyeznünk a neveket a következõképpen: using namespace std. amely két paramétert kap: az egyik. A program neve (ahogy az a parancssorban elõfordul) argc[0]-ként adódik át. §B.2. A paraméterek karakterláncok. melyek leírják az osztályokat. A leggyakrabban egyetlen kifejezés kiértékelésére használtam a programot. Vegyük az alábbi parancsot: dc 150/1. ezt rendszerint argv-nek hívják. melyet általában argc-nek neveznek. Ha egy kifejezést meg lehetne adni parancssori paraméterként.1).

156 Alapok Mivel a main() meghívására vonatkozó szabályok a C nyelv követelményeivel azonosak. break. Sajnos nincs elegáns módja annak. } if (input != &cin) delete input. cout << expr(false) << '\n'. hogyan használjuk azokat úgy. if (curr_tok == END) break. default: error("túl sok paraméter").1415926535897932385. table["e"] = 2. milyen parancssori paramétereket adunk meg. attól függõen. Egyszerû megoldás. while (*input) { get_token(). Az ötlet a következõ: olvassunk ugyanúgy a parancssori karakterláncból. case 2: // a karakterlánc paraméter beolvasása input = new istringstream(argv[1]). ha bevezetünk egy input nevû globális mutatót. // mutató bemeneti adatfolyamra int main(int argc. char* argv[ ]) { switch (argc) { case 1: // olvasás a szabványos bemenetrõl input = &cin. return no_of_errors. break. a probléma csak az. A karakterláncból olvasó adatfolyam neve – micsoda meglepetés – istringstream.7182818284590452354. amely a használandó bemeneti adatfolyamra mutat. hogy minél kevesebbet kelljen programoznunk. A parancssori paraméterek beolvasása egyszerû. } table["pi"] = 3. minden bemeneti eljárásban ezt fogjuk felhasználni: istream* input. if (curr_tok == PRINT) continue. hogy a számológép bemeneti függvényei hogyan hivatkozzanak vagy az istringstream-re vagy a cin-re. mint a bemeneti adatfolyamokból. hogy cin-ként az istringstream-re hivatkozhassunk. return 1. ezért ki kell találnunk. a híváskor C típusú tömbök és karakterláncok használatosak. // elõre megadott nevek beillesztése } .

Nem volt elegáns dolog úgy módosítani a bemeneti eljárásokat.217/rate" Azért használok idézõjeleket.3. Az istringstream használatához be kell építeni az <sstream> fejállományt. . amit csak egyetlen program használ fel. Kifejezések és utasítások 157 Az istringstream olyan istream. Általánosabb és hasznosabb megoldást készíthetünk. pontosan ugyanúgy jelzi azt.19.4) vagy objektum (§2. amely karakterlánc paraméterébõl olvas (§21.75/rate. hogy használják azokat. mert egyetlen paraméterként több kifejezést is átadhatunk: dc "rate=1. Az alapvetõ probléma. alacsonyszintû kód található benne. de erre nincs szükség.3). Ha egy számológép modul vagy számológép típus tervezése lett volna a célom. mert a . Ha megnézzük a számológép kódját (különösen az elsõ változatot). 17.fejezet). Könnyû lenne úgy módosítani a main()-t. hogy a „számológép” csak függvények és adatok gyûjteménye. amikor saját kezûleg olyan kódot ír.150/rate.2).5. A könyvtárak tervezéskor és megvalósításkor általában nagyobb figyelmet kapnak. akik nem ismerik az asszociatív tömböket.6[16]).1. Más rendszerek szabályai a program indításakor paraméterek megadására vonatkozóan eltérõek. A változtatás elkerülhetõ lett volna. mint az ostream. hogy legyen. Megjegyzés a stílussal kapcsolatban A standard könyvtárbeli map szimbólumtáblaként való használata majdnem „csalásnak” tûnhet azoknak a programozóknak a szemében. amely kifejezett és egyértelmû módon ábrázolja a számológépet. Amikor eléri a lánc végét. mint amennyit egy programozó megengedhet magának. §21. hogy nem sok hagyományos C stílusú.7.5. Nincs olyan modul (§2. hogy ezzel rugalmasabbak legyenek és különbözõ bemeneti forrásokkal mûködhessenek. amit ezzel a számológéppel érzékeltetni akartam. láthatjuk.8. ha kellõ elõrelátással már a kezdetektõl bevezetünk valamilyen. hogy cin helyett *input-ot használjanak. akkor természetesen meggondoltam volna.5[3]. hogy több parancssori paramétert is elfogadjon. és map (§3. az.6.3). §3. Számos hagyományos trükköt helyettesítettünk azzal.6. hogy a bemenet forrása valójában a számológép modul paramétere kell. hogy olyan standard könyvtárbeli osztályokat használtunk. ha észrevesszük. §10. 6. §3. milyen paraméterei lehetnek a modulnak/típusnak (§8.4. a UNIX rendszerekben parancs-elválasztóként használatos.5. string. De nem az. A standard könyvtár és más könyvtárak arra valók.4.1934. az input-hoz hasonló dolgot. mint a többi adatfolyam a bemenet végét (§3.

Általában ilyennek kellene lennie egy olyan kódnak. nem pedig (a+b)*c.). hogy ezt eldönthessük. a mutató egy mutató eredményû kifejezés. Az egyoperandusú (unáris) és az értékadó operátorok jobbról balra. meg kell néznünk a nyelvtant (§A.2. sõt az értékadások is viszonylag ritkán fordulnak elõ.158 Alapok Vegyük észre. hogy az aritmetika. nem (*p)++. amely nem konstans objektumot jelöl. amelynek az eredménye osztályobjektum. fejezet). ()-lel stb. A kifejezések formája független az operandusok típusától. *p++ jelentése pedig *(p++). Például a=b<c?d=e:f=g jelentése a=((b<c)?(d=e):(f=g)).2. amikor az operandusok beépített típusúak (§4.5). A típus csak akkor lehet egy teljesen általános típusnév (*-gal. a tag egy tag neve. az objektum egy olyan kifejezés.1).1. 6. és a balérték egy olyan kifejezés. . a kif egy kifejezés. Minden operátort egy vagy több név követ. A felsõbb cellákban levõ operátorok az alsó cellákban levõkkel szemben elõnyt élveznek. Az itt bemutatott jelentések arra az esetre vonatkoznak. az összes többi balról jobbra értelmezendõ. máshol megszorítások vonatkoznak rá (§A. de ahhoz. A táblázatokban az osztálynév egy osztály neve. Például a=b=c jelentése a=(b=c). ha zárójelek közé van zárva. mint a +. amely példaként szolgál az általánosan használt megnevezésekre és a szokásos használatra. Operátorok – áttekintés Ez a rész összefoglalja a kifejezéseket és bemutat néhány példát. A felhasználói típusú operandusokra alkalmazott operátorok jelentését magunk határozhatjuk meg (§2. Néhány nyelvtani szabályt nem lehet kifejezni a precedenciával és az asszociativitással (kötéssel). A táblázat minden cellájában azonos erõsségû (precedenciájú) operátorok találhatók. a+b+c jelentése (a+b)+c. mert a * magasabb precedenciájú. a ciklusok.5.5). 11. amely nem kezeli a hardvert közvetlenül és nem él alacsonyszintû elvont adatábrázolásokkal. Például a+b*c jelentése a+(b*c).

tag mutató -> tag mutató [kif] kif (kif_lista) típus (kif_lista) balérték ++ balérték -typeid (típus) typeid (kif) dynamic_cast <típus> (kif) static_cast <típus> (kif) reinterpret_cast <típus> (kif) const_cast <típus> (kif) sizeof kif sizeof (típus) ++ balérték -. Kifejezések és utasítások 159 Operátor – áttekintés hatókör-feloldás hatókör-feloldás globális hatókör globális hatókör tagkiválasztás tagkiválasztás indexelés függvényhívás érték létrehozása növelés utótaggal csökkentés utótaggal típusazonosítás futási idejû típusazonosítás futási idõben ellenõrzött típuskényszerítés fordítási idõben ellenõrzött típuskényszerítés nem ellenõrzött típuskényszerítés konstans típuskényszerítés objektum mérete típus mérete növelés elõtaggal csökkentés elõtaggal komplemensképzés (logikai) nem mínusz elõjel plusz elõjel cím operátor indirekció létrehozás (memóriafoglalás) létrehozás (memóriafoglalás és kezdeti értékadás) létrehozás (elhelyezés) létrehozás (elhelyezés és kezdeti értékadás) felszámolás (felszabadítás) tömb felszámolása típuskonverzió osztálynév :: tag névtér_név :: tag :: név :: minõsített_név objektum .kif + kif & balérték * kif new típus new (kif_lista) new (kif_lista) típus new (kif_lista) típus (kif_lista) delete mutató delete [ ] mutató (típus) kif .6.balérték ~ kif ! kif .

kif kif << kif kif >> kif kif < kif kif <= kif kif > kif kif >= kif kif == kif kif != kif kif & kif kif ^ kif kif | kif kif && kif kif || kif kif ? kif : kif .160 Alapok Operátor – áttekintés (folytatás) tagkiválasztás tagkiválasztás szorzás osztás moduló (maradékképzés) összeadás (plusz) kivonás (mínusz) balra léptetés jobbra léptetés kisebb kisebb vagy egyenlõ nagyobb nagyobb vagy egyenlõ egyenlõ nem egyenlõ bitenkénti ÉS bitenkénti kizáró VAGY bitenkénti megengedõ VAGY logikai ÉS logikai megengedõ VAGY feltételes kifejezés objektum .*tagra_hivatkozó_mutató mutató -> *tagra_hivatkozó_mutató kif * kif kif / kif kif % kif kif + kif kif .

mielõtt az operátort alkalmazzuk rájuk. amelyet „általános aritmetikai átalakítások”-nak nevezünk (§C. Ha egy operátornak balérték operandusa van. az eredmény pedig long érték lesz. A felhasználó által megadott operátorok jelentését és eredményét deklarációjuk határozza meg (§11.2.2). Az ==. A fõ cél az. Az int-nél kisebb operandusok (mint a bool és a char) int-té alakulnak. kif 6. akkor – ha ez logikailag lehetséges – az operátor eredménye egy olyan balérték lesz.6.1. amely a balérték operandust jelöli: . hogy a „legtágabb” operandustípussal megegyezõ eredmény jöjjön létre. a számítás hosszú egész (long) aritmetikával történik.3). relációs (összehasonlító) operátorok logikai értékeket adnak vissza. Eredmények Az aritmetikai mûveletek eredményének típusát az a szabályhalmaz dönti el. Ha long típusú operandusa van. a számítást lebegõpontos aritmetikával végezzük és az eredmény egy lebegõpontos érték lesz. Ha egy bináris operátor operandusa például lebegõpontos. Kifejezések és utasítások 161 Operátor – áttekintés (folytatás) egyszerû értékadás szorzás és értékadás osztás és értékadás maradékképzés és értékadás összeadás és értékadás kivonás és értékadás balra léptetés és értékadás jobbra léptetés és értékadás ÉS és értékadás megengedõ VAGY és értékadás kizáró VAGY és értékadás kivétel kiváltása vesszõ (mûveletsor) balérték = kif balérték *= kif balérték /= kif balérték %= kif balérték += kif balérték -= kif balérték <<= kif balérték >>= kif balérték &= kif balérték |= kif balérték ^= kif throw kif kif .6. <= stb.

melynek meghatározása a <cstddef> fejállományban szerepel. cout << "Az i negatív lett!" << i << '\n'. amelyek C++ kódot hoznak létre). Hasonlóan. . int y) { int j = x = y. ez viszont rendszerint a program hirtelen befejezõdését eredményezi. hogy ilyen módon megõrizzük a balértékeket. a túlcsordulás és a nullával való osztás nem vált ki szabványos kivételeket (§14. Az alulcsordulás. Ez különösen akkor fontos. int* p = &++x. A fordítónak nem kell ellenõriznie az aritmetikai túlcsordulást és általában nem is teszi meg. } // x=y értéke az x értékadás utáni értéke // p x-re mutat // hiba: x++ nem balérték // a nagyobb értékû int címe Ha a ? : második és harmadik operandusa is balérték és ugyanolyan típusúak. nagy rugalmasságot ad az operátorok használatában. int* q = &(x++). Ami ekkor történik. int* pp = &(x>y?x:y). amit ptrdiff_t-nek hívnak és szintén a <cstddef> fejállomány írja le. nem meghatározott. while (0 < i) i++. az eredmény a megfelelõ típusú balérték lesz. ha olyan kódot írunk. Például: void f() { int i = 1.10). az érték jellemzõen egy negatív számig „ér körbe” (az én gépemen ez -2147483648).162 Alapok void f(int x. Az. a mutató-kivonásé pedig egy elõjeles integrális típus. a nullával osztás eredménye sem meghatározott. A sizeof eredménye a size_t nevû elõjel nélküli integrális típus. amelynek egyformán és hatékonyan kell mûködnie beépített és felhasználói típusok esetében is (például ha olyan sablonokat vagy programokat írunk. } A ciklus elõbb-utóbb az i értékét a legnagyobb egész értéken túl növeli.

hogy a . Az olyan megoldás. nagyon rossz stílusról árulkodik és eredménye nem meghatározott. // nem meghatározott eredmény A fenti kifejezés vagy v[1]=1-ként. // nem meghatározott. hogy a kifejezés kiértékelése balról jobbra történik: int x = f(2)+g(3).2. f2( (v[i]. Beépített típusoknál a && második operandusa csak akkor értékelõdik ki. A || és a && használatára vonatkozó példák a §6. ha a kifejezések kiértékelési sorrendje nem kötött. // két paraméter // egy paraméter Az f1 meghívásának két paramétere van.i++). A . sajnos.6. v[i] = i++. de a kiértékelési sorrendre vonatkozó megszorítások hiánya elõre nem meghatározott eredményekhez vezethet: int i = 1. a && (logikai ÉS). amit arra használunk. Nézzük az alábbi példát: f1(v[i]. . A fordítóprogramok figyelmeztethetnek az ilyen kétértelmûségekre.2.2. a || második operandusa pedig csak akkor. a (v[i]. amely i++-szal egyenértékû. (vesszõ) mûveletsor-jelzõ logikailag különbözik attól a vesszõtõl. Az f2 meghívásához egy paramétert adtunk meg.3-ban találhatók. v[i] és i++. esetleg még furcsábban viselkedik. ha az elsõ operandus értéke false. ha az elsõ operandus true. Jegyezzük meg. így nem tételezhetjük fel például azt sem. a paraméter-kifejezések kiértékelési sorrendje pedig nem meghatározott. vagy v[2]=1-ként értékelõdik ki. és a || (logikai VAGY) operátorok esetében biztosított. ezt néha rövid vagy „rövidzáras” kiértékelésnek (short-circuit evaluation) nevezik.a+1) például a bnek 3-at ad értékül. A b=(a=2. Kiértékelési sorrend A kifejezéseken belüli részkifejezések kiértékelési sorrendje nem meghatározott. amely függ a paraméter-kifejezések sorrendjétõl. Kifejezések és utasítások 163 6. i++) „vesszõs” kifejezés. (vesszõ).i++) ). hogy f() vagy g() hívódik meg elõször Jobb kódot készíthetünk. hogy a bal oldali operandus a jobb oldali elõtt értékelõdik ki. a legtöbb ezt nem teszi meg. hogy a függvényhívásoknál elválasszuk a paramétereket.

a kifejezés i&(mask==0)-ként lesz értelmezve. Az operátorok sorrendje A precedencia és a „kötési” (asszociativitási) szabályok a leggyakoribb használatot tükrözik. hogy szükségünk van zárójelekre – fontoljuk meg. ezért – ha úgy érezzük. ha a felhasználó nem tud különbséget tenni köztük. Mivel az == elõnyt élvez az & (kétoperandusú) mûvelettel szemben.. A zárójelek használata még gyakoribb. Szerencsére a fordítóprogram könnyen figyelmeztethet az ilyen hibákra. hogy nem kellene-e egy külön változó használatával szétbontanunk a kifejezést. ha a*(b/c)-t akarunk kapni. Például a*b/c jelentése (a*b)/c. így a fordítóprogram pontosan úgy fogja az ilyen kifejezéseket kiértékelni..2. ezért zárójeleket kell használnunk. Az a*(b/c) kifejezés csak akkor értékelõdhet ki (a*b)/c-ként. majd megnézzük.... . A bonyolult részkifejezések mindig hiba forrásai lehetnek. Zárójeleket használni azonban mindig hasznos. Az a*(b/c) és az (a*b)/c számos lebegõpontos számításnál jelentõsen különbözik. ahogy azokat leírtuk. ha a programozónak kétségei vannak ezekkel a szabályokkal kapcsolatban.. hogy „ha i kisebb vagy egyenlõ 0-nál VAGY max kisebb i-nél”. ha a részkifejezések bonyolultabbak. Az alábbi – értelmetlen.. Ez egyenértékû az alábbival: if ( (i<=0) || (max<i) ) // . de szabályos – kifejezéssel viszont nem: if (i <= (0||max) < i) // . amikor az operátorok sorrendje nem a „magától értetõdõ” értelmezést eredményezi: if (i&mask == 0) // hoppá! == kifejezés & operandusaként Ekkor nem az történik. 6. hogy alkalmazzuk a mask-ot az i-re.3. Ebben az esetben a zárójelek fontosak: if ((i&mask) == 0) // . Például if (i<=0 || max<i) // . hogy az eredmény 0-e. Vannak olyan esetek. azt jelenti.164 Alapok A csoportosítás kikényszerítésére zárójeleket használhatunk..

6.3. amit aztán összehasonlítva 99-cel true-t kapunk. a | operátort unióként. a ~-t pedig komplemensként értelmezzük. melyet az ostream megvalósításából vettünk kölcsön: enum ios_base::iostate { goodbit=0. 6. Kifejezések és utasítások 165 Érdemes megjegyezni. . Az eredmény típusát a szokásos aritmetikai átalakítások (§C. mert az = jelentése sok nyelvben „egyenlõ”. |. hogy a feltételekben =-t (értékadást) használnak == (egyenlõ) helyett: if (a = 7) // hoppá! konstans értékadás a feltételben Ez természetes. long típusokra. a ^-ot szimmetrikus differenciaként.4. char. hogy a következõ nem úgy mûködik. Az & bináris operátort metszetként.. >> és << bitenkénti logikai operátorokat integrális (egész típusú) objektumokra és felsorolásokra alkalmazzuk – azaz a bool.) döntik el.6.. Ez megengedett. A bitenkénti logikai operátorok jellemzõ felhasználása a kis halmazok (bitvektorok) fogalmának megvalósítása. eofbit=1. és a bitek száma korlátozza a halmaz elemeinek számát. ahogy egy matematikus elvárná: if (0 <= x <= 99) // .. de értelmezése (0<=x)<=99. A fordítóprogramok általában figyelmeztetnek is erre. hogy megnevezzük egy ilyen halmaz elemeit. -. A következõképpen vizsgálhatjuk meg. Ebben az esetben egy elõjel nélküli egész minden bitje a halmaz egy elemét jelöli. ahol az elsõ összehasonlítás eredménye vagy true vagy false. short. ezek elõjel nélküli (unsigned) megfelelõire és az enum típusokra.. failbit=2. A logikai értéket a fordítóprogram aztán automatikusan 1-re vagy 0-ra alakítja.2. Felsoroló típust arra használhatunk. badbit=4 }. Gyakori hiba kezdõknél. ^..99 tartományban van-e: if (0<=x && x<=99) // . Bitenkénti logikai mûveletek Az &. Íme egy rövid példa. int. hogy x a 0.

a módosíthatóság. amikor léptetéssel és maszkolással veszünk ki bitmezõket egy szóból. amikor össze kell hasonlítanunk azt a bitvektort. // . más hasonló típusoknál viszont alapvetõ mûvelet. amely a kezelt megszakítások halmazát jelöli. Ezek az adatfolyam-állapotjelzõk megfigyelhetõk a folyam megvalósításán kívül is. mert az & elõnyt élvez a | mûveleti jellel szemben. melyek arra várnak.4. } A bitenkénti logikai operátorokat ne keverjük össze az &&. hogy elérte a bemenet végét: state |= eofbit. hogy kezeljék õket.3). Ezt természetesen megtehetjük a bitenkénti logikai operátorokkal is. Az egyszerû state=eofbit értékadás kitörölt volna minden más bitet. hogy ezt a „zsonglõrködést” a bitekkel az iostream megvalósításából vettük és nem a felhasználói felületbõl. if (state&(badbit|failbit)) // nem megfelelõ adatfolyam A külön zárójelek azért szükségesek.3.rdstate(). A kényelmes bitkezelés nagyon fontos lehet. és a vector<bool>-t (§16. de a megbízhatóság. hogy az állapothoz hozzáadjunk valamilyen új információt. Jegyezzük meg. vagy false-t adnak vissza. egy másik bitvektorral. bitset-et (§17. A mezõk (§C. Például így nézhetjük meg. amikor egy . Általánosabb halmazfogalomra nézzük meg a standard könyvtárbeli set-et (§17. Az utóbbiak vagy true-t.1) használata igazán kényelmes módon rövidíti le azt a mûveletet. A |= operátort arra használjuk. // rdstate() az állapotot adja vissza Az adatfolyam-állapotok különbségeinek kiszámítása nem túl gyakori. vagy a hordozhatóság érdekében a rendszer alacsonyabb szintjein kell tartanunk.. és elsõdlegesen akkor hasznosak.166 Alapok Az adatfolyam az állapotot így állíthatja be és ellenõrizheti: state = goodbit. Vegyük például azt az esetet. amely olyan megszakítások halmazát ábrázolja. Egy függvény így jelezheti.rdstate()^cout.11). Egy 32 bites long középsõ 16 bitjét például így vehetjük ki: unsigned short middle(long a) { return (a>>8)&0xffff..3).8.5. hogyan különbözik két adatfolyam állapota: int diff = cin. || és ! logikai operátorokkal.

Például az !0 (nem nulla) true érték. . Az x++ értéke azonban az x régi értéke: az y=x++ egyenértékû az y=(t=x. i<=length. A ++ és -. A csökkentést ugyanígy a -. while. const char* q) { while (*p++ = *q++) . és nem az összeadás és értékadás párosításával fejezzük ki. vagy for utasításban (§6. Kifejezések és utasítások 167 if. A növelõ operátorok különösen a ciklusokban használatosak. amely a -1 érték kettes komplemensbeli ábrázolása. A növelendõ objektumot jelölõ kifejezés (csak) egyszer értékelõdik ki.operátor fejezi ki. mert megengedi az ilyen tömör.3. 6. míg a ~0 (a nulla komplemense) egy csupa egyesbõl álló bitminta. Mivel a while (*p++ = *q++) . p++ a p-t a következõ tömbelemre állítja (§5.3. hogy egy érték növelését közvetlenül.3. §6.5. változók növelésére vagy csökkentésére.x+=1. A ++x értéke az x új (megnövelt) értéke lesz.operátorokat használhatjuk elõtagként és utótagként is.2.mûködését azok a tömbelemek határozzák meg. Definíció szerint a ++lvalue jelentése lvalue+=1. hogy a balértéknek nincs „mellékhatása". Növelés és csökkentés A ++ operátort arra használjuk. megéri közelebbrõl megvizsgálnunk.1). A mutatók összeadásához és kivonásához hasonlóan a mutatókra alkalmazott ++ és -. kifejezésközpontú kódolást.3) feltételt írunk. feltéve. ez pedig lvalue=lvalue+1-et jelent.6. Egy nulla végzõdésû karakterláncot például a következõképpen másolhatunk át: void cpy(char* p. például az y=++x egyenértékû az y=(x+=1)gyel. kifejezés meglehetõsen zavaros a nem C programozók számára. amelyekre a mutató hivatkozik.t)-vel. for (int i = 0. ahol t egy x-szel azonos típusú változó. i++) p[i] = q[i]. Vegyük elõször a karaktertömbök másolásának egy hagyományosabb módját: int length = strlen(q).2. } A C-hez hasonlóan a C++-t is szeretik és gyûlölik azért. ez a kódolási stílus viszont nem ritka a C-ben és a C++-ban.

a következõképpen írhatjuk újra a ciklust: while (*q != 0) { *p++ = *q++. // léptetés a következõ karakterre // léptetés a következõ karakterre // lezáró nulla Mivel az utótagként használt növelõ operátor megengedi. A nulla végzõdésû karakterlánc hosszát úgy határozzuk meg. Végül tovább rövidíthetjük a példát azzal.168 Alapok Ez pazarlás. hogy a nulla végzõdést keresve végigolvassuk azt. Hatékonyabb idõben és tárterületben. hogy meghatározzuk a hosszát. q++. mint az elõzõ? Egy tapasztalt C vagy C++ programozó számára nem. mert egy mutató vagy integrális feltétel mindig összehasonlítódik 0-val. p[i] = 0. nincs szükségünk az üres blokkra és hogy felesleges a „!=0” vizsgálat. hogy *q nulla. hogy észrevesszük. amelyet célul tûztünk ki. for (i = 0. hogy átmásoljuk. } *p = 0. Ez a változat vajon kevésbé olvasható. Így kétszer olvassuk végig a teljes láncot: egyszer azért. Következésképpen elhagyhatjuk az utolsó értékadást. mert p és q mutatók: while (*q != 0) { *p = *q. egyszer pedig azért. Ezért inkább próbáljuk ezt: int i. } *p = 0. while (*p++ = *q++) . Így megkapjuk azt a változatot. amíg be nem másoljuk *p-be és meg nem növeljük p-t. // lezáró nulla A *p++ = *q++ értéke *q. de ki lehet küszöbölni. mint az elõzõ? Az elsõ változatot ki- . q[i]!=0 . // lezáró nulla Az i változót indexelésre használjuk. ezért a példát így módosíthatjuk: while ((*p++ = *q++) != 0) { } Ebben az esetben addig nem vesszük észre. i++) p[i] = q[i]. amiben a nulla végzõdést adjuk értékül. hogy elõször felhasználjuk az értéket. p++. és csak azután növeljük meg.

Kifejezések és utasítások 169 véve. részesítsük elõnyben a standard könyvtár lehetõségeit a mutatókkal és bájtokkal való ügyeskedéssel szemben.9. Az. ami meghívta az strlen()-t. .1). gyakran azonban hasznos. hogy „a szabad tárban vannak” (free store). // . miután visszatértünk abból a függvénybõl. const char*). ahol létrehoztuk.6. Enode* right. Az ilyen objektumokat a new operátor hozza létre és a delete operátort használhatjuk felszámolásukra.1. A szintaktikai elemzõ függvények felépíthetnek egy kifejezésfát a kódkészítõ számára: struct Enode { Token_value oper. amely függetlenül létezik attól a hatókörtõl. hogy „kupac-objektumok” (heap). // a <string. char* strcpy(char*. vagy azt. 6. vagyis „a dinamikus memóriában vannak”. nem igazán. Enode* expr(bool get) { Enode* left = term(get). Enode* left.6.4) dönti el. Ahol lehetséges. §18. hogyan írnánk meg egy fordítóprogramot olyan stílusban. A new által létrehozott objektumokra azt mondjuk. Nézzük meg. mielõtt elhinnénk. hogy melyik változat a leghatékonyabb.7. a gép felépítésétõl és a fordítóprogramtól függ.2. Nevezetesen gyakori.. ha olyan objektumot hozunk létre. amelyek akkor is felhasználhatók.2.. hogy olyan objektumokat hozunk létre.h> fejállományból Általánosabb másolásra a szabványos copy algoritmust (§2. hogy valamilyen kézzel írt kódrészlet felülmúlja a könyvtári függvények teljesítményét. Szabad tár A névvel rendelkezõ objektumok élettartamát (lifetime) hatókörük (§4.1) használhatjuk. A standard könyvtár függvényei lehetnek helyben kifejtett függvények (§7. ahol létrehoztuk azokat.6. }.1) vagy egyedi gépi utasításokkal megvalósítottak. ahogy az asztali számológépnél tettük (§6. Ezért gondosan fontoljuk meg. a nulla végzõdésû karakterláncok másolásának leghatékonyabb módja viszont általában a standard könyvtárbeli másoló függvény.

} default: return left.9. Ezután a new újra felhasználhatja az objektum által lefoglalt tárhelyet. A delete operátort csak a new által visszaadott mutatóra vagy nullára lehet alkalmazni. } // Enode létrehozása a szabad tárban // csomópont visszaadása A kódkészítõ aztán felhasználná az eredményül kapott csomópontokat (node) és törölné azokat: void generate(Enode* n) { switch (n->oper) { case PLUS: // . amíg kifejezetten meg nem semmisítjük a delete-tel. hogy van „szemétgyûjtõ” (garbage collector). Ha van szemétgyûjtõ. amely megkeresi azokat az objektumokat.) } switch(curr_tok) { case PLUS: case MINUS: { Enode* n = new Enode. break. a deleteek a legtöbb esetben elhagyhatók (§C. hogy a new által létrehozott objektumokat magunknak kell megsemmisítenünk. } } // Enode törlése a szabad tárból A new által létrehozott objektum addig létezik. Ha a delete-et nullára alkalmazzuk. amelyekre nincs már hivatkozás és újra felhasználhatóvá teszi azok helyét. n->left = left.. left = n.170 Alapok for (. . delete n.6).. n->right = term(true)..1). A C++változatok nem garantálják. nem lesz hatása. Következésképpen feltételezzük. A new operátornak egyedi változatait is meghatározhatjuk (§15. n->oper = curr_tok. a delete-et használva.

. hogy egyes objektumokat felszámoljuk. delete[ ] p. Jegyezzük meg. delete p. // . a delete-nek vagy a delete[ ]-nek meg kell tudni állapítani. Ebbõl az következik. Ha a delete[ ]-et nullára alkalmazzuk. } A „sima” delete operátort arra használhatjuk. delete[ ] q. . char* argv[ ]) { if (argc < 2) exit(1).6. §16. mekkora a lefoglalt objektum mérete. ezért létrehozására és felszámolására a sima new-t és delete-et használhatjuk: void f(int n) { vector<int>* p = new vector<int>(n). hogy a vector (§3... // másolás p-bõl s-be return s. mint a statikus objektumok. int* q = new int[n]. char* p = save_string(argv[1]).. } // önálló objektum // tömb A delete[ ] operátort csak a new által visszaadott mutatóra vagy nullára alkalmazhatjuk. a delete[ ] tömbök felszámolására használatos.1.3) valódi objektum. hogy a szabványos new operátorral létrehozott objektumok valamivel több helyet foglalnak.p).2. Tömbök A new használatával létrehozhatunk objektumokból álló tömböt is: char* save_string(const char* p) { char* s = new char[strlen(p)+1]. strcpy(s. nem lesz hatása.7. Ha vissza akarjuk nyerni a new által lefoglalt tárhelyet.1. } int main(int argc. Kifejezések és utasítások 171 6.6. Az objektum méretét általában egy gépi szó tárolja. // .

// hely az önálló objektum számára // hely a tömb számára Ha a new operátornak egy objektum számára kell helyet foglalnia. } catch(bad_alloc) { cerr << "Elfogyott a memória!\n". Ha tömb számára foglal helyet. mit csináljon a new. hogy az megfelelõ számú bájtot foglaljon le.5-öt): void f() { try { } for(. delete. void operator delete[ ](void*). } . Mi történik. az operator new()-t hívja meg. Magunk is meghatározhatjuk.6. Az operator new() és az operator new [ ]() szabványos megvalósítása a visszaadott memóriát nem tölti fel kezdõértékkel. throw bad_alloc().4. Ha a new nem jár sikerrel. a kód végül meg fogja hívni a bad_alloc eseménykezelõjét. Memória-kimerülés A szabad tár new. az operator new[ ]() meghívására kerül sor. amelyet a <new> fejállományban bevezetett set_new_handler() függvénnyel elõzõleg beállítottunk (amennyiben ezt megtettük): void out_of_store() { cerr << "Az operator new nem járt sikerrel: nincs tárhely\n".) new char[10000]. void* operator new[ ](size_t). } Akármennyi memória áll a rendelkezésünkre.2. amikor kifogy a memória.172 Alapok 6. ha a new nem talál lefoglalható helyet? Alapértelmezés szerint a lefoglaló bad_alloc kivételt vált ki (a másik lehetõséget illetõen lásd §19. void operator delete(void*)..2. new[ ] és delete[ ] operátorai függvényekként vannak megvalósítva: void* operator new(size_t). elõször azt a függvényt hívja meg.

5. hogy új new_handler-t állítunk be. // eszköz a 0Xff00 címen // . ha szerez egy már megírt és ellenõrzött terméket (§C. } // out_of_store lesz a new_handler Ez azonban soha nem fog elérni addig. void f() { int* p = static_cast<int*>(malloc(100)). így elhagyhatóvá teheti a delete-et (bár ez kétségtelenül nem kezdõknek való feladat)...6) a new szabályos használata számára. Meghatározott típuskonverziók Néha „nyers memóriával” kell dolgoznunk. akinek automatikus szemétgyûjtõre van szüksége. mint hogy egyszerûen befejezi a programot.9. mint egy I/O eszköz címét: void* malloc(size_t). amely megvizsgálja. §19. melynek típusa ismeretlen a fordítóprogram számára. és ha nem talál ilyet.) new char[10000].4. hogy egy adott egész értéket úgy kell kezelni. azaz a tár olyan objektumot tartalmaz vagy fog tartalmazni.2. felvállaljuk.7. Majdnem mindenkinek.. vagy a felhasználó által adott további foglalási adatokra támaszkodunk (§10. a leghasznosabb.4. hogy a new visszatérhessen. hogy kiírja a „kész”-t.1). A memóriafoglalásnak kétféle útja létezik: vagy gondoskodunk nem szabványos lefoglaló és felszabadító függvényekrõl (§15. for (. vagyis a felhasználó gondoskodhat szemétgyûjtõrõl.4. létezik-e meghívható kezelõfüggvény. amikor a memóriafoglaló (allokátor) egy újonnan lefoglalt memóriaterületre hivatkozó void* típusú mutatót ad vissza. Egy new_handler azonban valami okosabbat is tehet. Ha tudjuk. mert saját operator new()-t és operator delete()-et írtunk –.6. 6. } . Ilyen eset. Ehelyett a következõt fogja kiírni: Az operator new nem járt sikerrel: nincs tárhely Lásd §14.11. hogy nekünk kell törõdnünk a memória kimerülésével kapcsolatos problémákkal a new minden használatakor. hogyan mûködik a new és a delete – például azért. cout << "kész\n". Kifejezések és utasítások 173 int main() { set_new_handler(out_of_store).5-öt az operator new() egy olyan lehetséges megvalósításáról.a kezelõfüggvény megpróbálhat valamennyi memóriát keresni. Azzal. vagy ha azt akarjuk kifejezni.). // a new által lefoglalt helyet int-ként használjuk IO_device* d1 = reinterpret_cast<IO_device*>(0Xff00). bad_alloc-ot vált ki.

és megkönnyíti.1) operátort szintén használhatjuk. de hagyományosan túl sokszor használják azokat. A static_cast operátor egymással kapcsolatban levõ típusok közötti konverziót végez. hogy a programozó megtalálja a veszélyesebb átalakításokat.4. esetleg egy mutatótípusról eltávolítja a const minõsítõt. Ez a C stílusú konverzió sokkal veszélyesebb.2. Néhány static_cast „hordozható”. hogy „hordozható” átalakítást végez egymással kapcsolatban levõ típusok között. Az explicit (pontosan meghatározott) típuskényszerítések (casting) néha szükségesek. Számos programban az ilyen típuskonverzió teljesen elkerülhetõ.7. de nem hordozhatót a nem rokon típusok között. az eredményt a reinterpret_cast-tal az eredeti típusra alakíthatjuk és használhatjuk azt. reinterpret_cast és a const_cast kombinációjaként. amelyet az érték meghatározására használtunk. A const minõsítõt eltávolító „konstanstalanító” const_cast (§15. vagy lebegõpontos típus és integrális típus közöttit.6. Ebben a könyvben explicit típuskényszerítést valósághû helyzetekben csak a §6.4. vajon a 0Xff00 érvényes cím-e. A futási idõben ellenõrzött konverziók egyik formája a dynamic_cast (§15. Ez a megkülönböztetés lehetõvé teszi. melyeket a reinterpret_cast jelöl. A reinterpret_cast eredményét csak akkor lehet biztosan felhasználni. például két. mint az eredeti érték. Azaz a (T)e lehet. például egészrõl mutatóra vagy mutatótípusról egy másik. §13. amelyek nincsenek kapcsolatban. A C++-ban az explicit típuskényszerítés a legtöbb esetben szükségtelen olyankor. A C++ a C-bõl örökölte a (T)e jelölést. máshol néhány eljárásra korlátozhatjuk a használatát.2.2. mint a paraméteréé.5. nem rokon mutatóra konvertál.3).4. A reinterpret_cast esetében nem sok dolog biztos. és §25. Következésképpen az átalakítások helyessége teljes mértékben a programozó kezében van.7. Ha a cél legalább annyi bites. hogy a fordítóprogram elvégezzen bizonyos minimális típusellenõrzést a static_cast esetében. vajon tényleg szükséges-e. hogy meggondoljuk. ahol a C++ korai változataiban szükséges volt (§1.1). ha annak típusa pontosan az a típus. ezt nem tudjuk eldönteni. és jelentõs hibaforrások. mert a jelölést nehezebben lehet észrevenni egy nagy programban és a programozó szándéka szerinti átalakítás fajtája nem nyilvánvaló. Ha nem tudjuk T és e pontos típusát. §B. Eredményül T típusú érték jön létre (§B.3). amely bármilyen átalakítást elvégez. szánjunk idõt arra.1 pontokban használunk.174 Alapok A fordítóprogram nem ismeri a void* által mutatott objektum típusát. Ha kísértést érzünk. §15.2.4. . integrális típus és felsoroló típus. A reinterpret_class olyan típusok átalakítását hajtja végre.6). és sok olyan esetben is. hogy pontosan meghatározott típuskényszerítést alkalmazzunk. mint a fent említettek. amit ki lehet fejezni a static_cast. ugyanazon osztályhierarchiában lévõ mutatótípus. §7. amelynek ugyanaz a bitmintája.2. Azt sem tudja. amikor a C-ben szükség lenne rá (§1. a reinterpret_cast-ok közül viszont csak kevés. általában új típust hoz létre.

Konstruktorok Egy T típusú érték létrehozása egy e értékbõl a T(e) függvényjelöléssel fejezhetõ ki: void f(double d) { int i = int(d). A char*(2) például formai hibának számít. A konstruktor jelölés használata beépített típusokra sablonok írásakor különösen fontos..8).9. Aritmetikai típusok esetében az értékek csonkulhatnak.5).2. az egészekrõl felsoroló típusra való átalakításoknál (§4. A jelölést megpróbálom kizárólag ott használni. A T alapértelmezett értékének kifejezésére a T() konstruktor jelölés használatos: void f(double d) { int j = int(). A mutató-konverziókat a T(e) jelölést használva nem fejezhetjük ki közvetlenül.4.9.4. azaz a szûkítõ aritmetikai átalakításoknál (§C. §10.6.5. // .6). §17. amit a konstruktor jelölés nyújt az ilyen veszélyes átalakítások ellen. ami azt vonja maga után. kikerülhetõ ha a mutatótípusokra typedef neveket (§4. hogy a sablon (template) paramétere beépített vagy felhasználói típusra vonatkozik-e majd (§16. amit a fordító az adott típusra konvertál (§4. ha létezik ilyen. . és a felhasználói típusok objektumainak létrehozásánál (§2. Kifejezések és utasítások 175 6.2. Ekkor a programozó nem tudhatja. hogy a T(e) használata nem mindig biztonságos. és még egy hosszabb egész típusról egy rövidebbre (például long-ról char-ra) való átalakítás is nem meghatározott viselkedést eredményezhet.7) használunk... complex z = complex().. Sajnos.3.2). complex z = complex(d).2. Ezért az int() egy másfajta módja a 0 írásának.3). A T felhasználói típusra T()-t az alapértelmezett konstruktor (§10. } // alapértelmezett int érték // alapértelmezett complex érték A beépített típusok konstruktorának értéke a 0.2) határozza meg. beépített T típusokra T(e) egyenértékû (T)e-vel.4.8. ahol az érték létrehozása pontosan meghatározott. Sajnos az a védelem.1. // . } // d csonkolása // complex létrehozása d-bõl A T(e) szerkezetet néha függvény stílusú konverziónak nevezik.

néhány példával: Az utasítások formai szabályai utasítás: deklaráció { utasítás_listanem kötelezõ } try { utasítás_listanem kötelezõ } kezelõ_lista kifnem kötelezõ . goto azonosító.kifnem kötelezõ ) utasítás case konstans_kif : utasítás default : utasítás break .3. for (kezdõérték_meghatározó feltételnem kötelezõ .176 Alapok 6. azonosító : utasítás utasítás_lista: utasítás utasítás_listanem kötelezõ feltétel: kif típusazonosító deklarátor = kif kezelõ_lista: catch (kif_deklaráció) { utasítás_listanem kötelezõ } kezelõ_lista kezelõ_listanem kötelezõ . Utasítások – áttekintés Íme a C++ utasítások összefoglalása. continue . return kifnem kötelezõ . if (feltétel) utasítás if (feltétel) utasítás else utasítás switch (feltétel) utasítás while (feltétel) utasítás do utasítás while (kif).

. hogy a deklaráció egy utasítás. string s = v[i]. mielõtt lenne egy olyan érték. alapvetõ fontosságú sok konstans esetében. mint a következõ: string s = "Voltaire". Kifejezések és utasítások 177 Jegyezzük meg. amíg egy megfelelõ kezdeti érték rendelkezésre nem áll jobb teljesítményhez is vezethet: string s. A fenti könnyen elõfordulhat. Hacsak egy változót static-ként nem adunk meg. illetve az olyan egyszeri értékadásos programozási stílusnál.. . } // . Deklarációk mint utasítások A deklaráció utasítás.4. Ritkán van ok új változó bevezetésére. if (s == p) { // .. Kezdeti érték nélkül általában akkor adunk meg egy változót. §6. ahol utasítást használhatunk (és még pár további helyen.1).. hogy lehetõvé tegyük a programozónak a kezdõérték nélküli változókból származó hibák csökkentését és a változók hatókörének lehetõ legnagyobb szûkítését a kódban.2.3. 6..1.3. hogy sokkal lassabb. int i.. A deklarációkat azért engedjük meg minden olyan helyen.". amikor a vezérlés áthalad a deklarációján (lásd még §10. értékadó és eljáráshívó utasítások pedig nincsenek: az értékadások és a függvényhívások kifejezések. Ilyenek például a bemeneti változók és a tömbök. */ s = "A legjobb a jó ellensége. Felhasználói típusoknál a változó meghatározásának elhalasztása addig. §6. const char* p) { if (p==0) return.1 pontban tárgyaljuk. hogy a deklarációkat végrehajtható kód után is elhelyezhetjük. ahol egy objektum értéke nem változik meg annak létrehozása és kezdeti értékadása után. A kivételek kezelésére vonatkozó utasításokat – a try blokkokat – a §8.3. /* .8).6.3.size()<=i) error("rossz index"). } A lehetõség.3. ha a változónak utasításra van szüksége a kezdeti értékadáshoz. minden esetben kezdõértéket fog kapni.1. amit a változónak tartalmaznia kell: void f(vector<string>& v. if (i<0 || v.

Ebbõl az következik. amelyrõl tudjuk. hogy a 0 mutatót nem minden gép ábrázolja „csupa nullával” (§5...178 Alapok 6.3. Például ha x egy egész. A p mutató esetében az alábbi egy közvetlen utasítás. . ugyanazt a kódot készítette mindkét vizsgálatra. hogy if (x != 0) // . a második utasításra ugrunk (ha megadtunk ilyet). hogy „p egy érvényes objektumra mutat”: if (p) // .. hogy nem mutat objektumra: if (p != 0) // . ami azt a vizsgálatot fejezi ki.1. amivel találkoztam. ha az összehasonlítás igaz.2. Kiválasztó utasítások Egy értéket az if vagy a switch utasítással vizsgálhatunk meg: if (feltétel) utasítás if (feltétel) utasítás else utasítás switch (feltétel) utasítás Az alábbi összehasonlító operátorok a logikai (bool) típusú true értéket adják vissza. hogy összehasonlítja egy olyan értékkel.. hogy bármilyen aritmetikai vagy mutató kifejezést lehet feltételként használni. Minden fordítóprogram.. és false-t. Jegyezzük meg... úgy. A következõ közvetett módon ugyanezt a kérdést fogalmazza. azt jelenti.1). ha a kifejezés nem nulla. ha hamis: == != < <= > >= Az if utasításban az elsõ (és egyetlen) utasítás akkor hajtódik végre. akkor if (x) // .. Ha nulla.

case 2: g(). A feltétel körül lévõ zárójelek nem szükségesek. Az && és a || mûveletek nem értékelik ki a második paramétert. és csak akkor nézi meg. ha p nem nulla. A fenti utasítás például elõször megvizsgálja. Például az if (a <= b) max = b. break. A switch utasítás if utasítások sorozataként is leírható. én azonban úgy gondolom. } . Például a switch (val) { case 1: f(). Kifejezések és utasítások 179 A && || ! logikai operátorok leggyakrabban feltételekben használatosak. hogy l<p->count teljesül-e. Néhány if utasítást kényelmesen feltételes kifejezésekre cserélhetünk. default: h(). csak ha szükség van rá: if (p && 1<p->count) // . jobban kifejezhetõ így: max = (a<=b) ? b : a. break. hogy p nem nulla-e. else max = a. break.6.. a kód könnyebben olvasható lesz tõlük..

default: cout << "Alapértelmezés: nincs ilyen eset\n". A switch utasítás olvashatóbb olyan példáknál. ha megjegyzésekkel látjuk el azon (ritka) eseteket. Vigyázzunk arra.eset\n". Vegyük a következõt: switch (val) { // vigyázat! case 1: cout << "1. else h().1. és jobb kódot is hozhatunk létre vele. } Ha val==1-gyel hívjuk meg. amikor a case-ek közötti továbblépés szándékos. A jelentés ugyanaz. mert a mûvelet természete (egy értéket állandók halmazával hasonlítunk össze) így világosabb.180 Alapok így is kifejezhetõ: if (val == 1) f(). . hogy a switch case-ét mindig fejezzük be valahogy. A case befejezésének leggyakoribb módja a break használata. a következõket írja ki: 1.1). eset Alapértelmezés: nincs ilyen eset Ez az avatatlanokat nagy meglepetésként érheti. így egy nem magyarázott továbblépésrõl feltételezhetjük. Jó ötlet. de a return is hasznos lehet (§6. hogy programhiba. case 2: cout << "2. else if (val == 2) g(). hacsak nem akarjuk a végrehajtást a következõ case-nél folytatni. eset 2. eset\n". amelyek nem maguktól értetõdõek. de az elsõ (switch) változatot részesítjük elõnyben.

Nevezetesen. Deklarációk feltételekben A véletlen hibás mûködés elkerülésére általában jó ötlet a változókat a legkisebb lehetséges hatókörben bevezetni. mielõtt kezdeti értékét beállítottuk volna. ha a d-t a feltétel elõtt vezetjük be.. Kifejezések és utasítások 181 6. // d két. } // . Ha volna egy else ág az if utasításban.. . // hoppá! // . Így viszont nagyobb lesz a d használatának hatóköre. A másik hagyományos és kézenfekvõ megoldás. Az említett két elv egyik legelegánsabb felhasználása. egymástól független használata A változók feltételekben történõ megadásának nemcsak logikai haszna van. amit a feltétel vezérel. kiterjedhet a kezdeti értékadás elé vagy a d szándékolt hasznos élettartama után is: double d. A feltételben lévõ deklarációnak egyetlen változót vagy konstanst kell megadnia és feltöltenie kezdõértékkel.0. d = 2.2. break. // . d2 = d. a d hatóköre mindkét ágra kiterjedne. amit a feltétel értékével hasonlítunk össze. amíg kezdeti értéket nem tudunk adni neki. Így nem kerülhetünk olyan helyzetbe. break. Vegyük a következõ példát: if (double d = prim(true)) { left /= d. A d hatóköre a deklaráció pontjától annak az utasításnak a végéig terjed. hogy a változót még azelõtt használjuk. if (d = prim(true)) { left /= d... ha a változót egy feltételben adjuk meg.6.1.3. rendszerint legjobb elhalasztani egy ideális változó bevezetését addig... } Itt d deklarált és kezdõértéket is kap. tömörebb forráskódot is eredményez.

valamilyen feltételnek már az elsõ alkalommal is teljesülnie kell. általában jobb. ami nagyon megnövelheti az olvashatóságot és ezzel csökkentheti a hibák gyakoriságát. amíg feltétele hamissá nem válik. de a for is segíthet olyan ciklusok írásánál. Akkor hajlok arra. Ha a kifejezést elhagyjuk.4.. return... } // "örökké" (végtelen ciklus) A while utasítás egyszerûen végrehajtja a ciklusmagot..) { // . A ciklusváltozót. vagy throw utasítással. a ciklusfeltételt. A vártnál sokkal gyakrabban vettem észre azt..3. módosítjuk a ciklusváltozót” fajtából való. Ennek az az oka. és a ciklusváltozót módosító kifejezést egyetlen sorban írhatjuk le. hogy a ciklusmag mindig végrehajtódik egyszer. amikor nincs magától értetõdõ ciklusváltozó vagy amikor a ciklusváltozó módosítása természetes módon a ciklusmag közepén történik. kifejezésnem kötelezõ ) utasítás Ezen utasítások mindegyike ismételten végrehajt egy utasítást (amit vezérelt (controlled) utasításnak vagy ciklusmagnak nevezünk). például az exit() (§9. Ahhoz azonban. amelyben nincs magától értetõdõ ciklusváltozó: while(cin>>ch) // .1) meghívásával. hogy a ciklusmag megfelelõen mûködjön. hogy a while-t részesítsem elõnyben a for-ral szemben. goto. amíg a feltétel hamissá nem válik vagy a programozó más módon ki nem lép a ciklusból. a for utasítás örökké a ciklusban marad. hacsak a felhasználó kifejezetten kilépésre nem kényszeríti egy break. melyeknek nincs meghatározott leállási feltétele: for(. Ha a feltételt elhagyjuk. for ( kezdõérték_meghatározó feltételnem kötelezõ . A for utasítás szabályos ciklusok kifejezésére való. A bemeneti ciklus egy olyan ciklusra példa.182 Alapok 6. a kezdõérték_meghatározó (inicializáló) utasítás üres is lehet. a ciklusmagban kell módosítanunk egy ciklusváltozót. Ha nem szükséges kezdeti értékadás. while vagy do utasítással fejezhetjük ki: while ( feltétel ) utasítás do utasítás while ( kifejezés ) . hogy egy felté- . Ciklusutasítások A ciklusokat for. megvizsgáljuk a feltételt.1. Ha a ciklus nem az egyszerû „bevezetünk egy ciklusváltozót. vagy valami kevésbé egyszerû módon. ha while utasítással fejezzük ki.3. Tapasztalatom szerint a do utasítás könnyen hibák és tévedések forrása lehet. mielõtt a feltétel kiértékelõdik.

6.3. a ciklusváltozót a cikluson kívül kell megadni (pl. i<max. amikor a C++ kódot program és nem közvetlenül egy személy készíti.3. melyet egy kódkészítõ program (kódgenerátor) hozott létre valamilyen nyelvtan alapján. akkor az általa bevezetett változó (vagy változók) hatóköre a for utasítás végéig terjed: void f(int v[ ]. ahogy az elvárható lett volna. Következésképpen én magam próbálom elkerülni a do utasításokat. } Ha az index végsõ értékét tudni kell a for ciklusból való kilépés után. Deklarációk a for utasításban Változókat a for utasítás kezdõérték-adó részében adhatunk meg. i++) v[i] = i*i.3.).4. Ezenkívül jobban szeretem a feltételt „elöl. Ha ez deklaráció. ahol jól láthatom”.3. hogy kilépünk egy beágyazott ciklusból vagy switch utasításból (a break csak a legbelsõ ciklusból vagy switch utasításból lép ki): void f() { int i. §6. például valamilyen valós idejû alkalmazás belsõ ciklusában. amikor a kódot módosították.4. A goto akkor is hasznos lehet. vagy késõbb.1. int j. Kifejezések és utasítások 183 tel nem úgy teljesült. Goto A C++-ban megtalálható a hírhedt goto : goto azonosító . ha a hatékonyság alapvetõ követelmény. 6. vagy amikor a programot elõször megírták és tesztelték. int max) { for (int i = 0. A goto kevés értelmes használatának egyike a mindennapi kódban az. azonosító : utasítás A goto az általános magasszintû programozásban kevés dologra használható. használhatjuk például olyan elemzõben. de nagyon hasznos lehet. 6. .

hogy biztosítsa azt. 6. ne megjegyzésben említsük meg. A fordítóprogram nem érti a megjegyzések tartalmát.4. hogy egyiket a másikkal szemben elõnyben részesítsük (bár a legtöbb programozóhoz hasonlóan nekem is van választott stílusom – a könyv nyilván tükrözi is azt). Ez az észrevétel az ilyenfajta megjegyzésekre vonatkozik: // a "v" váltózónak kezdõértéket kell adni // a "v" változót csak az "f()" függvény használhatja // az "init()" függvényt minden más függvény elõtt meg kell hívni ebben a fájlban . ami így nagymértékben rontja a program olvashatóságát. melyek érthetetlenek. ezért nincs mód arra. Ha valamit leírhatunk magával a programnyelvvel.. Számos behúzási stílus használatos és nem látok alapvetõ okot arra. [2] a programmal összhangban álló és [3] idõszerû legyen.1. // nem található // . hogy egy megjegyzés [1] értelmes. j++) if (nm[i][j] == a) goto found. Ugyanez vonatkozik a megjegyzések stílusára is. félreérthetõek. i<n.184 Alapok for (i = 0. Megjegyzések és behúzás A program olvasását és megértését sokkal kellemesebbé teheti.5-ben foglalkoztunk. akkor tegyük azt. Számos program olyan megjegyzéseket tartalmaz. ha okosan használjuk a megjegyzéseket és a behúzást. A megjegyzéseket számos módon lehet rosszul használni. vagy egyszerûen hibásak. i++) for (j = 0.. A rossz megjegyzések rosszabbak. found: // nm[i][j] == a } A ciklus végére ugró continue utasítás mûködésével a §6. mint ha egyáltalán nem használnánk megjegyzést. j<m.

2. hogy mit feltételez környezetérõl. Minden globális és névtér-változóhoz. mi a közös a fájlban levõ deklarációkban. A fentieket például kiválthatjuk. Mihelyt valamit világosan leírtunk a nyelvvel. másodszor már nem kell megemlítenünk egy megjegyzésben: a = b+c. mert növelik az elolvasandó szöveg hosszát. . hogy az ilyen megjegyzések széleskörûen használatosak tanítási célokra az olyan programozási nyelvekrõl szóló könyvekben. Ez az egyik. Minden osztályhoz. mint amilyen ez is. Minden forrásfájlban van megjegyzés. 4. // növeljük a számlálót Az ilyen megjegyzések még az egyszerûen feleslegeseknél is rosszabbak. és esetleg azt. gyakran összezavarják a program szerkezetét. általános ötleteket ad a kód módosításával kapcsolatban stb. utal a segédanyagokra. és lehet. Minden nem magától értetõdõ függvényhez van olyan megjegyzés. Van néhány megjegyzés ott. A fentieken kívül kevés megjegyzés van. amely leírja.4. Én a következõket szeretem: 1.6. 3. amiben egy könyvben lévõ program különbözik egy igazi programtól. 6. kezdõérték-adási és felszámolási szabályokat (§10. ha alkalmazzuk az összeszerkesztési (§9. sablonhoz és névtérhez tartozik megjegyzés. ahol a kód nem nyilvánvaló és/vagy más rendszerre nem átültethetõ. a felhasznált algoritmust (ha az nem nyilvánvaló). // a-ból b+c lesz count++. illetve konstanshoz van megjegyzés. 5. amely leírja a függvény célját. Kifejezések és utasítások 185 // a "cleanup()" függvényt meg kell hívni a program végén // a "weird()" függvényt ne használjuk // az "f()" függvénynek két paramétert kell adni A C++ megfelelõ használata az ilyen megjegyzéseket általában szükségtelenné teszi.2) vagy az osztályokra vonatkozó láthatósági. Meg kell azonban jegyeznünk.1). hogy hibásak.

5.1. §6. §6." pg 411. részesítsük elõnyben a jobban definiált konverziós operátorokat a C stílusú átalakítással szemben.2. Olyan mûvészet.7. 6. Tanácsok [1] Részesítsük elõnyben a standard könyvtárat a többi könyvtárral és a „kézzel írt kóddal” szemben. zárójelezzünk. [2] Kerüljük a bonyolult kifejezéseket.7. Jó megjegyzéseket írni legalább olyan nehéz. Inc. hogy ha kizárólag a // megjegyzéseket használjuk egy függvényben.8. §6. §6. melyek kiértékelési sorrendje nem meghatáro- . akkor ennek a függvénynek bármely részét megjegyzésbe tehetjük a /* */ jelöléssel (ez fordítva is igaz). mint megírni magát a programot.2. §6.8.186 Alapok Például: // tbl.2. melyet érdemes mûvelni.3. §6. [3] Ha kétségeink vannak az operátorok precedenciájával kapcsolatban.c: Implementation of the symbol table. [4] Kerüljük a típuskényszerítést (cast). [5] Ha explicit típuskonverzió szükséges.2.2. See Ralston: "A first course . /******************************* Copyright (c) 1997 AT&T. [7] Kerüljük az olyan kifejezéseket. /* */ Gaussian elimination with partial pivoting.3. [6] Kizárólag jól meghatározott szerkezeteknél használjuk a T(e) jelölést. All rights reserved *******************************/ A jól megválasztott és jól megírt megjegyzések alapvetõ részét képezik a jó programnak... // swap() assumes the stack layout of an SGI R6000. Jegyezzük meg azt is.

[13] A globális operator new() helyettesítésére adjunk meg inkább egy operator new() tagot (§15.5 a = b == c ++ a=b=c=0 a[4][2] *= * b ? c : * d * 2 a-b.1. (*1) Írjuk meg a következõ for utasítással egyenértékû while utasítást: for (i=0.2. [14] Bemenet beolvasásakor mindig vegyük számításba a rosszul megadott bemenetet is.3. i++) if (input_line[i] == '?') quest_count++. i<max_length.6.3.2.6.4. azaz úgy.1 + + b -. Tipp: §6. §6.3. (*1) Írjuk meg a bitenkénti logikai operátorok (§6. Számítsuk ki és írjuk ki minden névre az értékek összegét és számtani közepét.1. §6.3. §6.8. 2.2. amíg nincs érték. Gyakorlatok 1..2.és érték-) párok sorozatát. [9] Kerüljük a do utasítást. (*1) Zárójelezzük teljesen a következõ kifejezéseket: a = b + c * d << 2 & 8 a & 077 != 3 a == b || a == c && c < 5 c = x != 0 0 <= i < 7 f(1. [8] Kerüljük a goto-t.3.4.4.1. ahol a név egyetlen üreshellyel elválasztott szó.2)+3 a = . hogy ciklusváltozóként mutatót használunk.2.1. [12] Tartsunk fenn következetes stílust.4) értéktáblázatát a 0 és 1 . §6.2. §6. (*2) Olvassuk be üreshellyel elválasztott (név. Ezt írjuk át úgy. hogy a vizsgálat *p=='?' alakú legyen.6). 6. [10] Ne adjunk meg változót addig. §6. [11] A megjegyzéseket frissítsük rendszeresen.1. 4.c=d 3. §6.3. valamint az összes névre vonatkozó összeget és számtani közepet. amivel feltölthetnénk. §6.3. az érték pedig egy egész vagy lebegõpontos érték.3. §6.6. §6. Kifejezések és utasítások 187 zott.

ami egy C stílusú karakterlánc hosszát adja vissza. melynek jelentése nem meghatározott (§C. ahogy azok a <cstring>-ben (<string. 7. int b) { if (a = 3) // . Hajtsuk õket végre és nézzük meg.. ami két karakterláncot hasonlít össze. hogyan reagál a fordítóprogram. (*1) Nézzük meg. (*1. melynek jelentése a nyelvi megvalósítástól függ (§C. (*2) Írjunk 5 kifejezést. ha nullával osztunk? Mi történik túlcsordulás és alulcsordulás esetén? 9.m *a[i] 10. 14. (*2) Módosítsuk úgy a §6.5) Találjunk 5 különbözõ C++ szerkezetet. ami egy karakterláncot másol egy másikba. amelynek egy karakterlánc paramétere van és . és strcmp()..2).2).4. amelynek két C stílusú karakterlánc paramétere van és egy olyan karakterláncot ad vissza. (*1) Zárójelezzük teljesen a következõ kifejezéseket: *p++ *--p ++a-(int*)p->m *p. Az eredményeknek foglaljunk helyet a new-val. 13. 5. hogy a középsõ értéket (medián) is kiszámítsa.h>-ban) szerepelnek és ahogy a §20.) Találjunk 5 különbözõ C++ szerkezetet. (*1.5) Mi történik a rendszerünkben. (*2) Írjuk meg a következõ függvényeket: strlen().188 Alapok operandusok összes lehetséges párosítására. mi legyen a paraméterek és a visszatérési érték típusa. } „Készítsünk” több egyszerû hibát és nézzük meg. Gondoljuk meg. strcpy(). 6. (*1) Adjunk 10 különbözõ példát nem hordozható C++ kódra.. Ezután hasonlítsuk össze a függvényeket a standard könyvtárban lévõ változatokkal. (*2) Írjuk meg a cat() függvényt. 11..6[3] programot. 12. (*1. hogyan reagál a fordítóprogramunk ezekre a hibákra: void f(int a. 8. melyek kiértékelési sorrendje nem meghatározott. amely a paraméterek összefûzésébõl áll elõ.5. (*2) Írjuk meg a rev() függvényt. a := b+1. mit csinál velük egy – de lehetõleg több – C++változat. if (a&077 == 0) // .1 pontban leírtuk azokat.

Azaz a rev(p) lefutása után p utolsó karaktere az elsõ lesz és így tovább. mintha a felhasználó gépelte volna be azokat. (*2) Írjuk meg az atoi(const char*) függvényt. hogy a felhasználó függvényeket adhasson meg a számológéphez.5) Mit csinál a következõ példa és miért írna valaki ilyesmit? void send(int* to. 15. case 4: *to++ = *from++. hogy egy felhasználói függvénynek paraméterei legyenek. 22. A legtöbbet a „kis buta hibák” kijavításából fogunk tanulni. Az ilyen sorozatokat karakterláncként vagy szimbólumok (tokenek) listájaként tárolhatjuk. Ha azt akarjuk. Kifejezések és utasítások 189 megfordítja a benne lévõ karaktereket. int count) // Duff programja. Módosítsuk az atoi()-t úgy.6. (*2) Gépeljük be teljes egészében a számológép példát és hozzuk mûködésbe. hogy a symbol szerkezetet használja és ne a statikus number_value és string_value változókat.5) Alakítsuk át a számológépet. (*2) Írjunk egy olyan itoa(int i. case 5: *to++ = *from++. tízes számrendszerbeli számokkal együtt. amely egy számokat tartalmazó karakterláncot kap és visszaadja a megfelelõ egészet. amely kiveszi a megjegyzéseket a C++ progra- . case 7: *to++ = *from++. case 1: *to++ = *from++. 21. (*1. amikor a függvény meghívódik. { int n = (count+7)/8. case 3: *to++ = *from++. Tipp: adjunk meg egy függvényt mûveletek sorozataként. (*1. hogy már begépelt szövegrészeket használunk. } } 16. } while (--n>0). Például atoi("123") 123 lesz. int* from. 17. 19. úgy. az egyszerû.5) Írjunk olyan programot. char b[ ]) függvényt. (*3) Tegyük lehetõvé. case 6: *to++ = *from++. A megjegyzéseket szándékosan töröltem. Ezután olvassuk be és hajtsuk végre ezeket a mûveleteket. Ne takarítsunk meg idõt azzal. hogy kezelje a C++ karakterkonstans jelölést is. case 2: *to++ = *from++. 18. amely létrehozza b-ben i karakterlánc ábrázolását és visszaadja b-t. 20. hogy kezelje a C++ oktális és hexadecimális jelölését is. hogy kiírja a hibák sorának számát is. (*2. Módosítsuk a függvényt úgy is. (*2) Módosítsuk a számológépet. switch (count%8) { case 0: do { *to++ = *from++. akkor arra külön jelölést kell kitalálnunk.

hogy a kimenet szép legyen (az egy másik. Ne törõdjünk a hibás programokkal. hogy elképzelésünk lehessen a mostanság használatos stílusok (behúzások. Ne törõdjünk azzal. /* és */ használatától a megjegyzésekben. Azaz. elnevezések és megjegyzések) változatosságáról. majd írjuk ki az eredményt a cout-ra. 23. Óvakodjunk a //. olvassunk a cin-rõl. mind a /* */ megjegyzéseket. (*2) Nézzünk meg néhány programot. .190 Alapok mokból. távolítsuk el mind a //. karakterláncokban és karakterkonstansokban. sokkal nehezebb feladat lenne).

void exit(int). hogy meghívunk rá egy függvényt. hogy elõzetesen nem deklaráltuk. Peter Deutsch) Függvénydeklarációk és -definíciók • Paraméterátadás • Visszatérési értékek • Függvénytúlterhelés • A többértelmûség feloldása • Alapértelmezett paraméterek • stdargs • Függvényekre hivatkozó mutatók • Makrók • Tanácsok • Gyakorlatok 7.7 Függvények „Ismételni emberi dolog. . Rekurziót írni isteni.1. Függvénydeklarációk A C++-ban általában úgy végzünk el valamit. A függvényt azonban nem hívhatjuk meg úgy. A függvény deklarációja megadja a függvény nevét. és a függvény definiciójával írjuk le.” (L. amelyeket át kell adni a függvény meghívásakor: Elem* next_elem(). const char* from). char* strcpy(char* to. és azon paraméterek számát és típusát. visszatérési értékének típusát (ha van ilyen). hogyan kell azt elvégezni.

amit meghívunk a programban. *p = *q. const char*) { // a harmadik paraméter nem használatos } . int*). amely nem használja fel az összes paramétert: void search(table* t. Nem ritka az olyan függvénydefiníció. ha szükséges: double sqrt(double). void swap(int* p. melyek segítik a program olvasóját. 7. a void visszatérési típus azt jelenti. // sqrt() meghívása double(2) paraméterrel // hiba: sqrt() double típusú paramétert igényel Az ilyen ellenõrzés és konverzió jelentõségét nem szabad alábecsülni. double sr2 = sqrt(2). A fordítóprogram ezeket egyszerûen nem veszi figyelembe. const char* key. int* q) { int t = *p. amelyben megadjuk a függvény törzsét: extern void swap(int*. *q = t.1. A paraméterek nevei azonban nem részei a típusnak. A fordítóprogram ellenõrzi a paraméterek típusát és automatikus típuskonverziót végez. A függvénydeklaráció paraméterneveket is tartalmazhat.192 Alapok A paraméterátadás ugyanazt a szerepet tölti be. hogy a függvény nem ad vissza értéket. Amint §4. double sq3 = sqrt("three"). mint a kezdeti értékadás. valahol (de csak egyszer) meg kell határoznunk. Függvénydefiníciók Minden függvényt.1.7-ben említettük. így nem kell azonosaknak lenniük. A függvénydefiníció (függvény-meghatározás) olyan deklaráció. } // deklaráció // definíció A függvények definícióinak és deklarációinak ugyanazt a típust kell meghatározniuk.

a hívás sorában történõ létrehozást akkor biztosíthatjuk. A változó csak egyszer kap értéket. de a helyükön hagyjuk a paramétereket.1. hogy a fac() meghívásánál próbálja meg annak kódját a hívás sorában létrehozni. a másik 6*fac(5)-öt. A névtelen paraméterek jellemzõen a program egyszerûsítése folytán vagy annak késõbbi bõvíthetõségét biztosítandó kerülnek a kódba. hogy egy paramétert nem használunk fel. ahelyett. Nevezetesen az ilyen függvényeknek – és static változóiknak (§7. állandó címû objektum jelöli majd. illetve a bemenettõl függõen magukat újrahívó vagy nem újrahívó helyben kifejtett függvények lehetõsége miatt nem biztos. az egyes függvényhívásoknak pedig saját másolatuk van a változóról. biztosítjuk.2). így elõfordulhat. 7.7. Ha egy lokális változót static-ként vezetünk be.és szerkesztõ-programmal. akkor azt a függvény minden meghívásakor egyetlen. Mindkét esetben azzal.2. azt a tényt.) is – ugyanúgy egyedi címük van. úgy jelölhetjük. ha a függvény kifejtése – és nem csak deklarációja – is a hatókörben szerepel (§9. majd a szokásos függvényhívó eljárás szerint hívná meg.2. hogy nem nevezzük meg a paramétert. Az egymást kölcsönösen meghívó (kölcsönösen rekurzív) helyben kifejtett függvények. hogy a függvényt meghívókat nem érintik a módosítások. Az inline kulcsszó nem befolyásolja a függvény értelmezését. Statikus változók A lokális (helyi) változók akkor kapnak kezdõértéket. Ha nem rendelkezünk kivételesen intelligens fordító. } Az inline kulcsszó javaslat a fordítóprogram számára. Alapértelmezés szerint ez a függvény minden meghívásakor megtörténik. hogy az egyik 720-at. A függvényeket a fordító által a hívás sorában kifejtendõként (inline-ként) is megadhatjuk: inline int fac(int n) { return (n<2) ? 1 : n*fac(n-1). hogy bár nem használjuk fel. Egy okos fordítóprogram a fac(6) meghívásakor létre tudja hozni a720 konstanst.1. Függvények 193 Amint látjuk. amikor a végrehajtás elér a definiciójukhoz. hogy elõbb létrehozná azt. A fordítóprogramok intelligenciájának mértéke nem írható elõ. a harmadik pedig a fac(6) nem helyben kifejtett hívást hozza létre. amikor a végrehajtás eléri annak elsõ definícióját: . hogy egy inline függvény minden hívása a hívás sorában jön létre.

2. amelyet más függvények is elérhetnek és módosítással használhatatlanná tehetnek (lásd még §10. és végrehajt minden szabványos és felhasználói típuskonverziót. ref++. A fordítóprogram ellenõrzi.2. } .6) és alapértelmezett paraméterek (§7. x == 0 A statikus változók anélkül biztosítanak „emlékezetet” a függvénynek. hogy globális változót vezetnének be.1). // 'a' alkalommal kap kezdõértéket (az f() minden meghívásakor) } cout << "n == " << n++ << ". x == " << x++ << '\n'. x == 0 n == 2.194 Alapok void f(int a) { while (a--) { static int n = 0.2. x == 0 n == 1. a fordítóprogram a formális paraméterek számára tárterületet foglal le.4). de van lehetõség nem ellenõrzött (§7. Paraméterátadás Amikor egy függvény meghívódik. hogy az aktuális paraméterek típusa megegyezik-e a formális paraméterek típusával. A paraméterátadás szerepe azonos a kezdeti értékátadáséval.5) átadására is. // egyszer kap kezdõértéket int x = 0. az egyes formális paraméterek pedig a megfelelõ valódi (aktuális) paraméter-értékkel töltõdnek fel. Vegyük a következõ példát: void f(int val. A tömbök átadására egyedi szabályok vonatkoznak (§7. 7. int& ref) { val++. } int main() { f(3). } A fenti a következõket írja ki: n == 0.

a második (j) referencia szerint. . const char* from). de i-t nem. Amint §5. Függvények 195 Amikor f() meghívódik. char* strcpy(char* to. és néhány felhasználói típusú paraméter esetében lényeges (§10. hogy jelezzük. hogy a hívott függvény módosíthassa az objektum értékét: void f(const Large& arg) { // "arg" értéke nem módosítható. a const-ként megadott mutató paraméter azt jelzi az olvasónak. nem pedig érték szerint adunk át.4. int strcmp(const char*. a referencia-paraméterek. hogy szándékunkban áll a változót módosítani: void g(Large& arg). hogy g() módosítja arg-ot Hasonlóan. nehezen olvashatóvá teszik a programot és általában kerülendõk (ellenben lásd §21. } j-t fogja növelni.1). Az elsõ paraméter (i) érték szerint adódik át. val++ az elsõ aktuális paraméter helyi másolatát növeli. int j = 1. const char*). Észrevehetõen hatékonyabb lehet.2-et). // tételezzük fel.7. Ebben az esetben a paramétert megadhatjuk const-ként.j).3. ref++ pedig a második aktuális paramétert.5-ben említettük. csak hatékonysági okokból használunk referenciát és nem szeretnénk lehetõvé tenni. azok a függvények. f(i. csak explicit típuskonverzióval } A referencia-paraméter deklarációjában a const elhagyása azt fejezi ki. ha egy nagy objektumot referencia. Jegyezzük meg. hogy a paraméter által mutatott objektum értékét a függvény nem változtatja meg: int strlen(const char*). hogy a paraméterátadás szerepe különbözik a (nem kezdeti) értékadásétól. Ez a const paraméterek. // karakterek száma egy C stílusú // karakterláncban // C stílusú karakterlánc másolása // C stílusú karakterláncok összehasonlítása A const paraméterek használatának fontossága a program méretével együtt nõ.4. Az alábbi void g() { int i = 1. melyek módosítják a referencia szerint átadott paramétereiket.

0f). de nem const&-ként nem. hogy az értéket ideiglenes változóban adjuk át (amennyiben ez szükséges): float fsqrt(const float&). az update() csendben módosította volna azokat az ideiglenes változókat. 7. hogy egy ilyen paraméternek pontosan ugyanazokat az értékeket lehet adni. az ideiglenes változók bevezetésébõl adódó buta hibák elkerülhetõk: void update(float& i). void g(double d) { float r = fsqrt(2. a tömb elsõ elemére hivatkozó mutató adódik át: int strlen(const char*). float r) { update(2. void f() { char v[ ] = "egy tömb". amely átalakítást igényel. r = fsqrt(d). int j = strlen("Nicholas"). át lehet adni const& paraméterként. r = fsqrt(r).1. amelyek azonnal törlõdtek. állandót és olyan paramétert.0f). } // Fortran stílusú sqrt referencia-paraméterrel // a 2. // hiba: konstans paraméter update(r). Ez rendszerint kellemetlen meglepetésként érné a programozót. biztosított. int i = strlen(v). Tömb paraméterek Ha függvényparaméterként tömböt használunk.2. azáltal. Mivel a const T& paraméterek konverziója megengedett. } . // hiba: típuskonverzió szükséges } Ha ezek a hívások szabályosak lennének.0f-et tartalmazó ideiglenes változóra hivatkozó // referencia átadása // r-re hivatkozó referencia átadása // a float(d)-t tartalmazó ideiglenes változóra hivatkozó referencia // átadása Mivel a nem const referencia típusú paraméterek konverziója nem megengedett. // r-re hivatkozó referencia átadása update(d). mint egy T típusú értéknek.196 Alapok Literált. void g(double d.

int size.7). hogy a problémát megkerüljük. "vasárnap" }. hogy nem érték szerint adódnak át (ez nem is lehetséges). a tömb paraméter is módosul. vagyis a tömbök abban különböznek a többi típustól. Ez bosszantó lehet. Visszatérési érték A main() kivételével(§3. void compute2(const Vec& v). de több mód van rá. 7. de helyettük gyakran használhatunk mutatókból álló tömböket. }. Ebbõl az következik. alacsonyszintû tömbök és mutatók helyett is használhatók.2) minden nem void-ként megadott függvénynek értéket kell visszaadnia. "kedd". hogy tömbök helyett olyan típusokat használunk. "csütörtök". melyek nem igényelnek különleges bánásmódot: char* day[ ] = { "hétfõ".1. hogy ha egy paraméterként alkalmazott tömb egy eleméhez értéket rendelünk. Függvények 197 Azaz egy T[ ] típusú paraméter T* típusúvá lesz alakítva.7. struct Vec { int* ptr. // másik módszer // egyik módszer Választhatjuk azt is.7. A vector és a hozzá hasonló típusok a beépített.3). a void függvények nem adhatnak vissza értéket: int f1() { } void f2() { } // hiba: nincs visszatérési érték // rendben .3. int vec_size). mint a vector (§3. Megfordítva. ha paraméterként adódik át. A C stílusú karakterláncok nulla végzõdésûek. így méretük könnyen kiszámítható. "szombat". "péntek". A többdimenziós tömbök némileg bonyolultabbak (lásd §C. "szerda". Más tömböknél a méretet egy további paraméterrel adhatjuk meg: void compute1(int* vec_ptr. §16. A tömb mérete nem hozzáférhetõ a hívott függvény számára.

/* ... } // 1 automatikusan double(1)-gyé alakul Minden egyes alkalommal. Egy függvényben több return utasítás is lehet: int fac2(int n) { if (n > 1) return n*fac2(n-1). mert a mutatott hely tartalma kiszámíthatatlan módon megváltozhat: int* fp() { int local = 1. A tár a függvény visszatérése után ismét felhasználásra kerül. név nélküli változónak ad kezdõértéket. */ return local. A fordítóprogram összehasonlítja a return kifejezés típusát a visszatérési típussal és minden szabványos és felhasználói típusátalakítást végrehajt: double f() { return 1.. } // rossz Ez a hiba kevésbé gyakori.. ezért lokális változóra hivatkozó mutatót soha nem szabad visszaadni. } int f5() { return. } // rossz Szerencsére a fordítóprogram általában figyelmeztet. } Az önmagukat meghívó függvényeket rekurzív (újrahívó) függvényeknek nevezzük. hogy az egy visszatérési típusú. amikor egy függvény meghívódik. */ return &local. return 1. A return utasítást úgy tekintjük. } void f4() { return 1. paramétereinek és lokális (automatikus) változóinak egy új másolata jön létre. } void f6() { return. mint referenciát használó megfelelõje: int& fr() { int local = 1. hogy lokális változóra vonatkozó hivatkozást adtunk vissza. } // rendben // hiba: visszatérési érték void függvényben // hiba: visszatérési érték hiányzik // rendben A visszatérési értéket a return utasítás határozza meg: int fac(int n) { return (n>1) ? n*fac(n-1) : 1. } A paraméterátadáshoz hasonlóan a függvényérték visszaadásának szerepe is azonos a kezdeti értékadáséval. /* .198 Alapok int f3() { return 1. .

az mégis használható egész. */ return g(p). print. de a nyelv nem korlátozza és nem is segíti a programozót. a fordítóprogramnak ki kell találnia. csak egyetlen „név” van az összeadásra (+).2). fejezet) esetében. a konstruktorok (§11. majd azt a függvényt hívja meg. A gondolatot a programozó által készített függvényekre is kiterjeszthetjük: void print(int). de meghívásuk nem is eredményez ilyet.4. valamint az általánosított (generikus) programozás (§2. ahol a visszatérési típus egy sablonparaméter (lásd §18. de amikor egyes függvények lényegében ugyanazt a mûveletet végzik különbözõ típusú objektumokon. hogy minden egyes f nevû függvény formális paramétereinek típusát összehasonítja az aktuális paraméterek típusával. void h(int* p) { /* . ha nincs jól illeszkedõ függvény: .2. Ez a módszer a C++ alapmûveleteinél is használatos.4. Amikor a név jelentése fontos. Ezért a túlterhelt függvénynevek elsõdlegesen jelölésbeli kényelmet adnak. Amikor egy f függvény meghívódik. // egész kiírása // C stílusú karakterlánc kiírása Ami a fordítóprogramot illeti. Ez történik például a +. az egyetlen. ami közös az azonos nevû függvényekben. 18. * és << operátorok. mint az sqrt. Azt. Azaz. } // rendben: üres visszatérési érték Ez a fajta visszatérés olyan sablon (template) függvények írásánál fontos. 7. lebegõpontos. és open. Függvények 199 A void függvények nem adhatnak vissza értéket. void print(const char*). következésképpen egy void függvény return utasításában szereplõ kifejezésként használhatunk void függvényt: void g(int* p). a név. Ezt úgy teszi. és fordítási idejû hibát ad.4.7.. Túlterhelt függvénynevek A legtöbb esetben jó ötlet különbözõ függvényeknek különbözõ neveket adni. ez a kényelem alapvetõvé válik. melyik f nevû függvényt hívja meg.. hogy különbözõ típusokra vonatkozó mûveletekre ugyanazt a nevet használjuk. túlterhelésnek (overloading) nevezzük. Ez a kényelem az olyan hagyományos nevû függvényeknél igazán lényeges.7.7). kényelmesebb lehet ugyanúgy elnevezni azokat. A függvények feltehetõen hasonlóak valamilyen értelemben. amelynek paraméterei a legjobban illeszkednek. és mutató típusú értékek összeadására is.

float f) { print(c). double-ról intre. két illeszkedést is találunk. vagy int-rõl unsigned int-re (§C. hogy megkeresi azt a függvényt. többértelmû: print(long(1)) vagy print(double(1))? A fordítóprogram a túlterhelt függvények halmazából úgy választja ki a megfelelõ változatot. // pontos illeszkedés: print(int) meghívása . 5. Kiterjesztést használó illeszkedés: csak egész értékre kiterjesztés (integral promotion) szükséges (bool-ról int-re. A túlterhelést feloldó szabályok elsõsorban azért ennyire alaposan kidolgozottak. int i. Felhasználói konverziókat használó illeszkedés (§11. ahol elõször találunk megfelelõ illeszkedést. Ahhoz. az alábbiakat kell megkísérelni (ebben a sorrendben): 1. void f() { print(1L). print(1). Pontos illeszkedés: nincs szükség konverzióra vagy csak egyszerû konverziókat kell végezni (például tömb nevét mutatóra. vagy T-t const T-re). 3. 4. void print(long).1).2). void print(char).6).0). short s. illetve ezek unsigned megfelelõirõl int-re §C. a hívást a fordító többértelmûként elutasítja. short-ról int-re. Szabványos konverziókat használó illeszkedés: int-rõl double-ra.6). // pontos illeszkedés: print(char) meghívása print(i). mert figyelembe kellett venni a C és a C++ beépített numerikus típusokra vonatkozó bonyolult szabályait (§C. void print(double). print(1. void print(long).6. void print(const char*). függvény nevét függvényre hivatkozó mutatóra.4).6).200 Alapok void print(double). T*-ról void*-ra (§5. Vegyük a következõ példát: void print(int). 2.6). void h(char c. hogy mindez elvárásainknak (közelítõen) megfelelõ módon történjen. } // print(long) // print(double) // hiba. valamint float-ról double-ra. Ha azon a szinten. Derived*-ról Base*-ra (§12. char-ról int-re. Függvénydeklarációban három pontot (…) használó illeszkedés (§7. amelyiknél a hívás paraméter-kifejezésének típusa a legjobban illeszkedik a függvény formális paramétereire.

print('a'). A print('a') hívás a print(char)-t. char c. // rendben print_char(c). hogy viszonylag alacsony . const char* p. A túlterhelés viszonylag bonyolult szabályrendszeren alapul. Függvények 201 print(s). print_string(i). nem biztonságos mûveletekkel szemben. Ez fárasztó lehet. print("a").7. print_char(i). print(49). void print_char(char). Túlterhelés nélkül több függvényt kellene megadnunk.2) irányuló erõfeszítéseket. print(0).3. mert 0 egy int. // C stílusú karakterlánc void g(int i. print_int(d). // rendben print_int(c). Ez azonban még mindig a kisebbik rossz. Az átalakítások (konverziók) és a kiterjesztések között azért teszünk különbséget. Vegyük figyelembe a túlterhelés alternatíváját: gyakran hasonló mûveleteket kell végrehajtanunk többféle típusú objektumon. és általában arra ösztönzi a programozót. // kiterjesztés egésszé: print(int) meghívása // float kiterjesztése double-lé: print(double) // pontos illeszkedés: print(char) meghívása // pontos illeszkedés: print(int) meghívása // pontos illeszkedés: print(int) meghívása // pontos illeszkedés: print(const char*) meghívása } A print(0) hívás a print(int)-et hívja meg. hogy a neveket helyesen használjuk. mivel 'a' egy char (§4. meghiúsítja az általánosított programozásra (§2. mint az int-rõl char-ra történõ átalakítás.7. double d) { print_int(i). // rendben? print_int(int(c)) meghívása // rendben? print_char(char(i)) meghívása // hiba // rendben? print_int(int(d)) meghívása } A túlterhelt print()-hez képest több névre és arra is emlékeznünk kell. különbözõ nevekkel: void print_int(int). így a programozó néha meglepõdhet azon. print(f). void print_string(const char*). // rendben print_string(p).1). melyik függvény hívódik meg. A túlterhelés feloldása független a szóba jöhetõ függvények deklarációs sorrendjétõl. mert elõnyben akarjuk részesíteni a biztonságos kiterjesztéseket (például char-ról int-re) az olyan.

A túlterhelés és a hatókör A különbözõ. fl = sqrt(fla).4) vagy függvényhívásra vonatkozó feloldás környezetfüggetlen maradjon. §11.1. void g() { void f(double). Ebbõl az következik.4. többé nem lenne lehetséges. nem névtér hatókörben megadott függvények nem túlterheltek: void f(int).202 Alapok szintû típusokra irányítsa figyelmét.2. hogy az egyes operátorokra (§11. hogy egy nem megfelelõ paramétert a fordítóprogram elutasít. 7. ezen függvények paraméterein bármilyen szabványos konverziót elvégezhetünk. } // f(double) meghívása . A túlterhelés növeli annak az esélyét. hogy a fenti példában a négy „hibás” paraméterrel való hívás közül csak egyet vesz észre a fordítóprogram.1. ami szintén hibákhoz vezethet. azzal melyik függvényt hívták meg. Ez azért szükséges. f(1). 7. Vegyük a következõt: float sqrt(float). float fla) { float fl = sqrt(da). void f(double da. double sqrt(double).2.2. Mivel nincs túlterhelés. A túlterhelés és a visszatérési típus A túlterhelés feloldásánál a visszatérési típust nem vesszük figyelembe. d = sqrt(fla).4. double d = sqrt(da). hogy elszigetelten nézzük az sqrt() egy hívását és eldöntsük. } // sqrt(double) meghívása // sqrt(double) meghívása // sqrt(float) meghívása // sqrt(float) meghívása Ha a visszatérési típust a fordítóprogram figyelembe venné.

és gondoskodnunk kell róla. Az ilyen esetekben helyi deklarációkat adhatunk a kódhoz vagy törölhetjük azokat.6-ot.2). amely feloldja a többértelmûséget: inline void f1(int n) { f1(long(n)). using deklarációkat vagy using utasításokat használhatunk (§8. a nem szándékos azonban meglepetéseket okozhat. a szándékos elfedés hasznos módszer lehet. Ez azonban általában csúnya és ideiglenes szükségmegoldás. mint egészet.2) vagy névtér-hatókörök (§8.2. Ha osztály-hatókörök (§15. Mint mindig.2. A hívások feloldására típuskényszerítést is használhatunk: f2(static_cast<int*>(0)). Ez többnyire úgy oldható meg. az ilyen esetekben úgy kell tekintsük egy függvény túlterhelt változatainak halmazát. } A fenti függvényváltozat hozzáadása feloldja az összes f1(i)-hez hasonló többértelmûséget a szélesebb long int típus javára.7. de a hatókörben csak f(double) látható. ha egy függvénynek túl sok (vagy túl kevés) túlterhelt változatát adjuk meg: void f1(char). void f2(int*). void f2(char*). hogy megkapjuk a kívánt viselkedést. A többértelmûség „kézi” feloldása Többértelmûséghez vezethet.2.2) között átnyúló túlterhelést szeretnénk.3. Függvények 203 Világos.9. 7. .4. hogy a változatok azonos értelmûek legyenek.2. ha a függvénynek egy olyan új változatát adjuk hozzá az eddigiekhez. void k(int i) { f1(i). hiszen a következõ – hamarosan bekövetkezõ – hasonló függvényhívással ismét foglalkoznunk kell majd. hogy f(int) lett volna a legjobb illeszkedés f(1)-re. Lásd még §8. // többértelmû: f2(char*) vagy f2(int*) } Ahol lehetséges. void f1(long). // többértelmû: f1(char) vagy f1(long) f2(0).

a §7. 7. complex pow(double. int).0). complex pow(complex.4. void k(complex z) { int i = pow(2. complex z2 = pow(2. int). a pow(int. amely az adott paraméterre a legjobban. a tapasztaltabbak viszont becsülik ezeket az üzeneteket.2) vagy pow(2.0).double) elsõ paraméterére 2. mert hasznos jelzõi a tervezési hibáknak.0. complex pow(complex. complex pow(complex.int) második paraméterére pedig 2 a legjobb illeszkedés. Az a függvény hívódik meg. Ha nem létezik ilyen függvény. double d = pow(2. complex z3 = pow(z.z).2). double). . complex).int) meghívása // pow(complex. hogy a legegyszerûbb algoritmus (függvény) lesz felhasználva.complex) meghívása // pow(complex.204 Alapok Néhány kezdõ C++ programozót bosszantanak a fordítóprogram által kiírt többértelmûségi hibák.2. } // pow(int. double).0.int) meghívása // pow(double.double(2))? A függvényhívás azért többértelmû. complex). double pow(double. a többire pedig jobban vagy ugyanúgy illeszkedik.complex) meghívása A kettõ vagy több paraméterû túlterhelt függvények közötti választás folyamán minden paraméterre kiválasztódik a legjobban illeszkedõ függvény. mert a pow (double.0.4 szabályai alapján.2). amikor a számítások hatékonysága és pontossága jelentõsen különbözik a szóban forgó típusoknál: int pow(int. a hívást a fordító többértelmûként elutasítja: void g() { double d = pow(2.2). Több paraméter feloldása A túlterhelést feloldó szabályok alapján biztosíthatjuk.double) meghívása // pow(double.0.z). complex z4 = pow(z. } // hiba: pow(int(2.4.

print(31. // rendben int g(int =0. Alapértelmezett értékeket csak a záró paramétereknek adhatunk: int f(int. milyen számrendszerben írja ki a függvény az egészt. } A túlterhelés viszont kevésbé teszi nyilvánvalóvá az olvasó számára. int base).2. char*). inline void print(int value) { print(value.7. hogy az a szándékunk. } // az alapértelmezett alap 10 ezt a kimenetet eredményezheti: 31 31 1f 11111 Az alapértelmezett (default) paraméter hatását elérhetjük túlterheléssel is: void print(int value. Például a void print(int value. Vegyünk egy függvényt. Az alapértelmezett paraméter típusának ellenõrzése a függvény deklarációjakor történik és a paraméter a függvény hívásakor értékelõdik ki. int.5. a legtöbb program azonban az egészeket tízes számrendszer szerint írja ki.3) létrehozó függvények gyakran számos lehetõséget nyújtanak a rugalmasság érdekében. int =0.10). // hiba . mint amennyi az egyszerû esetek kezeléséhez kell.10). hogy meghatározza. hogy legyen egy egyszerû print függvényünk és egy rövidítésünk. print(31. Nevezetesen az objektumokat (§10. amely egy egészt ír ki. print(31. char* =0).16). Ésszerûnek látszik megadni a felhasználónak a lehetõséget. char* =0). Alapértelmezett paraméterek Egy általános függvénynek általában több paraméterre van szüksége. void f() { print(31). int base =10). // hiba int h(int =0. int =0. Függvények 205 7.2).

printf("A nevem %s %s\n". void f(int = 7). void f(int = 8).2): int nasty(char*=0). Nem meghatározott számú paraméter Néhány függvény esetében nem határozható meg a hívásban elvárt paraméterek száma és típusa. void g() { void f(int x = 9). világ!\n").5). vezetek_nev. kereszt_nev). §6..)..206 Alapok Jegyezzük meg. Az ilyen függvényeket úgy adhatjuk meg. melynek jelentése „és talán néhány további paraméter”: int printf(const char* . hogy a C standard könyvtárának printf() függvénye (§21. .2. 7. hogy van más paramétere is: printf("Helló.3.8) meghívásakor legalább egy char* típusú paramétert vár. printf("%d + %d = %d\n". de lehet. hogy a név elfedi ugyanannak a névnek egy külsõ hatókörben levõ deklarációját.6. hogy a * és az = közötti szóköz lényeges (a *= értékadó operátor. // .. hogy a paraméter-deklarációk listáját a … jelöléssel zárjuk le. } // hiba: az alapértelmezett paraméter nem adható meg kétszer // hiba: különbözõ alapértelmezett paraméterek // rendben: ez a deklaráció elfedi a külsõt Hibalehetõséget rejt magában. ha egy nevet úgy adunk meg egy beágyazott hatókörben.. A fenti azt határozza meg. // szintaktikus hiba Az alapértelmezett paraméterek ugyanabban a hatókörben egy késõbbi deklarációval nem ismételhetõk meg és nem módosíthatók: void f(int x = 7).

h> int main() { printf("A nevem %s %s\n". A három pont csak akkor szükséges. A fordítóprogram viszont általában nem tudhatja (és nem is biztosíthatja). Képzeljük el. ami a hiba súlyosságát jelzi. A printf() esetében az elsõ paraméter egy formátum-vezérlõ. lehetõvé téve. hogy a várt paraméterek tényleg ott vannak és megfelelõ típusúak-e: #include <stdio. int execl(const char* . hogy megoldják a típusellenõrzést a legtöbb olyan esetben. Egy jól megtervezett programban legfeljebb néhány olyan függvényre van szükség. const char* . ha a paraméterek száma és típusa is változik. hogy „várj egy char* paramétert”. a fordítóprogram nem fog elegendõ információval rendelkezni a szabványos típusellenõrzés és -konverzió elvégzéséhez. amelyek még nem használják ki a C++ által nyújtott újabb lehetõségeket: int fprintf(FILE*.. } A fenti kódot a fordító lefordítja és (a legjobb esetben) furcsának látszó kimenetet hoz létre. hogy „várj egy int paramétert”. melyekkel hozzáférhetünk az ilyen függvények nem meghatározott paramétereihez. hogy úgy hozzuk létre a hibaüzenetet.2). hogy a printf() helyesen kezelje a többi paramétert: a %s például azt jelenti. amelyek nem elérhetõk a fordítóprogram számára. egy float pedig double-ként. amelynek van egy egész paramétere. amikor az a paraméterek listáját értelmezi.). hogy egy olyan hibafüggvényt írunk. a %d pedig azt.. Függvények 207 Az ilyen függvények olyan adatokra támaszkodnak. hogy lezárja: .7. hogy minden szót külön karakterlánc paraméterként adunk át. (Próbáljuk ki!) Természetesen ha egy paraméter nem deklarált. Ezen paraméterek listáját egy char-ra hivatkozó „nullpointer” kell. A túlterhelt vagy alapértelmezett paramétereket használó függvények arra használhatók. Leggyakrabban akkor használjuk. amikor olyan C könyvtári függvényekhez készítünk felületet. amikor a paraméterek típusát szükségbõl meghatározatlanul hagyjuk. melynek paraméterei nem teljesen meghatározottak. amely egyedi karaktersorozatokat tartalmaz. és ezt tetszõleges hosszúságú (több karakterláncból álló) szöveg követi. Elképzelésünk az. a programozó pedig nem feltétlenül ezt várja.).. Ebben az esetben egy short vagy egy char int-ként adódik át.. // a <cstdio> fejállományból // UNIX fejállományból A <cstdarg> fejállományban szabványos makrókat találunk.

severity).) { va_list ap. cerr << p << ' '. for (.argv[0]... if (p == 0) break. cerr << '\n'. amikor a típusellenõrzést „elnyomja” a három pont. // paraméterek visszaállítása va_end(ap). } . char* argv[ ]) { switch (argc) { case 1: error(0. char[ ]).Null_cp).. amellyel a programozó szembenéz. extern char* itoa(int. error(1.6.208 Alapok extern void error(int . va_start(ap... hogy ha a 0 egész értéket használtuk volna befejezésként. } Az itoa() azt a karakterláncot adja vissza. if (severity) exit(severity). } // .char*). const char* Null_cp = 0. default: char buffer[8]. Vegyük észre.buffer). amelyik a függvény egész típusú paraméterének felel meg. a kód nem lett volna hordozható: néhány nyelvi megvalósítás a nulla egészt és a nulla mutatót (nullpointer) nem azonos módon ábrázolja.argv[1].)..argv[0].) { // a "severity" (súlyosság) után nullával lezárt char*-ok // következnek // kezdeti paraméterek } char* p = va_arg(ap. Ez a példa szemlélteti a nehézségeket és azt a többletmunkát."arguments". case 2: error(0. // lásd §6. A hibafüggvényt így adhatjuk meg: void error(int severity . Null_cp). break. "with".Null_cp)..argv[0].itoa(argc-1. break.[17] int main(int argc.

Azaz. egy függvényre hivatkozó mutatót nem kötelezõ a * operátorral feloldanunk. a va_arg() feltételezi. hogy efct egy mutató és meghívja az általa mutatott függvényt. jelentése ugyanaz. void f() { efct = &error. } // mutató függvényre // efct az error függvényre mutat // error meghívása efct-n keresztül A fordítóprogram rá fog jönni. void g() { f1("Vasa"). mint a függvényeknél. 7.. efct("error"). Amikor a függvény címét vesszük. void (*f2)(string) = error. A va_end() helyreállítja ezeket a módosításokat. hogy ilyen típusú aktuális paraméter került átadásra. } // rendben // ez is jó. A va_start makró paraméterei a va_list neve és az utolsó formális paraméter neve. Ugyanígy nem kötelezõ a & használata sem a függvény címének lekérdezésére: void (*f1)(string) = &error. ahol a va_start()-ot használtuk.7. hogy a va_start() úgy módosíthatja a vermet. hogy ezt biztosítani is tudja. A va_arg() makrót arra használjuk. A programozónak minden egyes híváskor meg kell adnia egy típust. meg kell hívnunk a va_end()-et. az így kapott mutatót használhatjuk arra. Ennek az az oka. Függvényre hivatkozó mutatók A függvényekkel csak két dolgot csinálhatunk: meghívhatjuk õket és felhasználhatjuk a címüket. Függvények 209 Elõször meghatározzuk a va_list-et és a va_start() meghívásával értéket adunk neki. (*f1)("Mary Rose"). de általában nincs mód arra. */ } void (*efct)(string). Mielõtt még visszatérnénk egy olyan függvénybõl. hogy sorba rendezzük a nem megnevezett paramétereket. hogy a visszatérést nem lehet sikeresen véghezvinni. A mutatókat használó értékadásokban ügyelni kell a teljes függvény típusára: .7. hogy meghívjuk a függvényt: void error(string s) { /* .. mint az &error-nak // rendben // ez is jó A függvényekre hivatkozó mutatók paramétereinek típusát ugyanúgy meg kell adnunk.

// mutató void(string)-re // void(string) // int(string) // void(int*) // rendben // hiba: rossz visszatérési típus // hiba: rossz paramétertípus // rendben // hiba: rossz paramétertípus // hiba: void értékadás int-nek A paraméterátadás szabályai a közvetlen függvényhívások és a függvények mutatón keresztül történõ meghívása esetében ugyanazok. Itt nincs lehetõségünk. Gyakran kényelmes. Íme egy példa egy UNIX-os rendszer-fejállományból: typedef void (*SIG_TYP)(int).210 Alapok void (*pf)(string). Az egér gombjaival kiválasztott menüpontokhoz kapcsolódó mûveleteket vezérlõ mutatókat így határozhatjuk meg és tölthetjük fel értékkel: PF* button2 = edit_ops. PF file_ops[ ] = { // fájlkezelés &open. &close. hogy ne mindig a meglehetõsen nehezen érthetõ deklarációformát használjuk. &write }. PF edit_ops[ ] = { // szerkesztõmûveletek &cut. ha nevet adunk egy függvényre hivatkozó mutató típusnak. &append. . hogy a rendszert részletesen ismertessük. void f1(string). typedef void (*SIG_ARG_TYP)(int). // a <signal. SIG_TYP signal(int. pf = &f2.h> fejállományból A függvényekre hivatkozó mutatókból álló tömbök gyakran hasznosak. &copy. &search }. PF* button3 = file_ops. de az alapvetõ ötlet ez: typedef void (*PF)(). void f() { pf = &f1. int f2(string). pf(1). SIG_ARG_TYP). Például az egeret használó szövegszerkesztõm menürendszere az egyes mûveleteket jelölõ függvényekre hivatkozó mutatókból összeállított tömbökkel van megvalósítva. void f3(int*). } int i = pf("Zeusz"). pf("Héra"). &paste. pf = &f3.

6) nélkül. Az ilyen változásokat (részben) úgy hajtjuk végre. // szükséges típuskényszerítés char* pj = b+j*sz. az egérgombok jelentése gyakran megváltozik a környezettel együtt. void ssort(void* base. pj[k] = pjg[k]. pjg[k] = temp. */ { Shell rendezés (Knuth. // button2 harmadik függvényének meghívása Akkor tudnánk igazán nagyra értékelni a függvényekre hivatkozó mutatók kifejezõerejét. i++) for (int j=i-gap. hogy módosítjuk a gombokhoz kapcsolt mutatók értékét. a megfelelõ mûvelet hajtódik végre: button2[2](). gap/=2) for (int i=gap. A függvényekre hivatkozó mutatók arra is használhatók.2. CFT cmp) /* A "base" vektor "n" elemének rendezése növekvõ sorrendbe a "cmp" által mutatott összehasonlító függvény segítségével. i<n. hogy minden menüelemet meghatározhassunk. k<sz.7.o. size_t n. Függvények 211 A teljes megvalósításhoz több információra van szükség ahhoz. 84. különbözõ típusú objektumra lehet alkalmazni – egyszerû formáját adják: typedef int (*CFT)(const void*. size_t sz. 3. } } } . hogy új függvényeket teszünk a mûvelettáblába. const void*). Egy menüt futási idõben úgy módosíthatunk. 0<gap. hogy a többalakú (polimorf) eljárások – azaz amelyeket több. 0<=j. kötet. j-=gap) { char* b = static_cast<char*>(base). amelyik meghatározza a kiírandó szöveget.pj)<0) { // base[j] és base[j+gap] felcserélése for (int k=0. Például tárolnunk kell valahol azt a karakterláncot. k++) { char temp = pj[k]. a virtuális függvények (§12. Az elemek "sz" méretûek. ha nélkülük próbálnánk ilyen kódot írni – és még jobban viselkedõ rokonaik. de új menüket is könnyen létrehozhatunk.) } for (int gap=n/2. // &base[j+gap] if (cmp(pjg. Amikor a felhasználó kiválaszt egy menüpontot (például a 3-as elemet a 2-es gomb számára). Ahogy a rendszert használjuk. // &base[j] char* pjg = b+(j+gap)*sz.

1). csak az elemek számát (a tömb méretét). i++) cout << v[i]. mint a második. } .G.W. de nem a legelegánsabb módja.".name << '\t' << v[i]. hogy megegyezzen a szabványos C könyvtári qsort() rendezõ eljárás típusával. "Schryer N. amelyeket rendez. "nls". } int cmp2(const void* p.L. 11276 void print_id(User* v.7.dept << '\n'. const void* q) // osztályok (dept) összehasonlítása { return static_cast<const User*>(p)->dept .212 Alapok Az ssort() nem ismeri azoknak az objektumoknak a típusát. §13. 11274. User heads[ ] = { "Ritchie D.L.5. hogy rendezni tudjunk. "nls". "Sethi R.". ha paraméterei egyenlõek. "bwk".". char* id. i<n. Ez a kódolási stílus gyakori C-ben. hogy ezt az algoritmust C++-ban írjuk le (lásd §13. "Szymanski T. 11275. int dept. "ravi".id << '\t' << v[i]. "dmr". }.". az egyes elemek méretét. nullát. "tgs". int n) { for (int i=0. 11273.M. 11271.". vagy egyedi rendezõ eljárást használnak. "Schryer N.3. melyet meg kell hívnia. egyéb esetben pedig pozitív számot: int cmp1(const void* p. Egy ilyen rendezõ függvényt a következõképpen lehetne egy táblázat rendezésére használni: struct User { char* name.".static_cast<const User*>(q)->dept. hogy elvégezze az összehasonlítást. const void* q) // nevek (name) összehasonlítása { return strcmp(static_cast<const User*>(p)->name.2).static_cast<const User*>(q)->name). Az ssort() típusát úgy választottuk meg. a C++ standard könyvtárának sort algoritmusát (§18. ha az elsõ paramétere kisebb. 11272. Az összehasonlító függvénynek negatív értéket kell visszaadnia. } Elõször meg kell határoznunk a megfelelõ összehasonlító függvényeket. }. A valódi programok a qsort()-ot. "Kernighan B. és azt a függvényt.

sizeof(User).cmp2). ssort(heads. ha függvényekre hivatkozó mutatókat adunk értékül vagy töltünk fel kezdõértékkel. . nem megfelelõ paraméter az ssort() számára. hogy int cmp3(const mytype*. Ezen típusokra vonatkozóan nincs automatikus konverzió.const mytype*). print_id(heads.sizeof(User).7. Ha cmp3-at elfogadnánk az ssort paramétereként. cout << '\n'.cmp1). print_id(heads.6). megszegnénk azt a vállalást.5-öt). int f(char). void (*pf1)(int) = &f. Függvények 213 Ez a program rendez és kiír: int main() { cout << "Fõnökök ábécésorrendben:\n". cout << "Fõnökök osztályok szerint:\n". } Egy túlterhelt függvény címét úgy használhatjuk fel. void (*pf3)(char) = &f.6. hogy a cmp3-at mytype* típusú paraméterekkel fogjuk meghívni (lásd még §9.2. // void f(int) // int f(char) // hiba: nincs void f(char) Egy függvényt egy függvényre hivatkozó mutatón keresztül pontosan a megfelelõ paraméter. hogy egy függvényre hivatkozó mutatóhoz rendeljük vagy annak kezdõértékül adjuk. int (*pf2)(char) = &f. Ebben az esetben a cél típusa alapján választunk a túlterhelt függvények halmazából: void f(int).6. ssort(heads.és visszatérési típusokkal kell meghívni. Ez azt jelenti.6).

214 Alapok 7. mint a hibakeresõk. számíthatunk arra. ha nem szükségesek. paraméterként meg kell adnunk két karakterláncot. amikor MAC behelyettesítõdik.11 mutatja be. Egy egyszerû makrót így adhatunk meg: #define NAME a sor maradék része ahol a NAME szimbólum elõfordul.8. Kövessük azt a szokást. Ha makrót kell használnunk. Mivel átrendezik a programkódot. mielõtt a fordítóprogram látná azt. Ezek x-et és y-t fogják helyettesíteni. számos programozási eszköz számára komoly problémát jelentenek. hogy sok nagybetû legyen bennük. kereszthivatkozás-vizsgálók és hatékonyságvizsgálók gyengébb szolgáltatást fognak nyújtani. Majdnem minden makró a programozási nyelv. olvassuk el figyelmesen C++-változatunk elõfordítójának (preprocessor) hivatkozási kézikönyvét és ne próbáljunk túl okosak lenni. Makrók A makrók nagyon fontosak a C-ben. ott kicserélõdik a sor maradék részére.y) argument1: x argument2: y Amikor MAC-ot használjuk. Például a expanded = MAC(foo bar. de kevesebb a hasznuk a C++-ban. yuk yuk) így alakul át: expanded = argument1: foo bar argument2: yuk yuk . hogy az olyan eszközök. vagy a programozó gyenge pontját mutatja. Például a named = NAME kifejezést a következõ váltja fel: named = a sor maradék része Megadhatunk paraméterekkel rendelkezõ makrót is: #define MAC(x. a program. A makrók formai követelményeit az §A. Az elsõ makrókra vonatkozó szabály: ne használjuk õket. hogy a makrókat úgy nevezzük el. Így ha makrót használunk.

7. amikor a makrót kifejtjük. int y = SQUARE(xx+2). void f() { int xx = 0.) Néhány teljesen fölösleges makró: #define PI 3. miért veszélyesek. keveset tudnak a C++ nyelvtanáról és semmit sem a C++ típusairól. Íme néhány lehetséges makró: #define CASE break. amikor a makró behelyettesítõdik.b. nem túlterhelés */ #define FAC(n) (n>1)?n*FAC(n-1):1 /* problémás: rekurzív makró */ A makrók karakterláncokat kezelnek.. ami nagyon homályos hibaüzenetekhez vezet. A fordítóprogram csak a makró behelyettesített formáját látja. Függvények 215 A makróneveket nem terhelhetjük túl és a makró-elõfordító rekurzív hívásokat sem tud kezelni: #define PRINT(a.b) cout<<(a)<<(b) #define PRINT(a. } // globális számláló // lokális változó // y=xx+2*xx+2 vagyis y=xx+(2*xx)+2 // a lokális xx növelése . próbáljuk meg behelyettesíteni ezt: int xx = 0.c) cout<<(a)<<(b)<<(c) /* problémás?: újbóli definíció.case #define FOREVER for(. INCR_xx. illetve a hatókörök szabályairól. így akkor jelzi a makróban lévõ esetleges hibát.141593 #define BEGIN { #define END } És néhány veszélyes makró: #define SQUARE(a) a*a #define INCR_xx (xx)++ Hogy lássuk. és nem akkor.

A #undef X utasítás biztosítja. ha az utasítás elõtt szerepelt ilyen. akkor az vagy lehetetlennek. inline. egyéni nyelvünket.cah)(). amelyek megjegyzésekre szorulnak. vagy szükségtelenül nehéznek bizonyulhat. bölcs dolog /* */ megjegyzéseket használnunk.b) (((a)<(b))?(a):(b)) Ha bonyolult makrókat kell írnunk. használjuk a :: hatókör-jelzõt. Két karakterláncot a ## makróoperátorral összefûzve például új karakterláncot hozhatunk létre: #define NAME2(a. Ha azonban ezt a „kibõvített nyelvet” részesítjük elõnyben a sima C++-szal szemben. de nem tudhatjuk. A const. Ez a következõt eredményezi a fordítóprogram számára: int hackcah().9. . hogy X nevû makró nem lesz definiálva – akkor sem. és a makró paraméterek elõfordulásait tegyük zárójelbe. Ha valami nem magától értetõdõt akarunk csinálni. Továbbá a C elõfordító egy nagyon egyszerû makró-feldolgozó. template<class T> inline T min(T a. hogy egy új névre van szükségünk valami számára.216 Alapok Ha makrót kell használnunk. hogy egy kódrészletben mit feltételezzünk X hatásairól. az a legtöbb C++ programozó számára érthetetlen lesz. ezek viszont nem ismerik a // jelölést: #define M2(a) something(a) /* értelmes megjegyzés */ Makrók használatával megtervezhetjük saját. Ez bizonyos védelmet ad a nem kívánt makrók ellen. mert a C++ eszközök részeként néha C elõfordítókat használnak. T b) { return (a<b)?a:b. enum és namespace megoldásokat arra szánták. } Amikor makrót írunk.4) hivatkozunk. amikor globális nevekre (§4.b) a##b int NAME2(hack. hogy a hagyományos elõfordított szerkezeteket kiváltsák: const int answer = 42. template. ahol csak lehetséges: #define MIN(a. nem ritka.

hogy feltételesen minden bemenetet figyelmen kívül hagyjon. kódrészletbõl a fordítóprogram ennyit lát (kivéve ha az arg_two nevû makrót a #define elõfordító direktívával korábban definiáltuk): int f(int a ). Feltételes fordítás A makrók egy bizonyos használatát majdnem lehetetlen elkerülni. }. Node* arg_two. amíg az #endif utasítással nem találkozik. Ez az ártatlannak látszó forrásszöveg zavart fog okozni.7. Ez megzavarja azokat az eszközöket. // . amelyek ésszerû viselkedést tételeznek fel a programozóról.3-at. Az #ifdef azonosító direktíva arra utasítja a fordítóprogramot. . Lásd még §9.int b #endif ).1. Függvények 217 7.8. Az #ifdef-et vezérlõ makrók neveit figyelmesen kell megválasztani. Például az int f(int a #ifdef arg_two . kevés kárt okoz.3. ha valaki a következõt írja: #define arg_two x Sajnos a szokványos és elkerülhetetlenül beépítendõ fejállományok sok veszélyes és szükségtelen makrót tartalmaznak. Az #ifdef legtöbb felhasználása kevésbé bizarr. hogy ne ütközzenek a szokásos azonosítókkal: struct Call_info { Node* arg_one. és ha mérséklettel használják...

Gyakorlatok 1. (*1) Deklaráljuk a következõket: függvény.8. int). amelynek ilyen mutató paramétere van.4. 7. ilyen függvényre hivatkozó mutató. [5] Kerüljük a nem meghatározott számú paraméterek használatát. Tanácsok [1] Legyünk gyanakvóak a nem const referencia paraméterekkel kapcsolatban.5.5.6. sok nagybetûvel.2. §7. [10] Ha makrókat kell használnunk. hogy egy virtuális függvény (§2.3. [2] Használjunk const referencia paramétereket. §7.7. használjunk csúnya neveket. használjunk inkább mutatókat és érték szerinti visszaadást.7. függvény. §7. hogy megszüntessük a többértelmûséget.8. ha azt akarjuk. [9] Ha függvényre hivatkozó mutató használatát fontolgatjuk. [7] Akkor használjuk a túlterhelést. 2. de következesen.10. ha a függvények elvben ugyanazt a mûveletet hajtják végre különbözõ típusokon. §7.3.9. [6] Ne adjunk vissza lokális változókra hivatkozó mutatókat vagy ilyen referenciákat. hogy a függvény módosítsa paraméterét.2) használata nem jobb megoldás-e. Tipp: használjunk typedef-et. §5. Írjuk meg azt a függvényt.5) vagy sablon (§2. amelynek egy karakterre hivatkozó mutató és egy egészre mutató referencia paramétere van és nem ad vissza értéket. [8] Amikor egészekre vonatkozik a túlterhelés.218 Alapok 7. §7. §7. §5. [4] Kerüljük a makrókat. ha a lehetõ legritkábbra kell csökkentenünk a paraméterek másolását. amely ilyen mutatót ad vissza. §7. (*1) Mit jelent a következõ sor? Mire lehet jó? typedef int (&rifii) (int. vizsgáljuk meg.4. függvény. amelynek egy ilyen mutatójú paramétere van és visszatérési értékként paraméterét adja vissza.5. [3] Használjuk a const-ot széleskörûen. §7. . használjunk függvényeket.

elnevezhetjük cat-nek. illetve delete-re. (*2) Írjuk újra az ssort()-ot (§7. és kiírja azokat egymás után a cout-ra. amellyel új szavakat tehetünk egy Tnode-okból álló fába.5) Írjunk olyan programot. a program ne végezzen titkosítást. }. int count. A program ciklikus módon használja a key-ben lévõ karaktereket. Ha nincs megadva key (vagy a paraméter null-karakterlánc). Ahol lehetséges.7. Távolítsuk el az extern deklarációkat a . . hogy megkapja a kimenetet. hogy a szavak ábécésorrendben vannak. Mivel ez a program összefûzi a paramétereit. const-ra vagy inline-ra. cseréljük ki a #define utasításokat enum-ra. Írjunk függvényt. 8. Tipp: qsort(). amit a szabad tár karaktertömbként tárol. Írjunk függvényt. ami egy tetszõlegesen hosszú szóra mutat. név!”. Módosítsuk a Tnode-ot. 6. (*2) Írjunk titkosító programot. alakítsunk át minden függvényt a C++ függvények formai követelményeinek megfelelõen. Tnode* left. Tipp: §C.5) Írjunk függvényt. Tnode* right.5) Írjunk egy „Helló. amely kiír egy Tnode-okból álló fát. hogy „Helló. melyek nevei parancssori paraméterként vannak megadva. Cseréljük ki a malloc() és free() hívásokat new-ra. ami parancssori paraméterként vesz egy nevet és kiírja. amely egy Tnode-okból álló fát úgy ír ki. egyszerû titkosító sémát: c karakter titkosított formája legyen c^key[i]. Írjunk olyan függvényt. amely kétdimenziós tömböt invertál. 5. ahol key egy karakterlánc. 4. Használhatjuk a következõ. (*1. (*2. hogy (csak) egy mutatót tároljon. Függvények 219 3.7) egy hatékonyabb rendezési algoritmus felhasználásával.5) Vegyük a következõt: struct Tnode { string word. Távolítsuk el a szükségtelen konverziókat. (*1. Módosítsuk a függvényeket. amely parancssori paraméterként adott. a new segítségével.c fájlokból. ami a cin-rõl olvas és a kódolt karaktereket kiírja a cout-ra. világ!”-szerû programot. (*2) Alakítsunk egy kis C programot C++ programmá. és ha szükséges.7. Módosítsuk ezt a programot úgy. hogy a Tnode új definicióját használják. amíg a teljes bemenetet el nem olvasta. amely tetszõleges számú fájlt olvas be. (*2. hogy minden meghívott függvény deklarálva legyen és határozzuk meg minden paraméter típusát. 9. hogy tetszõleges számú név paramétere lehessen és mondjon hellót minden egyes névvel. Módosítsuk a fejállományokat úgy. 7.

hogy tudná a kulcsot. Tipp: az asztali számológép (§6. Nézzük meg a §21.220 Alapok 10. (*2) Írjunk függvényeket. 18. ahogy azt a §6.1) tartalmaz egy szimbólumtáblát és egy lexikai elemzõt. mint az sqrt(). o. (*2) Írjuk meg magunk a print() függvényt a §7.b) a>b?a:b #define fac(a) (a)*fac((a)-1) 15. amelyek egy napot. ha nem tudjuk. (*3) Írjunk egy error nevû függvényt. egy hónapot. Macmillan. (*1) Hogyan választanánk meg a typedef használatával meghatározott függvényekre hivatkozó mutatótípusok neveit? 13. és sin(). log(). 17. 207-213. ami megadja egy adott Date-re következõ elsõ hétfõ Date-jét.6[13]-ban leírtuk. Írjunk olyan függvényt.5-bõl. (*1) Mi a hiba ezekben a makrókban? #define PI = 3. (*2) Nézzünk meg néhány programot. Használjuk a <cstdarg>-ot. %c és %d kifejezéseket tartalmazó. (*2) Adjunk hozzá a §6. New York. Hogyan használják a nagybetûket? Hogyan használják az aláhúzást? Mikor használnak rövid neveket. hogy egy adott Date a hét melyik napjára esik.8-at. Tipp: lásd David Kahn: The Codebreakers. 19.1 pontban lévõ asztali számológéphez olyan függvényeket. %c és %d.5) Írjunk programot. a függvényeket pedig függvényre hivatkozó mutatókból álló tömbön keresztül hívjuk meg. (*3) Írjunk makrófeldolgozót. #define MAX(a. 1967. Lásd még §11. hogy elképzelésünk lehessen a mostanság használatos nevek stílusának változatosságáról. Írjunk függvényt. 16. mint amilyen az i és x? 14. Olvassunk a cin-rõl és írjunk a cout-ra.10[9]-ben leírt módszerrel titkosított üzeneteket. amely nem hívja meg önmagát. (*1) Írjunk olyan faktoriális függvényt. 12. és egy évet adnak hozzá egy Date-hez.14[6]-ot. Ne felejtsük el ellenõrizni a függvényhívások paramétereit. mit jelent a %s. Elõször ne próbáljunk paraméterekkel rendelkezõ makrókat kezelni.141593. printf stílusú. 11. anélkül. . formázott karakterláncokat vesz paraméterként és ezen kívül tetszõleges számú paramétere lehet. amely egyszerû makrókat definiál és cserél ki (ahogy a C elõfordító teszi). amely %s. (*3. Tipp: adjuk meg elõre a neveket. ami segít megfejteni a §7. Ne használjuk a printf()-et. amit módosíthatunk. ami megadja.

sz. világ!” program is legalább két részre osztható: a felhasználói kódra. Modulok és felületek Minden valóságos program különálló részekbõl áll. és a kiírást végzõ I/O rendszerre. ami alól ne lenne valamilyen kivétel. világ! kiírását kéri.” (Robert Burton) Modulok. felületek és kivételek • Névterek • using • using namespace • Névütközések feloldása • Nevek keresése • Névterek összefûzése • Névtér-álnevek • Névterek és C kód • Kivételek • throw és catch • A kivételek és a programok szerkezete • Tanácsok • Gyakorlatok 8.8 Névterek és kivételek „– Ez a 787-es év! – I. . Még az egyszerû „Helló. ami a Helló.1.?” (Monty Python) „Nincs olyan általános szabály.

2. A szintaktikai elemzõ például közvetlenül csak az adatbeviteli függvény felületére. nem pedig a teljes lexikai elemzõre támaszkodik. ami a karakterekbõl szimbólumokat hoz létre 3. Az igazat megvallva a számológépet három részbõl állóra terveztem. Az egyszerûsítés kedvéért nem jelöltem. nem szükséges. Ezt ábrával így mutathatjuk be: . A (szintaktikai) elemzõbõl (parser). hogy 5 részbõl áll: 1. Láthatjuk. a vezérlõt és a hibakezelõt a teljesség miatt adtam hozzá. érték) párokat tároló szimbólumtáblából 4. hogy mindent tudjon a felhasznált modulról. a main() vezérlõbõl 5. ami a szintaktikai elemzést végzi. az adatbeviteli függvénybõl vagy lexikai elemzõbõl (lexer). Következésképpen különbséget teszünk a modul és a modul felülete (interfész) között. és a hibakezelõbõl Ábrával: vezérlõ elemzõ adatbeviteli függvény szimbólumtábla hibakezelõ A fenti ábrában a nyíl jelentése: „felhasználja”. Amikor egy modul felhasznál egy másikat.222 Alapok Vegyük a számológép példáját a §6. Az adatbeviteli függvény csak megvalósítja a felületében közzétett szolgáltatásokat. hogy mindegyik rész támaszkodik a hibakezelésre.1-bõl. Ideális esetben a modulok legnagyobb része nem ismert a felhasználó elem számára. a (karakterlánc.

Névterek és kivételek 223 vezérlõ szintaktikai elemzõ felülete lexikai elemzõ felülete szimbólumtábla felülete hibakezelõ szintaktikai elemzõ megvalósítása lexikai elemzõ megvalósítása szimbólumtábla megvalósítása A szaggatott vonalak jelentése: „megvalósítja”. anélkül. az a feladatunk. Az igazi programoknál nem valószínû. gyakran függvények. Nekünk. hogyan rendezhetjük el úgy a program forrásszövegét. A következõ részben bemutatjuk. hogy abból elõnyünk származzon.1). hogyan lehet a számológép program logikai felépítését világosan kifejezni. hogyan használhatjuk a kivételeket arra. Ezért miután tárgyaltuk.3). hogy nagyobb programok esetében is hasznos módszereket mutassunk be. hogy a hibakezelés okozta modulok közötti függõségekbõl minél kevesebb legyen. A C++ kivételeket nyújt arra a célra. A hibakezelés mindenütt fontos szerepet tölt be a program szerkezetében. hatékony. A tapasztalt programozó már az elején kiválaszthat egy „körülbelül megfelelõ” tervet. sablonok stb. hogy belefulladnánk a kódba. Ha ezt tesszük. a §9.1. programozóknak. ügyelnünk kell arra. A valódi programokban minden modul. Amikor egy programot modulokra bontunk vagy egy programot modulokból hozunk létre. osztályok. a „valódi életben” nem használnám olyan mértékben a névtereket és a külön fordítást (§2. A nyelvi eszközök bõ választékának bemutatásához több lépésben bontom modulokra a számológépet.8. §9. Ahogy azonban a program az évek során fejlõdik. és könnyen módosítható lesz. Ez tekinthetõ a program valódi felépítésének. hogy ezen lépések mindegyikét végrehajtanánk. A számológép kis program. Most csak azért használjuk ezeket. mint itt. hogy ezt hû módon adjuk vissza a kódban.3 pontban pedig azt. a kód egyszerû. hogy elkülönítsük a hibák észlelését és jelzését azok kezelésétõl. . százait tartalmazza. bemutatjuk. nem ritkák a drasztikus szerkezeti változtatások.2). érthetõ. mert közvetlenül fogja tükrözni eredeti elképzelésünket.4. amelyet önálló névtér jelöl. hogyan ábrázolhatjuk a modulokat névterekként (§8. hogy a modularitást tovább javítsuk (§8.

kényelmes és hatékony kapcsolattartás biztosítása jelenti.. }. */ } NUMBER. */ } double expr(bool get) { /* .1.1ben leírt függõségi kört. ASSIGN='='. A számológép elemzõjének (§6.. Érdekes módon minden rendszer könnyen modulokra bontható. mint ahogy ebben és a következõ fejezetben tesszük.. } Token_value get_token() { /* .. Token_value curr_tok. MUL='*'.2. double number_value. A nehézséget a modulok közötti biztonságos.224 Alapok A modularitás fogalma sokkal több módon értelmezhetõ. Programjainkat például részekre bonthatjuk párhuzamosan végrehajtott és egymással kapcsolatot tartó folyamatok segítségével is.. hogy megtörjük a §6. DIV='/'.. Ugyanígy az önálló címterek (address spaces) és a címterek közötti információs kapcsolat is olyan fontos témakörök. ha egyes deklarációk valamilyen jellemzõ alapján összetartoznak. akkor ezt a tényt kifejezhetjük úgy is..1. RP=')' .'. LP='('. a modularitás ezen megközelítései nagyrészt egymástól függetlenek és ellentétesek. PRINT='. Azaz. END. Úgy gondolom.. */ } } Az expr() függvényt elõször deklaráljuk és csak késõbb fejtjük ki. amelyeket itt nem tárgyalunk.1) deklarációit például a Parser névtérbe tehetjük: namespace Parser { double expr(bool). Névterek A névterek (namespace) mindig valamilyen logikai csoportosítást fejeznek ki. double prim(bool get) { /* . hogy közös névtérbe helyezzük azokat. string string_value. */ } double term(bool get) { /* . 8. A számológép bemeneti részét szintén önálló névtérbe helyezhetjük: namespace Lexer { enum Token_value { NAME. MINUS='-'. PLUS='+'.

Névterek és kivételek 225 A névterek ilyen használata elég nyilvánvalóvá teszi. A névtér tagjait a következõ jelölés használatával kell bevezetni: namespace névtér_név { // deklaráció és definíciók } . a szerkezet zavarossá vált volna. A felhasználó programelemek csak a deklarációkat tartalmazó felületet fogják látni. Láthattuk. */ } double Parser::term(bool get) { /* . hogy megtaláljuk a felületet. ráadásul a felületet sokszor már a megvalósítás részleteinek kidolgozása elõtt megtervezik. általában több oldalas (képernyõs) információn kell átrágnunk magunkat.8.. azaz.4. hogy a felület és a lényegi programrész szétválasztásának eredményeként most minden függvénynek pontosan egy deklarációja és egy definíciója van. */ } Vegyük észre... ahol a felületet (interfész) elkülönítjük a megvalósítástól (implementáció): namespace Parser { double prim(bool).. mit nyújt a lexikai és a szintaktikai elemzõ a felhasználó programelemnek. } double Parser::prim(bool get) { /* . A felületek meghatározása alapvetõ tervezési tevékenység (lásd §23. hiszen egy modul a különbözõ programelemek számára különbözõ felületeket nyújthat. amely a megvalósítást tartalmazza. */ } double Parser::expr(bool get) { /* . milyen szolgáltatások vannak felkínálva. a névtér_neve::tag_neve jelölést használva. A program megvalósítását – ebben az esetben a függvénytörzseket – a felhasználó elem látókörén kívül helyezzük el.. mire megtaláljuk. Külön meghatározott felületek helyett olyan eszközöket is biztosíthatunk. Ezt nem tekintem jó megoldásnak. amelyek kinyerik a felületet egy modulból.3. double term(bool). és kifejthetjük késõbb.4-et). Ha egy valóságos méretû névtér deklarációjába beletesszük a függvénytörzseket is. hogy egy tagot megadhatunk a névtér meghatározásán belül. Ha azonban a függvények forráskódját is a névterekbe helyeztem volna.. double expr(bool). Íme a Parser egy olyan változata.

. illetve az eltérõ típusokból adódó hibákat észrevegyük: double Parser::trem(bool). annál hasznosabbak a névterek. // hiba: nincs trem() a Parser névtérben // hiba: Parser::prim() logikai paraméterû A névtér (namespace) egyben hatókör (scope). A kivétel a main(). A Parser minõsítõre itt azért van szükség.10. } // figyeljük meg a Parser:: minõsítõt // nem kell minõsítõ // figyeljük meg a Lexer:: minõsítõt // figyeljük meg a Lexer:: minõsítõt // nem kell minõsítõ } // . Ezért – elméletileg – egy bonyolultabb program minden deklarációját önálló névterekbe kellene helyezni. amelynek globálisnak kell lennie. Minél nagyobb egy program. és nem valamilyen más globális függvény. ha minõsítjük névterének nevével: double Parser::term(bool get) { double left = prim(get). hogy kifejezzék a program részeinek logikai elkülönítését.1.. // . amelyet a Parser-ben bevezettünk. Másik névtérbõl származó nevet viszont csak akkor használhatunk. Ideális esetben egy program minden eleme valamilyen felismerhetõ logikai egységhez (modulhoz) tartozik. A közönséges lokális hatókörök. vagyis nagyon alapvetõ és viszonylag egyszerû fogalom. hogy ez a term() az. melyek neve a programban betöltött logikai szerepet jelzi...3.3). 8. minden további nehézség nélkül használhatjuk. így ha egy nevet elõzetesen a névtérben vagy egy körülvevõ blokkban adtunk meg.2. for (.) switch (Lexer::curr_tok) { case Lexer::MUL: left *= prim(true). a globális hatókörök és az osztályok maguk is névterek (§C. hogy könnyen meg lehessen találni minden nevet a névtérdeklarációban. Az általános hatókör-szabályok természetesen rájuk is vonatkoznak.226 Alapok A névtérdefiníción kívül új tagot nem adhatunk meg minõsítõ formában: void Parser::logical(bool). // hiba: nincs logical() a Parser névtérben A cél az. double Parser::prim(int). és hogy a gépelési.. Minõsített nevek A névterek külön hatókört alkotnak. hogy a futási idejû környezet felismerje (§8. Mivel a term() .3). hogy kifejezzük.

Vegyük a következõt: double Parser::prim(bool get) { if (get) Lexer::get_token(). if (Lexer::curr_tok != Lexer::RP) return Error::error(") szükséges"). } A Lexer minõsítés ismételgetése igen fárasztó. using Error::error. nem kell minõsítenie a prim()-et. mintha az nem deklarált lenne. 8.2.2. return v. } case Lexer::MINUS: // mínusz elõjel (egyoperandusú mínusz) return -prim(true). // elemi szimbólumok kezelése } switch (Lexer::curr_tok) { case Lexer::NUMBER: // lebegõpontos konstans Lexer::get_token(). hogy az ebben a hatókörben használt get_token a Lexer get_token-je: double Parser::prim(bool get) { using Lexer::get_token. using Lexer::curr_tok. } case Lexer::END: return 1. de ki lehet küszöbölni egy using deklarációval. a fordítóprogram a curr_tok változót úgy tekinti. Using deklarációk Ha egy név gyakran használatos saját névterén kívül. mivel a Lexer névtér tagjai nem tartoznak a Parser névtér hatókörébe. Lexer::get_token(). Névterek és kivételek 227 a Parser tagja. bosszantó lehet állandóan minõsíteni névterének nevével. return Lexer::number_value. amellyel egy adott helyen kijelentjük. // ')' lenyelése return e. if (Lexer::get_token() == Lexer::ASSIGN) v = expr(true). case Lexer::LP: { double e = expr(true).8. Ha azonban a Lexer minõsítõt nem tesszük ki. case Lexer::NAME: { double& v = table[Lexer::string_value]. // elemi szimbólumok kezelése // a Lexer get_token-jének használata // a Lexer curr_tok-jának használata // az Error error-jának használata . default: return Error::error("elemi szimbólum szükséges").

if (curr_tok != Lexer::RP) return error(") szükséges"). default: return error("elemi szimbólum szükséges"). // ')' lenyelése return e. case Lexer::LP: { double e = expr(true). // a Lexer get_token-jének használata // a Lexer curr_tok-jának használata // az Error error-jának használata } . A mi esetünkben azonban az elemzõ minden függvénye ugyanazokat a neveket használja a többi modulból. return v. get_token(). case Lexer::NAME: { double& v = table[Lexer::string_value].228 Alapok if (get) get_token(). double term(bool). if (get_token() == Lexer::ASSIGN) v = expr(true). } } A using direktíva egy lokális szinonímát vezet be. A lokális szinonímákat általában célszerû a lehetõ legszûkebb hatókörrel használni. } case Lexer::MINUS: // mínusz elõjel return -prim(true). így a using deklarációkat elhelyezhetjük a Parser névtér meghatározásában is: namespace Parser { double prim(bool). double expr(bool). using Lexer::curr_tok. hogy elkerüljük a tévedéseket. using Lexer::get_token. } case Lexer::END: return 1. return Lexer::number_value. using Error::error. switch (curr_tok) { case Lexer::NUMBER: // lebegõpontos konstans get_token().

hogy pontosan olyanok legyenek. ha célunk az. Using direktívák Mit tehetünk. } Azt is megtehetnénk. for (. Azért hagyjuk õket minõsített alakban. Névterek és kivételek 229 Így a Parser függvényeit majdnem az eredeti változatukhoz (§6. default: return left. kevésbé moduláris változatát névtereket használva alakítsuk át. mintha azokat a névterükön kívül vezettük volna be (§8. case Lexer::DIV: if (double d = prim(true)) { left /= d.1. using namespace Lexer. hogy emlékeztessenek. hogy a lexikai szimbólumok (token. } return error("osztás 0-val"). break. nyelvi egység) neveit a Parser névtérbe is bevezetjük. a Parser a Lexer-re támaszkodik.. // a Lexer összes nevét elérhetõvé teszi // az Error összes nevét elérhetõvé teszi } .8): namespace Parser { double prim(bool).8.3. A using direktíva majdnem ugyanúgy teszi elérhetõvé egy névtér neveit.2. double term(bool). using namespace Error.1) hasonlóra egyszerûsíthetjük: double Parser::term(bool get) { double left = prim(get).2. 8. double expr(bool). hogy a Parser függvényeit annyira leegyszerûsítsük.) // szorzás és osztás } switch (curr_tok) { case Lexer::MUL: left *= prim(true). mint eredeti változataik? Egy nagy program esetében ésszerûnek tûnik. break. hogy egy elõzõ.

ha kerüljük õket.3. for (. ahogy azt eredetileg tettük (§6..1).1): double Parser::term(bool get) { double left = prim(get).9).2. break. // az Error-beli error default: return left. case DIV: // a Lexer-beli DIV if (double d = prim(true)) { left /= d. } return error("osztás 0-val"). } . A globális using direktívák a nyelv régebbi változatairól való átállásra szolgálnak (§8. amit a Parser a felhasználó programelem számára nyújt.) // szorzás és osztás } switch (curr_tok) { // a Lexer-beli curr_tok case MUL: // a Lexer-beli MUL left *= prim(true).230 Alapok Ez lehetõvé teszi számunkra. 8. A Parser felülete a felhasználó elemek számára sokkal egyszerûbb kellene. Több felület használata Világos. hogy a Parser számára létrehozott névtér nem a felület.3. hogy a Parser függvényeit pontosan úgy írjuk meg.2.1.4.2. Inkább olyan deklarációhalmaznak tekinthetjük. függvényekben jelölésbeli segítségként vehetõk biztonságosan igénybe (§8. ami az egyes elemzõ függvények kényelmes megírásához szükséges. break. hogy legyen: namespace Parser { double expr(bool). } A using direktívák a névterekben más névterek beépítésére használhatók (§8. egyébként jobb.8).

using Error::error. double expr(bool). using Lexer::get_token.8. amit az elemzõ a felhasználó programelem rendelkezésére bocsát Ennek értelmében a main() vezérlõkód csak a következõt kell. . Láthatjuk. using Lexer::curr_tok. ahol az a legmegfelelõbb. } // felhasználói felület Bármelyik felületet is találtuk a legjobbnak az elemzõ függvények közös környezetének ábrázolására. hogy a Parser névtér két dolgot nyújt: [1] Közös környezetet az elemzõt megvalósító függvények számára [2] Külsõ felületet. Névterek és kivételek 231 Szerencsére a két névtér-meghatározás együttesen létezhet. double term(bool). a függvényeknek látniuk kell azt: namespace Parser { double prim(bool). hogy lássa: namespace Parser { double expr(bool). így mindkettõ felhasználható ott. // felület a megvalósításhoz } // a Lexer get_token-jének használata // a Lexer curr_tok-jának használata // az Error error-jának használata Ábrával: Parser' Parser Driver Parser megvalósítás A nyilak „a … által nyújtott felületen alapul” viszonyokat fejezik ki.

hogy a modulokat használó függvényeket (ebben az esetben a Parser-t használó main()-t) elkülönítsük az ilyen módosításoktól. A külön nevek hiánya nem okozhat zavart. és mert a program fizikai elrendezése (lásd §9. akkor bármi. Felülettervezési módszerek A felületek célja az. amin a fordítóprogram átsiklott. hogy a lehetséges mértékig csökkentsék a programok különbözõ részei között fennálló függõségeket. mert a legtöbb definíció a legtöbb kód számára nem bír jelentõséggel.3-at) természetesen különbözõ (fájl)neveket ad. Szándékosan választottam. nem C++ azonosító.3) tárgyalására is. bármitõl függhet. hogy ellenõrizze a névtér két definiciójának következetességét. a szerkesztõprogram pedig észreveszi a legtöbb olyan hibát. könnyebben módosítható és gyorsabban lefordítható. Korábbi definícióinkat adottnak véve vegyük a következõt: . sokkal gyakrabban változna. ha erre nincs mindig lehetõsége. mint a felhasználók által látható felület. A fordítóprogram azonban rendszerint akkor is megpróbálja ellenõrizni az összefüggéseket. Fontos. hogy a fordítóprogramok és a programozók az alábbi egyszerû hozzáállással viszonyulnak hozzájuk: „ha egy definíció az X pontról látható (a hatókörben van).232 Alapok A Parser’ (Parser prime) a felhasználó programelemek számára nyújtott szûk felület.4.2. megtehetnénk. Az itt bemutatott megoldást használom a fizikai modularitás (§9. hogy a két felület neve nem különbözik. amikor nincsenek további logikai megszorítások (lásd még §8. Persze a helyzet általában nem ennyire rossz. A felületek megtervezése az egyik legalapvetõbb tevékenység. ami az X pontban van leírva. valójában mit próbálunk megvalósítani.7-et). ennek a felületnek nincs külön neve a programban. de kétélû fegyver. A programozói felület nagyobb a felhasználóknak nyújtottnál. és ezt ajánlom arra az esetre is. fontos emlékeznünk arra. és több megoldást is kipróbálni. de ha akarnánk. ami abban a definícióban lett meghatározva”. Következésképpen érdemes végiggondolni. mert a programozók az egyes felületek számára különbözõ és maguktól értetõdõ neveket találnak ki.2. Az itt bemutatott megoldás az általunk megtekintettek közül a legegyszerûbb és gyakran a legjobb.1. A két felület ábrázolására nem kell önálló névtereket használnunk. melynek adatrejtési tulajdonságai jobbak. Amikor a függõségeket nézzük. valamint hogy a fordítóprogram számára nem áll rendelkezésre elegendõ információ. hogy jelöljem. A kisebb felület könnyebben érthetõ rendszerhez vezet. Legfõbb gyengéje. Ha ez a felület egy valódi rendszer valóságos méretû moduljának felülete lenne. 8.

} int main() { // .8. de idõre.. a következõt látjuk: . // . ami teljesen ésszerû megközelítés. } namespace Parser_interface { using Parser::expr. Névterek és kivételek 233 namespace Parser { // . számolgatásra stb. hogy ahol elõfordulhat függõség. ott elõ is fordul.. van szükség ahhoz. // .. hogy úgy fejezzük ki programunkat... ha egy pillantást vetünk a függõségek ábrájára. } // felület a megvalósításhoz A main() függvény csak a Parser::expr() függvénytõl függ. // .. double expr(bool). hogy erre rájöjjünk. Mégis.. Következésképpen a valóságos méretû programok esetében a programozók és a fordítási rendszerek többnyire „biztosra mennek” és feltételezik... gondolkodásra. Célunk ezért az. double expr(bool). hogy a Parser_interface-t használó programelemek kizárólag – és csupán közvetett módon – a Parser::expr() függvénytõl függnek. Elõször megpróbáljuk a magától értetõdõt: a már meglévõ megvalósítási felület segítségével az elemzõ számára felhasználói felületet határozunk meg: namespace Parser { // . } // felület a megvalósításhoz // felület a felhasználóknak Nyilvánvaló. hogy a lehetséges függõségek halmazát a valóban érvényben levõ függõségek halmazára szûkítjük. Parser::expr(false)....

így megszorítjuk a Parser_interface függõségét a Parser-tõl. Még a függõség ilyen megjelenése sem kívánatos. úgy. hogy a megvalósítási felületnek csak az elemzõ számára lényeges részét (ezt korábban Parser'-nek neveztük) tesszük láthatóvá ott. } // felület a felhasználóknak // eltérõ nevû felület a felhasználóknak Ábrával: Parser' Parser Parser_interface Driver Parser megvalósítás . } namespace Parser_interface { using Parser::expr. ahol a Parser_interface-t meghatározzuk: namespace Parser { double expr(bool).234 Alapok Parser Parser_interface Driver Parser megvalósítás Most a Driver (a vezérlõ) tûnik sebezhetõnek a Parser felület változásaival szemben. pedig azt hittük. jól elszigeteltük tõle.

2. hogy kiegészül a Parser_interface névtérrel. hogy meghatározhassuk a Parser_interface-t. Ez a megoldás csak abban különbözik a §8. Ha akarnánk. Névterek és kivételek 235 A Parser és a Parser' egységességét biztosítandó. Csak ott kell „láthatónak” lennie. az egyetlen fordítási egységen dolgozó fordítóprogram helyett ismét a fordítási rendszer egészére támaszkodunk. } Az utóbbi változatot ábrával így szemléltethetjük: Parser_interface Parser Parser_interface megvalósítás Driver Parser megvalósítás A függõségeket ezzel a lehetõ legkevesebbre csökkentettünk. Mindent kifejtettünk és megfelelõen elneveztünk.8. ahol a Parser_interface::expr() függvényt kifejtjük: double Parser_interface::expr(bool get) { return Parser::expr(get). . Mégis. } Most a Parser-nek nem kell a hatókörben lennie.4-ben szereplõtõl. a Parser_interface-t egy saját expr() függvénnyel konkrétan is ábrázolhatnánk: namespace Parser_interface { double expr(bool). ezt a megoldást a legtöbb esetben túlzónak találhatjuk.

int f(int). int f(int). igen nehéz lesz a programot különálló részekbõl létrehozni. hogy az önállónak feltételezett részek mindegyike ugyanazokat a neveket használja.5. ha minõsítõket (§8.2) vagy using direktívákat (§8.1). hogy védekezzünk a lehetséges névütközésekkel szemben. Ha csak egyetlen globális hatókört használunk.h-t is. Az a probléma merülhet fel. A célunk az.h: char f(char).. class String { /* ..3) használunk. hogy mindkét deklarációhalmazt saját.236 Alapok 8.2.2.h-t és a your. így amikor egyetlen programban egyesítjük azokat. double f(double).2.1. hogy felületet nyújtsunk a felhasználóknak: . */ }. */ }. // your. */ }.. Ha a fentieket meghatározzuk. Ez gyakran fontos gyakorlati jelentõséggel bír. A kézenfekvõ megoldás. A legegyszerûbb eset.. double f(double). } namespace Your { char f(char).. Vegyük a következõt: // my. Névtelen névterek Gyakran hasznos deklarációk halmazát névtérbe helyezni. külön névtérbe helyezzük: namespace My { char f(char). hogy a kód helyileg maradjon érvényes. class String { /* .2..2.h: char f(char). class String { /* . class String { /* . egy harmadik személy csak nehezen használhatja egyszerre a my. A névütközések elkerülése A névterek logikai szerkezetek kifejezésére valók. pusztán azért..5. nem pedig az. 8.. using deklarációkat (§8. } Most már alkalmazhatjuk a My és a Your deklarációit. */ }. a nevek ütközni fognak. amikor két személy által írt kódot kell megkülönböztetnünk.

A különbözõ fordítási egységekben lévõ névtelen névterek mindig különbözõek. */ } int g() { /* . Ilyen esetben a névteret névtelenül hagyhatjuk: #include "header. amely véletlenül ütközhet valaki más neveivel. Nevek keresése Egy T típusú paraméterrel rendelkezõ függvényt általában a T-vel azonos névtérben szokás megadni.. */ } int g() { /* . Ahogy azt szerettük volna.6. */ } } Világos..h" namespace { int a. */ } } Mivel nem akarjuk..2. A névtelen névtérhez tartozik egy rejtett using direktíva is. void f() { /* . Az elõzõ deklaráció egyenértékû a következõvel: namespace $$$ { int a.. */ }. */ } } using namespace $$$. Következésképpen ha egy függvényt nem találunk meg használati környezetében.....8. akkor paramétereinek névterében fogjuk keresni: namespace Chrono { class Date { /* . */ } int g() { /* . hogy egy névtelen névtér egy tagját egy másik fordítási egységbõl megnevezhessük. nincs mód arra.. . void f() { /* .. 8. nem érdemes olyan felesleges globális nevet kitalálni.. ahol a névteret meghatároztuk.. amely egyedi abban a hatókörben. hogy kell lennie valamilyen módszernek arra is.h" namespace Mine { int a. Névterek és kivételek 237 #include "header. hogy a Mine név „ismert” legyen az adott környezeten kívül is. void f() { /* .. hogy kívülrõl férhessünk hozzá egy névtelen névtér (unnamed namespace) tagjaihoz.. Itt $$$ valamilyen név.

4) esetében.. az (==-t stringekre meghatározó) std névtérben. } // Chrono::format() // hiba: a hatókörben nincs format() Ez a keresési szabály – a minõsítõk használatával ellentétben – sok gépeléstõl kíméli meg a programozót. std::string t = format(i). hogy maga a névtér a hatókörben kell.4-et.8. Létezik egy std::operator==()..2. ahol a minõsítõk használata nagyon fárasztó lehet. Lásd még §11. a hívás hatókörében... std::string s) { if (d == s) { // . illetve az egyes paraméterek névterében (beleértve a paraméterek osztályát és alaposztályát is) keresi.. Természetesen egy függvény több névtérbõl is kaphat paramétereket: void f(Chrono::Date d. a fordító a d==s hívásnál az operator==-t az f()-et körülvevõ hatókörben.2.4) és a sablonparaméterek (§C.238 Alapok bool operator==(const Date&.13.4). } else if (d == "1914 augusztus 4") { // . és minden talált függvényre elvégzi a túlterhelés feloldását (§7. és nem is „szennyezi” úgy a névteret. mint a using direktíva (§8. a függvényt pedig csak akkor találhatjuk meg és használhatjuk fel.2. de ennek nincs Date paramétere. hogy legyen. std::string format(const Date&). const std::string&).3). Nevezetesen. } } Az ilyen esetekben a függvényt a fordítóprogram a szokásos módon.. . // string ábrázolás } void f(Chrono::Date d. ezért a Chrono::operator==()-t használja. ha elõbb bevezettük. és a Chrono névtérben keresi. // . int i) { std::string s = format(d). Alkalmazása különösen fontos az operátorok operandusai (§11. amelynek viszont van. Vegyük észre.

4). Lib::String s5 = "Sibelius"..2. Névtér-álnevek Ha a felhasználók névtereiknek rövid neveket adnak. ATT::String s3 = "Grieg".8.2. Névterek és kivételek 239 Amikor egy osztálytag meghív egy névvel rendelkezõ függvényt. A::String s2 = "Nielsen".1. a különbözõ névterek nevei könynyebben ütközhetnek: namespace A { // .. A névtér-álnevek azt is lehetõvé teszik a felhasználónak.7. American_Telephone_and_Telegraph::String s4 = "Nielsen". hogy „a könyvtárra” hivatkozzon és egyetlen deklarációban határozza meg. Lib::set s. Valódi kódban viszont általában nem célszerû hosszú névtérneveket használni: namespace American_Telephone_and_Telegraph { // . ATT::String s4 = "Nielsen". §11. valójában melyik könyvtárra gondol: namespace Lib = Foundation_library_v2r11. az osztály és bázisosztályának tagjai elõnyben részesülnek azokkal a függvényekkel szemben.2. } // túl hosszú American_Telephone_and_Telegraph::String s3 = "Grieg".. } // rövid név.. A dilemmát úgy oldhatjuk fel.. (elõbb-utóbb) ütközni fog A::String s1 = "Grieg". ha a hosszabb névtérneveknek rövid álneveket (alias) adunk: // használjunk névtér-álneveket a nevek rövidítésére: namespace ATT = American_Telephone_and_Telegraph. melyeket a fordítóprogram a paraméterek típusa alapján talált.. Az operátoroknál más a helyzet (§11. // . 8. .

Az újrafordítás észre fogja venni a forrásszintû összeférhetetlenségeket... void g(Vector<String>& vs) { // . Másrészrõl. } // megtalálja a My_lib::His_string::String nevet . const String&).. hogy közvetlenül Lib-et használunk a Foundation_library_v2r11 helyett.. // . void my_fct(String&). // . } Ennek alapján – a My_lib névteret használva – már megírhatjuk a programot: void f() { My_lib::String s = "Byron". Névterek összefûzése Egy felületet gyakran már létezõ felületekbõl akarunk létrehozni: namespace His_string { class String { /* . // . // .. a (bármilyen típusú) álnevek túlzott használata zavart is okozhat. String operator+(const String&. void fill(char).. */ }. using namespace Her_vector. 8.. a Lib álnév értékének módosításával és a program újrafordításával a „v3r02” változatra frissíthetjük a könyvtárat.. } using namespace My_lib... Azáltal.. my_fct(vs[5]).240 Alapok Ez nagymértékben egyszerûsítheti a könyvtárak másik változatra történõ cseréjét.2. */ }.8.. } namespace Her_vector { template<class T> class Vector { /* .. const char*).. } namespace My_lib { using namespace His_string. String operator+(const String&.

1) együtt – komoly támogatást nyújtanak ehhez.. Az itt és a következõ részekben bemutatott összefûzési. 2.. 8. amely csak azokat a neveket tartalmazza. melyeket szeretnénk. const char*). const String&). 3. Egy elem valódi névterét csak akkor kell tudnunk.2.. String operator+(const String&. hogy olyan névtér-deklarációt írunk. a String jelentése My_lib::String. logikailag összetartozó szolgáltatások halmazát fejezi ki. String operator+(const String&..2. } void His_string::fill(char c) { // . } void My_lib::my_fct(String& v) { } // .8. Névterek és kivételek 241 Ha az említett névtérben egy explicit módon minõsített név (mint a My_lib::String) nem bevezetett.. amely csak magát a String-et és az összefûzõ operátort nyújtja: namespace His_string { // csak egy része a His_string-nek class String { /* . a fordító a nevet a using direktívákban szereplõ névterekben (például His_string) fogja keresni. nem ad hozzáférést a nem kapcsolódó szolgáltatásokhoz. beépítési módszerek –az #includedal (§9.1. } . Például megadhatnánk a His_string azon változatát. ha valamit megakarunk határozni: void My_lib::fill(char c) { // . Ezt meg tudnánk tenni úgy is. ami // His_string::String Ideális esetben egy névtér 1. Kiválasztás Alkalmanként elõfordul..8. és nem ró nagy jelölésbeli terhet a felhasználóra. // hiba: a My_lib-ben nincs megadva fill() // rendben: fill() szerepel a His_string-ben // rendben... hogy egy névtérbõl csak néhány névhez akarunk hozzáférni. */ }.

using namespace Her_lib. Fordítva is igaz: ha a His_string-bõl eltávolítunk egy szolgáltatást vagy megváltozatjuk a His_string felületét.2. // . Az adott névtérben szereplõ szolgáltatások kiválasztását jobban ki lehet fejezni using deklarációkkal: namespace My_string { using His_string::String. 8.. Ezek révén úgy adhatunk hozzáférést különféle eszközökhöz. akkor ez a változtatás automatikusan hozzáférhetõ lesz a My_string-et használó elemek számára. amelyre a legtöbb valódi programban szükségünk van.2... Összefûzés és kiválasztás A (using direktívákkal történõ) összefûzés és a (using deklarációkkal történõ) kiválasztás összekapcsolása azt a rugalmasságot eredményezi. */ }. // minden a His_lib-bõl // minden a Her_lib-bõl . } // bármelyik His_string-beli + használható A using deklaráció az adott név minden deklarációját a hatókörbe helyezi.. Így ha a His_string-et úgy módosítják. template<class T> class Vector { /* . hogy egy tagfüggvényt vagy az összefûzõ mûvelet egy túlterhelt változatát adják a String-hez. } namespace My_lib { using namespace His_lib. */ }.2. így például egyetlen using deklarációval egy túlterhelt függvény összes változatát bevezethetjük. */ }... A His_string „valódi” meghatározásának módosítása ebben a deklarációban nem fog tükrözõdni. hacsak nem mi vagyunk a His_string tervezõi vagy „karbantartói”.... hogy feloldjuk az egybeépítésükbõl adódó névütközéseket és többértelmûségeket: namespace His_lib { class String { /* ... } namespace Her_lib { template<class T> class Vector { /* ..242 Alapok Ez azonban könnyen zavarossá válhat. a fordítóprogram fel fogja ismerni a My_string minden olyan használatát. */ }.2). // . using His_string::operator+. amelyre ez hatással van (lásd még §15. class String { /* .8.

. template<class T> class List { /* . Névterek és régi kódok Sok millió sor C és C++ kód támaszkodik globális nevekre és létezõ könyvtárakra. // továbbiak // . // az esetleges ütközések feloldása a His_lib javára // az esetleges ütközések feloldása a Her_lib javára // továbbiak } Amikor megvizsgálunk egy névteret. vagy egyszerûen jó. */ }.. typedef Her_lib::String Her_string. Névterek és kivételek 243 using His_lib::String. melyeket más hatókörökbõl tettünk hozzáférhetõvé a using direktívával (lásd még §C. using His_lib::String.2. hogy csökkentsük az ilyen kódokban lévõ problémákat? A már létezõ kódok újraírása nem mindig járható út. Hogyan használhatjuk a névtereket arra. amikor új névtérbe teszem azokat.. a névtérben lévõ. using namespace Her_lib. using Her_lib::Vector. template<class T> class List { /* . ha van egy új nevünk: namespace Lib2 { using namespace His_lib.. függetlenül attól..1-et). kifejezetten megadott nevek (beleértve a using deklarációkkal megadottakat is) elõnyben részesülnek azokkal a nevekkel szemben.. } Az átnevezésre nincs külön nyelvi eljárás. Néha azonban új névre van szükség.10. Következésképpen a My_lib–et használó elemek számára a String és Vector nevek ütközését a fordítóprogram a His_lib::String és Her_lib::Vector javára fogja feloldani. // .. */ }. Rendszerint jobban szeretem változatlanul hagyni a neveket. using Her_lib::Vector. hogy szerepel-e List a His_lib vagy Her_lib névtérben. Szerencsére a C könyvtárakat úgy is .. Ehelyett az új elemek meghatározására való általános módszerek használatosak. Ily módon nem kell ugyanannak az elemnek két különbözõ nevére emlékeznem.. // minden a His_lib-bõl // minden a Her_lib-bõl // az esetleges ütközések feloldása a His_lib javára // az esetleges ütközések feloldása a Her_lib javára // átnevezés template<class T> class His_vec // "átnevezés" : public His_lib::Vector<T> { /* .8. Továbbá a My_lib::List lesz használatos alapértelmezés szerint.. 8. */ }.9.

244 Alapok használhatjuk..h: namespace std { // . int printf(const char* . világ!\n").9.. } .. készítettünk egy új fejállományt is.. Névterek és a C Vegyük a hagyományosan elsõ C programot: #include <stdio. A stdio...h> int main() { printf("Helló. Ez megõrzi a visszirányú kompatibilitást. } using namespace std. akik nem akarják. Emiatt a névterekre vonatkozó nyelvi szabályokat úgy határozták meg. A C++-ban írt könyvtárak esetében ez nem így van (§9.h C fejállományban lévõ szabványos bemeneti/kimeneti szolgáltatások deklarációi például egy névtérbe kerültek. } Ezt a programot nem lenne jó ötlet széttördelni.. hogy a lehetõ legcsekélyebb károkozással be lehessen azokat építeni a régebbi C++ programokba is. )..2. másrészrõl viszont a névtereket úgy tervezték.2. a cstdio-t: // cstdio: namespace std { // .. // . hogy a nevek automatikusan hozzáférhetõk legyenek.1). a következõképpen: // stdio... 8. Tulajdonképpen erre példa a számológép program (§6. int printf(const char* . Azoknak viszont. Az sem ésszerû. ). hogy viszonylag könnyedén lehessen egy névterek nélkül megírt program szerkezetét névterek használatával világosabban kifejezni. Ennek megvalósításához a kulcs a using direktíva..4).1. // . ha a szabványos könyvtárakat egyedi megoldásoknak tekintjük. mintha azokat egy névtérben deklarálták volna.

hogy a tényleges programkódot megváltoztatnánk: . Például: // old A. Ez alapvetõ ahhoz.2.. hogy a már meglévõ könyvtárakat a forráskód lehetõ legkisebb módosításával fejleszthessük névtereket használóvá.9.h: void f(int).h-t természetesen úgy fogják meghatározni.2. sokkal világosabban ki lehet fejezni minõsítésekkel és using deklarációkkal. hogy beleveszik a cstdio-t: // stdio. a stdio.. // old user. A névterek és az összeszerkesztés közötti kapcsolatot a §9.8. // f()-et hívja B.h" #include "B. A using direktívákat elsõdlegesen a nyelv régebbi változatairól való átállást segítõ eszközöknek tekintem.h: #include<cstdio> using namespace std.4) névtereken keresztül mûködik. Névterek és kivételek 245 A C++ standard könyvtárának azon felhasználói.h: void f(char). A legtöbb olyan kódot. 8..2. // old B. akik aggódnak a deklarációk másolása miatt. // .h" void g() { } f('a'). amely más névtérben lévõ nevekre hivatkozik..c: #include "A. Névterek és túlterhelés A túlterhelés (§7.h-ból Ezt a programot anélkül alakíthatjuk névtereket használó változatra. // .4 részben tárgyaljuk.

Hogy ezt megtehessük. // f()-et hívja B.3. azaz számos névtér deklarációjából adhatunk hozzájuk neveket: namespace A { int f()..2.c: #include "A. a névtér-meghatározásokat szét kell osztanunk számos fejállomány és forrásfájl kö- ..h: namespace A { void f(int). A névterek nyitottak A névterek nyitottak. using namespace B. // . } // new B.9.h: namespace B { void f(char). // .h" #include "B. a using direktívákat a fejállományokba tettük volna.h" using namespace A..h-ból Ha teljesen változatlanul akartuk volna hagyni a user. } // new user. } // most f() az A tagja // most A két tagja f() és g() Ezáltal úgy hozhatunk létre egyetlen névtéren belül lévõ nagy programrészeket.246 Alapok // new A. void g() { } f('a').. } namespace A { int g().c-t. 8. ahogy egy régebbi könyvtár vagy alkalmazás élt az egyetlen globális névtéren belül.

... // saját függvény // .. Ez azonban gyakran kivitelezhetetlen.) mutattuk. #include<stdio. a fordítóprogram a fentivel azonos jellegû hibát az újra megnyitott névterekben már nem érzékeli: .8).4. // saját függvény // . hogy a deklarációk sorrendjét megváltoztatnánk: // saját fejállomány: namespace Mine { void f(). a névterek nyitottsága lehetõvé teszi számunkra.. Például a // saját fejállomány: void f()..2..h> namespace Mine { int g(). újraírható anélkül. hogy a különbözõ programelemeknek különbözõ felületeket nyújtsunk azáltal. mint igazán nagy programrészeket egyetlen névtérbe rakni. hogy újra megnyitnánk a Mine-t: void Mine::ff() { // . jobban szeretek sok kisebb névteret használni (lásd §8. // saját függvény // . biztonságosabb a Mine:: utasításformát használni ahelyett.2. Ez a nyitottság szintén a nyelv régebbi változatairól való átállást segíti. Névterek és kivételek 247 zött.. } // hiba: nincs ff() megadva Mine-ban A fordítóprogram ezt a hibát észreveszi.8. ha nagyobb programrészeket alakítunk át névtereket használó változatra. Amikor egy névtér elõzetesen bevezetett tagját kifejtjük. // saját függvény // .. Ahogy azt a számológép példájában (§8. Mivel azonban egy névtéren belül új függvényeket is meghatározhatunk.h> int g(). hogy egy adott névtér különbözõ részeit mutatjuk meg nekik.. } #include<stdio. } Amikor új kódot írok.

A meghatározásokban szereplõ nevek minõsítésére használhatunk névtér-álneveket (§8. A könyvtár létrehozója felismerheti a futási idejû hibákat. de általában nem tud mit kezdeni velük. A könyvtárt felhasználó programelem tudhatná.. . hogyan birkózzon meg a hibákkal. hogy nincs lehetõségünk a számológép egészét megtervezni és nem akarjuk. Ehelyett tegyük fel. hogy nem tudták. A fordítóprogram nem tudhatja. 2. ezzel a definícióval adjuk hozzá // .3. miután hiba történt. az olyan hibaesemények jelzésére.2). hogy minden névtér függ az Error névtértõl (§8. de az adott névtér újbóli megnyitására nem. Kivételek Ha egy program különálló modulokból áll – különösen ha ezek külön fejlesztett könyvtárakból származnak –. hogy a program egészét egyszerre terveztük meg.. hogy nem egy új ff() függvényt akartunk meghatározni. Tegyük fel.2. hogy a elemzõt és a többi részt úgy írták meg.2. 8. de nem képes észlelni azokat – máskülönben a felhasználó kódjában szerepelnének a hibákat kezelõ eljárások és nem a könyvtár találná meg azokat.. az Error-ban lévõ hibakezelõ pedig arra támaszkodik. A számológép példájában ezt a problémát azzal kerültük ki. hogyan szeretné a vezérlõ kezelni a hibákat. látjuk. // hoppá! nincs ff() megadva Mine-ban. Amikor azonban a számológép logikai részeit különbözõ névterekre bontjuk szét. illetve a máshol észlelt hibák kezelésére.248 Alapok namespace Mine { // Mine újra megnyitása függvények meghatározásához void ff() { } } // .. hogy az Error és a többi modul között szoros legyen a kapcsolat. hogy minden modul megfelelõen viselkedik. melyeket nem lehet helyben megszüntetni. ezáltal beilleszthettük a hibakezelést a teljes szerkezetbe. a hibakezelést két különálló részre kell szétválasztanunk: 1.7).

je- . illetve mit várhat tõle. Ebben a részben röviden leírjuk a kivételeket. amellyel nem képes megbirkózni. olyan alapértelmezett értéket ad.8. A hibajelzés és a hibakezelés szétválasztására szánt C++ eszköz a kivétel. } // lásd §22. Az alapgondolat az. §10. amit külön fejlesztett könyvtárakból hoztunk létre. fejezet átfogóbban tárgyalja a kivételeket és azok használatát. no_of_errors++. Névterek és kivételek 249 Bár az error() nagyon egyszerû volt. A 14.3) char to_char(int i) { if (i<numeric_limits<char>::min() || numeric_limits<char>::max()<i) throw Range_error(i). vagy Range_error kivételt vált ki. és egy egyszerû hibaállapotot követ nyomon. hogy ha egy függvény olyan problémát talál. hogy a program minden része tudjon az error() létezésérõl és arról. hogy segítsenek megoldani a hibák jelzését: struct Range_error { int i.2. magában foglalt egy hibakezelési módszert: namespace Error { int no_of_errors. throw). } }.2 A to_char() függvény vagy az i számértékét adja vissza karakterként. Ha egy függvény képes erre.1. kivételt vált ki („kivételt dob”. return 1. } } Az error() függvény egy hibaüzenetet ír ki. hogy folytassa a számolást. hogyan lehet meghívni. Range_error(int ii) { i = ii. ahogy a számológép példájában lennének használatosak. Fontos. double error(const char* s) { std::cerr << "hiba: " << s << '\n'.5. azt remélve. abban a környezetben. Ez túl sok feltétel lenne egy olyan program esetében. mely lehetõvé teszi a hívó számára.3. return i. hogy (közvetett vagy közvetlen) meghívója képes kezelni a problémát. // konstruktor (§2. „Dobás és elkapás” A kivételeket (exception) arra találták ki. 8.2.

} } A catch ( /* . de megnevezheti az elkapott objektumot is.250 Alapok lezheti. Nem kötelezõ. } catch (Range_error x) { cerr << "hoppá: to_char(" << x.. } catch (Range_error) { cerr << "hoppá\n".. amely a függvényparaméterek deklarációjához hasonló módon használatos. vagy közvetlenül egy másik kivételkezelõ után.. A zárójelek olyan deklarációt tartalmaznak.i << ")\n". amit esetleg kiválthat. A catch szintén kulcsszó. Csak közvetlenül olyan blokk után használható. hogy el akarja kapni (catch) azokat a kivételeket. amit a try kulcsszó elõz meg. a következõt írhatjuk: void g(int i) { try { char c = to_char(i). } szerkezetet kivételkezelõnek (exception handler) nevezzük. // . hogy meghívjuk a to_char()-t és elkapjuk azt a kivételt. akkor pontosan úgy adhatunk nevet a catch paraméterének.. A deklaráció határozza meg azon objektum típusát.. melyet a kezelõ elkaphat. ahogy a függvényparamétereket nevezzük meg: void h(int i) { try { char c = to_char(i). // . melyek típusa megegyezik a probléma jelzésére használt típussal. Ha például meg akarjuk tudni a kiváltott Range_error értékét.. Ha a kivétel típusa megegyezik a kezelõnek megadott típussal. a try blokk kezelõit kell megvizsgálni. */ ) { // . } } Ha bármilyen try blokkban szereplõ vagy onnan meghívott kód kivételt vált ki. Ahhoz például.. ..

3. Syntax_error(const char* q) { p = q.5. A kivételek megkülönböztetése Egy program futásakor általában számos hiba léphet fel. Az osztályok és a kivételkezelés közötti kölcsönhatást a 14. Az elemzõt használó programrészben megkülönböztethetjük a két kivételt. §10. Másrészt a kezelõ a nyelvi hibákról bizonyára szeretne jelzést kapni. Ha nem.7). Egy nagy programban nem lenne hatékony mód arra. A C programozók úgy gondolhatnak a kivételkezelésre. Ha az egyik kezelõ „alján kiesünk”.8. Számológépünknek (§6. mint egy közönséges blokk. ha mindkettõjük számára hozzáadunk egy-egy kezelõt a try blokkhoz. } }.3).2.1. így szükség esetén a megfelelõ kezelõbe léphetünk. amelyik felismerte a nullával való osztás kísérletét.1) kétfajta futási idejû hibát kell kezelnie: a formai követelmények megsértését és a nullával való osztás kísérletét. hogy megtaláljam a más célra használt int kivételeket.2. Ahol szükséges. Beépített típusokat. mint egy olyan. a végrehajtás a kezelõk listájának végétõl folytatódik: . a kivételkezelõket figyelmen kívül hagyjuk és a try blokk úgy viselkedik. hogy az int egy efféle eltérõ használata nem okoz-e zavart az én kódomban. „jól viselkedõ” eljárásra. A kezelõnek nem kell értéket átadni abból a kódból.2. fejezetben tárgyaljuk. mint amilyen az int. amely a setjmp/longjmp (§16. mint a vezérlés átadása a hívó függvény megfelelõ részének. a hibáról információt adhatunk a hívónak. A kényelmesebb jelölés végett a szerkezethez hozzáadtam egy konstruktort (§2. Ez a lehetõ legkisebbre csökkenti a céljukkal kapcsolatos zavart.2) használatát váltja fel. Ha a kivételt nem kapja el egyetlen try blokk sem. Én a kivételkezelés céljára külön típusokat szoktam megadni. Névterek és kivételek 251 a kezelõ végrehajtja a megfelelõ mûveletet. 8. viszont sohasem használok kivételként. A C++ kivételkezelése alapvetõen nem más. melyeket különbözõ nevû kivételeknek feleltethetünk meg. így a nullával való osztást egy egyszerû üres típussal ábrázolhatjuk: struct Zero_divide { }. a program befejezõdik (§14. Itt egy karakterláncot adunk át: struct Syntax_error { const char* p. ezért sosem lehetnék biztos abban.

.252 Alapok try { } catch (Syntax_error) { // szintaktikus hiba kezelése } catch (Zero_divide) { // nullával osztás kezelése } // akkor jutunk ide. amint „belépnek” a kezelõjükbe. A nyelv szempontjából a kivételeket rögtön kezeltnek tekintjük. ha expr() nem okozott kivételt // .... } . // . és más módon sem változtatta meg a vezérlést). A függvényeknek nem kell az összes lehetséges kivételt elkapniuk..9. A kezelõk listája némileg egy switch utasításhoz hasonlít. megfelelõ kezelõvel rendelkezõ hívót keresve. E listák formai követelményei részben ezért különböznek a case-étõl. minden kezelõ külön hatókört (§4.. */ }. expr(false). // kizárólag akkor jutunk ide. részben pedig azért.. Az elõzõ try blokk például nem próbálta elkapni az elemzõ bemeneti mûveletei által kiváltott kivételeket. // nem váltott ki kivételt. ha expr() nem okozott kivételt vagy ha egy Syntax_error // vagy Zero_divide kivételt elkaptunk (és kezelõjük nem tért vissza. de itt nincs szükség break utasításokra.. A következõ például nem okoz végtelen ciklust: class Input_overflow { /* . throw Input_overflow().4) alkot. melyek a kezelõ végrehajtása közben lépnek fel.. } catch (Input_overflow) { // . void f() { try { } // . azok csupán „keresztülmennek” a függvényen. hogy jelöljék.. ezért a try blokkot meghívó programrésznek kell foglalkoznia azokkal a kivételekkel.

3. Kivételek a számológépben Az alapvetõ kivételkezelõ eljárásokból kiindulva újraírhatjuk a §6. hogy különválasszuk a futási idõben talált hibák kezelését a számológép fõ programrészétõl. */ }.. } . // valami bonyolult } catch (XXII) { // a bonyolult kezelõ nem járt sikerrel } Ilyen – gyakran rossz stílusra utaló – egymásba ágyazott kivételkezelõket azonban ritkán írunk. try { // . } catch (XXII) { try { } } // .8.. Ez a program olyan elrendezését eredményezi.. Syntax_error(const char* q) { p = q.3. struct Syntax_error { const char* p. lazán kapcsolódó részekbõl létrehozott programokéra. void f() { // . Névterek és kivételek 253 A kivételkezelõk egymásba is ágyazhatók: class XXII { /* . Helyette az elemzõ függvények csak a hibák jelzésére használatos típusokról fognak tudni: namespace Error { struct Zero_divide { }. } }. Elõször kiküszöbölhetjük az error() függvényt. amely jobban hasonlít a különálló.... 8...1 részben szereplõ számológépet.

default: throw Error::Syntax_error("elemi szimbólum szükséges"). használata miatt (§6.254 Alapok Az elemzõ három szintaktikus hibát ismer fel: Lexer::Token_value Lexer::get_token() { using namespace std. // az input. NAME =. } } double Parser::prim(bool get) { // . A throw operátor egy értéket is átad a kezelõnek. get_token().. // elemi szimbólumok kezelése } case Lexer::LP: { double e = expr(true). vagy hiba if (isalpha(ch)) { input->putback(ch). if (curr_tok != Lexer::RP) throw Error::Syntax_error("')' szükséges"). } throw Error::Syntax_error("rossz szimbólum"). return curr_tok=NAME. input->putback(ch). // ')' lenyelése return e. a kezelõnek egy Syntax_error objektumot ad át. a throw-t használja arra. string_value = ch. } Ha az elemzõ ilyen hibát talál. default: // NAME. amely a primary expected karakterláncra hivatkozó mutatót tartalmazza. while (input->get(ch) && isalnum(ch)) string_value.7) // .. *input >> string_value.push_back(ch). } case Lexer::END: return 1. isalpha(). return curr_tok=NAME. amelyet valamilyen (közvetett vagy közvetlen) hívó függvény határoz meg.. hogy átadja a vezérlést egy kezelõnek. .1.. stb. Például a throw Syntax_error("elemi szimbólum szükséges").

. } Ha nem történt hiba a PRINT (azaz sorvége vagy pontosvesszõ) szimbólummal lezárt kifejezés végén. } } if (input != &cin) delete input. } catch(Error::Zero_divide) { cerr << "nullával osztás kísérlete\n". cout << Parser::expr(false) << '\n'. Most már elkészíthetjük a vezérlõt. a main() meghívja a skip() helyreállító függvényt. } catch(Error::Syntax_error e) { cerr << "formai hiba:" << e. if (Lexer::curr_tok != Lexer::PRINT) skip(). while (*input) { try { Lexer::get_token(). Névterek és kivételek 255 A nullával való osztás hibájának jelzéséhez nem szükséges semmilyen adatot átadni: double Parser::term(bool get) // szorzás és osztás { // . if (Lexer::curr_tok == Lexer::PRINT) continue. a no_of_errors és az input kézenfekvõ választás a Driver névtér számára: . if (Lexer::curr_tok == Lexer::END) break. break. } // . amíg sorvégét vagy pontosvesszõt nem talál. azáltal.p << "\n".. A skip() az elemzõt egy meghatározott állapotba próbálja állítani. if (Lexer::curr_tok != Lexer::PRINT) skip(). return no_of_errors.8. case Lexer::DIV: if (double d = prim(true)) { left /= d. A skip() függvény. char* argv[ ]) { // .. hogy az kezelje a Zero_divide és Syntax_error kivételeket: int main(int argc.. hogy eldobja a karaktereket addig.. } throw Error::Zero_divide()..

3. A globális main() a program indító függvénye (§3. Ez a megközelítés nem felel meg olyan programok esetében. mint a kivételeket használó változat. Egy valóságos méretû programban a main() kódjának legnagyobb részét a Driver egy külön függvényébe tenném át. hogy megszámoljuk a hibákat. melyeket külön fejlesztett könyvtárakból hoztak létre. bool in_error = false. Felvetõdhet.. // rossz stílus . std::istream* input. még akkor is. egy állapotváltozó bevezetésével küszöböljük ki: int main(int argc. switch (ch) { case '\n': case '. } void Driver::skip() { no_of_errors++. miközben éppen az elemzõ kivételeinek kezelését végzik.256 Alapok namespace Driver { int no_of_errors. hogy a program részeit szorosan összekapcsolta. A main()-t nem tesszük a Driver névtérbe. while (*input) { char ch. ha a program képes volt helyreállni a hiba után. így a main() egy névtéren belül értelmetlen. } // karakterek elvetése sortörésig vagy pontosvesszõig } } A skip() kódját szándékosan írtuk az elemzõ kódjánál alacsonyabb elvonatkoztatási szinten.3. input->get(ch).2). hogy a különálló skip() hibakezelõ függvényt a main()-ben. Gyakran hasznos tudni a hibákról.': return.1. és ez a szám lesz a program visszatérési értéke. Ezt azonban úgy érte el. void skip(). char* argv[ ]) { // . 8. Megtartottam azt az ötletet.. Más hibakezelõ módszerek Az eredeti hibakezelõ kód rövidebb és elegánsabb volt. Így az elemzõben lévõ kivételek nem kapják el.

4. mint külön hibakezelõ függvényeket adni a kódhoz. in_error = true. ++ Driver::no_of_errors. a hibakezelõ kód ugyanis megismételheti azt a hibát. } if (in_error == false) cout << Parser::expr(false) << '\n'.5[7]). §8. ha a hibakezelés elvonatkoztatási szintje megegyezik annak a kódnak az absztrakciós szintjével. Nevezetesen az in_error-t használó main()-t kevésbé olvashatónak tartom. Általában jobb külön tartani a hibakezelést és a „közönséges” kódot. 3.p << "\n". mint a skip() függvényt használó változatot. ami a hibát okozta. különösen akkor. Az állapotváltozók gyakran zavart okoznak és hibák forrásai lehetnek. (A gyakorlatok között szerepel. if (Lexer::curr_tok == Lexer::PRINT) { in_error = false. Ezt számos okból rossz ötletnek tartom: 1. A kivételkezelés nem helyi problémák megoldására való. ha lehetõséget adunk rá. continue. Több munkával jár az egész kódot módosítani a hibakezelés hozzáadásával. hogy mi történik. Ha egy hiba helyben kezelhetõ. } catch(Error::Zero_divide) { cerr << "nullával osztás kísérlete\n". } catch(Error::Syntax_error e) { cerr << "formai hiba:" << e. Veszélyes.8. akkor majdnem mindig ezt is kell tennünk. amely a hibakezelést elõször kiváltotta. hogy kivételt használjunk a „túl sok paraméter” hiba fellépésekor: . ha a main() in_error-t használ. return Driver::no_of_errors. Névterek és kivételek 257 } while (*Driver::input) { try { Lexer::get_token(). } } if (Driver::input != &std::cin) delete Driver::input. ++ Driver::no_of_errors. hogy elszaporodjanak és hatásuk nagy programrészekre terjedjen ki. if (Lexer::curr_tok == Lexer::END) break. in_error = true. 2. Például nincs ok arra.

4.2. [9] Használjunk kivételeket arra. char* argv[]) { using namespace std. [7] Használjuk a Névtér::tag jelölést. §8. [3] A névtereket úgy tervezzük meg. . §8.7.9. Tanácsok [1] Használjunk névtereket a logikai felépítés kifejezésére. §8. [8] A using namespace–t csak a C-rõl vagy régebbi C++-változatokról való átálláskor. using namespace Driver.2.2.3. } } // mint korábban A kivételek további tárgyalása a 14. anélkül. [5] Ha szükséges.3. case 2: // karakterlánc paraméter beolvasása input = new istringstream(argv[1]). §8. 8.2.2.2.. fejezetben történik.2. return 1. §8. illetve helyi hatókörben használjuk. switch (argc) { case 1: // olvasás szabványos bemenetrõl input = &cin. §8. [6] Lehetõleg ne rójunk nehéz jelölésbeli terheket névtereink felhasználóira. §8. §8.7. amelyben a hibákkal foglalkozunk.2. használjunk névtér-álneveket a hosszú névtérnevek rövidítésére. [4] Lehetõleg ne adjunk a névtereknek rövid neveket. default: cerr << "túl sok paraméter\n".258 Alapok int main(int argc. független névterekhez. §8.4. amikor a névtér tagjait meghatározzuk.2. break. hogy véletlenül hozzáférhetnénk más. break.2. §8.8. [2] A main() kivételével minden nem lokális nevet helyezzünk valamilyen névtérbe. hogy utána kényelmesen használhassuk.2. hogy a szokásos feldolgozást végzõ kódrészt elválasszuk attól a résztõl.

5[3]-ban lévõ számológépet kivételek használatával. hogy a §8. hogy a stack osztályon belül hol jött létre kivétel. §8. Névterek és kivételek 259 [10] Inkább felhasználói típusokat használjunk kivételekként. Ne felejtsük el azt az esetet. Tegyünk javaslatokat arra.9. A main()-nel kapjuk el a kivételeket és írjuk ki. hogy a könyvtár névtereket használjon. amelyek ellenõrzik a túlcsordulást és az alulcsordulást. 8. milyen hibákat vétettünk. (*2) Írjunk programot. multiply() és divide() függvényeket. Adjunk erre listára egy sort() függvényt és egy olyat. Jegyezzük fel. Próbáljuk ki úgy. amely eldönti.4-ben található Stack modul stílusában. Gyakorlatok 1.4 pont stílusában. miként kerülhetnénk el az ilyen hibákat a jövõben. ahol a hívás mélysége 10.1-ben szereplõ main() elsõ változatában.2. melyik szinten lépett fel a kivétel. (*2) Készítsünk modult a számológép programból névterek felhasználásával a §2. . amelyben egy függvény kivételt „dob”.3. mint beépített típusokat. Minden függvénynek adjunk egy paramétert. ami megfordítja a listában szereplõ karakterláncok sorrendjét. (*1) Találjuk meg a hibát a §8. 7. (*2) Írjunk függvényt. 9. amely olyan egymást hívó függvényekbõl áll. (*2. 6. Mérjük meg a két módszer futási idejének különbségét. hogy létrehozunk egy programnyelvekbõl álló listát.5. melyiket kaptuk el. (*1) Írjunk programot. Tegyünk javaslatokat arra.5) Írjuk meg a plus(). [11] Ne használjunk kivételeket. §8. Módosítsuk úgy. 10. (*2. ami nem használ névtereket.1.3. hogy megmérjük. amely vagy visszaad egy értéket. 8. 5. és kivételeket váltanak ki. 2. (*2) Módosítsuk a számológépet. 3.3. amely legalább egy olyan könyvtárat használ. 11.3.5[10] függvényeit használja. Tipp: §8. (*2) Módosítsuk a §8. ha ilyen hibák történnek.5) Írjunk string elemeket tartalmazó kétirányú láncolt lista modult a §2. egy másik pedig elkapja. miként kerülhetnénk el az ilyen hibákat a jövõben.2. vagy egy paraméter alapján eldobja azt. Ne használjunk globális using direktívákat.3.8. van-e különbség a kivételek elkapásának nehézségében attól függõen. amikor a helyi vezérlési szerkezetek is megfelelõek. milyen hibákat vétettünk.5[5] programját úgy. Jegyezzük fel. Adjunk minden függvényhez egy karakterlánc objektumot és mérjük meg újra a különbséget. amikor a kivételt a kiváltó függvényben kapjuk el. (*2) Módosítsuk a §8. 4. (*2) Vegyünk egy nem túl nagy programot. minus().

9 Forrásfájlok és programok „A formának a rendeltetéshez kell igazodnia. Külön fordítás A fájl (az egyes fájlrendszerekben) a tárolás és fordítás hagyományos egysége. Vannak olyan rendszerek. Ez a leírás azonban csak azokra a rendszerekre összpontosít.1. . amelyek a C++ programokat nem fájlok halmazaként tárolják és fordítják.” (Le Corbusier) Külön fordítás • Összeszerkesztés • Fejállományok • A standard könyvtár fejállományai • Az egyszeri definiálás szabálya • Összeszerkesztés nem C++ kóddal • Az összeszerkesztés és a függvényekre hivatkozó mutatók • Fejállományok használata a modularitás kifejezésére • Egyetlen fejállományos elrendezés • Több fejállományos elrendezés • Állományõrszemek • Programok • Tanácsok • Gyakorlatok 9. és a programok sem fájlok formájában jelennek meg a programozó számára. amelyek a fájlok hagyományos használatára támaszkodnak.

2. majd kétféle módját ismertetjük annak. A programok forrásfájlokba rendezését is ugyanaz a függõségi kapcsolat vezérli. A fordítóprogram valójában csak ezekkel dolgozik és a C++ szabályai is ezek formáját írják le.4). Az elõfeldolgozás eredményét fordítási egységnek (translation unit) hívják.262 Alapok Egy teljes programot rendszerint lehetetlen egy fájlban tárolni. amelyek biztosítják mindazt az információt. ahol meg kell különböztetni azt.1. amit a programozó lát. és amit a fordítóprogram figyelembe vesz. ami ahhoz szükséges. mint az egyetlen forrásfájlból álló programokénak. hogy a fordítóprogram kikényszerítse ezt a logikai szerkezetet. §8. ha a programot megfelelõ méretû fájlokra bontjuk. hogy a programozó lehetõvé tegye az elkülönített fordítást. A teljes összeszerkesztést el lehet végezni a program futása elõtt. A szerkesztõt néha (zavaró módon) betöltõnek (loader) is szokták nevezni.8). A felhasználó a fordítóprogramnak egy forrásfájlt (source file) ad át. hogy irányítsa. Az újrafordításra használt idõ még egy közepes méretû program esetében is jelentõsen csökkenthetõ. akkor a teljes fájlt újra kell fordítani. Ez az a program.4. Ebben a könyvben csak ott teszek különbséget a forrásfájl és a fordítási egység között. ha (bármilyen kis) változtatást hajtottak végre rajta. amelyek segítenek ezt biztosítani. Amikor a fordítási egység a fájl. segítheti az olvasót a program megértésében és segíthet abban is. mint azok névterekbõl való összeállítását. Elõször áttekintünk néhány. hogy a fordítási egységet a program többi részétõl elkülönítve lehessen elemezni. nevezetesen a szerkesztõprogram (linker). az összeszerkesztéshez kapcsolódó részletet és szakkifejezést. Ezután a fájl elõfordítása történik: azaz végrehajtódik a makrófeldolgozás (§7. hogyan lehet fájlokra szétválasztani a számológépet (§6. az #include utasítások pedig beépítik a fejállományokat (§2. A program logikai és fizikai szerkezetének azonban nem kell megegyeznie. ami összekapcsolja a külön fordított részeket. már csak azért sem. Hasznos lehet például több forrásfájlt használni egyetlen névtér függvényeinek tárolására. A program fizikai szerkezetén általában a forrásfájlokba szervezett programot értik. mert a szabványos könyvtárak és az operációs rendszer forráskódja általában nem szerepel a program forrásában. Emellett lehetõség van arra is.2. §9. ha a felhasználó saját kódját egyetlen fájl tárolja. .1. amely számos következetlenséget képes észrevenni.2). A program forrásfájlokra való fizikai szétválasztását a program logikai felépítése kell. A valóságos méretû alkalmazásokban az sem kényelmes és célszerû. vagy egy névtér definícióit több fájl között szétosztani (§8. amelytõl az elõzõ függ. hogy késõbb új kódot adjunk a programhoz („dinamikus szerkesztés”). névtér-meghatározások egy gyûjteményét egyetlen fájlban tárolni.1). A rendszerünkben vannak olyan eszközök. A program elrendezési módja segíthet kihangsúlyozni a program logikai felépítését. A több fordítási egységbõl álló programok deklarációinak ugyanúgy következetesnek kell lenniük. vagy egy másik fájlon. olyan deklarációkat kell megadnia. Ahhoz.

// file2. Ha x már rendelkezne kezdõértékkel.c: extern int x. de egyszer sem definiáltuk. osztályok. egységes legyen. b-t kétszer deklaráltuk különbözõ típusokkal. kivéve. hogy a globális vagy névtér-hatókör- .2. felsorolások és felsorolók neveit következetesen kell használni az összes fordítási egységben. névterek. linkage error) a fordítóprogram – ami egyszerre csak egy fájlt néz – nem ismeri fel. és hogy minden deklaráció. int f().c-ben az x deklarációja (csak) deklaráció és nem definíció (§4. Vegyük például a következõ két fájlt: // file1. Az effajta hibákat (szerkesztési hiba. de a típusoknak pontosan meg kell egyezniük: // file1. int f() { /* csinálunk valamit */ } // file2. amelyben szerepel. extern int c. sablonok. Jegyezzük meg. mert a kezdõértéket is meghatározó deklarációk egyben definíciónak is minõsülnek. // jelentése int x = 0. függvény stb. Egy objektumot a programban csak pontosan egyszer határozhatunk meg.c-ben lévõ g() által használt x és f() meghatározása a file1. hogy minden névtér. extern double b.c: int x = 1. A programozó feladata biztosítani. osztály. Itt három hiba van: x-et kétszer definiáltuk. Forrásfájlok és programok 263 9. c-t pedig kétszer deklaráltuk.9. void g() { x = f().9). ha kifejezetten lokálisként nem határoztuk meg azokat. amely ugyanarra az egyedre vonatkozik.c: int x = 1. Összeszerkesztés A függvények. extern int c. megfelelõen legyen deklarálva minden olyan fordítási egységben.c-ben szerepel. Deklarálni többször is lehet. a fordítóprogram az extern kulcsszót egyszerûen figyelmen kívül hagyná. } A file2. hogy a file2. változók. int b = 1.c: int x. a szerkesztõ azonban a legtöbbet igen. Az extern kulcsszó jelzi.

5. ahol meghatározásuk szerepel. §10. hogy az f() meghívása a C nyelvben nem lenne hiba (§B. } .3) –.2) vagy a szabad tárban létrehozott objektumokra (§6.2. A következõ programrészlet két hibát tartalmaz: // file1.c: inline int f(int i) { return i. Ezért a következõ példa nem csak rossz stílusra vall.2.c: extern inline int g(int i).c: extern inline int g(int i) { return i+1.1. Ez nem vonatkozik a lokális változókra (§4. mert f()-et a file2.9) minden olyan fordítási egységben definiálni kell – azonos módon (§9. Az elõzõ példákban szereplõ összes név külsõ név. } Sajnos. int g() { return f().2. ezt a hibát a C++ egyes változatai nehezen veszik észre. int f() { return x.c: int x. ezért a helyben kifejtett kód és a külsõ szerkesztés következõ – különben teljesen logikus – párosítása tiltott. } // file2.6).c nem deklarálja. amelyekre csak abban a fordítási egységben lehet hivatkozni. mert x-et kétszer definiáltuk. hogy a fordítóprogram-írók élete könnyebb legyen: // file1. külsõ szerkesztésûnek (external linkage) nevezzük. hanem szabálytalan is: // file1.2.c: int x.264 Alapok ben kezdõérték nélkül megadott változók alapértelmezés szerint kapnak kezdõértéket. belsõ szerkesztésû névnek nevezzük. §10. A helyben kifejtett (inline) függvényeket (§7. Az olyan neveket. Jegyezzük meg. int h(int i) { return g(i). Az olyan neveket.c-ben az f() meghívása hiba. } // file2. } // hiba: g() nincs definiálva ebben a fordítási egységben // file2. amelyeket a nevet meghatározó fordítási egységtõl különbözõ fordítási egységben is használhatunk.9.2). amelyben használatosak. Ezenkívül a szerkesztõ nem fogja összeszerkeszteni a programot. } A file2.4.1.c: inline int f(int i) { return i+1.

Itt g() 77-et fog kiírni. */ }. void f(). // .2. amelyek egy adott fordítási egységben lokálisnak számítanak..c: typedef void T.2.c: namespace { class X { /* .7) belsõ szerkesztésûek.c: extern const int a... */ }.c: extern const int a = 77. const int x = 7. int i. Forrásfájlok és programok 265 Alapértelmezés szerint a const-ok (§5.9. A névtelen névtereket (§8.. ezért legjobb elkerülni õket. // . A globális konstansokat és a helyben kifejtett függvényeket rendszerint csak fejállományokba (§9. void g() { } cout << a << '\n'.9. // file2. gyakran okoznak zavart.5) arra használhatjuk.c: typedef int T. Következésképpen ez a példa szabályos (bár zavaró lehet): // file1.4) és a typedef-ek (§4.. Az olyan globális változók. A konstansokat kifejezett utasítással tehetjük külsõ szerkesztésûvé: // file1.. int i. void f(). // file2. A névtelen névterek és a belsõ szerkesztés hatása nagyon hasonló: // file 1.1) szabadna tennünk..c: class X { /* . hogy a neveket egy adott fordítási egységre nézve lokálissá tegyük.. const int x = 8. . hogy biztosítsuk a következetességet. } // file2.

4) belül használjuk. Fejállományok A típusoknak ugyanannak az objektumnak.c-ben lévõ f() függvény nem azonos a file2. hogy a végrehajtható kódot és/vagy adatleírásokat tartalmazó forrásfájlokba beépítjük (#include) a felületre vonatkozó információkat tartalmazó fejállományokat (header). Az #include szövegkezelõ eszköz.2. minden deklarációjában egységesnek kell lenniük. Továbbá a leg- . 9. osztálynak stb. A különbözõ fordítási egységekben lévõ deklarációk egységességének elérésére nem tökéletes.h" // a szabványos include könyvtárból // az aktuális könyvtárból Sajnos a beépítõ utasításban a szóközök mind a < >. Az #include "beépítendõ" utasítás a beépítendõ fájl tartalmára cseréli azt a sort. A fájl tartalmának C++ forrásszövegnek kell lennie. következésképpen a fordítónak átadott és késõbb összeszerkesztett forráskódnak is.2.1. de egyszerû módszer.1) és osztályokon (§10. mert a fordítóprogram ennek olvasásával halad tovább. hogy a forráskód-részeket egyetlen egységbe (fájlba) gyûjtsük össze a fordításhoz. függvénynek. Ha van egy adott fordítási egységre nézve lokális nevünk és ugyanazt a nevet használjuk máshol egy külsõ szerkesztésû egyed számára is. akkor magunk keressük a bajt. de a beépített fájlok jellemzõen csak deklarációkat tartalmaznak. hogy „használj belsõ szerkesztést” (§B.266 Alapok A file1. A static kulcsszót lehetõleg csak függvényeken (§7. amelyben az #include elõfordul. és nem olyan kódot. A standard könyvtárbeli fejállományok beépítéséhez a fájl nevét idézõjelek helyett a < > zárójelpárok közé kell foglalni: #include <iostream> #include "myheader.3). hogy egy fájlt minden egyes alkalommal újra kell fordítani. ha valahová máshová beépítjük.2. amelyet a fordítóprogramnak alaposan elemeznie kellene. ami arra való. mind a " " belsejében fontosak: #include < iostream > // nem fogja megtalálni az <iostream>-et Furcsának tûnhet.c-ben lévõ f() függvénnyel. A C nyelvû és a régebbi C++ programokban a static kulcsszót használták (zavaróan) annak a kifejezésére.2.

2. .cxx. green }. }. Ezzel ellentétben egy fejállomány sohasem tartalmazhatja a következõket: Közönséges függvénydefiníciók Adatdefiníciók Agregátum-definíciók Névtelen névterek Exportált sablondefiníciók char get(char *p) { return *p++. .141593. class Matrix. hogy csökkentse a munkát. csak ésszerû módja az #include használatának a logikai szerkezet kifejezésére. . mint a .c fájlok”-nak. namespace { /* … */ } export template<class T>f(T t) { /* … */ } A fejállományok hagyomány szerint . inline char get(char* p) { return *p++. } int a. hogy egy fejállományban a következõk szerepelhetnek: Nevesített névterek Típusdefiníciók Sablondeklarációk Sablondefiníciók Függvénydeklarációk Helyben kifejtett függvények definíciói Adatdeklarációk Konstansdefiníciók Felsorolások Névdeklarációk Beépítõ utasítások Makródefiníciók Feltételes fordítási utasítások Megjegyzések namespace N { /* … */ } struct Point { int x. yellow. const float pi = 3. ezért gyakran hívják ezeket „. #include <algorithm> #define VERSION 12 #ifdef __cplusplus /* check for end of file */ Mindez nem nyelvi követelmény. Fordítóprogramunk dokumentációja ezt jól meghatározza. enum Light { red. 3 }.és adatdefiníciókat tartalmazó fájlok kiterjesztése pedig .c.C. template<class T> class V { /* … */ }.h fájlok”-nak és „.cc. y. } extern int a.9.h kiterjesztésûek. template<class T> class Z. a függvény. Alapszabályként fogadjuk el. amit ugyanannak a fejállománynak az ismételt fordítása jelent.cpp és . Más szokásos jelöléseket is találhatunk. short tbl[ ] = { 1. extern int strlen(const char*). Forrásfájlok és programok 267 több modern C++-változat valamilyen formában támogatja az elõfordított fejállományokat.

2. mint a <map>. // .2. // az stdio elérhetõvé tétele a globális névtérben #endif ..h nevû szövegfájlban tárolódik a szokásos könyvtárban.2. #ifdef __cplusplus } } using namespace std. csak teljes deklarációkat és definíciókat építsünk be és csak globális hatókörben.9) extern "C" { // az stdio függvények C szerkesztésûek (§9. szerkesztési blokkokban. számomra teljesen ismeretlen fejállományban szereplõ makró helyettesít. amit egy olyan név okoz. hogy a több fordítási egységben elõforduló egyedeibõl másodpéldányt készítsenek. A nyelv adott megvalósítása ismerheti a beépített szabványos matematikai könyvtárat (§22.2. Egy olyan fejállomány. amit az #include <stdio. A standard könyvtár fejállományai A standard könyvtár eszközeit szabványos fejállományok halmazán keresztül mutatjuk be (§16.2. Azt ajánlom. A .. int printf(const char* .2) tegyük ezt.1.2.h kiterjesztés hiánya nem utal semmire a fejállomány tárolásával kapcsolatban. valószínûleg a map.h fájl általában valahogy így néz ki: #ifdef __cplusplus // csak C++ fordítók számára (§9. Bölcs dolog nem túl okosnak lenni az #include használatánál.. Ezenkívül az egyszerû esetek sokkal gyakoribbak. A C standard könyvtárának minden <X. Az egyes C++-változatok számára megengedett. Célszerû elkerülni a makrókkal való ügyeskedést is. hogy fejállományok. 9. illetve a szabványos fejállományok kezelésének módját. mert beillesztésükhöz az #include <…> formát használjuk az #include "…" helyett. mert az egyes C++-változatok nehezen tudják elkerülni.2). Az #include <cstdio> például azt nyújtja.4) #endif // .h>.3) és úgy kezelheti az #include <cmath> utasítást. A stdio. Másfelõl a szabványos fejállományokat nem muszáj hagyományos módon tárolni. hogy bármilyen fájlt beolvasnánk.. ezért jó kód készítéséhez fontosabbak. ami anélkül teszi elérhetõvé a szabványos matematikai függvényeket. vagy olyan névtérdefinícióknál. tudjuk róluk.. A standard könyvtárbeli fejállományokat nem kell utótaggal ellátnunk. amelyet egy közvetetten beépített.268 Alapok Az egyszerû állandókat ajánlatos fejállományokba tenni.h> fejállományához létezik megfelelõ szabványos <cX> C++ fejállomány. mint egy kapcsolót. hogy kihasználják a standard könyvtár definícióinak ismeretét és ezáltal optimalizálják annak megvalósítását..4) namespace std { // a standard könyvtárat az std névtér írja le (§8.). Az agregátumokat azonban nem. Az egyik legkevésbé kedvelt foglalatosságom olyan hibát nyomon követni. amikor régi kódot alakítunk át (§9.

vagy helyben kifejtett függvény kétféle definiálása kizárólag akkor fogadható el ugyanazon egyed két példányaként. ezen szimbólumok jelentése mindkét fordítási egységben ugyanaz. Gyakorlati szempontból ez azt jelenti. sablont stb. Sajnos. Egy osztály definícióját például össze lehet állítani makrók behelyettesítésével is. void f(S*).c: struct S { int a. hogy a C és C++ osztozzanak a fejállományon. }. Nem bölcs dolog azonban egy definíciót ilyen módon kétszer leírni. hogy a „fájl” fogalma nem része a C és C++ nyelvnek. . mindig csak egyszer definiálhatunk egy programban. hogy például egy osztálynak. így vannak olyan változatok. Ezt a szabályt gyakran az „egyszeri definiálás szabályának” (ODR. // file2. szimbólumról szimbólumra megegyeznek és 3. Az egyszeri definiálás szabálya Egy adott osztályt. hogy lehetõvé tegyük. one-definition rule) nevezik.c: struct S { int a. */ } Az ODR értelmében a fenti példa helyes és S ugyanarra az osztályra vonatkozik mindkét forrásfájlban. hogy egy osztály. de az összeszerkesztéssel és névterekkel kapcsolatos dolgokra oda kell figyelnünk. pontosan egy kifejtéssel kell rendelkeznie.9.2. hogy az ott szereplõ S az S egyetlen definiálása és szabadon megváltoztathatja azt. Ha valaki módosítja a file2. amelyek a programokat nem forrásfájlokban tárolják. Még ennél is nagyobb baj. különbözõ fordítási egységben szerepelnek és 2. sablon. Forrásfájlok és programok 269 Azaz a deklarációk (nagy valószínûséggel) közösek. char b. Például: // file1. }. a nyelvi szabály nem lehet ennyire egyszerû. ami nehezen felfedezhetõ hibát okozhat. definíciójának egyedinek kell lennie – valamelyest bonyolultabb és ravaszabb módon fogalmaztuk meg. char b. void f(S* p) { /* . sablon stb.1) szöveges formában két forrásfájlban is el lehet helyezni...2. de #include utasításokkal (§9. amelyet valahol egy fájlban tárolunk.3. felsorolást. Következésképpen a szabványban lévõ szabályt – amely azt mondja. azt feltételezheti. Azaz egy osztály. ha 1.c-t. 9.

h" // f() használata itt file2.h" void f(S*p) {/*.c: struct S2 { int a. char b. }. char b. }.// hiba . char b.h: struct S {int a. void f(S*). file1.h" void f(S* p) { /* . // file2. }. }. // hiba: két definíció Ez azért hiba.c: struct S1 { int a..c: #include "s. // file1. mert egy struct-ot egyetlen fordítási egységben nem lehet kétszer definiálni. char bb..c: struct S2 { int a.*/} Nézzünk példákat az ODR szabály megsértésének mindhárom módjára: // file1. }.c: #include "s.270 Alapok Az ODR szándéka az. char b}.c: #include "s. // file1.c: #include "s.. */ } Ábrával: s. char b.h" // f() használata itt // file2. struct S1 { int a. void f(S*). hogy megengedje egy osztálydefiníció beillesztését különbözõ forrásfájlokba egy közös forrásfájlból: // file s..h: struct S { int a.

char b...c: export template<class T> T twice(T t) { return t+t. // file2... }. struct S3 { X a. }... amelyek egy tag nevében különböznek. amennyire csak lehetséges. a fordítóprogram felismerte volna a hibát. char b. A helyi typedef-ek és makrók ugyanis módosíthatják a beépített deklarációk jelentését: // file s. de a példa hibás. A legtöbb C++-változat nem képes a különbözõ fordítási egységekben lévõ osztálydefiníciók következetességét ellenõrizni. struct S3 { X a. // file2. // hiba Itt az S3 két definiciója szimbólumról szimbólumra megegyezik.h" // . hogy csak a deklarációjukat adjuk meg: // file1. A sablondefiníciókat több fordítási egységbe is beépíthetjük. */ }. ezért az ODR-t megsértõ deklarációk nehezen észrevehetõ hibákat okozhatnak.h" // . #include "s. mert S2 olyan osztályokat nevez meg.h állományban vezettük volna be.9. } . Például ha a Point osztályt az s. char b.h: struct S { Point a.c: #define Point int #include "s. Forrásfájlok és programok 271 Ez azért hiba. }.c: class Point { /* . az exportált sablonokat pedig úgy is használhatjuk.c: typedef char X.c: typedef int X. Az ilyen kódmódosulás ellen úgy védekezhetünk a legjobban. // file1. ha a fejállományokat annyira különállóvá tesszük. amikor a közös definiciókat fejállományokba tesszük és aztán azokat építjük be. // file1. amíg ez nem sérti az ODR-t. mert az X név (trükkös módon) mást jelent a két fájlban. Sajnos az a módszer sem képes az ODR utolsóként bemutatott megszegése ellen védelmet nyújtani.

de különbözõ fordítóprogrammal lefordított kódrészletek között sem. } // deklaráció Az export kulcsszó azt jelenti. int g(int i) { return twice(i). hogy C++ kódrészletet használnak más nyelven megírt programok részeként. és meghatározza. Az extern "C"-ként megadott függvényekre is a C++ típusellenõrzési és paraméter-átalakítási szabályai vonatkoznak. hogy a C összeszerkesztési szabályainak megfelelõen kell azt hozzászerkeszteni a kódhoz: extern "C" char* strcpy(char*. const char*). nem pedig a gyengébb C szabályok. extern char* strcpy(char*.7). például a karakterláncok és egészek szerkezete. összeszerkesztési szabályt határozhatunk meg az extern deklarációkra. . const char*). hogy az extern "C"-ben szereplõ "C" az összeszerkesztési szabályt. hogy „más fordítási egységbõl elérhetõ” (§13. Összeszerkesztés nem C++ kóddal A C++ programok általában más nyelven megírt részleteket is tartalmaznak. 9. Hogy segítsünk. illetve azon nevek formája.4. és a szerkesztõtõl megkövetelt típusellenõrzések. Az extern "C" utasítás különösen fontos a C és a C++ közötti szoros kapcsolat miatt. A deklaráció hatása a „sima” deklarációkétól csak az strcpy() hívására használt összeszerkesztési szabályban tér el.272 Alapok // file2. Jegyezzük meg. melyeket a fordítóprogram a szerkesztõnek átad. nem pedig a programnyelvet jelöli. A következõ példa bevezeti a C és a C++ standard könyvtáraiban levõ strcpy() függvényt.c: template<class T> T twice(T t).2. melyek véletlenül éppen megfelelnek a C követelményeinek. A különbözõ nyelvek és ugyanazon nyelv különbözõ megvalósításai például különbözõképpen használhatják a gépi regisztereket a paraméterek tárolására. Az együttmûködés a különbözõ nyelven megírt programrészek között nem mindig könnyû – sõt még az azonos nyelven írt. Az extern "C" utasítás (csak) az összeszerkesztési szabályt határozza meg. Az extern "C"-t gyakran használják olyan Fortran vagy assembler eljárásokkal való összeszerkesztéshez. a függvényhívások szerepét nem befolyásolja. másképpen helyezhetik azokat a verembe. különbözõ lehet a beépített típusok. Hasonlóan gyakori az is.

hogy közös C és C++ fejállományt készítsünk: #ifdef __cplusplus extern "C" { #endif char* strcpy(char*. int g() { return f(1). int strlen(const char*).. #ifdef __cplusplus } #endif . melyet gyakran szerkesztési blokknak (linkage block) neveznek.. int strcmp(const char*. } // hiba: nem vár paramétert Kényelmetlen lehet. } Ezt a szerkezetet. // ..8..1) használunk. hogy belefoglalunk egy teljes C fejállományt és így alkalmassá tesszük azt a C++-ban való használatra: extern "C" { #include <string.h> } A fenti módszer gyakran használatos arra. úgy is használhatjuk. Egy másik lehetõség. ezért bevezettünk egy eljárást. const char*). const char*). hogy C fejállományokból C++ fejállományokat hozzanak létre. int strcmp(const char*. const char*). int strlen(const char*). mellyel deklarációk egy csoportjának összeszerkesztését határozhatjuk meg: extern "C" { char* strcpy(char*. // . ha feltételes fordítást (§7. const char*).9. ha sok deklarációhoz kell hozzáadnunk az extern "C"-t. Forrásfájlok és programok 273 Például: extern "C" int f().

nem definíció Ez a változók hatókörét és tárolási osztályát nem érinti. hogy C szerkesztésû könyvtárakat építsünk be egy általunk választott névtérbe. } // bármilyen deklaráció jöhet ide. amikor a fájlt C fejállományként használjuk. Sajnos ugyanez a rugalmasság nem áll rendelkezésünkre az olyan fejállományok esetében.274 Alapok A „készen kapott” __cplusplus makró használatával azt biztosíthatjuk. Egy szerkesztési blokkon belül bármilyen deklaráció szerepelhet: extern "C" { int g1. hogy a deklaráció jelentése változatlan maradjon. . pedig célja csak annyi. Egy C szerkesztésû nevet névtérben is megadhatunk. } // rendben // hiba: nincs globális printf() Még ha std::printf()-nek nevezzük is. nem pedig definiálni akarunk. hogyan lehet a névhez hozzáférni C++ programokból. // deklaráció. hogy a C++ szerkezetek eltûnjenek. így a létrehozott tárgykód (object fájl) tükrözni fogja a névterek használatát vagy annak hiányát.8). amikor egy extern deklarációhoz "C"-t adunk (és a fájl jelentése is. extern int g2. "). hogy a globális névteret „szennyeznénk". Ez lehetõvé teszi számunkra. az extern kulcsszót közvetlenül a deklaráció elõtt kell megadnunk: extern "C" int g3. amelyek C++ szerkesztésû függvényeket határoznak meg a globális névtérben. ahelyett. Ennek az az oka. és definiciója is lesz. hogy a C++ egyedek összeszerkesztésénél figyelembe kell venni a névtereket is. Ha egy változót csak deklarálni. amikor majd a szerkesztési blokkba foglaljuk). pl: // definíció // deklaráció. nem csak deklarációja. így g1 globális változó marad. Egy jellemzõ példa erre az std névtér printf() függvénye: #include<cstdio> void f() { std::printf("Helló. de azt nem. hogy a szerkesztõ hogyan fogja látni a nevet. printf("világ!\n"). ez még mindig ugyanaz a régi C printf() (§21. A névtér azt befolyásolni fogja. nem definíció Ez elsõ látásra furcsának tûnik.

} // FT C++ szerkesztésû // CFT C szerkesztésû // cmp C szerkesztésû void isort(void* p.1. size_t n.5. size_t sz. extern "C" { typedef int (*CFT)(const void*. Ha a két nyelv adott változatainak összeszerkesztési szabályai.sz. nyelvi kiterjesztésként elfogadhatja a hibaként megjelölt eseteket.1.&compare). és változónévre vonatkozni fog.2. Forrásfájlok és programok 275 9. size_t n. // compare() C++ szerkesztésû // ccmp() C szerkesztésû // hiba // rendben // rendben // hiba } Egy olyan nyelvi változat.sz.&ccmp).sz. extern "C" int ccmp(const void*. // cmp C++ szerkesztésû int compare(const void*. // cmp C szerkesztésû extern "C" void ysort(void* p. akkor az minden olyan függvénytípusra.sz. Ennyi közös tulajdonság azonban általában nem tételezhetõ fel. CFT cmp). így figyelnünk kell arra. size_t n.&ccmp). const void*). Például: typedef int (*FT)(const void*. Az összeszerkesztés és a függvényekre hivatkozó mutatók Ha egy programban a C és C++ kódrészleteket keverjük. qsort(v.1. size_t n. void qsort(void* p. elõfordulhat. FT cmp). hogy biztosítsuk a függvények oly módon történõ meghívását. void f(char* v. const void*). illetve a függvényhívási eljárások közösek. amit a deklaráció(k) bevezet(nek). // cmp C++ szerkesztésû void xsort(void* p.1. isort(v. CFT cmp). size_t sz. Ha egy deklaráció számára meghatározzuk az összeszerkesztési módot. amelyben a C és C++ ugyanazt a függvényhívási módot használja. ahogy azt a függvény elvárja. isort(v. int sz) { qsort(v. függvénynévre. FT cmp).9. const void*). size_t sz. Ez mindenféle furcsa – de néha alapvetõ – összeszerkesztési módot lehetõvé tesz. const void*).&compare). hogy az egyik nyelven megírt függvényekre hivatkozó mutatókat a másik nyelven definiált függvényeknek szeretnénk átadni. . a függvényekre hivatkozó mutatók átadása egyszerû. size_t sz.

PLUS='+'. extern double number_value.h fejállományban tárolhatnánk azoknak a neveknek a deklarációit. struct Syntax_error { const char* p.2) néhány lehetséges fizikai elrendezését.276 Alapok 9. .c – használhatnánk a függvények és adatleírások tárolására. table.c fájlba. DIV='/'. melyet minden . Syntax_error(const char* q) { p = q.h fejállomány így nézne ki: // dc.c fájlok közötti kapcsolatot biztosító típusok deklarációit pedig egyetlen . parser. } #include <string> namespace Lexer { enum Token_value { NAME.h fájlba tesszük.1. és a dc.c és main. END.c fájlt – lexer. MINUS='-'. }. A dc. } }.3. §8. LP='('. error. extern std::string string_value. A számológép program esetében öt . Fejállományok használata A fejállományok használatának illusztrálására most bemutatjuk a számológép program (§6. RP=')' extern Token_value curr_tok.c fájl beépít (#include). amelyek egynél több fájlban használatosak.1. MUL='*'.3. Egyetlen fejállományos elrendezés Egy programot úgy bonthatunk a legegyszerûbben több fájlra. 9. a . PRINT='. ASSIGN='='.c.c. NUMBER.'.c. hogy a definíciókat megfelelõ számú . } Token_value get_token().h: namespace Error { struct Zero_divide { }.

// elemi szimbólumok kezelése // szorzás és osztás // összeadás és kivonás } #include <map> extern std::map<std::string.. amikor a dc. Token_value get_token().. extern std::istream* input. Az egyes definíciók a megfelelõ .h-t a fájlokba beépítjük. A lényegi kód elhagyásával a lexer..9.c valahogy így néz ki: // lexer.h" #include <iostream> #include <cctype> Lexer::Token_value Lexer::curr_tok.c fájlban szerepelnek. } Minden változódeklarációban az extern kulcsszót használjuk annak biztosítására. double term(bool get). } // a dc.. A lexer..double> table. Lexer::Token_value Lexer::get_token() { /* . namespace Driver { extern int no_of_errors. std::string Lexer::string_value. amely a hozzá tartozó kifejtést tartalmazza. void skip(). hogy a benne lévõ minden deklaráció valamilyen ponton be legyen építve abba a fájlba. Forrásfájlok és programok 277 namespace Parser { double prim(bool get). double expr(bool get)... Lexer::Token_value Lexer::get_token() { /* .h-ból // .. using Lexer::get_token. double Lexer::number_value. */ } . hogy egy meghatározás ne forduljon elõ többször is. */ } A fejállomány ilyen használata biztosítja.c fordításakor például a fordítóprogramnak a következõ kód adódik át: namespace Lexer { // . using Lexer::curr_tok.c: #include "dc.

Ha a get_token()-t például Token_value típusú visszatérési értékkel vezettük volna be.h" std::map<std::string.. A fenti definíció a table-t globálisként határozza meg. */ } int main(int argc.c fájl így fog kinézni: // parser.c pedig így: // table.. A szimbólumtábla nem más. de int visszatérési értékkel definiáltuk volna. a szerkesztõ fogja észrevenni a problémát..c fájl fordítása hiúsul meg..278 Alapok Így a fordítóprogram biztosan észreveszi.h" #include <sstream> int Driver::no_of_errors = 0. std::istream* Driver::input = 0. */ } ..c: #include "dc.c: #include "dc. A parser. A main. ha egy névhez megadott típusok nem egységesek. */ } double Parser::expr(bool get) { /* .h" double Parser::prim(bool get) { /* . */ } A table. mint egy standard könyvtárbeli map típusú változó. Ha egy definíció hiányzik. a lexer.c fordítása típusütközési vagy „nem megfelelõ típus” (type mismatch) hiba miatt nem sikerült volna.. */ } double Parser::term(bool get) { /* . Egy valóságos méretû programban a globális névtér efféle kis „szennyezõdései” felhalmozódnak és végül problémákat okoznak.c fájl végül így fog kinézni: // main. Csak azért voltam itt ilyen hanyag.. akkor valamelyik . char* argv[ ]) { /* . hogy lehetõségem legyen figyelmeztetni rá.. void Driver::skip() { /* . ha egy deklaráció.c: #include "dc..double> table..

c table. a szerkezet homályos lesz. ha a program kicsi és részeit nem áll szándékunkban külön használni. .c lexer. hogy amikor névtereket használunk. A kis programoknál az elrendezés egyszerûsíthetõ. hogy a felül lévõ fejállományok mind a standard könyvtár fájljai. A közös fejállomány módosítása maga után vonja az egész program újrafordítását és nagy a hibalehetõség. fájl alapú fejlesztõkörnyezetekben.c Észrevehetjük.9. hogy a program main() függvényének ismerjék fel. ha több programozó is módosítja az egyetlen fejállományt. ezért itt nem használtunk névteret.c parser. a main()-nek globális függvénynek kell lennie. A program fizikai elrendezését valahogy így lehet bemutatni: <sstream> <map> <string> <cctype> <iostream> dc. bár ezen a megjegyzések segíthetnek.h-ban egyben a program logikai felépítését is ábrázoljuk. Ha nem használunk névtereket. Forrásfájlok és programok 279 Ahhoz. Jegyezzük meg. ha minden #include utasítást közös fejállományba teszünk. A program elemzésekor ezek a könyvtárak számos esetben kihagyhatók. a logikai felépítés a program növekedésével együtt romlani fog.h main. Hacsak nem fektetnek hangsúlyt a névterekkel és osztályokkal kapcsolatos programozási stílusra. mert széleskörûen ismertek és stabilak. A nagyobb programok egyetlen fejállományos elrendezése nem mûködik a hagyományos. Az egyetlen fejállományos fizikai részekre bontás akkor a leghasznosabb. a dc.

Ily módon az elemzõt három fájl képviseli. Ekkor minden .. hogy megvalósítsa a felületében közzétett szolgáltatásokat.h felhasználói fejállományt azért építjük be.h" #include "error. double term(bool get). és rendszerint további olyan .. } // felület a megvalósításhoz A parser. Minden .h nyújtja: // parser.h fájl tartalmazza.1). */ } .c fájlhoz tartozik egy megfelelõ . a programozói felület egy _impl.h fájlokat is..280 Alapok 9.h fájl.h: #include "parser. melyekre a Parser függvényeinek szüksége van: // parser.h végzõdésû fájlban szerepel.2.h" namespace Parser { double prim(bool get). pedig a . double expr(bool get). Több fejállományos elrendezés Egy másik fizikai elrendezés szerint minden logikai modulnak saját fejállománya lenne. a modul függvény..c fájlban együtt tároljuk azokra a fejállományokra vonatkozó #include utasításokkal.h" #include "table. Ez a fizikai elrendezés megegyezik a modul logikai felépítésével.h: namespace Parser { double expr(bool get). felhasználói felületét pedig a parser.h" #include "lexer. hogy a fordítóprogram ellenõrizhesse a következetességet (§9. ami meghatározza a .h" double Parser::prim(bool get) { /* .3.és változódefiníciói stb. using Lexer::curr_tok..h adja: // parser_impl.h fájlját. amelyek meghatározzák.. */ } double Parser::expr(bool get) { /* .3.c szolgáltatásait (felületét).c: #include "parser_impl. mire van szüksége más modulokból ahhoz. } // felület a felhasználóknak Az elemzõt megvalósító függvények közös környezetét a parser_impl.c fájlokban vannak elhelyezve. using Lexer::get_token.c fájl beépíti a saját . Az elemzõ függvényeket a parser. A felhasználóknak szánt felületet a . */ } double Parser::term(bool get) { /* . amely leírja a modul által nyújtott szolgáltatásokat.

ez elég jól egyezik a §8.c helyett.3.3-ban leírt logikai szerkezettel.h-t: parser.h parser_impl. ami nem szükséges az elemzõ függvények közös környezetének kifejezéséhez.h table. a prim() használja. A table.c prim.c fájlba és csak oda építsük be a table.h main.h error.h-ba építettük volna be a parser.h table.h parser.h lexer. Forrásfájlok és programok 281 Az elemzõ és annak a vezérlõ általi használata ábrával így mutatható be: parser. így ha a függõségeket valóban a lehetõ legkevesebbre szeretnénk csökkenteni.h azonban valami olyasmire példa.c Ahogy vártuk. csak a függvények megvalósításainak van szüksége rá.9.h-t a parser_impl. Ha a table.h lexer.c parser. Tulajdonképpen egyetlen függvény. a prim()-et tegyük külön .c . a szerkezetet még tovább egyszerûsíthettük volna.h parser_impl.h error.

1. Egy nagy program elemzésekor vagy módosításakor alapvetõ. mint a mi apró elemzõnk és számológépünk. ahol egy globális információtár van a középpontban – felülrõl lefelé haladó megközelítést igényel. amelyeknek közös környezetre van szükségük. hogy kevesebb információ kell a modul lefordításához. Miért törõdünk ezzel a bonyolultabb több fejállományos elrendezéssel? Nyilván sokkal kevesebb gondolkodást igényel. csak helyi szemszögbõl. A jobb összpontosítás azt eredményezi. hogy pontosan eldönthessük. hogy minden olyan deklarációt megnézzünk. . ha egyszerûen minden deklarációt bedobunk egy fejállományba. 9.282 Alapok Ilyen alaposságra a nagyobb modulokat kivéve nincs szükség. Az ilyen fájlok csak ott kellenek. mint ahogy azt a dc.h használata nem szabványos és még csak nem is gyakori megoldás – én egyszerûen így szeretek elnevezni dolgokat. hogy nem igényelnek saját _impl. hogy egynél több _impl. hogy a kód módosítása mindig hiányos információk és helyi nézõpont alapján történik. A hatás drámai lehet. és hogy figyelmen kívül hagyhassuk a program többi részét. mert egy egyszerû függõségelemzés a fejállományok jobb használatához vezet. Továbbá nem ritka. így az gyorsabban történik. A számológép egyéb moduljai A számológép többi modulját az elemzõhöz hasonlóan rendezhetjük el.h fájlokat. mert jobban azonosítja a kapcsolatokat. hogy a programozó viszonylag kis kódrészletre összpontosíthasson.h-nál tettük. hogy odaillõ-e. Alapvetõen azért használtuk ezt az elrendezéstípust. mitõl függ az elemzõ kód. hogy pontosan mi függ egy másik dologtól. amelyek nagyságrendekkel nagyobbak. hogy az _impl. és eldöntsük. A több fejállományos elrendezés megengedi. A valóságos méretû modulok esetében gyakori.h fájl van. amelyet valamelyik modul használ.3. pusztán azért. Meg kell jegyeznünk. A lényeg. hogy sikeresen dolgozzunk „belülrõl kifelé”. ahol egyes függvények számára azok szükségesek. mivel a modul függvényeinek részhalmazai különbözõ közös környezetet igényelnek. Az egyetlen fejállományos elrendezés – mint minden más elrendezés. ahol egy logikai modul sok függvénybõl áll. hogy a fordítási idõ tizedrészére csökken. A több fejállományos elrendezés segít. így örökké gondolkodnunk kell azon. Elõfordul. A több fejállományos elrendezés olyan modulok és programok esetében hatásos.2. Ezek a modulok azonban olyan kicsik. hogy további fájlokat építenek be ott. Az egyetlen fejállományos elrendezés rákényszerít minket.

Forrásfájlok és programok 283 A hibakezelõt a kivételtípusok halmazára egyszerûsítettük. extern double number_value.h: namespace Error { struct Zero_divide { }.h: #include <string> namespace Lexer { enum Token_value { NAME. RP=')' extern Token_value curr_tok. lexer) meglehetõsen nagy és rendezetlen felületet nyújt: // lexer. az <iostream>-re és a <ctype>-ban megadott.c: #include #include #include #include "lexer. így nincs szükségünk az error. END. MUL='*'.h-ra. LP='('. Syntax_error(const char* q) { p = q. } Az adatbeviteli kód (lexikai elemzõ. } }.h" <iostream> <cctype> .'.h" "error. MINUS='-'.h-n kívül a lexikai elemzõ az error. ASSIGN='='. DIV='/'. } Token_value get_token(). }. NUMBER.9.c-re: // error. struct Syntax_error { const char* p. A lexer. PLUS='+'. a karakterek fajtáit eldöntõ függvényekre támaszkodik: // lexer. PRINT='. extern std::string string_value.

284 Alapok Lexer::Token_value Lexer::curr_tok.h" #include "table.. bár a standard könyvtárbeli <map> fejállomány használatával számos érdekes dolog kerülhet bele. std::string Lexer::string_value. A szimbólumtábla alapvetõen önálló.c fájlba is bele lehetnek építve. hogy hatékonyan valósíthassa meg a map sablonosztályt: // table.h #include utasításait külön tehettük volna. */ } Az error.h fájlba.h" #include "error.h: #include <map> #include <string> extern std::map<std::string.h közötti különbség csak az extern kulcsszó: // table. ez azonban túlzás egy ilyen kis program esetében.h-t –.c és a table. A vezérlõ tulajdonképpen mindenre támaszkodik: // main.h" std::map<std::string. double Lexer::number_value. még akkor is. A modul megvalósításában szokásos módon építjük be (#include) a modul által nyújtott felületet – ebben az esetben a lexer. ha a table.double> table.double> table..h" . Lexer::Token_value Lexer::get_token() { /* .c: #include "table.h" #include "lexer.c: #include "parser. hogy az egyes fejállományok több . a Lexer-hez tartozó _impl. Mivel feltesszük. a table deklarációját külön kell választanunk annak kifejtésétõl. hogy a fordítóprogram ellenõrizhesse a következetességet.

A nagyobb programokat rendszerint megéri úgy elrendezni.c-be tesszük azt. Ekkor ugyanis nem támaszkodhatunk a main()-ben megírt kódra és fel kell készülnünk arra is. void skip(). A fent tárgyalt alapvetõ módszerek ekkor is alkalmazhatók..h fájlként. Hasonlóan. amikor elérik az ezres nagyságrendet. 9. std::istream* input. Ezen tényezõk közül sok inkább a rendszer fájlkezelésébõl adódik és nem a C++-ból. nevezetesen hogy meghív egy külön forrásfájlban lévõ vezérlõ függvényt.9. Például. hogy minden szerkezet deklarációját külön fájlba tesszük). ha szövegszerkesztõ programunk nem képes arra. mint egyetlen 1000 soros fájlé. Gyakran ésszerû az olyan mûveletekbõl is minél kevesebbet alkalmazni. ha 20 darab 50 soros fájl olvasása észrevehetõen több idõt igényel. Forrásfájlok és programok 285 namespace Driver { int no_of_errors. Ha azonban egy nagy program deklarációit logikailag a lehetõ legkisebb fejállományokra bontjuk (például úgy. Ez különösen fontos olyan kódok esetében. melyeket gyakran százas nagyságrendben számolhatunk) a program végrehajtási környezete számára rendszerint még kezelhetõ. hogy különbözõ függvényekbõl hívják meg kódunkat (§9. hogy a vezérlõnek kevesebb közvetlen függõsége legyen.3. A fejállományok használata A programban használt fejállományok (header file) száma több tényezõtõl függ. amit az #include utasítással beépítünk. hogy . Az igazi bonyodalom ott kezdõdik.2. mielõtt egy kis projektben a több fejállományos stílust használjuk. hogy több fájlt nézzünk vele egyszerre. } #include <sstream> int main(int argc. */ } Mivel a Driver névteret kizárólag a main() használja. Nagy projekteknél persze elkerülhetetlen a sok fejállomány. de az állományok kezelése sziszifuszi feladattá válik.6[8]). Az ilyeneknél több száz fájl (nem számolva a szokásos fejállományokat) az általános. könnyen egy több száz fájlból álló. amit a main() tesz. akkor nem elõnyös sok fejállományt használnunk. char* argv[ ]) { /* . amelyeket könyvtárként akarunk használni. Emlékezzünk..2. Külön is szerepelhetne driver. kezelhetetlen zûrzavar lehet az eredmény. Néhány figyelmeztetés: egy tucatnyi fejállomány (természetesen a szokásos fejállományokkal együtt. akkor kétszer is gondoljuk meg. a main.

Rendszerint megéri különbséget tenni a fejlesztõi és a felhasználói felület között. A tapasztalt felhasználók felületei (a „teljes felületek”) sokkal több szolgáltatást építenek be. Az „átlagos felhasználó” kifejezés nem lekicsinylõ. találunk valamilyen módot arra. hogy a fejállományok többszöri beépítése megengedett legyen. hogy nem építjük be azokat a fejállományokat. hogy minden logikai egység teljes legyen. hogy célszerû a felhasználók többségének egyszerû. amint egy osztályleírást vagy helyben kifejtett függvényeket tartalmazó fejállományt ugyanabban a fordítási egységben (§9. mint amennyirõl egy átlagos felhasználónak tudnia kell. felesleges. 9. amelyek olyan szolgáltatásokat írnak le. Valójában az átlagos felhasználó felületét úgy határozhatjuk meg. mert az ilyen programok rendszerint eleve több fejállományt tartalmaznak. Az elsõ megközelítés – ami a számológép végsõ változatához vezetett – fárasztó és valóságos méretû programoknál gyakorlatilag kivitelezhetetlen. hogy az egyetlen és a több fejállományos elrendezés közül válasszunk. A fölös #includeok kiszûrése és az ennek eredményeképpen létrejött egyszerûsített program nagyon elõnyös lehet mind logikai szempontból. hogy minden logikai modult következetes.3) kétszer építünk be az #include-dal. Két választásunk lehet. hogy eltávolítsuk a fölösleget. A fölöslegre azért is szükségünk van. Ahol nem muszáj szakértõnek lennem. és újra kell gondolnunk azokat. Nagyobb programoknál az ilyen fölösleg (redundancia) hibákhoz vezethet. önálló egységként ábrázoljunk. hogy egy felület nem szolgálhat minden célra ugyanolyan jól.286 Alapok a valódi méretû programoknál az egyetlen fejállományos elrendezést általában nem választhatjuk.3. vagy 2. hogy csökken a fordítási idõ. Az összes . Így ugyanis kevesebb a veszekedés. A kétfajta elrendezési módszer között a program alkotórészeinek létrehozásakor kell (néha többször is) választanunk. A program egészének szempontjából nézve viszont azon deklarációk többsége. jobban szeretek átlagos felhasználó lenni.2. Ezek olyan egymást kiegészítõ módszerek. hogy a program egyes egységei elkülönülten is érthetõek legyenek. melyeket mindig figyelembe kell vennünk a lényegi modulok tervezésekor.3. Ezenkívül sok nagyobb program szerkezete olyan. Igazán nem is a mi ízlésünkre van bízva. 1. Rendkívül fontos emlékeznünk arra. melyek ahhoz kellenek. „Állomány-õrszemek” A több fejállományos megközelítés gondolata az. amelyek ismeretlenek lennének az átlagos felhasználó számára. mind azáltal. ahogy a rendszer fejlõdik. a tapasztaltabb felhasználóknak pedig terjedelmesebb felületet nyújtani. Átszervezhetjük a programunkat.

. Ez nem kívánatos.9. hogy a fejállományokba „állomány-õrszemeket” (beépítésfigyelõket. beolvassa annak tartalmát. ami kiszámíthatatlanul és kedvezõtlenül befolyásolhatja a program jelentését. Szükségtelenül hosszú fordítási idõt okozhatnak és számos deklarációt és makrót elérhetõvé tehetnek.h-val elõször találkozik. melyek optimalizálják a fejállományok feldolgozását. Lehetõleg szisztematikusan kell használnunk.. így alkalmaznunk kell valamilyen eszközt. a makrónevek ütközése ellen pedig nincs névtér védelem. } #endif // CALC_ERROR_H A fájlnak az #ifndef és az #endif közötti tartalmát a fordítóprogram nem veszi figyelembe. a CALC_ERROR_H pedig értéket kap. Ez makrókkal való ügyeskedés. másodszor már nem fogja figyelembe venni. Forrásfájlok és programok 287 elõfordulás megtalálása azonban ritkán sikerül. A fejállományokat mindenféle környezetben használják. ha a CALC_ERROR_H már definiált. mert nem tudhatjuk. include-guards) illesztünk: // error. amikor tényleg szükség van rá. hogy a felhasználó mennyire alapos elemzést tart érdemesnek. Csak akkor építsünk be fejállományokat. de mûködik és mindenütt jelen van a C és C++ világában. Ezért amikor a fordítóprogram az error. A hagyományos megoldás az. Az állomány-õrszemeknek ezért hosszú és csúnya neveket szoktam választani.. A standard könyvtár fejállományainak mindegyike tartalmaz állomány-õrszemeket. Ha ismét találkozna vele a fordítás során. még azoknál a C++-változatoknál sem.h: #ifndef CALC_ERROR_H #define CALC_ERROR_H namespace Error { // . Amint a programozó hozzászokik a fejállományokhoz és az állomány-õrszemekhez. ami megengedi a fölös #include-ok jelenlétét. hajlamos közvetlenül vagy közvetve sok fejállományt beépíteni.

amit egy globális változó kezdeti értékadása váltott ki (§14.9) vagy el nem kapott kivételt (§14. double sqx = sqrt(x+y). Programok A program külön fordított egységek gyûjteménye. Ezen az egyszerû meghatározáson a globális változókat tartalmazó (§10. double y.4.1. §9. vagy static osztályváltozók) a main() meghívása elõtt. 9. fõleg a bonyolult kezdeti értékadást igénylõ globális változók használatát kell korlátoznunk. egyedi meghatározással (definícióval) kell rendelkeznie (§4. a fordítási egységben definiciójuk sorrendjében kapnak kezdõértéket (§10. A különbözõ fordítási egységekben lévõ globális változók kezdeti értékkel való feltöltésének sorrendjét számos módon kényszeríthetjük ki.9).2. A main() által visszaadott int érték lesz a program visszatérési értéke. ebben a gyûjteményben használt függvénynek.2). A különbözõ fordítási egységekben lévõ globális változók kezdõértékkel való ellátásának sorrendje nem kötött. Továbbá nem lehetséges olyan kivételt sem elkapni. // nem lokális változók Itt az x és az y az sqx elõtt kap kezdõértéket.2).4. típusnak stb. következésképpen nem bölcs dolog ezeknél a kezdõértékek között sorrendi függõségeket létrehozni. névtér-. melyet a szerkesztõprogram egyesít.4. Fõleg a dinamikus csatolású könyvtárak (DLL) nem képesek .9. Kezdeti értékadás nem lokális változóknak Elvileg a függvényeken kívül megadott. Ha egy ilyen változónak nincs pontosan meghatározott (explicit) kezdõértéke. amit a main()-t meghívó rendszer megkap. de nincs köztük olyan. amely egyszerre „hordozható” és hatékony is lenne. így az sqrt(2) hívódik meg. A beépített típusok és felsorolások esetében az alapértelmezett kezdõérték a 0: double x = 2. Általában az a legjobb.3) és pontosan egy main() nevû függvényt kell tartalmaznia (§3. Minden.288 Alapok 9.7) kiváltó programok esetében finomítanunk kell. objektumnak. A program által végzett fõ tevékenység a main() meghívásával kezdõdik és az abból való visszatéréssel ér véget.4. ha minél kevesebb globális változót használunk.4. nem lokálisnak számító változók (azaz a globális.7). akkor a típusának megfelelõ alapértelmezett értékkel töltõdik fel (§10.

5) nem függhetnek más fordítási egységben levõ objektumok értékétõl és nem igényelnek futási idejû kezdeti értékadást. } // növelés és kiírás A nem lokális statikus változók kezdeti értékadását bármilyen eljárás vezérelheti. ezért el kell kerülnünk azon nem lokális változók használatát. §10. . amit az adott nyelvi változat arra használ. } A use_count() hívás most globális változóként mûködik. kivéve. A program befejezése A programok futása számos módon érhet véget: ♦ ♦ ♦ ♦ A main()-bõl való visszatéréssel Az exit() meghívásával Az abort() meghívásával El nem kapott kivétel kiváltásával Továbbá többféle hibás felépítés és nyelvi változattól függõ módszer létezik arra.. amit nem C++ program használ.1.4. így minden esetben biztonságosan használhatók.4. hogy elsõ használatakor kap kezdõértéket (§5. Ha a program befejezésére a standard könyvtárbeli exit() függvényt használjuk. ha a main() végrehajtására sor kerül. hogy a módszer megfelelõen mûködik.2. Ha azonban a program a standard könyvtár abort() függvényét használja. // .4).1.9. Csak akkor garantált. akkor meghívódnak a létrehozott statikus objektumok destruktorai (§10..9. hogy egy program összeomoljon. Forrásfájlok és programok 289 a bonyolult függõségekkel rendelkezõ globális változókkal boldogan együtt élni. hogy elindítsa a C++ programot. Jegyezzük meg. Globális változók helyett gyakran használhatunk referenciát visszaadó függvényeket: int& use_count() { static int uc = 0.5): void f() { cout << ++use_count(). return uc. hogy a kezdõértéket konstans kifejezésektõl kapó változók (§C. melyek futási idejû kezdeti értékadást igényelnek olyan C++ kódban. 9.

4. Az exit() meghívása azt jelenti. hogy az atexit() paraméterének nem lehet paramétere és nem adhat vissza értéket.9. Az atexit(f) meghívása elõtt létrehozott objektum destruktora az f meghívása után fog meghívódni.4. az atexit(f) meghívása után létrehozott objektum destruktora pedig az f meghívása elõtt. és atexit() függvények deklarációját a <cstdlib> fejállomány tartalmazza. mi legyen a továbbiakban. . a függvény nem nulla érték visszaadásával jelzi. mint amilyennek elsõ pillantásra látszik.290 Alapok a destruktorok meghívására nem kerül sor.2) hasonlóan az exit() paramétere is visszaadódik „a rendszernek” a program visszatérési értékeként. Az exit() függvény típusa void exit(int). hogy kódot hajthassunk végre a program befejezõdésekor: void my_cleanup(). hogy az exit() nem fejezi be rögtön a programot. Jegyezzük meg. abort(). Jegyezzük meg: ez azt is jelenti. hogy eldöntse. A nulla sikeres befejezést jelent.4). Emellett az exit() meghívása úgy fejezi be a programot. Ezek a korlátozások az atexit()-et kevésbé használhatóvá teszik. és megengedjük egy kivételkezelõnek. A lokális objektumok megfelelõ megsemmisítését (§14. Az atexit függvények számát az adott nyelvi változat korlátozza. void somewhere() { if (atexit(&my_cleanup)==0) { // normál programbefejezéskor a my_cleanup hívódik meg } else { // hoppá: túl sok atexit függvény } } Ez nagyban hasonlít a globális változók destruktorainak a program befejezõdésekor történõ automatikus meghívásához (§10. A main() visszatérési értékéhez (§3. A C (és C++) standard könyvtárának atexit() függvénye lehetõséget ad arra. Az exit(). ha a környezetet egy kivétel kiváltásával elhagyjuk.7) egy kivétel „dobása” és elkapása biztosítja. destruktorban való meghívása végtelen ciklust eredményezhet. hogy megoldja a problémát. ha ezt a korlátot elérték. hogy a hívó függvény lokális változóinak és az azt hívó függvények hasonló változóinak destruktorai nem hívódnak meg.2. §10. Ezért gyakran az a legjobb. hogy az exit() hívójának nem ad lehetõséget arra.

2.2.5) Írjunk programot. de különbözõ jelentéssel különbözõ fordítási egységekben. [12] Kerüljük az olyan nem lokális objektumok használatát.3.5.1.9.6. Tanácsok [1] Használjuk fejállományokat a felületek ábrázolására és a logikai szerkezet kihangsúlyozására. §9.2. [4] Kerüljük a fejállományokban a nem helyben kifejtendõ függvényeket. [8] A C fejállományokat névterekben építsük be.2. amelyben függvényeiket kifejtjük. §9. §9.3.1.1. a befoglalás mélységének jelölésére. Írassuk ki neveiket.3. §9. [10] Különböztessük meg a fejlesztõi és a felhasználói felületet. Próbáljuk ki a programot néhány valódi forrásfájlon (hogy elképzelésünk legyen a beépített információ nagyságáról). Gyakorlatok 1. §9.3. [5] Csak globális hatókörben és névterekben használjuk az #include-ot. 9. hogy minden beépített fájlra kiírja a megjegyzések és a nem megjegyzések sorainak számát. [6] Csak teljes deklarációkat építsünk be. amelyek futási idejû kezdeti értékadást igényelnek olyan kódban.2. illetve a nem megjegyzésként szereplõ. 4. §9. Forrásfájlok és programok 291 9. §9. [11] Különböztessük meg az átlagos és a tapasztalt felhasználók felületét. (*3) Módosítsuk az elõbbi programot. (*2. hol tárolja rendszerünk a szabványos fejállományokat.3. [2] Abban a forrásfájlban építsük be õket (#include).1 [7] Használjunk „állomány-õrszemeket”.3. §9. (*2) Hol tárolódnak a nem szabványos „foundation” könyvtárak fejállományai? 3. amit nem C++ program részeként szándékozunk felhasználni. §9.2. [3] Ne adjunk meg globális egyedeket ugyanazzal a névvel és hasonló.1. hogy elkerüljük a globális neveket. §9. üreshelyekkel elválasztott szavak számát. §9. (*2) Találjuk meg. Van-e olyan nem szabványos fejállomány.3.2. amely beolvas egy forrásfájlt és kiírja a beépített fájlok neveit.1.4. [9] Tegyük a fejállományokat különállóvá. §9.2. . Használjunk behúzást a beépített fájlok által beépített fájlok kiírásakor.3.2. §9. amely ezekkel együtt tárolódik? Be lehet-e építeni nem szabványos fejállományokat a <> jelölést használva? 2.

2). és fordításonként csak egyszer végez beépítést. hogy megfeleljen a kód.1 pontban. tervezzünk módszert a tesztelésére.3-ban leírt „állomány-õrszemekkel” szemben. (*3) Hogyan valósul meg a dinamikus csatolás (szerkesztés) a rendszerünkben? Milyen megszorítások vonatkoznak a dinamikusan szerkesztett kódra? Milyen követelményeknek kell. Nyissunk meg és olvassunk be egy 150 000 karakterbõl álló fájlt. (*2) Rajzoljuk meg a „modulfüggõségi diagramokat” (§9. Készítsünk egy ilyen szerkezeti elemet.3.3. melyek mindegyike 1500 karaktert tartalmaz.292 Alapok 5. egy egyszerû függvényhívással.5. (§8. (*2. 8. amely a megfigyelt fájlon kívül végzi az ellenõrzést. hogy meg lehessen hívni a main()-bõl vagy más függvénybõl is. Van-e eltérés a teljesítményben? Hány fájl lehet egyszerre megnyitva rendszerünkben? Válaszoljuk meg ezeket a kérdéseket a beépített fájlok használatával kapcsolatban is. . (*2) Módosítsuk a számológépet. (*3) Nyissunk meg és olvassunk be 100 fájlt. hogy dinamikusan csatolható legyen? 7.2.2) a számológép azon változataira. és fejtsük ki elõnyeit és hátrányait a §9.5) A külsõ beépítésfigyelõ olyan programelem. 9. Van-e a külsõ beépítésfigyelõknek bármilyen jelentõs futási idõbeli elõnye a rendszerünkben? 6. Tipp: nézzük meg a példát a §21. melyek az error()-t használták kivételek helyett.

illetve bemutatjuk az összefoglaló néven objektumorientált programozásnak és általánosított (generikus) programozásnak nevezett eljárásokat. 12. 13. Fejezetek 10. 14. Osztályok Operátorok túlterhelése Származtatott osztályok Sablonok Kivételkezelés Osztályhierarchiák . 15. 11. milyen lehetõségeket nyújt a C++ nyelv új típusok meghatározására és használatára.Második rész Absztrakciós módszerek Ebben a részben azzal foglalkozunk.

Mert ellenségei azok.. akiknek az új rendelkezések szolgálnak hasznukra.” Niccolo Machiavelli (A fejedelem (§vi).. pusztán lagymatag védelmezõi. akiknek a régi törvények hasznára vannak.„. kétesebb kimenetelû.. Lutter Éva fordítása) . mint új törvények bevezetéséért síkraszállni. nincs nehezebb.. veszélyesebb dolog. azok pedig.

hogy kapcsolataikat hatékonyan használhassuk ki.10 Osztályok „Ezek a típusok nem „elvontak”. Bevezetés A C++ nyelv osztályai azt a célt szolgálják. mint az int és a float. Ezenkívül az öröklõdés (12. fejezet) segítségével úgy szervezhetjük az egymással kapcsolatban álló osztályokat. ugyanannyira valóságosak. . fejezet) és a sablonok (13.” (Doug McIlroy) Fogalmak és osztályok • Osztálytagok • Az elérhetõség szabályozása • Konstruktorok • Statikus tagok • Alapértelmezett másolás • const tagfüggvények • this • struct-ok • Osztályon belüli függvénydefiníciók • Konkrét osztályok • Tagfüggvények és segédfüggvények • Operátorok túlterhelése • A konkrét osztályok használata • Destruktorok • Alapértelmezett konstruktorok • Lokális változók • Felhasználói másolás • new és delete • Tagobjektumok • Tömbök • Statikus tárolás • Ideiglenes változók • Uniók • Tanácsok • Gyakorlatok 10. hogy a programozó a beépített adattípusokkal azonos kényelmi szinten használható új adattípusokat hozhasson létre.1.

y. Ha a programozó alkalmas osztályokat használ. mûveleteivel együtt a valós szám matematikai fogalmának egy megközelítése. amelynek nincs közvetlen megfelelõje a nyelv beépített típusai között. Az alábbiakban az osztályok meghatározásának. -. illetve az osztályba tartozó objektumok létrehozásának és használatának fõbb eszközeit mutatjuk be. a használat módjában nem. m. hogyan ábrázolnánk a „dátum” fogalmát egy Date adatszerkezettel (struktúrával. // ábrázolás . Tagfüggvények Vizsgáljuk meg. struct) és egy sor. A C++ beépített float típusa például a +. esetleges részleteinek (például a tárolandó adatok elrendezésének) elválasztása a típus helyes használatához alapvetõen szükséges tulajdonságoktól. más néven felhasználói típus. 10. vagy list<Paragraph> típusunk egy szövegszerkesztõ programban. A fordítóprogram például felderítheti az objektumok nem szabályos használatát. Lehet például Trunk_line típusunk egy telefonos kapcsolatokat kezelõ programban. például az adatokat elérõ függvények teljes listájától. „konkrét” felhasználói típusokkal foglalkozik. a program tömörebb lesz. Egy programot könnyebb megérteni és módosítani. ha az adott típus adatszerkezetét érintõ összes külsõ használatot és belsõ rendrakó függvényt csak az adott típusra vonatkozó programozási felületen keresztül tesszük elérhetõvé.2. Az osztály egy felhasználói típus.296 Absztrakciós módszerek A típus egy fogalom konkrét ábrázolása. Explosion típusunk egy videójáték számára.2. Osztályok Az osztály (class) a programozó által meghatározott. }. ráadásul sokféle kódelemzõ eljárás használata válik lehetõvé. Ez a fejezet a viszonylag egyszerû. ha abban az általa kezelt fogalmaknak megfelelõ típusok szerepelnek. Ez az elválasztás legjobban úgy fejezhetõ ki. hogy meghatározzunk egy fogalmat. ilyen változókat kezelõ függvénnyel: struct Date { int d. Azért tervezünk új típust. Új típus definiálásakor az alapvetõ szempont a megvalósítás véletlenszerû. amit másként csak egy alapos ellenõrzés során fedezhetnénk fel. * stb. 10.1. Ideális esetben ezek csak létrehozásuk módjukban különböznek a beépített típusoktól.

12. a tagfüggvények meghatározásakor meg kell adnunk az adatszerkezet nevét is: void Date::init(int dd. // kezdeti értékadás // n év hozzáadása // n hónap hozzáadása // n nap hozzáadása }. int yy). my_birthday.1996).10. y = yy. void add_day(Date& d. void add_month(int n). int mm. void init(int dd. today. } .init(16. Ilyen kapcsolatot azáltal hozhatunk létre. // kezdeti értékadás d-nek // n évet ad d-hez // n hónapot ad d-hez // n napot ad d-hez Az adattípus és ezen függvények között nincs kifejezett kapcsolat.init(30. y. void f() { Date today. int n). void add_year(int n). hogy a függvényeket tagfüggvényekként adjuk meg: struct Date { int d. tomorrow.10..add_day(1). void add_day(int n). §10. // . Az osztálydefiníción belül bevezetett függvényeket (a struct is osztály. m. Date tomorrow = today. int n).1950). void add_year(Date& d. int n). A tagfüggvényeket az adatszerkezetek tagjainak elérésére vonatkozó szokásos formában alkalmazhatjuk és csak megfelelõ típusú objektumra: Date my_birthday. void add_month(Date& d. m = mm. int mm.2. int. int).. Osztályok 297 void init_date(Date& d.8) tagfüggvényeknek hívjuk. int yy) { d = dd. } Minthogy a különbözõ adatszerkezeteknek azonos nevû függvényeik is lehetnek. int.

m változóra vonatkozik.m változóra vonatkozna. Ebbõl azonban nem derül ki. Az elsõ. Az elérhetõség szabályozása A Date elõzõ pontbeli deklarációja azon függvények halmazát adja meg. nyilvános (public) rész az osztály nyilvános felülete. az m=mm értékadás a my_birthday. 10. A class X { . amelyek nem definíciók. az m=mm értékadás a today. A tagfüggvényeket a megszokott módon definiálhatjuk és használhatjuk: . Ekkor a név azon objektum megfelelõ tagjára vonatkozik.. hogy nem sértjük meg az egyszeri definiálás szabályát (§9. Történeti okokból az osztálydefiníciót néha osztálydeklarációként említik. amelyek közvetlenül függnek a Date típus ábrázolásától és közvetlenül elérhetik az ilyen típusú objektumokat.2. A megszorítást úgy fejezhetjük ki. hogy milyen objektumra hívták meg.3). A második. public: void init(int dd.2. A public címke két részre osztja az osztály törzsét. A struct szerkezetek egyszerûen olyan osztályok. az osztálydefiníciók az #include utasítás felhasználásával több forrásállományban is szerepeltethetõk.2. feltéve. Amikor például a Date::init() tagfüggvényt alkalmazzuk a today változóra. m. melyekben a tagok alapértelmezett elérhetõsége nyilvános (§10. hogy kizárólag ezek lehetnek mindazok a függvények. melyekkel a Date típusú objektumot kezelhetjük.2. A tagfüggvény mindig „tudja”. amelyre a tagfüggvényt meghívtuk. int yy). int mm. ha struct helyett class-t használunk: class Date { int d.298 Absztrakciós módszerek A tagfüggvényekben a tagokat az objektum kifejezett megadása nélkül is használhatjuk. mert egy új típust határoz meg. void add_month(int n). // kezdeti értékadás // n év hozzáadása // n hónap hozzáadása // n nap hozzáadása }. Ha ugyanezt a tagfüggvényt a my_birthday változóra alkalmaznánk. }. privát (private) részbeli neveket csak a tagfüggvények használhatják..8). void add_day(int n). void add_year(int n). Azon deklarációkhoz hasonlatosan. kifejezést osztálydefiníciónak hívjuk. y.

ezért a címek megfelelõ kezelésével vagy pontosan meghatározott típuskonverzióval megkerülhetõ. akkor elég a tagfüggvényeket ennek megfelelõen módosítanunk. hogy a program egyáltalán lefutna. A privát tagok védelme az osztálytagok név szerinti elérhetõségének korlátozásán múlik.-át). ezért nem kell újraírni (bár lehet.2. hogy megtudja. hogyan lehet használni az osztályt. december 36. hogy újra kell fordítani).y -= 200. Egy általános célú nyelvben csak hardverszinten lehetne a rosszindulatú használat ellen védekezni. akkor biztosak lehetünk abban. 10. hogy az osztály viselkedésének bármilyen módosítása csakis a tagfüggvények megváltoztatásával érhetõ el. Konstruktorok Az init()-hez hasonló függvények használata az objektumok kezdeti értékadására nem elegáns és hibák forrása lehet. az objektumnak értéket adó függvényünk. hogy a leendõ felhasználónak elég a tagfüggvények meghatározását tanulmányoznia ahhoz. Ebbõl következik. } Mindazonáltal a nem tag függvények a privát tagokat nem használhatják: void timewarp(Date& d) { d. részben pedig azért. hogy ez a hiba csak valamelyik tagfüggvényben lehet. ha a tagok elérhetõségét egy pontosan megadott lista függvényeire korlátozzuk. Minthogy sehol sincs lefektetve. Például ha megváltoztatjuk egy osztály adatábrázolását. ha egy hiba miatt a Date érvénytelen értéket kap (mondjuk 1985. mert általában célszerû. a programozó elfelejtheti azt – vagy éppen többször is megteheti (mind- . és igazi rendszerekben még ez is nehezen kivitelezhetõ. a hiba helyének behatárolása már azelõtt megtörténik. Ez egyik esete annak az általános megfigyelésnek. A másik elõny. a csalás ellen.3. hogy a hibakeresés elsõ szakasza. A C++ a véletlen hibák ellen véd.10. Például. mert az adattagok priváttá tétele miatt erre kényszerültünk. ha van egy. Ez persze csalás. // hiba: Date::y privát } Számos elõnnyel jár. nem a védelmi rendszer tudatos megkerülése. Osztályok 299 inline void Date::add_year(int n) { y += n. Az osztályt használó kód közvetlenül csak az osztály nyilvános felületétõl függ. hogy egy objektumnak kezdõértéket kell adni. Az init() függvényt részben azért vettük fel.

Mivel az ilyen függvény létrehozza az adott típusú értékeket. int. int). mint magának az osztálynak: class Date { // . Date(int. azokat meg kell adni: Date today = Date(23. Date(int.6. akkor minden. Date(int). int). Ezt úgy érhetjük el. // rövidített forma // hiba: nincs kezdõérték // hiba: a harmadik paraméter hiányzik Gyakran célszerû. aktuális év // nap. 1983"). hónap. mint más függvényekre (§7. y. ha a kezdeti értékadás többféleképpen is lehetséges. melynek célja kifejezetten az objektumok elõkészítése.12.. hogy megadjon egy olyan függvényt. int). hogy ugyanaz a neve. constructor) hívjuk. m. Date().300 Absztrakciós módszerek két esetben egyformán végzetes következményekkel). // konstruktor Ha egy osztály rendelkezik konstruktorral.. int. Date now. Több gondot igényel ugyanis . // alapértelmezett kezdeti értékadás az aktuális dátummal A Date példa esetében megfigyelhetjük a konstuktorok „elburjánzását”. melyiket kell az egyes meghívásokkor alkalmazni: Date today(4). nap // nap. hónap. }. A konstruktort arról ismerjük meg. Date guy("5 Nov"). Jobb megoldás. A programozó egy osztály tervezésekor mindig kísértést érez új és új függvényekkel bõvíteni azt.. Ha a konstruktornak paraméterekre van szüksége. Date my_birthday. // nap.. Date(const char*).1983). Date release1_0(10. a fordítóprogram ki fogja tudni választani.12). aktuális hónap és év // alapértelmezett Date: mai dátum // a dátum karakterlánccal ábrázolva A konstruktorokra ugyanazok a túlterhelési szabályok vonatkoznak. ami általános jelenség. }. ebbe az osztályba tartozó objektum kap kezdõértéket. ha többféle konstruktor áll rendelkezésre: class Date { int d. public: // . ha lehetõvé tesszük a programozónak. Date xmas(25.1990). valakinek úgyis szüksége lesz rájuk. Amíg a konstruktorok kellõen különböznek a paraméterek típusaiban. Date july4("July 4. Date(int. mondván.4). konstruktornak (vagyis létrehozónak.

melynek jelentése: „vegyük az alapértelmezett today-t”. m. A Date osztály paramétereinek például egy olyan alapértelmezett értéket adhatunk. A day (nap) és month (hónap) paraméterek esetében ez a halmaz világosan meghatározható. int mm. de ez a stílus vezet az eredeti programozón kívül más számára használhatatlan kódhoz. de annak objektumaihoz nem. hogy Date érvényes dátum-e Ha egy paraméter-értéket az alapértelmezett érték jelzésére használunk. mire van igazán szükség és arra szorítkozni.d. Ez a fajta megszorítás az osztályt az eredeti környezeten kívül használhatatlanná teszi. Statikus tagok A Date típushoz tartozó kényelmes alapértelmezett értéket egy jelentõs rejtett probléma árán hoztuk létre: Date osztályunk a today nevû globális változótól függ. class Date { int d. m = mm ? mm : today. // . } // ellenõrizzük.5). public: Date(int dd =0..4. y = yy ? yy : today.2. amikor ilyen környezetfüggõ osztályokat próbálnak használni és a kód módosítása is problematikus lesz. int yy) { d = dd ? dd : today.. A felhasználóknak túl sok kellemetlen meglepetésben lesz részük. Ezzel az egy „kis globális változóval” még talán megbirkózunk. Osztályok 301 mérlegelni. Az egymással rokon függvények számának csökkentésére az egyik mód az alapértelmezett paraméter-értékek használata (§7. A statikus tagokból mindig pontosan egy példány . az idõszámításunk utáni elsõ év (year==1) közvetlenül az idõszámításunk elõtti elsõ év (year==-1) után következik. y. Date::Date(int dd. Szerencsére az európai naptárban nincs 0-dik év.y. int mm =0. Ez az odafigyelés ugyanakkor általában kisebb és érthetõbb programokhoz vezet. 10. Kerüljük el! Szerencsére a kívánt célt elérhetjük a nyilvánosan elérhetõ globális változó jelentette tehertétel nélkül is. Így az osztály csak akkor használható. ha a today változót definiáltuk és minden kódrészletben megfelelõen használjuk. statikus tagnak nevezzük. melyek egy osztályhoz tartoznak. }.10. a year (év) mezõnél azonban a zéró érték nem esik nyilvánvalóan a halmazon kívülre. annak kívül kell esnie a lehetséges értékek halmazán.m. int yy =0). Az olyan változókat.

int. ekkor az osztály nevével minõsíthetjük: void f() { Date::set_default(4. static void set_default(int. statikus tagfüggvénynek hívjuk. }. int yy =0). int m.m.y). sõt. int y) { Date::default_date = Date(d. y. static Date default_date. m = mm ? mm : default_date. A Date konstruktort immár így határozhatjuk meg: Date::Date(int dd. Tervezzük át az osztályt úgy.12. melyek egy adott osztálytaghoz hozzáférnek. y = yy ? yy : default_date. hogy Date érvényes dátum-e Amikor szükséges.1770). int yy) { d = dd ? dd : default_date.m. } .1945)..d. int).. int mm. // . Egy statikus tagra ugyanúgy hivatkozhatunk. hogy megõrizzük az alapértelmezett konstruktor-értékek szerepét. nem statikus adattagokból. akár egy objektum megnevezése nélkül is.y. mint a közönséges. nem pedig objektumonként egy. Ehhez hasonlóan az olyan függvényeket. void Date::set_default(int d.302 Absztrakciós módszerek létezik. mint bármilyen más tagra. m. módosíthatjuk az alapértelmezett értéket.5. } A statikus tagokat – mind az adattagokat. public: Date(int dd =0. de közben elkerüljük a globális változó használatának hátrányát: class Date { int d. mind a függvényeket – definiálni kell valahol: Date Date::default_date(16. } // ellenõrizzük. de nem szükséges objektumra meghívni azokat. int mm =0.

még akkor is. Osztály típusú objektumok másolása Alapértelmezés szerint az osztály típusú objektumok másolhatók és kezdõértékként egy azonos típusú osztály egy objektumának másolatát is kaphatják.10. a programozó megadhatja a megfelelõ értékadó operátort (§10. amíg valaki át nem állítja valami másra. . 10. } int year() const. Vegyük észre.) Ennek megfelelõen az osztály objektumokat alapértelmezés szerint értékadással is másolhatjuk: void f(Date& d) { d = today.2. } int month() const { return m.2. Következésképpen nincs szükség külön függvényre az alapértelmezett dátum lekérdezéséhez. }.4. az X::X(const X&) másoló konstruktorral megváltoztathatjuk azt. Osztályok 303 Az alapértelmezett érték itt Beethoven születési dátuma. hogy a Date() jelölés a Date::default_date értéket szolgáltatja: Date copy_of_default_date = Date(). // kezdeti értékadás másolással Alapértelmezés szerint az osztály objektum másolata minden tag másolatából áll. amelyekkel kiolvashatjuk az évet. Konstans tagfüggvények A Date osztályhoz eddig olyan tagfüggvényeket adtunk. (Erre a §10. y. de az érték lekérdezésére sajnos nem adtunk lehetõséget.4. // .. Ha ez nem megfelelõ egy osztály számára. Ha nem ez a megfelelõ viselkedés egy X osztály számára.5. } Az alapértelmezett viselkedés itt is a tagonkénti másolás.6.1). 10. ha készítünk néhány függvényt.4. Ezen könnyen segíthetünk. m. a hónapot és a napot: class Date { int d.4. ha konstruktorokat is megadtunk: Date d = today. public: int day() const { return d..1 pontban részletesebben is visszatérünk. melyek értéket adnak egy Date objektumnak vagy megváltoztatják azt.

ha visszaadunk egy. .7.year(). a const utótagot ki kell írnunk: inline int Date::year() const // helyes { return y. Ez azt jelenti. egymással kapcsolatban levõ frissítõ függvények esetében sokszor hasznos. add_month(). hogy azok nem adnak vissza értéket. } // hiba: a const minõsítõ hiányzik a tagfüggvény típusából Vagyis a const minõsítés része a Date::day() és Date::year() függvények típusának. mert a mûveleteket ekkor láncba kapcsolhatjuk („láncolhatjuk”). a frissített objektumra mutató referenciát. cd. értéke nem módosítható } 10. és add_year() állapotfrissítõ függvényeket úgy határoztuk meg. Természetesen a fordítóprogram megakadályozza. // rendben d. hogy ezek a függvények nem változtatják meg az objektum állapotát. // rendben int j = cd.304 Absztrakciós módszerek Vegyük észre a const minõsítõt a függvénydeklarációkban az (üres) paraméterlista után.add_year(1). Egy konstans tagfüggvényt alkalmazhatunk állandó (konstans) és változó (nem konstans) objektumokra is.add_year(1). const Date& cd) { int i = d. // hiba: kísérlet tag értékének módosítására konstans függvényben } Ha egy konstans tagfüggvényt osztályán kívül határozzuk meg. Önhivatkozás Az add_year(). } inline int Date::year() { return y. Az ilyen.2. // rendben // hiba: cd konstans.year(). a nem konstans tagfüggvényeket viszont csak nem konstans objektumokra: void f(Date& d. hogy véletlenül megszegjük ezt az ígéretet: inline int Date::year() const { return y++.

amelyre a tagfüggvényt meghívták. d.. Az X osztály egy konstans tagfüggvényben a this típusa const X* lesz. így pontosan hivatkozhat rá: Date& Date::add_year(int n) { if (d==29 && m==2 && !leapyear(y+n)) { d = 1.) Egy nem statikus tagfüggvényben a this kulcsszó egy mutatót jelent arra az objektumra.. Mindazonáltal a this nem közönséges változó. amelyre a függvényt meghívták. hogy ne lehessen megváltoztatni magát az objektumot (lásd még §5. (Egyenértékû a Simula nyelv THIS és a Smalltalk self kifejezésével. Date& add_year(int n). Minden (nem statikus) tagfüggvény tudja. } // figyeljünk február 29-re! A *this kifejezés azt az objektumot jelenti..4. } Ezzel egy napot.10.add_year(1). Osztályok 305 Tegyük fel. // .. } y += n. Date& add_month(int n). m = 3.. így nem lehet a címét felhasználni vagy értéket adni neki. . // n év hozzáadása // n hónap hozzáadása // n nap hozzáadása }.add_month(1). return *this. melyik objektumra hívták meg.. Date& add_day(int n).add_day(1). Ehhez viszont minden függvényt úgy kell megadnunk. hogy azok egy Date típusú referenciát adjanak vissza: class Date { // .1). hogy a következõt szeretnénk írni: void f(Date& d) { // . egy hónapot és egy évet adunk d-hez. Az X osztály egy nem const tagfüggvényében a this típusa X*.

3.. string string_rep() const. this->m = 3. ám fáradságos módon így is megadhattuk volna: Date& Date::add_year(int n) { if (this->d==29 && this->m==2 && !leapyear(this->y+n)) { this->d = 1. string cache. hogy egy tagfüggvény logikailag állandó.7. A felhasználó számára a függvény nem módosítja az objektum állapotát. Egy ilyen ábrázolás felépítése idõigényes feladat. a felhasználó által közvetlenül nem látható részlet megváltozik. ezért érdemes egy példányt tárolni belõle. mégis meg kell változtatnia egy tag értékét..2.1. amíg a Date értéke meg nem változik. // . amit az egymást követõ lekérdezések mind felhasználhatnak. cache) inkább bonyolultabb adatszerkezeteknél használatos. 10. Fizikai és logikai konstansok Esetenként elõfordulhat. } this->y += n.7. Például minden nem statikus tagra való hivatkozás tulajdonképpen a this-t használja. return *this. hogy a megfelelõ objektum tagját érje el.4). Az ilyen helyzetet gyakran hívják logikai konstans mivoltnak. de valamilyen. } A this-t meghatározott (explicit) módon gyakran láncolt listák kezelésére használjuk (például §24. // gyorstár feltöltése // ábrázolás karakterlánccal .. de nézzük meg. Az add_year függvényt például egyenértékû. }. melyet a felhasználó a kimenetben felhasználhat. void compute_cache_value().306 Absztrakciós módszerek A this használata legtöbbször automatikus. public: // . A Date osztályt például egy függvény visszatérési értéke egy karakterlánccal ábrázolhatja.. hogyan mûködhetne ez a Date osztály esetében: class Date { bool cache_valid. Ilyen belsõ gyorsítótár (gyorstár.

// nem meghatározott viselkedés A d1 változó esetében a string_rep() egyszerûen az eredeti típusra alakít vissza.string_rep() hívás nem biztos.4. mutable string cache. // . th->compute_cache_value(). }. string s1 = d1. public: // .1) használtuk. hogy egy Date* típusú mutatót kapjunk a this-re.. Ez aligha elegáns megoldás. const Date d2.10.string_rep().2. Ezért a d2. Ám d2-t konstansként adtuk meg és az adott nyelvi változat esetleg valamilyen memória-védelmet alkalmaz az állandó értékek megõrzésére. ezért világos. az adott nyelvi változattól független eredménnyel fog járni. ha a gyorsítótárba kerülõ adatokat „változékony”-ként (mutable) adjuk meg: class Date { mutable bool cache_valid. void compute_cache_value() const. Osztályok 307 A felhasználó szemszögébõl a string_rep függvény nem változtatja meg az objektum állapotát. hogy pontosan meghatározható. hogy egy eredetileg is állandóként megadott objektum esetében is mûködik: Date d1. hogy konstans tagfüggvénynek kell lennie. A mutable minõsítõ Az elõbb leírt típuskényszerítés (a const minõsítõ átalakítása) és a vele járó.7..string_rep().. Másrészt a gyorsítótárat fel kell tölteni a használat elõtt.2. } return cache. string string_rep() const. így a dolog mûködik. string s2 = d2.2. megvalósítástól függõ viselkedés elkerülhetõ. } // konstans elvetése Vagyis a const_cast operátort (§15. // (változékony) gyorstár feltöltése // ábrázolás karakterlánccal . 10. és nem biztos. Ezt elérhetjük típuskényszerítés alkalmazásával is: string Date::string_rep() const { if (cache_valid == false) { Date* th = const_cast<Date*>(this).. th->cache_valid = true.

Vagyis a mutable azt jelenti. cache_valid = true..6) // a gyorstár által mutatott elem feltöltése // ábrázolás karakterlánccal . de a tagok többsége módosulhat. string s4 = d4.. hogy akkor is módosítható legyen. hogy „soha nem állandó”. hogy a tagot úgy kell tárolni. } Ezáltal a string_rep()-et megfelelõen használatba vehetjük: Date d3. void compute_cache_value() const.4. jobb a változó adatrészt külön objektumba tenni és közvetett úton elérni. // rendben! A tagok változékonyként való megadása akkor alkalmas leginkább. } // kezdeti értékadás a konstruktorban (§10. // . Ha az objektum logikailag változatlan marad. string Date::string_rep() const { if (!c->valid) { compute_cache_value().string_rep().string_rep(). } return cache. ha az ábrázolásnak csak egy része változhat. string rep. } return c->rep. ha konstans objektum. c->valid = true.308 Absztrakciós módszerek A mutable tárolási minõsítés azt jelenti. Ezt felhasználva egyszerûsíthetünk a string_rep() meghatározásán: string Date::string_rep() const { if (!cache_valid) { compute_cache_value(). class Date { cache* c.. }. Ezzel a módszerrel a gyorsítótárba helyezett karakterláncot tartalmazó program így írható meg: struct cache { bool valid. public: // .. string s3 = d3. string string_rep() const. }. const Date d4.

„lusta” vagy takaros kiértékelés (lazy evaluation) különféle formáira is átvihetõk. melynek tagjai alapértelmezés szerint nyilvánosak. int yy). mintsem a típus tulajdonságait garantálják (mint az invariánsok. Én általában azokat az osztályokat adom meg struct-ként. A konstruktorok és lekérdezõ függvények nagyon hasznosak lehetnek a struktúrák számára is. egyszerûen rövidítése az alábbinak: class s { public: . hogy a következõ tagok privát elérésûek. Struktúrák és osztályok Definíció szerint a struktúra (struct). Ezekre az osztályokra úgy gondolok.7..8. amelyekben minden tag nyilvános. int yy).2. m. }. olyan osztály.3. // n év hozzáadása A választott stílust csak a körülmények és az egyéni ízlés határozza meg. int mm. Attól eltekintve. public: Date2(int dd. y. . void add_year(int n).10. hogy a nevek különböznek. A private: elérhetõségi minõsítés annak jelzésére használható. lásd §24. Osztályok 309 A gyorsítótárat támogató eljárások az ún. az alábbi deklarációk egyenértékûek: class Date1 { int d. csak adatszerkezetek. }. // n év hozzáadása struct Date2 { private: int d. hogy a következõ tagok nyilvánosak. m..1). mint amik nem igazi típusok. Vagyis a struct s { . int mm. de inkább csak jelölésbeli könnyebbséget jelentenek.. void add_year(int n). public: Date1(int dd.. 10. a public: pedig azt mondja. y.

int mm. Osztályon belüli függvénydefiníciók Az osztályon belül definiált (nem csak deklarált) függvények helyben kifejtett (inline) tagfüggvénynek számítanak. public: void add_year(int n). Több privát rész használata szintén ezt eredményezi. Persze az osztályhoz hasonlóan jelentésének minden felhasználásakor azonosnak kell lennie (§9. int yy). y.3). }. . amelyben szerepel. Vagyis az osztály meghatározásán belüli kifejtés kicsi. private: int d. az osztályon belül kifejtett függvény is szerepelhet több fordítási egységben (az #include utasítással beépítve). // n év hozzáadása Ha azonban a deklaráció több nyilvános részt is tartalmaz (mint a Date4 osztálynál). ahol általában mind a nyilvános felület. int mm. 10. Ahhoz az osztály-definicióhoz hasonlóan. mint a tankönyvi példákban. azaz a fordítóprogram a függvény meghívása helyett közvetlenül beilleszti a kódot. de gyakran használt függvények számára hasznos. hogy az elérhetõségi minõsítések ismétlõdhetnek. m. mind a tényleges megvalósítás terjedelmesebb.2. private: int d. m.310 Absztrakciós módszerek Az osztályokban nem szükséges elõször az adattagokat megadni. akkor a kód zavarossá válhat. Mindazonáltal a számítógép által elkészített kódok számára kedvezõ.9. Az elérhetõségi minõsítéseket az osztálydeklarációkon belül többször is használhatjuk: class Date4 { public: Date4(int dd. }. sõt. sokszor jobb azokat a deklaráció végére tenni. hogy kihangsúlyozzuk a nyilvános felhasználói felületet alkotó függvényeket: class Date3 { public: Date3(int dd. // n év hozzáadása Valódi kódban. rendszerint a Date3 stílusát részesítem elõnyben. void add_year(int n).2. y. int yy).

karakterláncokat. csomópontokat. (érték–egység) párokat. hogy egy program közvetetten támaszkodik ezen típusok némelyikére és még többre közvetlenül. forráskód-helyeket. rögzített pontos számokat. téglalapokat. törtrésszel bíró számokat. mivel egy osztály egy tagfüggvénye az osztály minden tagjára hivatkozhat. értékkészleteket. pénznemeket. mutatókat. vektorokat és tömböket. private: int d. Számos program használ egyszerû. } // . dátumokat. // zavaró lehet // return Date::d Ez szabályos C++-kód. idõpontokat. // . pontokat. Most az egyszerû és hatékony tervezésre helyezzük a hangsúlyt és azt mutatjuk be. inline int Date::day() const { return d. y. konkrét típusokkal ábrázolva: latin vagy kínai karaktereket. kapcsolatokat. BCD karaktereket.. }. . komplex számokat. koordinátákat.. }. y. kisebb gondhoz vezet az adatábrázolást felhasználó nyilvános „inline” függvények tekintetében. mintha az osztály definiciója már a tagfüggvény-törzsek beolvasása elõtt teljes lett volna.3. private: int d. (mutató–eltolás (offset)) párokat. de sûrûn elõforduló elvont fogalmakat. vagy az „inline” tagfüggvényeket az osztály után fejtem ki: class Date { public: int day() const. könyvtárak közvetítésével. Hatékony felhasználói típusok Az elõzõ Date osztály példáján bemutattuk az osztályok meghatározásához szükséges alapvetõ nyelvi elemeket. m. m. } 10.. ezért én vagy elõreveszem az adatokat. Vegyük ezt a példát: class Date { public: int day() const { return d.10. hogy az egyes nyelvi elemek hogyan támogatják ezt. vonalakat. Gyakran elõfordul.. A kódot olvasó programozót azonban ez megzavarhatja. Osztályok 311 Az a stílus. lemezcímeket. mely szerint az adattagokat az osztály definiciójának végére helyezzük. lebegõpontos számokat.

Tehát szükség van olyan eljárásokra.3). dec }. itt is érvényes. hogy az egyszerû és földhözragadt sokkal jelentõsebb. mar.2. // függvények a Date módosításához Date& add_year(int n). Month. int yy =0).4 és §12. static Date default_date. int year() const. mert ezek az „elegáns” programozás alapkövei. may. static void set_default(int. Date(int dd =0. jun.4). void char_rep(char s[ ]) const. class Bad_date { }. int). // n év hozzáadása // n hónap hozzáadása // n nap hozzáadása // ábrázolás // ábrázolás karakterlánccal // ábrázolás C stílusú karakterlánccal // kivételosztály // 0 jelentése "vedd az // alapértelmezettet" . m. Month mm =Month(0). }. melyekkel a felhasználó adott célú típusokat adhat meg. sep. // függvények a Date vizsgálatához int day() const. nov. oct. mint a bonyolult és körmönfont. Az ilyen típusokat konkrét típusoknak vagy konkrét osztályoknak hívjuk. Egy általános célú programozási nyelv tervezõje nem is láthatja elõre az egyes alkalmazások igényeit. string string_rep() const. Month month() const. private: int d. hogy megkülönböztessük õket az absztrakt (elvont) osztályoktól (§12. hogy az ilyen felhasználói típusok megadását és hatékony használatát is támogassa. jul. A C++ nyelv egyik kifejezett célja volt. Date& add_day(int n). ám számuk miatt nem lehetséges az összeset közvetlenül támogatni.312 Absztrakciós módszerek A C++ más programozási nyelvekkel egyetemben közvetlenül támogat néhányat a fenti típusok közül. feb. y. Mint általában. Date& add_month(int n). Ennek fényében készítsünk egy jobb dátumosztályt: class Date { public: // nyilvános felület enum Month { jan=1. apr. illetve az osztályhierarchiák osztályaitól (§12. aug.

A §10. amit az okoz. mint a felhasználói felületet nem érintõ megvalósítási részlettel. hogy jobb történész szakértõkre bízni. mint a Month. mint önálló típusokat bevezessem.7. Majdnem minden ilyen hiba amúgy is kiderül futási idõben – nemigen dolgozom olyan dátumokkal. hogy nem módosítják annak az objektumnak vagy változónak az állapotát.jul. melyek segítségével a Date-ek szabadon másolhatók. mint a 27-ik év július 26-ika. hogy a napok és évek ábrázolására a Day-t és a Year-t.1901). 3. Osztályok 313 A végezhetõ mûveletek ilyen halmaza meglehetõsen jellemzõ a felhasználói adattípusokra.jul. illetve az egyes elemek szerepével való bajlódás nélkül is meghívhatók. A következõk szerepelnek benne: 1. Automatikusan definiált mûveletek. 5.10. kiegészíthetjük vele az osztályt. hogy a Date(1995. Gondolkodtam azon. Az 1800 elõtti történelmi dátumok kezelése annyira bonyolult.jan. melyek az ábrázolás vagy a konkrét megvalósítás ismerete. amelyre meghívták õket.7)-nek vagy európai stílusban Date(7. hogy emlékeznünk kell rá: vajon június 7-ét amerikai stílusban Date(6.1995) összekeveredésének veszélyét elkerüljem. 4.1. Ezek const minõsítése jelzi. Egy konstruktor. hogy kezeljem azt a problémát. Az alapértelmezett paraméter-értékek kezelésére is gondoltam. Ha mégis szükséges.6)-nak kell-e írnunk. A Bad_date osztály. A Date objektumokat és változókat kezelõ függvények. Ezenkívül pedig egy „valahanyadikát” nem lehet rendesen ellenõrizni a hónap és az év ismerete nélkül. mellyel a hibák mint kivételek jelezhetõk.1-ben említett gyorsítótáras (cache) módszer egy ilyen egyszerû típusnál felesleges. A Month (hónap) típust azért vezettem be.7.27) és a Date(27. amely kezdõértéket ad az objektumoknak és változóknak 2. (Egy alkalmas Year típus meghatározására nézve lásd: §11.2. Lekérdezõ függvények.) Az alapértelmezett dátumot mint érvényes Date objektumot definiálni kell valahol: Date Date::default_date(22. . így kihagytam. Ezek a típusok azonban nem lennének annyira hasznosak. melyekkel egy Date-et megvizsgálhatunk. ezzel külön eljárás foglalkozik.

Figyeljük meg a Date::feb jelölést. struct Date { int day. } if (midnight()) d.[2]). hogy a << kimeneti és a + összeadó mûvelet a Date-ekre definiált. hogy a dátum fogalma „szétszóródna”. hogy lehet Date-eket használni: void f(Date& d) { Date lvb_day = Date(16. Ha egy fogalmat egyszerû adatszerkezetként bocsátunk a felhasználók rendelkezésére. mint egy dátum? Végül is beérhetnénk egy egyszerû adatszerkezettel...month()==Date::feb) { // . Az év-hónap-nap adatábrázolás ráadásul sok program számára szegényes. mégis gondot igényel úgy megírni.day()==29 && d. mit csinálnak vele. year. (ezt a §10.year()). csak a kijelölt függvényeket kell módosítanunk. De ha ezt tennénk. month. hogy helyesen mûködjék. } cout << "A következõ nap:" << d+1 << '\n'.d.314 Absztrakciós módszerek Íme egy kicsi elméleti példa arra... azzal a ténnyel. Ez pedig azzal járna. így meg kell adni.6. }. az szükségszerûen külön munkát igényel tõlük. hogy a Date-nek és nem valami másnak a feb-jérõl van szó. Ha viszont úgy döntünk. vagy külön függvényekben.. csak a Date tagfüggvényeit kellene megváltoztatnunk (§10. akkor minden felhasználónak magának kellene a Date-ek összetevõit kezelnie: vagy közvetlenül. hogy a programozók döntsék el. Ezenkívül bár a Date típus látszólag egyszerû. így azt nehezebb lenne megérteni. január elseje utáni vagy elõtti napok számával akarnánk ábrázolni. Feltételezzük. if (d. Ha a Date-et például az 1970. hogy megváltoztatjuk.és hagynánk. . . dokumentálni és módosítani. Például egy Date objektum növeléséhez szökõévekkel kell törõdni. Az f() nem tagfüggvénye Date-nek.Date::dec. hogy a hónapok különbözõ hosszúságúak és így tovább (lásd a §10. Miért éri meg egy külön típust megadni egy olyan egyszerû dolog számára.3-ban valóban meg is tesszük).add_day(1).3..6[1]-es feladatot).

3.1994) esetében. mint például a Date(30. break. Íme a Date konstruktorának definíciója: Date::Date(int dd. A többi tagfüggvény számíthat erre az állapotra és kötelessége fenntartani azt. y = yy.Date::Feb. break. } A konstruktor ellenõrzi.1-es pontot). Más szóval a konstruktor felállítja az osztályra jellemzõ invariánst (ebben az esetben azt. . Ha nem.month(). Month mm. case jan: case mar: case may: case jul: case aug: case oct: case dec: max = 31. if (dd == 0) dd = default_date.day(). if (mm == 0) mm = default_date. d = dd. kivételt vált ki (§8. Ha a kapott adatok elfogadhatóak. a kezdeti értékadás megtörténik. Másfelõl ha a Date objektum már létrejött.3. int yy) { if (yy == 0) yy = default_date. amely jelzi. amit nem lehet figyelmen kívül hagyni. default: throw Bad_date(). Ez meglehetõsen jellemzõ eljárásmód. switch (mm) { case feb: max = 28+leapyear(yy). // valaki csalt } if (dd<1 || max<dd) throw Bad_date(). Tagfüggvények Természetesen minden tagfüggvényt ki kell fejteni valahol.3. 14. break. akkor az további ellenõrzés nélkül felhasználható és másolható. hogy olyan jellegû hiba történt. int max. hogy a kapott adatok érvényes dátumot adnak-e.1.10. Ez a tervezési módszer óriási mértékben leegyszerûsítheti a kódot (lásd a 24. case apr: case jun: case sep: case nov: max = 30.7. Osztályok 315 10. m = mm. fejezet).year(). hogy egy érvényes dátumról van szó).

7. } aa. Vegyük észre. de jobb egy nyilvánvalóan érvénytelen értéket használni erre a célra. hogy a 0 értéket azért használhatjuk. if (n>0) { int delta_y = n/12. } Date& Date::add_month(int n) { if (n==0) return *this. mint a kivételek elkapásán alapuló. int mm = m+n%12. hogy az adatellenõrzést külön. if (12 < mm) { delta_y++. amely bonyolultabb és kevésbé hatékony. hogy a >> mûvelet értelmezett a Date osztályra: void fill(vector<Date>& aa) { while (cin) { Date d. egy is_date() függvénybe teszem. Például: inline int Date::day() const { return d. mint hogy olyan látszatot keltsünk.8). hogy 13 hónap van egy évben. A Month felsorolásban megadhatnánk egy értéket kifejezetten ennek jelzésére. } } // lásd §3.push_back(d). mm -= 12. a tagfüggvények meghatározása a triviális és a nem túl bonyolult között mozog. mert az a Month felsorolás biztosított garantált értéktartományba esik (§4. } catch (Date::Bad_date) { // saját hibakezelõ continue. try { cin >> d. Tegyük fel például. de ez olyan kódhoz vezetne. Gondolkodtam azon.316 Absztrakciós módszerek A Month(0) értéket (amely nem jelent igazi hónapot) a „vegyük az alapértelmezett hónapot” jelzésére használjuk. } // megjegyzés: int(dec)==12 .3 Mint az ilyen egyszerû konkrét osztályok esetében szokásos.

Date b). m = Month(mm).1): #include "Date. Hogyan kapcsolódnak az ilyen segédfüggvények a Date osztályhoz? Hagyományosan a deklarációjukat az osztály deklarációjával azonos fájlba tennénk. mert nincs szükségük a belsõ adatábrázolás közvetlen elérésére: int diff(Date a. Date next_saturday(Date d). 10. így azon felhasználók számára. akiknek szükségük van a Date osztályra.h" A Date.3.. rögtön ezek is rendelkezésre állnának a felületet leíró fejállomány beépítése után (§9. Date b).a] tartományban bool leapyear(int y).2): namespace Chrono { class Date { /* .2.2. } // negatív n kezelése } return *this. hogy az osztályt és segédfüggvényeit egy névtérbe foglaljuk (§8. // napok száma az [a.h fejállomány használata mellett vagy helyett a segédfüggvények és az osztály kapcsolatát úgy tehetjük nyilvánvalóvá. return *this. az bonyolultabbá tenné az osztály felületét és a belsõ adatábrázolás esetleges módosításakor több függvényt kellene ellenõrizni. Ha ezeket a függvényeket magában az osztályban fejtenénk ki. */}. bool leapyear(int y). Osztályok 317 // most azok az esetek jönnek. int diff(Date a.b] vagy [b. amikor Month(mm)-nek nincs d napja y += delta_y. Date next_weekday(Date d).. // dátumkezelõ szolgáltatások .10. melyeket nem szükséges magában az osztályban tagként megadni. Segédfüggvények Egy osztályhoz általában számos olyan függvény tartozhat.

.. A Chrono névtér természetesen a többi kapcsolódó osztályt is tartalmazná. Date d). Operátorok túlterhelése Gyakran hasznos lehet olyan függvényeket felvenni. ostream& operator<<(ostream&. a vektorok (§3.. Date). int n).year(). Date& operator-=(Date& d. bool operator>(Date.day()==b.4) – esetében ezek használata annyira beidegzõdött a felhasználóknál. Az operator== függvény például lehetõvé teszi az == egyenlõségi operátor használatát a Date objektumokra: inline bool operator==(Date a.318 Absztrakciós módszerek } Date next_weekday(Date d). Az operátorok túlterhelésével a 11. amelyek a hagyományos jelölésmód használatát biztosítják. hogy szinte kötelezõ megadni õket.day() && a.3). // . // egyenlõtlenség // kisebb // nagyobb // Date növelése egy nappal // Date csökkentése egy nappal // n nap hozzáadása // n nap kivonása // n nap hozzáadása // n nap kivonása // d kiírása // beolvasás d-be A Date osztály számára ezen operátorok használhatósága pusztán kényelmi szempontnak tûnik. int n).year()==b.7.3. Date& d). Date). Egy egyetlen osztályt tartalmazó névtér használata általában csak túlbonyolított. istream& operator>>(istream&. int n). bool operator<(Date. Date b) // egyenlõség { return a. például a Time (Idõ) és Stopwatch (Stopper) osztályokat és azok segédfüggvényeit is. Date operator+(Date d. int n). } Egyéb kézenfekvõ jelöltek: bool operator!=(Date. 10. Ám sok típus – például a komplex számok (§11.1) és a függvényszerû objektumok (§18. Date). Date& operator+=(Date& d. Date next_saturday(Date d). Date& operator++(Date& d). Date& operator--(Date& d). fejezet foglalkozik.month() && a.. kényelmetlen kódhoz vezet.. Date operator-(Date d.month()==b.3. // .

}. hogy a felhasználónak eszközt adjunk a kezébe egy konkrét osztály viselkedésének megváltoztatására.6. fejezetben tárgyalt öröklõdési eljárást úgy használhatjuk fel egy új típus meghatározására. A memóriakiosztásnak más nyelvekkel.10. Time t). akkor írhatunk egy másikat. hogy csak az eltéréseket kell leírnunk. Ez az adott típus „újrahasznosításával” is elérhetjük. ami a kívánalmaknak megfelelõen mûködik. Ha nem tetszik egy konkrét típus viselkedése. hogy többalakú (polimorf) viselkedést tanúsítsanak (§2. ezért az objektumok számára helyet foglalhatunk a futási veremben is. használatukat pedig értékközpontú programozásnak (value-oriented programming). viszonylag egyszerû dolgot jól és hatékonyan csináljanak.2). azaz a szabad tárat érintõ mûveletek nélkül. Egy valamirevaló fordítóprogrammal egy. külön erõfeszítés nélkül megoldható..4. // . Time t).6). §12. a Date-hez hasonló konkrét osztály használata nem jár a szükséges tárolóhely vagy a futási idõ rejtett növekedésével. Így a konkrét osztályokat nem szánjuk arra sem. amit gyakran objektum-orientált programozásnak hívnak (§2.3). Date_and_time(int d. mint egy int-et: class Date_and_time { private: Date d..3. public: Date_and_time(Date d. Date::Month m. Használati modelljük és mögötte levõ „filozófia” nagyon különbözik attól.5. int y. . A Vec osztályt például a vector alapján készíthetjük el (§3. így a helyben fordítás egyszerû feladat. A konkrét osztályok mérete fordítási idõben ismert.2). Általában nem cél. a típust pontosan úgy használhatjuk fel az új típus megvalósításához. Time t. A memóriakiosztás is ismert.4) és az osztályhierarchiáktól (12. illetve hogy hangsúlyozzam az olyan beépített típusokkal való hasonlóságukat. Ezeket értéktípusoknak (value types) is nevezik.5.5. Osztályok 319 10. A konkrét osztályok jelentõsége Azért hívjuk a Date és más egyszerû felhasználói típusokat konkrét típusoknak. hogy megkülönböztessem azokat az absztrakt osztályoktól (§2. A 12.2. A konkrét osztályok dolga az. hogy egyetlen. mint az int vagy a float.7. például a C-vel vagy a Fortrannal való összeegyeztetése is hasonlóan könnyen.

Vegyünk például egy valamilyen Name típusú elemek táblázatát tartalmazó Table osztályt.. destructor) függvények. .1. 10. Esetenként az ilyen környezet létrehozása valamilyen erõforrás – fájl. de hatékony típusok.7). A konstruktornak le kell foglalnia az elemek tárolásához szükséges memóriát. // . zár. illetve azt.4. vagyis a konstruktorok hozzák létre azt a környezetet. egy dinamikusan létrehozott objektumot törölnek és így tovább.4. a rájuk vonatkozó szabályokat. hogy felszabadítsa a konstruktorban lefoglalt memóriaterületet. az objektumok kezdõállapotát beállító konstruktorokat és a használatból kikerülõ objektumok „eltakarítására” szolgáló destruktorokat tárgyaljuk. Destruktorok Az objektumok kezdõállapotát a konstruktorok állítják be. hogy a programozó kifejezetten meghívja a destruktort (§10. a memóriát fel kell szabadítani. Ha a Table objektum bármilyen módon törlõdik. memóriaterület – lefoglalásával jár. Nagyon különleges esetben van csak szükség arra.320 Absztrakciós módszerek Az ilyen egyszerû típusok megfelelõ halmaza teljes programok alapjául szolgálhat. Feladatuk általában a rendbetétel és az erõforrások felszabadítása. Az alábbiakban ezeket a lehetõségeket. hogy minden programozó megírja az „egyszerû és sûrûn használt” adatszerkezeteket közvetlenül kezelõ kódot. A destruktorok automatikusan meghívódnak. amit a használat után fel kell szabadítani (§14. akkor a túl általános és „költséges” osztályok használata komoly futási idõbeli és tárfelhasználás-beli pazarláshoz vezethet. amikor egy automatikus változót tartalmazó blokk lefut. 10. Ha egy alkalmazásban nincsenek meg a megfelelõ kicsi. A destruktor legjellemzõbb feladata.4.. }. amikor egy objektum létrejön: ezek a destruktor (megsemmisítõ.4. hasonlóan ahhoz. hogy megírjuk a konstruktort kiegészítõ függvényt: class Name { const char* s.11). osztályok tagjai stb. Objektumok Objektumok többféleképpen jöhetnek létre: lehetnek automatikus vagy globális változók. Következésképpen némelyik osztálynak szüksége van egy olyan függvényre. A konkrét típusok hiánya másfelõl zavaros programokat eredményez. Ezt úgy érhetjük el. ahogy a konstruktor meghívására is biztosan sor kerül. amelyben a tagfüggvények mûködnek. amikor egy objektum megsemmisül. hogy máshol fel lehessen majd használni. amely biztosan meghívódik.

máskülönben szükség esetén megpróbál létrehozni egyet. size_t sz. Ezekre úgy tekinthetünk. amelyiket paraméter nélkül hívhatjuk meg. mint amelyeknél egy olyan destruktorunk van. }. mint amelynek van alapértelmezett konstruktora. A destruktort jelentõ ~Table() jelölés a komplemensképzést jelölõ ~ szimbólumot használva utal a destruktornak a Table() konstruktorhoz való viszonyára.2. } ~Table() { delete[ ] p. A standard könyvtár tárolói. int vi[10]. ennek a módszernek valamelyik változatát használják. A fordítóprogram által létrehozott alapértelmezett konstruktor automatikusan meghívja az osztály típusú tagok és a bázisosztályok (§12.) A leírtak alkalmazhatóak a destruktor nélküli osztályokra is. Osztályok 321 class Table { Name* p. hogy az elemeik számára tárolóhelyet biztosítsanak. amikor valamelyik standard könyvtárbeli tárolót használja. // konstruktor // destruktor }. Minthogy a fenti példában a 15 mint alapértelmezett érték adott. 10. Tables tt.2) alapértelmezett konstruktorát: struct Tables { int i.4. a Table::Table(size_t) függvény alapértelmezett konstruktor.10. Ha a programozó megadott alapértelmezett konstruktort. Az alapértelmezett konstruktor az. Table t1.2. például a map. ezért a programozó a következõkben leírtakra támaszkodik. Az összetartozó konstruktor–destruktor pár meghatározása a C++-ban szokásos eljárás változó méretû objektumok megvalósítására. Alapértelmezett konstruktorok Hasonlóképpen a legtöbb típust úgy tekinthetjük. akkor a fordítóprogram azt fogja használni. amely nem csinál semmit. Table vt[10]. } Name* lookup(const char *).(Így viselkedik például a szabványos string osztály is. bool insert(Name*). public: Table(size_t s = 15) { p = new Name[sz = s]. .

9 . Szabad tárbeli objektumként.10). §5.4.6 §10. amikor a tömb.t1-re és tt. }. Mivel a const-ok és a referenciák kötelezõen kezdõértéket kell. amely akkor keletkezik és semmisül meg. Létrehozás és megsemmisítés Tekintsük át a különbözõ módokat: hogyan hozhatunk létre objektumot és késõbb az hogyan semmisül meg. melynek eleme. A beépített típusoknak szintén van alapértelmezett konstruktoruk (§6. amely a new operátor használatával jön létre és a delete operátor használatával semmisül meg.8).5. Másrészt tt.vi elemei nem kapnak kezdõértéket. a program indulásakor jön létre és a program befejezésekor semmisül meg.4. X x.4. illetve semmisül meg. névtérbeli vagy statikus osztály-objektumként.2.i és tt.4 Névvel ellátott automatikus objektumként. és akkor semmisül meg.4. 10.5 §10. hacsak a programozó kifejezetten nem gondoskodik konstruktorról (§10. Az osztályok és a beépített típusok egymástól eltérõ kezelésmódjának a Cvel való egyeztetés és a futási idõ növelésétõl való tartózkodás az oka. amelyen belül a deklaráció szerepelt.322 Absztrakciós módszerek Itt tt kezdõértékkel való feltöltése fordítás közben létrehozott alapértelmezett konstruktor segítségével történik. amikor a program kilép abból a blokkból. Lokális statikus objektumként. amely akkor jön létre. Tömbelemként.6.4). §10. amikor a program végrehajtása során elõször találkozik a deklarációjával és egyszer semmisül meg: a program befejezésekor.4. hogy kapjanak (§5. Nem statikus tagobjektumként. amely egyszer.7 §10. az ilyeneket tartalmazó tagoknak nem lehet alapértelmezett konstruktora. amely a Table(15)-öt hívja meg tt.vt minden egyes elemére.4.4. Objektum a következõ módokon hozható létre: §10.1): struct X { const int a. amely akkor keletkezik.3.4.8 §10. amely egy másik osztály objektum tagjaként jön létre és azzal együtt keletkezik. mert ezek az objektumok nem osztály típusúak.4. amikor a program végrehajtása során deklarációja kiértékelõdik. Globális. const int& r. // hiba: nincs alapértelmezett konstruktor X számára Az alapértelmezett konstruktorok közvetlen módon is hívhatók (§10.

2. semmisül meg.10. // . } Itt aa..4. melyben elõfordult.1. // . valahányszor a vezérlés fonala „keresztülhalad” a változó deklarációján.. melynek mutató tagjai vannak. amely egy kifejezés kiértékelésekor jön létre és a teljes kifejezés végén. meglepõ (és rendszerint nemkívánatos) hatás léphet fel. a cc a bb után jön létre. Ez a felsorolás nagyjából a fontosság sorrendjében készült.12 Unió tagjaként. A lokális változók destruktorai konstruktoraik sorrendjéhez viszonyítva fordított sorrendben hajtódnak végre: void f(int i) { Table aa. Objektumok másolása Ha t1 és t2 a Table osztályba tartozó objektumok. amelyek egy konstruktor–destruktor pár által kezelt erõforrásokat tartalmaznak: . Ha egy hívásnál i>0.4.4. Osztályok 323 §10. §10. bb. sem destruktora. Table bb. §10.4.. a memóriába helyezett objektumként. paraméterekkel vezérelt lefoglalási mûvelet segítségével nyert. a destruktor végrehajtására pedig akkor kerül sor. if (i>0) { Table cc. 10. A tagonkénti másolás rendszerint nem megfelelõ olyan objektumok számára. amikor kilépünk a változó blokkjából.4. } Table dd..4.10 Ideiglenes objektumként. aa sorrendben semmisülnek meg. amelynek nem lehet sem konstruktora. 10. amikor a vezérlés kilép az f()-bõl.4. ha olyan osztály objektumaira alkalmazzuk.5). Lokális változók A lokális változók konstruktora minden alkalommal végrehajtódik.11 Felhasználó által írt függvénnyel végzett. t2=t1 alapértelmezés szerint t1-nek tagonkénti átmásolását jelenti t2-be (§10. A következõ alpontokban részletesen elmagyarázzuk az objektumok létrehozásának ezen változatait és használatukat. és dd létrejötte elõtt semmisül meg. bb és dd ebben a sorrendben keletkeznek az f() meghívásakor és a dd. Ha nem bíráljuk felül ezt az alapértelmezett viselkedést.

for (int i = 0. lásd §11. Ez nem meghatározott és valószínûleg katasztrofális eredményhez vezet. ha megadjuk. mintha ez a másolás megtörtént volna. // másoló konstruktor // másoló értékadás A programozó bármilyen alkalmas jelentést meghatározhat ezen másoló mûveletek számára.12): Table::Table(const Table& t) { p = new Name[sz=t. mert a t3=t2 értékadás következtében felülíródik. Másrészt a t1 részére létrehozott tömb t1-ben. de az ilyen típusú tárolók esetében a másoló mûvelet hagyományos feladata az. A Table destruktor viszont háromszor hívódik meg: t1-re. }.324 Absztrakciós módszerek void h() { Table t1. A mutató. Az ilyen anomáliák elkerülhetõk. nem marad meg. így a h() függvény végén t1. hacsak nincs automatikus szemétgyûjtés (§10. így az általa elfoglalt tárterület a program számára örökre elvész. // kezdeti értékadás másolással: problémás // értékadás másolással: problémás Itt a Table alapértelmezett konstruktora kétszer hívódik meg: egyszer t1-re és egyszer t3-ra. i<sz. t2-re és t3-ra is. t2 és t3 mindegyike arra a névtömbre hivatkozó mutatót fogja tartalmazni. // másoló konstruktor // értékadás // óvakodjunk az ön-értékadástól: t = t . mely a t3 létrejöttekor kijelölt névtömbre mutat.p[i]. mert ez a változó a t1-bõl való másolással kapott kezdõértéket. hogy lemásolja a tartalmazott elemeket (vagy legalábbis a felhasználó számára úgy tesz. } Table& Table::operator=(const Table& t) { if (this != &t) { delete[ ] p. amely t1 létrejöttekor kapott helyet a szabad tárban. mit jelent egy Table objektum másolása: class Table { // . i++) p[i] = t. Table(const Table&). tehát háromszor is törlõdik.5).4.. Table t3. Table t2 = t1. Alapértelmezés szerint az értékadás tagonkénti másolást jelent. A t2-re nem hívódik meg. Table& operator=(const Table&). t2-ben és t3-ban egyaránt megjelenik.. } t3 = t2.sz].

3. mindössze a memória pazarlása. a program viselkedése nem meghatározott és nagy valószínûséggel katasztrofális lesz. mint a Table::~Table() destruktort. i<sz.6. // valószínûleg futási idejû hibát okoz } A Table::Table() konstruktort kétszer hívjuk meg. mindazonáltal egy hosszan futó programnál az ilyen „memóriaszivárgás” vagy „memórialyuk” (memory leak) súlyos és nehezen felderíthetõ hiba. Mint majdnem mindig. csakúgy. míg az értékadó mûveletnek egy már létrehozott objektumot kell helyesen kezelnie. hogy a másoló konstruktor le nem foglalt memóriát készít fel a felhasználásra. Table* q = new Table.10.4.5. Nyelvi szempontból egy objektum nem törlése nem hiba. p = new Name[sz=t. delete p. törölni kell a régi elemeket. Szerencsére léteznek az ilyesfajta memóriaszivárgást keresõ eszközök is. a szabad tárban létrehozott objektumok konstruktorát a new operátor hívja meg. Ennek alapvetõ oka az. Osztályok 325 } } return *this. for (int i = 0. delete p. amíg a rájuk hivatkozó mutatóra nem alkalmazzuk a delete operátort: int main() { Table* p = new Table. de az értékadó operátor általános célja egyszerû: védekezni kell a saját magával való értékadás ellen.) 10. A p által mutatott objektum kétszeri törlése súlyos hiba.4. . Az értékadást bizonyos esetekben optimalizálni lehet. A szabad tár A dinamikusan kezelt memóriaterületen.sz]. míg a q által mutatottat egyszer sem. és ezek az objektumok addig léteznek. a másoló konstruktor és az értékadó mûvelet itt is jelentõsen eltér. Sajnos azonban ebben a példában a new-k és delete-ek nem felelnek meg egymásnak: a p által hivatkozott objektumot kétszer töröltük. elõkészíteni és bemásolni az új elemeket. i++) p[i] = t.p[i]. Általában minden nem statikus tagot másolni kell (§10.

ott is a csak memória-felszabadítást végzõ destruktorokat lehet megtakarítani. members().5 és 19. Club(const string& n. a delete operátor kétszeri meghívása egyben a destruktor (ha van ilyen) kétszeri meghívását fogja eredményezi. Sajnos az egyes nyelvi változatok nem képesek megbízható módon jelezni az ilyen hibákat.4. A legtöbb esetben az objektumok ezen viselkedése csak apróbb kényelmetlenséget jelent.2.7. Lehetséges a lefoglalás. tárgyalja. bármilyen hozzáférési kísérlet az objektumhoz hibának számít.. amely egy kisebb cégrõl tárolhat adatokat: class Club { string name. Az osztálytagok konstruktorainak paramétereit a tartalmazó osztály konstruktordefiniciójának tag-kezdõérték listájában (member initializer) adjuk meg: Club::Club(const string& n. így az objektum kétszer törlõdik. de viselkedésük nem szabványosított. ami ilyenkor is súlyos hiba. hogyan történjék a new használata esetén a memória lefoglalása. ahol van szemétgyûjtés.6).6. A Club osztály konstruktoránál paraméterként meg kell adni a nevet és az alapítás dátumát. Date fd).4. Osztály típusú tagok Nézzünk egy osztályt. sõt bizonyos programoknál a futási idõ növekedése és a viselkedés megjósolhatatlansága is (§C..6. Miután egy objektumot a delete mûvelettel töröltünk.4. Ha van is szemétgyûjtés. Jelesül. // . Ennek az egyszerûsítésnek a hordozhatóság elvesztése az ára. Table officers.4. 10. }. a konstruktorok és a kivételek együttmûködésének a megadása is (§14. A szabad tárban levõ tömböket a §10. Date fd) : name(n). Table members. A programozó megszabhatja.5). } .9.2 és §15. illetve annak a delete-tel való felszabadítása (§6.1). Date founded. officers(). founded(fd) { // .326 Absztrakciós módszerek Bizonyos C++-változatok automatikusan újrahasznosítják az elérhetetlen objektumok által elfoglalt memóriát (ezek a szemétgyûjtést alkalmazó megvalósítások)...

majd a tagok destruktorai a deklarációval ellentétes sorrendben... founded(fd) { // ... // . így a következõ kódrészlet egyenértékû az elõzõ példabelivel: Club::Club(const string& n. A tagok konstruktorainak végrehajtása megelõzi a tartalmazó osztály saját konstruktora törzsének végrehajtását. Club& c) : i(ii). az osztály megsemmisítésekor elõször saját destruktor függvényének (ha van ilyen) törzse hívódik meg. pc(c) { } }. Club c. a destruktor pedig felülrõl lefelé (a tagokat utoljára) bontja le azt. Az esetleges zavarok elkerülése érdekében nyilván célszerû a tagokat a deklarációban elfoglalt sorrendjükben felvenni a kezdõérték-adó kifejezések listájára. A tagok szükségszerû kezdeti értékadása Azon tagok feltöltése kezdõértékkel szükségszerû. Osztályok 327 A tagok kezdõérték-listáját kettõspont elõzi meg és az egyes tagoknak kezdõértéket adó kifejezéseket vesszõk választják el. A konstruktorok a tagoknak az osztály deklarációjában elfoglalt sorrendjében és nem a kezdõértéket adó kifejezéseknek a listában való felsorolási sorrendjében hajtódnak végre.10. Date fd) : name(n). c(n.1.d). A tagok destruktorai a konstruktorok sorrendjével ellenkezõ sorrendben hívódnak meg.4. Ha egy osztálynak osztály típusú tagjai vannak. Ha egy tag konstruktorának nincs szüksége paraméterre. . a const és a referencia típusú tagoké: class X { const int i. } A Table::Table konstruktor a Club::officers tagot mindkét esetben a 15-tel. Club& pc. amelyeknél a kezdeti értékadás különbözik az egyszerû értékadástól – azaz az alapértelmezett konstruktor nélküli osztályba tartozó. const string& n. Date d.6. A konstruktor alulról felfelé haladva (a tagokat elõször) építi fel a tagfüggvények végrehajtási környezetét. nem szükséges felvenni a listára. X(int ii. mint alapértelmezett paraméterrel hozza létre. 10.

0. // rendben: Curious::c1 meghatározott . Konstans tagok Egy statikus. // szükséges. ha a kezdõértéket kapott tagot memóriában tárolt objektumként használjuk.2.. Person(const string& n.. string address.328 Absztrakciós módszerek Ezen tagok kezdeti értékadására nincs egyéb lehetõség. const string& a) : name(n) { address = a.4. de ott nem szabad megismételni a kezdõérték-adó kifejezést: const int Curious::c1. const string& a). A legtöbb típus esetében azonban a programozó választhat a kezdeti és a „sima” értékadás közül. static const int c4 = f(17). } Itt a name az n egy másolatával kap kezdõértéket. hogy egyértelmû legyen a kezdeti értékadás ténye. egész típusú konstans tagot lehetséges a deklarációban egy kezdõérték-adó konstans kifejezéssel is feltölteni: class Curious { public: static const int c1 = 7. // rendben.6. Másfelõl az address elõször egy üres karakterlánccal töltõdik fel. Ilyenkor én általában a tag-kezdõérték listás megoldást választom. // . de ne felejtsük el a meghatározást // hiba: nem állandó // hiba: nem statikus // hiba: a kezdõérték-adó nem állandó // hiba: a kezdõérték-adó nem egész értékû Akkor és csak akkor. Person::Person(const string& n. majd értékül az a egy másolatát kapja. const int c3 = 13. static int c2 = 11. hogy az ilyen tag (de csak egy helyen) definiált legyen. }. Person(const Person&). Ez a módszer ráadásul hatékonyabb is: class Person { string name.. 10. }. szükséges. de a kezdõérték-adó nem szerepelhet itt még egyszer const int* p = &Curious::c1.. // . és hiba azt nem megtenni is. static const float c5 = 7.

c4 = 17 }..4. Y y1.8. Unique_handle a. c2 = 11. konstans. jelképes állandóként használhatunk felsoroló konstanst (§4. Osztályok 329 Másik megoldásként. adjon kezdõértéket.10. megelõzendõ az // alapértelmezett másolást (§11. 10. // . vagy olyan felhasználói típus melynek nincsen másoló értékadása. Ha ez nem lehetséges. az ilyen osztályú objektum másolási kísérlete hiba: class Unique_handle { private: // a másoló mûveleteket priváttá tesszük.6. struct Y { // ..4. §14...1) az osztály összes tagját másolja. Így a programozó nem fog kísértésbe esni. ha az osztály egy nem statikus tagja: referencia. hogy az osztályban változóknak.6. }. lebegõpontos számoknak stb. ha szükséges: class X { enum { c1 = 7.. c3 = 13. Tagok másolása Az alapértelmezett másoló konstruktor és az alapértelmezett másoló értékadás (§10.3) is az osztály deklarációján belül.4.2. public: // . }. }. §15. Unique_handle& operator=(const Unique_handle&).. Y y2 = y1. .3. // explicit kezdõértéket igényel // hiba: Y::a nem másolható Ezenkívül az alapértelmezett értékadás nem jöhet létre a fordításkor.2) Unique_handle(const Unique_handle&).4.

így az alapértelmezés szerinti üres karakterláncot kapja kezdõértékként. void f() { Ibuffer words[100].name) { } // vigyázat! Itt elfelejtettem az address tagot másolni. amely elõállítja a kívánt értékeket: class Ibuffer { string buf.. de sokszor nem erre van szükség egy másoló konstruktorban: Person::Person(const Person& a) : name(a.2. §18. Ha új taggal bõvítünk egy osztályt. Tömbök Ha egy osztály egy tagjának van alapértelmezett. ügyeljünk arra.. Ha feltétlenül szükséges. . } // minden elem a cin-rõl kap kezdõértéket Az ilyen trükköket azonban általában jobb elkerülni. Ez gond lehet. hogy egy tömb tagjai különbözõ kezdõértéket kapjanak. } // .7) alkalmazásán kívül nincs más mód egy tömb elemeinek konstruktorai számára (nem alapértelmezett) paramétereket megadni. Ha másoló konstruktort írunk. hogy a referencia típusú tagok ugyanarra az objektumra hivatkoznak az eredeti objektumban és a másolatban is. azaz paraméter nélkül hívható konstruktora. ne felejtsük el ellenõrizni.. amelyet szükséges. // .7. akkor ilyen osztályú objektumok tömbjét is meghatározhatjuk: Table tbl[10]. ha a hivatkozott objektumot törölni kell.330 Absztrakciós módszerek Jegyezzük meg. írjunk olyan alapértelmezett konstruktort. Alapértelmezés szerint az elemek alapértelmezett módon kapnak kezdõértéket. }. A fenti egy 10 Table elembõl álló tömböt hoz létre és minden elemet a Table::Table() konstruktorral. A kezdõérték-lista (§5. hogy minden tagot másoljunk. a 15 értékû alapértelmezett paraméterrel tölt fel. 10. hogy vannak-e olyan felhasználó által megadott konstruktorok. amelyeket az új tagok kezdeti értékadására és másolására való tekintettel meg kell változtatni. public: Ibuffer() { cin>>buf.1.4.6..

Ha az olvasó túl nehézkesnek találja a C stílusú tömbök használatát. de általában futtatáskor fog valami csúnya dolog történni. hogy minden objektumról tartsa nyilván.és futási idõ-többletet jelentene. §16. A kifejezetten tömbök törlésére szolgáló delete[ ] logikailag nem szükséges. akkor ez automatikusan történik. Elképzelhetõ lenne. delete[ ] t3. Ha nem new mûvelettel létrehozott tömbrõl van szó. hogy egyedi elemet vagy tömböt kell-e törölni: void f(int sz) { Table* t1 = new Table.1. Egyszerû és érdektelen esetekben. delete t4. hogy egyedi objektum avagy tömb. Ezért a különbözõ változatok különbözõképpen fognak viselkedni. az összes elemére meghívódik a destruktor. itt is használhat helyettük olyan osztályokat. Osztályok 331 Amikor egy tömb megsemmisül. probléma // helytelen. hogy a szabad tártól megköveteljük. probléma } A tömbök és egyedi elemek dinamikus tárterületen való elhelyezése az adott nyelvi változattól függ.3): void g() { vector<Table>* p1 = new vector<Table>(10).3). a fordító észreveheti a hibát. delete p1.10. // helyes // helyes // helytelen. Table* t3 = new Table. Table* t4 = new Table[sz]. de ez a kötelezettség egyes C++-változatokban jelentõs memória. delete t1. mint az elõzõ példa.7. A C nyelvhez hasonlóan a C++ sem különbözteti meg az egyedi elemre és a tömb kezdõelemére hivatkozó mutatót (§5. delete p2. ha hibásan használjuk a delete és delete[ ] operátorokat. ezért a programozónak meg kell adnia. delete[ ] t2. Table* t2 = new Table[sz]. Ekkor a nyilvántartás terhét levennénk a programozó válláról. mint a vector (§3. Table* p2 = new Table. } .

. Mivel a tbl2 változó deklarációját tartalmazó blokk nem hajtódik végre az f(0) meghíváskor. amikor a program leáll (§9.1).9.332 Absztrakciós módszerek 10...8. a blokk újbóli végrehajtásakor nem. Hogy pontosan mikor.4.1.. névtérbeli és osztályhoz tartozó statikus) változók a main() függvény meghívása elõtt jönnek létre (és kapnak kezdõértéket). Lokális statikus adatok A lokális statikus objektumok (§7. // . Nem lokális adatok A függvényeken kívül meghatározott (azaz globális. így nem semmisül meg. hiszen ilyenkor a kezdeti értékadásra akkor kerül sor. tbl2 is csak f(1) végrehajtásakor jön létre. // . amikor a végrehajtási szál elõször halad keresztül az objektum meghatározásán: void f(int i) { static Table tbl. } Itt tbl konstruktora f() elsõ meghívásakor hívódik meg.. amikor f()-bõl visszatér a vezérlés és nem jön újra létre f() második meghívásakor..4.2) konstruktora akkor hajtódik végre. A lokális statikus objektumok destruktorai akkor hívódnak meg. amikor a dinamikus kód a futó programhoz kapcsolódik.4. Mivel tbl-t statikusként adtuk meg. A dinamikus könyvtárak használata (dinamikus csatolás) kissé bonyolultabbá teszi ezt. if (i) { static Table tbl2. f(1). 10.. } } int main() { f(0). // .1. és minden létrehozott objektum destruktora a main() függvénybõl való kilépés után végre fog hajtódni. f(2). az nincs meghatározva.

hogy tbl1 vagy tbl2 fog elõbb létrejönni. Table tbl. static Table memtbl. } A konstruktorok végrehajtási sorrendje a következõ: tbl. hogy az egyes fordítási egységek nem lokális objektumai milyen sorrendben jönnek létre: // file1.. }. Table X::memtbl. hogy egy statikus objektum számára memóriaterületet foglaljunk le azért. X::memtbl. X::memtbl.. tbl. A destruktorok végrehajtási sorrendje is hasonlóan változatfüggõ. Vegyük észre. // file2.10. Osztályok 333 A fordítási egységeken belül a nem lokális objektumok konstruktorainak végrehajtása a definiciójuk sorrendjében történik: class X { // . }. Nincs nyelvi változattól független meghatározása annak. Ilyen típusú adatot csak arra célra fogunk használni. amely kizárólag a kezdeti értékadás és rendrakás célját szolgálja. ~Zlib_init().c: Table tbl2. a C++ adott változatától függ. A destruktorok a konstruktorokkal ellentétes sorrendben hajtódnak végre: Z::tbl2. hogy lefusson a konstruktora és a destruktora: class Zlib_init { Zlib_init(). Könyvtárak tervezésekor szükséges vagy egyszerûen kényelmes lehet egy olyan. Az. // Zlib elõkészítése használatra // Zlib utáni takarítás . konstruktorral és destruktorral bíró típus elkészítése. namespace Z { Table tbl2. Dinamikus csatolás használata vagy akár a fordítási folyamat kis módosítása is megváltoztathatja a sorrendet.c: Table tbl1. hogy a definíció és nem a deklaráció sorrendje számít. Z::tbl2. de a sorrend azon belül is változhat.

} // kezdeti értékadás elsõ használatkor Az elsõ használatot figyelõ kapcsolók nem kezelnek minden elképzelhetõ helyzetet. ha az objektum elsõ használata egy végrehajtási idõre érzékeny függvényben történik.4. amit a nyelvi változatok általában a lokális statikus objektumokra alkalmaznak: egy-egy. hogy az utolsó használatot nem tudjuk egy jelzõvel jelezni. Egy másik probléma.334 Absztrakciós módszerek class Zlib { static Zlib_init x.. }.5. Sajnos egy több fordítási egységbõl álló program esetében nincs garancia arra. Lehetséges például olyan objektumokat megadni. // . az fárasztó feladat lehet.2). }. hogy a konstruktor nélküli statikus objektumok 0 kezdõértéket kapnak. } public: // nincs konstruktor void f() { if (initialized == false) initialize()..1 és §21. de a legtöbb nem. Ehelyett lásd §9. Egyes C++-változatok biztosíthatják ezt. A dolog akkor válik igazán problematikussá. hogy egy ilyen objektum kezdeti értékadása az elsõ használat elõtt megtörténik és a destruktor az utolsó használat után fut le. de megoldható.5. fokozatosan kell létrehozni azokat. ..1): int& obj() { static int x = 0. Ha mégis ilyen objektumokra van szükség. akkor óvatosan. static void initialize() { /* kezdeti értékadás */ initialized = true. // . az elsõ használatot figyelõ kapcsolót: class Zlib { static bool initialized.2. } // . Egy lehetséges másik megközelítés. return x..1. Ha sok függvényben kell lekérdezni az elsõ használatot figyelõ kapcsolót. Ilyenkor további trükkökre van szükség (§21.. ahol az ellenõrzés és szükség esetén a kezdeti értékadás túl sok idõt vehet igénybe. hogy az egyes objektumokat függvényekkel helyettesítjük (§9.. Ez a módszer azon alapul. amelyek a kezdeti értékadás alatt egymásra hivatkoznak – az ilyesmit jobb elkerülni.4. Programozói szinten azonban lehetséges azt a megoldást alkalmazni.

if (strlen(cs=(s2+s3). §20.6. így a cs változó bármiféle ottani használata nem biztos. Ettõl az objektumtól aztán elkérjük a C stílusú karaktertömböt. de együttes használatuk furcsa problémákhoz vezethet: void f(string& s1.4.4. hogy „nem kell ilyet csinálni”. törlõdik a tartalmazó teljes kifejezés kiértékelése végén. a programozó ritkán kell. A cout << cs kimeneti mûvelet mûködhet a várt módon. A teljes kifejezés olyan kifejezés. Ideiglenes objektumok Ideiglenes objektumok legtöbbször aritmetikai kifejezésekbõl jönnek létre. hogy mûködik. habár ez is elõfordul (§11. hogy az ideiglenes objektumokkal törõdjék. Vajon hol foglalt helyet a fordító a C stílusú karaktertömb számára? Valószínûleg az s1+s2-t tartalmazó ideiglenes objektumban.7). hogy nem semmisül meg az a terület is.c_str(). mert a teljes kifejezés. és annak megsemmisülése után nem biztos.10. maga az if feltétele. A + operátor karakterláncok esetében összefûzést jelöl. hacsak nincs referenciához kötve vagy nem egy nevesített objektumnak ad kezdõértéket. Elõször egy ideiglenes. de ez puszta szerencse kérdése.1). A fordítóprogram esetleg felderítheti az ilyen problémát és figyelmeztethet rá.10. és egyetértek vele. amely az s1+s2 mûvelet eredményét tárolja. nullkarakterrel lezárt karaktertömböt ad vissza (§3.4. hogyan kell azt értelmezni. amely nem részkifejezése más kifejezésnek. string& s2. amelyben az s2+s3-at tartalmazó ideiglenes objektum létrejön. .c_str())<8 && cs[0]=='a') { // cs használata } } Az olvasó valószínûleg azt mondja erre. A szabványos string osztály c_str() nevû tagfüggvénye egy C stílusú. string& s3) { const char* cs = (s1+s2).6). string osztályú objektum jön létre. Például az x*y+z kifejezés kiértékelése során egy ponton az x*y részeredményt valahol tárolni kell.1. így érdemes tudni. Hacsak nem a program gyorsításán dolgozik (§11. Ezek nagyon hasznos dolgok a karakterláncok kezelésekor. cout << cs. majd a kifejezés végén az ideiglenes objektum törlõdik. következésképpen cs felszabadított memóriaterületre mutat. Osztályok 335 10.5. Mindazonáltal az ideiglenes objektum a feltételesen végrehajtandó utasítás végrehajtásának megkezdése elõtt megsemmisül. Maga a feltétel a várakozásnak megfelelõen fog mûködni. de ilyen kódot szoktak írni. Egy ideiglenes objektum. Az if utasításos példa egy kicsit ravaszabb. §22.

3) és hogy ideiglenes objektumot nem adhatunk egy nem konstans referencia kezdõértékéül (§5. hogy hiba egy lokális változóra mutató referenciát visszaadni egy függvénybõl (§7. Egy tisztább programozási stílus nem csak jobban olvasható programrészletet eredményezett volna. int y) { s. } g(s. string& s2) { const string& s = s1+s2.move(Point(x. const string&). // Point létrehozása a Shape::move() számára // . hogy egy magasabb szintû adatot alacsony szinten használtunk. Az ideiglenes változó megsemmisül.. int x. void h(string& s1. de az ideiglenes objektumokkal kapcsolatos problémákat is teljesen elkerülte volna: void f(string& s1. mint az automatikusan létrehozottak. string& s2. Ideiglenes változót létrehozhatunk kifejezett konstruktorhívással is: void f(Shape& s.336 Absztrakciós módszerek Vegyük észre. string ss = s1+s2. mint sok más esetben is. string s = s2+s3. az ideiglenes objektumokkal kapcsolatos probléma abból adódik..length()<8 && s[0]=='a') { // s használata } } Ideiglenes változót használhatunk konstans referencia vagy nevesített objektum kezdõértékeként is: void g(const string&. if (s. Emlékezzünk arra. hogy ebben az esetben.y)).5). . } Az ilyen módon létrehozott ideiglenes változók is ugyanolyan szabályok szerint semmisülnek meg.ss). // s és ss itt használható Ez a kódrészlet jól mûködik. amikor az „õ” hivatkozását vagy nevesített objektumát tartalmazó kódblokk lefut. string& s3) { cout << s1+s2.

6). és ezt. // . } void* buf = reinterpret_cast<void*>(0xF00F). hogy minden new operátor a méretet várja elsõ paraméterként. Hogy melyik operátort fogja egy adott hívás elérni.10. ha megadunk egy memória-lefoglaló függvényt.11.4). Az objektumok elhelyezése A new operátor alapértelmezés szerint a szabad tárban hozza létre az objektumokat. }. // explicit elhelyezõ operátor // fontos cím // X létrehozása a 'buf-ban'. mint a létrehozandó objektum méretét. az operator // new(sizeof(X). void* p) { return p. mint a kívánt típust adja vissza. Osztályok 337 10. X* p2 = new(buf)X. azt a szokásos paraméter-egyeztetési szabályok fogják eldönteni (§7. hogy egy bizonyos helyrõl (Arena objektumtól) foglaljunk memóriát: . így aztán a lényegébõl fakadóan nyelvi változattól függõ. A reinterpret_cast a „legdurvább” és a legnagyobb károkozásra képes a típuskonverziós operátorok közül (§6. Mit tegyünk. veszélyes és esetenként feltétlenül szükséges egészek és mutatók közötti átalakításra használható. ha máshol szeretnénk. Az „elhelyezõ” operator new() a legegyszerûbb ilyen lefoglaló függvény.2. amelynek további paraméterei vannak. Az objektumokat tetszés szerinti helyre tehetjük. Legtöbbször egyszerûen a paraméterének megfelelõ bitsorozatú értéket. amely az operator new-nak további paramétereket ad. minden new() operátornak egy size_t típusú elsõ paramétere van. és definiciója a <new> szabványos fejállományban szerepel.. Jegyezzük meg.4.buf) meghívásával Ezen használat miatt a new (buf) X utasításforma.7). automatikusan megkapja (§15. és a new operátor használatakor megadjuk ezeket a paramétereket: void* operator new(size_t. Az elhelyezõ new operátor felhasználható arra is. hogy egy objektum létrejöjjön? Vegyünk példaként egy egyszerû osztályt: class X { public: X(int). elhelyezõ utasításként (placement syntax) ismert..

. Esetenként mégis alapvetõ szükségünk van rájuk: például nehéz lenne egy hatékonyan mûködõ általános tárolóosztályt készíteni a standard könyvtár vector (§3. // memória felszabadítása } Jegyezzük meg. // . // destruktor meghívása a->free(p). Ennek alapvetõ módja az. void g(int i) { X* p = new(Persistent) X(i).3. Arena* a) { p->~X(). X* q = new(Shared) X(i). közvetlen destruktorhívás nélkül. Mindazonáltal egy kezdõ C++-programozó inkább háromszor gondolja meg. } A különbözõ Arena objektumokban szükség szerint tetszõleges típusú objektumokat hozhatunk létre: extern Arena* Persistent.7. hogy közvetlenül meghívjuk a destruktort: void destroy(X* p. } // X állandó tárterületen // X megosztott memóriában Ha egy objektumot olyan helyre helyezünk. mint az egyedi igényeket kielégítõ globális memória-lefoglalók – használatát inkább kerüljük el. hogy a destruktorok közvetlen meghívását – csakúgy. §16. amelyet nem (közvetlenül) a szabványos szabadtár-kezelõ kezel. // ... extern Arena* Shared. virtual void free(void*) =0. Arena* a) { return a->alloc(sz). ha lehet..1.8) típusa nyomán. mielõtt közvetlenül . void* operator new(size_t sz. némi óvatosságra van szükség annak megsemmisítésekor. }.338 Absztrakciós módszerek class Arena { public: virtual void* alloc(size_t) =0.

használjunk inkább konkrét típust.4. hogy nyilvánvalóvá tegyük egy osztálynak és segédfüggvényeinek összetartozását.5). hogy milyen típusú objektum van az unióban. §10. A fordítóprogram általában nem tudhatja. §10.10.4-es pontot. Egy uniónak lehetnek tagfüggvényei. [5] Használjunk névteret arra.3. §10. [4] Egy függvény csak akkor legyen tagfüggvény. amelyek nyilvántartják.8. amikor tényleg csak adatok vannak és nincs rájuk nézve invariánst igénylõ feltétel. §10. hogy mi van az unióban (§10.2. de nem lehetnek statikus tagjai. [3] A konkrét típusok a legegyszerûbb osztályok.6. §10. 10. illetve azt.4.2).12.5. vagyis nem ismert.3. ha nem változatja meg az objektumának az értékét. [7] Egy függvény.4. ha közvetlenül kell hozzáférnie az osztály ábrázolásához. de nincs is szükség rá. A tömböknél nincs megfelelõje az elhelyezõ operátornak. Tanácsok [1] A fogalmakat osztályokra képezzük le. mert akkor nem lehetne a helyes memóriakezelést biztosítani.3.2. mint bonyolultabb osztályokat vagy egyszerû adatszerkezeteket.2. de nem . Az uniók felhasználása leginkább alacsony szinten vagy olyan osztályok belsejében történik. [2] Csak akkor használjunk nyilvános adatokat (struct-okat). §10. hogy az unió melyik tagja van használatban.6[20]). mert az elhelyezõ operátort tetszõleges típusokra alkalmazhatjuk. amelynek hozzá kell férnie az osztály ábrázolásához. Ezért egy uniónak nem lehet olyan tagja.2. amelynek konstruktorral vagy destruktorral rendelkezik. legyen const tagfüggvény. hogy az unió megsemmisülésével a megfelelõ destruktor hívódik meg.8.1. Az elhelyezõ operátor és a kivételkezelés kapcsolatáról lásd a §14. Osztályok 339 meghívna egy destruktort és akkor is inkább kérje elõtte tapasztalt kollégájának tanácsát. Uniók Egy nevesített unió (union) olyan adatszerkezet (struct). [6] Egy tagfüggvény. Tömbökre vonatkozóan azonban megadhatunk például egyedi operator delete()-et (§19. 10. Hacsak lehet. amelyben minden tag címe azonos (lásd §C.

hogy az adatábrázolásra az 1970. amelyben létrejöttek. §10.4.4. 2. (*1) Hogyan érjük el a Chrono névtér Date osztályának set_default függvényét (§10. §10. nincsenek-e felhasználói konstruktorok. Gyakorlatok 1. Ha egy osztálynak szüksége van másoló mûveletre vagy destruktorra.6. ha egész konstansokra van szükség egy osztály deklarációjában.340 Absztrakciós módszerek [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] szükséges. Aztán találjunk még két további hibát a §10.3. akkor legyen destruktora az osztálynak.1.3. hogy a végrehajtási sorrendtõl való függést a lehetõ legkisebbre csökkentsük.9. Használjunk elsõ használatot jelzõ kapcsolókat.4. Ha egy konstruktor lefoglal valamilyen erõforrást. Használjunk felsoroló konstansokat. valószínûleg szüksége lesz konstruktorra. 3. §10.4. Globális vagy névtérhez tartozó objektumok használatakor kerüljük a végrehajtási sorrendtõl való függést.4.4. amelyik felszabadítja azt.4.4. (*2) Keressünk egy kereskedelmi használatban levõ Date osztályt. 5.4.6.4.1.9. Az osztályra állapotbiztosítóit (invariáns) a konstruktorban állítsunk be. §10. másoló konstruktorra és másoló értékadásra is.2. §10.01. 4. §10.2.4. §10. §10. vitassuk meg az osztályt egy tényleges felhasználóval. Ha egy osztálynak van mutató tagja. §10. Másoló konstruktor írásakor ügyeljünk arra.7-beli változatban. Gondoljunk arra. 10. §10. hogy az ideiglenes objektumok annak a teljes kifejezésnek a végén megsemmisülnek. hogy minden szükséges elemet másoljunk (ügyeljünk az alapértelmezett kezdeti értékadásra). amelyekben kezdõértéket kell adni az új tagnak.4. valószínûleg szüksége lesz másoló mûveletekre (másoló konstruktorra és másoló értékadásra) is. §10. hogy egy objektumon keresztül hívjuk meg. 10. óta eltelt napokat használjuk.2)? Adjunk meg legalább három változatot.4.4.1. §10. Ha új taggal bõvítünk egy osztályt.4. Írjuk újra úgy. (*1) Találjuk meg a hibát a §10.1.01.5) Fejezzük be és próbáljuk ki a Date osztályt.1. Elemezzük az általa nyújtott szolgáltatásokat.1.2.10.3. (*2. A másoló értékadásnál ügyeljünk az önmagával való értékadásra.6. Ha lehetséges.6. legyen statikus tagfüggvény. ellenõrizzük.2.2-beli Date::add_year() függvényben. (*2) Határozzuk meg a Histogram osztályt.3. akkor legyenek másoló mûveletei (másoló konstruktora és másoló értékadása).4. Ha egy osztálynak van referencia tagja. destruktorra. amely a konstruktorában paraméter- .

eval() << "\n". A nyilvános felület ilyesmi legyen: class Expr { // . (*3) Hozzunk létre egy olyan osztályt. Hasonlítsuk össze a két változatot. 9. kiértékelni..5) Készítsük el a Table osztályt. vannak. public: Expr(const char*). Ezután módosítsuk a számológép programot (§6. void print(). 8. Kísérletezzünk a kifejezés különbözõ kiíratásaival: teljesen zárójelezve. (*1. amely az eloszlást megadja.10[7]-beli Tnode-ot. assembly kóddal stb. másszor egy karakterlánccal ábrázolva. a mûveleti jelet utótagként használva. Biztosítsunk mûveletet a grafikon kiíratására és kezeljük az értelmezési tartományon kívül esõ értékeket is. hogy az a map helyett a Table osztályt használja. 6. készítsük el és ellenõrizzük az Intset osztályt.. 10. tárolni és kiírni. (*3) Határozzuk meg. Az Expr::Expr() konstruktor karakterlánc paramétere a kifejezés. x. az Expr::print() pedig ábrázolja azt a cout-on. amelynek konstruktorai. amely a következõ értéket adja vissza.10. ahol a Node egy meghatározott adatszerkezet. * és / mûveletekbõl álló egyszerû aritmetikai kifejezéseket képes elemezni.1). mint olyan osztályt. cout << "x = " << x. amely egész konstansokból és a +. Legyen meg az unió. amely (név–érték) párokat tárol. A program így nézhet ki: Expr x("123/4+123*4-3"). Határozzuk meg az Expr osztályt kétféleképpen: egyszer mint csomópontok láncolt listáját. -. amelyek bizonyos (például egyenletes vagy exponenciális) eloszlások szerinti véletlen számokat adnak. Az Expr::eval() függvény visszaadja a kifejezés értékét. és a szimmetrikus differencia mûvelet is. }. destruktorai stb. hogy csomópontok (Node objektumok) halmazát jelentse. amely egészek halmazát ábrázolja. .print(). Osztályok 341 ként megadott idõtartományokra vonatkozó gyakoriságokat tartja nyilván. int eval(). mint osztályt (konstruktorokkal és destruktorokkal). a metszet. Adjuk meg a Tnode-ok egy fáját. 7. (*2) Határozzunk meg osztályokat. (*2) Írjuk újra a §7.5) Módosítsuk az Intset osztályt. Mindegyik osztálynak legyen egy konstruktora. 11. és egy draw függvénye. (*2.

17.6[131]-beli szimbólumtábla osztályt. } Módosítsuk úgy.vector<int> > objektumot. Készítsük el a Char_queue-t mint (a) láncolt listát. Tegyük lehetõvé a kimenetnek a bemenethez hasonló módon többféle helyre való irányítását. (*3) Írjunk olyan függvényt. hogy a nyilvános felület ne függjön az ábrázolástól. (*3) Tervezzünk egy szimbólumtábla és egy szimbólumtábla-elem osztályt valamely nyelv számára.342 Absztrakciós módszerek 12.3) az §5. Hozzunk létre Calculator objektumokat és alkalmazzuk azokat a cin-bõl származó bemenetre. (*2) Módosítsuk a 10. 13. 16. úgy. (*2) Határozzunk meg két osztályt. mindegyikben egy-egy statikus taggal. amely legalább 1000 sort tartalmaz. (*2. világ!\n". illetve (b) vektort. Futtassuk a programot egy olyan szövegfájllal.6[11]-beli kifejezésosztályt. hogy a következõ kimenetet adja: Kezdeti értékadás Helló.10[19] feladatra adott megoldással. Használjuk a 10. milyen különbségekkel kell számolni a két osztály módosításakor. 15. amilyet a §6.1-beli függvények nagyrészt megvalósítanak. amely minden karakterláncot és azok elõfordulásának sorszámát tartalmazza. (*2) Határozzuk meg a Char_queue osztályt. hogy változókat is kezelni tudjon. hogyan néznek ki ott az igazi szimbólumtáblák. amely egy istream-bõl és egy vector<string>-bõl kiindulva elkészít egy map<string. 14. (*2) Határozzunk meg egy olyan Calculator osztályt. hogy mindegyik létrehozásához a másikra hivatkozunk. Hol fordulhat elõ ilyesmi igazi kódban? Hogyan lehet módosítani az osztályokat. világ! Takarítás A main() függvényt semmilyen módon nem változtathatjuk meg. a parancssori paraméterekre és a programban tárolt karakterláncokra.9[13] és a §7. valamint a = értékadó mûveletet is. hogy kiküszöböljük a végrehajtási sorrendtõl való függést? 18. és legalább 10 . (*1) Adott a következõ program: #include <iostream> int main() { std::cout << "Helló. Nézzük meg az adott nyelv egy fordítóprogramjában.5) Hasonlítsuk össze a Date osztályt (§10. 19. Értékeljük a megtalált hibákat és gondoljuk meg.

se kevesebbet.11 Operátorok túlterhelése „Amikor én használok egy szót. azt értem alatta.” (Humpty Dumpty) Jelölés • Operátor függvények • Egy.és kétoperandusú mûveleti jelek • Az operátorok elõre meghatározott jelentése • Az operátorok felhasználói jelentése • Operátorok és névterek • Komplex szám típusok • Tag és nem tag operátorok • Vegyes módú aritmetika • Kezdeti értékadás • Másolás • Konverziók • Literálok • Segédfüggvények • Konverziós operátorok • A többértelmûség feloldása • Barát függvények és osztályok • Tagok és barát függvények • Nagy objektumok • Értékadás és kezdeti értékadás • Indexelés • Függvényhívás • Indirekció • Növelés és csökkentés • Egy karakterlánc osztály • Tanácsok • Gyakorlatok . amit én akarok – se többet.

344 Absztrakciós módszerek 11. class complex { // nagyon leegyszerûsített complex típus double re. im. így felhasználói típussal kell azokat ábrázolni. melyet a + és a * mûveletek kezelnek. tárgyalását. Bevezetés Minden mûszaki szakterületnek – és a legtöbb nem mûszakinak is – kialakultak a maga megszokott rövidítései. A legtöbb nyelvvel együtt a C++ is támogat egy sor. logikai jelölésekre vagy karakterláncokra van szükségünk a C++-ban.operator(c)-t jelenti. hogy ezeket a fogalmakat ábrázoljuk. amelyek kényelmessé teszik a gyakran használt fogalmak kifejezését. megszokottabb és kényelmesebb jelölés felhasználásával kezelhetjük az objektumokat. mintha csak az alapvetõ függvény-jelölést használnánk. ha mátrix-mûveletekre. Ezek után közelítõleg meghatározhatjuk a complex számokat tartalmazó kifejezések megszokott jelentését: .1. A felhasználó adja meg a complex::operator+() és complex::operator*() operátorokat. a beépített típusokra vonatkozó mûveletet. A legtöbb fogalomnak. public: complex(double r. mint a vegyük y-t z-szer és az eredményt adjuk x-hez Nem lehet eléggé megbecsülni a szokásos mûveletek tömör jelölésének fontosságát. azonban nincs megfelelõje a beépített típusok között. Ha ezekre az osztályokra vonatkozó mûveleteket definiálunk. Az alábbi például x+y*z világosabb számunkra. double i) : re(r). Egy complex értéket egy kétszeres pontosságú lebegõpontos számpár ábrázol. osztályokat használunk. Például ha komplex számokkal akarunk számolni. amelyre mûveleteket szoktak alkalmazni. im(i) { } complex operator+(complex). complex operator*(complex). akkor a b+c a b. hogy értelmezze a + és * mûveleteket. }. Ha például b és c complex típusúak. Itt például a komplex szám fogalmának egy egyszerû megvalósítását láthatjuk.

1994]. hanem nevet várnak és a tagokra való hivatkozás alapvetõ módjai.* (tagkiválasztás a tagra hivatkozó mutatón keresztül.2).9. hogy b=b+(c*a). 11.11.2.4. Az operátorok túlterhelése azonban nemcsak konkrét típusoknál hasznos. b = b+c*a.7) .3. amelyek második operandusként nem értéket. mint a ->. mint ahogy a sizeof (§4.2. a = b+c. Ha túl lehetne terhelni ezeket – azaz ha a felhasználó határozhatná meg jelentésüket – akkor ez érdekes mellékhatásokkal járhatna [Stroustrup. . §10. } A szokásos kiértékelési szabályok érvényesek. a [ ] és a (). (tagkiválasztás.5) Ezek olyan operátorok (mûveleti jelek). 2). Általános és absztrakt felületek felépítésénél például gyakran használunk olyan operátorokat.2. complex c = b. §4. Operátorok túlterhelése 345 void f() { complex a = complex(1.4) sem. hogy b=(b+c)*a.4. delete[ ] A következõknek viszont nem lehet felhasználói jelentést tulajdonítani: :: (hatókör-feloldás. 3.4) . és nem azt.3). c = a*b+complex(1. Az operátorok túlterhelésének legnyilvánvalóbb alkalmazásai közül sok konkrét típusokra vonatkozik (§10. a ?: (§6.2) sem terhelhetõ túl. §5. A háromparaméterû feltételes-kifezés operátor. így a második kifejezés azt jelenti. complex b = complex(1. §15.6) és a typeid (§15.2) jelentését meghatározó függvényeket megadhatjuk: + | -= << >= -> ~ *= >> && [] * ! /= >>= || () / = %= <<= ++ new % < ^= == -new[ ] ^ > &= != ->* delete & += |= <= .1). Operátor függvények A következõ operátorok (§6.

Az operátorral való jelölés csak az operátor függvény közvetlen meghívásának rövidítése: void f(complex a. Ha mindkettõ értelmezett. Ha @ kétoperandusú mûveletet jelöl.a) // ::operator+(a.X). Elsõ pillantásra nyilvánvalónak és egyszerûnek tûnhet a ** operátort használni a hatványozásra. illetve hogy egyáltalán bármelyik alkalmazható-e: class X { public: void operator+(int). Az operátor függvényeket ugyanúgy deklarálhatjuk és hívhatjuk meg.operator@(bb)-t. melyik alkalmazható. ehelyett a függvényhívási jelölés használható. void operator+(X. complex b) { complex c = a + b. ha a rendelkezésre állókon kívül további operátorokra is szükség van.operator+(1) // ::operator+(X(1). de gondoljunk csak meg: a ** mûveleti jel balról kössön. például operator <<.0) . Ezek a megszorítások túl szigorúnak tûnhetnek. a+1. hogy pow(). complex d = a. mint a Fortranban. vagy jobbról. mint a többi függvényt. void f(X a) { a+1. Így például ne **-ot használjunk.double). hanem azt.1. a túlterhelés-feloldási szabályok (§7.0.346 Absztrakciós módszerek Új mûveleti jeleket sem adhatunk meg.4) döntik el. mint az Algolban? Az a**p kifejezést hogyan értelmezzük: mint a*(*p)-t vagy mint (a)**(p)-t? Az operátor függvények neve az operator kulcsszóból és azt követõen magából az operátorból áll. }.bb)-t jelöli. } // rövid forma // explicit hívás A complex elõzõ definicióját adottnak véve a fenti két kezdeti értékadás jelentése azonos. Egy. } // a. void operator+(X. X(int). 11.2. de rugalmasabb szabályok könnyen az egyértelmûség elvesztéséhez vezetnének.1.és kétoperandusú mûveletek Kétoperandusú mûveleti jelet egyparaméterû nem statikus tagfüggvényként vagy kétparaméterû nem tag függvényként definiálhatunk. vagy operator@(aa. akkor aa@bb vagy aa.operator+(b). 1+a.

2. ez biztosítja. vagy operator@(aa. X operator/().1) // hiba: háromoperandusú // hiba: egyoperandusú / }. hogy elsõ operandusuk balérték (lvalue) lesz (§4.operator@()-t. illetve hogy egyáltalán bármelyik alkalmazható-e.11. Ha mindkettõ értelmezett. a -> operátort a §11. melyik alkalmazható.4. hogy legyen.) Ha mindkettõ értelmezett. Ha @ elõtag és egyoperandusú mûveletet jelöl. operator() és az operator-> nem statikus tagfüggvény kell.2. X operator-(X. X operator&(X. akár utótagként használt) mûveleti jelek paraméter nélküli nem statikus tagfüggvényként vagy egyparaméterû nem tag függvényként definiálhatók.operator@(int)-et. Operátort csak a nyelvi szabályoknak megfelelõen definiálhatunk (§A. // nem tag függvények : X operator-(X).6. 11. (Ezt részletesebben a §11.X).11 pont írja le. a () operátort a §11. . illetve hogy egyáltalán bármelyik alkalmazható-e.6). akkor aa@ vagy aa.2. a memóriafoglaló és felszabadító operátorokat a §6. akkor @aa vagy aa.operátorokat a 11.X). a túlterhelés-feloldási szabályok (§7.10. melyik alkalmazható.9.int)-et jelöli.9. Az operátorok elõre meghatározott jelentése A felhasználói operátorok jelentésének csak néhány elõírásnak kell megfelelniük.X). ismét csak a túlterhelés-feloldási szabályok (§7. operator[ ]. // elõtagként használt egyoperandusú mínusz (mínusz elõjel) // kétoperandusú mínusz (kivonás) // utótagként használt csökkentõ operátor // hiba: nincs operandus // hiba: háromoperandusú // hiba: egyoperandusú % A [ ] operátort a §11. Az operator=. // elõtagként használt egyoperandusú & (cím) // kétoperandusú & (és) // utótagként használt növelõ operátor (lásd §11. X operator--(X&. X operator&(X).2. X operator++(int).11 és a §15. vagy operator@(aa)-t jelöli.11.4) döntik el. Operátorok túlterhelése 347 Az egyoperandusú (akár elõ-. X operator%(X). X operator-().6 pontokban írjuk le.X. Ha @ utótag és egyoperandusú mûveletet ad meg. a §10. X operator-(X. így nem lehet például egyoperandusú % vagy háromoperandusú + mûveletünk: class X { // tagok (a 'this' mutató automatikus): X* operator&().4) döntik el.5).8.int). a ++ és -.

ha legalább egy felhasználói típus elõfordul benne. (az osztályba tartozó objektumokra vonatkozó =. operátorokat kivéve).2. ilyen összefüggések nem állnak fenn a felhasználói operátorokra. Ebbõl adódóan nem definiálható olyan operátor. A C++ tehát bõvíthetõ. (vesszõ. melyeket arra szánunk.. nem lehetnek tagfüggvények.2. hogy a=a+1.348 Absztrakciós módszerek Bizonyos beépített operátorok jelentése megegyezik más operátoroknak ugyanazon paraméterre összetetten gyakorolt hatásával. 11. // . hogy megadtuk a Z::operator+() és Z::operator=() mûveleteket. }. void f(X a.2) operátorok elõre definiáltak. a & (címképzõ) és a . X b) { a = b. Az olyan operátorok. hogy elsõ paraméterként valamilyen alaptípust fogadjanak el. hogy a programozó egy kifejezés értelmét csak akkor módosíthassa. Hacsak a felhasználó nem gondoskodik róla. amikor egy complex változót akarunk a 2 egészhez hozzáadni: az aa+2 kifejezést alkalmas tagfüggvény . akkor ++a jelentése megegyezik a+=1-gyel. így a fordítóprogram például nem fogja kitalálni a Z::operator+=() mûvelet jelentését pusztán abból. & és .) privát Alkalmas módon definiálva azonban új jelentés is tulajdonítható nekik. ha privátként adjuk meg azokat: class X { private: void operator=(const X&).b. &a. Operátorok és felhasználói típusok Az operátoroknak tagfüggvénynek kell lenniük vagy paramétereik között legalább egy felhasználói típusnak kell szerepelnie (kivételek ez alól a new és delete operátorok jelentését felülbíráló függvények. amely kizárólag mutatókkal mûködik. Például ha a egy int. void operator. Hagyományosan az = (értékadó). } // hiba: az értékadó operátor privát // hiba: a cím operátor (&) privát // hiba: a vesszõ operátor (.(const X&).3. ami pedig azt jelenti.. §6. de nem változtatható meg. Vegyük például azt az esetet.) Ez a szabály biztosítja. Ezeket az elõre meghatározott jelentéseket az általános felhasználó elõl elrejthetjük. ha osztályba tartozó objektumra alkalmazzuk azokat. a. void operator&().

Névterek operátorai Az operátor mindig valamilyen osztály tagja vagy valamilyen névtérben (esetleg a globálisban) definiált. extern ostream cout. §11. Az ilyesmit rendszerint nem tag függvényekkel kezelhetjük (§11.3. hogy nem lép-e fel többértelmûség. A felsorolások felhasználói típusok.2. 11. }. tue. hogy 2+aa-val és aa+2-vel is megbirkózzunk. Vegyük például a standard könyvtár karakterlánc-kiírási mûveletének egyszerûsített változatát: namespace std { // egyszerûsített std class ostream { // . nem tételezheti fel róla a felcserélhetõséget (kommutativitást). akkor is két tagfüggvény kellene ahhoz.. Operátorok túlterhelése 349 megléte esetén értelmezhetjük aa.4.11. class string { // . } A fordítóprogram minden kifejezést ellenõriz. hogy annak alapján 2+aa-t mint aa+2-t kezelje. }. hogy a 2.. amelynek + olyan tagfüggvénye lehetne.operator+(2)-ként..2. wed. a kifejezés ellenõrzése a §7. thu. Minthogy a fordítóprogram nem ismeri a felhasználói + mûvelet jelentését. fri. } ostream& operator<<(ostream&. mon. De ha lenne is. mert nincs int osztály. így rájuk is értelmezhetünk operátorokat: enum Day { sun.. Ha egy felhasználói operátor is biztosít lehetséges értelmezést. ostream& operator<<(const char*). const string&). de a 2+aa kifejezést nem. sat }.4 pontban leírtak szerint történik.5). . Day& operator++(Day& d) { return d = (sat==d) ? sun : Day(d+1).operator(aa) eredményre jussunk.

mint ahogy a függvényeket paramétereik típusa szerint (§8. hogy nem tettem mindent elérhetõvé az std névtérbõl azáltal. const std::string&) . amikor a << számára alkalmas definíciót keresünk. Ehelyett az std:: elõtagot alkalmaztam a string és a cout elõtt. világ!”. Így aztán a fordítóprogram megtalálja és felhasználja a következõ függvényt: std::operator<<(std::ostream&. így std::cout << s jelentése: operator<<(std::cout. A C stílusú karakterláncok (char*) kimeneti mûvelete az std::ostream egy tagja. így std::cout << p jelentése definíció szerint: std::cout. hogy azt írtam volna: using namespace std. amelyet az std::string-re alkalmazhatnánk.s) A névtérben definiált operátorokat ugyanúgy operandusuk típusa szerint találhatjuk meg. } Ez természetesen azt írja ki. Vagyis a legrendesebben viselkedve nem „szennyeztem be” a globális névteret és egyéb módon sem vezettem be szükségtelen függéseket. így az std is szóba kerül.350 Absztrakciós módszerek int main() { char* p = "Helló". Minthogy a cout az std névtérben van. std::cout << p << ".6).2. " << s << "!\n". hogy „Helló. De miért? Vegyük észre. std::string s = "világ".operator<<(p) Mivel azonban az std::ostream-nek nincs olyan tagja.

♦ Keresünk egy operator@ deklarációt az x@y kifejezést körülvevõ környezetben. ♦ Ha Y az M névtér tagja.1) módosítása nélkül definiálhatja. Operátorok túlterhelése 351 Jelöljön @ egy kétoperandusú mûveletet.9. } . Egy matematika tankönyvet olvasva azt várnánk.) Az egyoperandusú mûveletek feloldása hasonlóan történik. complex b = 3. complex e = -b-c. hogy a következõ függvény mûködik: void f() { complex a = complex(1. Az operátorok el nem rejtése biztosítja. az operator@-t az M névtérben keressük. ha egyáltalán van ilyen.3. A szabványos iostream könyvtár például definiálja a << tagfüggvényeket a beépített típusokra. hogy a beépített operátorok nem válnak elérhetetlenné. nem felhasználói típusok (§4.3. (A typedef-fel megadott nevek csak szinonimák.3.6). keresünk egy operator@-t. complex d = 2+b. amely az X osztálynak vagy valamelyik bázisosztályának tagfüggvénye. §11. b = c*2*c. hogy bárkinek is tessék.7).2. Ez a keresési eljárás csak akkor alkalmazandó. hogy az operátorok feloldásában a tagfüggvények nem élveznek elõnyt a nem tag függvényekkel szemben. complex c = a+2. a feloldási szabályokat kell alkalmazni (§7. akkor x@y feloldása. és hogy a felhasználó a meglevõ osztálydeklarációk módosítása nélkül adhat meg új jelentéseket. azaz a paraméterek típusának megfelelõ függvény megkeresése a következõképpen történik: ♦ Ha X egy osztály.2). Ha x az X típusba. Ha az operator@ többféle deklarációját is megtaláltuk.2. hogy a felhasználói konverziókat (§11. ami azt is jelenti. ♦ Ha X az N névtér tagja.11. Jegyezzük meg. Komplex szám típusok A komplex számoknak a bevezetõben említett megvalósítása túl keveset nyújt ahhoz. Ez eltér a névvel megadott függvények keresésétõl (§8.4) is figyelembe vesszük. ha legalább egy felhasználói típus szerepel az operandusok között. hogy a legjobb egyezést megtaláljuk. 11.2. az y pedig az Y típusba tartozik. az operator@-t az N névtérben keressük. a felhasználó viszont a felhasználói típusoknak a << mûvelettel való kimenetre küldését az ostream osztály (§21.4).

y).. mint például a +=.operator+=(y) // r2. Azokat az operátorokat. // az ábrázolás elérése a += operátoron keresztül } Ezen deklarációk alapján már leírhatjuk a következõt: void f(complex x. im. r2 += z.operator+=(z) Esetleges hatékonysági különbségektõl eltekintve r1 és r2 kiszámítása egyenértékû. A complex osztály egy konkrét típus. mint például a +. az osztályon kívül definiálom és az alapvetõ operátorok segítségével valósítom meg: class complex { double re.. // . hogy a complex osztály definiálása az operátor-túlterhelésre vonatkozó szinte valamennyi szabály alkalmazását igényli. return r += b. ha csak azokat az operátorokat adjuk meg magában az osztályban. } // r1 = operator+(operator+(x. amelyek csak egy új értéket állítanak elõ paramétereik alapján.1.z) // r2 = x // r2. complex b) { complex r = a. .352 Absztrakciós módszerek Ráadásul elvárnánk. // hozzá kell férni az ábrázoláshoz complex operator+(complex a. így felépítése megfelel a §10. public: complex& operator+=(complex a). r2 += y. complex y. például a == az összehasonlításra és a << a kimenetre. amelyek értelmüknél fogva módosítják elsõ paraméterüket. és még a matematikai függvények (mint a sin() és a sqrt()) megfelelõ készletét is igényelnénk. hogy létezzék néhány további mûvelet is. 11. ha minél kevesebb függvény férne hozzá közvetlenül egy adott objektum belsõ adatábrázolásához.3-beli elveknek. Ráadásul a komplex aritmetika felhasználói olyan nagy mértékben építenek az operátorokra.3. Tag és nem tag operátorok Elõnyös lenne. complex z) { complex r1 = x+y+z. Ezt úgy érhetjük el. }. complex r2 = x.

pedig pusztán abból következik.3. például a += -t és a *= -t általában könnyebb definiálni. Operátorok túlterhelése 353 Az összetett értékadó operátorokat. De nincs mindig jó optimalizálónk és nem minden típus olyan „egyszerû”.re. return *this.re. ha nem használunk ideiglenes változókat: inline complex& complex::operator+=(complex a) { re += a.11. 11. } A fenti megoldásnál nincs szükség ideiglenes változóra az eredmény tárolására és a fordítóprogram számára is könnyebb feladat a teljes helyben kifejtés. hogy az összeadásnál 3 objektum játszik szerepet (a két összeadandó és az eredmény). amely különbözõ típusú paramétereket is elfogad. Ez többnyire meglepést kelt.5 pont tárgyalja.im. Egy jó fordítóprogram az optimálishoz közeli kódot készít a sima + operátor használata esetén is. public: complex& operator+=(complex a) { re += a. hogyan adhatunk meg olyan operátorokat. im += a. Ezt könnyen megvalósíthatjuk. amelyek hozzáférhetnek az osztály ábrázolásához. ha megadjuk az operátor megfelelõ változatait: class complex { double re. hogy a complex d = 2+b. kódot kezelni tudjuk. im. mint egyszerû megfelelõiket. im += a. ezért a §11. } .2.im. return *this. Vegyes módú aritmetika Ahhoz. olyan + operátorra van szükségünk. A Fortran kifejezésével élve tehát „vegyes módú aritmetikára” (mixedmode arithmetic) van szükség. míg a += operátornál csak kettõ. mint a complex. Az utóbbi esetében hatékonyabb a megvalósítás. a + és * operátorokat.

double b) { complex r = a.. return r += a. complex b) { complex r = a.re=3. ezt tükrözik a fenti definíciók is. return *this.3. // complex::operator+=(double)-t hívja meg } complex operator+(double a. } }. // .complex)-et hívja meg } 11. // complex::operator+=(double)-t hívja meg } Egy double hozzáadása egy komplex számhoz egyszerûbb mûvelet. // operator+(double.im=0-t kell jelentenie . complex b) { complex r = b. // operator+(complex. return r += b. Kezdeti értékadás A complex változóknak skalárokkal való kezdeti és egyszerû értékadás kezeléséhez szükségünk van a skalárok (egész vagy lebegõpontos (valós) értékek) complex-szé átalakítására: complex b = 3. b.complex)-et hívja meg complex r2 = x+2. return r += b. complex operator+(complex a.3. // complex::operator+=(complex)-et hívja meg } complex operator+(complex a. így hatékonyabbak lesznek. // b. complex y) { complex r1 = x+y. A double operandust kezelõ mûveletek nem érintik a komplex szám képzetes részét.354 Absztrakciós módszerek complex& operator+=(double a) { re += a.. A fenti deklarációk mellett most már leírhatjuk a következõt: void f(complex x. // operator+(complex.double)-t hívja meg complex r3 = 2+x. mint egy komplex szám hozzáadása.

Ha egy adott típusú értéket kell létrehozni egy (kezdeti vagy egyszerû) értékadó kifejezés értékébõl és ebbõl egy konstruktor létre tudja hozni a kívánt típusú értéket. im(i) { } // . Ez a konstruktor a valós számegyenesnek a komplex síkba való szokásos beágyazását jelenti. im. public: complex(double r) : re(r). Természetesen szükségünk lesz egy olyan konstruktorra is. im(0) { } complex(double r. Felhasználói konverzióra csak akkor kerül sor automatikusan. Operátorok túlterhelése 355 Az olyan konstruktor. amelynek két double típusú paramétere van. A fenti egyenértékû a következõvel: complex b = complex(3). }. akkor konstruktort alkalmazunk. konverziót jelent a paraméter típusáról a konstruktor típusára: class complex { double re.11. hogyan adhatunk meg csak explicite meghívható konstruktorokat. Ezért az egyparaméterû konstruktorokat nem kell explicit meghívnunk: complex b = 3.0) kezdõértéket adó alapértelmezett konstruktor is hasznos: class complex { double re...4).1 pontot. public: complex() : re(0).7.. im(0) { } complex(double r) : re(r). . Arra nézve. im(0) { } // . ha az egyértelmû (§7.. hogyan hozhatunk létre egy adott típusú értéket. }. lásd a §11. amely egyetlen paramétert vár. Egy konstruktor mindig azt írja elõ. im. és a (0. double i) : re(r).

im(i) { } // . complex z2 = { 3.4. 4 }. . im.2. Ezenkívül pedig sok tag esetén fárasztó dolog kézzel kiírni a tagonkénti másolást és könnyû közben hibázni (§10.5) is. amelyeknél ez megfelelõ. A mûködést pontosan így határozhatnánk meg: class complex { double re.3). A fordítóprogram is ismeri és azt is.5): complex z1 = { 3 }. public: complex(double r =0.. hogy ismeri az alapértelmezett mûködést. A másoló konstruktor paramétereként referenciát kell használnom. mintha bármi mást írnék..356 Absztrakciós módszerek Alapértelmezett paraméter-értékeket használva így rövidíthetünk: class complex { double re.3. és a kód olvasójáról feltételezem. im. im(c. hogyan lehet azt optimalizálni. }. public: complex(const complex& c) : re(c. §4.4.re). mert a függvény meghívása végtelen rekurzióhoz vezet.im) { } // hiba hibás. A másoló konstruktor határozza meg a másolás jelentését – beleértve a paraméter másolásáét is – így a complex::complex(complex c) : re(c. Az alapértelmezett másoló konstruktor egyszerûen lemásolja a tagokat. // hiba: complex rendelkezik konstruktorral // hiba: complex rendelkezik konstruktorral 11.. Rövidebb lesz a kód.re). double i =0) : re(r). Ha egy típusnak van konstruktora.im) { } // .. Másolás A megadott konstruktorokon kívül a complex osztálynak lesz egy alapértelmezett másoló konstruktora (§10. }. im(c.7. Én elõnyben részesítem az alapértelmezett másoló konstruktort azon osztályok esetében.6. a kezdeti értékadásra nem használhatunk kezdõértéklistát (§5.9.

Errõl bõvebben ír a §11. complex operator+(complex.0). Mindig az osztály készítõje dönt. complex y(2. A felhasználó szemszögébõl nézve nincs sok különbség egy complex és egy const complex& paramétert kapó függvény között. mert az alapértelmezett mûködés pont megfelelõ. A másoló konstruktor – akár a fordítóprogram hozta létre. A kezdeti értékadáshoz hasonlóan a két azonos osztályba tartozó objektum közötti értékadás alapértelmezés szerint tagonkénti értékadást jelent (§10.11.0) létrehozása. de ilyen egyszerû osztály esetében erre nincs ok.2.1). Ezek szerepét a nyelv a kezdeti értékadáséval azonosként határozza meg (§7. 11. // x kezdõértéke 2 // y kezdõértéke (2. érték visszaadásakor és a kivételkezeléskor is (lásd §11. // complex(2) létrehozása.complex). .1.. Konstruktorok és konverziók A négy alapvetõ aritmetikai mûveletnek eddig három-három változatát határoztuk meg: complex operator+(complex.0).1). ezzel adunk kezdõértéket y-nak A fordítóprogram azonban optimalizál és elhagyja a másoló konstruktor meghívását. ezzel adunk kezdõértéket x-nek // complex(2. hanem paraméter-átadáskor.3.3.2) vagy ha egy konstruktort explicit-ként adunk meg (§11. Elvileg a másoló konstruktort az ilyen egyszerû kezdeti értékadásoknál használjuk: complex x = 2. Ha a másoló konstruktort priváttá tesszük (§11. complex operator+(double. complex y = complex(2. §7. Operátorok túlterhelése 357 Más.7.double).complex). A complex osztálynál erre a célra megadhatnánk kifejezetten a complex::operator= mûveletet. complex paraméterû függvények esetében én érték és nem referencia szerinti paraméter-átadást használok. az = stílusú értékadás által elfogadható értékek körét a () stílusú értékadás által elfogadotthoz képest korlátozhatjuk.2. §14.2. Írhattuk volna így is: complex x(2).6 pont.0) A complex-hez hasonló aritmetikai típusok esetében jobban kedvelem az = jel használatát. akár a programozó írta – nemcsak a változók kezdõértékének beállítására használatos.7).5. // .5)..

3. Valójában majdnem mindegyik úgy mûködik. Ahol ilyen okok nem lépnek fel jelentõs mértékben. a kifejezés többértelmû és hibás (lásd §7. és ami fárasztó. hogy a vegyes módú aritmetikából adódóan nagyon sokféle függvényt kelljen megírnunk. és -> operátorok bal oldalán nem történik automatikus felhasználói konverzió. ideiglenes változónak számítanak és amint lehetséges. A .y) x==3. } // rendben: complex(3)+z // hiba: 3 nem egy osztály objektuma // hiba: 3 nem egy osztály objektuma . melyek miatt jobb külön függvényeket megadni.complex). Ez akkor is így van. implicit (a kifejezésbe beleértett): void g(complex z) { 3+z. a fordítóprogram feladata a legalkalmasabb változat kiválasztása. Ha egy függvény vagy operátor több változattal rendelkezik.complex(3)) 3==y. ott a függvény legáltalánosabb formáját megadva (esetleg néhány kritikus változattal kiegészítve) és a konverziókra hagyatkozva elkerülhetjük. megsemmisülnek (lásd §10. hogy a paramétereket egy közös típusra alakítja. void f(complex x. a háromparaméterûekbõl huszonhét és így tovább.4. amely egy double értéket alakít complex-szé. Az olyan objektumok. Egyes esetekben például a konverzió túl bonyolult mûvelet lehet. Ezek a változatok gyakran nagyon hasonlóak. majd egy szabványos algoritmust hajt végre. 3+=z. Ha nincs legjobb változat.358 Absztrakciós módszerek Ez fárasztóvá válhat. // jelentése operator==(x. // jelentése operator==(x. // jelentése operator==(complex(3). complex y) { x==y. típuskonverziókra hagyatkozhatunk.10). ha maga a . ott könnyen elõfordulhatnak hibák. hogy a paraméterek minden lehetséges párosítására megadnánk egy függvényt. a kétparaméterûekbõl kilenc. melyeket a konstruktor közvetlen meghívása vagy automatikus használata hozott létre.4). Tegyük fel. így a complex osztály számára elég egyetlen egyenlõség-vizsgáló mûveletet megadnunk: bool operator==(complex. máskor bizonyos paramétertípusokra egyszerûbb algoritmusok alkalmazhatók. a paramétertípusok és a lehetséges (szabványos vagy felhasználói) konverziók alapján. Ahelyett.operator+=(z).y) } Lehetnek azonban okok. ha minden paraméter háromféle típusú lehetne? Minden egyparaméterû mûveletbõl három változat kellene. Mi lenne. hogy complex osztályunknak van egy olyan konstruktora.

real()==b. Literálok Osztály típusú literálokat nem definiálhatunk abban az értelemben.imag()==b. im. A valós és a képzetes rész lekérdezése például sûrûn használatos: class complex { double re. így const-ként adható meg. A real() és imag() függvények alapján egy sor hasznos függvényt definiálhatunk anélkül. hogy a mûvelet bal oldali operandusként balértéket vár. complex b) { return a.2 és 1. hogy a valós és a képzetes részt elég olvasnunk. ha a tagfüggvények fel vannak készítve a kezelésükre. 11.7. } // . Kiegészítõ tagfüggvények Eddig csak konstruktorokat és aritmetikai mûveleteket adtunk a complex osztályhoz. }. } Vegyük észre. hogy azoknak hozzáférést kellene adnunk a complex osztály adatábrázolásához: inline bool operator==(complex a. Egyszerû és helyben kifejtett (inline) konstruktorok esetében ésszerû a literál paraméterû konstruktorhívásokra mint literálokra gondolni. A tényleges használathoz ez kevés. A complex(3) kifejezést például én úgy tekintem.3. . írnunk sokkal ritkábban kell.3. mint egy complex értékû literált.6.. 11.. Az egyparaméterû konstruktorok általános eljárást biztosítanak erre a célra.2e3 double típusú literálok. ahogyan 1. Az alapvetõ típusokba tartozó literálokat viszont gyakran használhatjuk.imag(). } double imag() const { return im. noha a szó „technikai” értelmében véve nem az. public: double real() const { return re.real() && a.11. A complex osztály többi tagfüggvényével ellentétben a real() és az imag() nem változtatja meg egy complex objektum értékét. Operátorok túlterhelése 359 Ezt kihasználva egy mûveletet tagfüggvénnyé téve kifejezhetjük.

bool operator==(complex.complex). } double imag() const { return im..complex).complex). 11. bool operator!=(complex.. // -=. } // d hozzárendelése z. // -. z = complex(z.3. Kiegészítésként egy sor segédfüggvényt kell biztosítanunk: complex operator+(complex. *. complex operator+(double.real(). im. } complex& operator+=(complex). és /= }. istream& operator>>(istream&. Segédfüggvények Ha mindent összerakunk. és / complex operator-(complex). complex operator+(complex. public: complex(double r =0. ostream& operator<<(ostream&. // bemenet // kimenet // egyoperandusú mínusz // egyoperandusú plusz . a következõt írhatjuk: void f(complex& z.complex). complex operator+(complex).im-hez Egy jól optimalizáló fordító ebbõl egyetlen értékadást készít. complex& operator+=(double). double i =0) : re(r). complex osztályunk így alakul: class complex { double re.d).complex). *=.360 Absztrakciós módszerek Ha „részleges frissítésre” van szükségünk. im(i) { } double real() const { return re.complex&). double d) { // .8.double).

A következõ segédfüggvények is nagyrészt ezekre építenek.11. automatikus átalakítást megadni felhasználói adattípusról beépített adattípusra (mert a beépített adattípusok nem osztályok) 2. de nemkívánatos következményei vannak. // . Megadhatnánk olyan függvényeket is. Operátorok túlterhelése 361 Vegyük észre. // a kényelmesebb jelölésért // a kényelmesebb jelölésért Végül szükségünk lesz a további alapvetõ matematikai függvényekre: complex acos(complex). akkor az X::operator T() függvény hatá- . Felhasználói szemszögbõl nézve az itt bemutatott complex osztály szinte azonos a complex<double>-lal (lásd a standard könyvtárbeli <complex>-et. double imag(complex). complex atan(complex). §22. átalakítást megadni egy újabban megadott osztályról egy régebbire. hogy a real() és imag() függvények szerepe alapvetõ az összehasonlító függvények definiálásában. double norm(complex). Ha T egy típus neve. Ezeket a feladatokat az átalakítandó osztály konverziós (átalakító) operátorának definiálásával oldhatjuk meg. double arg(complex).. double theta). amelyek a polár-koordinátás jelölést támogatják: complex polar(double rho.4. Egy konstruktor nem tud 1. double abs(complex). Konverziós operátorok Konstruktorok használata típuskonverzió céljára kényelmes lehet. complex asin(complex).. complex conj(complex). a régebbi osztály deklarációjának megváltoztatása nélkül. double real(complex). 11.5).

akkor arra a helyre a megfelelõ int érték fog kerülni: int main() { Tiny c1 = 2. int i = c1+c2. } Tiny& operator=(int i) { assign(i). Tiny c4 = c3. Ha egy int helyén egy Tiny szerepel. melynek objektumait aritmetikai kifejezésekben szabadon keverhetjük egészekkel: class Tiny { char v. i = c3-64. c2 = c3-64. határozzuk meg a Tiny-rõl int-re való automatikus konverziót. c1 = c1+c2. az alapértelmezett másoló konstruktor és értékadás éppen megfelelõ. hogy a konverzió céltípusa az operátor nevének része és nem szabad kiírni. Tiny c2 = 62. v=i. } public: class Bad_range { }. operator int() const { return v. mint a konverziós függvény visszatérési értékét: Tiny::operator int() const { return v. } }. return *this. hogy az érték a megengedett tartományba esik-e. } // konverzió int típusra Amikor egy Tiny egy egésztõl kap értéket vagy kezdõértéket. nem negatív egészeket ábrázoló Tiny osztályt. Minthogy egy Tiny másolásakor nincs szükség az értékellenõrzésre. } int Tiny::operator int() const { return v. c3 = c4. Tiny c3 = c2-c1. } // helyes // hiba Ilyen tekintetben a konverziós operátor a konstruktorra hasonlít. void assign(int i) { if (i&~077) throw Bad_range(). Jegyezzük meg. ellenõrizzük. // c3 = 60 // nincs tartományellenõrzés (nem szükséges) // i = 64 // tartományhiba: c1 nem lehet 64 // i = -4 // tartományhiba: c2 nem lehet -4 // nincs tartományellenõrzés (nem szükséges) } . hogy a Tiny változókra is lehetõvé tegyük az egészeknél szokásos mûveleteket. Tiny(int i) { assign(i). Ahhoz. a Tiny::operator int()-et. Definiálhatunk például a 6 bites.362 Absztrakciós módszerek rozza meg az X típus T-re való konverzióját.

az a kifejezések többértelmûségéhez vezethet. int i) { t+i. amely automatikusan a cin objektum állapotát tükrözõ értékre alakítódik.1.11. A kezdeti értékadásnál hasonló a helyzet. de ne mindkettõre. lehetséges. Ha késõbb valamelyik ilyen függvény annyira népszerû lesz. Az istream és ostream típusok egy konverzió segítségével támogatják az alábbihoz hasonló vezérlési szerkezeteket: while (cin>>x) cout<<x.3. amelyeknél az adatoknak (a konverziós operátor által definiált) kiolvasása egyszerû feladat. Általában azonban nem jó ötlet adatvesztéssel járó automatikus konverziót meghatározni két típus között. A többértelmûséget mint hibát jelzi ugyan a fordítóprogram. 11.Tiny). de kiküszöbölni fáradságos lehet. Célszerû takarékoskodni a konverziós operátorok bevezetésével. vagy ha van egy egyedi konverzió V-rõl Z-re. ha kezdetben nevesített függvényekkel végeztetjük az átalakítást (például X::make_int()). Ha túl sok van belõlük. Ha vannak felhasználói konverziók és felhasználói operátorok is.Tiny(i)) vagy int(t)+i ? } Ezért vagy felhasználói konverziókra építsünk. Talán a legjobb eljárás az. ellentétben az értékadással és a kezdõérték-adással. hogy többértelmûség lép fel a felhasználói és a beépített operátorok között: int operator+(Tiny.3. // hiba. többértelmû: operator+(t. void f(Tiny t.). vagy felhasználói operátorokra. Operátorok túlterhelése 363 A konverziós függvények különösen hasznosak olyan adatszerkezetek kezelésekor.4. hogy alkalmazása nem „elegáns” többé. ha van olyan X::operator=(Z) értékadó operátor. Ezt azután a while utasítás ellenõrzi (§21. amely szerint V típus egyben Z is. . Többértelmûség Egy X osztályú objektum értékadása egy V típusú értékkel akkor megengedett. A cin>>x bemeneti mûvelet egy istream& referenciát ad vissza. akkor kicserélhetjük az X::operator int() konverziós operátorra.

X f(X). }. */ X(int).4). g(Z(X("Mack")))-et // nem próbáltuk // rendben: g(Z(X("Doc"))) // rendben: g(Z(X("Suzy"))) } A felhasználói konverziókat a fordítóprogram csak akkor veszi figyelembe. // hiba: többértelmû f(X(1)) vagy f(Y(1))? // rendben // rendben // hiba: két felhasználói konverzió szükséges. A konverziós szabályok se nem a legegyszerûbben megvalósítható.. void k2() { h(1). mint megtalálni egy hibát. void h(XX). */ Z(X).. g("Mack"). se nem a legegyszerûbben leírható. }. void h(double). */ XX(int). g(Z("Suzy")). mert ehhez csak egy szabványos (és nem felhasználói) konverzióra van szükség (§7.. Ezt a helyzetet közvetlen konverzióval kell megoldani. de nem is az elképzelhetõ legáltalánosabb szabályok. f(Y(1)). A programozónak sokkal könnyebb egy többértelmûséget feloldani. ha szükségesek egy hívás feloldásához: class XX { /* . }. X(char*). Néha a kívánt típusú érték többféleképpen is létrehozható. amit egy nem sejtett konverzió alkalmazása okoz. . Viszont viszonylag biztonságosak és alkalmazásukkal kevésbé fordulnak elõ meglepõ eredmények... */ Y(int). class Y { /* . } // h(double(1)) vagy h(XX(1))? h(double(1))! A h(1) hívás a h(double(1)) hívást jelenti... Y f(Y).. void k1() { f(1). ez pedig hiba: class X { /* . f(X(1)). az automatikus felhasználói konverzióknak csak egy szintje megengedett. Z g(Z). class Z { /* . g(X("Doc")). }.364 Absztrakciós módszerek Bizonyos esetekben a kívánt típusú értéket konstruktorok és konverziós operátorok ismételt alkalmazásával állíthatjuk elõ.

} // kétszeres pontosságú összeadás // Quad aritmetika kikényszerítése Ezen tervezési mód választásának egyik oka. // . Quad r2 = Quad(a1)+a2..int(). void f(double a1. int i = a. milyen fokú pontosságot akar a programozó egy összeadásnál..double().int(). operator int(). Operátorok túlterhelése 365 Az elemzés során alkalmazott szigorúan „alulról felfelé” való haladás elvébõl az is következik. } Az elemzés itt is alulról felfelé történik. // i = a. Quad operator+(Quad. double a2) { Quad r1 = a1+a2. egyszerre csak egy operátornak és paramétereinek figyelembe vételével. }. hogy a szigorú „alulról felfelé” való haladás elve érthetõbb. void g(Real a) { double d = a.. a másik pedig az. .Quad). Ha egy kezdeti vagy egyszerû értékadás mindkét oldalának eldõlt a típusa. // i = a.11. hogy a visszatérési értéket nem vesszük figyelembe a túlterhelések feloldásakor: class Quad { public: Quad(double).. hogy nem a fordítóprogram dolga eldönteni. // d = a. i = a. d = a. // . }.double(). akkor az értékadás feloldása ezen típusok figyelembe vételével történik: class Real { public: operator double(). // d = a.

class Vector { float v[4]. Vector operator*(const Matrix& m..4). és csak tagfüggvényeiken keresztül kezelhetjük õket. Ahhoz. const Vector& v) { Vector r.2. alacsonyszintû hozzáférést megengedni. hogy ezt elkerüljük. mind a Vector osztály alkalmazza az adatrejtés elvét. }. for (int j = 0... j++) r.v[j] * v. i++) { // r[i] = m[i] * v.v[i] += m. csak az elsõt. Barát függvények Amikor egy függvényt egy osztály tagjaként adunk meg.5. }. logikailag különbözõ dolgot jelzünk: 1. // . a * operátort mindkét osztályban friend („barát”) függvényként határozzuk meg: class Matrix. friend Vector operator*(const Matrix&. } . j<4. } return r. A függvény az osztály hatókörébe tartozik. // .v[j]. friend Vector operator*(const Matrix&. 2. akkor ez csak az elsõ két tulajdonságot jelenti. r. A függvény hozzáférhet az osztály deklarációjának privát részeihez.. A függvényt az osztály egy objektumára kell meghívni (egy this mutató áll a rendelkezésére). class Matrix { Vector v[4]. hogy minden felhasználó írhassa és olvashassa a Matrix és Vector osztályok teljes adatábrázolását. const Vector&).v[i] = 0. A szorzást megvalósító függvény azonban nem lehet mindkét osztály tagja. ha friend-ként („barátként”). Nem is akarunk általános. Természetesen mind a Matrix. Definiáljunk például egy Matrix-ot egy Vector-ral szorzó operátort. Ha egy tagfüggvényt static-ként határozunk meg (§10. i<4. const Vector&). for (int i = 0. 3.v[i]. három.366 Absztrakciós módszerek 11.

.. Számos esetben viszont választhatunk. hogy friend osztályokat csak szorosan összetartozó fogalmak kifejezésére szabad használnunk. friend Matrix invert(const Matrix&). }. E deklaráció hatására a List_iterator osztály összes tagfüggvénye a List osztály barát függvénye lesz.. // hiba: a hatókörben nincs Xform // hiba: a hatókörben nincs invert() . }.. // . A barát függvények elérése A tagfüggvények deklarációjához hasonlóan a friend deklarációk sem vezetnek be új nevet a tartalmazó hatókörbe: class Matrix { friend class Xform. class List { friend int* List_iterator::next().. mint a tagfüggvények. int* next().. A tagfüggvényekhez hasonlóan a barát függvényeket is az osztály deklarációjában adjuk meg. }. // . }.4). Nem szokatlan helyzet. 11.11. Világos..1.5. // . hogy egy osztályt tag (beágyazott osztály) vagy nem tag barátként adunk meg (§24. hogy egy osztály összes tagfüggvénye egy másik osztály „barátja”. Operátorok túlterhelése 367 A friend deklarációt az osztály privát és nyilvános részébe is tehetjük. Xform x. Matrix (*p)(const Matrix&) = &invert. Ennek jelzésére egy rövidítés szolgál: class List { friend class List_iterator. így ugyanolyan mértékben hozzátartoznak az osztály felületéhez.. Egy osztály tagfüggvénye lehet egy másik osztály barát függvénye: class List_iterator { // .

még akkor is. azoknál a sablon osztályoknál pedig. // X h() "barátja" } // értelmetlen // paramétere alapján megtalálható // a hatókörben nincs f() . A közvetlenül tartalmazó névtér hatókörén kívüli neveket nem veszünk figyelembe: class AE { /* . friend class AE. // Y "barátja" } A barát függvényeket ugyanúgy megadhatjuk pontosan. ez kifejezetten fontos. ha nem a közvetlenül tartalmazó hatókörben adtuk meg: void f(Matrix& m) { invert(m). */ }. de elérhetjük paramétereik alapján is (§8. amelyek több különbözõ környezetben példányosíthatók (13. fejezet).. }.2. friend void h(const X&). hogy egy barát függvényt vagy egy tartalmazó hatókörben kell közvetlenül megadnunk.. // Y "barátja" class Y { friend class X. máskülönben nem hívhatjuk meg: // a hatókörben nincs f() class X { friend void f(). ha egy osztály nem ad hozzá titokban új neveket a tartalmazó hatókörhöz.. mint a barát osztályokat. friend class Z.. // nem "barátja" Y-nak namespace N { class X { /* . // a Matrix "barát" invert()-je } Ebbõl következik. }.. void g(const X& x) { f().. */ }.368 Absztrakciós módszerek Nagy programok és osztályok esetében elõnyös. A barát (friend) osztályt elõzõleg meg kell adnunk a tartalmazó hatókörben vagy ki kell fejtenünk az osztályt közvetlenül tartalmazó nem osztály típusú hatókörben. class Z { /* . vagy az osztályának megfelelõ paraméterrel kell rendelkeznie.6). h(x). */ }.

m2(). int m2() const. mert nem const referencia paraméterekre a fordító nem alkalmaz felhasználói átalakítást (§5. amely egy mûvelet különféle módozatait jeleníti meg: class X { // . hanem az. friend int f3(X).5. felhasználói átalakítást a fordító nem végez: void g() { 99. hacsak nem szól valamilyen érv amellett. Sokszor azonban van mérlegelési lehetõség. Az f1() globális függvény hasonló tulajdonsággal rendelkezik.. Az f2() és f3() paramétereire azonban alkalmazható ilyen: .m2()-õt Az X(int) átalakítást a fordító nem alkalmazza. Barátok és tagfüggvények Mikor használjunk barát függvényt és mikor jobb választás egy tagfüggvény egy mûvelet számára? Az elsõ szempont egy osztálynál az. Operátorok túlterhelése 369 11. Mivel egy tagfüggvény neve az osztályra nézve lokálisnak számít.m1()-et // hiba: nem próbáltuk X(99).5. 99. hogy nem tag függvény legyen. A tagfüggvényeket csak az adott osztály objektumaira alkalmazhatjuk. a függvényt inkább tagfüggvényként adjuk meg. Bizonyos mûveleteknek tagoknak kell lenniük: például a konstruktoroknak.2. X(int). friend int f2(const X&). §11. hogy a 99-bõl X típusú objektumot csináljon. hogy minél kevesebb függvény érje el közvetlenül az adatábrázolást és hogy az adatlekérdezést a segédfüggvények megfelelõ körével támogassuk. Ezért az elsõdleges kérdés nem az.11. Vegyünk egy X osztályt. }.2.3.6). statikus tag vagy barát?”. destruktoroknak és a virtuális függvényeknek (§12. } // hiba: nem próbáltuk X(99). mint elsõ ránézésre gondolnánk. hogy „ez a függvény tag legyen. hogy „tényleg szüksége van-e az ábrázolás elérésére?” Általában kevesebb függvénynek van erre ténylegesen szüksége.m1().5).. friend int f1(X&). int m1().

amelynek balértékre van szüksége. Ha azonban az inv() azt a Matrix-ot invertálja. egy nem tag függvénynek meghatározott paraméterre van szüksége. a külsõ függvények neve hosszabb szokott lenni. Nem tudhatjuk. akkor nincs kényszerítõ ok arra sem. ezért aztán a kétoperandusú operátorok a friend függvények leggyakoribb forrásai. Megfordítva: ha egy mûvelet összes operandusa automatikusan konvertálható. Az ilyen mûveleteknek gyakran az operandus-osztály ábrázolásának elérésére van szükségük. hogy az objektum állapota megváltozhat. akkor a megvalósító függvény csak olyan nem tag függvény lehet. mint a külsõ függvénybeli megfelelõik. f3(99). mint a másik lehetséges m.). hogy válasszunk a tagfüggvény és a referencia paramétert váró barát függvény közül. A tagfüggvényhívási forma világossá teszi a felhasználó számára. Ilyenkor a programozó aszerint dönthet. || stb. amelyikre alkalmaztuk és nem egy új Matrix-ként adja vissza az inverzt. Továbbá sokkal rövidebbek a kifejezések egy tagfüggvény törzsében. hogy valaki nem ad-e majd meg valamikor egy konverziós operátort. míg a tagfüggvény automatikusan használhatja a this mutatót. Ha más szempontok nem játszanak közre. válasszunk tagfüggvényt. Ezenkívül. // rendben: f3(X(99)).inv() jelölés. // rendben: f2(X(99)). az objektum állapotát megváltoztató mûvelet vagy tag legyen. akkor persze csak tagfüggvény lehet. mivel a tagfüggvények neve az osztályra nézve lokálisnak számít. referencia paraméter használata esetén ez sokkal kevésbé nyilvánvaló. vagy pedig nem const referencia (vagy nem const mutató) paraméterû globális függvény. amely paraméterként const referencia vagy nem referencia típust vár. Olyan mûveletet. ha alapvetõ adattípusra alkalmazzuk azokat (+. Ha nincs típuskonverzió. ha alapvetõ adattípusra alkalmazzuk (=. // hiba: nem próbáltuk f1(X(99))-et f2(99).370 Absztrakciós módszerek void h() { f1(99). } Ezért egy. . a legtermészetesebb módon felhasználói típus tagfüggvényeként definiálhatunk. és azt sem láthatjuk elõre. -. *=. melyeknek nincs szükségük balértékre. ++ stb.). ha egy m Matrix inverzérõl van szó. A legtöbb embernek például jobban tetszik az inv(m) jelölés. Ez gyakori eset olyan mûveleteket megvalósító függvényeknél. hogy melyik formát részesíti elõnyben. hogy egy jövõbeli módosítás nem változtatja-e meg az objektum állapotát.

Nagy objektumok A complex osztály mûveleteinek paramétereit complex típusúként határoztuk meg. friend Matrix operator*(const Matrix&.11. Két double másolása „költséges” mûvelet lehet ugyan. return sum. j<4.m[i][j] = arg1. friend Matrix& operator*(const Matrix&.m[i][j]. Operátorok túlterhelése 371 11. megadhatunk referencia típusú paramétereket kezelõ függvényeket: class Matrix { double m[4][4].3). de egy memória-lefoglalási problémát okoz. Az összeadást így definiálhatnánk: Matrix operator+(const Matrix& arg1. az eredmény maga nem lehet automatikus változó (§7. const Matrix& arg2) { Matrix sum. i<4. friend Matrix operator+(const Matrix&. Referenciát visszaadni hatékonyabbnak tûnhet: class Matrix { // . public: Matrix(). mint egy pár mutatóé. i++) for (int j=0. mert a mutatóra alkalmazott operátorok jelentését nem változtathatjuk meg. Ez szabályos kód. const Matrix&). az . j++) sum. Nem minden osztálynak van azonban kényelmesen kicsi ábrázolása. de valószínûleg „olcsóbb”.6. hogy a paraméterek minden mûveletnél lemásolódnak. Ez azt jelenti. const Matrix&). A referenciák alkalmazása nagy objektumokra is lehetõvé teszi a szokásos aritmetikai mûveletek használatát. const Matrix&). de objektum-értéket ad vissza. Minthogy a függvénybõl az eredményre vonatkozó referenciát adjuk vissza.. const Matrix&). Mutatókat nem használhatunk. Mivel egy mûveletet többször is alkalmazhatunk egy kifejezésen belül.. }. }. nagymérvû másolások nélkül is.m[i][j] + arg2. friend Matrix& operator+(const Matrix&. A nagymérvû másolásokat elkerülendõ. for (int i=0. } Ez az operator+() az operandusokat referenciákon keresztül éri el.

} Így egy Matrix másolása csak egy kifejezés értékén alapuló értékadáskor történik meg. A legegyszerûbb ezek közül egy statikus objektumokból álló átmeneti tár használata: const max_matrix_temp = 7. const Matrix& arg2) { Matrix& res = get_matrix_temp(). A visszatérési érték másolása (végrehajtási idõben. kódés adatméretben mérve) gyakran olcsóbb. } Matrix& operator+(const Matrix& arg1. Egy másik módszer háromváltozós mûveletek meghatározására és azok olyankor automatikusan történõ meghívására támaszkodik. Így aztán a mátrixleírók úgy képviselhetik az objektumokat. mint az objektum szabad tárba helyezése és onnan eltávolítása.. static Matrix buf[max_matrix_temp]. és programozni is sokkal egyszerûbb.7). §25. amelyhez max_matrix_temp-nél több ideiglenes érték kell! Hibákra kevesebb lehetõséget adó módszer.6. mint a=b+c vagy a+b*i (§21. ha a mátrix típust csak a tényleges adatot tároló típus leírójaként (handle. if (nbuf == max_matrix_temp) nbuf = 0.12 és §11.372 Absztrakciós módszerek eredmény nem lehet lokális statikus változó sem. hogy közben a lehetõ legkevesebb helyfoglalás és másolás történik (§11. Ez az eljárás azonban a visszatérési értékként objektumot és nem referenciát vagy mutatót használó operátorokon alapul.3 és §22. .14[18]).4. ha olyan kifejezést találnánk írni. Az eredmény másolásának elkerülésére vannak módszerek. Matrix& get_matrix_temp() { static int nbuf = 0.. De az ég legyen irgalmas. Ezért aztán az eredménynek jellemzõen a szabad tárban foglalnánk helyet. return buf[nbuf++]. // .4.7) határozzuk meg. return res. amikor olyan kifejezések kiértékelése történik.

amikor egy olyan ideiglenes . Ez különösen fontos akkor.4. X& operator=(const X&). } // string érték szerint átadva (másoló konstruktor használatával) // string visszaadása (másoló konstruktor használatával) // string kezdõértéket kap (másoló konstruktor használatával) Világos. s = g(s). Amikor g() visszaadja a visszatérési értéket..11. ha X egy típus. megsemmisítését és másolását végzõ összes függvényre: class X { // . hogy a kezdeti és az egyszerû értékadás különbözõ mûveletek (§10. X(const X&). még ha kevésbé nyilvánvaló is. akkor az X(const X&) másoló konstruktor kezeli azt az esetet. Alapvetõ operátorok Általánosságban. amikor egy X típusú objektumnak egy ugyanilyen objektumot adunk kezdõértékül. Ennek szerepe azonos az egyéb kezdeti értékadásokéval. Nem lehet eléggé hangsúlyozni.4. illetve kivételként. Ugyanez igaz a visszatérési értékre és a kivételre is. a string(const string&) újabb hívása következik. a formális paraméter kap kezdõértéket. amikor a destruktorral is számolnunk kell.7. Ha az X osztálynak van valamilyen nem magától értetõdõ feladatot – például a szabad tárban lefoglalt memória felszabadítását – végzõ destruktora.1). ~X(). X(Sometype). Ilyen esetben a másoló konstruktor végzi a munkát: string g(string arg) { return arg. }. hogy az s változó értékének "Newton"-nak kell lennie a g() meghívása után. Ha paraméterként kerül átadásra. a string osztály másoló konstruktorának hívása ezt megteszi. // konstruktor: objektumok létrehozása // másoló konstruktor // másoló értékadás: takarítás és másolás // destruktor: takarítás Ezenkívül még háromféle helyzetben másolódik egy objektum: átadott függvényparaméterként. függvény visszatérési értékeként. Operátorok túlterhelése 373 11. Nem nehéz feladat az s értékének egy másolatát az arg formális paraméterbe másolni. akkor az osztálynak valószínûleg szüksége lesz az objektum létrehozását.. } int main () { string s = "Newton". egy addig kezdõérték nélküli változó.

2.10). Ez egyben azt is jelenti. Ha a programozó nem ad meg másoló konstruktort vagy másoló értékadást egy osztály számára. // n bájt lefoglalása String(const char* p). a fordítóprogram hozza létre a hiányzó függvényt vagy függvényeket (§10. Az ideiglenes változók aztán persze a string::~string() destruktor segítségével megsemmisülnek (§10.374 Absztrakciós módszerek változó kap értéket. Így ahol elvileg egy másoló konstruktorra van szükség (§11. hogy a másoló mûveletek nem öröklõdnek (§12. hogy az s-et megadó programozó ezt akarta volna. 11.// a kezdõérték (p) egy C stílusú karakterlánc }.5). explicit String(int n). Az automatikus konverziókat az explicit kulcsszó alkalmazásával akadályozhatjuk meg.4).4. amely aztán az s-nek ad értéket. String s1 = 'a'..3.3).2. void f(String)..1. Hatékonysági okokból az egyik (de csak az egyik) másolást gyakran elhagyhatjuk. String s5("Fawlty"). String s3 = String(10). // z kezdeti értékadása complex(2)-vel Máskor viszont nem kívánatos és hibák forrása lehet: string s = 'a'. int('a') számú elemmel Nagyon valószínûtlen. Bizonyos típusok számára ez ideális: complex z = 2. Explicit konstruktorok Alapértelmezés szerint az egyparaméterû konstruktor egyben automatikus konverziót is jelent. String g() { // hiba: nincs automatikus char->String átalakítás // rendben: String 10 karakternyi hellyel // rendben: String 10 karakternyi hellyel // rendben: s4 = String("Brian") . String s2(10). // s karakterlánc. Vagyis egy explicit-ként megadott konstruktort csak közvetlen módon lehet meghívni. String s4 = "Brian".7. ott az explicit konstruktor nem hívódik meg automatikusan: class String { // .

// rendben: karakterlánc 10 karakternyi hellyel csekélynek tûnhet. hogy az int-nek Year-ré va- . Ha a Date osztály létfontosságú szerepet játszott volna. }. f("Arthur"). biztosítottuk. mint kitalált példákban. Azáltal. // hiba: nincs automatikus int ->String átalakítás A különbség aközött. Date d3(1978. ahol szükséges. Operátorok túlterhelése 375 f(10). Year y).. hogy a konstruktort explicit-ként adtuk meg.. // hiba: nincs automatikus int ->String átalakítás // rendben: f(String("Arthur")) String* p1 = new String("Eric"). Month m. } }. Az operator int()-nek köszönhetõen a Year automatikusan mindenhol int-té alakul. hogy String s2(10). wrapper) osztály az int körül.11.feb.Year(1978)).21). hogy fordítási idõben szigorúbb ellenõrzések történjenek: class Year { int y. f(s1). class Date { public: Date(int d. // hiba: a 21 nem Year típusú // rendben // Year létrehozása int-bõl // átalakítás Year-rõl int-re A Year egy egyszerû „csomagoló” (beburkoló. hogy String s1 = 'a'. // hiba: nincs automatikus char ->String átalakítás és aközött. } return 10.3). public: explicit Year(int i) : y(i) { } operator int() const { return y. String* p2 = new String(10). f(String(10)). akkor bevezethettük volna a Year osztályt. Date d4(21. // . A Date osztályban egy sima int-et használtunk az év ábrázolására (§10.feb. de igazi kódban kevésbé az.

ha nem. is definiálhatunk. Hasonló módszer tartomány (intervallum) típusokra (§25.5-beli példát. void print_all() const. public: Assoc() {} const double& operator[ ](const string&). visszaadjuk az értékét. így aztán vektorokat. Assoc& operator=(const Assoc&). // a másolást megakadályozandó privát // a másolást megakadályozandó privát Az Assoc típusú objektumok Pair-ek vektorát tartalmazzák. a futási idõ és a szükséges tárhely növekedésétõl sem kell tartanunk. vector<Pair> vec. Pair(string n ="". double v =0) :name(n).376 Absztrakciós módszerek ló alakítása csak ott történik meg. double& operator[ ](string&). Minthogy a Year tagfüggvényeit könnyû helyben kifejtve (inline) fordítani.8. ha megtaláltuk. }. Indexelés Osztály típusú objektumoknak az operator [ ] (subscripting) függvény segítségével adhatunk „sorszámot” (indexet). új Pair-t hozunk // létre és az alapértelmezett 0 értéket adjuk vissza { . 11. asszociatív tömböket stb.5 pontban: double& Assoc::operator[ ](string& s) // megkeressük s-t. A megvalósításban ugyanazt az egyszerû és nem túl hatékony keresési módszert használjuk. Assoc(const Assoc&). ahol ezt kérjük és a „véletlen” értékadások a fordításkor kiderülnek. Akkor egy függvényt használtunk. Az operator[ ] második paramétere (az index) bármilyen típusú lehet. most egy asszociatív tömb típust: class Assoc { struct Pair { string name. val(v) { } }.1) is alkalmazható. amelyben egy asszociatív tömb segítségével írtunk egy fájlban a szavak elõfordulását megszámoló kis programot. Példaként írjuk most újra a §5. mint az §5.6. double val.

Az operator[ ]() függvényeknek tagfüggvénynek kell lenniük.end(). } Végül megírhatjuk a fõprogram egyszerû változatát: int main() // szavak elõfordulásának megszámlálása a bemeneten { string buf. // kezdõérték: 0 // az utolsó elem visszaadása (§16. mint egy kétoperandusú mûvelet.back(). 11. ahol a kifejezés a bal oldali. szükség van egy kimeneti függvényre: void Assoc::print_all() const { for (vector<Pair>::const_iterator p = vec. . Függvényhívás A függvényhívás (function call).9. ++p) if (s == p->name) return p->val. A () hívó operátor a többi operátorhoz hasonló módon túlterhelhetõ.4.val.1 pont. a kifejezés-lista pedig a jobb oldali operandus. p!=vec. vec.print_all(). vagyis a kifejezés(kifejezés-lista) jelölés úgy tekinthetõ.end(). ++p) cout << p->name << ": " << p->val << '\n'.0)). Operátorok túlterhelése 377 for (vector<Pair>::iterator p = vec.begin(). } return vec. A függvényhívó operátor túlterhelése elsõsorban olyan típusok létrehozásakor hasznos.11.3) Minthogy az Assoc objektum ábrázolása kívülrõl nem érhetõ el. } Az asszociatív tömb ötletét továbbfejleszti a §17. amelyeknek csak egy mûveletük van vagy általában csak egy mûveletük használatos. vec. Az operator()() paraméterlistájának kiértékelése és ellenõrzése a szokásos paraméter-átadási szabályok szerint történik.push_back(Pair(s. Assoc vec.3. while (cin>>buf) vec[buf]++.begin(). p!=vec.

add23). amely a megfelelõ módon mûködik: class Add { complex val. hogy a valamiképpen függvényként viselkedõ objektumokat függvényként hívhassuk meg. Mi lenne. double i) { val = complex(r. Ez a függvényeknek nem természetes tulajdonsága. } Hogyan tudnánk egy olyan függvényt írni.378 Absztrakciós módszerek A ( ) hívó mûvelet legnyilvánvalóbb és talán legfontosabb alkalmazása az. amelyben valamilyen nem magától értetõdõ mûveletet paraméterként adunk át. ami nem „tiszta” megoldás.begin().ll.aa.begin(). } // a vektor összes elemének negálása // a lista összes elemének negálása Ez a vektor és a lista minden elemét negálja. } void f(vector<complex>& aa. hogy olyan kódot írjunk. list<complex>& ll) { for_each(aa.negate).i).end(). } // a paraméter növelése az értékkel . } void g(vector<complex>& aa.begin(). melyet többször meghívva egy-egy tetszõleges értéket adhatunk az elemekhez? Olyasmire van szükségünk.end().negate).4). Vegyük az alábbi példát: void negate(complex& c) { c = -c.begin(). Az ilyen függvényobjektumok fontosak. Viszont írhatunk egy osztályt. A standard könyvtárban például sok olyan algoritmus található.3).3)-at akarnánk hozzáadni? Ezt könnyen megtehetjük: void add23(complex& c) { c += complex(2.ll.end().add23). void operator()(complex& c) const { c += val. mert lehetõvé teszik. ha a lista minden eleméhez complex(2. for_each(ll.end(). list<complex>& ll) { for_each(aa. for_each(ll. Jellemzõ megoldásként valahova a függvényt körülvevõ környezetbe helyezve adjuk át az értéket. Egy függvényként viselkedõ objektumot függvényszerû vagy egyszerûen függvényobjektumnak hívunk (§18. aminek megadhatjuk a kívánt értéket és utána ezt az értéket használja fel minden hívásnál. public: Add(complex c) { val = c. melyek egy függvényt hívnak meg egy tároló minden elemére. } }.aa. } // az érték mentése Add(double r.

amelyet aztán a for_each ismételten felhasznál. . dereferencing) operátort egyparaméterû. a lista elemeihez pedig z-t.begin(). Operátorok túlterhelése 379 Egy Add osztályú objektum kezdõértékének egy komplex számot adunk.4.5) való használat. mi is igazából a harmadik paraméter: template<class Iter. Legyen adott egy osztály: class Ptr { // . } Elsõ pillantásra ez a módszer furcsának tûnhet.5. }. hatékony. Nem egyszerûen egy egyszer vagy többször meghívott függvényrõl van szó. complex z) { for_each(aa.4). Fct f) { while (b != e) f(*b++).. Az operator()() további népszerû alkalmazásai a részláncok képzésére vagy több dimenziós tömbök indexelésére (§22. 11.3)-at fog adni. Vegyük észre.begin().. és nagyon hasznos (lásd §3.aa. list<complex>& ll. return f. Indirekció A -> indirekció („hivatkozástalanítás”. Iter e. mert a for_each egy sablon (template). utótagként használt operátorként definiálhatjuk. amely a ( ) mûveletet alkalmazza a harmadik paraméterére. } Ez a tömb minden eleméhez complex(2. §18.11.Add(2. de egyszerû. A többször meghívott függvény az Add(z) operator()() függvénye. hogy törõdne vele. X* operator->().ll.3)).Add(z)). Az operator()()-nak tagfüggvénynek kell lennie. Mindez azért mûködik.end(). hogy Add(z) egy olyan objektumot hoz létre. majd a ( ) mûveletet végrehajtatva ezt a számot hozzáadjuk a paraméterhez: void h(vector<complex>& aa. anélkül. for_each(ll.end().8. class Fct> Fct for_each(Iter b.10.

hogy milyen m tagra mutat. a Rec_ptr::operator->() függvény a memóriába tölti az objektumot. X* q2 = p. Az operator->() ebben az értelemben egyoperandusú utótag-operátor. // . azaz olyan objektumoké.identifier). public: Rec_ptr(const char* p) : identifier(p).operator->() mutatóvá való átalakítása nem függ attól. in_core_address(0) { } ~Rec_ptr() { write_to_disk(in_core_address. return in_core_address. a mutatókhoz nagyon hasonló módon: void f(Ptr p) { p->m = 7.380 Absztrakciós módszerek Ekkor a Ptr osztályú objektumokat az X osztály tagjainak elérésére használhatjuk. valahányszor egy objektumot érnek el rajtuk keresztül. } . így a tagnevet ki kell írni utána: void g(Ptr p) { X* q1 = p->. } // szintaktikus hiba // rendben A ->() operátor túlterhelésének fõ alkalmazása az okos vagy intelligens mutató (smart pointer) típusok létrehozása. }. a Rec_ptr destruktora pedig szükség esetén a megváltozott objektumot a lemezre írja: class Rec_ptr { const char* identifier.operator->())->m = 7 A p objektumnak a p.operator->().. } // (p. A Rec_ptr konstruktora egy nevet vár. amellyel a lemezen tárolt Rec osztályú objektumok érhetõek el.. de ráadásul valamilyen tennivalót végeznek. formai követelményei viszont nem újak. Például létrehozhatunk egy Rec_ptr osztályt. amikor azt a Rec_ptr-en keresztül el akarjuk érni. amelyek mutatóként viselkednek. } Rec* operator->(). melynek segítségével a keresett objektum a lemezen megkereshetõ. Rec* Rec_ptr::operator->() { if (in_core_address == 0) in_core_address = read_from_disk(identifier). Rec* in_core_address.

} Y& operator[ ](int i) { return p[i]. // a Rec típus..m Ahogy már megszokhattuk. akkor teljesül a következõ: p->m == (*p).. amelyben a ++. a felhasználói operátorokra nézve ez nem biztosított. a Rec típus pedig paraméter. Ha egy osztályban több ilyen operátort határozunk meg. ugyanúgy. +=. }. ha x=x+1-gyel azonos hatással jár. Operátorok túlterhelése 381 A Rec_ptr-t így használhatjuk: struct Rec { string name. + mûveletek értelmezettek.. amire Rec_ptr mutat // Rec_ptr elõállítása s-bõl // s módosítása. p->name = "Roscoe". Ha adott egy típus: Y* p. // . ha szükséges. elõször beolvassa a lemezrõl } Természetesen az igazi Rec_ptr egy sablon lenne. } }..m == p[0]. } Y& operator*() { return *p. Közönséges mutatók esetében a -> használata egyenértékû az egyváltozós * és [ ] használatával. ha x egy olyan osztályú változó. hogy a fenti egyenértékûség teljesüljön. Szükség esetén persze gondoskodhatunk errõl: class Ptr_to_Y { Y* p. mint ahogy ++x és x+=1 is jó. void update(const char* s) { Rec_ptr p(s). Egy valóságos program hibakezelést is tartalmazna és kevésbé naív módon kezelné a lemezt.11. public: Y* operator->() { return p. akkor tanácsos lehet ezt úgy tenni. . // .

azon oknál fogva.. Ez különösen nyilvánvaló és szükséges olyankor.2. amelyre csak akkor tudjuk alkalmazni az indirekció operátort. Vegyük például az alábbi – egyébként problematikus – hagyományos programot: void f1(T a) { T v[200]. ha mutatót vagy olyan típust ad vissza. még a növelés vagy csökkentés hatására is.csökkentõ (decrement) mûvelet is hozzátartozik. A -> operátor másik haszna.4). hogy az indirekció (dereferencing) kulcsfogalom.. }. Az operator-> tagfüggvény kell. amelyre a -> alkalmazható. *p = a. hogy legyen. a -> operátor túlterhelése pedig tiszta. ezért ésszerû e megszorítás ellenõrzését a tényleges használatig elhalasztani. hogy p-t csak úgy lehessen növelni vagy csökkenteni. A bejárók (iterátorok) (19. amikor a cél egy közönséges mutatónak egy okosra való kicserélése. Ha egy sablon osztály számára adjuk meg. 11. sokszor dönt úgy. Azt is el szeretnénk érni. sokszor elõfordul. fejezet) jellemzõ és lényegi példát adnak erre. Növelés és csökkentés Amint a programozó kitalál egy „intelligens mutatót”. p--. Valami ilyesmit szeretnénk tehát: class Ptr_to_T { // . amely azonos jelentés mellett csak némi futási idejû hiba-ellenõrzéssel van kiegészítve. .382 Absztrakciós módszerek A -> operátor túlterhelhetõsége nem csak kis különlegesség. a beépített típusokra értelmezett növelés és csökkentés mintájára. de hasznos módon lehetõvé teszi a C++ nyelvben a delegációt (§24. Csak úgy használható. T* p = &v[0]. *p = a. hogy nem is kerül tényleges felhasználásra. hanem érdekes programok egy osztálya számára fontos is.11. ha tömbön belüli objektumra mutat. hogy ehhez a ++ növelõ (increment) és -. ++p. } // hagyományos használat // hoppá: 'p' tartományon kívüli és nem kaptuk el // rendben A p mutatót ki szeretnénk cserélni valamilyen Ptr_to_T osztályú objektumra. ha tényleg egy objektumra mutat. közvetlen és hatékony módja annak egy programban való megjelenítésére. hogy korlátozott.

Ptr_to_T operator++(int).az elõtagként.és utótagként való használat között tesz különbséget. }. amelyek elõtagként (prefix) és utótagként (postfix) egyaránt használhatók.200). a kezdõérték p Ptr_to_T& operator++(). // elõtag // utótag // elõtag // utótag // elõtag Az int paraméterrel jelezzük a ++ utótagként való alkalmazását. ha arra gondolunk.200). amely az elõ. . // csatolás s méretû v tömbhöz. // rendben } A növelõ és csökkentõ operátorok az egyetlenek a C++ nyelv operátorai között. Operátorok túlterhelése 383 void f2(T a) // ellenõrzött { T v[200].operator--(0). a kezdõérték p Ptr_to_T(T* p). hogy a többi (aritmetikai és logikai) egy paraméterû operátorhoz hasonlóan az ál-paraméter nélküli ++ és -. melyik melyik.v. Magát az int-et nem használjuk. A Prt_to_T osztályt használva a példa egyenértékû az alábbival: void f3(T a) // ellenõrzött { T v[200]. a paraméteres változat a „furcsa” utótagként való mûködéshez kell. Ptr_to_T& operator--(). p--. csak ál-paraméter. public: Ptr_to_T(T* p.11. T& operator*(). // csatolás önálló objektumhoz. Ptr_to_T p(&v[0]. *p = a. int size. Könnyen megjegyezhetjük. p. T* array. Ezért a Ptr_to_T típus számára mindkét fajta növelõ és csökkentõ operátort definiálnunk kell: class Ptr_to_T { T* p.v. T* v. int s). Ptr_to_T p(&v[0]. // futási idejû hiba: 'p' tartományon kívüli ++p. Ptr_to_T operator--(int). *p = a.

az egyenlõségvizsgáló és összefûzõ mûveleteket.3 egy mutatósablont mutat be. ha azonos az értékük..12. A karakterláncokat C stílusú. // . p.6.. eldobhatjuk a gyakorlatainkat és használhatjuk a standard könyvtárbeli string-et (20. Az én majdnem valóságos String osztályom három segédosztályt használ: az Srep-et.és bemenetet. Egy karakterlánc osztály Íme a String osztály egy valóságosabb változata.operator++().operator*() = a.12[12]). Egy többet tudó és/vagy több szolgáltatást nyújtó string osztály írása jó gyakorlat (§11. amely kivételeket is használ a futási idõben fellépõ hibák jelzésére. Srep *rep. amely öröklõdés használata mellett is jól mûködik. }. class Range { }. egy másik gyakorlat (§14. hogy több azonos értékû String is használhassa ugyanazt az eltárolt adatot. // adatábrázolás // referencia char-ra // kivételkezeléshez . A §13. amely a céljainknak még éppen megfelel. hogy egy írás és olvasás között különbséget tevõ index-operátort támogasson: class String { struct Srep. az adatfolyam ki. Átdolgozása olyan sablonná. a karakterliterálokat. érték-szemantika). a Range-et az értéktartomány-megsértési hibákat jelzõ kivételek kiváltásához.14[7-12]). az ellenõrzött és ellenõrizetlen elérést.operator*() = a. p.14[19]). 11. a másolások számának csökkentésére pedig hivatkozásszámlálót használ. Ha megvagyunk vele. Ez a karakterlánc-osztály támogatja az érték szerinti mûködést (value semantics. fejezet).384 Absztrakciós módszerek } p. a karakteríró és -olvasó mûveleteket. nullával lezárt karaktertömbként tárolja. és a Cref-et. // futási idejû hiba: 'p' tartományon kívüli // rendben A Prt_to_T osztály kiegészítése gyakorlatnak marad (§11. public: class Cref.

} private: Srep(const Srep&).. } Srep* get_own_copy() { if (n==1) return this.11. s = new char[sz+1]. sz = nsz. const char* p) { if (sz != nsz) { delete[ ] s. . // mutató az elemekre // karakterek száma // hivatkozásszámláló Srep(int nsz.s). }. sz = nsz. amit gyakran hívnak beágyazott osztálynak. // hely a lezáró nulla számára is // másolás. ha szükséges // a másolás megakadályozása A String osztálynak megvannak a szokásos konstruktorai. majd késõbb kifejthetjük: struct String::Srep { char* s. strcpy(s. int sz. const char* p) { n = 1. } void assign(int nsz. s = new char[sz+1].. destruktora és értékadó mûveletei is: class String { // .p). return new Srep(sz. } ~Srep() { delete[ ] s. Srep& operator=(const Srep&). Operátorok túlterhelése 385 A többi taghoz hasonlóan a tagosztályokat (member class.p). nested class-nak is) deklarálhatjuk az osztályban. } strcpy(s. int n. n--.

~String(). } String::~String() { if (--rep->n == 0) delete rep. amely az adatábrázolásra mutat. } String& String::operator=(const String& x) { x. Ha egy osztálynak megvannak a hagyományos aritmetikai mûveletei. // x = "" // x = "abc" // x = másik_karakterlánc A String osztály érték szerint mûködik. } // másoló értékadás // védelem az "st = st" ellen // az ábrázolás megosztása // másoló konstruktor // az ábrázolás megosztása A const char* paraméterû ál-másoló mûveletek bevezetésével a karakterliterálokat is megengedjük: . String(const char*).rep. } String::String(const String& x) { x.. A másik megoldás az lenne. vagyis ha késõbb az egyiket módosítjuk. String& operator=(const char *). mint a komplex számokkal. return *this. hogy ennek támogatása ne kerüljön túl sokba. String& operator=(const String&).386 Absztrakciós módszerek String().rep.""). }. rep = x. akkor annak nem lesz hatása a másikra.rep->n++. ha a String osztály mutatókkal dolgozna. // . rep = x. a String-et leíróként ábrázolom. Ahhoz viszont. azaz egy s1=s2 értékadás után s1 és s2 két teljesen különbözõ karakterlánc lesz. amit csak szükség esetén kell másolni: String::String() // az alapértelmezett érték egy üres karakterlánc { rep = new Srep(0. én elõnyben részesítem az érték szerinti mûködést. if (--rep->n == 0) delete rep. Ekkor az s1=s2 értékadás után s2 megváltoztatása s1-et is érintené. karakterláncokkal végzettek..rep->n++. mátrixokkal. vektorokkal. String(const String&).

illetve kevésbé hatékony ellenõrzött eljárásokat (a hagyományos jelöléssel): class String { // . return rep->s[i]. Az ötlet az. Operátorok túlterhelése 387 String::String(const char* s) { rep = new Srep(strlen(s). } char read(int i) const { return rep->s[i]..s). } // Srep újrahasznosítása // új Srep használata Az egyes karakterláncokat elérõ operátorok megtervezése nehéz. rep = new Srep(strlen(s). rep->s[i]=c. } char operator[ ](int i) const { check(i). Én úgy készítettem el az osztályt.s). hogy hatékony ellenõrizetlen mûveleteket adtam meg (egy kicsit kényelmetlenebb jelöléssel). void check(int i) const { if (i<0 || rep->sz<=i) throw Range().11. ellenõrizetlen elérést használni: . char c) { rep=rep->get_own_copy(). } void write(int i. Sajnos. de a felhasználónak legyen módja egyszerre végignézni a teljes tartományt és a gyorsabb. ez a három követelmény nem teljesíthetõ egyszerre. } int size() const { return rep->sz. hogy a hagyományos [ ] jelöléssel az ellenõrzött elérés legyen biztosított a közönséges felhasználás számára. } }..i). } return *this. else { rep->n--. mert az ideális megoldás az lenne. ha ezek a szokásos jelölést (azaz a [ ]-t) használnák. a lehetõ leghatékonyabbak lennének és a paraméter értékét is ellenõriznék. return Cref(*this. // . } String& String::operator=(const char* s) { if (rep->n == 1) rep->assign(strlen(s). } Cref operator[ ](int i) { check(i)...s).

ha nem fogadható el az a megoldás. i(ii) { } public: operator char() const { return s. vagy pedig az elérõ függvényeket kell inline-ként meghatározni a String-en kívül és az String::Srep után (§11. for (int i = 1.write(i. például a [ ]-t úgy meghatározni. Itt például ez nem lehetséges. hogy írásakor meghívja a String::Sref::get_own_copy()-t: class String::Cref { friend class String.operator=('c') .read(i)>>1. A tényleges másolást a String::get_own_copy() végzi. // hivatkozás s[i]-re // érték kijelölése // érték módosítása Például: void f(String s. return h. } void operator=(char c) { s. String& s. } // ellenõrzés nélküli hozzáférés s-hez Nehéz dolog egy operátort. a String::operator[ ]() egy Cref-et ad vissza. mert a String-et úgy határoztam meg. i++) h ^= s.read(0).c). const int max = s. míg az egyik String-et ténylegesen nem írják: az érték másolása csak ekkor történik meg. amit aztán a felhasználó kedve szerint felhasználhat. olyan helyre kell elhelyezni definiciójukat. mint a char&. ahonnan az Srep osztályé elérhetõ.388 Absztrakciós módszerek int hash(const String& s) { int h = s.14[2]). int ii) : s(ss). Ezt a módszert általában íráskori másolásnak vagy „másolás íráskor”-nak (copy-on-write) hívják.operator[ ](1).operator[ ](1). hogy egyszerûen egy referenciát adunk vissza. hogy az egyes értékadással. } }. paraméter-átadással stb. // c1 = s. A Cref úgy viselkedik. int i. // s. Cref(String& ss. Tehát vagy az Srep-et kell a String osztályon belül megadni. hogy az elérõ függvényeket helyben kifejtve (inline) lehessen fordíttatni.size(). Abból a célból. hogy az az író és az olvasó jellegû hozzáférést is támogassa. Megkülönböztetendõ az írást és az olvasást. const String& r) { char c1 = s[1]. azzal a különbséggel. i<max. megadott értékû String-ek ugyanazt a belsõ ábrázolást használják.operator char() s[1] = 'c'. ha nem const objektumra hívták meg.read(i).

. const String&). String operator+(const String&. illetve az összefûzést meghagytam gyakorlatnak. Ahhoz. y. y. s) != 0. r. const char* s) { return strcmp(x. } }. friend ostream& operator<<(ostream&. cout << "Az f-ben: " << a << ' ' << b << ' ' << c << '\n'. String b) { a[2] = 'x'.rep->s) != 0.rep->s. char c = b[3].. } .rep->s.és bemeneti operátorokat. hogy teljessé tegyük a String osztályt. const char*).rep->s. r[1] = 'd'. const String&).rep->s. return b. Operátorok túlterhelése 389 } char c2 = r[1]. A fõprogram pusztán egy kissé megdolgoztatja a String operátorokat: String f(String a. const String& y) { return strcmp(x. String& operator+=(const char*). Hely-megtakarítás céljából a ki. friend bool operator==(const String& x. const String& y) { return strcmp(x. meghatározunk még egy sor hasznos függvényt: class String { // . } friend bool operator==(const String& x. } friend bool operator!=(const String& x.operator[ ](1) = 'd' Vegyük észre.1) lesz. String&). s) == 0. } friend bool operator!=(const String& x.operator[ ](1) // hiba: char értékadás. String operator+(const String&. friend istream& operator>>(istream&.rep->s) == 0. String& operator+=(const String&).operator[ ](1) értéke Cref(s. // c2 = r.11. const char* s) { return strcmp(x. hogy egy nem const objektumra az s.

§11.2. [8] Használjunk névteret. inkább tagfüggvény legyen. például nincs az értéket C stílusú adatként visszaadó mûvelet (11.y). cout << "Bemenet: " << x << ' ' << y << '\n'.2.4. String z = x.4. cout << "Adjon meg két karakterláncot!\n".6. 11. §11. x[0] = '!'. [9] Szimmetrikus mûveletekre használjunk nem tag függvényeket. [6] Ha egy függvénynek szüksége van az adatábrázolás elérésére. [2] Nagy operandusok esetében használjunk const referencia paramétereket. [3] Nagy értékeket adó eredményeknél fontoljuk meg az eredmény-visszaadás optimalizálását.3. fejezet).1.14[10].2. [10] Többdimenziós tömb indexelésére használjunk ()-t.9. if (x == z) cout << "Az írás nem sikerült!\n". §11.5. y = f(x. y.1.3.2.6. 11.2. cout << "Kilépés: " << x << ' ' << y << ' ' << z << '\n'.2. } Ebbõl a String osztályból még hiányoznak fontosnak vagy akár alapvetõnek tekinthetõ dolgok.390 Absztrakciós módszerek int main() { String x. if (x != z) cout << "Az x sérült!\n". §11. 11. hogy a segédfüggvényeket osztályukhoz rendeljük. Tanácsok [1] Operátorokat elsõsorban azért adjunk meg. ha azok megfelelõek az osztályunk számára. 20. §11. 11.5.13. [5] Bíráljuk felül vagy tiltsuk meg az alapértelmezett másolást. . 11. cin >> x >> y. 11. hogy a szokásos használati módot támogassuk. §11. ha az egy adott típus számára nem megfelelõ.7. [11] Az egyetlen „méret” paraméterû konstruktorok legyenek explicit-ek. [7] Ha egy függvénynek nincs szüksége az adatábrázolás elérésére. inkább ne legyen tagfüggvény. [4] Hagyatkozzunk az alapértelmezett másoló mûveletekre.

§11. Y). . int i = 2. X x = 1.14. 2. y + y. (*2) Fejezzük be és teszteljük a §11. y + 10 * y. int main() { i + 10. struct Y { int i.3.5. y + 10. Y operator+(X). ne a gyakorlatok megoldása révén kapott saját változatot. }. hogy futás közben kiírjon minden megengedett kifejezést. x + y + i. Operátorok túlterhelése 391 [12] Általános célra használjuk a szabványos string-et (20. extern int f(X). 106 + y. X X(int). [14] Bal oldali operandusként balértéket váró mûveleteket tagfüggvényekkel valósítsunk meg. operator int(). Y(X).4. operator+(int). Y y = x. §11.11. [13] Legyünk óvatosak az automatikus konverziók bevezetésekor. (*2) Milyen konverziókat használunk az alábbi program egyes kifejezéseiben: struct X { int i. fejezet). }. 11. Gyakorlatok 1. x * x + i. f(7). f(y). } Módosítsuk a programot úgy.12. §11.12-beli String osztályt. extern X operator*(X.

4. hogy az mûködjön a standard könyvtárbeli string-gel. . amelyiknél eltérõre. Teszteljük az 1000 faktoriálisának kiszámíttatásával. (*2) Definiáljuk az INT osztályt. (Segítség: definiáljuk az INT::operator int()-et). hogy csak a következõ mûveletek engedélyezettek: (egy és két paraméterû) +. (Vagyis adott egy kurrens elemünk a String-ben. // igény szerint további mûveletek Hasonlítsuk ezt össze hasznosság. Milyen más mûveletet szeretnénk egy karakterláncon végezni? 9. és a vele kapcsolatos mûveletek. mint a RINT.) 8. // hivatkozás a következõ elemre }.5) Módosítsuk úgy a §11.) 7. (egy és két paraméterû) -. (*1. aztán egy olyat. hogy a részlánc-operátort egy értékadás bal oldalán is fel lehessen használni. 10. amely pontosan úgy viselkedik. Elõször egy olyan változatot írjunk. 6. (*2) Definiáljuk a String osztály számára az értéket C stílusú adatként visszaadó mûveletet. de legalább 64 bites pontosságú. hogy ez egy konverziós operátor. amely pontosan úgy viselkedik. amely pontosan úgy viselkedik. / és %. (*1) Definiáljuk a RINT osztályt. (*2) Határozzunk meg a String osztály számára egy külsõ bejárót (iterátort): class String_iter { // hivatkozás karakterláncra és annak elemére public: String_iter(String& s).5) A () operátor túlterhelésével határozzunk meg egy részlánc-mûveletet egy karakterlánc-osztály számára. Vitassuk meg annak elõnyeit és hátrányait. (Segítség: ne definiáljuk a RINT::operator int()-et). (*1. 12.14[11]-beli eszközt. (Segítség: a String osztályéhoz hasonló társzervezésre lesz szükség. 11. (*4) Definiáljunk egy tetszõleges pontosságú aritmetikát támogató osztályt. amelyiknél egy részláncot egy azonos hosszúságú teljes karakterláncra lehet cserélni. *. Vitassuk meg a C stílusú adat számára szükséges memória lefoglalásának különféle módjait. (*3) Definiáljuk a LINT osztályt. // bejáró s számára char& next(). A string definícióját nem változtathatjuk meg.5) Tervezzünk és készítsünk egy egyszerû reguláris kifejezés illesztõ eszközt a String osztály számára. (*3) Tervezzük meg úgy a String osztályt. mint egy int. azzal a különbséggel. mint egy int. stílus és hatékonyság szempontjából a String egy belsõ bejárójával. 5.392 Absztrakciós módszerek 3. (*2.

+=. 21.11 pont Ptr_to_T osztályát és ellenõrizzük. -. (*2) Definiáljuk az Imaginary osztályt képzetes számok ábrázolására. és próbáljuk meg futtatása nélkül kideríteni. 20. ->. amely a Vec4-hez hasonlít. (*2) Definiáljunk egy Vector típust. amely a Mat4-hez hasonlít. Definiáljuk a [ ] mûveletet. Definiáljuk a [ ] mûveletet. A gyakorlat végére meg fogjuk tanulni. (*3) Definiáljuk a Mat4 típust. Bíráljuk felül a „népszerû” függvényeket referencia típusú paramétereket használó függvényekként. 22. 17. amit operátor-túlterheléssel és a makrók használatával olvashatatlanná teszünk. y.Index) hatványozó függvény kitevõje számára. Operátorok túlterhelése 393 13. Néhány félrevezetõ megjegyzéssel is nagy zavart lehet kelteni. }. /. mint ha x és p C tagja lenne.-t jelentsen és fordítva. ha a Mat4-re alkalmazzuk. melynek segítségével majdnem úgy használhatjuk valamely S és T x-ét és p-jét. Ne okozzunk futási idejû hibát.I)-t. (*2) Írjunk egy programot. (*3) Cseréljük ki egy barátunkkal a §11. hogy mit ne tegyünk többé. 16. mint négy float-ból álló vektort. }. Írjunk egy C osztályt. (*2) Definiáljuk a Vec4 típust. hogy . de Matrix::Matrix(int. (*1. =. Legyenek meg legalább a *. struct T { char* p. Definiáljuk a Complex osztályt ennek alapján. +. mint ami Vec4-et ad vissza. 19. mint négy Vec4-bõl álló vektort.int) konstruktorának paraméterként megadhatjuk a dimenziókat. . (*3) Definiáljunk egy Matrix típust. *= és /= mûveleteket a vektorok és float-ok együttes használatára. Adjuk meg a +.11. Találjunk módot arra. Készítsünk függvényt. 18. (*1) Adott két struktúra: struct S { int x.14[13] feladatra adott megoldásunkat. Készítsük el az alapvetõ aritmetikai mûveleteket. *. mit csinál. char* q. hogy INT-et jelentsen. 15. 14. Ezután egy makróval határozzuk meg úgy az intet. Adjuk meg a szokásos mátrix-mûveleteket.mûveletek. -=. de Vector::Vector(int) konstruktorának paraméterként megadhatjuk a méretet.5) Definiáljuk az Index osztályt a mypow(double. =. ++ és -. amely a Gauss-féle kiküszöbölés módszerét alkalmazza egy Mat4-re. (*2) Fejezzük be a §11. csak ha egy hibás mutatót ténylegesen felhasználnak. hogy 2**I meghívja mypow(2. Egy ötlet: határozzuk meg + -t úgy az INT-ekre.

” (W. hogy a programban használt fogalmak modellezésére osztályokat használjon. a C++ valódi erõsségének használatáról mond le. ha nem szükséges. Bevezetés A C++ a Simula nyelvtõl kölcsönözte az osztály.12 Származtatott osztályok „Ne szaporítsuk az objektumokat. A C++ nyújtotta nyelvi szerkezetek közvetlenül támogatják ezeket a tervezési elveket. . Occam) Fogalmak és osztályok • Származtatott osztályok • Tagfüggvények • Létrehozás és megsemmisítés • Osztályhierarchiák • Típusmezõk • Virtuális függvények • Absztrakt osztályok • Hagyományos osztályhierarchiák • Absztrakt osztályok mint felületek • Az objektumok létrehozásának adott helyre korlátozása • Absztrakt osztályok és osztályhierarchiák • Tanácsok • Gyakorlatok 12. ha a nyelvi lehetõségeket a tervezési elvek támogatására használjuk. Aki a nyelvi elemeket csak a hagyományosabb programozás jelölésbeli alátámasztására használja.1. illetve az osztályhierarchia fogalmát. Megfordítva is igaz: akkor használjuk a C++ nyelvet hatékonyan. valamint a rendszertervezés azon elvét. mint felhasználói típus.

hogy a Shape (Alakzat) osztály a közös bennük. akkor valami lényegeset mulasztunk el. Sokak számára ez a megszokottól a kevésbé ismert felé való haladást is fogja jelenti. más problémák kapcsán vagy más összefüggésben lehetséges. hogy a programozót az új felé tereljem. azaz hogy kifejezzék. fejezet tárgyalja. A nyelvi lehetõségek és módszerek bemutatása az egyszerûtõl és konkréttól a bonyolultabb. majd egy hosszabb példa kapcsán azt tárgyalom. utak. olaj. De ez nem csupán egyszerû utazás a „régi. hogy az olvasó megismerje az összes eljárást. Ez a fejezet azt feszegeti. mentõk. motor. Amikor rámutatok egy megközelítés korlátaira. bírság. hanem más fogalmakkal együtt és erejének egy részét is a rokon fogalmakkal való kapcsolatból meríti. mindig adott problémák kapcsán teszem. gyalogos. . A cél az. vezetõ. gyorshajtás. De ha tudnánk. A kör és a háromszög fogalmában például közös. Használható programokat az itt tárgyalt módszerek mindegyikének felhasználásával írtak már. hogy autó. hiszen osztályainkat a mindennapi életben használt fogalmaknál szûkebben és precízebben akarjuk meghatározni. mi következik ebbõl az egyszerû elvbõl – ami valójában az általában objektumorientáltnak nevezett programozási elv alapja. A származtatott osztály fogalma és a vele kapcsolatos nyelvi eljárások célja a viszonyok kifejezése. hogy a korábbi módszer alkalmazása a jobb választás. teherautó. mi az. felmerül a kérdés: hogyan ábrázoljuk a fogalmak közötti kapcsolatokat? Persze tetszõleges kapcsolatot egy programozási nyelvben nem tudunk közvetlenül kifejezni. hogy nem vonjuk be a síkidom fogalmát.396 Absztrakciós módszerek Egy fogalom sohasem önmagában létezik. akkor sem akarnánk. rossz módszerektõl” az „egyedüli igaz út” felé. például a többszörös öröklõdést vagy a futási idejû típusazonosítást a 15. hogy mindkettõ síkgörbe-alakzat. Az objektumorientált programozást támogató további nyelvi eszközöket. mi a közös az osztályokban. Ha egy programban úgy szerepeltetünk köröket és háromszögeket. A fejezetben elõször az objektumorientált programozást támogató alapvetõ nyelvi eszközöket mutatom be. Minthogy a fogalmakat osztályokként ábrázoljuk. kifinomultabb. így a Circle (Kör) és Triangle (Háromszög) osztályokat úgy írhatjuk le. Hamarosan bevezetjük a következõ fogalmakat: kerék. hogy aztán okos és kiegyensúlyozott módon tudjon majd választani közülük. elvontabb felé halad. amikor igazi feladatokat kell megoldania. Próbáljuk csak megmagyarázni. motel stb. hogy pontosan meghatározott (explicit) módon megmondjuk. hogyan lehet ezek alkalmazásával jól szerkesztett programot írni.

az Employee a Manager bázisosztálya. Egy efféle programban lehet egy ilyen adatszerkezet: struct Employee { // alkalmazott string first_name. vagy az emp tag címét kellene az alkalmazottak listájára tennünk. }. Ez nyilvánvaló lehet a programozó (különösen egy figyelmes programozó) számára. Date hiring_date.. Vagy típuskényszerítést kellene alkalmaznunk a Manager*-ra. short level. char middle_initial.. // . a Manager-ek egyben Employee-k is. hogy a Manager egyben Employee is. }. hogy kifejezetten megmondjuk. Egyik megoldás sem elegáns és zavaró is lehet.) és ezekhez jönnek hozzá a saját tagok (group. short level. A Manager osztálynak megvannak azok a tagjai. . Származtatott osztályok Vegyünk egy programot. }. // . A Manager az Employee-ból származik..2.. Ezután „meghatározhatunk” egy fõnököt is: struct Manager { Employee emp. Származtatott osztályok 397 12. family_name.. ezért az Employee (Alkalmazott) adatokat a Manager (Fõnök. // fõnök // a fõnök mint alkalmazott // beosztottak A fõnök egyben alkalmazott is. amely egy cég dolgozóit kezeli. level stb. department stb. short department. vezetõ) objektum emp tagjában tároljuk. így a kettõ nem cserélhetõ fel. de a fordítóprogram és az egyéb eszközök sehonnan nem fogják tudni.12. amelyek az Employee-nek is (first_name. és fordítva. Egy Manager* nem Employee* is egyben. // . Egy Employee-ket tartalmazó listára nem vehetünk fel egy Manager-t a megfelelõ kód megírása nélkül.).. csak további adatokat is tartalmaznak: struct Manager : public Employee { set<Employee*> group. set<Employee*> group. A helyes megközelítés az.

. A származtatás népszerû és hatékony megvalósítása a származtatott osztályú objektumot a bázisosztály olyan objektumaként ábrázolja.... mivel annál több adatot tárol és több függvényt biztosít. Az utóbbi szóhasználat (superclass – fõosztály. amely kiegészül a származtatott osztályra egyedileg jellemzõ adatokkal is: Emplyee: first_name family_name .. group level . illetve derived class (subclass). hogy a származtatott osztályból egy nyilat rajzolnak a bázisosztály felé. Az angol kifejezések a bázisposztályra és a származtatott osztályra: base class (vagy superclass)..398 Absztrakciós módszerek A származtatást gyakran úgy ábrázolják grafikusan. hogy a származtatott osztály a bázisosztályra hivatkozik (és nem fordítva): Employee Manager Általában úgy mondják. Ennek alapján ezt a kapcsolatot öröklõdésnek (öröklés. Manager: first_name family_name . a származtatott osztály tulajdonságokat örököl a bázisosztálytól. inheritance) is hívják. jelezve. hiszen a származtatott osztály bizonyos értelemben „szélesebb” a bázisosztálynál.. subclass – alosztály) azonban zavaró lehet.

} Ne feledjük. mert pe // a Manager típusú mm-re mutat pm->level = 2. Egy osztály bázisosztályként való használata egyenértékû az osztály egy (névtelen) objektumának deklarálásával. akiknek egy része vezetõ beosztású (Manager): void f(Manager m1. A másik irányban (Base*-ról Derived*-ra) explicit konverzió szükséges: void g(Manager mm. Manager* pm = &ee. ha egy Derived (származtatott) osztálynak egy Base (bázis) osztály nyilvános bázisosztálya (§15. Az Employee viszont nem feltétlenül Manager. egy Manager is megfelel. egy Manager egyben Employee is. így tehát mindenhol. Következésképpen egy osztályt csak akkor használhatunk bázisosztályként. Most már készíthetünk egy listát az alkalmazottakról (Employee)..7): . Employee ee) { Employee* pe = &mm. elist. } // ez is jó: pm a Manager típusú mm-re mutat. Általában. elist. így Employee*-ot nem használhatunk Manager*-ként. pm->level = 2. A másik irányban ez nem áll. Employee e1) { list<Employee*> elist.push_front(&m1). Származtatott osztályok 399 A Manager osztálynak az Employee-ból ilyen módon való származtatása a Manager típust az Employee altípusává teszi.3). ha definiáltuk (§5. így egy Manager*-ot használhatunk Employee*-ként is.4.2 pont írja le. // rendben: minden Manager egyben Employee is // hiba: nem minden Employee fõnök // katasztrófa: ee nem rendelkezik 'level' taggal pm = static_cast<Manager*>(pe). // amelynek van 'level' tagja Vagyis mutatók és referenciák használatakor a származtatott osztály objektumait úgy kezelhetjük. // . akkor egy Base* változó típuskényszerítés nélkül kaphat Derived* típusú értéket. // "nyers erõvel": mûködik. A static_cast és a dynamic_cast használatát a §15. ahol Employee objektum használható.12. mint a bázisosztály objektumait.push_front(&e1)..

. } A származtatott osztály azonban nem éri el a bázisosztály privát (private) tagjait: void Manager::print() const { cout << "A keresett név " << family_name << '\n'... // csak deklaráció. és ezt úgy kell megtennünk. public: void print() const..1...400 Absztrakciós módszerek class Employee. §15. }. }.. family_name.. public: void print() const. class Manager : public Employee { // .. // . }. mintha maguk vezették volna be azokat: void Manager::print() const { cout << "A keresett név " << full_name() << '\n'..2. // ... } // . A származtatott osztályok tagfüggvényei ugyanúgy elérhetik a bázisosztály nyilvános (public) – és védett (protected. nem definíció // hiba: Employee nem definiált 12. } // hiba! . char middle_initial. class Manager : public Employee { // . string full_name() const { return first_name + ' ' + middle_initial + ' ' + family_name. // .. // . Tagfüggvények Az egyszerû adatszerkezetek – mint a Manager és az Employee – nem túl érdekesek és sokszor nem is különösebben hasznosak. Az információt megfelelõ típusként kell megadnunk. hogy közben nem kötõdünk az adott ábrázoláshoz: class Employee { string first_name. melyhez az elvégezhetõ mûveletek is hozzátartoznak.3) – tagjait..

A legtisztább megoldás általában az. hogy a programozó hozzáférhetne az osztály privát részéhez egy osztályt származtatva belõle. majd a származtatott osztályokból származtatott további osztályokat és azok függvényeit és így tovább. // alkalmazottak adatainak kiírása cout << level. A származtatott osztálynak nincs különleges engedélye a bázisosztály privát tagjainak elérésére. Egy védett tag a származtatott osztályok számára olyan. mert a print() függvényt a Manager osztály újradefiniálja. // a fõnökökre vonatkozó adatok kiírása } Vegyük észre.. mi lenne fordított esetben: ha a származtatott osztály tagfüggvénye elérhetné a bázisosztály privát tagjait. A függvénynevek ilyen módon való újrafelhasználása igen általános. Ha óvatlanul ilyet írunk: void Manager::print() const { print().12.3). Némelyek meglepõdnek ezen. a többiek számára azonban privátnak minõsül (§15. Ott. hogy a :: hatókör operátort kellett használni. inkább védett (protected) és ne privát (private) tagokat használjunk. . // hoppá! } // a fõnökökre vonatkozó adatok kiírása a program váratlan módon újra és újra meg fogja hívni önmagát. mint egy nyilvános (public). Továbbá nem lehetne többé egy privát tag használatát a tagés barát (friend) függvények átnézésével megkeresni. Ez legjobb esetben is fárasztó és sokszor kivitelezhetetlen is. ahol ez elfogadható. így a Manager::print() számára a family_name nem érhetõ el. Származtatott osztályok 401 A Manager::print() második változatát a fordító nem fogja lefordítani.. // . Az egész program minden forrásállományát át kéne nézni: a származtatott osztályokat és azok függvényeit keresni. de gondoljuk el. ha a származtatott osztály a bázisosztálynak csak a nyilvános tagjait használja: void Manager::print() const { Employee::print(). A privát tag fogalma értelmetlenné válna azáltal.

} .. // .4.. }. }.. family_name.402 Absztrakciós módszerek 12. } // tagok kezdeti értékadása Manager::Manager(const string& n. public: Employee(const string& n. // .. akkor a megfelelõ konstruktort csak explicit módon lehet meghívni.. akkor az egyiket meg is kell hívni.. // . Konstruktorok és destruktorok Egyes származtatott osztályoknak konstruktorokra van szükségük. short department. // a bázisosztály kezdeti értékadása level(lvl) // tagok kezdeti értékadása { // . Az alapértelmezett konstruktorokat automatikusan is meghívhatjuk. A bázisosztály konstruktora a származtatott osztály konstruktorának definiciójában kap paramétereket.6): Employee::Employee(const string& n. Ha a bázisosztálynak vannak ilyen függvényei.. Ebbõl a szempontból a bázisosztály konstruktora úgy viselkedik. mintha a származtatott osztály tagja lenne (§10.. int lvl) : Employee(n. Vegyük a következõ példát: class Employee { string first_name.2. department(d) { // .d). int d. public: Manager(const string& n.. // .. int d).2. int lvl).. de ha a bázisosztály minden konstruktora paramétert igényel. int d) : family_name(n). class Manager : public Employee { set<Employee*> group. // beosztottak short level. int d..

Employee(const Employee&).. elõször a bázisosztály. meglepõ lehet és hibákhoz vezethet.6). Származtatott osztályok 403 Egy származtatott osztály konstruktora csak a saját tagjai és a bázisosztály konstruktora számára adhat meg kezdõértéket.1). A tagok a deklaráció sorrendjében jönnek létre és fordított sorrendben semmisülnek meg (§10. Az osztályba tartozó objektumok „alulról felfelé” épülnek fel. // hiba: family_name nem deklarált a Manager osztályban department(d). A megsemmisítés fordított sorrendben történik: elõször a származtatott osztály. int d.6 és §15. Az objektumnak ez a „felszeletelõdése” (slicing). A felszeletelõdés megakadályozása az egyik oka annak. aztán a tagok. és két ízben is megpróbál közvetlenül kezdõértéket adni az Employee tagjainak. hogy ekkor az objektumnak csak egy szelete másolódik le. }. 12. azaz a tény.2.4. // m Employee részének másolása e-be } Minthogy az Employee osztály másoló függvényei nem tudnak a Manager osztályról.12. aztán a tagok. // hiba: department nem deklarált a Manager osztályban level(lvl) { // . // e létrehozása m Employee részébõl e = m... a Manager objektumnak csak az Employee része fog lemásolódni. void f(const Manager& m) { Employee e = m.4 és §12. A hatékonysági megfontolások mellett további ok. int lvl) : family_name(n). ..1): class Employee { // . hogy osztályhierarchiába tartozó objektumok esetében célszerûbb mutatókat és referenciákat használnunk.5. hogy megõrizzük a többalakú (polimorfikus) viselkedést (§2.2.2. a bázisosztály tagjainak nem: Manager::Manager(const string& n. végül maga a származtatott osztály.4. Másolás Egy osztályba tartozó objektum másolását a másoló konstruktor és az értékadások határozzák meg (§10.4.4. } A fenti definíció három hibát tartalmaz: nem hívja meg az Employee konstruktorát.3. végül a bázisosztály. Employee& operator=(const Employee&).

2 pont részletesen elmagyarázza. */ }... */ }.: class Temporary { /* . hogy az értékadó operátorok nem öröklõdnek. */ }. class Director : public Manager { /* .. public Secretary { /* . hogy ha nem határozunk meg másoló értékadó operátort. */ }. */ }. a C++ nyelv képes az osztályoknak egy irányított körmentes gráfját kifejezni. Osztályhierarchiák Egy származtatott osztály lehet maga is bázisosztály: class Employee { /* .) 12.. de lehet ennél általánosabb gráf is... Ebbõl következik... akkor a fordítóprogram fog létrehozni egyet.4. (A konstruktorok soha.2. public Manager { /* . Ábrával: Employee Temporary Secretary Tsec Manager Consultant Director Vagyis ahogy a §15. A hierarchia leggyakrabban fa szokott lenni.... class Consultant : public Temporary. . Az egymással kapcsolatban álló osztályok ilyen halmazát hagyományosan osztályhierarchiának hívjuk. */ }... class Tsec : public Temporary.404 Absztrakciós módszerek Jegyezzük meg. */ }. class Secretary : public Employee { /* .. class Manager : public Employee { /* .

legtöbbször miért kerülendõ. // beosztottak }. family_name.2. char middle_initial. A „fõnök – alkalmazott” példát így írhatnánk át: struct Employee { enum Empl_type { M. A többi megoldás lehetõvé tesz heterogén listákat is. hogy lássuk. §15. Származtatott osztályok 405 12. Típusmezõk Ha a deklarációkban alkalmazott kényelmes rövidítésnél többre akarjuk használni az osztályok származtatását. Nézzünk meg elõször egy típusmezõs megoldást. mint a 2. vagy a 3. struct Manager : public Employee { Manager() { type = M. Empl_type type. 13.12.4. meg kell válaszolnunk a következõ kérdést: ha adott egy Base* mutató. akkor milyen származtatott osztályba tartozik az objektum.5. A 3. short department. Érjük el. E }.-nak egyedi. §12. megoldásnak a nyelv által támogatott változata. hogy csak egyféle objektum jöhessen szóba (§2. §15. a 4. azaz azonos típusú objektumokat eredményez.5. amelyre mutat? A problémának négy alapvetõ megoldása van: 1.7. Ekkor az elsõ megoldás homogén listákat. majdnem mindig világosabb kódot eredményeznek. azaz olyanokat. 4.5. Használjunk virtuális függvényeket (§2. megoldás párosításai különösen érdekesek és erõsek. megoldás a 2. mint a halmaz (set).. // . Employee() : type(E) { } string first_name. }. a vektor (vector) és a lista (list). pedig a 2.. megoldás.2. } set<Employee*> group. Helyezzünk egy típusmezõt a bázisosztályba és a függvények ezt kérdezzék le.4. . Bázisosztályra hivatkozó mutatókat gyakran használunk olyan tároló osztályokban (container class). fejezet). Az 1.6). típusbiztos átalakítása.. short level. Date hiring_date.5).2. 2. 3. és a 4.. Használjuk a dynamic_cast-ot (dinamikus típuskonverzió. // . ahol különbözõ típusú objektumok (vagy ilyenekre hivatkozó mutatók) lehetnek.

különösen az egyetlen programozó által fenntartott kis programokban. Alapvetõ gyengéje.end(). } Ez jól mûködik. // .. case Employee::M: { cout << e->family_name << '\t' << e->department << '\n'.. break. hogy kezelje a típusokat... cout << " szint " << p->level << '\n'. cout << " szint " << p->level << '\n'..begin(). if (e->type == Employee::M) { const Manager* p = static_cast<const Manager*>(e). } } . // . } } } Az alkalmazottak listája így írható ki: void print_list(const list<Employee*>& elist) { for (list<Employee*>::const_iterator p = elist.. mint a print_employee. A problémát általában súlyosbítja.. hogy a programozó a megfelelõ (és a fordítóprogram által nem ellenõrizhetõ módon) kell. ++p) print_employee(*p). // . // . const Manager* p = static_cast<const Manager*>(e). hogy az a feltétele..406 Absztrakciós módszerek Most már írhatunk egy függvényt. hogy az olyan függvények.. amely minden Employee-rõl ki tudja írni az információt: void print_employee(const Employee* e) { switch (e->type) { case Employee::E: cout << e->family_name << '\t' << e->department << '\n'. break. // . p!=elist.. a szóba jöhetõ osztályok közös vonásait használják ki: void print_employee(const Employee* e) { cout << e->family_name << '\t' << e->department << '\n'.

Vagyis a típusmezõs megoldás hibákra ad lehetõséget és nehezen módosítható kódot eredményez. virtual void print() const. nehéz lehet az összes ilyen típusmezõ-ellenõrzést megtalálni.. Származtatott osztályok 407 Egy nagy függvényben. hogy segítségükkel a programozó olyan függvényeket deklarálhat a bázisosztályban. Virtuális függvények A virtuális függvények azáltal kerülik el a típusmezõs megoldás problémáit. Minden olyan függvényt meg kell vizsgálni. int dept). Ahogy a rendszer mérete nõ. short department. hogy mi is történik. ahol sok származtatott típust kell kezelni.. 12.2. A tisztább felépítés és könnyebb módosíthatóság kedvéért a különálló dolgokat kezeljük külön. mert a típusmezõ alkalmazása ellentmond a modularitás és az adatrejtés elvének. hogy hozzá kell férni a kritikus forráskódhoz és külön munkát jelent a változtatás utáni teszt is. amelyik ellenõrzi a típusmezõt. . // . hogy javítani lehetne a kódon. amelyeket a származtatott osztályok felülbírálhatnak. a probléma súlyosbodik. Ezenkívül ha van egy olyan adat (például egy típusmezõ). A közös bázisosztály így mindenféle „hasznos információk” gyûjteményévé válik. public: Employee(const string& name.. A fordító.és betöltõprogram gondoskodik az objektumtípusok és az alkalmazott függvények összhangjáról: class Employee { string first_name. hogy további ilyen mezõket hozzon létre. Ez azt jelenti. A típuskényszerítés árulkodó jele annak. De ha megtaláltuk is. amelyiknek egy változtatás után szüksége lehet a típusmezõ ellenõrzésére. family_name.. Ez aztán a bázisosztály és a származtatott osztályok megvalósításának legkevésbé kívánatos összefonódásához vezet. // . amely a típusmezõs osztály leszármazottja. Továbbá.12. a kölcsönös függéseket pedig kerüljük el. az összes fontos függvényt módosítani kell – vagyis az összes olyat.6. Minden típusmezõt használó függvénynek ismernie kell az összes olyan osztály ábrázolását (és megvalósításuk egyéb részleteit). amely minden származtatott osztályból elérhetõ. nehéz lehet megérteni. akkor a programozó hajlamos arra. ha a rendszer új Employee-vel bõvül. }.

.. cout << "\tszint " << level << '\n'. ha osztályából nem is származtatunk további osztályt. // . }. } . Osztály származtatásakor csak akkor írjunk egy megfelelõ változatot a függvénybõl. short level. void Manager::print() const { Employee::print(). A virtuális tagfüggvényeket néha metódusoknak (method) is hívják.. ha azoknak ugyanolyan típusú paramétereik vannak... Ha a származtatott osztályokban szerepelnek ilyenek. // ..408 Absztrakciós módszerek A virtual kulcsszó azt jelenti.. annak a függvénybõl nem kell feltétlenül saját változat. // . Egy virtuális függvény akkor szolgálhat felületként a származtatott osztályokban definiált függvényekhez. int dept. illetve a származtatott osztályokban definiált print() függvényekhez. hacsak nem tisztán virtuális (pure virtual) függvényként adtuk meg (§12. public: Manager(const string& name. A virtuális függvényt mindig definiálnunk kell abban az osztályban. amelyben elõször deklaráltuk.2). a fordítóprogram mindig az adott Employee objektumnak megfelelõ függvényt fogja meghívni. és a visszatérési érték is csak nagyon csekély mértékben különbözik (§15. ha pedig származtatunk. int lvl). // . void print() const.3): void Employee::print() const { cout << family_name << '\t' << department << '\n'.. hogy a print() függvény felületként szolgál az ebben az osztályban definiált print() függvényhez. } Virtuális függvényt akkor is használhatunk. mint a bázisosztálybelinek.6. ha valóban szükséges: class Manager : public Employee { set<Employee*> group.

Manager m("Smith". Származtatott osztályok 409 A származtatott osztály azonos nevû és azonos típusú paraméterekkel bíró függvénye felülírja vagy felülbírálja (override) a virtuális függvény bázisosztálybeli változatát.12.push_front(&e). Ha helyesen alkalmazzuk.2. empl.begin(). p!=s.push_front(&m).end().s.1234. empl.5) szükségtelen. az objektumorientált tervezés sarokköve lesz és a programok fejlesztésénél bizonyos fokú stabilitást ad. mert a helyébe a print() tagfüggvények léptek.5. hogy a virtuális függvény melyik változatát akarjuk használni – mint az Employee::print() hívásnál –. mielõtt a Manager osztályt egyáltalán kitaláltuk volna! Ez az osztályoknak egy kulcsfontosságú tulajdonsága.1234).5 Minden Employee a típusának megfelelõen íródik ki. az objektumhoz leginkább illõ felülíró függvény lesz meghívva. A globális print_employee() függvény (§12.2 De akár így is: void print_list(const list<Employee*>& s) { for_each(s.4 például az alábbi kimenetet eredményezi: Smith 1234 szint 2 Brown 1234 Ez akkor is mûködik.7. A int main() { Employee e("Brown". list<Employee*> empl. . print_list(empl). } // lásd §2.begin().8.mem_fun(&Employee::print)).2). ++p) (*p)->print().end(). Az alkalmazottak (Employee) listáját így írathatjuk ki: void print_list(const list<Employee*>& s) { for (list<Employee*>::const_iterator p = s. } // §2. Hacsak közvetlen módon meg nem mondjuk. } // lásd §3. ha a print_list() függvényt azelõtt írtuk meg és fordítottuk le.

akkor ezáltal kikapcsoljuk a virtualitást. A Manager::print() függvény ennek példája. a fordítóprogram felismeri annak pontos típusát. A virtuális függvényekkel bíró típus neve többalakú típus (polimorfikus típus). akkor a fordítóprogram a :: minõsítõvel jelzett hívásokat képes helyben kifejteni. és erre is csak azon osztályokban van szükség. ahol egy közönséges függvényhívás elfogadhatóan hatékony. hogy az Employee függvényei attól függetlenül „helyesen” viselkednek. Világos. hogy a többalakúság támogatása érdekében a fordítóprogramnak minden Employee típusú objektumban valamilyen. a típusra vonatkozó információt (típusinformációt) kell nyilvántartania. . melynek segítségével képes a megfelelõ print() függvényt meghívni. Ha egy függvényt (miként a Manager::print()-et is) a :: hatókör-feloldó operátor segítségével hívunk meg. így a futási idejû többalakúságra nincs szükség.5. többalakúságnak (polimorfizmus. amikor mondjuk egy virtuális függvény ugyanarra az objektumra egy másik függvényt is meghív. amelyeknek van virtuális függvényük. polymorphism) nevezzük. hogy pontosan milyen fajta Employee-re hívtuk meg azokat. hogy a virtuális függvényhívás hagyományos és nyilvánvaló megvalósítása az egyszerû közvetett függvényhívás (indirekció) (§2. A minõsített név használatának van még egy elõnye: ha egy virtuális függvény inline (ami elõ szokott fordulni).410 Absztrakciós módszerek Azt. tehát nem minden osztályban és még csak nem is minden származtatott osztályban. Minthogy a Manager::print() meghívásakor meghatározzuk az objektum típusát. Érdemes megemlíteni. A típusmezõs megoldás választása esetén ehhez képest jelentõs mennyiségû tárterületet kellett volna a típusmezõ számára biztosítanunk. Máskülönben a Manager::print() végtelen rekurziót idézne elõ. így a hatékonyság elvesztésétõl való félelem ne riasszon vissza senkit a virtuális függvények használatától ott. az Employee::print() ezt követõ meghívásakor a típusról már nem kell újra dönteni. Ehhez rendszerint egyetlen mutatónyi hely is elég.5). Ennek segítségével a programozó hatékonyan képes azokat az egyedi eseteket kezelni. Ha közvetlenül kezelünk egy objektumot és nem mutató vagy referencia segítségével. A C++ nyelvben a többalakú viselkedést virtuális függvények használatával vagy az objektumoknak mutatókon vagy referenciákon át való kezelésével érhetjük el.

}. virtual void draw() = 0. hogy önmagában és származtatott osztályok bázisosztályaként is hasznos.. // nem "elegáns" Egy ilyen meghatározatlan alakzatot meg tudunk ugyan adni (a nyelv megengedi). abstract class) hívjuk. de nem sok értelme van: Shape s. A Shape-nek csak mint bázisosztálynak van értelme. olyan elvont fogalmakat jelenítenek meg. akkor absztrakt osztálynak (elvont osztály. Absztrakt osztályok Sok osztály hasonlít az Employee osztályra annyiban. } // .. Ez abból is látható. például a Shape (Alakzat).. amelyekhez nem létezhetnek objektumok. // hiba: s az absztrakt Shape osztály változója lenne . hogy nem tudunk hozzá virtuális függvényeket értelmesen definiálni: class Shape { public: virtual void rotate(int) { error("Shape::rotate"). De nem minden osztály ilyen. A virtuális függvények az =0 „kezdeti értékadástól” lesznek tisztán virtuálisak: class Shape { public: virtual void rotate(int) = 0. }. virtual bool is_closed() = 0. ha a Shape osztály virtuális függvényeit tisztán virtuális (pure virtual) függvényként deklaráljuk. Jobb megoldás. // absztrakt osztály // tisztán virtuális függvény // tisztán virtuális függvény // tisztán virtuális függvény Ha egy osztály legalább egy tisztán virtuális függvénnyel rendelkezik. Az ilyen osztályok számára elegendõek az elõzõ pontban bemutatott módszerek.12.3. Származtatott osztályok 411 12.. } virtual void draw() { error("Shape::draw"). // . // butaság: "alak nélküli alakzat" A dolog azért értelmetlen. ilyen osztályba tartozó objektumot pedig nem hozhatunk létre: Shape s. mert az s minden mûvelete hibát fog eredményezni. Bizonyos osztályok.

class Circle : public Shape { public: void rotate(int) { } void draw(). } Circle(Point p.. public: void draw(). bool is_closed() { return true. }. a draw és a rotate nincs felülírva . // hiba: a Polygon osztálynak nem lehet objektuma class Irregular_polygon : public Polygon { list<Point> lp. */ }. hogy segítségükkel a megvalósítás egyéb részeinek elérhetõvé tétele nélkül biztosíthatunk felületet. Irregular_polygon poly(some_points).. }. sõt. // .412 Absztrakciós módszerek Az absztrakt osztályokat csak felületként (interface). // a Shape::draw felülírása // a Shape::rotate felülírása // jó (megfelelõ konstrukort feltételezve) Az absztrakt osztályok fontos képessége. }. akkor az tisztán virtuális függvény marad. ... void rotate(int).. } // . illetve más osztályok bázisosztályaként használhatjuk: class Point { /* . Egy operációs rendszer például egy absztrakt osztály mögé rejtheti eszközmeghajtóinak tulajdonságait: class Character_device { public: virtual int open(int opt) = 0.. private: Point center. virtual int close(int opt) = 0. // a Shape::rotate felülírása // a Shape::draw felülírása // a Shape::is_closed felülírása Ha egy tisztán virtuális függvényt a származtatott osztályban nem definiálunk.. a származtatott osztály is absztrakt osztály lesz.. int r). int radius. // absztrakt osztály // a Shape::is_closed felülírása Polygon b. Ez a megvalósítás lépcsõzetes felépítését teszi lehetõvé: class Polygon : public Shape { public: bool is_closed() { return true.

Az alapötlet az. hogy elszigeteljük programunkat ettõl a sokféleségtõl és felderíthessük a különbözõ tervezési módokat. mert a kapcsán egy sor eljárást és tervezési szempontot lehet bemutatni. Azt az általános megközelítést alkalmazzuk.12. ha még nem áll rendelkezésre. és sokféle eszközmeghajtót kezelhetünk ezen felületen keresztül. hogy egy alkalmazást elválasszunk a felhasználói felülettõl. kezdjük a munkát ezen egyszerû adatbeviteli mûvelet modelljének felállításával. Osztályhierarchiák tervezése Vegyük a következõ egyszerû tervezési problémát: hogyan lehet egy program számára lehetõvé tenni egy egész érték bekérését a felhasználói felületrõl? Zavarbaejtõen sokféle módon. A program elkérheti egy Ival_box objektum értékét és felszólíthatja arra is. ahová a felhasználó beírhatja az értéket.) = 0. int n) = 0. virtual int write(const char* p. Azért választottam ezt. Az absztrakt osztályok bevezetésével immár minden eszköz a kezünkben van arra. mert általános. Azt is megkérdezheti. int n) = 0. A virtuális destruktorok fontosságát a §12.2 pont magyarázza el. hogy moduláris módon. hanggal vezérelhetõ eszközök. E felület számos rendszeren elkészíthetõ. Ahhoz.4. Származtatott osztályok 413 }. hogy lesz egy Ival_box (értékmezõ) osztályunk. hogy kérje be ezt az értéket a felhasználótól. hogy sokféle különbözõ Ival_box lesz: csúszkák. virtual int read(char* p. abból kell kiindulnunk. szöveges adatbeviteli mezõk. így kódja „hordozható” lesz. Minthogy ez az alapötlet sokféleképpen megvalósítható.4. Természetesen vannak más módok is arra.. mert ezeket a módszereket alkalmazzák a valódi felhasz- . hogy az érték megváltozott-e a legutóbbi kérés óta. virtual ~Character_device() { } // virtuális destruktor Az egyes eszközmeghajtókat a Character_device-ból származtatott osztályként definiálhatjuk. hogy egy virtuális felhasználói felületet” bocsátunk az alkalmazás rendelkezésére. számtárcsák.. virtual int ioctl(int . A tényleges felhasználói felület elkészítésének részleteit késõbbre halasztjuk. amely tudja. 12. építõkövekként osztályokat használva egy teljes programot írjunk. amely a létezõ felhasználói felületek szolgáltatásainak egy részét biztosítja. hogy milyen értékeket fogadhat el.

val = i. „pongyolák”.414 Absztrakciós módszerek nálói felületeteket kezelõ rendszerekben. } virtual int get_value() { changed = false. } virtual void reset_value(int i) { changed = false. 12. } virtual void prompt() { } virtual bool was_changed() const { return changed. } virtual void set_value(int i) { changed = true. high. val = low = ll. val = i.) Az „ival osztályokat” egy programozó így használhatná fel: void interact(Ival_box* pb) { pb->prompt().. int low. A függvények alapértelmezett változatai meglehetõsen vázlatosak. int hh) { changed = false. hogy illusztrálják a megközelítést. int i = pb->get_value(). valamit csinálunk vele } . ilyennel a Simula.. high = hh. if (pb->was_changed()) { // új érték.4. } // felhasználók számára // alkalmazások számára }.1. Smalltalk és régebbi C++-programokban találkozhatunk. // jelzés a felhasználónak // . és végül – a leglényegesebb ok –. Hagyományos osztályhierarchia Elsõ megoldásunk egy hagyományos osztályhierarchia. (Egy „valódi” osztály például értékellenõrzést is végezne. bool changed. melyet az egyes Ival_box-ok sajátjaikkal felülbírálhatnak. Ezenkívül megadjuk az alapmegoldáshoz szükséges adatokat is: class Ival_box { protected: int val. Az Ival_box osztály az összes Ival_box által használatos felületet írja le és egy olyan alapértelmezett megvalósítást ad. mert ezek a módszerek a felhasználói felületetek szûk tartományánál jóval szélesebb körben is alkalmazhatók. public: Ival_box(int ll. return val. céljuk leginkább az.

majd a szál állapotát kérdezzük le. hogy egyáltalán különbözõ Ival_box-ok léteznek. Az itt bemutatott tervezési módszerek és az azokat támogató nyelvi eszközök nem kötõdnek adott felhasználói felülethez.12). elvonnánk a figyelmet a programozási eljárások és nyelvi eszközök tárgyalásától. hogy az Ival_box-ot egy eseményhez kapcsoljuk és egy visszahívás (callback) segítségével válaszolunk. Így a programnak nem kellene tudnia az esetleg nagy számú különbözõ Ival_box-változatokról. és egyszerû Ival_box-okat. eltekintek attól a kérdéstõl. Az ilyen döntések alapvetõ fontosságúak a felhasználói felületet kezelõ rendszerek tervezésekor. }. // a régi érték jó volt. Ival_box* p2 = new Ival_dial(1. illetve azokra hivatkozó mutatókat használna. A kód legnagyobb részének még arról sem kell tudnia. viselkedését meghatározó grafikai elemek public: Ival_slider(int.. hogy a program a get_value() függvényben ténylegesen vár a felhasználói bemenetre. hogy a származtatott osztályok- .5). // az Ival_slider az Ival_box osztályból származik } A programkód legnagyobb része az interact() függvény stílusában íródna. esetleg a programmal külön végrehajtási szálat indíttatunk el az Ival_box számára. int). A különbözõ Ival_box-okat az Ival_box-ból származtatott osztályokként határozhatjuk meg: class Ival_slider : public Ival_box { // a csúszka kinézetét. int get_value(). ezt is felhasználjuk valahogy } void some_fct() { Ival_box* p1 = new Ival_slider(0.12. void prompt(). amelyek ilyen objektumokat létrehoznak. Az Ival_box adattagjait védettként (protected) vezettük be. hogyan vár a program bemenetre. jóval szélesebb körben is alkalmazhatók. Ez a felhasználókat elszigeteli a származtatott osztályok esetleges módosításaitól. Hogy egyszerûsítsem a tárgyalást. Lehetséges megoldás. Származtatott osztályok 415 else { } // . csak annak a viszonylag kis számú függvénynek kellene ismernie azokat. de ha itt a valóságot akár csak megközelítõ részletességgel tárgyalnánk ezeket. interact(p2).. interact(p1). megoldható úgy is.

átméretezhetõ. és a Popup_ival_slider. */ }. ha a prompt() függvénnyel erre kérjük. amelynél egy gomb forgatásával adhatunk meg egy értéket.. áthelyezhetõ lesz stb. ha Ival_box-unkat úgy írjuk át. Ábrával: BBwindow Ival_box Ival_slider Ival_dial Popup_ival_slider Flashing_ival_slider .. */ }... Az Ival_box-ból az Ival_slider mellett más változatokat is származtathatunk. a Flashing_ival_slider..” („Sok Pénz Rt. Így aztán az Ival_slider::get_value() függvény elhelyezheti az értéket az Ival_box::val adattagban. osztályok mindegyike egy-egy fajta BBwindow (Big Bucks window) kell.3). Ezt a legegyszerûbben úgy érhetjük el.. a felhasználótól szinte kikövetelve egy érték megadását. class Popup_ival_slider : public Ival_slider { /* . */ }.”) rendszerét használjuk.. hogy a BBwindow-ból származtatott osztály legyen. class Ival_dial : public Ival_box { /* . hogy legyen.. Így aztán az összes osztályunk a BBwindow-ból származtatott lesz. tehát elhelyezhetõ lesz a képernyõn. amely leírja a képernyõn levõ objektumok alapvetõ tulajdonságait. az Ival_dial stb... Ezek között ott lehet az Ival_dial. De vajon honnan vegyük a grafikus elemeket? A legtöbb felhasználói felületet kezelõ rendszer biztosít egy osztályt. a BBwindow rendszer szabályainak megfelelõen. amely felvillan.. // újraírva a BBwindow használatára class Ival_slider : public Ival_box { /* . megjelenése igazodik majd a rendszer többi grafikus elemének megjelenéséhez. amely a prompt() hatására valamilyen feltûnõ helyen jelenik meg. akkor az Ival_slider. A védett tagok elérhetõk az osztály és a származtatott osztályok függvényei számára is. */ }. Osztályhierarchiánk tehát így fog kinézni: class Ival_box : public BBwindow { /* . */ }. Ha például a „Big Bucks Inc.416 Absztrakciós módszerek ból elérhetõek legyenek. class Flashing_ival_slider : public Ival_slider { /* . de az általános felhasználó számára nem (§15.

hogy az Ival_box a BBwindow osztály leszármazottja. típusokat és konstansokat tartalmazzon. akkor az összes származtatott osztályt újra kell fordítani. hogy ha egy bázisosztály mérete megváltozik. // BB változat // CW változat // IB változat // LS változat Ha ennyi változatunk van. ez azzal az elõnnyel jár. mégis bekerültek az Ival_box felületbe. ha cégünk kulcsfontosságú üzleti döntése. ezt a részletkérdést elsõrendû tervezési döntéssé emeli. minthogy ez a csúszka állásából meghatározható. Bírálat Ez így sok tekintetben jól fog mûködni és az ilyesfajta osztályfelépítés számos problémára jó megoldás. A BBwindow osztály nem alapvetõ része az Ival_box-ra épített rendszernek. */ }. módosításuk. valahányszor végrehajtják a get_value()-t. A tapasztalat is azt mutatja. hogy a BBwindow osztály változásakor az Ival_box felhasználóinak újra kell fordítaniuk. változatkövetésük rémálommá válhat. hogy nem biztosított. class Ival_box : public IBwindow { /* . class Ival_box : public CWwindow { /* . hogy az Ival_box-ban deklarált adatok minden származtatott osztály rendelkezésére állnak. mert így a származtatott osztályok írói nem zavarhatják össze azokat.12. A BBwindow osztályt utólag tettük az Ival_box bázisosztályává.. Általában is problematikus két rokon. Ezek az adatok megint csak egy „apró” részletet jelentenek. class Ival_box : public LSwindow { /* . */ }. Ha az Ival_box a BBwindow-ból származik.4. hogy az Ival_box felhasználói a BBwindow minden szolgáltatását igénybe vehetik. hogy ne legyenek többé összhangban. Ez abban az esetben helyes. ha az adattagok privátok. */ }. esetleg újra kell írniuk a kódjukat. ha az adatok a származtatott osztályokban vannak. */ }.. melyek az „Imperial Bananas”.1. hogy a „Big Bucks Inc. Az Ival_slider esetében például nem szükséges az adat külön tárolása.” által biztosított környezetet használjuk. Ez gyakorlati szempontból azt is jelenti. A védett felület szinte mindig csak függvényeket.. Még jobb. hogy mindig a megfelelõ adatot kapjuk. Az. melyek miatt más tervezési lehetõségek után fogunk nézni.. ami sajnos azt is jelenti. hogy kezdõ programozók szükségtelen és nehezebb módosíthatóságot eredményezõ módon szeretnek a védett (protected) adattagokkal ügyeskedni.. Elõbb-utóbb valaki eléri. A legtöbb C++-változat úgy mûködik. de eltérõ adathalmaz tárolása. Jobb. . megléte csupán részletkérdés.. Egy másik probléma. ami nem egészen helyes.. Származtatott osztályok 417 12. a „Liberated Software” vagy a „Compiler Whizzles”-tõl származnak? Ekkor programunkból négy változatot kellene készítenünk: class Ival_box : public BBwindow { /* .1.. Ám van néhány hátulütõje. mert akkor pontosan meg tudnak felelni a követelményeknek és nem keseríthetik meg az egymással nem rokon származtatott osztályok életét. De mi történik. ha Ival_box-unkat olyan rendszerekre is át szeretnénk ültetni.

ne legyen szükséges az Ival_box családot felhasználó kód újrafordítása. Elhagytuk az adattagokat és a tagfüggvények egyszerûsített kifejtését is. virtual bool was_changed() const = 0. vagy mert programunknak különbözõ rendszerek felhasználóival kell kapcsolatot tartania. ha a felhasználói felületet az egyetlen Ival_box felületünk bázisosztályaként „bedrótozzuk”. A felhasználói felület valóban olyan részletkérdés legyen. mint az Ival_box osztály eredeti deklarációja volt. . Ehelyett egy virtuális destruktorunk van. mert valahogy ezek egy képernyõn tudnak osztozni. 4. Különbözõ felhasználói felületekhez tartozó Ival_box-ok tudjanak egyszerre létezni a programban.418 Absztrakciós módszerek Végül lehetséges. Többféle megoldás kínálkozik erre. kezdjük újra a tervezést. mivel nincs kezdõértékre váró adat. 3. Elõször is.4. Az Ival_box osztály ne tartalmazzon adatokat. Ha megváltozik a felhasználói felületet kezelõ rendszer. virtual void reset_value(int i) = 0. Ehhez pedig nem elég rugalmas megoldás. ahol különbözõ felhasználói felületek ablakai léteznek egyidejûleg. most egy olyat mutatok be. Vagy azért. Ez sokkal világosabb. Absztrakt osztályok Nos. 12. amely biztosítja az öröklõ osztályok adatainak helyes „eltakarítását”. akiket nem érdekel. hogy programunknak olyan vegyes környezetben kell futnia. virtual ~Ival_box() { } }. virtual void prompt() = 0. Elmaradt a konstruktor is.2. amelyrõl nem kell tudomást venniük azon felhasználóknak. 2. virtual void set_value(int i) = 0. amely tisztán megvalósítható a C++ nyelvvel. az Ival_box osztályt puszta felületként (pure interface) határozzuk meg: class Ival_box { public: virtual int get_value() = 0. hogy megoldjuk a hagyományos felépítés bírálatában felvetett problémákat: 1.

BBwindow::mouse1hit() private: // a csúszka adatai }. Az ablaknak az Ival_box osztály egy BBwindow* típusú tagjaként való ábrázolása teljesen eltérõ szerkezethez vezet.1. Érdekes módon az Ival_slider ilyen módon való deklarálása esetén ugyanolyan kódot írhatunk. Hasonlóan. Csak azért változtattunk. hogy az Ival_slider-bõl származtatott osztályok számára a BBwindow-t elérhetõvé tegyük. Ebbõl következik.3.int).4. melynek megvannak a maga elõnyei és hátrányai (§12. hogy az Ival_slider-t felhasználó programozó nem használhatja közvetlenül a BBwindow által nyújtott eszközöket.3.7). ~Ival_slider(). Mivel a BBwindow osztályból való származása mindössze segítséget nyújt a megvalósításhoz. protected BBwindow { public: Ival_slider(int.1 pontban láttuk.. void set_value(int i). ezért onnan valók az eszközei. ezért nyilvános (public) módon származik onnan.. Vegyük észre. illetve abból. nem járható. de ez nemkívánatos mellékhatásokkal jár.7[14]. ezért közvetve vagy közvetlenül mindkét osztályból származnia kell. hogy az Ival_box a BBwindow-ból származik). meg kell valósítania annak tisztán virtuális (pure virtual) függvényeit.2). hogy a BBwindow „megvalósítási osztály” tagja legyen az Ival_box-nak. int get_value(). Mint a §12. onnan védett (protected) módon származik (§15. melyekkel ezt megteheti. mint azelõtt. Származtatott osztályok 419 Az Ival_slider definíciója így alakulhat: class Ival_slider : public Ival_box.12. az az út.2). . §25. A BBwindow osztályból is származik. BBwindow::draw(). protected: // a BBwindow virtuális függvényeit felülíró függvények // pl. Mivel az Ival_slider osztály az absztrakt Ival_box osztályból származik. lehetséges ugyan az Ival_slider közvetett származtatása a BBwindow-ból (azáltal. hogy a szerkezet logikusabb módon tükrözze a megvalósítást. Azért használunk védett származtatást a szigorúbb megkötést jelentõ (és általában biztonságosabb) privát helyett. mind a BBwindow függvényei közül felül kell írnia néhányat. mert egy osztály nem írhatja felül tagjainak virtuális függvényeit (§24. // . Az Ival_slider felülete az Ival_box-tól örökölt részbõl áll. hogy Ival_slider-nek mind az Ival_box. amit maga az Ival_slider kifejezetten deklarál. Az Ival_box adja a származtatott osztály felületét. A több osztályból való közvetlen öröklõdést általában többszörös öröklõdésnek (multiple inheritance) hívják (§15.4).

protected BBwindow { /* . Az Ival_box hierarchiát most így írhatjuk le: class Ival_box { /* .420 Absztrakciós módszerek Számos osztálynak szüksége van valamilyen „rendrakásra”. hogy egy származtatott osztálynak nincs-e szüksége erre. } A delete operátor megsemmisíti az objektumot. class Flashing_ival_slider : public Ival_slider { /* . de mivel az Ival_box-nak virtuális destruktora van... Mivel az absztrakt Ival_box osztály nem tudhatja. mielõtt egy objektuma megsemmisül. protected BBwindow { /* . hogy igenis szükség van rá... */ }. */ }. */ }.. Egyszerû rövidítésekkel pedig így ábrázolhatjuk: BBwindow Ival_box BBwindow Ival_slider Ival_dial Popup_slider Flashing_slider A szaggatott nyilak a védett (protected) módú öröklõdést jelölik. */ }. A rendrakást úgy biztosítjuk. hogy pontosan milyen osztályú objektumról van szó. delete p... hogy a bázisosztályban definiáljuk az Ival_box::~Ival_box() virtuális destruktort és a származtatott osztályokban megfelelõ módon felülírjuk azt: void f(Ival_box* p) { // . (ha az adott osztálynak van ilyen)... . Az általános felhasználók számára ezek csak részletkérdések. class Ival_dial : public Ival_box.. */ }.. Nem tudhatjuk. fel kell tételeznie. amelyre p mutat.. class Ival_slider : public Ival_box. class Popup_ival_slider : public Ival_slider { /* . a megfelelõ destruktor fog meghívódni.

. // CW // . */ }.. // .4.. // közös class Ival_slider : public Ival_box. Ráadásul a BBwindow-hoz és a CWwindow-hoz írt Ival_slider-ek nem létezhetnek együtt. Ábrával: BBwindow Ival_box CWwindow BB_ival_slider CW_ival_slider Hogy programunk Ival_box osztályait jobban elszigeteljük a megvalósítás egyéb részleteitõl... */ }. hogy különbözõ nevû Ival_slider osztályokat hozunk létre: class Ival_box { /* . de nem kevésbé hatékony. */ }. class BB_ival_slider : public Ival_box. */ }. */ }. */ }. még akkor sem. protected BBwindow { /* . protected BBwindow { /* . */ }. A változatkövetési problémát azonban nem oldja meg: class Ival_box { /* . class Ival_slider : public Ival_box { /* . class CW_ival_slider : public Ival_slider... */ }. protected CWwindow { /* . // . ha egyébként maguk a BBwindow és CWwindow felhasználói felületek igen.... majd ebbõl örököltethetjük az egyes rendszerfüggõ Ival_slider-eket: class Ival_box { /* ..... class BB_ival_slider : public Ival_slider. protected CWwindow { /* .... protected BBwindow { /* .. mint a hagyományos. protected CWwindow { /* ..3. származtathatunk egy absztrakt Ival_slider osztályt az Ival_box-ból..12. class CW_ival_slider : public Ival_box. . */ }. A nyilvánvaló megoldás az. Származtatott osztályok 421 12..... // BB class Ival_slider : public Ival_box.. */ }. Egyéb megvalósítások Ez a szerkezet tisztább és könnyebben módosítható..

Ábrával: BBwindow Ival_box CWwindow BBslider Ival_slider CWslider BB_ival_slider CW_ival_slider Ez a javítás jelentõs lehet abban a (sûrûn elõforduló) esetben.. protected BBslider { /* . */ }... Ha például a „Big Bucks Inc.. protected CWslider { /* . alkalmazásközpontú rendszer származtatott osztályokként megvalósított felületeinek viszonyrendszerébõl fog állni: class Ival_box { /* . class Flashing_ival_slider : public Ival_slider { /* .. */ }.” rendszerében van egy csúszka (slider) osztály. ha a hierarchiában egyedibb osztályokat használunk.422 Absztrakciós módszerek Ábrával: Ival_box BBwindow Ival_slider CWwindow BB_ival_slider CW_ival_slider Általában még ennél is jobban járunk. */ }. */ }..... A teljes hierarchia egyrészt az eredeti.. */ }... class CW_ival_slider : public Ival_slider. */ }. class Ival_slider : public Ival_box { /* . akkor a mi Ival_slider-ünket közvetlenül a BBslider-bõl származtathatjuk: class BB_ival_slider : public Ival_slider. class Ival_dial : public Ival_box { /* . class Popup_ival_slider : public Ival_slider { /* . Ekkor a programozás tulajdonképpen a rokon fogalmak közötti leképezésre egyszerûsödik.. */ }. és a BBwindow-hoz hasonló általános bázisosztályokból való öröklõdés ritkán fordul elõ.. . ha a mi fogalmaink nem esnek távol a megvalósítás céljából felhasznált rendszer fogalmaitól.

Származtatott osztályok 423 Illetve a hierarchiát – szintén az öröklõdés segítségével – többféle grafikus felhasználói felületre leképezõ származtatott osztályokból: class BB_ival_slider : public Ival_slider. protected BBwindow_with_bells_and_whistles { /* . mint a konkrét felhasználói felületet bázisosztályként szerepeltetõ. .. A program szempontjából ezek a szerkezetek egyenértékûek abban az értelemben.. ha az egyik szerkezetrõl a másikra váltunk. Az utóbbiban a fa gyökere a megfelelõ ablakosztály. az elõbbiben viszont változatlanul az alkalmazás osztályhierarchiája marad a tényleges megvalósítást végzõ osztályok alapja.12. class CW_ival_slider : public Ival_slider.4..1. hogy majdnem az egész kód változtatás nélkül és ugyanúgy mûködik mindkét esetben. class BB_popup_ival_slider : public Popup_ival_slider.. // . protected CWslider { /* . és mindkettõnél az alkalmazott felhasználói felülettõl függõ elemekre való tekintet nélkül vizsgálhatjuk az Ival_box család osztályait.. Bírálat Az absztrakt osztályokat használó osztályszerkezet rugalmas és majdnem ugyanolyan egyszerûen kezelhetõ. A §12. */ }. */ }.. */ }.. csak a konkrét megvalósítást végzõ osztályok veszik körül. 12.3.. class BB_flashing_ival_slider : public Flashing_ival_slider..4. A kapott felépítményt egyszerû rövidítések segítségével így ábrázolhatjuk: Ival_box Ival_slider Ival_dial ipopup iflash BBslider BBslider CWsl CWsl BBb&w CWsl BBislider BBipop CWipop CWifl BBifl CWislider Az eredeti Ival_box hierarchia változatlan marad.. */ }.1-beli interact() függvényt például nem kell újraírnunk. protected BBslider { /* . protected BBslider { /* .

Az objektumokat azonban az adott rendszerre jellemzõ nevek (például CW_ival_dial és BB_flashing_ival_slider) felhasználásával kell létrehozni. int) =0. int) =0. Az ilyen osztályokat gyárnak (factory) hívják. ha az ilyen rendszerfüggõ nevek minél kevesebb helyen fordulnának elõ. mely felületrõl a felhasználók tudhatnak. hacsak nem szisztematikusan járunk el. ha a megvalósítást végzõ elemek készítõje egy új. Ez különösen akkor fontos. Az objektumok létrehozásának adott helyre korlátozása A program legnagyobb része megírható az Ival_box felület felhasználásával.424 Absztrakciós módszerek Mindkét esetben újra kell írnunk az egyes Ival_box osztályokat.4. Ezt többféleképpen is megtehetjük. // elõugró csúszka (popup slider) // készítése // . semmi sem öröklõdik automatikusan egy rendszerfüggõ bázisosztálytól. // tárcsa (dial) készítése virtual Popup_ival_slider* popup_slider(int. Az Ival_maker osztály az Ival_box hierarchia minden olyan felülete számára rendelkezik az adott típusú objektumot létrehozó függvénnyel. mivel az objektumok létrehozása nehezen köthetõ helyhez. de az absztrakt osztályokat használó szerkezet esetében szinte az egész kód védett a megvalósítás változásától és egy ilyen változás után nem kell újrafordítani. Jó lenne. felületeket. amely az objektumokat létrehozó mûveletekért felelõs: class Ival_maker { public: virtual Ival_dial* dial(int. máshol nem használható megvalósítás csapdájába való bezáródás veszélyének. ha a felhasználói felület nyilvános felülete megváltozik. }. mint a sima Ival_box. . Szokás szerint az indirekció (közvetett hivatkozás) bevezetése a megoldás.. akkor nagyrészt használhatjuk az Ival_box. „majdnem kompatibilis” változatot bocsát ki. 12.4.6.2). függvényeiket pedig – némiképp félrevezetõ módon – virtuális konstruktoroknak (§15.. mert csak az Ival_box hierarchiában kifejezetten megadott lehetõségek érhetõk el. Ha a származtatott felületek továbbfejlõdnek és több szolgáltatást nyújtanak. Egyszerû megoldás lehet például egy olyan osztály bevezetése. Ival_slider stb. Ráadásul az absztrakt osztályos megoldást választók a klasszikus hierarchia híveinél kevésbé vannak kitéve az egyedi. Az elvont Ival_box osztályokra épített programot választva nem használhatjuk „véletlenül” a megvalósító osztályok nyújtotta lehetõségeket.

// BB-változatok készítése // LS-változatok készítése Minden függvény a kívánt felületû és megvalósítási típusú objektumot hozza létre: Ival_dial* BB_maker::dial(int a. // . // . user(&LS_impl). int). Popup_ival_slider* popup_slider(int.. LS_maker LS_impl.. // . Származtatott osztályok 425 Az egyes különbözõ felhasználói felületeket kezelõ rendszereket most az Ival_maker osztályból származtatott osztályokként ábrázoljuk: class BB_maker : public Ival_maker { public: Ival_dial* dial(int. } Ha adott egy mutató egy Ival_maker objektumra..b).. }. hogy nem kell tudnia. akkor a programozó ennek segítségével úgy hozhat létre objektumokat. int). } BB_maker BB_impl. } // megfelelõ tárcsa létrehozása // BB-felhasználóknak // LS-felhasználóknak // BB használata // LS használata .b). int). pontosan milyen rendszerû felhasználói felület van használatban: void user(Ival_maker* pim) { Ival_box* pb = pim->dial(0. }. int). int b) { return new LS_ival_dial(a.12.. } Ival_dial* LS_maker::dial(int a. Popup_ival_slider* popup_slider(int. class LS_maker : public Ival_maker { public: Ival_dial* dial(int. void driver() { user(&BB_impl).99). int b) { return new BB_ival_dial(a..

Az osztályhierarchia annak eszköze. hogy a felületeket elválasszuk a megvalósítási részletektõl. hogy minél kisebb felületeket használhassunk.2.3. mint bármely más virtuális függvényt. némely absztrakt osztály pedig jelentõs szolgáltatásokat kínál. Az ilyen felépítés ideálisan támogatja a lépésenkénti finomítással való fejlesztést. hogy miféle elvonatkoztatási réteg határát lépi át. [2] Az objektumok felszeletelõdését (slicing) elkerülendõ használjunk mutatókat és referenciákat.2. [4] Használjunk absztrakt osztályokat.3. anélkül hogy a megvalósítás részleteivel keveredne vagy jelentõsen növelné a program futási idejét. Osztályhierarchiák és absztrakt osztályok Az absztrakt osztályok felületek (interface). [5] Használjunk absztrakt osztályokat.6. §12. de klasszikus hierarchiaként építsünk fel.5. Természetesen minden osztály ad egy felületet a programozó számára. §12. 12. A fentiekbõl adódó végkövetkeztetés az. de „felület” és „építõkõ” szerepük alapvetõen az absztrakt osztályoknak és az osztályhierarchiáknak van.426 Absztrakciós módszerek 12. . amennyiben ezek megfelelõen illeszkednek a hierarchiába. hogy egy rendszert a felhasználók felé mindig absztrakt osztályok hierarchiájaként mutassunk. §12. Az absztrakt osztályok segítségével felépített rendszer tisztább és hatékonyabb módot ad a fogalmak kifejezésére. [3] Használjunk absztrakt osztályokat. hogy fokozatosan építsünk fel osztályokat.5. §12.2.4.4. Egy absztrakt osztály virtuális függvényét meghívni semmivel sem kerül többe. amelyekre építhetünk. illetve az új osztályok létrehozását. A virtuális függvények meghívása egyszerû és független attól. Tanácsok [1] Kerüljük a típusmezõk alkalmazását. hogy a világos felületek elkészítésére összpontosíthassunk. illetve egyben a fejlettebb vagy egyedi feladatot végzõ osztályok számára építõkõül szolgálnak. A klasszikus felépítés a tényleges megvalósítást sokszor összekapcsolja a felhasználóknak nyújtott felülettel. §12. amelynek egyes osztályai hasznos szolgáltatásokat kínálnak a felhasználóknak. Ez ügyben az absztrakt osztályok segíthetnek. Klasszikus hierarchiának azt a felépítést nevezzük.2.

amely kiírja az osztály nevét.4.) Ebben a feladatban és a továbbiakban a következõ osztályokat használjuk: Window (Ablak). A képernyõ pontjait az (x. Line (Vonal).m) hozzon létre egy n-szer m méretû területet a képernyõn. hogy minél kevesebbszer kelljen a felhasználói kódot újrafordítani.12.4. hogy a program többféle rendszeren is mûködjön. Hozzunk létre egy-egy ilyen osztályú objektumot és hívjuk meg rájuk az iam() függvényt.4. Gyakorlatok 1.current() – . [7] Használjunk absztrakt osztályokat. [11] Az önálló fogalmakat külön ábrázoljuk. [8] Használjunk absztrakt osztályokat.4.1. Származtatott osztályok 427 [6] Használjunk virtuális függvényeket. mondjuk a * megfelelõ pozícióra való helyezését jelenti.5) Készítsünk egy egyszerû grafikus rendszert a rendelkezésünk álló grafikus felhasználói felület felett. §12. §12. [10] Az absztrakt osztályoknak általában nincs szükségük konstruktorra.2. 12.4. §12. Dot (Képernyõpont). Circle (Kör). §12. akkor legyen virtuális destruktora is. A Window(n. Shape (Alakzat. (*3.1. Idom).3. §12. és az írás a megfelelõ karakter.2. 12. ahol egy pont egy karakterpozíciónak felel meg. 2.1. Point (Pont). [9] Ha egy osztálynak van virtuális függvénye.y) derékszögû (descartes-i) koordináták segítségével címezzük meg. Származtassunk két osztályt a Base-bõl.7. hogy késõbb új megvalósítást készíthessünk a meglevõ felhasználói kód befolyásolása nélkül. akkor készíthetünk egy egyszerû. mindegyiknek legyen egy iam() függvénye. Rectangle (Téglalap).2. Rendeljünk a származtatott osztályok objektumaira hivatkozó mutatókat Base* típusú mutatókhoz és hívjuk meg ezeken keresztül az iam() függvényt. (Ha nincs ilyen vagy nincs tapasztalatunk ilyesmivel. ASCII karakterekbõl felépített megvalósítást. A Window osztályba tartozó w aktuális helye – w. (*1) Ha adott a következõ: class Base { public: virtual void iam() { cout << "Bázisosztály\n". Square (Négyzet) és Triangle (Háromszög). } }.4.

8. 5.5) Gondoskodjunk arról. Ezt a jelenséget gyakran hívják „levágásnak” (clipping). Ezt a Shape::outline() függvény állítsa be. a Shape osztály a Dot. ne (északkelet). A Line(x. E célból – gyakorlatként – ne hagyatkozzunk a felhasznált grafikus felhasználói felületre. Alapértelmezés szerint egy karakter a koordináta-tengelyen minden irányban egy egységnyi helyet foglaljon el. A Point objektumokat egy koordináta-pár adja meg: Point(x. (*2) Határozzunk meg egy függvényt. egy zárt Shape belseje és egy Shape határa. például: w. Háromféle dolog lehet színes: a háttér.c().10)). A Text legyen egy téglalap alakú Shape. s (south – dél). Egy Point nem Shape is egyben. amely karaktereket tud megjeleníteni. Egy Rectangle-t a bal alsó és a jobb felsõ csúcsával adunk meg. A Dot(p)-ként létrehozott Dot egy Point p-t jelent a képernyõn.Point(10. hogy a vonal láthatatlan. A Shape-ek nem láthatók. w (west – nyugat).current(). . 7. szaggatott vagy pontokból álló is. amelyek segítségével meg tudjuk változtatni ezen szakaszok kinézetét. a Rectangle. amely egy házat ábrázol tetõvel.y). a Line. ahol p egy Point. Minden Shape-nek 9 érintkezési pontja van: e (east – kelet). sw (délnyugat) és c (center – középpont). amíg a draw() függvényt meg nem hívjuk. Egyszerû tesztként jelenítsünk meg egy gyermekrajzot. hogy azon pontok és vonalszakaszok. 6.current(). a Circle stb. A pozíciót a w. Rectangle(w. így az arrowhead() függvény paramétere vagy paraméterei ki kell. (*2.5) Egészítsük ki grafikai rendszerünket a Text típussal. A vonal lehessen tömör. 4. amely egy vonal végére egy nyilat rajzol. (*2) Egy Shape fontos részei szakaszokként jelennek meg a képernyõn. amely megtalálja két Shape egymáshoz legközelebbi pontjait és összeköti azokat. kezdetben Point(0. hogy tudják fejezni ezt a négyféle lehetõséget. amelyek kívül esnek egy Window-n.428 Absztrakciós módszerek 3. n (north – észak).0). nw (északnyugat). közös felülete. a Line objektumokat egy Point pár – Line(w.current(p) hívással állíthatjuk be. Ha egy Shape-re alkalmaztuk a draw() függvényt.y. (*3. és egy ajtóval.draw(Circle(w.5) Írjuk meg a Line::arrowhead() függvényt.current().thickness(n) a 0. Adjunk meg olyan mûveleteket. két ablakkal.nw()) például egy vonalat húz az x közepétõl az y bal felsõ sarkához. Az s. az aktuális pozíció a Shape se()-je lesz. se (délkelet). (*2.1. Minthogy egy vonalnak két vége van és a nyíl a vonalhoz képest kétféle irányba mutathat.p2) –.10)). ahol a 2 az alapértelmezett érték és a 0 érték azt jelenti.2. (*3) Vezessük be grafikai rendszerünkbe a szín fogalmát.3 értékek valamelyikére állítsa be a vonalszélességet. ne jelenjenek meg.

Definiáljuk a new_char_vec()-t. Ne írjuk meg az átfedés tényleges megállapítására szolgáló kódot. hogy a két Shape átfedõ-e. A task-oknak kapcsolatot kell tartaniuk egymással. Ehhez szükséges lesz az osztályok megfelelõ (virtuális) függvényekkel való bõvítése. Monster (szörny) és Object (tárgy. el lehet dobni. Az egyes elvégzendõ feladatokat a task osztályból öröklõ osztályok objektumaiként adhassuk meg. amit fel lehet kapni. . (*2) Vegyük az alábbi osztályt: class Char_vec { int sz. határozzuk meg az intersect() függvényt. Erre a célra tervezzünk egy queue osztályt. hogy egybefüggõ memóriaterületet foglalhassunk le egy Char_vec objektum számára.h> fejállományt. char& operator[ ](int i) { return element[i]. hogy ez az ütemezõ a task része vagy önálló osztály lesz-e. (*5) Tervezzünk és írjunk meg egy eseményvezérelt szimulációkat végzõ könyvtárat. amely két Shape* paramétert vesz és a megfelelõ függvények meghívásával megállapítja. Hogyan lehetne egy ilyen könyvtárat használó programban hibakeresést végezni? 12.. a fõ tervezési döntések egyike. hogy a megfelelõ függvényeket hívjuk meg. } // . Ez egy régi program. (*2) Határozzuk meg egy kalandjáték számára a Warrior (harcos). Az. Square és Triangle osztályok. char element[1].5) Ha adottak a Shape osztályból származó Circle. Legyen egy task nevû osztály. (*2. Származtatott osztályok 429 9. A task-ok által végrehajtandó programokat virtuális függvényekkel határozzuk meg. Egy új task számára legyen lehetséges paramétereket megadni konstruktora(i)nak paramétereként. az olvasó jobbat tud írni. Ezt az eljárást angolul általában „double dispatch” vagy „multi-method” néven emlegetik. használni lehet stb. }. Segítség: nézzük meg a <task. Kezeljük a futási idejû hibákat azonos módon. Legyen egy ütemezõ. amely megvalósítja a virtuális idõ fogalmát. public: static Char_vec* new_char_vec(int s). metszi-e egymást. Egy task-nak legyen lehetõsége több forrás felõl érkezõ bemenetre várakozni. A task osztályú objektumok legyenek képesek állapotuk mentésére és visszaállítására (mondjuk a task::save() és a task::restore() függvényekkel). Milyen körülmények között okoz ez a trükk komoly problémákat? 10. 11. Legyen egy task::delay(long) függvény. csak arra ügyeljünk.12. hogy kiegészítõ eljárásként (co-routine) mûködhessenek. így elemeit az element() függvénnyel indexelhetjük. amely „fogyasztja” ezt a virtuális idõt. olyasmi.) osztályok felületét..

ha új fogalmat vezetünk be a hierarchiába. (*3) Vázoljuk az Ival_box példa (§12. a programozás könnyûsége. .és egy megvalósítási hierarchiával.7[2]-ben Point és Dot osztály is? Milyen körülmények között lenne jó ötlet a Shape osztályokat a kulcsosztályok. hogy bemutassák a típuskonverziókból adódó lehetséges problémákat. (*1. és két hierarchiával fogunk rendelkezni: egy felület. például a Line konkrét változataival bõvíteni? 14. és szükség van-e újrafordításra.430 Absztrakciós módszerek 13. Gondoljuk át a következõ szempontokat: a használat könnyûsége. amelyek elég részletesek ahhoz. mennyire könnyû változtatásokat eszközölni a felületekben vagy a megvalósításban.4) egy eltérõ megvalósítási módját: minden. ha változott a rendszerfüggõ elemek megvalósítása. a program által elérhetõ osztály egyszerûen egy mutatót tartalmazzon a megvalósító osztályra. Ilyen módon minden „felületosztály” egy megvalósító osztály leírója (handle) lesz. Írjunk olyan részkódokat. mennyire könnyû a megvalósító osztályok és a felületek újrahasznosítása.5) Miért van a §12.

13 Sablonok „Az Olvasó idézetének helye. Bevezetés Független fogalmakat függetlenül ábrázoljunk és csak szükség esetén használjunk együtt.1. Ha megsértjük ezt az elvet. Stroustrup) Sablonok • Egy karakterlánc sablon • Példányosítás • Sablonparaméterek • Típusellenõrzés • Függvénysablonok • Sablonparaméterek levezetése • Sablonparaméterek meghatározása • Függvénysablonok túlterhelése • Eljárásmód megadása sablonparaméterekkel • Alapértelmezett sablonparaméterek • Specializáció • Öröklõdés és sablonok • Tag sablonok • Konverziók • A forráskód szerkezete • Tanácsok • Gyakorlatok 13. .” (B. Az így létrejövõ osztályok futási idõ és tárigény tekintetében felveszik a versenyt a kézzel írott és egyedibb feladatot végzõ kóddal. hogy általános fogalmak széles körét ábrázoljuk és egyszerû módon használjuk együtt. A sablonok (template) egyszerû módot adnak arra. így kevésbé rugalmas részekbõl vagy összetevõkbõl kell majd a programokat összeállítanunk. akkor vagy nem rokon fogalmakat kapcsolunk össze vagy szükségtelen függéseket teremtünk.

a komplex számok (complex) összeadását. hogy egyazon öröklõdési hierarchia tagjai legyenek.7. azaz a típusoknak paraméterként való használatát.1 és a §3. de a legfõbb mûveleteket is. mint a legtöbb program. a << kimeneti mûveletet.13 pontban vannak. hogy egy osztály vagy függvény definiálásakor egy típust paraméterként adjunk meg. ha valóban szüksége van rájuk.).6 Öröklõdés és sablonok (futási és fordítási idejû többalakúság) §13.7. Ebben a fejezetben a sablonokat úgy mutatjuk be. vagy a rendezést (sort()). amelyeket ténylegesen ki is használ. ostream. Lehetõvé teszik.5 Sablon többféle megvalósítása különbözõ definiciókkal §13.8 pont vezette be. egy lista (list) következõ elemének vételét. Egy adott v-hez a fordítóprogram automatikusan választja ki a legalkalmasabb rendezõ függvényt. A standard könyvtár minden fõbb fogalmat egy-egy sablonként ábrázol (például string. hogy az elsõdleges hangsúly a standard könyvtár tervezéséhez. megvalósításához és használatához szükséges módszerekre esik. A sablon által felhasznált paramétertípusok nem kell. A sort(v) például sokféle tároló objektum tartalmazta sokféle típusú elemnek sokféle rendezõ algoritmusához adhat felületet. list és map). . A standard könyvtár nagyobb mértékû általánosságot. illetve a sablonok formai követelményeire vonatkozó részletes szabályok a §C. Következésképpen ez a fejezet a sablonok fogalmát járja körül és csupán a használatuk alapvetõ módjait bemutató kisebb példákra összpontosít: §13.7 A forráskód szerkezete A sablon (template) fogalmát a §2. hogy rokonságban álljanak egymással. rész) gazdag forrásai a sablonokra és az azokra építõ programozási módszerekre vonatkozó példáknak. Következésképpen a tárgyalandó eljárások széles körben használhatóak és igen sokféle probléma megoldásához biztosítanak hatékony segítséget. A sablonnevek feloldására.4 Általánosított algoritmusok eljárásmódjának megadása sablonparaméterekkel §13. típusok levezetése §13. például a karakterláncok (string-ek) összehasonlítását. A sablon a felhasznált típusnak csak azon tulajdonságaitól függ. függvények túlterhelése.3 Függvénysablonok. rugalmasságot és hatékonyságot követel. így nem szükséges az sem.2 Az osztálysablonok létrehozására és használatára szolgáló alapvetõ eljárások §13. hogy egyszerû felületek mögé kifinomult megvalósításokat rejtsünk és csak akkor „mutassuk be” a bonyolult részleteket a felhasználónak. Ezért aztán e könyvnek az említett könyvtárral foglalkozó fejezetei (a III. A C++ sablonjai lehetõvé teszik.432 Absztrakciós módszerek A sablonok közvetlenül támogatják az általánosított (generikus) programozást (§2. complex.

public: String(). Bevezetése után a C-t ugyanúgy használhatjuk. hogy a template<class C> elõtag azt jelenti. kínai vagy görög stb. hogy C egy típusnév. String<Jchar> js. amely karaktereket tárol és olyan indexelési.2. Ezért ha a §11. hogy egy sablon deklarációja következik és abban a C típusparamétert fogjuk használni. mint bármely más osztálynév: String<char> cs. String(const String&). String<wchar_t> ws. }. ezen kívül nem sok egyébre. Egy egyszerû karakterlánc sablon Vegyünk egy karakterláncot. hogy minél kevésbé függjünk egy adott karakterkészlettõl. String<unsigned char> us. amelyeket rendesen a „karakterlánc” fogalmához kötünk. A karakterlánc definiciója arra épít.. Például az elõjeles és elõjel nélküli. mint bármely más típusnevet.2-beli char-okból felépülõ string osztályban a karakterek típusát paraméterré tesszük. Ezért úgy szeretnénk a karakterlánc fogalmát ábrázolni. általánosabb karakterlánc-osztályt kapunk: template<class C> class String { struct Srep. String(const C*). Sablonok 433 13. Ezeket különféle karakterkészletek számára szeretnénk biztosítani. C read(int i) const.. nem feltétlenül kell osztálynévnek lennie. A string (karakterlánc) olyan osztály. Srep *rep. hogy egy karaktert le lehet másolni. // . A C hatóköre a template<class C> elõtaggal bevezetett deklaráció végéig terjed. A template<class C> elõtag azt jelenti.13. . class Jchar { // japán karakter }. összefûzési és összehasonlítási mûveleteket nyújt. Jegyezzük meg. karakterekbõl álló láncok számos összefüggésben hasznosak lehetnek. Az osztálysablon neve a <> jelpár közé írott típusnévvel együtt egy. a sablon által meghatározott osztály nevét adja és ugyanúgy használható.

A standard könyvtárban a string mint a basic_string<char> szinonimája szerepel: typedef basic_string<char> string. hogy a szószámláló példát így írjuk át: int main() // szavak elõfordulásának megszámlálása a bemeneten { string buf.int> m.12. hogy sablonból létrehozott típusról van szó. A String sablonná tétele lehetõvé teszi. mintha a §11. Ráadásul. Például ha a standard könyvtárbeli map és String sablonokat használjuk. . Ez lehetõvé teszi.434 Absztrakciós módszerek A névre vonatkozó sajátos formai követelményektõl eltekintve a String<char> pontosan ugyanúgy mûködik. map<string. // eredmény kiírása } A Jchar japán karaktereket használó változat ez lenne: int main() // szavak elõfordulásának megszámlálása a bemeneten { String<Jchar> buf. ha nem érdekel bennünket egy típus pontos definiciója. while (cin>>buf) m[buf]++. map<String<Jchar>. akkor egy typedef elrejti elõlünk. hogy a char-okból álló karakterláncok szolgáltatásait más típusú karakterekbõl álló String-ek számára is elérhetõvé tegyük. while (cin>>buf) m[buf]++.8 pont szószámláló példája így írható át: int main() // szavak elõfordulásának megszámlálása a bemeneten { String<char> buf. while (cin>>buf) m[buf]++. map<String<char>.3).12-beli String-definícióval definiáltuk volna.int> m. a §11. §20. // eredmény kiírása } A standard könyvtárban szerepel a sablonná alakított String-hez hasonló basic_string sablon is (§11. // eredmény kiírása } A typedef-ek általában is hasznosak a sablonokból létrehozott osztályok hosszú neveinek lerövidítésére.int> m.

mielõtt megpróbáljuk a viselkedését teljes általánosságában megérteni. de nem feltétlenül jelenti a létrehozott kód mennyiségének csökkenését sem. }. Ha egy ilyen tagot az osztályán kívül írunk le. De ha jobban tetszik.7. kifejezetten sablonként kell megadnunk: template<class C> struct String<C>::Srep { C* s.13. mint a sablonon kívül definiált típusok. A sablon osztályok tagjai maguk is sablonok. A String<C> hatókörén belül a <C>-vel való minõsítés felesleges. ha meg akarunk érteni egy sablont. Általában jó ötlet hibakereséssel ellenõrizni egy osztályt.. valahol máshol is elég.C()). mint egy nem sablon osztálytag esetében (§C. Ezt a fajta hibakeresést (debugging) a legtöbb programozó jól ismeri. Ezáltal számos tervezési hibát. ezért a sablonok használata semmivel sem igényel hosszabb futási idõt. mint a sablon osztályéi. hiszen a sablon neve már tartalmazza azt. // mutató az elemekre int sz. } template<class C> String<C>::String() { rep = new Srep(0. Késõbb aztán anélkül foglalkozhatunk a típus általánosításából esetleg adódó problémákkal. } A sablonparaméterek – mint a C – inkább paraméterek.13. mielõtt sablont készítünk belõle (String<C>).. ugyanúgy. és a legtöbben jobban boldogulnak egy konkrét példával. mint egy egyenértékû „kézzel írott” osztályé.2. paramétereik pedig ugyanazok. például a String-et. akkor hasznos annak viselkedését elõször egy konkrét típusú paraméterrel (például a char-ral) elképzelni.1. Egy sablon osztály (template class) tagjait ugyanúgy deklaráljuk és definiáljuk. de ez nem érinti azt a módot. template<class C> C String<C>::read(int i) const { return rep->s[i]. mint a közönséges osztályokét.). Sablonok 435 13. meg is adhatjuk a minõsítést: . Egy tagot nem szükséges magában az osztályban definiálni. // hivatkozásszámláló // . ahogyan az azokat használó sablonkódot írjuk. // elemek száma int n. hogy a hagyományosabb hibák elvonnák a figyelmünket. Hasonlóan. mint egy elvonttal. Sablonok meghatározása A sablonból létrehozott osztályok teljesen közönséges osztályok. a kódhibáknak pedig a legtöbbjét egy adott osztály összefüggésében kezelhetünk. így a konstruktor neve String<C>::String lesz.

hogy egy sablonnak több változatát is elkészíthessük.7): String<char> cs. hogy minden felhasznált paramétertípus számára létrehozza a megfelelõ sablon függvényt (§C. 13.C()).. melynek során egy sablon osztályból és egy sablonparaméterbõl egy osztálydeklaráció keletkezik. } cs = "Az adott nyelvi változat feladata. */ }.7.13.. // hiba: két meghatározás A sablonparaméterként használt típusnak biztosítania kell a sablon által várt felületet. Ha függvényt hozunk létre egy sablon függvénybõl és egy sablonparaméterbõl. §20. void f() { String<Jchar> js. addig a specializációk (§13.2).. A sablon adott paramétertípus számára megadott változatát specializációnak (specialization) nevezzük.1. hogy egy sablon különbözõ paraméterei öröklõdési viszonyban álljanak egymással.". így ha egy hatókörben már megadtunk egy osztálysablont. Sablonok példányosítása Az eljárást.1).3. De amíg a függvényeket csak túlterhelni lehet (§13. milyen kódot kell létrehozni. */ }. A String sablon paramétereként használt típusnak például támogatnia kell a szokásos másoló mûveleteket (§10. .2.5): template<class T> class String { /* .4.4. class String { /* . hogy kitalálja.).436 Absztrakciós módszerek template<class C> String<C>::String<C>() { rep = new Srep(0. } Egy programban egy tagfüggvényt csak egyetlen függvény definiálhat. az a függvény-példányosítás. Ugyanígy a sablon osztályok tagfüggvényeit is csak egy függvénysablon definiálhatja.. Jegyezzük meg: az nem követelmény.13. Az osztálysablonok neve nem terhelhetõ túl. gyakran sablon-példányosításnak (template instantiation) hívják (§C.2.2. ott nem lehet ugyanolyan néven másik egyedet bevezetni (lásd még §13.5) használata lehetõvé teszi. Általában az adott C++ fordító és nem a programozó dolga.

6.13. int). Egy másik példa erre a Range osztály a §25. egy sablonparamétert felhasználhatunk a további sablonparaméterek meghatározásában is. Nyilvánvaló. Más tagfüggvényeket nem használunk. Ugyanígy a létrehozott függvények is közönséges függvények és a függvényekre vonatkozó szokásos szabályok szerint viselkednek. Sablonok 437 A fenti esetben a String<char> és a String<Jchar>. ahol a futási idejû hatékonyság és a program tömörsége elsõdleges szempont. int sz.2..3). és ahol ezért nem lehet az általánosabb string-et vagy vector-t használni.. közönséges típusú (pl. Az egész típusú paraméterek méretek és korlátok megadásánál hasznosak: template<class T. hogy a sablonok hatékony eszközt adnak arra. és sablon típusú paramétereik (§C. hogy viszonylag rövid definíciókból hozzunk létre kódot.3. A létrehozott osztályok közönséges osztályok. 13. int i> class Buffer { T v[i]. Buffer<char. Ezért aztán nem árt némi óvatosság. a destruktorok és az alapértelmezett konstruktorok. Sablonparaméterek A sablonoknak lehetnek típust meghatározó. Mivel a sablon a méretet paraméterként megkapja. Buffer<Record. */ }. .13. T def_val> class Cont { /* .. Természetesen egy sablonnak több paramétere is lehet: template<class T. }. a megfelelõ Srep típusok.127> cbuf. Ahogy a példa mutatja. A Buffer-hez hasonló egyszerû és korlátozott tárolók ott lehetnek fontosak.8> rbuf. hogy elkerüljük a memóriának csaknem azonos függvény-definiciókkal való elárasztását (§13.1-ben.5). így az osztályokra vonatkozó szokásos szabályok érvényesek rájuk. a kifejtésben el lehet kerülni a szabad tár használatát. public: Buffer() : sz(i) {} // .. illetve a String<char>::operator=(char *) deklarációit a fordító hozza létre. így ilyeneket nem kell készítenie (remélhetõleg nem is teszi).

String<Uchar> s4.2010>-rõl felismeri. a typedef-ek nem vezetnek be új típust. Buffer<char. Típusok egyenértékûsége Ha adott egy sablon.20-10> b3. A mutató. így a String<Uchar> ugyanaz. azok ugyanarra a létrehozott típusra fognak hivatkozni. így a Buffer<char. Buffer<String<char>. A tagra hivatkozó mutatókat &X::of alakban kell megadni.5). Megfordítva. ahol f egy függvény neve. akkor különféle paramétertípusok megadásával különféle típusokat hozhatunk létre belõle: String<char> s1. külsõ szerkesztésû objektum vagy függvény címe (§9. De mit is jelent itt az. illetve f alakú. illetve egy tagra hivatkozó. . 13.4.5). így a paraméter értékének módosítására tett kísérlet hibának számít.2). Karakterlánc literált nem használhatunk sablonparaméterként. a nem típusba tartozó paraméterek a sablonon belül állandók. &of alakú kell. } // hiba: konstans kifejezés szükséges Megfordítva. hogy legyen. hogy a Buffer<char.10> b2.i> bx. Az egész típusú paramétereknek konstansnak kell lenniük: void f(int i) { Buffer<int.3). ahol of a tag neve. String<int> s3. mint a String<unsigned char>. A fordítóprogram ki tudja értékelni a konstans kifejezéseket is (§C. a String<char> és a String<unsigned char> is különbözõek lesznek. String<unsigned char> s2. ha sablon paramétereként akarjuk használni. String<char> s5.5). typedef unsigned char Uchar.10>-zel azonos típus. hogy „azonos”? Szokás szerint.2. mivel a char és az unsigned char különbözõ típusok (§4. Buffer<char. ahol of egy objektum vagy függvény neve. Ha azonos paraméterekkel adunk meg sablonokat.10> b1. túl nem terhelt mutató (§15.438 Absztrakciós módszerek A sablon paramétere lehet konstans kifejezés (§C.

1).print_all(). néha azonban csak késõbb. } . } }. de nem minden „egyszerû” hibát könnyû felderíteni.5. A sablon definiciójában használt névnek vagy ismertnek kell lennie. A List<T>::print_all() példában a cout << p->val kifejezés használata némileg „kifinomultabb” példa.13. Ebben a példában három hibát vétettem (szándékosan). ha a hibák hamar kiderülnek.. void print_all() const { for (Link* p = head. suc(s).o. Link(Link* p. */ }. Link* suc. az o változó (amely persze egy hibásan írt nulla) nem lehet a List<T>::Link konstruktor paramétere. amelyek a konkrét paraméterek ismerete nélkül felderíthetõek: template<class T> class List { struct Link { Link* pre. val(v) { } } // szintaktikus hiba: hiányzik a pontosvesszõ Link* head. const List<Rec>& lr) { li.t)) { } // hiba: 'o' nem definiált azonosító // . p. a használatnál. Hasonlóan.13. A felhasználók jobban szeretik. T val. void f(const List<int>& li..2. lr. A sablondefinícióban a fordítóprogram ellenõrzi a formai hibákat. public: List() : head(7) { } // hiba: kezdeti értékadás mutatónak int-tel List(const T& t) : head(new Link(0. A T sablonparamétertõl való legközönségesebb és legkézenfekvõbb függés egy T típusú tag vagy T típusú paraméter használata. Sablonok 439 13. A sablonparaméterek használatával összefüggõ hibák csak a sablon használatának helyén deríthetõk fel: class Rec { /* . Link* s.8.. illetve az olyanokat. Típusellenõrzés A sablonokat paraméterekkel definiáljuk és késõbb így is használjuk. p=p->suc) cout << p->val << '\n'.print_all(). A fordítóprogram az egyszerû nyelvi hibákat már a definíciónál kiszûrheti.const T& v) : pre(p). mert ilyen név az adott pontról nem elérhetõ.. vagy valamilyen ésszerû és nyilvánvaló módon kell függnie valamelyik sablonparamétertõl (§C. A sablon paraméterétõl függetlenül egy T* típusú mutatónak nem adhatjuk a 7 kezdõértéket.

Ezt a pontot rendszerint elsõ példányosítási pontnak (first point of instantiation) vagy egyszerûen példányosítási pontnak hívják (§C. } // deklaráció // sort(vector<int>&). ) 2 Magyarul: D.2.1). mint a basic_string (§20.size().7). 13. Ha ebben a fordítási egységben a print_all()nak csak a deklarációja és nem a definíciója ismert. ahol a sablonparaméterek használatával összefüggõ hiba kiderülhet.print_all() rendben van. void f(vector<int>& vi. hogy a sablon melyik példányát használjuk. Keresés és rendezés. Mûszaki könyvkiadó. sort(vs). III. E.3). hogy melyik ponton megy végbe.3). a sablonnak az adott paraméterrel való elsõ használata. // sort(vector<string>&).1).7): template<class T> void sort(vector<T>& v) { const size_t n = v. 95.9[16]). Budapest. Nézzük például egy tömb rendezését: template<class T> void sort(vector<T>&).2) vagy a map (§17. a list (§17. o. 84.3. Függvénysablonok A legtöbb programozó számára a sablonok elsõ számú és legnyilvánvalóbb felhasználása olyasféle tároló osztályok létrehozása és használata. a vector (§16. oldal . Knuth: A számítógép-programozás mûvészete III. Természetesen a sablon függvényt valahol definiálnunk kell (§C. kötet. kötet. A legelsõ pont. A sablonparaméterekre vonatkozó megszorításokat a tagfüggvények segítségével is kifejezhetjük (§13. // definíció 2 // Shell rendezés (Knuth. Az adott C++-változat – megengedett módon – ezt az ellenõrzést a program összeszerkesztéséig elhalaszthatja.3.4. lehetséges.440 Absztrakciós módszerek Itt a li. vagyis a sablonparamétereket a függvényparaméterekbõl vezetjük le (deduce) (§13. Késõbb azonban felmerül a sablonként használt függvények szükségessége. függetlenül attól. hogy az adott fordítónak el is kell halasztania a típusellenõrzést a program összeszerkesztéséig (§13.7).13. 1988. de a lr. vector<string>& vs) { sort(vi). A típusellenõrzés azonos szabályok szerint történik.13.print_all() típushibás. mert a Rec típusnak nincs << kimeneti mûvelete. A felhasználók itt is a minél korábbi ellenõrzést szeretik. A sablon függvények (template function) meghívásakor a függvény paraméterei határozzák meg.

A függvénysablonok paraméterei A függvénysablonok alapvetõ fontosságúak a tároló típusok (§2. A fordítóprogram akkor tudja levezetni egy hívás típusos és nem típusba tartozó paramétereit. §3. Nem minden típusnak van azonban < operátora.2. 13. de ez a korlátozás könnyen megkerülhetõ (§13. mert a rendezendõ elemek típusára vonatkozóan több információra támaszkodhat. .. hogy nincs szükség közvetett függvényhívásra.13. Alapvetõ jelentõségû.6. }. ami korlátozza a sort() ezen változatának használhatóságát. } // v[j] és v[j+gap] felcserélése Hasonlítsuk össze a sort() ezen definicióját a §7. Sablonok 441 } for (int gap=n/2. v[j+gap] = temp.7. mellyel az értékcserét természetes formára alakíthatjuk: if (v[j+gap]<v[j]) swap(v[j]. Ebbõl következik. Ez a sablonná alakított változat világosabb és rövidebb. i++) for (int j=i-gap. j-=gap) if (v[j+gap]<v[j]) { T temp = v[j]. 18. v[j] = v[j+gap]. class Record { const char[12]. int i> T& lookup(Buffer<T. // .8). a < összehasonlítást pedig könnyen lehet helyben kifejtve (inline) fordítani. ha a függvény paraméterlistája egyértelmûen azonosítja a sablonparaméterek halmazát (§C.v[j+gap]).8.7-belivel.1. További egyszerûsítést jelenthet a standard könyvtárbeli swap() sablon használata (§18. gap/=2) for (int i=gap.4): template<class T.3. const char* p). 0<gap. hogy egy függvényhíváskor a sablonparamétereket le lehet vezetni. ki lehet következtetni (deduce) a függvény paramétereibõl. Ez a kód hatékonyságát semmilyen módon nem rontja. fejezet) széles körére alkalmazható általános algoritmusok írásához. i<n. mert nincs szüksége az összehasonlító függvényre hivatkozó mutatóra.13. 0<=j..i>& b.4). Valószínûleg gyorsabb is. Ebben a példában a < mûveletet használtuk összehasonlításra.

2 pontbeli make_pair()-t). az i-rõl pedig azt. const char* p) { } return lookup(buf. hogy a fordítóprogram az osztálysablonok paramétereit soha nem vezeti le (§C. hogy az osztályok többféle konstruktora nyújtotta rugalmasság ezt sok esetben megakadályozná. Megjegyzendõ.442 Absztrakciós módszerek Record& f(Buffer<Record. class U> T implicit_cast(U u) { return u.128>& buf.4. U típusa int. hogy a létrehozást egy függvény meghívásával hajtatjuk végre (lásd a §17. template<class T> T* create(). Ha egy levezett típusú objektumot kell létrehoznunk. */ }. az explicit megadott sablonparaméterek közül is csak az utolsókat lehet elhagyni.5). Ennek az az oka. ezt sokszor megtehetjük úgy.double>(i).4).1. sablonparamétere 'int' // függvény.13. sablonparamétere 'int' A közvetlen meghatározás (explicit specification) egyik szokásos használata a sablon függvény visszatérésiérték-típusának megadása: template<class T. esetleg áttekinthetetlenné tenné. hiba: int // nem alakítható char*-ra Az alapértelmezett függvényparaméter-értékekhez hasonlóan (§7.. mint ahogy egy sablon osztály számára közvetlenül megadjuk a sablonparamétereket: template<class T> class vector { /* .5). int* p = create<int>(). hogy értéke128. } // hiba: T nem vezethetõ le // T típusa double. akkor közvetlenül meg kell adnunk. implicit_cast<char. U típusa double // T típusa char*.p).int>(i). // T létrehozása és rá hivatkozó mutató visszaadása void f() { vector<int> v. ahol T egy Record és i értéke 128 Itt T-rõl azt állapítja meg a fordítóprogram.. Ezt ugyanúgy tehetjük meg. . Egy osztály különféle változatai közötti választásra a specializált változatok használata ad eszközt (§13. hogy Record.13.4). implicit_cast<double>(i). } // osztály. Ha egy paramétert nem lehet levezetni a sablon függvény paramétereibõl (§C. // lookup() használata. U típusa int // T típusa char. } void g(int i) { implicit_cast(i). implicit_cast<char*.

static_cast stb. A dynamic_cast. 13.1.5).6) explicit változatai. akkor lehetne-e valamilyen sablonparaméterrel alkalmazni. amelyik a paramétereknek a legjobban megfelel. Az automatikus (implicit) konverziók (§C.13.3. . // sqrt<int>(int) sqrt(2. Ehhez az összes függvénysablont megvizsgáljuk. például az implicit_cast() idõnként igen hasznosak lehetnek.5.2).0).13. §C. akkor a következõ lépésekben csak azt vesszük figyelembe. 2. // sqrt<double>(complex<double>) } Ahogy a sablon függvény fogalma a függvény fogalmának általánosítása.1). amelyeket nem fejezhetünk ki más nyelvi elemmel. void f(complex<double> z) { sqrt(2). formai követelményei megfelelnek az explicit minõsítésû sablon függvényekéinek. A módszer alapvetõen a következõ: megkeressük minden sablonhoz azt a specializált változatot.2. Az sqrt(z) hívás esetében például a következõ jelöltek adódnak: sqrt<double>(complex<double>) és sqrt<complex<double>>(complex<double>).13. // sqrt(double) sqrt(z). §C. hogy ha más ugyanilyen nevû függvény vagy sablon függvény nem lenne elérhetõ. Függvénysablonok túlterhelése Azonos néven több függvénysablon is szerepelhet. Sablonok 443 A sablonparaméterek közvetlen megadása átalakító függvénycsaládok és objektum-létrehozó függvények definicióját teszi lehetõvé (§13.2. double sqrt(double).3. sõt ugyanolyan néven több közönséges függvény is. Ha két sablon függvény is meghívható lenne és az egyik specializáltabb a másiknál (§13. Meg kell keresni azokat a specializált sablon függvény változatokat (§13. A beépített típuskonverziós operátorok azonban olyan mûveleteket támogatnak. Ezután ezekre a példányokra és az összes közönséges függvényre is a szokásos túlterhelés-feloldási szabályokat alkalmazzuk: 1. template<class T> complex<T> sqrt(complex<T>). amelyek részt fognak venni a túlterhelés feloldásában. A túlterhelt (vagyis azonos névvel mást és mást jelentõ) függvények meghívásakor a megfelelõ meghívandó függvény vagy függvénysablon kiválasztásához a túlterhelés (overloading) feloldása szükséges: template<class T> T sqrt(T). ugyanúgy a sablon függvényekre alkalmazandó túlterhelés-feloldási szabályok is a függvényekre alkalmazandó túlterhelés-feloldási szabályok általánosításai.2.

9).4). max(s. Emiatt a sqrt(2.T). Ha több ugyanolyan mértékben megfelelõ függvényt is találunk.7.9) // max<int>(int(s). Ezek után végezzük el a közönséges túlterhelés-feloldást ezen függvényekre és a közönséges függvényekre (§7. max<double>(2. Az sqrt(2) hívás pontosan megfelel az sqrt<int>(int)-nek. max('a'.7.4. int i) { return max<double>(d. int j) { return max<int>(i.d2).4. így azt választjuk a sqrt(double) helyett. 5. akkor arra nem alkalmazhatunk kiterjesztést (promotion).2) // max<char>('a'. } inline double max(double d. akkor a hívás többértelmû és ezért hibás: template<class T> T max(T.i). max(2. Ha nem találunk megfelelõ függvényt. megfelel sqrt<T>(T)-nek is.7) (egyszerû konverzió) // hiba: többértelmû (nincs szabványos konverzió) // hiba: többértelmû (nincs szabványos konverzió) } A fenti példa két nem egyértelmû hívását explicit minõsítéssel oldhatjuk fel: void f() { max<int>('a'.7).1). max('a'.1) // max<double>(2.1). ami megfelel sqrt<T>(complex<T>)-nek. Ha egy függvény és egy specializált változata ugyanolyan mértékben megfelelõ.). max(2.3. // max<int>(1.7.1). } inline double max(int i. Ha egy sablon függvény paraméterét a sablonparaméterekbõl vezettük le (§13. } .'b'). void k() { max(1.j). double d) { return max<double>(i. } inline double max(double d1. 3. 4.'b') // max<double>(2. illetve szabványos vagy felhasználói konverziót.d).444 Absztrakciós módszerek Az sqrt(z) hívás esetében az sqrt<double>(complex<double>)-t választjuk az sqrt<complex<double>>(complex<double>) helyett: minden hívás.4. double d2) { return max<double>(d1.0)-hoz a sqrt(double)-t választjuk.4).7. akkor a hívás hibás. akkor a függvényt választjuk. } // max<int>(int('a'). const int s = 7.double(4)) Vagy megfelelõ deklarációk alkalmazásával: inline int max(int i. és nem a sqrt<double>(double)-t.2).7.

a második paraméter azonban teljesen közönséges.1). amelyek nem vesznek részt a sablonparaméter levezetésében.. az elsõ paraméterre nem alkalmazható konverzió. template<class T> class D : public B<T> { /* . pontosan úgy kezelhetjük. így a szokásos átkonverziók megengedettek: template<class T. Minthogy C-t a hívás aktuális paraméterébõl kell levezetni.double(4)) Közönséges függvényekre a közönséges túlterhelés-feloldási szabályok érvényesek (§7. // az n-edik elem Ez a függvény feltételezhetõen a C típusú tároló n-edik elemét adja vissza. Egy D<int>* típusú paraméterünk van.. template<class T> void f(B<T>*). // f<int>(static_cast<B<int>*>(pd)). A max() függvény igen egyszerû. */ }. de a sablon specializált használata könnyû és általánosan használható módja az ilyen túlterhelés-feloldó függvények írásának.4). max(2. Az olyan függvényparamétereket..7. int n).. mint egy nem sablon függvény paraméterét. hogy a sablon függvények helyesen mûködnek együtt az öröklõdéssel: template<class T> class B { /* . . így a fordítóprogram könnyen jut arra a következtetésre. így a szokásos konverziók mindegyike tekintetbe vehetõ: class Index { public: operator int(). hogy a hívás nem jár külön „költséggel”. így explicit módon is írhattuk volna. } // max(int('a'). // . void g(B<int>* pb.4). hogy T-t int-nek véve a hívás egyértelmûen feloldható. // f<int>(pb) f(pd). class C> T get_nth(C& p.. és a helyben kifejtés (inline) biztosítja.. */ }. D<int>* pd) { f(pb). szabványos átalakítás D<int>*-ról B<int>*-ra } Ebben a példában az f() sablon függvény minden T típusra elfogadja B<T>*-ot.7. Sablonok 445 void g() { max('a'.1) // max(2.13. }. A túlterhelés-feloldási szabályok biztosítják. f(B<int>*)-ként.

short s. // szabványos konverzió: short-ról int-re int i3 = get_nth<int>(v.str2[i])) return C::lt(str1[i]. Nem „betonozhatjuk be” a rendezési elvet a tárolóba.) eljárást leírhatunk. const String<T>& str2) { for(int i=0. Ehelyett a megfelelõ mûvelet végrehajtásakor kell megadni az alkalmazandó feltételeket. Index i) { int i1 = get_nth<int>(v.i). ezért bármely általános megoldás megköveteli.length()-str2. sem egy általános rendezõ algoritmus nem tudhat a nevek rendezésének „svéd szokásairól”. hogy a rendezõ eljárást ne csak egy adott típusra adhassuk meg. mire van szüksége az elemek típusával kapcsolatban. Ezzel minden (összehasonlító. } Ha valaki azt szeretné. hogyan rendezhetjük a karakterláncokat. Általánosítsuk például a C standard könyvtárának strcmp() függvényét tetszõleges T típusból álló String-ekre (§13.length() && i< str2. hanem adott típusra való adott alkalmazáskor is. ha az a tároló és a „C-mûveletek” nyelvén megfogalmazható: . helyi sajátosságok). // felhasználói konverzió: Index-rõl int-re } 13.2). i++) if (!C::eq(str1[i].és nagybetûk közötti különbségtõl vagy figyelembe vegye a program nyelvi környezetét (locale. Három dolog játszik szerepet: a karakterlánc. az elemek típusa és a lánc elemeinek összehasonlításakor alkalmazott szempont.4.length().446 Absztrakciós módszerek void f(vector<int>& v.length(). de az elemek típusába sem. Milyen rendezési elvet alkalmazzunk.2): template<class T. Eljárásmód megadása sablonparaméterekkel Gondoljuk meg. Természetesen sem egy általános string típus. hogy a compare() eltekintsen a kis. ha például svéd neveket tartalmazó karakterláncokat akarunk rendezni? A svéd nevek rendezése számára a karakterek két különbözõ numerikus megfeleltetési módja (collating sequence) használatos. mert az elemeket sokféle módon rendezhetjük. return str1. rendezõ stb. mert az általában nem szabhatja meg.str2[i]) ? -1 : 1. i<str1.s). akkor ezt a C::eq() és a C::lt() függvények megfelelõ definiálásával teheti meg. // pontos illeszkedés int i2 = get_nth<int>(v. class C> int compare(const String<T>& str1.

T b) { return a==b. class Literate { // svéd nevek összehasonlítása public: static int eq(char a. hogy csak a szokásostól eltérõ összehasonlítási szempontot kelljen megadni: . } Az összehasonlító mûveletek sablonparaméterként való megadásának két jelentõs elõnye van az egyéb lehetõségekhez. Ez alapvetõ fontosságú feltétele annak. String<char> swede2) { compare< char. 13. Minden osztálysablonból létrehozott osztály saját példányokat kap a sablon statikus változóiból (§C.Literate >(swede1. Alapértelmezett sablonparaméterek Fáradságos dolog minden egyes hívásnál közvetlenül meghatározni az összehasonlítási feltételeket. Természetesen összehasonlító mûveleteket nemcsak a beépített. hogy általános algoritmusokat olyan típusokra alkalmazhassunk. a futási idõ növekedése nélkül. hanem a felhasználói típusokra is megadhatunk.13. például a függvénymutatók alkalmazásához képest. Sablonok 447 template<class T> class Cmp { // szokásos.1). } }. az eq() és az lt() összehasonlító mûveleteket könnyû helyben kifejtve (inline) fordítani. char b) { return a==b. A sablonparaméterek megadásakor most már pontosan megadhatjuk az összehasonlítási szabályokat: void f(String<char> swede1.swede2). T b) { return a<b. Túlterheléssel szerencsére könnyen megadhatunk olyan alapértelmezést.4. míg egy függvénymutatón keresztüli hívás ilyen módú fordítása különleges mértékû figyelmet követel a fordítóprogramtól. compare< char. } static int lt(char.9[14]) }.Cmp<char> >(swede1. Egyrészt több mûvelet megadható egyetlen paraméterként.swede2). // kikeresés táblázatból karakterérték alapján (§13. alapértelmezett összehasonlítás public: static int eq(T a.4).char). amelyeknek nem maguktól értetõdõ összehasonlítási feltételeik vannak (§18. } static int lt(T a. Másrészt.1.13.

compare<char. String<char> s2) { compare(s1.No_case>(s1. 20. const String<T>& str2) { for(int i=0. i++) if (!C::eq(str1[i].length(). §19. // összehasonlítás C // használatával // összehasonlítás // Cmp<T> használatával De a szokásos rendezést megadhatjuk.448 Absztrakciós módszerek template<class T. } // kisbetû-nagybetû különbözik // kisbetû-nagybetû nem különbözik A standard könyvtár széles körben alkalmazza azt a módszert.Literate>(swede1. fejezet) összehasonlításaira ez nem áll.és nagybetûk közötti különbséget figyelembe vevõ.str2[i])) return C::lt(str1[i]. compare<char. Eléggé furcsa módon azonban a basic_string (§13. a standard könyvtárbeli tárolók pedig a memóriafoglalókét (allokátor.length()-str2. */ }. . } // Cmp<char> használata // Literate használata Egy (nem svédek számára) kevésbé ezoterikus példa a kis. class C> int compare(const String<T>& str1. } Így már leírhatjuk a következõt: void f(String<char> swede1. String<char> swede2) { compare(swede1.length(). és ennek a legáltalánosabb eljárásmód az alapértelmezett értéke (például §18.2. i<str1.2.2. const String<T>& str2). return str1.length() && i< str2.4). Az eljárásmódot kifejezõ sablonparamétereket gyakran nevezik „jellemvonásoknak” (traits) is.s2).s2). mint alapértelmezett sablonparaméter-értéket is: template<class T.swede2).4. class C = Cmp<T> > int compare(const String<T>& str1.swede2).2).str2[i]) ? -1 : 1. const String<T>& str2). hogy az alkalmazandó eljárásmódot (policy) egy sablonparaméter adja meg. a szabványos algoritmusok a bejárók (iterátorok) jellemvonásait (§19. Például a standard könyvtárbeli string a char_traits-re épül (§20. template<class T> int compare(const String<T>& str1. void f(String<char> s1. illetve elhanyagoló rendezés: class No_case { /* ..) használják fel..1).

Sok hasonló tervezési szempontot figyelembe lehet úgy venni. Vector<Node*> vpn.13.3. . Sablonok 449 Egy alapértelmezett sablonparaméter értelmi ellenõrzése ott és csak akkor történik meg.5. Specializáció Alapértelmezés szerint egy sablon (template) egyetlen definíciót ad a felhasználható által elképzelhetõ összes paraméterérték (vagy paraméterértékek) számára. void swap(Vector&). vagy hogy „hiba.. hogy „ha a sablonparaméter egy mutató. Vegyük egy Vector sablon valószínû felhasználásait: template<class T> class Vector { T* v. public: Vector(). Vector<char*> vpc. Vector<string> vs. // általános vektortípus }. használd azt”. Ez azonban nem minden sablon írásakor kedvezõ. Vector<Shape*> vps. szakosítás) hívjuk. akkor olyan X típusokra is használhatjuk a compare()-t. amelyekre a fordító nem fordítaná le aCmp<X>-et. } T& operator[ ](int i). Vector<int> vi. Így ha nem használjuk fel az alapértelmezett Cmp<T> paramétert. hiszen ezek sablonparamétert használnak az alapértelmezett értékek megadására (§16. egyedi célú felhasználói változatok használata. int sz. Elõfordulhat. ha nem.4).. hogy a sablonnak többféle definíciót adunk és a fordítóprogram az alkalmazott paramétertípusok szerint választ közülük. Vector(int). // . hogy olyasmit szeretnénk kifejezni. mert mondjuk az X-re a < nem értelmezett. A sablon ilyenféle többszörös meghatározását specializációnak (specialization. használd ezt. ahol és amikor az alapértelmezett paramétert ténylegesen felhasználjuk. ha a sablonparaméter nem a My_base osztály egy leszármazottjára hivatkozó mutató”. T& elem(int i) { return v[i]. 13. Ez döntõ jelentõségû a szabványos tárolók tervezésénél.

void*& operator[ ](int i). ami mutatókat tartalmazó Vector-ok. }.4. Ezért aki objektumorientált programozást folytat és típusbiztos. mert a többalakú (polymorph) viselkedés megõrzése céljából mutatókat kell használnunk (§2. azaz ezen változat használatakor nincs sablonparaméter. Ezt a változatot aztán az összes. amelyikre a T típusa void*. Ez jó a végrehajtási sebesség szempontjából. hogy ezt a definíciót kell minden olyan Vector esetében használni. és csak azok esetén használandó. de fõleg azért. Szerencsére létezik egyszerû megoldás.5. Több okból is. az biztosan számos mutatót tartalmazó tárolót fog használni. Ezt specializált változat készítésével érhetjük el. // .2.6). hogy ennél a specializált változatnál nem kell sablonparamétert megadnunk. részleges specializációra van szükségünk: template<class T> class Vector<T*> : private Vector<void*> { public: typedef Vector<void*> Base. például standard könyvtárbeli tárolókat használ.. A template<> elõtag azt jelenti. A mutatókat tartalmazó tárolóknak elég egyetlen megvalósítás. a Vector<void*>-ot a következõ módon deklarált Vector-ok számára használjuk: Vector<void*> vpv. Elõször definiáljuk a Vectornak a void mutatókra vonatkozó változatát („specializációját”): template<> class Vector<void*> { void** p. §12. hogy milyen típusú sablonparaméterre használjuk. a név utáni <> jelpár között adjuk meg: vagyis a <void*> azt jelenti. A Vector<void*> egy teljes specializáció. Vector() : Base() {} explicit Vector(int i) : Base(i) {} .450 Absztrakciós módszerek A legtöbb Vector valamilyen mutatótípus Vector-a lesz.. Azt. Ha olyan változatot akarunk megadni. amit meg kellene adni vagy le kellene vezetni. A legtöbb C++-változat alapértelmezés szerint lemásolja a sablon függvények kódját. de kritikus esetekben (mint az iménti Vector-nál) a kód „felfúvódásával” jár. mutatót tartalmazó vektor közös megvalósításaként használhatjuk.

Az általános sablont az összes specializált változat elõtt kell megadni: template<class T> class List<T*> { /* . Az összes mutatót tartalmazó lista egyetlen specializált változattal való megvalósítása jó példa arra.. template<class T> class List { /* . hogyan lehet a kódfelfúvódást megakadályozni úgy... */ }. // . Ez a módszer a gyakorlatban a kód felfúvódásának megakadályozásában volt sikeres. hogy ezt a változatot kell minden mutatótípus esetében használni.13. Ha adott a Vector ezen részlegesen specializált változata. a sablonparaméter nem egyszerûen az aktuális sablonparaméter. így T is Shape // <T*> most <int**>. A specializáció a közös felület többféle meghatározásának eszköze. Akik nem alkalmaznak ilyen módszereket (akár a C++-ban.. melyet kizárólag az öröklõdés és a helyben kifejtés eszközével valósítottuk meg. } }. Így például a Vector<Shape*> esetében T típusa Shape és nem Shape*. A Vector<T*> osztály egyszerûen egy felület a void*-os változathoz. így T is int* Jegyezzük meg. // <T*> most <Shape*>. */ }. Természetesen az általános Vector-t és a mutatókra vonatkozó változatot hívhattuk volna különbözõképpen is. Amikor ezt kipróbáltam. mégsem a mutatós változatot használta és a kapott kód a vártnál sokkal nagyobb lett. akinek tudnia kellett volna róla. } T*& operator[ ](int i) { return reinterpret_cast<T*&>(Base::operator[ ](i)). akár egyéb típus-paraméterezési lehetõségeket tartalmazó nyelvekben). hogy a közös kódot a lehetõ legjobban növeljük. kiderült... hogy sok felhasználó. A név utáni <T*> specializáló minta azt jelzi. Ebben az esetben sokkal jobb a fontos részleteket egy közös felület mögé rejteni. // hiba: általános sablon a specializált után . könnyen azt vehetik észre. Fontos. A vektormûveletek különbözõ változatainak lefordításához szükséges idõ megtakarításával ez a módszer a fordítási és szerkesztési idõt is drámai módon csökkenti. hogy az ismétlõdõ kód közepes méretû programok esetében is megabájtokra rúghat. hogy a Vector ezen finomítása a felhasználói felület megváltoztatása nélkül történt. ami T* alakba írható: Vector<Shape*> vps. azaz minden olyan sablonparaméternél. hogy részleges specializáció használata esetén a sablonparaméter a specializációra használt mintából adódik. Vector<int**> vppi. Sablonok 451 T*& elem(int i) { return reinterpret_cast<T*&>(Base::elem(i)). akkor ez az összes mutatótípusra vonatkozó Vector közös megvalósítása.

1.. Ha használjuk is. hogy legyen: template<class T> class List { /* . a Vector<void*> paramétereként pedig csak void* mutatót. illeszkedik a másikra is. template<class T> class List<T*> { /* . (§13.5.2) a leginkább specializált változat részesül elõnyben. de a Vector<T*> paramétereként csak mutatót. mint magát a sablont. List<int*> li. Vagyis a specializált változat explicit megadásából következik. template<class T> class Vector<T*>. template<> class Vector<void*>. akkor az általános sablont valahol definiálnunk kell (§13.7). // hiba Itt a List-et az int*-ra a List<int*> használata után specializálunk.. akkor annak deklarációja a specializált használat minden helyérõl elérhetõ kell. Egy sablon minden specializált változatát ugyanabban a névtérben kell megadni.3...5) deklarációjában. akkor egy explicit deklarált (azaz nem egy általánosabból létrehozott) sablonnak valahol szintén explicit definiáltnak kell lennie (§13.452 Absztrakciós módszerek Az általános sablon által adott létfontosságú információ az. Ezért elég az általános sablont a specializált változat deklarációja vagy definiciója elõtt megadni: template<class T> class List. // általános // mutatókhoz // void*-okhoz Minden típust használhatunk a legáltalánosabb Vector paramétereként. */ }. . 13. illetve a túlterhelés feloldásakor (§13. Ha használják. */ }.. Ha valahol szerepel egy felhasználói specializált változat. Az objektumok és mutatók stb.. template<class T> class List<T*> { /* .7). hogy számára a fordító nem hoz létre definíciót. hogy milyen paramétereket kell a felhasználás vagy a specializáció során megadni. Ez fordítva nem áll fenn: template<class T> class Vector. ha minden olyan paraméterlista. amely illeszkedik az egyikre. */ }. Specializációk sorrendje Az egyik változat specializáltabb egy másiknál.

Sablonok 453 A specializáló minta megadásánál a sablonparaméterek levezetésénél (§13.v[j])) swap(v[j].2. const char*-ra vonatkozó specializációja fog ügyelni: template<> bool less<const char*>(const char* a.size(). } template<class T> void sort(Vector<T>& v) { const size_t n = v.7 és §13. mert két char*-ot a < segítségével hasonlít össze. i++) for (int j=i-gap. 0<gap. azaz az elsõ karakter címét fogja az összehasonlítás alapjául venni. Erre a less()-nek egy egyszerû. Minthogy a sablonparaméter közvetlenül levezethetõ a függvény paraméterlistájából. 13.b)<0. Ehelyett a mutatott karakterek szerinti összehasonlítást szeretnénk. Függvénysablon specializáció A specializáció természetesen a sablon függvényeknél (template function) is hasznos.3. Ez az elemeket a < segítségével hasonlítja össze és a részletezett kód segítségével cseréli fel. 0<=j. amikor a sablonparaméter const char*. i<n. const char* b) { return strcmp(a. de lehetõséget nyújt a megvalósítás javítására. } Ez nem javítja magát az algoritmust.3 példabeli Shell rendezést.5).5. hogy ez egy sablonparaméter megadása nélküli specializált változat.13. A sablon függvény neve utáni <const char*> azt jelenti.b)<0. for (int gap=n/2. T b) { return a<b. Vegyük a §7. Eredeti formájában egy Vector<char*>-ot nem rendez jól. j-=gap) if (less(v[j+gap]. nem kell explicit megadnunk. Így egyszerûsíthetünk a specializáció megadásán: template<> bool less<>(const char* a.v[j+gap]). hogy a függvényt azokban az esetekben kell alkalmazni. a <> sablon-elõtag azt jelzi. gap/=2) for (int i=gap. Jobb definíció lenne a következõ: template<class T> bool less(T a.1) használt típusokból összeállított típusok használhatók fel. } . } Mint az osztályoknál is (§13. const char* b) { return strcmp(a.

// y másolása x-be y = t. ha a paramétertípus valamilyen szabálytalansága miatt a szabványos algoritmus valamilyen nem kívánt módon mûködne (mint a less() esetében).b)<0. hiszen nem hordoz új információt.a.9. Ezenkívül akkor is hasznos. De ezt is megoldhatjuk megfelelõ specializációval. . Vegyük a swap() kézenfekvõ meghatározását: template<class T> void swap(T& x. // ideiglenes változó másolása y-ba } Ez nem túl hatékony. const char* b) { return strcmp(a.12. // x másolása az ideiglenes változóba x = y. Így aztán általában így írnánk: template<> bool less(const char* a.sz). üres <> felesleges.3.3. } A less() és a swap() ezen változatait használja a standard könyvtár (§16. } Én jobban kedvelem ezt a rövidebb deklarációs formát. T& y) { T t = x. Maga a Vector objektum csak annyi adatot tartalmaz.a. adjunk meg egy swap() függvényt a Vector osztály számára (§13. ha a sablonparaméterek egy adott halmazára egy általános algoritmusnál létezik hatékonyabb megvalósítás (itt a swap()). az összes elem másolásával cseréli fel a Vector-okat.v).454 Absztrakciós módszerek Minthogy adott a template<> elõtag. Így aztán a cserét a megfelelõ ábrázoló adatok cseréjével lehet megoldani. §13.5): template<class T> void Vector<T>::swap(Vector & a) { swap(v. A specializáció akkor hasznos.16) is. Hogy lehetõvé tegyük az ábrázoló adatok kezelését. Ráadásul ezek a példák széles körben használt módszereket mutatnak be. ha Vector-ok vektoraira hívjuk meg. §20.2). Vector<T>& b) { a. hogy az elemeihez való közvetett hozzáférést támogassa (mint a string §11.swap(b). swap(sz. } // ábrázolások cseréje A swap() tagot felhasználhatjuk az általános swap() specializált definiciójában: template<class T> void swap(Vector<T>& a. a második.és tömbtípusok. Ezek a „szabálytalan” típusok többnyire a beépített mutató.

// . */ }.5 pontbeli vektor jó példa erre: template<class T> class List<T*> : private List<void*> { /* . template<class T> class Vec : public vector<T> { /* . amelyekkel meglevõk alapján új típusok építhetõk.... hogy sablonok egy halmaza közös megvalósításon osztozzék. Természetesen a sablon osztályok közötti öröklõdés is hasznos.7. Ha a bázisosztály adatai vagy mûveletei valamely származtatott osztály sablonparaméterétõl függenek. és amelyekkel általánosságban a közös vonások különféle kihasználásával hasznos kód írható. // hozzáférés biztosítása C mûveleteihez const C& derived() const { return static_cast<const C&>(*this). Mint a §3. ezen eszközök párosítása sok hasznos módszer alapja. // minden elem összehasonlítása bool operator!=(const C&) const. A §13. §3.7. A bázisosztály egyik haszna az. */ }. Egy sablon osztálynak (template class) egy nem sablon osztályból való származtatása módot nyújt arra.8. Sablonok 455 13. amikor ugyanaz a sablonparaméter szerepel a bázis.. Érdekes. Az az eset a leggyakoribb.2).13.5 pontokban láttuk. hogy további osztályok számára építõkõül szolgál.3. */ }. . } }.6. akkor magának a bázisosztálynak is paramétereket kell adni (lásd például a §3.5 és §13. A sablon függvények túlterhelés-feloldási szabályai biztosítják a függvények helyes mûködését az ilyen származtatott típusokra (§13.. de ez nem szükségszerû.és a származtatott osztályban is.. Öröklõdés és sablonok Az öröklõdés és a sablonok olyan eszközök. hogy a sablon elegáns és típusbiztos felületet ad egy másként nem biztonságosan és nem elegánsan használható eszközhöz. hogy magát a származtatott osztályt adjuk át a bázisosztálynak: template <class C> class Basic_ops { // tárolók alapmûveletei public: bool operator==(const C&) const.1.. bár ritkábban alkalmazott módszerek alapulnak azon. Másként nézve a példa azt mutatja.2 pontbeli Vec osztályt): template<class T> class vector { /* ..

My_array<double> > mc.. friend bool operator==(const Mcontainer&. // . i<derived()... hogy a Math_container egy hagyományos vektorra hasonlít. Ám mivel az olyan mûveletek definíciójához.. public: T& operator[ ](size_t i) { return elements[i]. mint az == és a != mind a tárolónak. a bázisosztályt át kell adni a tárolósablonnak. mind annak elemeinek típusára szükség van. }. const Mcontainer&). T& operator[ ](size_t).456 Absztrakciós módszerek template<class T> class Math_container : public Basic_ops< Math_container<T> > { public: size_t size() const. */ }. // elemek // összehasonlítása }. egyszeri meghatározása. ++i) if (derived()[i] != a[i]) return false. Mcontainer< double.size(). return true.. class C> class Mcontainer { C elements.. const T& operator[ ](size_t) const.size() != a. for (int i = 0.. template<class T> class My_array { /* . Ha feltesszük. // .. . Ezáltal lehetõvé válik a tárolók alapvetõ mûveleteinek az egyes tárolóktól elkülönített. } A tárolók és a mûveletek elválasztására szolgáló másik módszer öröklõdés helyett sablonparaméterekkel kapcsolja össze azokat: template<class T.. // . friend bool operator!=(const Mcontainer&. const Mcontainer&). } .size()) return false. akkor a Basic_ops egy tagja valahogy így nézhet ki: template <class C> bool Basic_ops<C>::operator==(const C& a) const { if (derived().

Hogy megkülönböztessük õket. azaz a mûveletek helyben kifejtve történõ fordíthatósága alapvetõ szempont.real()). és az absztrakt osztályt használó kód legnagyobb része nem is függ a megvalósítástól. 13.1 pont ír. public: template<class T> complex(const complex<T>& c) : re(c.2. Sablonok 457 Egy sablonból létrehozott osztály teljesen közönséges osztály. Minthogy mindkettõ azt teszi lehetõvé. Ha a futási idõ nagyon fontos. 13.3. néha mindkettõt többalakúságnak (polimorfizmus. A sablon kódja minden paramétertípus esetében azonos.. A tervezés szempontjából a két megközelítés annyira közeli. akkor legjobban egy közös absztrakt osztályból öröklõdõ osztályként ábrázolhatjuk azokat.6. Ebben a példában azért használtam barátokat. akkor legyenek sablonparaméterek.13. polymorphism) hívják.2). im.6. a sablonok által nyújtottat pedig fordítási idejû (compile-time) többalakúságnak vagy paraméteres (parametric) többalakúságnak hívják.. amelyeknek azonos mûveleteik vannak. a virtuális függvények által biztosítottat futási idejû (run-time) többalakúságnak. Ha nem szükséges egymással alá.4. . amelyek maguk is sablonok: template<class Scalar> class complex { Scalar re. Tag sablonok Egy osztálynak vagy osztálysablonnak lehetnek olyan tagjai is.1. Ha az objektumok aktuális típusa a fordítási idõben nem ismert. hogy a C paraméterként tároló helyett sablont használjunk. Ilyen esetekben meg lehetne fontolni azt is. }.2) is. im(c.és fölérendeltségi viszonyban állniuk. Az absztrakt osztályok felületeket írnak le. hogy a == és a != számára a két paraméter szokásos felcserélhetõségét biztosítsam (§11. használjunk sablont. csakúgy. Errõl részletesebben a §24.13. hogy közös nevet érdemel. mint a sablont használó legtöbb kódrész. hogy egy algoritmust egyszer írjunk le és aztán különféle típusokra alkalmazzuk. különféle megvalósításaik kódrészeit az osztályhierarchiák közösen használhatják. ezért lehetnek barát (friend) függvényei (§C. Paraméterezés és öröklõdés A sablonok egy típust vagy függvényt egy másik típussal paramétereznek.imag()) { } // . Végül is mikor válasszunk absztrakt osztályt és mikor alkalmazzunk sablont? Mindkét esetben olyan objektumokat kezelünk.

így közvetlenül deklarált másoló konstruktor híján a fordító alapértelmezett konstruktort hozott volna létre. complex<Quad> cq. A végrehajtási idõben a csonkításból eredõ hibákat el lehetne kapni.458 Absztrakciós módszerek complex<float> cf(0.6.. // hiba: virtuális sablon Ez szabálytalan. Elég különös módon egy sablon konstruktorból a fordító soha nem hoz létre másoló konstruktort. Sajnos azonban a C++ nyelv elfogad a beépített típusok közötti bizonyos ésszerûtlen konverziókat is.. ha T1-nek kezdõértékül adhatjuk T2-t.2. . }. }. implicit_cast (§13. im(checked_cast<Scalar>(c. A szerkesztõnek az intersect() függvény minden új paramétertípussal történõ meghívása esetén egy újabb elemmel kellene bõvítenie a Shape osztály virtuálisfüggvény-tábláját. ami ebben az esetben azonos lett volna az általam megadottal.5) nem lenne alkalmazható. complex<int> ci = cq. Ha megengedett lenne.imag())) { } // .real()).real())). class Quad { }.0). // rendben: float-ról double-ra alakítás használata // nincs átalakítás int-re // hiba: nincs átalakítás Quad-ról int-re Vagyis kizárólag akkor tudunk complex<T2>-bõl complex<T1>-et építeni.3. akkor a virtuális függvények megvalósításának hagyományos virtuálisfüggvény-táblás módja (§2. például double-ról int-re.1) vagy checked (§C. Egy sablon tag nem lehet virtuális: class Shape { // .. template<class T> virtual bool intersect(const T&) const =0.5.2) stílusú ellenõrzött konverzióval: template<class Scalar> class complex { Scalar re. ami ésszerû megszorításnak tûnik. im(c. im(0) { } complex(const complex<Scalar>& c) : re(c. complex<double> cd = cf. A teljesség kedvéért alapértelmezett és másoló konstruktort is megadtam.6.imag()) { } template<class T2> complex(const complex<T2>& c) : re(checked_cast<Scalar>(c. public: complex() : re(0).. im.

amely szükség esetén a felhasználó elõírásai szerinti típusokat hoz létre. tehát a Circle-ök halmazát Shape-ek halmazaként is kezelhetem” érvelés „tehát” pontja hibás. mint új típusok létrehozását segítõ általánosításra. Ha megengednénk a set<Circle*>-öknek set<Shape*>-ként való kezelését.. hogy a set<Circle*> elemei Circle-ök. s. míg a Shape-ek halmaza esetében ez egyáltalán nem biztos: class Triangle : public Shape { /* .. A C++ nyelv szabályai szerint két. így a Circle-ök halmaza egyben Shape-ek halmaza is. Ilyenkor egyesek megpróbálják a set<Circle*>-ot set<Shape*>-ként kezelni.13.. Például az f() függvény egy Triangle* elemet tesz set<Shape*> paraméterébe. lehetõvé teszi. hogy az elemekre biztonságosan és hatékonyan végezzünk Circle-ökre jellemzõ mûveleteket. class Circle : public Shape { /* . Más szóval a sablon egy olyan eszköz. például a sugár lekérdezését. // . mert nincs beépített konverzió set<Circle*>&-rõl set<Shape*>&-re....insert(new Triangle()). Sablonok 459 13.6. ha a set<Shape*> egy set<Circle*> lehetne. nem set<Shape*> } A fenti példát a fordítóprogram nem fogja lefordítani. akkor ez már nem lenne biztosított. Öröklõdési viszonyok Általában úgy gondolnunk egy sablonra. */ }.. Az a garancia..3. hogy egy set<Circle*> csak Circle*-okat tartalmaz. */ }. akkor nem lenne többé igaz. // típushiba: s típusa set<Circle*>. } void g(set<Circle*>& s) { f(s). Nagyon helyesen. .. hogy a Circle-ök halmaza kizárólag Circle típusú objektumokat tartalmaz. Ezért az osztálysablonokat néha típuskészítõknek vagy típusgenerátoroknak hívják. */ }. void f(set<Shape*>& s) { // .. ami hibás érvelésen nyugvó súlyos logikai hiba: az „egy Circle egyben Shape is. azonos osztálysablonból létrehozott osztály nem áll rokonságban egymással: class Shape { /* . Oka.

Ezért ha a T* automatikusan T2*-ra konvertálható. // mutató T-re // Ptr<T> konverziója Ptr<T2>-re Ezután szeretnénk a konverziós operátorokat úgy definiálni. Sablonok konverziója Az elõzõ példa azt mutatta be. ha egy T2* típusú mutató értékül kaphat egy T* típusú mutatót. szeretnénk tükrözni a mutatott objektumok közötti öröklõdési viszonyokat. Egyes osztályoknál azonban mégiscsak szeretnénk ilyen kapcsolatot kifejezni.6. template<class T2> operator Ptr<T2> (). a Ptr<T>rõl Ptr<T2>-re történõ konverzió mûködni fog: void f(Ptr<Circle> pc) { Ptr<Shape> ps = pc. Ptr<Circle> pc2 = ps. }. Tag sablonok (§13. } A return utasítást a fordító csak akkor fogadja el. hogy Ptr-jeinkre fennálljanak a beépített mutatóknál megszokott öröklõdési kapcsolatok: void f(Ptr<Circle> pc) { Ptr<Shape> ps = pc. } // rendben: Circle* átalakítható Shape*-ra // hiba: Shape* nem alakítható Circle*-gá .2) igény esetén sokféle hasonló kapcsolatot kifejezhetnek: template<class T> class Ptr { T* p. Ptr<Circle> pc2 = ps. ha a Shape valóban közvetett vagy közvetlen nyilvános bázisosztálya a Circle-nek.. public: Ptr(T*).1. azonos osztálysablonból létrehozott osztály között. Ezt így tehetjük meg: template<class T> template<class T2> Ptr<T>::operator Ptr<T2> () { return Ptr<T2>(p). hogy a Ptr<T>-rõl Ptr<T2>-re történõ konverzió csak akkor legyen engedélyezett. // . miért nem lehet semmilyen alapértelmezett kapcsolat két.6. } // mûködnie kell // elvileg hibát eredményez Azt szeretnénk. ha p (ami T* típusú) a Ptr<T2>(T2*) konstruktor paramétere lehet.3..460 Absztrakciós módszerek 13. Általában úgy szeretnénk a konverziós operátorokat megadni. Például ha egy mutató sablont készítünk. hogy az elsõ kezdõérték-adás csak akkor legyen engedélyezett.

mielõtt használnánk azokat. ami azt is jelenti.c-nek és építsük be. definiciójukat külön fordítjuk. A használat elõtt a sablonoknak csak a deklarációját emeljük be. Hogy lássuk a kétféle megközelítés közötti különbséget. hogy nem lehetséges egy sablonnak. vegyünk egy egyszerû példát: #include<iostream> template<class T> void out(const T& t) { std::cerr << t. 2. } Hívjuk ezt a fájlt out. Jegyezzük meg. A fordítóprogram dolga.c" // out() használata Vagyis az out()-ot és a hozzá szükséges valamennyi deklarációt több különbözõ fordítási egységbe is beépítjük. Sablonok 461 Legyünk óvatosak és csak logikailag értelmes konverziókat definiáljunk. Ezenkívül lehetséges.7.c: #include "out. hogy a sablonfüggvényeket egy fordítási egységen belül elõször csak deklaráljuk.13. . } 13. majd használjuk és csak végül definiáljuk. illetve annak szintén sablon tagjának sablonparaméter-listáit egyesíteni: template<class T. class T2> // hiba Ptr<T>::operator Ptr<T2> () { return Ptr<T2>(p). A sablonok definícióját beépítjük (#include) egy fordítási egységbe. mint a helyben kifejtett (inline) függvényeket. hogy a sablon függvényeket ugyanúgy kezeli.c" // out() használata // user2. A forráskód szerkezete A sablonokat használó kód szerkezete alapvetõen kétféle lehet: 1. ha szükséges. hogy csak akkor hozzon létre kódot.c: #include "out. és hogy a felesleges információk feldolgozását optimalizálja. valahányszor szükségünk van az out()-ra: // user1.

hogy minden olyan információ. A felhasználó csak a deklarációt (vagyis a felületet) építi be: // user1.2. és az adott fordító dolga szükség esetén az out() leírásának megkeresése. az out()-ot felhasználó valamennyi fordítási egységbe belekerül.c állományt két részre bontjuk: // out.h" // out() használata Ezen a módon a sablon függvényeket a nem helyben kifejtett függvényekhez hasonlóan kezeljük. és ilyen módon megnõ a fordítóprogram által feldolgozandó adat mennyisége. az out. nem is befolyásolhatják azt. a definíciónak minden használat helyérõl elérhetõnek kell lennie.) Ha nem így teszünk.h: template<class T> void out(const T& t). E gondolatmenet logikus folyománya a külön fordítás: ha a sablon nem épül be a felhasználói kódba.c-beli definíciót külön fordítjuk.c: #include<iostream> #include "out.h" // out() használata // user2. Az out. amelyre az out()-nak szüksége van. Egy másik gond. hogy a definícióhoz vagy egy megelõzõ deklarációhoz hozzáadjuk az export szót. // out. hogy a felhasználók esetleg véletlenül „rákapnak” az eredetileg csak az out() definiciójához szükséges deklarációk használatára.462 Absztrakciós módszerek Ezzel az a nyilvánvaló gond.c fájl most az out() definiálásához szükséges összes információt tartalmazza. a makrók használatának elkerülésével és általában a beépítendõ információ mennyiségének csökkentésével háríthatjuk el. hiszen a sablon-meghatározás fölös példányainak kiszûrése helyett szükség esetén meg kell találnia az egyetlen definíciót. Így az eredeti out. melyektõl függ.h" export template<class T> void out(const T& t) { std::cerr << t. ha kifejezetten export-ként adjuk meg (§9.c: #include "out.h csak a meghívásához szükségeset. Ezt a veszélyt névterek használatával. Ez némi terhet ró a fordítóra.c: #include "out. (Ez úgy történik. hogy a sablon definíciója csak akkor érhetõ el más fordítási egységbõl. azok az elemek. } Az out. Jegyezzük meg.3). .

alapvetõen más sablon függvényeket hívó függvényeket érdemes minden olyan fordítási egységben elhelyezni. Egy. ha a sablon definiciója sok deklarációt igényel. ahol felhasználják azokat. a sablon-példányosítás terén átlagos támogatást nyújtó szerkesztõprogram esetében ez meggyorsítja a fordítást és pontosabb hibaüzenetekhez vezet.1) csak egyetlen definiciója lehet. nem pedig úgy. .2. [3] A tárolókat specializáljuk mutatótípusokra.5. 13. a sablonok külön fordítása pedig egyes nyelvi változatok esetében költséges mulatság lehet. illetve a fejlesztés külsõ feltételeitõl függ. valamelyik fordítási egységben. a fejlesztett program fajtájától. Általában a helyben kifejtett függvényeket és az egyéb. de akkor is ez az eljárás követendõ. Ebbõl következõen ilyen tagokat lehetõleg ne használjunk olyan sablonoknál. Ezen elvek alkalmazását azonban mindig az adott helyzethez kell igazítanunk. amelyek sokféle paramétertípusra alkalmazhatók. [4] A specializáció elõtt mindig adjuk meg a sablon általános formáját. hogy csökkentsük a kód méretét.13. ha a sablondefiníciókat külön fordítjuk.8. Tanácsok [1] Olyan algoritmusok leírására. amelyek sok fordítási egységben szerepelnek. §13. Én azt tekintem ideális megoldásnak. Sablonok 463 A fordító. Ezért a nagyobb vagy bonyolultabb függéseket igénylõ sablonokat jobb külön fordíttatni. §13. Ezt inkább úgy érhetjük el. sablont használjunk. Ha a definíciót beépítjük. §13. a nem helyben kifejtett statikus tagoknak (§C. Bármelyik megközelítést válasszuk is. akár külön fordított egységekbe elosztva. §13. hogy a kód ugyanúgy mûködjék. hogy csökkentjük a definíció függését a környezetétõl. Fontos cél.13. mert így értelmét a beépítés helyén érvényes makrók és deklarációk befolyásolhatják. hogy a környezetbõl minél többet átviszünk a példányosítás folyamatába. hogy melyik módszer vagy azok milyen párosítása a legjobb.3. akár egyetlen egységben szerepel. sebezhetõvé tesszük azt. mert ezek nem kívánatos mellékhatásokkal járhatnak a sablon felhasználásának helyén.5. a felhasználói kódban pedig csak deklarációjukat szerepeltetjük.és szerkesztõprogramtól. [2] A tárolókat sablonként készítsük el.

2. §13. §13.8. [12] Mielõtt sablonná általánosítanánk valamit.1.1. [13] Ne felejtsük el kitenni az export kulcsszót azoknál a definícióknál. §13.464 Absztrakciós módszerek [5] A specializációt deklaráljuk. [10] Specializáció és túlterhelés segítségével adjunk azonos felületet ugyanazon fogalom különbözõ típusokra vonatkozó megvalósításának. [17] A fordítási és összeszerkesztési idõvel való takarékoskodás céljából explicit példányosítást használjunk. öröklõdés helyett használjunk sablonokat. §13. [11] Egyszerû esetekre vonatkozóan egyszerû felületet adjunk. §C.5. amelyeket más fordítási egységbõl is el kell érni. §13.2.3.2. §13.6. [21] Ha olyan beépített típusokat és adatszerkezeteket kell használnunk.5. [19] Ha fontos szempont. §13.6. §13. [20] Ha nem lehet közös bázisosztályt megadni.7. §13. §13. [14] A nagy vagy nem magától értetõdõ környezeti függõségû sablonokat külön fordítási egységben helyezzük el.5. §13. sablonok helyett használjunk öröklõdést. hogy a sablonnak nincs-e szüksége C stílusú karakterláncokra és tömbökre vonatkozó specializációkra. [7] Definiáljunk minden deklarált specializációt. milyen paraméterei lehetnek a sablonnak. öröklõdés helyett használjunk sablonokat.1. végezzünk hibakeresést egy konkrét példán. [6] A sablonok függését a példányosítás módjától csökkentsük a lehetõ legkisebbre. . [8] Gondoljuk meg. §13. §C.5. hogy új változatokat újrafordítás nélkül vezethessünk be.9[16].10. §13.4. §C. §13.1.6. a ritkább eseteket túlterheléssel és alapértelmezett paraméter-értékekkel kezeljük.13.5.13.1. öröklõdés helyett használjunk sablonokat.6.7.4. [15] Konverziókat sablonokkal fejezzünk ki. [9] Paraméterezzünk eljárásmód objektummal. [18] Ha a futási idõ döntõ szempont. mielõtt használnánk. de ezeket nagyon óvatosan definiáljuk. amelyeknek a korábbi változatokkal összeegyeztethetõnek kell maradniuk. §13.13.1. §13.10.5. §13.6. [16] Szükség esetén egy constraint() függvény segítségével korlátozzuk.

Azonos stílusú be. (*2. amely egy Link típusból származtatott típusú elemeket tud tárolni. hasonlítsuk össze a kétféle kódot. a count és a price. Határozzunk meg egy Record osztályt.5) A §11. (*2) Készítsünk egy qsort() sablont. 10. és akkor is.8 pontbeli szószámláló program hatékonyságát egy asszociatív tömböt nem használó programéval. . (*1.13. illetve a sablonból létrehozott változat ellenõrzésére.2. a 280. milyen típusok lehetnek kulcsok. melynek két tagja van.5) A §13. Adjuk meg. Milyen mûveletek szükségesek az egyszeresen láncolt lista mûveletein felül? 4. A Link típus tartalmazza az elemek összekapcsolásához szükséges információkat. oldaltól kezdõdõen. 9.2 pontbeli String sablont a §11. 3. (*2) Határozzunk meg egy olyan sort()-ot. illetve értékek. (*3) Írjunk osztálysablont egy egyszeresen láncolt lista számára. ha az alkalmazott típusnak van alapértelmezett konstruktora. (A topologikus rendezést a [Knuth.és kimeneti mûveleteket használjunk mindkét esetben. Gyakorlatok 1. 2.1987] elsõ kötete írja le.value) párokat olvas be és az egyes kulcsokhoz (key) tartozó értékek (value) összegét írja ki.) 12. Az ilyen listát „tolakodó” (intrusive) listának nevezik. (*2. (*2) Írjunk egy programot.12 pontbeli String osztály alapján. 7. 6.5) Írjunk tolakodó és nem tolakodó kétszeresen láncolt listákat.9[7]-beli összegzõ programot javítsuk ki. (*2) Fejezzük be a §13.9[8]-beli Map-et valamilyen alkalmasabb adatszerkezet – például vörös-fekete fa (red-black tree) vagy S-fa (splay tree) – felhasználásával. Futtassunk le egy egyszerû programot a saját.5 pontbeli definiciójában és írjuk meg a fordítóprogram által az f() függvény és a List számára létrehozott kóddal egyenértékû kódot. (*2. amely (key. Rendezzünk egy set<Record> halmazt mindkét tag szerint. (*3) Hasonlítsuk össze a §11.8 pontbeli Assoc osztály alapján készítsünk egy egyszerû Map osztályt. (*2) Javítsuk ki a hibákat a List §13. A Map mûködjék helyesen kulcsként használt C stílusú karakterláncokkal és string-ekkel is. amely az összehasonlítási feltételt sablonparaméterként veszi át. Ezen lista felhasználásával írjunk egy bármilyen típusú elemeket tartalmazni képes (azaz nem tolakodó) egyszeresen láncolt listát. 5.5) Használjuk fel a Map-et topologikus rendezést megvalósító függvény írására. Hasonlítsuk össze a két lista hatékonyságát. valamint akkor is.9. Amennyiben módunk van rá. elõnyeiket és hátrányaikat. Sablonok 465 13. hogy szóközöket is tartalmazó nevekre is helyesen mûködjön. ha nincs. 8. (*3) Írjuk át a §13. 11.

ár)). hogy a fordítóprogram minden olyan sablon minden paraméterére ellenõrizzen bizonyos megszorításokat. (*2) Használjuk a §13. szám. 16. Adjunk egy. (*2) Tervezzünk egy módszert.466 Absztrakciós módszerek 13. és ott is. ahol unsigned. amelyeknek megfelelõ típusú objektum létrejön a programban. (*2) Írjunk readline() sablonokat különféle sorfajták számára (például (cikk. 15. 14.és nagybetûk közötti különbséget elhanyagoló rendezést támogató változatot is. Csak „a T paraméternek egy My_base-bõl származtatott osztálynak kell lennie” típusú megszorítások ellenõrzése nem elég! . amellyel biztosítható. (*1. hogy a módszer olyan C++-változatokkal is mûködjön. a kis. amely legalább háromféle eltérést mutat be egy függvénysablon és egy makró között (a formai követelmények különbségén felül).5) Szerkesszünk egy példát.4 pontbeli Literate-ben vázolt módszert karakterláncok fordított ábécésorrendû rendezésére. Gondoskodjunk róla. ahol a char elõjeles (signed).

hogyan kell az ilyen hibákat kezelni. amelyek nem hibák • Kivételek specifikációja • Váratlan kivételek • El nem kapott kivételek • A kivételek és a hatékonyság • A hibakezelés egyéb módjai • Szabványos kivételek • Tanácsok • Gyakorlatok 14.” (Winston S. amelyet nem tud kezelni. Az alapötlet az. mit kezdjen velük.3 pontban rámutattunk. A könyvtár felhasználója tudhatja. Churchill) Hibakezelés • A kivételek csoportosítása • A kivételek elkapása • Minden kivétel elkapása • Továbbdobás • Az erõforrások kezelése • auto_ptr • A kivételek és a new operátor • Az erõforrások kimerülése • Kivételek konstruktorokban • Kivételek destruktorokban • Olyan kivételek. hogy ha egy függvény olyan hibát talál. A kivételek (exception) az ilyen problémák kezelését segítik. egy könyvtár szerzõje felfedezheti a futási idejû hibákat. Hibakezelés Ahogy a §8. throw) . akkor egy kivételt vált ki („kivételt dob”.14 Kivételkezelés „Ne szóljon közbe.1. de nem tudja felderíteni azokat – máskülönben a felhasználói kódban kezelné és nem a könyvtárnak kellene megtalálnia õket. amikor éppen közbeszólok. de általában fogalma sincs róla.

Ha egy függvény például int típussal tér vissza. De ha alkalmazható is ez a módszer. mert minden hívást ellenõrizni kell. ami megakadályozná a hibás hívások halmozódását. 3. hogy el akarják kapni (catch) a kivételt (§2.8). amelynek nem szabad „elszállnia”. hogy a vezérlés visszakerüljön a hívóhoz. A hibakezelés ezen módja felveszi a versenyt a hagyományosabb módszerekkel. mert nem mindig létezik elfogadható „hibát jelentõ érték”. Tekintsük át a többi lehetõséget: ha a program észreveszi. egy „hiba” jelentésû értéket adhat vissza. amelyet nem lehet helyben megoldani. Az adott problémát kezelni tudó függvények jelezhetik. . Alapértelmezés szerint az elsõ eset. nem használhatunk olyan programban. meghívhat egy. A C standard könyvtárának számos függvénye például az errno globális változót állítja be. hogy a program nem megengedett állapotba került. hogy hibát jelezzen (§14. A második eset („hiba jelentésû érték visszaadása”) nem mindig kivitelezhetõ. befejezheti a program futását. A harmadik módszer („normális érték visszaadása és a program szabálytalan állapotban való hagyása”) azzal a gonddal jár. hogy minden hibát észleljenek vele. amelyek nem ismerik a befoglaló program célját vagy annak általános mûködését.8). Ezért aztán ezt a módszert ritkán alkalmazzák annyira következetesen. A legtöbb hiba kezelésére ennél jobb megoldás szükséges és lehetséges. amelyet nem kap el a program. ha a megfelelõ mûvelet helyben nem végezhetõ el. Ezenkívül az egyidejû hozzáférésnél (konkurrencia. a programok azonban jellemzõen elmulasztják az errno változó kellõen következetes vizsgálatát.3). ha olyan kivétel lép fel. §8. Azok a könyvtárak. akkor 1. azaz a program futásának befejezése történik. sokszor akkor is kényelmetlen. concurrency) a globális változók hibajelzésre való használata nem mûködik jól. akkor minden int érték hihetõ visszatérési érték lehet. Olyan könyvtárat. hogy lehetõséget adnak arra. „hiba” esetén meghívandó függvényt. hogy olyan probléma lépett fel. hogy a hívó esetleg nem veszi észre. A kivételek szerepét tehát úgy is megfogalmazhatnánk. 2. ami a program méretét akár kétszeresére is növelheti (§14. hogy a (közvetett vagy közvetlen) hívó képes kezelni a problémát. 4.2. amely feltétel nélkül befejezi a program futását. nem hajthatnak végre egyszerûen egy abort()ot vagy exit()-et. egy normális értéket adhat vissza és a programot „szabálytalan” állapotban hagyhatja.468 Absztrakciós módszerek abban a reményben.4.

Mindazonáltal ez elõnyösebbnek tûnik. a „hibakezelõ függvény meghívása” mód alkalmas. amit nem tud kezelni. A hagyományos kezelés az volt. hogy valahogy átevickéltünk a hibán. amikor a felhasználó a program szerkezetét ismerõ programozó. amelynek van fogalma a program összefüggéseirõl. amikor a program már a mit sem sejtõ felhasználók kezében van.14.4. majd feltétel nélküli leállás történne. Ez pedig jobb. a kód olyan része számára továbbítsa. akkor el lehet kapni az összes kivételt (§14.2). a felhasználó segítségét kérõ ablakokkal stb. így a kivétel csak akkor állítja le a programot. kivételek híján viszont a hibakezelõ függvénynek csak a többi három lehetõsége van a hiba kezelésére. hogy a hibák (különösen a könyvtárakban fellépett hibák) alapértelmezett kezelése a program leállítása. Ezenkívül sokszor nincs is hová értesítést küldeni a hibáról (például ha a program olyan környezetben fut.5 pont tárgyalja. hogy a hibát. mint amelyekre a negyedik. próbálták enyhíteni. és a hibaüzenetek a végfelhasználó számára úgysem mondanának semmit. A kivételkezelõ eljárás szabályosabb hibakezelést tesz lehetõvé és megkönnyíti a külön megírt kódrészek közötti együttmûködést. A hibakezelõ függvények és a kivételek témáját a §14. A C++ kivételkezelésének a Pascal. Ha a leállás az adott programnál elfogadhatatlan. mintha késõbb a fejlesztés során lépnének fel hibás eredmények. „a program belövése”) hasznos. vagy akár a fejlesztés lezárulta után. környezetérõl. ha azok nem elégségesek.6. ezáltal a programot olvashatóbbá és a kódelemzõ eszközök számára kezelhetõbbé teszi. A kivételek lehetõvé teszik az adott kódrészlet számára. Kivételkezelés 469 A kivételkezelés nem az olyan esetek kezelésére szolgál. Egyesek a hibákon való „átevickélés” nem vonzó tulajdonságait hibaüzenetek kiíratásával. Az a legkevesebb. nem „elegánsak” vagy hibákat okozhatnak. Az ilyesmi fõleg a program hibakeresésénél (debugging. amely talán boldogul vele. Ennél rosszabb.és C-programozók számára új vonása. A kivételek használata lehetõvé teszi a hibakezelõ kódnak a „közönséges” kódtól való elválasztását. hogy a program elfogadhatóan fusson. A kivételkezelést akkor célszerû használnunk a hagyományos módszerek helyett. mondjuk egy grafikus felhasználói felületrõl érkezett rossz adat hatására „bad argument to atan2”. mint ha a hiba hagyományos módon történt „nem teljes” kezelését követõen végzetes hiba. hogy több gondot és figyelmet kell fordítani arra. ha a programozó ezt hagyja. mondjuk finnül egy angol felhasználó számára. A programnak csak olyan része lehet képes értelmes hibaüzenet küldésére. hogy a hibaüzenet esetleg nem is a megfelelõ nyelven jelenne meg. Egy jó könyvtár nem „halandzsázik” így.3.2) vagy az összes adott fajtájút (§14. ahol a cerr nem vezet a felhasználó által elérhetõ helyre). Nem fejlesztõk számára az esetleg jelen sem levõ felhasználó/kezelõ segítségét kérõ könyvtár elfogadhatatlan. hogy a hibaüzenet jellemzõen a könyvtár fogalmaival lenne megfogalmazva. . A kivételkezelés törékenyebbé teszi a programot abban az értelemben. aztán reménykedtünk.

de vigyázzunk. ahol ez a rendszer szerkezetébõl adódóan a legtermészetesebb.1. A kivételkezelésnek azonban a hibakezelés és a hibatûrõ viselkedés támogatása az elsõdleges célja és ez a fejezet is ezekre összpontosít. A tervezési folyamatra nagyobb hangsúlyt helyez és megnövelheti egy kezdeti (még hibás) változat elõállításához szükséges munka mennyiségét. A C++ nyelv kivételkezelõ rendszerét úgy tervezték. Ennek megfelelõen a kivételek nyelvileg támogatott kezelése kifejezetten támogatja a „jó stílusú” programozást. 14. amelyeknek semmi közük a hibákhoz (§14. A kivételkezelés csak a szinkron kivételek.4) alapuló vezérlési szerkezet. leírásuk itt nem szerepel. Ezért kivételeket olyan esetekben is szabályosan alkalmazhatunk. . mint azok a módszerek.3 pontot. signal) használata). Az aszinkron események világos és hatékony kezeléséhez a kivételkezelés itt leírt módjától alapvetõen különbözõ eljárásokra van szükség. amelyek különbözõ emberek számára különbözõ jelentéssel bírnak. de mivel ezek rendszerfüggõek szoktak lenni.470 Absztrakciós módszerek A kivételkezelésre úgy is tekinthetünk. A C++ nyelv kivételkezelése a programozónak a hibák azon helyen való kezelésére ad lehetõséget. például a tömbindex-hibák vagy ki. mint a billentyûzet felõl érkezõ megszakítások vagy bizonyos aritmetikai hibák nem feltétlenül kivételek és nem közvetlenül ezzel az eljárással kezelendõk. Az aszinkron események. mint ahogy a C++ nyelv más eszközei támogatják azt. Meg kell azonban jegyeznünk. amely a kivételkezelés alapvetõ vonásait mutatja be. Más nyelveken (C vagy Pascal) a jó stílus csak egyes szabályok „megkerülésével” és nem is tökéletesen érhetõ el. amelyet alternatív visszatérési eljárásként is tekinthetünk. Ám az eredmény egy olyan kód. A kivételek más megközelítései A „kivétel” (exception) azon szavak egyike. A kivételkezelés egy nem lokális. mint a fordítási idejû típusellenõrzés és többértelmûség-kiszûrés futási idejû megfelelõjére. Sok rendszernek vannak az aszinkronitás kezelésére szolgáló eljárásai (például szignálok (jelzések.5). A kivételek nyilvánvalóvá teszik a hibakezelés bonyolultságát. amelynek sokkal nagyobb az esélye arra.1. hogy egy nagyobb program része lehessen. a (végrehajtási) verem „visszatekerésén” (stack unwinding. §14. hogy az elvárt módon fusson. Ezen a ponton célszerû újraolvasni a §8. hogy más programozók számára is érthetõ legyen. hogy eszközökkel lehessen kezelni. hogy hibák és más kivételes jelenségek kezelését támogassa – innen a neve – és hogy támogassa a hibák kezelését függetlenül fejlesztett összetevõkbõl álló programokban. hogy a hibakezelés ezután is nehéz feladat marad és a kivételkezelõ eljárás – jóllehet rendszerezettebb.és bemeneti hibák kezelésére szolgál. amelyeket helyettesít – a vezérlést kizárólag helyben szabályozó nyelvi elemekhez képest kevésbé hatékony. hogy a rossz hírért ne annak hozóját hibáztassuk.

A legtöbb nagy program normális és sikeres futtatása során azonban néhány kivétel kiváltása és elkapása biztosan elõ fog fordulni. amíg egy megfelelõ (vagyis a kivételt kiváltó függvényt közvetve vagy közvetlenül meghívó) függvényben catch-et nem találunk. egészen addig. hogy szabályosan zárolja (lock) a megosztott adatszerkezeteket a használat elõtt. Ebbõl következõleg a kivételek csoportosításához és kezeléséhez az öröklõdés hasznos segítséget nyújthat. . class Underflow: public Matherr { }. de a programozónak kell eldöntenie. itt csak azt jegyzem meg. Tekintsünk-e kivételesnek egy olyan eseményt. amely a program legtöbb futásakor fellép? Lehet-e egy tervezett és kezelt esemény hiba? Mindkét kérdésre igen a válasz. Ez nem mindig könnyû (§14.3). A hibát észlelõ kód (általában egy könyvtár) „eldobja” (throw) az objektumot (§8. A C++ kivételkezelõ eljárásainak a hibák és kivételes események jelentése és kezelése a céljuk.2. Jobb úgy értelmezni a kivételt. class Overflow: public Matherr { }.. Szokás szerint ilyenkor valami mással próbálkozunk. processz) fogalmát. A „kivételes” nem azt jelenti. hogy konkurens programban is hatékony legyen. A használt rendszer dokumentációja leírja az ezeket kezelõ eszközöket. melynek osztálya valamilyen kivétel elõfordulását írja le. például azt. hogy „a rendszer valamely része nem tudta megtenni. A hibát kezelni képes kódrészlet beavatkozási szándékát egy „elkapó” (catch) záradékkal jelzi. feltéve. Kivételek kiváltása („dobása”) a függvényhívásokhoz képest ritkán forduljon elõ. A kivétel „eldobása” „visszatekeri” a végrehajtási vermet. 14. hogy a programozó vagy a rendszer betart bizonyos alapvetõ szabályokat. A kivételek gyakran természetes módon családokra oszthatók.5). különben a rendszer szerkezete áttekinthetetlen lesz. hogy egy adott programban mi számít kivételesnek. hogy a C++ nyelv kivételkezelõ rendszerét úgy tervezték. Egy matematikai könyvtár kivételeit például így csoportosíthatjuk: class Matherr { }. hogy „szinte soha nem történhet meg” vagy hogy „végzetes”. amire kérték”. class Zerodivide: public Matherr { }. Kivételkezelés 471 A szabványos C++ nem ismeri a végrehajtási szál (thread) és a folyamat (process. A kivételek csoportosítása A kivétel olyan objektum. // . ezért az egyidejû hozzáféréssel összefüggõ kivételes helyzeteket itt nem tárgyaljuk.14..

. */ } catch (Underflow) { /* . Az összes többi Matherr kivételt az általános záradék fogja kezelni. De ha van is. Ha a matematikai könyvtár egy új kivétellel bõvülne.. ez pedig a legtöbb könyvtár számára elfogadhatatlan.. vagy ha igen. hogy minden kód a rendelkezésünkre áll. A kivételeknek hierarchiába való szervezése fontos lehet a kód tömörsége céljából.. ha nem csoportosítanánk a matematikai kivételeket. hogyan lehetne a matematikai könyvtár összes kivételét kezelni. Gondoljuk meg. az összes kivételt kezelni kívánó kódrészletet módosítani kellene. } catch (Overflow) { // az Overflow (túlcsordulás) vagy más onnan származó hiba kezelése } catch (Matherr) { // nem Overflow matematikai (Matherr) hibák kezelése } Itt az Overflow-t külön kezeltük. hogy tényleg hajlandóak vagyunk átdolgozni..2). } catch (Overflow) { /* . ha nem volnának csoportosítva. nem biztos..472 Absztrakciós módszerek Ez a szerkezet lehetõvé teszi..6. minden. de egy kivétel könnyen ki is maradhat. Gondoljuk meg például. hogy a pontos típusra való tekintet nélkül kezeljünk bármilyen Matherr-t: void f() { try { } // ... Általában a könyvtár elsõ változatának kibocsátása után ilyen teljes körû módosításokra nincs mód.. Ezért tehát a kivételeket célszerû könyvtárankénti vagy alrendszerenkénti csoportokban megadni (§14.. */ } catch (Zerodivide) { /* . */ } Ez nemcsak fárasztó. Az összes kivételt fel kellene sorolni: void g() { try { } // . nem biztos. hogy az elsõ változat kibocsátása után a könyvtár nem bõvülhetne további kivételekkel. mi lenne. Ezek az újrafordítási és módosíthatósági szempontok ahhoz az irányelvhez vezetnének.

Származtatott kivételek Az osztályhierarchiák kivételkezelésre való használata természetesen vezet olyan kivételkezelõkhöz. int a.14.. A kivétel elkapásának és megnevezésének mûködése a paraméteres függvényekével azonos.. int a1. } A Matherr kezelõbe való belépéskor az m egy Matherr objektum. nem saját osztályának kezelõje.. class Int_overflow: public Matherr { const char* op. Vagyis a formális paraméter a paraméter-értékkel kap kezdõértéket (§7. 14. slicing.. . Kivételkezelés 473 Jegyezzük meg. } }. például a nullával való osztást.. } // . hogy sem a beépített matematikai mûveletek. } catch (Matherr m) { // .. hogy számos utasításcsövet alkalmazó (pipelined) rendszer aszinkron módon észlel bizonyos aritmetikai hibákat. a1 = a. amelyeket a kivételek hordozta információnak csak egy része érdekel.10 írja le. A standard könyvtárbeli kivételeket a §14.' << a2 << ')'. Következésképpen az Int_overflow-ban levõ többlet információ elérhetetlen.2. public: Int_overflow(const char* p.1. Ennek egyik oka.3): class Matherr { // . sem a (C-vel közös) alapvetõ matematikai könyvtár nem kivételek formájában jelzik az aritmetikai hibákat. Vagyis egy kivételt általában egy bázisosztályának a kezelõje kap el. a2 = b.2. még ha g() egy Int_overflow-t váltott is ki. hogy a kivételnek csak az elkapott osztálynak megfelelõ része másolódik le (felszeletelõdés. a2. void f() { try { } g(). §12. A Matherr hierarchia ezért itt csak illusztrációul szolgál. virtual void debug_print() const { cerr << "Matematikai hiba".2). int b) { op = p. } virtual void debug_print() const { cerr << op << '(' << a1 << '. }. Ebbõl következik.

*/ }.. } return x+y. public File_system_err { /* . Ha a kivételt érték és nem referencia szerint kaptuk volna el.2).2..-2). } int i1 = add(1.. int y) { if ((x>0 && y>0 && x>INT_MAX-y) || (x<0 && y<0 && x<INT_MIN-y)) throw Int_overflow("+". mutatók vagy referenciák használatával megakadályozhatjuk az információvesztést: int add(int x. Összetett kivételek Nem minden kivételcsoport fa szerkezetû.x. 14. int i2 = add(INT_MAX. Egy kivétel gyakran két csoportba is tartozik: class Netfile_err : public Network_err.2).2. // x+y nem fog túlcsordulni void f() { try { } } catch (Matherr& m) { // . m. } . int i3 = add(INT_MAX...y).474 Absztrakciós módszerek Mint mindig. Egy ilyen Netfile_err -t el lehet kapni a hálózati kivételekkel törõdõ függvényekben: void f() { try { } // valami } catch(Network_err& e) { // .. akkor ehelyett Matherr::debug_print()-re került volna sor.debug_print(). // ez az! Az utolsó add() hívás kiváltotta kivétel hatására Int_overflow::debug_print() fog végrehajtódni.

Ebben a példában a g() írója esetleg nem is tudott arról. hogy hálózat is szerepet játszik (lásd még §14. . H ugyanaz a típus. amikor a szolgáltatások (például a hálózati szolgáltatások) a felhasználó számára láthatatlanok. 3.6). 2. Kivételkezelés 475 De akár a fájlrendszer-kivételekkel foglalkozó függvényekben is: void g() { try { } // valami más } catch(File_system_err& e) { // . vagy 2. H referencia és típusára teljesül 1... A kivételek elkapása Vegyük az alábbi kódrészletet: void f() { try { } throw E(). 14.. H és E mutatótípusok és a mutatott típusokra teljesül 1. } A hibakezelés ilyen nem hierarchikus szervezése akkor fontos.3. H egyértelmû bázisosztálya E-nek. 4. } catch(H) { // mikor jutunk ide? } A vezérlés akkor kerül a kezelõhöz. ha 1. mint E.14. vagy 2.

7). hogy nem tudja teljes egészében kezelni azt.3. mint egy függvény paraméterénél. Az az elv. A fordítóprogram az ilyen esetek egy részét – de nem mindet – felderítheti és figyelmeztethet rájuk. terminate() hívás fog bekövetkezni (§14. hogy az elkapott kivételt módosíthassuk. hogy a kivételrõl kiváltásakor másolat készül. Így a hibát végül is a legalkalmasabb helyen lehet kezelni.1. a kezelõ pedig az eredeti kivétel másolatát kapja meg. A kivételek továbbdobása Gyakran elõfordul. úgy dönt. majd továbbdobja a kivételt. // a kivétel továbbdobása } } } A kivétel továbbdobását az operandus nélküli throw jelzi. hogy a memória elfogyásakor szabványos módon kiváltandó kivétel. Ez akkor is igaz.476 Absztrakciós módszerek Ezenkívül egy kivétel elkapásánál ugyanúgy alkalmazhatjuk a const minõsítõt. ha nincs is kivétel. amit lehet throw.4. hogy egy kivételrõl elkapása elõtt több másolat is készül. ezért nem lehet olyan kivételt kiváltani.5) számára van hely. hogy miután egy kezelõ elkapott egy kivételt. Lehetséges. csak megakadályozza. Az adott nyelvi változat nagyon sokféle módszert alkalmazhat a kivételek tárolására és továbbítására. de az biztosított. 14. ami nem másolható. a bad_alloc (§14. ha a kivétel kezeléséhez szükséges információ nem egyetlen helyen áll rendelkezésre és a hiba következményeit legjobban több kezelõ között elosztva küszöbölhetjük ki: void h() { try { // esetleg matematikai hibákat kiváltó kód } catch (Matherr) { if (teljesen_le_tudjuk_kezelni) { // Matherr kezelése } else { return. Ettõl az elkapható kivételek típusa nem változik meg. . Ilyenkor a kezelõ általában megteszi helyben. // megtesszük. Ha akkor próbálunk meg kivételt továbbdobni. amit lehet.

2.2 pont ír arról. ami Matherr-ként rendelkezésre áll. sor kerül a kezelõ függvény rendrakó tevékenységére.7. Amint a helyi rendrakás megtörtént.) { // takarítás throw.) jelentése is a tetszõleges kivétel elkapása: void m() { try { } // valami } catch (. Sok fontos esetben azonban egy ilyen kivételkezelõ nem a legelegánsabb megoldás (lásd §14. ... Más szóval. Minden kivétel elkapása Az elkapási és továbbdobási módszer egy végletes esetével is érdemes megismerkednünk. A §14.6). tetszõleges paramétert jelent (§7. hogyan lehet információhoz jutni a .. Mint ahogy függvényekre a . Kivételkezelés 477 A továbbdobás az eredeti kivételre vonatkozik. 14.14.4). akkor a kivételkezelõbe beírhatjuk a nekik elfogadható értéket adó kódot. §24. akkor a h() hívója is Int_overflow-t kap. Például ha m() bizonyos mutatókat olyan állapotban hagy hátra.3. } // minden kivételt elkapunk Így ha m() fõ részének végrehajtása során bármilyen kivétel lép fel. amelyet h() egy Matherr-ként kapott el és úgy döntött. a catch(.1).3. az arra okot adó kivétel továbbdobódik a további hibakezelés kiváltása céljából..3. kezelõ által elkapott kivételrõl. Így a „minden kivételt elkapó” kezelõ tetszõleges invariánsok kezelésére alkalmas hely lehet...6. ha a program Int_overflow-t váltott ki.. ahogy azokat találta. A hibakezelésnek általában (a kivételkezelésnek pedig különösen) fontos szempontja a program által feltételezett állapot érvényességének megõrzése (invariáns. hogy továbbdobja.. nem csak annak elkapott részére.

A kezelõk kipróbálására ebben a sorrendben kerül sor: void f() { try { } // ..4....2) } Itt az exception soha nem jut szerephez.2) } Mivel a fordítóprogram ismeri az osztályhierarchiát. mert az exception leszármazottja.478 Absztrakciós módszerek 14.3.) { // minden kivétel kezelése (§14.3.10) } catch (.10) } catch (std::bad_cast) { // dynamic_cast hibák kezelése (§15. A kezelõk sorrendje Mivel egy származtatott osztályú kivételt több típusú kivétel kezelõje is elkaphat.3.. } catch (.) { // egyéb kivételek kezelése (§14. sok logikai hibát kiszûrhet: void g() { try { } // . a try utasításban a kezelõk sorrendje lényeges..10) } catch (std::exception& e) { // standard könyvtárbeli kivételek kezelése (§14. Még ha el is távolítjuk a mindent elkapó kezelõt.1.2) } catch (std::exception& e) { // standard könyvtárbeli kivételek kezelése (§14.. } catch (std::ios_base::failure) { // i/o adatfolyam hibák kezelése (§14.2. .. a bad_cast akkor sem kerül szóba.

"r").) { fclose(f). akkor egy kivétel miatt a use_file() függvény az fclose() végrehajtása nélkül térhet vissza. amely minden hibát elkap. try { // f használata } catch (. . Még egy közönséges return utasítás miatt is visszatérhet a use_file() az fclose() végrehajtása nélkül.."r"). Ez mûködõképesnek tûnik. Kivételkezelés 479 14. // f használata } fclose(f). } A fájlt használó kód egy try blokkban van. –. bezárja a fájlt. } fclose(f). de az fclose() meghívása elõtt. zárol valamit stb..14. Egy elsõ kísérlet a use_file() hibatûrõvé tételére így nézhet ki: void use_file(const char* fn) { FILE* f = fopen(fn. throw. Erõforrások kezelése Amikor egy függvény lefoglal valamilyen erõforrást – például megnyit egy fájlt. Például a C standard könyvtárának longjmp() függvénye ugyanilyen problémát okozhat. lefoglal valamennyi memóriát a szabad tárban. gyakran fontos feltétel a rendszer további mûködése szempontjából. hogy ha valami hiba történik az fopen() meghívása után. hogy az erõforrást rendben fel is szabadítsa a hívóhoz való visszatérés elõtt: void use_file(const char* fn) { FILE* f = fopen(fn. amíg észre nem vesszük. majd továbbdobja a kivételt. Ugyanez a probléma kivételkezelést nem támogató nyelvek esetében is felléphet.4.

vagy az fopen()-hez szükséges paraméterekkel hozhatunk létre. public: File_ptr(const char* n.. } File_ptr(FILE* pp) { p = pp. hiszen a programozók elfáradnak. Szerencsére van jobb megoldás. // elsõ erõforrás felszabadítása } Általában fontos szempont.. A megoldandó helyzet általános formájában így néz ki: void acquire() { // elsõ erõforrás lekötése // . // n-edik erõforrás lekötése // erõforrások felhasználása // n-edik erõforrás felszabadítása // . operator FILE*() { return p. Így aztán az ilyen erõforrás-lefoglalási és -felszabadítási problémákat konstruktorokkal és destruktorokkal bíró osztályok objektumainak használatával kezelhetjük. } Egy File_ptr objektumot vagy egy FILE* mutatóval. Például megadhatjuk a File_ptr osztályt. destruktora pedig bezárja a fájlt. bármelyik esetben élettartama végén a File_ptr objektum megsemmisül. fáradságos és esetleg lassú is lehet. amely egy FILE*-hoz hasonlóan viselkedik: class File_ptr { FILE* p. // f használata } .. hogy feleslegesen hosszú. Programunk mérete így minimálisra csökken: void use_file(const char* fn) { File_ptr f(fn.. } }.480 Absztrakciós módszerek Ezzel a megoldással az a gond. const char* a) { p = fopen(n. Ez erõteljesen emlékeztet a konstruktorok által felépített és destruktorok által megsemmisített helyi objektumok viselkedésére."r"). hogy az erõforrásokat megszerzésükkel ellentétes sorrendben szabadítsuk fel. Ráadásul minden túl hosszú és fáradságos megoldás nyomában ott járnak a hibák.a). } ~File_ptr() { fclose(p).

hogy a függvénybõl „normálisan” vagy kivétel kiváltása folytán léptünk-e ki. Kivételkezelés 481 A destruktor attól függetlenül meghívódik. 14. de a zárat nem. Vegyünk egy X osztályt. Ahogy a vermet „visszatekerik”. Egy objektumot addig nem tekintünk létezõnek. Egy részobjektumokból álló objektumot olyan mértékben tekintünk létrehozottnak.4. egy jól megírt konstruktor – amennyire csak lehetséges – olyan állapotban hagyja a rendszert. Ideális esetben a „naív módon” megírt konstruktorok teljesítik ezt és nem hagyják az objektumot valamilyen „félig létrehozott” állapotban. Ha ez nem érhetõ el. amíg konstruktora le nem fut. egy tömböt pedig addig az eleméig. Ez az általános eljárás a konstruktorok és destruktorok tulajdonságain. úgy hívják meg a létrehozott lokális objektumok destruktorait. Azt a mûveletet. hogy a hibakezelést eltávolítsuk a fõ algoritmusból.1. Ezek megszerzése nem biztos. Ezenkívül ezt anélkül kell elérni. valamint a kivételkezelõ rendszerrel való együttmûködésükön alapul. hogy sikerül. Így a kivételkezelõ eljárás lehetõvé teszi. amikor a kivételek kezelésekor a hívási láncban megkeressük a megfelelõ kezelõt. a File_ptr-t és a Lock_ptr-t. Az így adódó kód egyszerûbb és kevesebb hibát okoz.14. Ezt a „kezdeti értékadás az erõforrás megszerzésével” módszernek a tagokra való alkalmazásával érhetjük el. amilyen mértékben részobjektumainak konstruktorai lefutottak. rendszerint a „verem visszatekerésének” hívják. Konstruktorok és destruktorok használata Az erõforrások lokális objektumok használatával való kezelését szokás szerint úgy hívják. mint hagyományos megfelelõje. Az X osztály konstruktorának semmilyen körülmények között nem szabad úgy visszatérnie. hogy a dolog bonyolultságával a programozónak törõdnie kelljen. Destruktora csak ebben az esetben fog a verem visszatekerésekor meghívódni. hogy az objektum teljesen és szabályosan létrejöjjön. hogy az állományt lefoglalta. amelynek konstruktora már lefutott. A megszerzett erõforrások ábrázolására két osztályt használunk. hogy „kezdeti értékadás az erõforrás megszerzésével” (resource acquisition is initialization). Az adott erõforrás megszerzését az azt jelölõ lokális objektum kezdeti értékadása ábrázolja: . mint meghívása elõtt volt. amelynek konstruktora két erõforrást igényel: egy x állományt és egy y zárolást. és kivételt válthat ki. A konstruktor azt próbálja elérni. A destruktor a verem visszatekerésekor csak a teljes egészében létrehozott elemekre fut le.

Lock_ptr bb. }. init(). Ha az init()-ben kivétel keletkezik.. Ebbõl következõleg ahol az erõforrások megszerzése ezen egyszerû modell szerint történik. de a bb-é elõtt kivétel váltódik ki. ott a konstruktor írójának nem kell kifejezett kivételkezelõ kódot írnia.. A felhasználónak nem kell ezzel törõdnie.. // 'x' lefoglalása // 'y' lefoglalása Csakúgy mint a lokális objektumos példában.482 Absztrakciós módszerek class X { File_ptr aa. } // . } // . A fenti módszer használata általános gyakorlat – és a „memória elszivárgásához” (memory leak) vezethet. . Az alkalmi módon kezelt erõforrások között a leggyakoribb a memória: class Y { int* p. mivel az objektum nem jött létre teljesen. akkor az aa destruktorának meghívására sor kerül. const char* y) : aa(x. void init(). így destruktora nem fut le.."rw"). }. void init(). a lefoglalt memória nem fog felszabadulni. de a bb-ére nem. } ~Y() { delete[ ] p. bb(y) {} // .. itt is az adott fordító dolga a szükséges nyilvántartások vezetése. Íme egy biztonságos változat: class Z { vector<int> p. public: X(const char* x.. Például ha az aa létrehozása után. public: Y(int s) { p = new int[s]. public: Z(int s) : p(s) { init(). }.

explicit auto_ptr(X* p =0) throw() { ptr=p. az általa mutatott objektum is automatikusan törlõdik: void f(Point p1. konstans (const) auto_ptr-eket nem lehet másolni. Ezen tulajdonos szerinti kezelés (vagy destruktív másolás) támogatása céljából az auto_ptreknek a közönséges mutatóktól gyökeresen eltérõ másolási módszerük van: egy auto_ptrnek egy másikba való másolása után a forrás nem mutat semmire. Kivételkezelés 483 A p által használt memóriát a vector kezeli. Point p2.. if (in_a_mess) throw Mess().2. // az auto_ptr<Shape> pont úgy használható.. Ezenkívül amikor az auto_ptr megsemmisül. Auto_ptr A standard könyvtár auto_ptr sablon osztálya támogatja a „kezdeti értékadás az erõforrás megszerzésével” módszert.. Itt a Rectangle.. a pb által mutatott Shape és a pc által mutatott Circle egyaránt törlõdni fog. akár váltódott ki kivétel.. akár nem. } ~auto_ptr() throw() { delete ptr. */ }.14. auto_ptr<Shape> pbox(pb). Shape* pb) { auto_ptr<Shape> p(new Rectangle(p1.6 . X* ptr. public: typedef X element_type. // lásd §14. } // segédosztály // throw() jelentése "nem vált ki kivételt". // kilépéskor ne felejtsük // el törölni pb-t // p téglalapra mutat } p->rotate(45). Íme egy lehetséges kifejtése: template<class X> class std::auto_ptr { template <class Y> struct auto_ptr_ref { /* .p2)).4. auto_ptr<Circle> pc. Minthogy az auto_ptreket a másolás megváltoztatja. mint a Shape* // . Az auto_ptr sablont a <memory> fejállomány deklarálja. // . Egy auto_ptr-nek alapvetõen egy mutatóval adhatunk kezdõértéket és ugyanúgy hivatkozhatunk a mutatott objektumra. a lefoglalt memóriát p (automatikusan meghívott) destruktora fogja felszabadítani.. 14. mint egy hagyományos mutatónál. Ha az init() kivételt vált ki.

// másol. auto_ptr<Circle> p3 = p2.ptr=0 X& operator*() const throw() { return *ptr. // destruktív másolás auto_ptr-bõl Az auto_ptr_ref célja. } X* get() const throw() { return ptr.. ha több auto_ptr is mutat egy objektumra. // Ezt ne tegyük: a rendezéstõl v sérülhet . } void reset(X* p =0) throw() { if (p!=ptr) { delete ptr.ptr=0 template<class Y> auto_ptr(auto_ptr<Y>& a) throw().484 Absztrakciós módszerek // figyeljük meg: másolás és értékadás nem konstans paraméterekkel auto_ptr(auto_ptr& a) throw(). ptr=p.ptr=0 template<class Y> auto_ptr& operator=(auto_ptr<Y>& a) throw(). majd a. Jegyezzük meg.begin(). // másolás auto_ptr_ref-be template<class Y> operator auto_ptr<Y>() throw(). mi történik. hogy az auto_ptr destruktív másolási módszere miatt nem teljesíti a szabványos tárolók elemeire vagy a sort()-hoz hasonló szabványos eljárásokra vonatkozó követelményeket: vector< auto_ptr<Shape> >& v.. // veszélyes: auto_ptr használata tárolóban // . Shape* ps = p3. } } // mutató kinyerése // tulajdonosváltás }.v. majd a.end()). // másol. hogy az objektum kétszer törlõdik – ami hiba.get(). // másol. de az a legvalószínûbb. Ha egy D*-ot B*-gá lehet alakítani. akkor a sablonkonstruktor és a sablon-értékadás (meghatározott módon vagy automatikusan) egy auto_ptr<D>-t auto_ptr<B>-vé tud alakítani: void g(Circle* pc) { auto_ptr<Circle> p2 = pc. auto_ptr<Shape> aps = p3.ptr=0 auto_ptr& operator=(auto_ptr& a) throw(). return t. // másolás auto_ptr_ref-bõl template<class Y> operator auto_ptr_ref<Y>() throw(). ugyanakkor megakadályozza egy konstans auto_ptr másolását. } X* operator->() const throw() { return ptr. ptr=0. p2->m = 7. majd a. majd a. // másol.get()==0 // mutató kinyerése auto_ptr-bõl // tulajdonosváltás és típuskonverzió // programozói hiba: most p4 is felel a // törlésért Az nem meghatározott. auto_ptr<Circle> p4 = pc. auto_ptr(auto_ptr_ref<X>) throw(). } X* release() throw() { X* t = ptr. hogy megvalósítsa a közönséges auto_ptr számára a destruktív másolást. sort(v. } // most p2 felel a törlésért // most p3 felel a törlésért (p2 már nem) // programozói hiba: p2.

Egy könyvtár tervezõje például általában nem élhet feltevésekkel a könyvtárat használó program hibatûrési követelményeit illetõen. 14. így aztán el kell kerülnie minden feltétel nélküli futási idejû hibát és minden erõforrást fel kell szabadítania. X* p5 = new(a) X. hogy a védelme megérje a „kezdeti értékadás az erõforrás megszerzésével” módszernek. X* p3 = new(buffer[10]) X.. Kivételkezelés 485 Világos. hogy az auto_ptr nem egy általános „okos” vagy „intelligens” mutató (smart pointer). Az itt leírt módszer olyan alkalmazások számára hasznos. egyszerûen a bemenetet olvasó és azzal le is futó programnál a súlyosabb futási idejû hibákat úgy is kezelhetjük. így a felhasználó újrafuttathatja a programot egy jobb bemenettel. X* p2 = new X[10]. A „kezdeti értékadás az erõforrás megszerzésével” módszer a kivételeknek a hibák jelzésére való felhasználásával együtt sok ilyen könyvtár számára megfelelõ. Ezzel a rendszerre bízzuk a program által lefoglalt összes erõforrás felszabadítását. X* buffer) { X* p1 = new X.4. az auto_ptr-nek. // X átmeneti tárba helyezése (nem // szükséges felszabadítás) // tárfoglalás az 'a' Arena-tól (a-t kell // felszabadítani) .) alkalmazásának fáradságát. hogy egy alkalmas hibaüzenet kiadása után a programot leállítjuk.4. amelyeknél a hibák ilyesféle egyszerûsített kezelése elfogadhatatlan..14. A kivételek és a new operátor Vegyük a következõt: void f(Arena& a. illetve a catch(.4. 14. } X* p6 = new(a) X[10].3. mielõtt egy könyvtári függvény visszatér. De amire tervezték – az automatikus mutatók kivételbiztos kezelésére – arra lényeges „költség” nélkül megfelel. Figyelmeztetés Nem minden programnak kell mindenfajta hibával szemben immunisnak lennie és nem minden erõforrás annyira létfontosságú. X* p4 = new(buffer[11]) X[10]. Például sok.

hogy megpróbálná maga orvosolni a helyzetet és a hiba felléptének helyérõl folytatni a számítást. ha X konstruktora kivételt vált ki? Felszabadul-e a new() operátor által lefoglalt memória? Közönséges esetben a válasz igen. mint minden olyan esetet. Az elhelyezõ utasítás némely felhasználása lefoglal némi memóriát.4.5.4. . amely egy „sikertelen” számításból a hívó által adott hibakezelõbe tér vissza (ami aztán újra megpróbálhatja a számítást elvégeztetni). így p1 és p2 kezdeti értékadása nem okoz memória-elszivárgást. hogy segítséget adjon egy ismeretlen kódrészletnek annak erõforrás-lefoglaló problémájában. más módon nem próbál felszabadítást végezni. Az erõforrások kimerülése Visszatérõ programozási dilemma. §10.6.11). Ha egy Z::operator new() memóriafoglaló szerepelt. hanem csak az adott számítás. mi van. amikor a programozó összeillõ lefoglaló és felszabadító függvénypárt írt. de némelyik nem. A tömbök kezelése ezzel azonos módon történik (§15. csakúgy.11) használjuk. Az elsõ esetben a hívónak fel kell készülnie arra. ahelyett.1). Újrakezdés: kérjünk segítséget valamelyik hívó függvénytõl és folytassuk a program futását. Ilyen problémával szembesülve a programozók kétféle megoldást szoktak alkalmazni: 1. így aztán jellemzõen nem szabványos módon is kell felszabadítani azt. A „befejezés” (termination) azon eljárás hagyományos neve. A második eset a legtöbbször sokkal egyszerûbb és a rendszer elvonatkoztatási szintjeinek jobb szétválasztását teszi lehetõvé. 2. amit aztán fel kellene szabadítani. hogy mi történjék.4. akkor a rendszer meghívja a Z::operator delete()-et. hogy a „befejezés” választásakor nem a program futása fejezõdik be. Következésképpen a továbbiak a felhasznált memóriafoglalótól (allokátortól) függnek. ha nincs meg a fájl vagy hogy kifogytunk-e a szabad tárból. A második esetben a hívónak arra kell felkészülnie. hogy kezelje az erõforrás-lefoglalás sikertelenségébõl adódó problémát. Befejezés: hagyjuk abba a számítást és térjünk vissza a hívóhoz.486 Absztrakciós módszerek Mi történik. 14. a válasz nem ennyire egyszerû. anélkül. ha nem sikerül egy erõforrás lefoglalási kísérlete. például mert korábban meggondolatlanul nyitogattunk fájlokat (az fopen()nel) és foglaltunk le memóriát a szabad tárban (a new operátorral). Ez az eljárás helyesen kezeli a standard könyvtárbeli elhelyezõ new operátort (§10. Ezenkívül az elhelyezõ utasítás alkalmazásának éppen a memória nem szabványos lefoglalása a lényege. hogy törõdtünk volna vele. feltéve. hogy van ilyen. Ha az elhelyezõ utasítást (placement syntax. Jegyezzük meg.

.14. a szabványos set_new_handler() függvény által fenntartott függvénymutató. A _new_handler ekkor kivételt válthat ki. az operator new() más változatai más módokat választhatnak. a new() meghívja a _new_handler-t. Ha sikerült memóriát találni. _new_handler(). Ha az talál a malloc() számára lefoglalható memóriát.. Mindkettõt szemlélteti a standard könyvtárbeli new() operátor egy egyszerû megvalósítása és felhasználása: void* operator new(size_t size) { for (. Kivételkezelés 487 A C++-ban az újrakezdõ modellt a függvényhívási eljárás..) { if (void* p = malloc(size)) return p. ilyen módon valamelyik hívóra hagyva a helyzet tisztázását: void my_new_handler() { int no_of_bytes_found = find_some_memory(). a new() operátor visszaadhatja az arra hivatkozó mutatót. if (_new_handler == 0) throw bad_alloc(). Ha nem. . } catch (bad_alloc) { // valahogy reagálunk a memória kifogyására } A new() operátor ezen változatában használt _new_handler egy. if (no_of_bytes_found < min_allocation) throw bad_alloc(). ha nem. ezt írhatjuk: set_new_handler(&my_new_handler). akkor a kezelõ nem térhet vissza a new() operátorba végtelen ciklus okozása nélkül. a megfelelõ kivételkezelõvel: try { // . } } // megpróbálunk memóriát találni // nincs kezelõ: feladjuk // segítséget kérünk Itt a C standard könyvtárának malloc() függvényét használtam a szabad memóriahely tényleges megkeresésére. Ha saját my_new_handler()ünket szeretnénk _new_handler-ként használni. akkor minden rendben. a befejezõ modellt a kivételkezelõ eljárás támogatja. } // feladjuk Valahol lennie kell egy try blokknak is.

hogy a memória elfogyásakor is maradjon annyi tartalék..12[1]). ha az ilyen függéseket a legkisebb mértékre szorítjuk vissza. ezt írhatjuk: void f() { void(*oldnh)() = set_new_handler(&my_new_handler). // .. a két kód annál inkább függ egymástól. ám minél több jut el belõle egy futási idejû hiba észlelésének helyétõl a kijavítást segítõ helyig. amennyi egy bad_alloc kiváltásához kell.488 Absztrakciós módszerek Ha el akarjuk kapni a bad_alloc kivételt is. } catch (bad_alloc) { // . try { // kezelõ újbóli beállítása // továbbdobás // kezelõ újbóli beállítása Még jobb. Az egyes C++változatoktól elvárjuk.) kezelõt a „kezdeti értékadás az erõforrás megszerzésével” módszernek (§14. mint a hívó által biztosított segédfüggvények meghívása. . throw. Általában célszerû az erõforrások megszerzését rétegekbe. } } set_new_handler(oldnh). hogy valamilyen más kivétel dobása a memória elfogyásához vezet. Nagyobb rendszerek esetében a tapasztalat azt mutatja. Könnyû több információt közvetíteni.. ha a catch(. Ebbõl adódóan az egyik kódrészleten csak a másikat értve és esetleg azt módosítva lehet változtatni. A különbözõ helyen levõ programrészek elszigetelésére jó módszer... A kivételek kiváltásához szükség van egy „eldobandó” objektumra... hogy a sikeres rendszerek ezt az utat követik. hogy egy réteg a hívó réteg segítségére szoruljon.4) a _new_handler-re való alkalmazásával elkerüljük (§14. de lehetséges.) { set_new_handler(oldnh). és elkerülni. elvonatkoztatási szintekbe szervezni. A kivételkezelõ eljárás jobban támogatja az elszigetelést.. A _new_handler alkalmazásával a hiba észlelésének helyétõl semmiféle további információ nem jut el a segédfüggvényig. } catch (.

a hagyományos (azaz kivételeket nem alkalmazó) lehetõségek a következõk: 1. Kivételkezelés 489 14. Állítsunk be egy nem lokális változót (például az errno-t). Ez a függvény jelenti majd a hibát. hogy a létrehozás sikertelenségére vonatkozó információt a konstruktorból átadjuk. 4. hogy ellenõrzi a változót. 3. // . Ne végezzünk kezdeti értékadást a konstruktorban és bízzunk meg a felhasználóban.. // . return p.. Minthogy a konstruktorok nem adnak vissza külön visszatérési értéket. 2. megbízva a hívóban. amelyet a hívó megvizsgálhatna. és megpróbálhatunk valami értelmeset kezdeni velük: Vector* f(int i) { try { Vector* p = new Vector(i). enum { max = 32000 }. hogy az elsõ használat elõtt elvégzi azt. A kivételkezelés lehetõséget ad arra. Egy egyszerû Vector osztály például így védekezhet a túlzott igények ellen: class Vector { public: class Size { }. A Vector-okat létrehozó kód most elkaphatja a Vector::Size hibákat. és végezze az elsõ meghívott tagfüggvény a kezdeti értékadást. Kivételek konstruktorokban A kivételek adnak megoldást arra a problémára. ha a kezdeti értékadás nem sikerült.. Vector(int sz) { if (sz<0 || max<sz) throw Size(). } }.. Adjunk vissza egy hibás állapotú objektumot. és bízzunk a felhasználóban.. hogy az majd ellenõrzi az állapotot. } .4.14.. // . hogyan jelentsünk hibát egy konstruktorból. hogy jelezze a létrehozás sikertelenségét. Jelöljük meg az objektumot „kezdõérték nélküli”-ként.6.

ha a teljes függvénytörzset a tag kezdõérték-listájával együtt egy try blokkba zárjuk: class X { Vector v.4). Ez lényegében a sok erõforrás kezelésének problémáját az egy erõforrást kezelõ egyszerûbb eljárás ismételt alkalmazására vezeti vissza. } // v kezdõértéke s // a v által kiváltott kivételt itt kapjuk el . } catch (Vector::Size) { // .. public: X(int).. de maga a konstruktor is elkaphatja azt... }. Más szóval. Valahányszor a hívó egy kivételt kap. Ha a kivétellel együtt a megfelelõ információ is továbbítódik. ha egy tag kezdeti értékadása közvetlenül vagy közvetve kivételt vált ki? Alapértelmezés szerint a kivételt az a hívó függvény kapja meg. amelyik a tag osztályának konstruktorának meghívta. // . és ezt ráadásul megbízhatóan és kényelmesen tegyük.490 Absztrakciós módszerek } catch(Vector::Size) { // mérethiba kezelése } Mint mindig. Kivételek és a tagok kezdeti értékadása Mi történik. 14. a hibakezelõ módszerek alapvetõ célja az..6. // . X::X(int s) try :v(s) { // . a probléma kezeléséhez rendelkezésre álló ismeretek halmaza akár bõvülhet is. maga a hibakezelõ az alapvetõ hibakezelõ és -helyreállító módszerek szokásos készletét alkalmazhatja. A „kezdeti értékadás az erõforrás megszerzésével” eljárás az egynél több erõforrást igénybe vevõ konstruktorok kezelésének legbiztosabb és legelegánsabb módja (§14....4. megváltozik annak értelmezése.1. ahol elegendõ ismeret áll rendelkezésre a hiba következményeinek elhárításához. hogy mi volt a hiba. hogy a hiba felfedezésének eredeti helyérõl olyan helyre jutassunk el információt.

3.4. .3) vagy egy delete hívás folytán (§10. és E.5) stb. Az utóbbi esetben a kivétel nem léphet ki a destruktorból.7.7). amely destruktorral bíró objektumot tartalmaz. Normál (szabályos) meghívás: a hatókörbõl való szabályos kilépéskor ((§10. Ez lehetõvé teszi a destruktor attól függõ programozását. Kivételkezelés közbeni meghívás: a verem visszatekerése közben (§14. akkor ez ellen védekezhet: X::~X() try { f().2. hogy az objektum szabályos vagy a verem visszatekerése közbeni megsemmisítésérõl van-e szó.) { // valamit csinálunk } A standard könyvtárbeli uncaught_exception() függvény true-val tér vissza. függelékekben részletesen tárgyaljuk a kivételkezelés és a tárolók erõforrás-gazdálkodásának kapcsolatait. Végülis a kivételkezelõ eljárásnak és a destruktornak általában nincs módja eldönteni.4... ami kivételt válthat ki. az értékadásnak biztosítania kell. hogy mindkét operandusát azonos állapotban hagyja.2. Más esetben megszegjük a standard könyvtárbeli elõírásokat.3. 14.4. Ebben az esetben az objektum nem jön létre.4. Az E.3. Ha megteszi. 2. Mielõtt ez utóbbit tenné. amely kivételt válthat ki. A kivétel kiváltása elõtt a másoló konstruktor fel kell szabadítsa a lefoglalt erõforrásokat. hogy elfogadható-e az egyik kivételnek a másik kedvéért való elhanyagolása. // kivételt válthat ki } catch (. amelyet még nem kaptak el. A vector másoló konstruktora például memóriát foglal le és átmásolja az elemi objektumokat (16.14. Kivételkezelés 491 14. ha van olyan „eldobott” kivétel..4. Kivételek és másolás Más konstruktorokhoz hasonlóan a másoló konstruktor is jelezheti a hibás futást kivétel kiváltásával. Ha a destruktor olyan függvényt hív meg. az a kivételkezelõ eljárás hibájának számít. Kivételek destruktorokban A kivételkezelõ eljárás szemszögébõl nézve egy destruktort kétféleképpen lehet meghívni: 1.6. E.). és meghívódik az std::terminate() (§14.4) a kivételkezelõ eljárás elhagy egy blokkot.2. A másoló értékadó operátor ugyanígy lefoglalhat erõforrásokat és kiválthat kivételt. melynek következménye nem meghatározott viselkedés lehet.

amelyek nem hibák Ha egy kivételre számítunk és elkapjuk és így annak nincs a program mûködésére nézve rossz következménye..) { X m = q. ha a sor üres Ez egészen jónak tûnik. A standard könyvtár például tartalmaz egy tetszõleges elemekbõl álló queue-t (sor) is. } } catch (Queue<X>::Empty) { return. a kivételeket úgy is tekinthetjük. const string& s) { // megtalálta s-t . } Tree* find(Tree* p.5. akkor miért lenne hiba? Csak mert a programozó a kivételre mint hibára és a kivételkezelõ eljárásokra pedig mint hibakezelõ eszközökre gondol? Nos.s).s). if (p->right) fnd(p->right.2). mintha vezérlõszerkezetek lennének: void f(Queue<X>& q) { try { for (.3. } } // 'Empty' kivételt vált ki. Ezért kivételeket csak ott használjunk. ha a kivételre ténylegesen sor kerül. Kivételek. mint az if–hez vagy a for-hoz hasonló helyi vezérlési szerkezetek és azoknál gyakran kevésbé hatékony is. A kivételkezelés kevésbé rendezett eljárás. így ebben az esetben tényleg nem teljesen világos. ahol a hagyományosabb vezérlési szerkezetek nem elegánsak vagy nem használhatóak..492 Absztrakciós módszerek 14. if (p->left) fnd(p->left. mi számít hibának és mi nem. A keresõfüggvényeknél – különösen a rekurzív hívásokat nagymértékben alkalmazó függvényeknél (például egy fában keresõ függvénynél) – jó ötlet befejezésként kivételt alkalmazni: void fnd(Tree* p. kivételek alkalmazása nélkül (§17.. // . const string& s) { if (s == p->str) throw p.get().

ami szokványos esetben meghívja az abort()-ot. } catch (Tree* q) { return q.s).4. ami segít egy világos modellt kialakítani arról. Ez érthetõbbé teszi a kódot. x3) { // törzs } . (Részletesen lásd a §9.6.1 pontban. Kivételek specifikációja A kivétel kiváltása vagy elkapása befolyásolja a függvénynek más függvényekhez való viszonyát.14. 14. mi számít hibának és hogyan kezeljük. amelyeket a függvény kiválthat.) Valójában a void f() throw (x2. Az unexpected() alapértelmezett jelentése std::terminate(). a program szerkezete pedig ezt (bizonyos fokig kívánatos módon) tükrözni fogja. Ha ésszerû. ragaszkodjunk „a kivételkezelés hibakezelés” elvhez. akkor ez a kísérlet az std::unexpected() hívássá fog átalakulni. milyen kivételek léphetnek fel végrehajtása közben. x3). void f(int a) throw (x2. // q->str==s Ugyanakkor a kivételek ilyen használata könnyen túlzásba vihetõ és áttekinthetetlen kódhoz vezethet. Ekkor a kód világosan két részre különül: közönséges és hibakezelõ kódra. Ha egy függvény megadja. Ha a függvény futása közben valamit olyasmit próbál tenni. Ezért érdemes lehet a függvény deklarációjával együtt megadni azon kivételeket is. Becsüljünk meg mindent. Sajnos a „való” világ nem ilyen tiszta. } return 0. A hibakezelés lényegénél fogva nehéz. Kivételkezelés 493 try { } fnd(p. akkor ezzel valójában garanciát nyújt a használóinak. ami ezt a garanciát érvénytelenné tenné.1.

hogy „aknázzák alᔠa kivételkezelõ rendszert és hogy hibás kódot írjanak a kivételek elfojtására. A függvénydefiníciók viszont nem általánosan elérhetõek. } catch (x3) { throw. az lenne a jó alapértelmezés. hogy a függvény deklarációja a hívók által elérhetõ felület része. 14. Ez aztán arra ösztönözné a programozókat. ez pedig az aknamunkát észre nem vevõknek hamis biztonságérzetet adna. } catch (. } // továbbdobás // továbbdobás // az unexpected() nem fog visszatérni Ennek legfontosabb elõnye. ráadásul a megkivétel-specifikációkkal (exception specification) ellátott függvénydeklarációk sokkal rövidebbek és világosabbak. A kivétel-specifikációk fordítási idejû ellenõrzésének szabályai a könnyen felderíthetõ hibákat tiltják. hogy a függvény azok mindegyikét ki fogja váltani. // nem vált ki kivételt Azt gondolhatnánk.. de azért fordításkor sok ellenõrzés történik.494 Absztrakciós módszerek egyenértékû a következõvel: void f() try { // törzs } catch (x2) { throw.. határozottan nem szeretünk sûrûn beléjük nézegetni. A kivétel-specifikációk nélküli függvényekrõl azt kell feltételeznünk. Ekkor azonban alapvetõen minden függvény részére szükséges lenne kivételeket meghatározni.6. mint az egyenértékû kézzel írott változat. .1. hogy bármilyen kivételt kiválthatnak: int f().) { std::unexpected(). Ha hozzá is férünk az összes könyvtár forrásához. hogy a függvény nem váltana ki kivételt. A kivételek ellenõrzése Egy felületnek fordítási idõben nem lehet minden megsértési kísérletét ellenõrizni. emiatt pedig sokszor kellene újrafordítani és meg is akadályozná a más nyelveken írt programokkal való együttmûködést. A specifikált (tehát „engedélyezett”) kivételeket a fordító úgy értelmezi. // bármilyen kivételt kiválthat A kivételt ki nem váltó függvényeket üres listával adhatjuk meg: int g() throw ().

ha a kivétel-specifikációk megsértését nem lehet majd futási idõben elkapni. ahol a nagyobb frissítések költségesek és nincs is meg az összes forrás. .. Kivételkezelés 495 Ha egy függvény valamely deklarációja megad kivételeket is. hogy ez ne történjen meg. void h() throw(X. de a nagyobb rendszerek legtöbbje számára fontos. }.Y). mint B::g() // hiba: D::h() megengedõbb. class D : public B { public: void f() throw(X). A lényeg az. betartja a felülírt függvény kivétel-specifikációjában felállított szabályokat. mint maga az eredeti függvény: class B { public: virtual void f(). Természetesen az adott nyelvi változat ellenõrizhet így.14. // bármit kiválthat // rendben // rendben: D::g() szigorúbb. vagy ha mégis.. amely kevesebb kivételt vált ki. annak elkapását nem várhatnánk el a hívótól. int f() { // . mint B::h() Ezt a szabályt a józan ész diktálja. csak akkor jelezzen végzetes hibát. hogy a kivételek fordítási egységek határain átnyúló ellenõrzése nem kötelezõ. amely legalább annyira szigorúan határozza meg a kivételeket. hogy új kivétellel való bõvítés ne kényszerítse ki a kapcsolódó kivételspecifikációk kijavítását és az esetleg érintett összes kód újrafordítását. A rendszer így egy félig felújított állapotban tovább mûködhet és a váratlan kivételek dinamikus (futási idõbeli) észlelésére hagyatkozhat. Ha egy származtatott osztály olyan kivételt váltana ki. } // hiba: a kivétel-meghatározás hiányzik Fontos szempont. virtual void g() throw(X. void g() throw(X). virtual void h() throw(X). ami alapvetõ az olyan nagy rendszereknél. amit az eredeti függvény nem adott meg lehetségesként. Egy virtuális függvényt csak olyan függvénnyel lehet felülírni. Másrészt egy felülíró függvény. }. akkor mindegyik deklarációjának (és definiciójának is) tartalmaznia kell pontosan ugyanazokat a kivételeket: int f() throw (std::bad_alloc).Y).

// rendben // hiba: f() megengedõbb. Yerr.496 Absztrakciós módszerek Ugyanígy egy. A fenti esetében a void f() throw (Xerr. mint pf2 Kivétel-specifikáció nélküli függvényre hivatkozó mutatót kivételeket meghatározó függvénymutatóhoz különösképpen nem rendelhetünk: void g().Y) = &f. // hiba 14. */ }. void (*pf2)() throw() = &f. Így tehát f() a Some_Yerr osztályú hibát is továbbítani fogja és f()-ben semmilyen Yerr nem fog unexpected() hívást kiváltani. exception). mint pf3 void (*pf3)() throw(X) = &g. de fordítva nem: void f() throw(X).. A kivétel-specifikáció nem része a függvény típusának.2. Az ilyen hívások a tesztelésen kívül általában nem kívánatosak.. a kivételeket szigorúbban meghatározó függvényt hozzárendelhetünk egy megengedõbb függvénymutatóhoz. void (*pf1)() throw(X. Egy megfelelõen kidolgozott Y alrendszer gyakran az Yerr osztályból származtatja a kivételeit: class Some_Yerr : public Yerr { /* . Váratlan kivételek Ha a kivételek köre specifikált. . minden Yerr-t továbbítani fog a hívójának.6. A standard könyvtár által kiváltott összes kivétel az exception osztály (§14. de az unexpected() hívásokat is el lehet fogni és ártalmatlanná tenni. // bármit kiválthat // hiba: g() megengedõbb. A kivételek gondos csoportosításával és a felületek megfelelõ meghatározásával viszont elkerülhetõk.10) leszármazottja. az ott nem szereplõ kivételek az unexpected() meghívását válthatják ki. a typedef-ek pedig nem is tartalmazhatnak ilyet: typedef void (*PF)() throw(X).

A bad_exception-nel semmi baj. amely nem hálózati környezethez készült.. void g() throw(Yerr). vagy át kell írnunk g()-t. Ugyanígy a váratlan kivételre adott választ egy _unexpected_handler határozza meg. amely hálózati kivételeket kezel. Kivételek felhasználói leképezése Vegyünk egy g() függvényt.3.3. A memória elfogyását a _new_handler kezeli. hogy az újraírás nem lehetséges vagy nem kívánatos. amelyet a set_new_handler() állít be. class Y { }.std::bad_exception) { // . ha ilyennel találkozik. Ennek az a legegyszerûbb módja. ráadásul az információ. elvész.6. Kivételkezelés 497 14. 14.1. Ekkor az unexpected() egyszerûen egy bad_exceptiont fog kiváltani egy kezelõfüggvény meghívása helyett: class X { }. Tegyük fel. Tételezzük fel. void f() throw(X.14. // bad_exception-t vált ki } A kivételkezelõ ekkor elkapja az elfogadhatatlan Y kivételt és bad_exception típusút vált ki helyette. Kivételek leképezése A programot leállítani kezeletlen kivétel fellépése esetén néha túl könyörtelen eljárás. A g() hálózati környezetben való használatához olyan kódra van szükségünk. Ilyenkor az unexpected() viselkedését kell elfogadhatóbbá tenni. amelyet az <exception> fejállományban megadott std::set_unexpected() állít be: . Természetesen g() nem fog tudni semmit a hálózati kivételekrõl és az unexpected()-et fogja meghívni. a terminate()-nél kevésbé drasztikus – de azért így is meglehetõsen durva beavatkozás. throw Y(). hogy milyen kivétel okozta a problémát. hogy a standard könyvtárbeli std::bad_exception-t felvesszük a specifikált kivételek közé. hogy g() csak a saját „Y alrendszerével” kapcsolatos kivételek kiváltását engedélyezi.6. Ekkor a problémát az unexpected() jelentésének felülbírálásával oldhatjuk meg. hogy g()-t hálózati környezetben kell meghívnunk.. Tegyük fel.

Ezután definiálunk egy függvényt az unexpected() erre az esetre kívánt jelentésével: class Yunexpected : public Yerr { }. Végül elkészíthetjük g()-nek egy hálózati környezetben alkalmazható változatát: void networked_g() throw(Yerr) { STC xx(&throwY). hogy mentettük és visszaállítottuk az _unexpected_handler-t. A váratlan kivételek megfelelõ kezeléséhez létre kell hoznunk egy osztályt. } ~STC() { set_unexpected(old).498 Absztrakciós módszerek typedef void(*unexpected_handler)(). a kivétel-specifikáció nem sérül. } A throwY()-t unexpected()-ként használva bármilyen kivételbõl Yunexpected lesz. public: STC(unexpected_handler f) { old = set_unexpected(f). hogy a „kezdeti értékadás az erõforrás megszerzésével” módszert használhassuk az unexpected() függvényekben: class STC { // tárol és visszaállít unexpected_handler old. A váratlan kivételek engedélyezetté alakításának e módszere alapvetõen a rendszer által a bad_exception-nal nyújtott szolgáltatás rugalmasabb változata. void throwY() throw(Yunexpected) { throw Yunexpected(). . Azáltal. Ha a throwY() olyan kivételt váltott volna ki. } }. amit a specifikáció nem engedélyez. akkor a terminate() hajtódott volna végre. // az unexpected() most Yunexpected-et vált ki g(). unexpected_handler set_unexpected(unexpected_handler). több alrendszer számára tettük lehetõvé – egymásra való hatás nélkül – a váratlan kivételek kezelését. } Mivel az Yunexpected az Yerr-bõl származik.

A clone() függvényt arra használjuk. Ha megpróbál így tenni. azt pedig elvileg egy catch(. El nem kapott kivételek Ha egy kivételt nem kapnak el. az unexpected() maga fog bad_expection-t kiváltani (§14. Például így tudnánk információt gyûjteni a Network_exception-ökrõl: class Yunexpected : public Yerr { public: Network_exception* pe. Kivételek típusának visszaállítása A váratlan kivételek Yunexpected-dé alakítása lehetõvé tenné a networked_g() felhasználójának. hogy azon típusok összes kivételét kezeljük.. Egy unexpected() függvény nem tekinthet el a hibától és nem térhet vissza.14.7. } catch(. A throwY() függvényt az unexpected() hívja meg.2. Yunexpected(Network_exception* p) :pe(p?p->clone():0) { } ~Yunexpected() { delete p.. hogy melyik kivétel leképezése történt meg. 14. hogy megtudja: egy váratlan kivételt képeztünk le az Yunexpected-re. Ez a másolat túléli a verem „visszatekerését”. a verem visszatekerése során meghívott destruktor kivétel kiváltásával próbál véget érni. hogy a kivétel egy másolata számára helyet foglaljon a szabad memóriában. .6. vagy ha egy. akkor az std::terminate() meghívására kerül sor. void throwY() throw(Yunexpected) { try { throw.) { throw Yunexpected(0). Kivételkezelés 499 14. } catch(Network_exception& p) { throw Yunexpected(&p). amelyeket meg tudunk nevezni. } } // A továbbdobott kivételt azonnal el kell kapni! // leképezett kivétel kiváltása A kivételek továbbdobása és elkapása lehetõvé teszi. A terminate() fog meghívódni akkor is.3. Így tehát biztosan van továbbdobható kivétel. ha a kivételkezelõ eljárás a vermet sérültnek találja.3).. } }. Azt azonban nem fogja tudni.6. Egy egyszerû eljárással lehetõvé tehetjük ennek megjegyzését és továbbítását.) kezelõ..

Az exit() függvény visszatérési értékével jelezhetjük a rendszernek. amikor a kivételkezelõ eljárás által megvalósított hibakezelõ stratégia csõdöt mondott és ideje a hibatûrés más szintjére áttérni. különösen a program hibakeresése (debugging) alatt. hogy a destruktorok ne hívódjanak meg. A terminate() alapértelmezés szerint az abort()-ot hívja meg (§9.2): int main() try { // . Ha biztosítani akarjuk a rendrakást egy el nem kapott kivétel esetében. a terminate() meg fogja hívni az abort()-ot. A terminate()-et például használhatjuk arra. Bizonyos rendszereknél alapvetõ. A terminate() meghívása drasztikus intézkedés: akkor kell használni. Ha az adott függvény megpróbálja. Az adott nyelvi változat határozza meg..1. hogy nem fognak visszatérni a hívójukhoz. Ehhez hasonlóan. hogy a program futásának el nem kapott kivétel miatti befejezõdésénél a destruktorok meghívódnak-e. A terminate() meghívásának oka. Ez az alapértelmezés a legtöbb felhasználó számára megfelelõ választás. a main() függvényben a ténylegesen elkapni kívánt kivételek mellé írhatunk egy minden kivételt elkapó kezelõt is (§14. az el nem kapott kivételek kezelését az _uncaught_handler végzi. A visszatérési érték a set_terminate()-nek elõzõleg adott függvény. hogy idõnként a kivételkezeléssel fel kell hagyni kevésbé kifinomult hibakezelési módszerek javára.500 Absztrakciós módszerek A váratlan kivételeket az _unexpected_handler kezeli.. Más rendszerek számára felépítésükbõl adódóan szinte lehetetlen nem meghívni a destruktorokat a kivétel kezelõjének keresésekor. } . amelyet az <exception> fejállományban megadott std::set_uncaught() állít be: typedef void(*terminate_handler)().1). hogy a program futtatását folytatni lehessen a hibakeresõbõl.1.3. Az _uncaught_handler függvényekrõl a rendszer feltételezi. hogy a programból szabályosan vagy nem szabályosan léptünk-e ki (§9. hogy megszakítsunk egy folyamatot vagy esetleg újraindítsunk egy rendszert (új kezdeti értékadással). hogy az abort() a programból való nem szabályos kilépést jelent. amelyet az std::set_unexpected() állít be.4. terminate_handler set_terminate(terminate_handler). Jegyezzük meg.1).4.

\n". illetve a kivételkezelést a C hívási sorrendjével és a hibakeresõk szabványos eljárásaival is össze szeretnénk egyeztetni. ami újabb ok arra. Ha azonban a memóriaigényt is csökkenteni szeretnénk. hogy az ne járjon a futási idõ növekedésével. amelyek következményeit a programban nem küszöböljük ki. A kivételek és a hatékonyság Elvileg lehetséges a kivételkezelést úgy megtervezni. programok vagy fejlesztõk számára elõnyösebb lehet nem elkapni azokat a kivételeket. } catch (std::bad_alloc) { cerr << "A new kifogyott a memóriából..8. Egy nem lokális statikus objektum kezdeti értékadása közbeni throw esetében csak a set_unexpected() (§14.2) segítségével kaphatjuk meg a vezérlést.) { // . } Ez a globális változók konstruktorai és destruktorai által kiváltottak kivételével minden kivételt elkap. amelyeknél a kód felét a hibakeresésre szánták. mint amit egy hibakeresõ (debugger) tudhat a program állapotáról. vagyis kevesebb információt kapunk.14. hogy egy kivétel kiváltása ne legyen különösebben költséges egy függvényhíváshoz képest. . hogy a kivételek használatának alternatívái sincsenek ingyen. hogy lehetõség szerint kerüljük a globális változókat. Ezért bizonyos C++ fejlesztõkörnyezetek. ha nem kerül sor kivétel kiváltására. A globális változók kezdeti értékadása alatt fellépõ kivételeket nem lehet elkapni.6... ha nem is lehetetlen. de nehéz feladatra vállalkozunk – de emlékezzünk arra. Kivételkezelés 501 catch (std::range_error) { cerr << "Tartományhiba: már megint!\n". Ráadásul ezt úgy is meg lehet tenni. Nem szokatlan olyan hagyományos rendszerekkel találkozni. } catch (. A kivételek kiváltásánál a kivétel fellépésének pontos helye általában nem ismert.. 14.

6) nagyon hasznos lehet a fordító által létrehozott kód javítására. hogy g() nem vált ki kivételt. . vagyis a kivételkezelõ rendszerre hagyni. if (g(1)) if (g(2)) return true. } A programozók szokás szerint persze nem kezelik ennyire módszeresen a hibákat. // . és ennek ismeretében jobb kódot készíthet. és az nem is mindig létfontosságú. hogy a C-nek csak néhány standard könyvtárbeli függvénye vált ki kivételt. Érdemes megjegyezni. A kivételek specifikációja (§14.. void f() { string s. hanem valami ilyesmi: bool g(int). akkor az f() számára létrehozott kódon javíthattunk volna: void g(int) throw(). így a legtöbb programban az összes C függvény az üres throw() kivétel-meghatározással adható meg. Ha az alábbi módon kijelentettük volna. } Ám g() kivételt válthat ki. else return false. így f()-nek tartalmaznia kell a kivétel fellépte esetén s-et megsemmisítõ kódot. else return false. akkor jobb azt a számítógépre. bool f() { string s. amely kivételek helyett hibákat kezel. Az adott nyelvi változat tudhatja.502 Absztrakciós módszerek Vegyünk egy egyszerû f() függvényt. De ha gondos és módszeres hibakezelés szükséges.. // . valamilyen más módon kellett volna a hibát jeleznie. nem a fenti egyszerû kód lenne. Ha g() nem váltott volna ki kivételt. hogy a hagyományos C függvények nem váltanak ki kivételt.. például az atexit() és a qsort(). Így a fentivel összehasonlítható hagyományos kód. g(1). amelynek látszólag semmi köze nincs a kivételkezeléshez: void g(int). g(2)..

hogy vagy teljes sikerrel járjon vagy egy pontosan meghatározott módon legyen sikertelen. A sikeres hibatûrõ rendszerek többszintûek. hogy a new operátort használja. gondoljuk meg. hogy menekülési utat hagy arra az esetre. Elõször is fel kell adni azt a tévhitet. anélkül. Kivételkezelés 503 Mielõtt egy „C függvényt” ellátnánk az üres kivétel-meghatározással (a throw()-val). A terminate() ezt a kezelési módot azzal támogatja. amely ugyanezt teheti. hogy a hibakezelés módjára legjobb a rendszer tervezésének legkorábbi szakaszában gondolni. hogy átfogó stratégia érvényesüljön. egy bonyolult módszerhez nem tudnánk következetesen alkalmazkodni. Ebbõl következik. nem válthat-e ki kivételt. ha maga a kivételkezelõ rendszer sérülne vagy nem tökéletes használata miatt maradnának nem elkapott kivételek. 14. így alapvetõ jelentõségû. hogy túlságosan megszenvedne vele. A kivételkezelõk hatékony használatához átfogó stratégiára van szükségünk.14. Egy. ez csak bonyolítaná a helyzetet. Vagyis a program különbözõ részeinek egyet kell érteniük abban. és hogy az alkalmazott módszernek (a teljes program összetettségéhez képest) egyszerûnek és pontosan meghatározottnak kell lennie. Nagyobb programok esetében a következõ okokat sorolhatjuk fel: . hogy „tûzfalként” viselkedjen. a maradék kezelését viszont a magasabb szintekre hagyja. ha a kivételek specifikációjára épülõ hibakezelõ rendszer mégsem jelentene a kivételek számára áthatolhatatlan falat. Mindegyik szint annyi hibával birkózik meg. hogy a program más részeit értesítse egy kivételes körülmény észlelésérõl. A feltevés az. amely viszont bad_alloc-ot válthat ki vagy olyan C++-könyvtárat hívhat meg. hogy az összes hibát egyetlen eljárással kezelhetjük. Nem minden függvény kell.9. a lényegénél fogva annyira kényes területen. az programról programra és programozóról programozóra változik. A hibakezelés egyéb módjai A kivételkezelõ eljárás célja. Az unexpected() célja ugyanígy az. amennyivel csak tud. hogy a két rész függetlenül készült és a kivételt kezelõ rész tud valami értelmeset kezdeni a hibával. hogy lehetõvé tegye egy programrész számára. hogy menekülési utat hagyjon arra az esetre. A legtöbb rendszerben nem lehet minden függvényt úgy megírni. A kivételkezelõ eljárás lényegénél fogva nem lokális. hogyan használják a kivételeket és hol kezelik a hibákat. Például átírhatták. Hogy ez miért nem lehetséges. mint a hibakezelés.

amely végül aláásná a teljes rendszer megbízhatóságát. alapvetõ. minden programrészre kiterjedõ hibakezelési stratégia megalkotásakor figyelembe kell vennünk az egyes. c_function(). 4. } } extern "C" void call_from_C() throw() { . hogy következetesen mindenhol érvényesüljön. mielõtt a C++-könyvtárból visszatérünk egy C programba: void callC() throw(C_blewit) { errno = 0. 2. Ugyanakkor egy program olyan különálló részrendszerekre bontása. amelyek vagy teljes sikerrel járnak. Az a cél. részrendszert vagy kulcsfontosságú függvényt így kell megtervezni. Ez a pusztán helyi értelemben vett „megbízhatóság” olyan bonyolultsághoz vezetne. Alkalmasint szükséges lehet az egyik stílusú hibajelzésrõl a másikra áttérni. Ehhez pedig tekintetbe kell vennünk olyan kérdéseket. Más nyelven írt függvények nem alkalmazkodnának a szabályokhoz. A szükséges tárterület és futási idõ növekedése nagyobb lenne az elfogadhatónál (mert rendszeresen ugyanazon hibák – például érvénytelen paraméterek – ismételt ellenõrzésére kerül sor). elkaphatunk egy kivételt és az errno-t beállíthatjuk. vagy fordítva. mint hogy a programrészlet hogyan kezeli az erõforrásokat. if (errno) { // takarítás (ha lehetséges és szükséges) throw C_blewit(errno). a miénktõl eltérõ módszert alkalmazó programrészleteket is. megtehetõ és gazdaságos.504 Absztrakciós módszerek 1. ha valójában eltérõ belsõ eljárást alkalmaz. Túl sok munka kellene ennek a „megbízhatóságnak” olyan biztosításához. vagy meghatározott módon lesznek sikertelenek. Például egy C könyvtári függvény meghívása után ellenõrizhetjük az errno-t és kivételt válthatunk ki. A kivételeket ilyen könyvtárak vagy részrendszerek felületei számára célszerû meghatározni. Egy fõbb könyvtárat. hogy egy rendszer összes kódját „a nulláról” készíthessük el. 3. Ezért egy általános. Rendszerint nem adatik meg az a luxus. hogy a programrészlet látszólag akkor is az általános hibakezelési módszer szerint kezelje a hibákat. vagy milyen állapotba kerül egy hibája után a rendszer.

§19.) { // takarítás (ha lehetséges és szükséges) errno = E_CPLPLFCTBLEWIT. mint a „kezdeti értékadás az erõforrás megszerzésével”.6.7. általános eszközöket mutatja be: Szabványos (a nyelvbe beépített) kivételek Név bad_alloc bad_cast bad_typeid bad_exception Kiváltó new dynamic_cast typeid kivétel_specifikáció Hivatkozás §6.2. Szabványos kivételek A következõ táblázat a szabványos kivételeket és az azokat kiváltó függvényeket.4.4. } Ilyenkor fontos. 14.10. operátorokat.2. hogyan lehet állapotbiztosítókat (invariánsokat) és feltevéseket használni. a végtelen ciklusok lehetõségével pedig a hibákat kezelõ kódba egy nem kívánatos lehetõséget épít. Az ilyen kérések az egymástól függõ elemek között körbe-körbe járó végtelen ciklusokat okoznak. illetve olyan egyszerûsítõ feltevéseket.6. ne kérjen segítséget vagy erõforrást a hívójától.5 §15. ahol elmagyarázzuk. A hibakezelés – a lehetõségekhez mérten – hierarchikus legyen..1.1 §15.3 Fejállomány <new> <typeinfo> <typeinfo> <exception> . Kivételkezelés 505 try { } c_plus_plus_function(). } catch (. Lásd még a §24. Ezzel a hibakezelõ kódot szabályosabbá tehetjük. hogy a hibajelentõ stílusok átalakítása teljes legyen.14. hogy következetesen járjunk el.4.1 pontot. hogy a kivételek kiváltása szabályosabb legyen. Alkalmazzunk olyan egyszerûsítõ módszereket.3. ami a programot áttekinthetetlenné teszi.. mint „a kivételek hibákat jelentenek”. Ha egy függvény futási idejû hibát észlel.4 §14.

exception(const exception&) throw(). virtual ~exception() throw().6 Fejállomány <stdexcept> <stdexcept> <stdexcept> <stdexcept> <ios> at() bitset<>::operator[ ]() invalid_argument bitset konstruktor overflow_error bitset<>::to_ulong() ios_base::failure ios_base::clear() A könyvtári kivételek a standard könyvtár exception nevû – az <exception> fejállományban megadott – kivételosztályból kiinduló osztályhierarchia tagjai: class exception { public: exception() throw().3 §17. exception& operator=(const exception&) throw().3 §17..5. virtual const char* what() const throw(). private: // .3.3.3..3.3.506 Absztrakciós módszerek Szabványos (a standard könyvtár által kiváltott) kivételek Név out_of_range Ki dobja Hivatkozás §16.5.§20.3.3 §21.5.1 §17. }. A hierarchia így néz ki: exception logic_error runtime_error lenght_error domain_error out_of_range invalid_argument bad_alloc bad_exception bad_cast bad_typeid range_error overflow_error underflow_error ios_base::failure .

az exception-bõl származó kivétel standard könyvtárbeli kivétel. így hiba lenne az összes kivételt az exception elkapásával megpróbálni lekezelni. Kivételkezelés 507 Ez meglehetõsen körülményesnek tûnik ahhoz képest. kipróbálja. A kivételkezelõ rendszer fenntart a saját céljaira egy kis memóriát (például a veremben). // . a többi viszont nem feltétlenül. függvény. hogy a függvényhívásnál vagy a kivételkezelésnél fogy-e ki elõször a memória: void perverted() { try { throw exception(). Így az alábbit írhatjuk: void f() try { // a standard könyvtár használata } catch (exception& e) { cout << "standard könyvtári kivétel " << e.. A többi futási idejû hiba (run-time error).vagy konstruktorparaméterek ellenõrzésével el lehetne kapni. A logikai hibák (logic error) azok. } catch (. A standard könyvtárbeli kivételek az exception által meghatározott függvényeket nem bõvítik újakkal. } // ismétlõdõ kivétel .. Ebbõl következõen egy standard könyvtárbeli kivétel kiváltása nem vált ki bad_alloc kivételt. Jegyezzük meg. hogy a hierarchia megpróbál a standard könyvtárban meghatározott kivételeken kívüli kivételek számára is használható keretrendszert adni.what() << '\n'.. amit ha meghívunk.14. amelyeket elvileg a program indulása elõtt. Hasonlóan hiba lenne feltételezni. amely a rendszer összes memóriáját elfogyasztja és így hibát kényszerít ki. hogy az expection-mûveletek maguk nem váltanak ki kivételt.) { cout << "másik kivétel\n". // .. Természetesen írhatunk olyan kódot. hogy az összes. hogy nyolc szabványos kivételt ölel fel. Íme egy függvény. } // talán A standard könyvtárbeli kivételek az exception-bõl származnak. Oka. Némelyek ezt az összes hiba és kivétel számára hasznos keretrendszernek látják – én nem. hogy ott tárolhassa a kivételeket. csak megfelelõen definiálják a megkívánt virtuális függvényeket... a felhasználók ugyanis hozzáadhatják saját kivételeiket az exception hierarchiához.

2. [16] Legyünk résen. Meghatározott kezelõkód helyett a „kezdeti értékadás az erõforrás megszerzésével” módszert használjuk. [11] A main() függvény kapja el és jelentse az összes hibát. amit megengedtek nekik.4.508 Absztrakciós módszerek } catch (exception& e) { perverted(). §14. 14. §14. [9] Ha egy értékadás vált ki kivételt. hogy minden kivételt kiválthatnak.4. §14. ha a konstruktor kivételt vált ki. hogy egy konstruktorban minden lefoglalt erõforrást felszabadítsunk. } // ismétlõdõ függvényhívás A kimeneti utasítás egyedül azt a célt szolgálja. [13] Gondoskodjunk arról.5. ha a helyi vezérlési szerkezetek elégségesek. §14.4.4. [17] A függvényekrõl tételezzük fel.1. [4] Nem minden programnak kell „kivételbiztosnak” lennie.1. Tanácsok [1] Hibakezelésre használjunk kivételeket.5 és §14.9. [2] Ne használjunk kivételeket. [5] Az invariánsok érvényességének megõrzésére használjuk a „kezdeti értékadás az erõforrás megszerzésével” módszert és a kivételkezelõket. hogy minden kivétel az exception osztály leszármazottja.2. §14. [8] A konstruktorhibák jelzésére váltsunk ki kivételt. Ehelyett váltson ki kivételt és hadd döntsön a hívó. §14.11. [12] Különítsük el a közönséges kódot a hibakezeléstõl. §14. §14. [14] Az erõforrások kezelése hierarchikus legyen. §14.7.3. §14. §14.6.4. §14. §14. §14.9.1. [18] Ne tételezzük fel. §14.4. §14. §14.10.4.4.what().4.9. elõtte gondoskodjon paramétereinek következetes állapotba hozásáról.4. §14. [10] A destruktorokban kerüljük a kivételek kiváltását. . §14. §14.4.2.7. hogy megakadályozza a fordítóprogramot az e nevû kivétel által felhasznált memória újrahasznosításában.6. §14.3.1. [3] Az erõforrások kezelésére a „kezdeti értékadás az erõforrás megszerzésével” módszert használjuk. [7] Nem minden függvénynek kell minden lehetséges hibát kezelnie.9. nehogy a new által lefoglalt és a kivételek fellépte esetén fel nem szabadított memória elszivárogjon. [19] Egy könyvtár ne fejezze be egyoldalúan a program futását. §14.5. §14. [15] A fõbb felületekben határozzuk meg az engedélyezett kivételeket. cout << e.4. [6] Minél kevesebb try blokkot használjunk.

ha egy Exception osztályt használnánk az összes kivételként használt osztály õseként (bázisosztályaként). hibaüzenetté alakítsa azokat.és alulcsordulás esetén kivételt vált ki.5) Írjunk egy teljes Vector sablont Range és Size kivételekkel. (*1) Adott a következõ függvény: int main() { /* .3.11 pontbeli Ptr_to_T osztályt mint egy olyan sablont. olvasására. (*2.5) Vegyük a felhasznált operációs rendszer C felületének állományok megnyitására. 5. (*2) Írjunk visszahívások (callback) megvalósítására alkalmas osztályt vagy sablont. amely egy bináris fa csúcsaiban keres egy char* típusú mezõ alapján. Ez miért nem jó ötlet? 8. 2.9 14. mint az elemi int típus.12[6]-beli Vector összegét a Vector méretének lekérdezése nélkül. §14. (*3. (*1) Írjunk egy ciklust. 3. mi lenne. és írjunk hozzájuk egyenértékû C++ függvényeket. 4. 7. majd meghívja az abort()-ot. 11. amely kivételekkel jelzi a futási idejû hibákat.5) Gondoljuk meg. */ } Változtassuk meg úgy. A hello-t tartalmazó csúcs megtalálásakor a find(”hello”) a csúcsra hivatkozó mutatóval térjen vissza. (*2) Általánosítsuk a §14. amely valamely rendszer számára a konkurens hozzáférést támogatja. . (*2. amely pontosan úgy viselkedik.9 pontbeli call_from_C() függvény nem teljesen kezel minden esetet. írására való alapvetõ mûveleteit. 10. (*3) Írjunk függvényt.6. ami kiszámítja a §14. a keresés sikertelenségét kivétellel jelezzük. §14.12. csak túl.1 pontbeli STC osztályt sablonná. Gyakorlatok 1. amely a „kezdeti értékadás az erõforrás megszerzésével” módszert használja különféle típusú függvények tárolására és visszaállítására. hogy ezt az osztályt kell használni? 9. [21] A tervezés során minél hamarabb alakítsuk ki a hibakezelési stratégiát.1.) Hozzunk létre egy Int osztályt. (*3) Egészítsük ki a §11. Kivételkezelés 509 [20] Egy könyvtár ne bocsásson ki a végfelhasználónak szánt diagnosztikai üzeneteket. (*2. de hiba esetén kivételt váltanak ki.. (*2. Ehelyett váltson ki kivételt és hadd döntsön a hívó. Hogyan nézne ki? Hogyan lehetne használni? Mire lenne jó? Milyen hátrányok származnának abból a megkötésbõl..5) Írjunk egy Lock osztályt. lezárására. 6. Vigyázat: a §14.14. hogy minden kivételt elkapjon. amelyek a megfelelõ C függvényeket hívják meg.

Bevezetés és áttekintés Ez a fejezet a származtatott osztályoknak és a virtuális függvényeknek a más nyelvi elemekkel. a szabad tár kezelésével.” (Andrew Koenig) Többszörös öröklõdés • A többértelmûség feloldása • Öröklõdés és using deklarációk • Ismétlõdõ bázisosztályok • Virtuális bázisosztályok • A többszörös öröklõdés használata • Hozzáférés-szabályozás • Védett tagok • Bázisosztályok elérése • Futási idejû típusinformáció • dynamic_cast • Statikus és dinamikus konverzió • typeid • Kiterjesztett típusinformáció • A futási idejû típusinformáció helyes és helytelen használata • Tagra hivatkozó mutatók • Szabad tár • „Virtuális konstruktorok” • Tanácsok • Gyakorlatok 15.3 Hozzáférés-szabályozás .1. a konstruktorokkal. például a hozzáférés-szabályozással. a névfeloldással. a mutatókkal és a típuskonverziókkal való kölcsönhatását tárgyalja.2 Többszörös öröklõdés §15. Öt fõ részbõl áll: §15.15 Osztályhierarchiák „Az absztrakció szelektív tudatlanság.

Ekkor olyan szimulált egyedeket határozhatunk meg. hogy a felhasználóknak ne kelljen indokolatlan mértékben azzal törõdniük.3 pontokban láttuk. ahol a párhuzamos tevékenységeket a Task (Feladat) osztállyal. Az egyszeres öröklõdésnél (single inheritance) csak egy közvetlen bázisosztály van.5.6 A szabad tár használata Az osztályokat általában bázisosztályok „hálójából” hozzuk létre. hogy egy osztály milyen módon épül fel más osztályokból.5 Tagokra hivatkozó mutatók §15. }.delay(10). az adatgyûjtést és -megjelenítést pedig a Displayed (Megjelenítés) osztállyal ábrázoljuk. public Displayed { // . azaz több osztályt is megadhatunk a : jel után az osztály deklarációjában. függetlenül attól. A Satellite-okra saját mûveleteiken kívül a Task-ok és Displayed-ek mûveleteinek uniója is alkalmazható: void f(Satellite& { s. } s) // Displayed::draw() // Task::delay() // Satellite::transmit() . 15. egy osztálynak több közvetlen bázisosztálya is lehet.2. s. Többszörös öröklõdés Ahogy a §2..4 és §12. illetve az osztályhálók fordítási és futási idejû bejárásának eszközeirõl szól.. hogy a hierarchia melyik osztálya deklarálta a függvényt a meghíváshoz. Vegyünk egy szimulációs programot.4 Futási idejû típusazonosítás §15.transmit(). Ez a fejezet az osztályhálók összeállításának és az osztálytagok elérésének módjairól. mint a Satellite (Mûhold): class Satellite : public Task. Több közvetlen bázisosztály használatát szokás szerint többszörös öröklõdésnek (multiple inheritance) nevezik. akkor mindig ugyanaz a függvény hajtódik végre.draw(). Így például a virtuális függvények meghívási módja biztosítja. s. Az osztályokat célszerû úgy megtervezni.512 Absztrakciós módszerek §15. Mivel a legtöbb ilyen háló hagyományosan fa szerkezetû. az osztályokból álló hálókat vagy osztályhálókat (class lattice) gyakran nevezik osztályhierarchiának (class hierarchy) is. hogy ha egy f() függvényt meghívunk egy objektumra.

a Satellite példát a valóságból merítettük. }. A program mûholdakat. ha egy függvény Task vagy Displayed paramétert vár. // mutató átadása a Satellite Task részére } A program létrehozása nyilván valamilyen egyszerû eljárás alkalmazását követeli meg a fordítóprogramtól.15. hogy a Task-ot váró függvények más részét lássák egy Satellite-nak. Ezen lehetõségek mindegyike csökkenti a rugalmasságot. illetve Task-ként kezelünk. bármilyen meglepõ. virtual void pending() = 0. illetve Satellite::pending() fog meghívódni. class Displayed { // . Displayed és Task osztályok megvalósításának megválasztásában. // felülírja a Task::pending() függvényt void draw(). de nem mindkettõ (hacsak a Task nem a Displayed-bõl származik vagy fordítva). Volt – és talán még mindig van – egy olyan program.. vagy Displayed lehetne. mint a Displayed-et várók. }. // mutató átadása a Satellite Displayed részére suspend(p). ha egy Satellite-ot Displayed-ként. amely a többszörös öröklõdés leírására itt használt minta szerint épült fel. Mi szüksége lehet bárkinek egy Satellite osztályra? Nos. void pending(). Egy Satellite vagy Task. Jegyezzük meg. . hogy Satellite::draw(). Ez biztosítja... hogy ha csak egyszeres öröklõdést használhatnánk. void g(Satellite* p) { highlight(p). void suspend(Task*). A virtuális függvények a megszokott módon mûködnek: class Task { // . public Displayed { // . class Satellite : public Task.. akkor ez a körülmény korlátozná a programozót a Satellite. akkor adhatunk neki egy Satellite-ot is: void highlight(Displayed*). // felülírja a Displayed::draw() függvényt }. virtual void draw() = 0... Osztályhierarchiák 513 Hasonlóan.

514 Absztrakciós módszerek földi állomásokat stb. ha egy földi állomást vihar akadályoz. } // hiba: többértelmû // rendben // rendben Az explicit megnevezés azonban zavaró.1. public Displayed { // . akkor ezeket a függvényeket egyértelmûen kell megneveznünk: void f(Satellite* sp) { debug_info* dip = sp->get_debug(). ha a származtatott osztályban készítünk egy új függvényt: class Satellite : public Task. debug_info* get_debug() { // felülírja a Task::get_debug() és // Displayed::get_debug() függvényeket . illetve a hibakeresés és -elhárítás céljából. illetve részegységeik állapotának tárolására is. és szükség van a Satellite-okhoz hasonló objektumok.. virtual debug_info* get_debug(). class Displayed { // . dip = sp->Displayed::get_debug(). virtual debug_info* get_debug(). ezért általában legjobb elkerülni az ilyen problémákat. Ha egy Satellite-ot használunk. 15.2... meg tudjuk határozni. mûholdas és földi kapcsolatok elõnyeit-hátrányait tudjuk elemezni stb. }. Ennek legegyszerûbb módja. A különbözõ körülmények utánzása számos megjelenítési és hibakeresési mûveletet igényel. A többértelmûség feloldása Két bázisosztálynak lehetnek azonos nevû tagfüggvényei is: class Task { // . mi történik. az elemzés. Egy ilyen szimuláció birtokában meg tudjuk válaszolni a forgalmi adatokra vonatkozó kérdéseket. magába foglaló hírközlési rendszerek szerkezetének tanulmányozására szolgált... dip = sp->Task::get_debug().. }.

A Telstar::draw minõsített név a Telstar-ban vagy valamelyik bázisosztályában megadott draw-ra vonatkozhat: class Telstar : public Satellite { // . például a Task és Displayed osztályok Satellite-té való „összegyúrásánál” az elnevezésekben megmutatkozó hasonlóság általában nem jelent közös célt. Displayed::draw(). Satellite::Displayed::draw(). a Satellite::get_debug() hívódik meg. } debug_info* dip1 = Task::get_debug(). Amikor az ilyen nevek ütköznek. // hoppá!: rekurzív hívás // megtalálja a Displayed::draw-t // felesleges kétszeri minõsítés Vagyis ha a Satellite::draw nem a Satellite osztályban bevezetett draw-t jelenti. Osztályhierarchiák 515 }.4).2..15. ez gyakran meglepetésként éri a felhasználót: . valahányszor get_debug()-ot hívunk meg egy Satellite objektumra. debug_info* dip2 = Displayed::get_debug(). Satellite::draw(). void draw() { draw(). Ha csak egyet talál. Öröklõdés és using deklarációk A túlterhelés feloldására nem kerül sor különbözõ osztályok hatókörén keresztül (§7. vagyis Task::draw-t és Displayed::draw-t keres. Mivel a Satellite::get_debug() elfedi mindkét bázisosztályának get_debug() függvényét. Ezáltal a Satellite bázisosztályaira vonatkozó információk felhasználását helyhez kötöttük.. akkor azt fogja használni. } }. a különbözõ bázisosztályok függvényei közötti többértelmûségek feloldása pedig nem történik meg a paramétertípus alapján.2. 15. a Satellite::draw ismeretlen vagy többértelmû lesz. Egymással alapvetõen nem rokon osztályok egyesítésekor. akkor a fordítóprogram végignézi a bázisosztályokat. ha többet vagy egyet sem. return dip1->merge(dip2).

}. // . p->Displayed::debug(1).. using B::f.. void g(Satellite* p) { p->debug(1). . }. }.. char f(char). többértelmû: Displayed::debug(int) vagy // Task::debug(double) ? // rendben // rendben De mi van akkor. } // hiba.2) hozhatjuk közös hatókörbe: class A { public: int f(int)...2. ha a prioritás // alacsonyabb p-nél // minél nagyobb 'v... ha a különbözõ bázisosztályokban tudatos tervezési döntés következtében szerepelnek azonos nevek.. public Displayed { // . char f(char).. és a felhasználó számára kívánatos lenne a paramétertípus alapján választani közülük? Ebben az esetben a függvényeket a using deklarációval (§8.. // elfedi A::f(char)-t AB f(AB). void debug(double p). }.516 Absztrakciós módszerek class Task { // . public B { public: using A::f. }. class Displayed { // . p->Task::debug(1). class AB: public A. }. void debug(int v).' annál több hibakeresési információ íródik ki class Satellite : public Task. // információ kiírása csak akkor. class B { public: double f(double). // .

3. }..15.. amelyek egyébként elérhetõek lennének. 15. Például ha mind a Task. ab. mind a Displayed a Link (Kapcsolat) osztályból származott volna. Ismétlõdõ bázisosztályok Azáltal.2. elõfordulhat. hogy a bázis.2).. a Satellite-oknak két Link-je lenne: struct Link { Link* next.és származtatott osztályok függvényeibõl túlterhelt függvények halmazát állítsuk elõ. annak származtatott osztályán. A using deklarációk nem szolgálhatnak kiegészítõ információ elérésére sem. . class Task : public Link { // a Link-et a Task-ok listájához (az ütemezõ listához) használjuk // . }. csak az egyébként is hozzáférhetõ információk kényelmesebb használatát teszik lehetõvé (§15. } // A::f(int) // AB::f(char) // B::f(double) // AB::f(AB) A using deklarációk lehetõvé teszik.3.f(ab). hogy egy osztály kétszer fordul elõ a bázisosztályok között.1)..3) pedig nem szerepelhetnek egy osztály definiciójában és nem vonatkozhatnak osztályra.f('a'). ab.0). mint egyébként (§15. Osztályhierarchiák 517 void g(AB& ab) { ab. ab.2) egy bázisosztály tagjára kell vonatkoznia.2. class Displayed : public Link { // a Link-et a Displayed objektumok listájához (a megjelenítési listához) használjuk // .3. a using direktívák (§8. Egy osztály tagjára vonatkozó using deklaráció nem szerepelhet az osztályon.2.f(2. A származtatott osztályban megadott függvények elfedik (hide) a bázisosztály függvényeit.2.f(1). illetve annak tagfüggvényein kívül.2. Egy osztálydeklaráció using deklarációjának (§8. A bázisosztály virtuális függvényei ugyanúgy felülírhatók (override). hogy több bázisosztály lehet. }.

2. Egy objektumnak saját magát egy fájlból kiolvasni vagy oda visszaírni való képességét például így ábrázolhatjuk: class Storable { public: virtual const char* get_file() = 0.3. override). Két külön Link objektum szolgál a listák ábrázolására és a két lista nem zavarja egymást. 15.2.. A Link-hez hasonlóan többször szereplõ bázisosztályok olyan elemek. p->Task::Link::next = 0. . Ha egy ilyen osztályra olyan pontról kell hivatkozni.3.1. virtual void read() = 0. p->Displayed::Link::next = 0. mint amit a tagokra való többértelmû hivatkozások feloldására használunk (§15. Egy Satellite objektumot így rajzolhatunk le: Link Link Task Satellite Displayed Ha a közös bázisosztályt nem szabad két külön objektummal ábrázolni. } // hiba: többértelmû (melyik Link?) // hiba: többértelmû (melyik Link?) // rendben // rendben Ez pontosan ugyanaz az eljárás.. a többértelmûség elkerülése érdekében a hivatkozást minõsíteni kell: void mess_with_links(Satellite* p) { p->next = 0. amelyeket nem szabad a közvetlenül öröklõ osztályon kívül használni.4) alkalmazhatunk. Felülírás A többször szereplõ bázisosztályok valamely virtuális függvényét a származtatott osztály (egyetlen) függvénye felülírhatja (felülbírálhatja.1).2.2. virtuális bázisosztályokat (§15. ahonnét annak több példánya is látható.518 Absztrakciós módszerek Ez nem gond. Természetesen a Link osztály tagjaira nem hivatkozhatunk a kétértelmûség veszélye nélkül (§15. // . p->Link::next = 0.1).

hogy olyan osztályokat írjon.2 pont írja le. }..6 pont szól.. Osztályhierarchiák 519 }.4.. virtual void write() = 0. // . Receiver::write(). }. class Receiver : public Storable { public: void write(). A felülíró függvény általában meghívja a bázisosztálybeli változatokat és a származtatott osztályra jellemzõ tennivalókat végzi el: void Radio::write() { Transmitter::write(). a §25. Arról. class Radio : public Transmitter. // . // kiírja a Radio-ra jellemzõ adatokat } Az ismétlõdõ bázisosztályokról származtatott osztályokra való típuskonverziót a §15. hogyan lehet az egyes write() függvényeket a származtatott osztályok külön függvényeivel felülírni. Ezt az ötletet így valósíthatjuk meg: class Transmitter : public Storable { public: void write(). void write(). public Receiver { public: const char* get_file().. virtual ~Storable() { } Természetesen több felhasználó építhet erre. // .. ha mentjük az alkotóelemeket és késõbb visszaállítjuk azokat. }. . Például leállíthatunk és újraindíthatunk egy szimulációt. amelyek függetlenül vagy együtt használva jobban kidolgozott osztályokat adnak. void read().15..

különben szükségtelenül nehéz feladat lenne az objektum többszöri tárolásának megakadályozása. // ..4. class Receiver : public virtual Storable { public: void write().. A Storable osztályt például úgy is meghatározhatnánk.. Virtuális bázisosztályok Az elõzõ pont Radio példája azért mûködik. virtual ~Storable(). class Radio : public Transmitter. amelyek jó építõkövei más osztályoknak. Ez azonban az olyan osztályok esetében rendszerint nem igaz. public Receiver { public: void write(). // . Az objektum összes része a Storable azonos példányán kell.520 Absztrakciós módszerek 15. virtual void write() = 0. }. kényelmesen és hatékonyan lehet többszörözni. // . virtual void read() = 0. Storable(const Storable&).. }. . Storable& operator=(const Storable&). private: const char* store. mert a Storable osztályt biztonságosan. A virtuális bázisosztályok (virtual base class) ezt a megosztást segítik.2.. }. hogy osztozzék. }.. A Storable ezen látszólag csekély módosítása után meg kell változtatnunk a Radio szerkezetét is. A származtatott osztály minden virtuális bázisosztályát ugyanaz a (megosztott) objektum ábrázolja: class Transmitter : public virtual Storable { public: void write(). mint ami tartalmazza az objektum mentésére használt fájl nevét: class Storable { public: Storable(const char* s).

például mert a nyelv elõírja. Osztályhierarchiák 521 Ábrával: Storable Receiver Radio Transmitter Hasonlítsuk össze ezt az ábrát a Satellite objektum §15. }.3-beli rajzával.1. // nincs konstruktor // alapértelmezett konstruktor // nincs alapértelmezett konstruktor . A virtuális bázisosztály konstruktorát a teljes objektum. a nem virtuális bázisosztályokat viszont saját részobjektumuk. azaz a legtávolabbi származtatott osztály konstruktora hívja meg (automatikusan vagy közvetlenül): class A { // . Virtuális bázisosztályok programozása Amikor a programozó függvényeket készít egy olyan osztály számára. 15.2. hogy egy virtuális bázisosztály konstruktora csak egyszer futhat le. ami gondot jelenthet. nem tudhatja. hogy a bázisosztály egy adott függvényének meghívására pontosan egyszer kerüljön sor..2.. // . Az öröklõdési gráfban egy adott nevû osztály minden virtuálisként megadott bázisosztályát az osztály egyetlen objektuma ábrázolja. class B { public: B(). }.15. hogy a bázisosztályt meg kell-e osztani egyéb származtatott osztályokkal. class C { public: C(int). ha egy szolgáltatást úgy kell megvalósítani. hogy lássuk a különbséget a közönséges és a virtuális öröklõdés között...4. }. amelynek virtuális bázisosztálya van.

*/ }. .. // az óralap és a mutatók megjelenítése void draw(). // a menü megjelenítése void draw(). ha a virtuális bázisosztály függvényét csak a legtávolabbi származtatott osztályból hívja meg.. }. }. class Window_with_menu : public virtual Window { // a menü kódja void own_draw(). mert egy virtuális draw() függvénybõl akarjuk meghívni azokat. public Window_with_menu { // az óra kódja void own_draw(). A virtuális bázisosztály konstruktora a származtatott osztályok konstruktora elõtt hívódik meg. virtual public B. amely ki tudja rajzolni tartalmát: class Window { // alapkód virtual void draw(). Tegyük fel. Az ablakokat emellett többféle módon díszíthetjük és szolgáltatásokkal egészíthetjük ki: class Window_with_border : public virtual Window { // a szegély kódja void own_draw(). */ } // hiba: C-nek nincs alapértelmezett konstruktora D(int i) : C(i) { /* .. }. amelyre meghívták. virtual public C { D() { /* . // a szegély megjelenítése void draw(). Az own_draw() függvényeknek nem kell virtuálisaknak lenniük.. ami pontosan ismeri az objektum típusát. // rendben // . Ebbõl egy mûködõképes Clock osztályt állíthatunk össze: class Clock : public Window_with_border.. }. }. Szükség esetén a programozó ezt a mûködést utánozhatja is. hogy van egy alapvetõ Window osztályunk..522 Absztrakciós módszerek class D : virtual public A.

// a menü megjelenítése } void Clock::draw() { Window::draw(). 15.2. A többszörös öröklõdés használata A többszörös öröklõdés legegyszerûbb és legnyilvánvalóbb felhasználása két egyébként egymással rokonságban nem álló osztály „összeragasztása” egy harmadik osztály részeként. Window_with_border::own_draw(). Window_with_menu::own_draw(). milyen fajta Window-ra hívták meg: void Window_with_border::draw() { Window::draw(). hogy bármelyik draw() meghívása pontosan egyszer hívja meg a Window::draw()-t.15. // a szegély megjelenítése } void Window_with_menu::draw() { Window::draw(). own_draw(). . Osztályhierarchiák 523 Ábrával: Window Window_with_border Clock Window_with_menu A draw() függvényeket most már úgy írhatjuk meg az own_draw() függvények felhasználásával.5. függetlenül attól. own_draw(). // az óralap és a mutatók megjelenítése } A virtuális bázisosztályokról származtatott osztályokra való konverziót a §15. own_draw(). A Task és Displayed osztályokból a §15.2 pontban összerakott Satellite osztály is ilyen.2 pont írja le.4.

Egy módszernek azonban nem kell „okosnak” lennie ahhoz.) Ha a bázisosztály nem ismételhetõ.524 Absztrakciós módszerek A többszörös öröklõdés ilyen módon való használata egyszerû. A többszörös öröklõdés szerepe itt lényegbevágó. A többszörös öröklõdés lehetõvé teszi a testvérosztályok számára. A kétféle szerep az osztályok stílusában és az alkalmazott hozzáférési szabályokban tükrözõdik. Persze lenne értelme annak.4) és a Clock (§15. hogy a megvalósítás részletei maradjanak rejtve. hatékony és fontos – de nem túl érdekes. A §12. így az adatbekérõ mezõk kezelésén .4. Ez az eset. Ott végül az összes Ival_box osztályt absztrakttá tettük. A többszörös öröklõdés használata absztrakt osztályok készítésére már nagyobb jelentõségû.2. amely a felületet nyújtja. Ez lehetõvé tette. hogy a Popup_ival_slider-t megvalósító osztály nagy része közös legyen a sima Ival_slider-t megvalósító osztályéval. vagyis hogy kizárólag felületek. amely a megvalósításról gondoskodik. hogy befolyásolja a program tervezésének módját. (Lásd a Radio (§15.4. hogy hasznos legyen. hogy egyetlen közös õs jelentette függés bevezetése nélkül osztozhassanak adatokon. Vegyük például újra a §12. Ebben a példában a két bázisosztály logikailag különbözõ szerepet játszik. protected BBslider // megvalósítás { // az 'Ival_slider' és a 'BBslider' által igényelt függvények megvalósítása // a 'BBslider' szolgáltatásainak használata }. hogy a káró alakú öröklési háló akkor kezelhetõ a legjobban.4 pont Ival_box osztályait.2. a másik pedig egy védett (protected) konkrét osztály. Az én véleményem az. mind a megvalósítás virtuális függvényeit felül kell írnia. hogy kifejezzük szerepüket.3-beli BB_ival_slider osztály egy példa erre: class BB_ival_slider : public Ival_slider // felület . akkor virtuális (és nem közönséges) bázisosztályra van szükség. annyiban. és az egyes részeken való osztozás is csak a megvalósítás céljára használt ablakozó rendszer klasszikus hierarchiájában történt. Az egyik egy nyilvános absztrakt osztály.1) példákat. amikor az úgynevezett káró alakú öröklõdés (diamond-shaped inheritance) lép fel. hiszen alapjában véve csak a továbbító függvények megírásától kíméli meg a programozót. ha vagy a virtuális bázisosztály vagy a belõle közvetlenül származó osztályok absztraktak. hogy a lényegi programkód minden részét a megfelelõ megvalósító osztályokba rejtsük. mert a származtatott osztálynak mind a felület. Az eljárás nem befolyásolja számottevõen a program általános szerkezetét és alkalmasint ütközhet azzal a kívánalommal.

protected BBslider { /* . */ }. . Hogy miért nem tettem ezt. másrészt a virtuális bázisosztályok megvalósításának legnyilvánvalóbb és leggyakoribb módszerei annyira hely.. hogy kiterjedt használatuk egy osztályon belül nem vonzó.. protected BB_ival_slider { /* . protected BB_ival_slider { /* . Ekkor az is természetes lenne. class BB_popup_ival_slider : public virtual Popup_ival_slider. */ }. Ha az ötletet végigvisszük. Ábrával: Ival_slider BBslider Popup_ival_slider BB_ival_slider BB_popup_ival_slider Könnyen elképzelhetjük a Popup_ival_slider-bõl származó további felületeket és az azokból és a BB_popup_ival_slider-bõl származó további megvalósító osztályokat. legáltalánosabb. hogy az Ival_box-ot ábrázoló objektum általában csak egy mutatót tartalmaz a virtuális táblára.2. és legrugalmasabb megoldásnak tûnik. Ahogy a §15. */ }. Osztályhierarchiák 525 kívül azonosak lennének.... */ }.4 pontban láttuk.. egy változókat nem tartalmazó absztrakt osztály veszély nélkül ismételhetõ. Mielõtt ez a tárigényben és futási idõben mért költség visszariasztana bennünket egy más szemszögbõl vonzó szerkezet választásától.. gondoljuk meg. protected BBslider { /* . hogy az elõálló csúszka-osztályban elkerüljük az Ival_slider objektumok ismétlõdését.és idõigényesek. Így a virtuális bázisosztály helyett közönségeset alkalmazhatunk: class BB_ival_slider : public Ival_slider. */ }.. akkor a program felületét képezõ absztrakt osztályokból való minden származtatást virtuálissá teszünk.. class BB_popup_ival_slider : public Popup_ival_slider.15. class Popup_ival_slider : public Ival_slider { /* . Ehhez az Ival_slider-t virtuálissá tesszük: class BB_ival_slider : public virtual Ival_slider. */ }.. Ez valóban a leglogikusabb... class Popup_ival_slider : public virtual Ival_slider { /* . annak egyrészt a hagyományok figyelembe vétele az oka.

A virtuális bázisosztályok függvényeinek felülírása A származtatott osztályok felülírhatják (override) közvetlen vagy közvetett virtuális bázisosztályaik virtuális függvényeit. mint a felhasználói felület kezelésének részét: class Window { // . // háttérszín beállítása class Window_with_border : public virtual Window { // .5. így több származtatott osztály járulhat hozzá a virtuális bázisosztály által adott felület megvalósításához. bevallottan világosabb szerkezetnek. virtual void prompt() = 0. void set_color(Color). optimalizált változata az elõzõekben bemutatott. mint a színellenõrzõ séma részét. // háttérszín kezelése }. class Window_with_menu : public virtual Window { // .2. Ekkor a Window_with_border felülbírálhatja a set_color()-t.. 15.. . A Window osztálynak lehetnek például set_color() és prompt() függvényei.. a Window_with_menu pedig a prompt()ot. virtual void set_color(Color) = 0. }.. void prompt().526 Absztrakciós módszerek Ábrával: Ival_slider Ival_slider BBslider Popup_ival_slider BB_ival_slider BB_popup_ival_slider Ez valószínûleg megvalósítható. Két különbözõ osztály akár a virtuális bázisosztály különbözõ függvényeit is felülírhatja..1. // felhasználói tevékenységek kezelése }..

vagyis egy függvénynek az összeset felül kell írnia. Ilyenkor nem lehet virtuális függvénytáblát építeni.15. Osztályhierarchiák 527 class My_window : public Window_with_menu. void prompt(). pontbeli Radio nem adta volna meg a write() függvényt. Az olyan osztályokat.// a felhasználói tevékenységek kezelését nem hagyjuk a bázisosztályra }.. prompt() } Window_with_border { set_color() } Window_with_menu { prompt() } My_window { prompt() } Ha két osztály felülír egy bázisosztálybeli függvényt. amely felülírja a függvényt. az ilyen konfliktust a felülíró függvénynek a legtávolabbi származtatott osztályból való meghívásával oldhatjuk meg. Mint a Radio esetében is. public Window_with_border { // . Ábrával: Window { set_color(). mert a teljes objektumra vonatkozó függvényhívás kétértelmû lenne. }. ha különbözõ származtatott osztályok ugyanazt a függvényt írják felül? Ez csak akkor megengedett. ha az egyik származtatott osztály minden olyan osztály örököse. Mi történik. akkor az osztályhierarchia hibás.. ... A My_window például felülírhatja a prompt()-t. public Window_with_border { // . akkor a Receiver és Transmitter osztályokbeli write() deklarációk a Radio-ban hibát okoztak volna. de a két függvény egyike nem írja felül a másikat. amelyek egy virtuális bázisosztály némelyik (de nem mindegyik) függvényének megvalósítását tartalmazzák.2. hogy a Window_with_menu-belinél jobb változatot adjon: class My_window : public Window_with_menu. gyakran „mixin”-nek nevezik.4. Például ha a §15.

3.. az érdektelen a hozzáférés szempontjából. védett (protected) vagy nyilvános (public): ♦ Ha privát. §16. és az egyéb függvények.528 Absztrakciós módszerek 15. ha privát: . típusok. Hogy a név mit jelöl. Például egy hatékony nem tolakodó (non-intrusive. Ez azt jelenti.2. valamint az osztályból származtatott osztályok és barátaik tagfüggvényeiben lehet felhasználni.1) listaosztálynak valószínûleg szüksége van az elemeket nyilvántartó adatszerkezetekre. hogy egy osztályt háromféle függvény érhet el: az osztályt megvalósító függvények (azaz a barátok és a tagok). Ez azt a nézetet tükrözi. a nevét mindenhol fel lehet használni. a nevét csak a tagfüggvényekben és a deklaráló osztály barátaiban (friend-jeiben) lehet felhasználni. ♦ Ha nyilvános. Hozzáférés-szabályozás Egy osztálytag lehet privát (private). Az ilyen információ legjobb. állandók stb. mint privát adattagok. Ezt így ábrázolhatjuk: általános felhasználók származtatott osztály tagfüggvényei és barátai saját tagfüggvények és barátok nyilvános: védett: privát: A hozzáférési szabályok egyöntetûen vonatkoznak a nevekre. egy származtatott osztályt megvalósító függvények (azaz a származtatott osztályok barátai és a tagok). ♦ Ha védett. hogy ugyanúgy lehetnek privát tagfüggvények. a nevét csak a deklaráló osztály és barátai tagfüggvényeiben.

}. Chunk* allocated.. return p->val. Link* free.. T get(). p->next = free. struct Chunk { enum { chunk_size = 15 }. public: class Underflow { }. Chunk* next. Link* head. Osztályhierarchiák 529 template<class T> class List { private: struct Link { T val. }. Link v[chunk_size]. return p. // kivételosztály void insert(T).15. } . head = p->next. free = free->next. } template<class T> List<T>::Link* List<T>::get_free() { if (free == 0) { // új Chunk lefoglalása és Link-jeinek a szabad listára helyezése } Link* p = free. lnk->next = head. head = lnk. template<class T> void List<T>::insert(T val) { Link* lnk = get_free(). lnk->val = val. Link* p= head. }. Link* next. // . free = p. } template<class T> T List<T>::get() { if (head == 0) throw Underflow(). Link* get_free().

. if (List<T>::Chunk::chunk_size > 31) { // hiba: List<T>::Chunk::chunk_size privát // . Másfelõl a draw() mûveletek az általános felhasználást szolgálják. Ezt a különbséget a Window osztály felületének két. az általános felhasználás számára nem biztonságosak. nem kényelmesek... a struktúrák (struct) tagjai nyilvánosak (public. // hiba: List<T>::free privát // . Mivel a get_free() visszatérési értékét elõbb említjük. // egyéb kirajzoló kód private: // ábrázolás stb..2. A nem tag függvényeknek – a barát (friend) függvények kivételével – nincs ilyen hozzáférésük: void would_be_meddler(List<T>* p) { List<T>::Link* q = 0.8).1. . §10..3. }. protected: void own_draw().1 pontbeli Window példát. egy védett és egy nyilvános felületre való szétválasztásával lehet kifejezni: class Window_with_border { public: virtual void draw()...2..4. hogy csak a származtatott osztályok számára szolgáljanak építõkövekül. Az own_draw() függvények (akarattal) nem teljes körû szolgáltatást adnak. 15. // hiba: List<T>::Link privát // . q = p->free. } } Az osztályok (class) tagjai alapértelmezés szerint privátok (private). Védett tagok A védett (protected) tagok használatának bemutatására vegyük a §15. Arra a célra terveztük õket.530 Absztrakciós módszerek A List<T> hatókörbe azáltal lépünk be. a Link rövidítés helyett a teljes List<T>::Link nevet kell használnunk. mint a List<T>::get_free() nevet. hogy a tagfüggvényben List<T>::-t írunk. // .

// . mint a privátként bevezetettekkel. A mûveleteket megvalósító tagok és barátok ezen felhasználók érdekében kezelik az objektumokat.1. hogy a védett tagokat – csakúgy. hogy az egyik származtatott osztály összezavarja a másik származtatott osztály adatait. De ha származtatott osztályokat használunk. az osztályokra a privát az alapértelmezett hozzáférési kategória és általában ez a jobb választás. mint a nyilvánosakat – nem könnyû átszervezni.. A protected-ként deklarált tagokkal sokkal könnyebb visszaélni. Szerencsére nem kell feltétlenül védett tagokat használni.. mert nincs jó módszer az összes használat felderítésére. class Cyclic_buffer : public Buffer { // .. Még rosszabb. */ }.. Osztályhierarchiák 531 A származtatott osztály a bázisosztály védett tagjai közül csak a saját osztályába tartozó objektumokat tudja elérni: class Buffer { protected: char a[128]. }. . Ezért a tagok védettként való megadása általában tervezési hiba. A privát/nyilvános modell lehetõvé teszi a megvalósítás és az általános felhasználás pontos megkülönböztetését.. Ha jelentõs mennyiségû adatot úgy helyezünk el egy közös osztályban..3). Az én tapasztalatom az. az adatok sérülhetnek. amelyek azáltal léphetnének fel. class Linked_buffer : public Buffer { /* . hogy az összes származtatott osztály által közvetlenül használható. akkor kétféle felhasználója lesz egy osztálynak: a származtatott osztályok és „a nagyközönség”. így a védett tagok megnehezítik a program módosítását. void f(Linked_buffer* p) { a[0] = 0. de a származtatott osztályok megfelelõ kezelését nem. jelentõs mennyiségû adat közös osztályban való elhelyezése helyett mindig adódik más megoldás. A védett tagok használata Az adatok elrejtésének egyszerû privát/nyilvános modellje jól szolgálja a konkrét típusokat (§10. hogy az összes származtatott osztály használhatja azokat. // hiba: más típus védett tagját próbáltuk elérni } }.1. // rendben: Cyclic_buffer saját védett tagját éri el p->a[0] = 0. Ez megakadályozza az olyan hibákat. 15.3.15.

5). ami által erõsebb garanciák adhatók.. akkor nyilvános és védett tagjai csak D tagfüggvényeibõl és barátaiból érhetõk el. amikor egy osztályt a felület korlátozásával határozunk meg. Az osztály hozzáférési szintjét nem muszáj explicit megadni. A privát bázisosztályok akkor a leghasznosabbak. a protected minõsítõvel remekül adhatunk meg a származtatott osztályokban használható mûveleteket.. A §12.4.2) való minden hozzáférés ellenõrzött legyen. */ }. class Y : protected B { /* . a további öröklés lehetetlenné vált volna.. */ }. Ha ebben a példában a megvalósító osztály privát lett volna.7. Bázisosztályok elérése A tagokhoz hasonlóan egy bázisosztály is lehet nyilvános. */ }. ahol jellemzõen további öröklés történik (a §12. 15.. A bázisosztály hozzáférési szintje az osztály tagjainak elérhetõsége mellett a származtatott osztályra hivatkozó mutatóknak vagy referenciáknak a bázisosztály típusára való konvertálhatóságát is jelzi.2. Csak D barátai és tagjai konvertálhatnak egy D* mutatót B*-gá.3. */ }.. Ilyenkor az alapértelmezett hozzáférése class esetén privát. */ }.532 Absztrakciós módszerek Jegyezzük meg. ez az öröklés legáltalánosabb formája.4.1 tartalmaz.. Vegyünk egy B bázisosztályból származó D osztályt: ♦ Ha B privát bázisosztály.. hogy ezek a kifogások nem érvényesek a védett tagfüggvényekre.. A mutatókra vonatkozó Vector például érték-ellenõrzéssel bõvíti ki Vector<void*> bázisosztályát (§13. struct esetén nyilvános lesz: class XX : B { /* . bázisosztályát privátként kell megadnunk.2 pontbeli Ival_slider jó példa erre). így a Vec-ek nem konvertálódnak nem ellenõrzött vector-rá: template <class T > class Vec : private Vector <T> { /* .. védett vagy privát: class X : public B { /* .. // B nyilvános bázisosztály Az olvashatóság szempontjából azonban legjobb kiírni a hozzáférési szintet megadó kulcsszót. A védett és a privát öröklés a megvalósítás módjának jelölésére használatos. . */ }. A védett öröklés olyan osztályhierarchiákban a leghasznosabb. hogy a Vec-hez (§3.. // B privát bázisosztály struct YY : B { /* . A nyilvános öröklés a származtatott osztályt a bázisosztály egy altípusává (subtype) teszi. class Z : private B { /* .2 pontbeli Ival_slider is példa erre.11.. Ha azt is biztosítani szeretnénk. A tagok elérhetõségére példákat a §C.

Osztályhierarchiák 533 ♦ Ha B védett bázisosztály. Ez alapvetõen azonos a tagok elérhetõségi szabályaival (§15. class X2 : public B { /* .3.. ha valamelyik út mentén elérhetõ: struct B { int m. // . nyilvános tagjai bárhol használhatók. ♦ Ha B nyilvános bázisosztály. public X2 { /* . B* pb = pd. class DD : public D1.. attól még egyértelmûen. }. DD* pd = new DD.. akkor nyilvános és védett tagjai csak D tagfüggvényeibõl és barátaiból. Bármely függvény végezhet D*-ról B*-ra való konverziót. Ezenkívül védett tagjai D tagfüggvényeibõl és barátaiból.. A BBwindow-t például azért adtam meg az Ival_slider védett bázisosztályaként (§12. .4. mint felületének része...11. valamint a D-bõl származó osztályok tagfüggvényeibõl és barátaiból érhetõk el.3).. A bázisosztályok elérhetõségét ugyanazon szempontok szerint adjuk meg. */ } .15.. static int sm.. */ } . class D1 : public virtual B { /* . class XX : public X1. int i1 = pd->m. valamint a D-bõl származó osztályok tagfüggvényei és barátai végezhetnek konverziót D*-ról B*-ra. A többszörös öröklõdés és az elérhetõség Ha egy nevet vagy bázisosztályt egy többszörös öröklõdési háló több útvonalán is elérhetünk. valamint a D-bõl származó osztályok tagfüggvényeibõl és barátaiból érhetõk el.1. */ }. akkor abban az esetben lesz elérhetõ. mert a BBwindow inkább az Ival_slider megvalósításának. azaz többértelmûségi hiba nélkül hivatkozhatunk rá: class X1 : public B { /* .. class D2 : public virtual B { /* . */ } .2 mutat példákat. mert az Ival_slider-bõl további osztályokat akartam származtatni és azoknak el kellett érniük a megvalósítást. mint a tagokét.. Csak D tagfüggvényei és barátai. */ }..2.. private D2 { /* . A BBwindow-t azonban nem rejthettem el teljesen úgy. A bázisosztályok elérhetõségére a §C. hogy privát bázisosztállyá teszem.. */ } .2). 15. // rendben: elérhetõ D1-en keresztül // rendben: elérhetõ D1-en keresztül Ha egy bizonyos elemet több út mentén is elérhetünk.

2. . int i1 = pxx->m. public: int c. // hiba. class D : public B { public: using B::a.3.2 pontot.534 Absztrakciós módszerek XX* pxx = new XX. }. csak az egyébként is hozzáférhetõ adatok kényelmesebb használatát teszik lehetõvé. // hozzáférést ad a B::b és B::c nevekhez.2. using B::c. }. de a B::a-hoz nem Lásd még a §15. akkor a hozzáférési jog más felhasználók felé továbbadható: class B { private: int a. Másrészt viszont. A using deklarációk és az elérhetõség A using deklarációk nem szolgálhatnak több információ elérésére. // B::b nyilvánosan elérhetõ D-n keresztül }. többértelmû: XX::X1::B::m vagy XX::X2::B::m // rendben: csak egy B::sm van egy XX-ben 15. protected: int b. ha egy információ elérhetõ. // hiba: B::a privát using B::b. int i2 = pxx->sm.2. Ha egy using deklaráció privát vagy védett öröklõdéssel jár együtt. akkor a bázisosztály által rendesen felkínált szolgáltatások egy részéhez felületet adhat: class BB : private B { public: using B::b.

se nem kívánatos a rendszer és a program közötti ezen párbeszédben. Következésképpen egy objektum típusának futási idõben való lekérdezéséhez a legnyilvánvalóbb és leghasznosabb mûvelet az. valahányszor valamilyen tevékenység történt. Osztályhierarchiák 535 15. hogy a rendszer a my_event_handler()-t arra a BBwindow-ra hivatkozó mutatóval hívja meg. nem a mi alkalmazásunk osztályainak nyelvén. Például tegyük fel. Futási idejû típusinformáció A §12.4 pontban leírt Ival_box-ok valószerû használata lenne. Az objektum tényleges típusának kiderítése és megemlítése se nem szükséges. az objektumra hivatkozó mutatóra vagy referenciára van szükségünk. Az „elveszett” adatok visszanyeréséhez valahogy meg kell tudnunk kérdezni az objektumtól a típusát. egy jól megtervezett felület pedig elrejti a lényegtelen részleteket. Ez szükségszerû és rendjén is való. ha átadnánk azokat egy képernyõkezelõ rendszernek. ha nem. Sok felhasználói felület mûködik így. de azzal a kellemetlen következménnyel jár. hogy információt vesztünk a rendszernek átadott és késõbb nekünk visszaadott objektumok típusáról. ha az objektum a várt típusú. mondjuk Ival_slider. Az objektum az Ival_box egy bizonyos fajtája lesz. amellyel egy tevékenység történt. amely érvényes mutatót ad vissza. A rendszer felületét a rendszer saját osztályai és objektumai nyelvén adják meg. Ekkor az Ival_box osztály do_something() függvényét használva meghívhatnánk programunkat: void my_event_handler(BBwindow* pw) { if (Ival_box* pb = dynamic_cast<Ival_box*>(pw)) pb->do_something(). Bármilyen mûveletet akarunk is végezni az objektummal.4. Létezik felület a párbeszéd lényegének leírására. majd az visszaadná õket a programnak. alkalmas típusú. amelyet a BBwindow egy bizonyos típusa valósít meg. else { // hoppá! nem várt esemény } } // Vajon pw egy Ival_box-ra mutat? A folyamatot úgy is magyarázhatjuk. mondjuk a BBslider. . Fontos észrevenni. Egy felhasználói felületet kezelõ rendszer azonban nem feltétlenül tud a mi Ival_box-ainkról. Pontosan ezt teszi a dynamic_cast operátor.15. hogy a dynamic_cast fordít a felhasználói felületet kezelõ rendszer sajátos nyelvérõl az alkalmazás nyelvére. hogy mi nem nyert említést ebben a példában: az objektum tényleges típusa. illetve „nullpointert”.

4.536 Absztrakciós módszerek Rajzban a pb = dynamic_cast<Ival_box*>(pw) utasítás hatását így ábrázolhatjuk: pw BBwindow Ival_box pb BBslider Ival_slider BB_ival_slider A pw-bõl és pb-bõl kiinduló nyilak az átadott objektumra hivatkozó mutatókat jelölik. míg a többi nyíl az átadott objektum különbözõ részei közötti öröklõdési viszonyokat ábrázolja. A bázisosztályról testvérre – például BBwindow-ról Ival_box-ra – való konverziót „keresztbe történõ konverziónak” (crosscast) hívják. Ehhez hasonlóan a származtatott osztályról bázisosztályra történõ konverzió neve „felfelé történõ konverzió”. egy <> közé írt típust és egy () közé írt mutatót vagy referenciát. mert az öröklési fák a hagyományos ábrázolás szerint a gyökértõl „lefelé nõnek”. A bázisosztályról származtatott osztályra történõ konverziót gyakran „lefelé történõ vagy származtatott irányú konverziónak” (downcast) hívják.1. Vegyük elõször a mutató esetét: dynamic_cast<T *>(p) . A típusinformációk futási idõben való használatát hagyományosan futási idejû típusinformációnak (run-time type information) hívják és gyakran RTTI-nek rövidítik. Dynamic_cast A dynamic_cast (dinamikus típuskényszerítés) operátor két paramétert vár. 15. vagy „bázisirányú konverzió” (upcast).

ahol a konverzió nem sikerül és 0-át ad.. Azt azonban jó tudni. }.. }. // rendben: pbb2 értéke 0 lesz Ez nem túl érdekes. hogy a konverzió csak egyértelmûen azonosított objektumoknál mûködik. Osztályhierarchiák 537 Ha a p a T * típusba. Lehet olyan példákat hozni. a dynamic_cast<T *>(p) eredménye 0 lesz. // rendben Ival_slider* pi2 = dynamic_cast<Ival_slider*>(p). // nem többalakú bázisosztály (Date nem rendelkezik // virtuális függvényekkel) . // rendben } BBslider* pbb1 = p. ahol T bázisosztálya D-nek. Ha az objektum T osztályú vagy van egy egyértelmû T típusú õse. vagy olyan D * típusba tartozik.4. Ha p értéke 0. A dynamic_cast célja azon esetek kezelése. Jegyezzük meg. void f(BB_ival_slider* p) { Ival_slider* pi1 = p.. akkor a dynamic_cast egy.2). az objektumra hivatkozó T * típusú mutatót ad vissza.. hogy a dynamic_cast nem engedi meg a védett vagy privát bázisosztályok védelmének véletlen megsértését. A dynamic_cast-nak a lefelé vagy keresztbe történõ konvertáláshoz többalakú (polimorf) mutatóra vagy hivatkozásra van szüksége: class My_slider: public Ival_slider { // többalakú bázisosztály (Ival_slider rendelkezik // virtuális függvényekkel) // . // . // hiba: BBslider védett bázisosztály BBslider* pbb2 = dynamic_cast<BBslider*>(p). protected BBslider { // . mert a p által mutatott objektumnak több T típusú bázisosztályt képviselõ részobjektuma van (§15. class My_date : public Date { }. amelyeknél a fordítóprogram nem tudja a konverzió helyességét megítélni: dynamic_cast<T *>(p) A fenti kód megvizsgálja a p által mutatott objektumot (ha van ilyen).. mintha a p-t egy T * változónak adtuk volna értékül: class BB_ival_slider : public Ival_slider. akkor az eredmény ugyanaz.15.. más esetben 0-át.

hogy többalakú legyen. akkor pontos típusának ismerete nélkül nem kezelhetõ biztonságosan. nincs szükség dynamic_cast-ra. hogy a dynamic_cast hatékonyan felhasználható.. majd késõbb kicsomagoljuk belõle a konkrét típust: . Általános megoldás. nincs szükség hosszadalmas keresésre vagy karakterláncok összehasonlítására. hogy egy objektumokat kezelõ ki. My_date* pd2 = dynamic_cast<My_date*>(pd).5) helyezik. ezáltal egy „típus-információ objektumot” fûznek az objektumhoz: My_slider: . egyszerûsíti a dynamic_cast megvalósítását. hogy a típust jelzõ mutatót az objektum virtuális függvénytáblájába (§2. ha csak egy többalakú részobjektumra hivatkozó mutató adott.és bemeneti rendszeren keresztül továbbítsuk (§25. amelynek segítségével a teljes objektum kezdete megtalálható. mondjuk. } // rendben // hiba: Date nem többalakú Az a megkötés. mert megkönnyíti az objektum típusának tárolásához szükséges információ helyének megtalálását.. A dynamic_cast céltípusa nem kell. Date* pd) { My_slider* pd1 = dynamic_cast<My_slider*>(pb). hogy a mutatónak többalakúnak kell lennie. hogy az ilyen objektum ne kerüljön olyan környezetbe. Ha egy objektumnak nincs virtuális függvénye. ahol nem ismeretes a pontos típusa.. a bázisosztályt leíró type_info objektumot kell összehasonlítani..1). ezért ügyelni kell.538 Absztrakciós módszerek void g(Ival_box* pb. Ha azonban a típusa ismert. vtbl: type_info: "My_slider" bázisosztályok type_info: "Ival_slider" A szaggatott nyíl az eltolást (offset) jelöli.4. ezért egy konkrét típust többalakúba csomagolhatunk. A dynamic_cast-nak többalakú típusokra való korlátozása logikai szempontból nézve is értelmes.5. vptr . Világos. csak néhány.

azaz a mutató nem mutat semmilyen objektumra. se nem kívánatos. Következésképpen egy r referencia esetén a dynamic_cast<T&>(r) nem kérdés. Referenciák dinamikus típuskényszerítése Egy objektum többalakú (polymorph) viselkedéséhez akkor férünk hozzá. // . void f(Io_obj* pio) { Date* pd = dynamic_cast<Date*>(pio). Osztályhierarchiák 539 class Io_obj { virtual Io_obj* clone() = 0. hanem állítás: „a r által mutatott objektum T típusú.1. hogy egy referencia mindig egy objektumra vonatkozik. Ezért egy dynamic_cast mûvelet eredményét mindig ellenõriznünk kell..4. ha mutatón vagy referencián át kezeljük. A p mutatón végzett dynamic_cast<T*>(p) egy kérdésként fogható fel: „a p által mutatott objektum T típusú?” Másrészt viszont jogosan tételezhetjük fel. void* pd2 = dynamic_cast<void*>(pd).15.” A dynamic_cast mûvelet eredményét maga a dynamic_cast-ot megvalósító kód ellenõrzi automatikusan. // bázisosztály-objektum az I/O rendszer számára class Io_date : public Date. } Egy többalakú objektum kezdõcímét egy void*-ra való dinamikus típuskényszerítéssel határozhatjuk meg: void g(Ival_box* pb. public Io_obj { }. akkor bad_cast kivételt vált ki: . }. Ha egy mutatóról. Ez referenciákra se nem kivitelezhetõ. mint eredményrõl van szó. 15. és ha a dynamic_cast operandusa nem a várt típusú hivatkozás.1. A dynamic_cast mûvelet sikertelenségét 0-val jelzi. } // rendben // hiba: Date nem többalakú Ez azonban csak nagyon alacsony szintû függvényekkel való együttmûködés céljára hasznos. akkor fel kell készülnünk arra a lehetõségre. Date* pd) { void* pd1 = dynamic_cast<void*>(pb). hogy az eredmény 0 lesz..

15. Ival_box& r) { if (Ival_slider* is = dynamic_cast<Ival_slider*>(p)) { // Vajon p egy Ival_slider-re mutat? // 'is' használata } else { // *p nem slider } Ival_slider& is = dynamic_cast<Ival_slider&>(r). Többszörös öröklõdés használata esetén nincs egyetlen gyökér. amit g() elkap. illetve a referenciák közötti alapvetõ különbség tükrözõdik. // 'is' használata // az r egy Ival_slider-re hivatkozik! } A sikertelen dinamikus mutató.. A 0 érték ellenõrzése elhagyható. amely sikertelenség esetén a 0 érték visszaadása helyett kivételt vált ki (§15.540 Absztrakciós módszerek void f(Ival_box* p.*new BB_ival_slider). Ez önmagában nem bonyolítja nagyon a helyzetet. akkor írhat egy olyan konverziós függvényt. } f(new BBdial. } // a paraméterek Ival_box-ként // adódnak át // a paraméterek Ival_box-ként adódnak át // §14.4. Ha egy felhasználó védekezni akar a referencia-átalakítás sikertelensége ellen.*new BBdial). Ez egyszerû. így alkalmasint véletlenül el is fog maradni.. Osztályhierarchiák bejárása Ha csak egyszeres öröklõdést használunk. akkor egy megfelelõ kivételkezelõre van szüksége: void g() { try { f(new BB_ival_slider. akkor némi óvatossággal kell az adott osztályú objektumra vagy objektumokra hivatkoznunk. Ha ez aggasztja az olvasót. az osztály és bázisosztályai egyetlen bázisosztályban gyökerezõ fát alkotnak. de gyakran túl erõs megszorítást jelent.illetve referencia-átalakítások eredményének eltérésében a mutatók. } catch (bad_cast) { // . de ha egy osztály többször fordul elõ a hierarchiában. míg a második bad_cast kivételt vált ki. .10 Az f() függvény elsõ meghívása sikeresen fog visszatérni.8[1]).2.

1).4.4 és §15.. hogy alkalmas osztályt találjunk.2. Ilyenkor egyszerûen nem lehet tudni. */ }. Ábrával: Storable Component Component Receiver Transmitter Radio Itt a Radio objektumnak két Component osztályú részobjektuma van. Itt azt tekintjük át. De ha már kialakult egy bonyolultabb hierarchia. // pc = 0 } ... class Receiver : public Component { /* . */ }. a bázisosztályt vagy annak egy származtatott osztályát megjelenítõ objektumra hivatkozó mutatóra van szükségünk.4. public Transmitter { /* . class Transmitter : public Component { /* .. Hogy szemléltessük az elérhetõ módszereket és a rájuk vonatkozó szabályokat. Osztályhierarchiák 541 Természetesen a hierarchiákat igyekszünk annyira egyszerûnek venni – de nem egyszerûbbnek –. // . amennyire programunk megengedi. hamarosan szükségünk lesz annak bejárására (vagyis végignézésére). class Radio : public Receiver. Ez az igény két esetben merülhet fel. hogyan lehet típuskényszerítést (cast) használva a kívánt típusú mutatóhoz jutni. Néha kifejezetten meg akarunk nevezni egy bázisosztályt vagy annak egy tagját (például §15. */ }.. Máskor egy. ha adott egy mutató a teljes objektumra vagy annak valamely részobjektumára (§15. Ezért a Radio-n belüli. Storable-rõl Component-re való dinamikus típuskényszerítés többértelmû lesz és nullát ad.. amelyet felületként használhatunk. Component* pc = dynamic_cast<Component*>(ps). */ }.2.3 és §15..1). melyik Component-re gondolt a programozó: void h1(Radio& r) { Storable* ps = &r. vegyünk egy többször szereplõ és virtuális bázisosztályt egyaránt tartalmazó hálót: class Component : public virtual Storable { /* ...15..

például a Fortran vagy a C határozza meg.7) nem vizsgálja a kiinduló objektum típusát. Miért akarna valaki static_cast-ot használni egy osztályhierarchia bejárására? A dynamic_cast némileg növeli a futási idõt (§15. Közönséges bázisosztályok és lefelé (azaz a származtatott osztály felé történõ.1).2. Az ilyen kódok más módokon .4. Olyan objektum is lehet például virtuális bázisosztály. } A többértelmûségnek ez a fajta felderítése csak virtuális bázisosztályoknál szükséges. // rendben. §15. hogy millió sornyi kód van a dynamic_cast bevezetése elõtti idõkbõl. de a dynamic_cast megvalósításához szükséges információt a futási idejû típusinformáció tartalmazza. // .1). // a Storable a Radio virtuális bázisosztálya pr = static_cast<Radio*>(ps).4. így nem képes ezekre: void g(Radio& r) { Receiver* prec = &r. A static_cast (§6.4. hogy Component-re mutat-e { Component* pc = dynamic_cast<Component*>(ps).. nincs ellenõrzés pr = dynamic_cast<Radio*>(prec).. // a Receiver a Radio közönséges bázisosztálya Radio* pr = static_cast<Radio*>(prec). Ezekre vonatkozóan csak statikus adatok állnak rendelkezésre. 15.542 Absztrakciós módszerek Ez a többértelmûség fordítási idõben általában nem deríthetõ fel: void h2(Storable* ps) // ps-rõl nem tudjuk. de ennél jelentõsebb ok. // rendben. futási idejû ellenõrzés Storable* ps = &r. amelynek alapján meg lehetne keresni azon objektumokat.4) konverzió esetén a kívánt típusú részobjektum mindig egyértelmû (ha létezik). amelynek a memóriában való elhelyezkedését egy másik nyelv. mert egy nem többalakú objektum nem tárol olyan információt.1. Statikus és dinamikus konverzió A dynamic_cast mûvelet többalakú bázisosztályról származtatott osztályra vagy testvérosztályra való átalakítást tud végezni (§15. amelyeknek õse (bázisa). // hiba: virtuális bázisosztályról nem lehet átalakítani pr = dynamic_cast<Radio*>(ps). futási idejû ellenõrzés } A dynamic_cast-nak többalakú operandusra van szüksége. Ezzel egyenértékû többértelmûség lép fel felfelé (azaz a bázisosztály felé történõ) konverzió esetén és ezek a többértelmûségek fordítási idõben kiderülnek.2. // rendben.

A fordítóprogram nem tételezhet fel semmit egy void* mutató által mutatott memóriaterületrõl.4. const-ot nem konstanssá konvertálni pedig csak const_cast-tal lehet (§6..2. mint egyszerûen a memória egy része (§4. 15.9. Ehhez static_cast kell: Radio* f(void* p) { Storable* ps = static_cast<Storable*>(p). Ebbõl következik.7). megsemmisítése felülrõl lefelé történik. dynamic_cast<Receiver*>(pcr).2. Az ilyen – általában C stílusú típuskonverzióval (§6.. // hiba: nem férhet hozzá // hiba: nem férhet hozzá // hiba: const minõsítés nem vész el // hiba: const minõsítés nem vész el // rendben } Privát bázisosztályra nem lehet konvertálni. hogy az objektum típusa felõl érdeklõdõ dynamic_cast nem képes void*-ról konvertálni.1). Az osztályobjektumokat konstruktoraik építik fel a „nyers memóriából” és destruktoraik lefutásával válnak újra „nyers memóriává”. mind a static_cast tiszteletben tartja a const minõsítést és a hozzáférési korlátozásokat: class Users : private set<Person> { /* .2. return dynamic_cast<Radio*>(ps). void f(Users* pu. ha az objektumot eredetileg nem const-ként deklaráltuk (§10. így. és még akkor is csak úgy kapunk helyes eredményt. Osztályhierarchiák 543 biztosítják az alkalmazott típusátalakítások helyességét. } // Bízzunk a programozóban! Mind a dynamic_cast.15.7) íródott – kódban azonban gyakran maradhatnak rejtett hibák. Receiver* pr = const_cast<Receiver*>(pcr).6). használjuk a biztonságosabb dynamic_cast-ot. dynamic_cast<set<Person>*>(pu).3. Osztályobjektumok felépítése és megsemmisítése Egy valamilyen osztályba tartozó objektum több. const Receiver* pcr) { static_cast<set<Person>*>(pu). így a dynamic_cast-tal végeztetett ellenõrzés feleslegesnek tûnik. Az objektum felépítése alulról felfelé. */ }.. static_cast<Receiver*>(pcr). // . és az osztályobjektum olyan mértékben létezõ .7. hacsak lehet..

544 Absztrakciós módszerek objektum. de a sorrendet megfigyelhetjük.7) és a virtuális függvényekre vonatkozó szabályokban. Fontos tulajdonsága. A typeid() leginkább egy referenciával vagy mutatóval jelölt objektum típusának lekérdezésére használatos: void f(Shape& r. nem a Receiver. Ha operandusként egy típusnevet kap. Például tudni szeretnénk az objektum osztályának nevét vagy memóriakiosztását.4) hívunk akkor. amikor az objektum még nincs készen. a kivételkezelésre (§14. a <typeinfo> fejállományban definiált type_info nevû típusra való referenciát ad vissza. // ál-deklaráció Azaz a typeid() egy standard könyvtárbeli. ha kifejezést. Néha azonban alapvetõ fontosságú tudni az objektum pontos típusát. amennyire felépítése. Nem bölcs dolog az objektumfelépítés vagy -megsemmisítés sorrendjére támaszkodni.2 pontbeli hierarchia Component konstruktora egy virtuális függvényt hív. akkor a Storable vagy Component-beli változatot fogja meghívni. illetve megsemmisítése megtörtént. Transmitter vagy Radio-belit. valahogy így adhatnánk meg: class type_info. csak egy részben felépített objektum. Typeid és kiterjesztett típusinformáció A dynamic_cast operátor az objektumok típusára vonatkozó. dynamic_cast-ot vagy typeid-t (§15. Ha a typeid() függvény lenne. 15. Így a dynamic_cast a virtuális függvényekhez hasonlóan megõrzi a rugalmasságot és bõvíthetõséget. const type_info& typeid(type_name) throw(). // az r által hivatkozott objektum típusa typeid(*p). a kifejezés által jelölt objektumot ábrázoló type_info-ra fog hivatkozni.4. Az objektum létrehozásának ezen pontján az objektum még nem Radio.4.4. // a p által mutatott objektum típusa typeid(p). A typeid operátor ezt az operandusa típusát jelzõ objektum visszaadásával támogatja. Például ha a §15. hogy biztosítja a felhasználó kód helyes mûködését a programozó által használt osztályokból származó osztályokkal is. a typeid() az azt ábrázoló type_info-ra való referenciával tér vissza. Ennek fényében legjobb elkerülni a virtuális függvényeknek konstruktorból vagy destruktorból való meghívását. // ál-deklaráció const type_info& typeid(expression) throw(bad_typeid). // a mutató típusa. Shape* p) { typeid(r). vagyis Shape* (nem gyakori.4.4. ha virtuális függvényeket. leginkább tévedés) } . Ez tükrözõdik a futási idejû típusazonosításra (RTTI). futási idõben jelentkezõ információigény legnagyobb részét kielégíti.

így a megoldás útja a pontos típuson keresztül vezet (§15.4. type_info& operator=(const type_info&).15. // . sokkal egyszerûbb használat az osztály nevének diagnosztikai kimenet céljára való lekérdezése: #include<typeinfo> void g(Component* p) { cout << typeid(*p). Egyes esetekben azonban nem tételezhetõ minden egyes kezelt objektumra vonatkozó közös felület. Nem biztos. Egy másik. bool operator==(const type_info&) const. bool before(const type_info&) const.. Osztályhierarchiák 545 Ha a mutató vagy referencia operandus értéke 0. } . hogy a rendszer minden egyes típusát egyetlen type_info objektum képviseli. Ezért a == mûvelettel az egyenlõséget a type_info objektumok és nem az azokra hivatkozó mutatók esetében vizsgáljuk.. const char* name() const. Néha tudni akarjuk egy objektum pontos típusát.1).4. hogy valamilyen szabványos mûveletet végezzünk az egész objektumon (és nem csak annak egy õsén). így nem szükséges a pontos típus ismerete. A meghatározott rendezési sorrendnek nincs köze az öröklési viszonyokhoz. Ideális esetben az ilyen mûveletek virtuális függvények formájában állnak rendelkezésre. bool operator!=(const type_info&) const. Dinamikus csatolású könyvtárak használata esetén például valóban nehéz a több type_info objektumot elkerülõ megvalósítás elkészítése. a typeid() bad_typeid kivételt vált ki. A type_info megvalósítás-független része így néz ki: class type_info { public: virtual ~type_info(). }. private: type_info(const type_info&).name(). // többalakú // összehasonlítható // rendezés // a típus neve // másolás megakadályozása // másolás megakadályozása A before() függvény lehetõvé teszi a type_info objektumok rendezését.

2. Tegyük fel.4. C stílusú karakterláncokkal történik.2) // §17. // hasítóérték kiszámítása (§17. } }.546 Absztrakciós módszerek Az osztályok nevének szöveges ábrázolása az adott nyelvi változattól függ. void f(B* p) { Layout& x = layout_table[typeid(*p). amely minden felhasznált osztályról megmondja az objektum memóriakiosztását. Kiterjesztett típusinformáció Egy objektum pontos típusának meghatározása általában csak az elsõ lépés a rá vonatkozó részletesebb információk megszerzése és használata felé. Ezeket a leírókat egy map-be tehetem.TI_eq> icon_table. Layout> layout_table. Gondoljuk meg. hogy van egy eszközünk.4. const type_info* q) { return *p==*q.TI_hash. amelyek a rendszerhez tartozó memóriarészben vannak. // x használata } Valaki más egészen eltérõ információt adhat: struct TI_eq { bool operator()(const type_info* p. void g(B* p) { Icon& i = icon_table[&typeid(*p)].6 hash_map<const type_info*.6. // i használata } . }. struct TI_hash { int operator()(const type_info* p). 15.Icon. ezért ne próbáljuk meg a delete[ ] mûveletet alkalmazni rájuk. hogy egy program vagy programozást segítõ eszköz hogyan tudna futási idõben típusokról szóló információt adni a felhasználóknak. hogy annak alapján a felhasználói kód megtalálhassa a memóriakiosztási információt: map<string.name()].hash_fct.1.

hogy a típusokhoz egymástól teljesen független információkat rendeljenek: layout_table: "T" . .15. Az objektum memóriakiosztása A típus ikonos ábrázolása Ez nagyon fontos. &typeid(T) .. mert annak valószínûsége. } A dynamic_cast-nak a typeid helyett való használata alig javítana ezen a kódon.5.. hogy valaki információknak olyan halmazával tud elõállni..4. Az RTTI helyes és helytelen használata Csak szükség esetén használjunk explicit futási idejû típusinformációt.. a nullával egyenlõ. „olcsóbb” és – alkalmasint – jobban szerkesztett programokhoz vezet. Osztályhierarchiák 547 A typeid-ekhez információ rendelésének ez a módja több programozó vagy eszköz számára teszi lehetõvé. icon_table: . A statikus (fordítási idõben történõ) ellenõrzés biztonságosabb.. 15. amely egymagában minden felhasználó igényeit kielégíti... hogy egy rosszul álcázott switch utasítást írjunk: // a futási idejû típusinformáció helytelen használata void rotate(const Shape& r) { if (typeid(r) == typeid(Circle)) { // nem csinálunk semmit } else if (typeid(r) == typeid(Triangle)) { // háromszög forgatása } else if (typeid(r) == typeid(Square)) { // négyzet forgatása } // . A futási idejû típusinformációt például arra használhatjuk..

különben viszont szükséges. hogy a programot switch utasítások halmazaként építse fel. akkor az RTTI használata elkerülhetõ.. ilyen kódot tényleg írnak. class Ship : public Object { /* . */ }. Ezt a problémát a §24. nagymértékben a dinamikus típusellenõrzésre építõ nyelveken nevelkedtek.5. Futási idejû típusazonosítás helyett inkább virtuális függvényeket (§2.548 Absztrakciós módszerek Sajnos ez nem egy légbõl kapott példa.5. esetleg más.. } // futási idejû ellenõrzés . De ha a felhasználó hajlandó is a könyvtári osztályok módosítására. // . az ilyen módosítás problémákhoz vezethet.. Object* get(). §12. Az RTTI-nek egy egyszerû..3 némileg részletesebben tárgyalja.4. akik a Smalltalkon vagy a Lispen. }.4 pontbeli Ival_box használata ennek egy példája. a Pascalhoz. // ... Az RTTI helyes használata sokszor merül fel akkor. Ship* f(Ship* ps. Object* p = c->get(). // többalakú class Container : public Object { public: void put(Object*).6) használjunk a legtöbb olyan eset kezelésére.1 írja le.4. A §15. objektumokat kezelõ ki. vagy az Adához hasonló nyelveken nevelkedett programozó majdnem ellenállhatatlan kísértést érez. if (Ship* q = dynamic_cast<Ship*>(p)) { return q. */ }. Vegyük ezt a példát: // a futási idejû típusinformáció helytelen használata class Object { /* .. a Modula-2-höz. Például szükségessé válhat a virtuális függvények ál-megvalósítása olyan osztályok esetében. Container* c) { c->put(ps). amelyeknél azok nem szükségesek vagy nem értelmesek. Ha a felhasználó hajlandó és képes a könyvtári osztályok – például a BBwindow – módosítására. amikor a kódban valamilyen szolgáltatást egy bizonyos osztályhoz kapcsolunk és a felhasználó öröklõdéssel akar további szolgáltatásokat hozzáadni.. amikor a típuson alapuló futási idejû megkülönböztetés szükséges. Azok számára.2. csábító dolog az RTTI-t túlságosan általános típusokkal együtt használni. Ennek a kísértésnek általában ellen kell állni. A C-hez.és bemeneti rendszer elkészítésére szolgáló használatát a §25.

. virtual void quit() = 0. ha kizárólag egy adott típusú mutatót tartalmazó tároló sablonokat használunk: Ship* f(Ship* ps. Túlságosan általános. Osztályhierarchiák 549 else { } } // valami mást csinálunk (általában hibakezelést végzünk) Itt az Object osztály szükségtelen és mesterkélt. // . virtual void suspend() = 0. Ráadásul az ilyen kérések közvetett vagy közvetlen módon programoktól érkezhetnek. nagyon általános felülete. a képernyõn megjelenített objektumnak tudnia kell válaszolni. return c. amelyet sokféle használatra szántak..15. } Virtuális függvényekkel együtt használva így majdnem minden esetet megoldhatunk. list<Ship*>& c) { c. mert az adott alkalmazásban nem felel meg semmilyen elvonatkoztatási szintnek és a programozót egy megvalósítás-szintû fogalom használatára kényszeríti.5. virtual void full_size() = 0. Vegyük ezen elv egy egyszerû változatát: class Std_interface { public: virtual void start() = 0. virtual void small() = 0. Például sok „objektumorientált” felhasználói felület határoz meg egy sor kérést. Tagra hivatkozó mutatók Sok osztálynak van egyszerû. amelyre minden.push_front(ps). 15. Az ilyen jellegû problémákat gyakran jobban oldja meg. virtual ~Std_interface() {} . }.pop_front(). virtual void resume() = 0.

Ennek egyszerû módja az alkalmazandó mûveletet jelölõ karakterlánc küldése. De amíg egy egész számot a számítógépek kényelmesen kezelnek. Mondjuk a 2 jelenthetné a suspend-et. valahányszor a mûveletek halmaza megváltozik. Ehelyett küldhetnénk csak egy.550 Absztrakciós módszerek Minden mûvelet pontos jelentését az az objektum definiálja. de a fordító természetesen figyelembe veszi az adattagok. Ez gyakran túlságosan közvetettnek és fáradságosnak tûnik. valakinek pedig meg kell fejtenie. Például a suspend() meghívása céljából a „suspend” (felfüggesztés) szöveget küldhetnénk. p->suspend(). A C++ nyelv lehetõvé teszi az osztályok tagjainak közvetett elérését. A tagra hivatkozó mutató olyan érték. ha egyáltalán tartozik valamelyikhez. mint egy. az adott osztályhoz tartozó objektumban levõ tag helyére. Ugyancsak szükségünk lesz a suspend() révén felfüggesztendõ objektumra hivatkozó mutatóra vagy referenciára. akkor egy olyan mutatóra lesz szükségünk. közötti különbséget. Vegyünk egy igen egyszerû példát: typedef void (Std_interface::* Pstd_mem)(). amely meghatározza. ráadásul még mindig meg kell írnunk a kódot. melyik mûvelethez tartozik. a köztes rétegeket fel kellene újítani. Gondolhatunk rá úgy. amely az osztály egy tagját azonosítja. az emberek számára ez meglehetõsen zavaró lehet. Ha tudnának. ami az Std_interface::suspend() tagra mutat. void f(Std_interface* p) { Pstd_mem s = &Std_interface::suspend. hogy közvetlenül megneveznénk. } (p->*s)().). amelyre alkalmazzák. Valakinek azonban létre kell hoznia a karakterláncot. a virtuális és nem virtuális függvények stb. az alkalmazandó mûveletre vonatkozó adatot a kérés forrásától a fogadóhoz. Ha meg akarjuk hívni a suspend()-et valamely objektumra anélkül. full_size() stb. hogy a 2 a suspend()-et jelenti és meg kell hívnunk a suspend()-et. Vegyük az Std_interface-t. Ideális esetben az ilyen köztes rétegeknek nem kell semmit tudniuk az egyes mûveletekrõl (resume(). Gyakran a kérést kiadó személy vagy program és a fogadó objektum között egy szoftverréteg van. // közvetlen hívás // hívás tagra hivatkozó mutatón keresztül // tagra hivatkozó mutató . a mûveletet jelölõ egész értéket. Következésképpen az ilyen köztes rétegek csupán továbbítanak valamely.

* operátorokkal fejezhetjük ki. A közönséges függvénymutatókhoz hasonlóan a tagfüggvényekre hivatkozó mutatókat akkor használjuk. Az eredmény az m típusának megfelelõen használható. feltéve. az obj. Osztályhierarchiák 551 Egy tagra hivatkozó mutatót (pointer to member) úgy kapunk.15. mint egy változó címe vagy egy függvénymutató. A közönséges függvényekre hivatkozó mutatókhoz hasonlóan a nem virtuális tagfüggvényekre hivatkozó mutatókat sem lehet másik címtérnek átadni. ezért biztonságosan átadható egy másik címtérnek (address space). amikor a tag nevének ismerete nélkül akarunk egy függvényre hivatkozni. de a ->* vagy . ami egy bizonyos objektum egy bizonyos tagját azonosítja. az X::* forma viszont láthatóan nagyon szépen megfelel a hagyományos * deklarátornak. fully qualified) osztálytagra alkalmazzuk (például &Std_interface::suspend()). a tagra hivatkozó mutatókkal való bajlódás helyett. Ezt rajzzal így ábrázolhatjuk: vtbl: s p X::start X::suspend Mivel egy virtuális tagra hivatkozó mutató (a fenti példában s) egyfajta eltolás. melyik tagot akarjuk meghívni. hogy az objektum elhelyezkedése a kettõben azonos. olyasvalami keletkezik. nem függ az objektum helyétõl a memóriában. A C szintaxis áttekinthetõségének hiányát általában a typedef alkalmazásával ellensúlyozzák. . inkább egy adatszerkezeten belüli eltolásra (offszetre) vagy egy tömbbeli indexre hasonlít. Természetesen ha tudnánk. Például a p->*m az m-et a p által mutatott objektumhoz köti. közvetlenül meghívnánk azt.*m pedig az obj objektumhoz. A tagra hivatkozó mutató azonban nem egyszerûen egy mutató egy memóriaterületre.* mûvelet eredményét nem lehet késõbbi használatra félretenni. A kapcsolatot a ->* és . Az „X osztály egy tagjára hivatkozó mutató” típusú változókat az X::* formában deklarálhatjuk. hogy a címképzõ & operátort egy teljes nevû („teljesen minõsített”. Amikor egy tagra hivatkozó mutatót a megfelelõ típusú objektumra hivatkozó mutatóval párosítjuk. Egy m tagra hivatkozó mutatót egy objektummal kapcsolatban használhatunk.

oper() A tagfüggvényekre hivatkozó mutatók legfontosabb használatát a §3.12 pont írja le.Std_interface*> variable. string oper) { (variable[var]->*operation[oper])(). Bázis. A statikus tagok nem tartoznak egy bizonyos objektumhoz.8. amelyre a függvénymutatót alkalmaztuk.5 és §18. hogy egy bázisosztály egy tagjára hivatkozó mutatót biztonságosan értékül adhatunk egy származtatott osztálytagra hivatkozó mutatónak.5. Ezt a tulajdonságot gyakran kontravarianciának (contravariance) hívják: . }. de fordítva nem. Ez a függvénymutatóknak igen lényeges tulajdonsága. void call_member(string var.552 Absztrakciós módszerek Jegyezzük meg.. void (*p)() = &Task::schedule.Pstd_mem> operation. // rendben // hiba: egyszerû mutatót adtunk értékül // tagra hivatkozó mutatónak Az adattagokra hivatkozó mutatókat a §C. hogy a függvénymutatón keresztül meghívott függvény lehet virtuális is. void (Task::* pm)() = &Task::schedule. de gyakran többel. interpreter) függvény egy tagra hivatkozó mutató segítségével meghívhat egy karakterlánccal (vagyis szöveggel) megadott függvényt: map<string. Egy tolmácsoló (továbbító. static void schedule(). akkor azon objektum típusának megfelelõ suspend()-et kapjuk..és származtatott osztályok Egy származtatott osztály legalább azokkal a tagokkal rendelkezik. map<string. Ez azt jelenti. } // var. amelyeket a bázisosztályból örököl. így egy statikus tagra hivatkozó mutató csupán egy közönséges mutató: class Task { // .4 pontbeli mem_fun() függvényben vizsgálhatjuk meg.1. 15. Például amikor egy függvénymutatón keresztül meghívjuk a suspend()-et.

15. . hogy egy mutató soha nem mutat olyan objektumra. amelynek nincsenek meg a mutató típusa által ígért tulajdonságai. virtual void print(). hogy biztosítsa az alapvetõ garanciát arra.6. Osztályhierarchiák 553 class text : public Std_interface { public: void start().. amellyel pmi-nek kezdõértéket próbáltunk adni. Ebben az esetben az Std_interface::* bármilyen Std_interface-re alkalmazható.15. hogy egy származtatott osztályra hivatkozó mutatót értékül adhatunk a bázisosztályára hivatkozó mutatónak. public: // . hogy valaki az alapértelmezett viselkedésre számít vagy ezen függvények valamilyen más változatát már meg is írta.. Gyakran jobb megközelítés ezeket a mûveleteket egy bizonyos osztályra megírni. Ezen globális függvények kicserélése azonban nem a félénkeknek való. void suspend().6.2. Tegyük fel. hogy a §12.2.6-beli Employee osztályt és annak minden leszármazottját szeretnénk egyedi memóriafoglalóval (allokátorral) és -felszabadítóval (deallokátorral) ellátni: class Employee { // . private: vector s. void (text::*pmt)() = &Std_interface::start. // hiba // rendben A fel nem cserélhetõség ezen szabálya látszólag ellenkezik azzal a szabállyal.2). }.. // . Ezért aztán nincs meg a text::print tagjuk. Valójában azonban mindkét szabály azért van. és a legtöbb ilyen objektum vélhetõleg nem text típusú lesz. void (Std_interface::* pmi)() = &text::print. A kezdeti értékadás megtagadásával a fordítóprogram egy futási idejû hibától ment meg bennünket... Ez az osztály több származtatott osztály alapja is lehet.. A szabad tár Az operator new() és az operator delete() definiálásával át lehet venni a memóriakezelést egy osztálytól (§6. hiszen elõfordulhat.

ezért nincs this mutatójuk és nem változtatnak meg egy objektumot.6. void operator delete(void*. amit az Employee::operator new() foglalt le // felszabadítjuk a memóriát } } Az eddig rejtélyes size_t paraméter haszna most világossá válik. void* Employee::operator new(size_t s) { // lefoglal 's' bájtnyi memóriát és rá hivatkozó mutatót ad vissza } void Employee::operator delete(void* p. Honnan tudja a fordítóprogram a delete() operátort a megfelelõ méretettel ellátni? Könnyen. delete p. Ez azonban nehezebbé teszi egy általános célú memóriafoglaló sebességének és memóriafogyasztásának javítását. amíg a delete mûveletnek átadott típus azonos az objektum tényleges típusával. // . az Employee bázisosztályban meg kell adni egy virtuális destruktort: . Ez ugyanis a ténylegesen törlendõ objektum mérete. size_t s) { if (p) { // törlés csak ha p!=0. csak olyan tárterületet adnak. Természetesen az adott osztály egyedi memóriafoglalója tárolhatja az ilyen információt (mint ahogy az általános célúnak is meg kell ezt tennie) és nem törõdhet az operator delete() függvény size_t paraméterével.2. size_t). hogy 'p' 's' bájt memóriára mutat. ha egy Manager-t. §6.2 // feltesszük. akkor sizeof(Manager)-t kapunk.. void f() { Employee* p = new Manager. lásd §6.2.. } // problémás (a pontos típus ismerete elvész) Ekkor a fordítóprogram nem fogja tudni a pontos méretet. Az operator new() és az operator delete() tagfüggvények automatikusan statikusak lesznek. A tömb törléséhez hasonlóan itt is a programozó segítségére van szükség. }. Ha egy „sima” Employee-t törlünk. amelyet a konstruktorok feltölthetnek és a destruktorok kitakaríthatnak.554 Absztrakciós módszerek }. akkor paraméterértékként sizeof(Employee)-t.6. void* operator new(size_t). Ez azonban nincs mindig így: class Manager : public Employee { int level.

15. ha a származtatott osztályban nem szerepel felhasználói destruktor: void f() { Employee* p = new Manager. akkor is. size_t). .. vagy nem szabad felhasználni a felszabadítóban a size_t paramétert. Természetesen meg lehetett volna úgy tervezni a nyelvet. hogy minden belõle származtatott osztálynak lesz destruktora (és így tudja a méretet). Sõt ha az Employee-ben van destruktor. virtual ~Employee(). Osztályhierarchiák 555 class Employee { public: void* operator new(size_t).sizeof(Manager)) Vagyis ha olyan memóriafoglaló/felszabadító párt akarunk írni. Akár egy üres destruktor is megteszi: Employee::~Employee() { } A memória felszabadítását a (méretet ismerõ) destruktor végzi. de ez csak a kevésbé biztonságos rendszerekben lehetséges optimalizálások elõnyeirõl való lemondás árán történhetett volna. void operator delete(void*. amely származtatott osztályokkal is jól mûködik. hogy ne legyen szükség ilyen megfontolásokra. // most már jó (az Employee többalakú) } A memória lefoglalása egy (a fordítóprogram által létrehozott) hívással történik: Employee::operator new(sizeof(Manager)) A felszabadításról szintén egy (a fordítóprogram által létrehozott) hívás gondoskodik: Employee::operator delete(p. akkor vagy virtuális destruktort kell írni a bázisosztályban. delete p. akkor ez biztosítja. }.. // .

. a hívásban a második paraméter s*sizeof(Employee) +delta lett volna. Például olyan módokon mûködik együtt a memóriakezelõ eljárásokkal.2. }.556 Absztrakciós módszerek 15. void f(int s) { Employee* p = new Employee[s]. // . ahol a delta az adott fordítótól függõ minimális többlet..helyett kétparaméteres formában adtuk volna meg. Az operator new[ ]() és az operator delete[ ]() pontosan ugyanezt a szerepet játssza a tömbök esetében: class Employee { public: void* operator new[ ](size_t). A memóriát az alábbi hívás szabadítja fel: Employee::operator delete[ ](p). void operator delete[ ](void*). Ezért nincs konstruktorra hivatkozó mutató. nyilvánvaló a kérdés: „lehetnek-e a konstruktorok is virtuálisak?”. hogy nem.6. Memóriafoglalás tömbök számára Az operator new() és az operator delete() függvények lehetõvé teszik.6. hogy a programozó végezze az egyes objektumok számára a memóriafoglalást és -felszabadítást. „Virtuális konstruktorok” Miután virtuális destruktorokról hallottunk. } Itt a szükséges memóriát a Employee::operator new[ ](sizeof(Employee)*s+delta) hívás fogja biztosítani.. // felszabadít s*sizeof(Employee)+delta bájtot Az elemek s számát (illetve a delta-t) a rendszer „megjegyzi”. delete[ ] p. Ezért a konstruktor nem lehet virtuális. Ha a delete[ ]()-et egy. mint a közönséges függvények. ahogy a közönséges függvények nem. 15. Ráadásul a konstruktor nem egészen olyan. // . A kicsit hosszabb: nem. Egy objektum létrehozásához a konstruktornak tudnia kell a létrehozandó objektum pontos típusát... de a kívánt hatás könnyen elérhetõ. A rövid válasz az.1.

Cond(const Cond&). // . . gyakran hívják õket „virtuális konstruktornak”. } A p2-höz rendelt mutató megfelelõ. mert gyakran hasznos lehet. Egy származtatott osztály felülírhatja a new_expr() és/vagy clone() függvényt. Az Ival_box_maker osztály (§12. Expr(const Expr&). Itt az ötlet egy másik változatát mutatom be. } Cond* clone() { return new Cond(*this). Mivel a new_expr()-hez és a clone()-hoz hasonló függvények virtuálisak és közvetett úton objektumokat hoznak létre.. Ez kedvezõ. hogy megfelelõ objektumot hozzon létre. Cond* new_expr() { return new Cond(). bár az elnevezés némileg félrevezetõ. // alapértelmezett konstruktor // másoló konstruktor }. } // . } // ..15. hogy egy Expr osztályú objektumhoz a felhasználó „pontosan ugyanolyan típusú” objektumot tud létrehozni: void user(Expr* p) { Expr* p2 = p->new_expr(). Ez azt jelenti.4) pontosan erre a célra szolgált. }. bár ismeretlen típusú. virtual Expr* new_expr() { return new Expr().4. Osztályhierarchiák 557 Mindkét megszorítás megkerülhetõ egy konstruktort meghívó és a létrehozott objektumot visszaadó függvény készítésével.. ha a pontos típus ismerete nélkül tudunk új objektumot létrehozni. Mindegyik egy konstruktort használ. } virtual Expr* clone() { return new Expr(*this).... hogy egy saját típusú objektumot adjon vissza: class Cond : public Expr { public: Cond(). ahol egy osztály objektumai a felhasználóiknak képesek saját maguk másolatát átadni: class Expr { public: Expr().

.7. §15. [7] A protected-del szemben részesítsük elõnyben a private-et. Ugyanígy egy B& visszatérési érték D&-ra módosítható.4. [11] Ritkán – és lehetõleg csak felülíró virtuális függvényekben – használjuk a tagnevek explicit minõsítését feloldás céljára. hogy B nyilvános bázisosztálya D-nek.3.2. §15. ha egy hierarchia némely (de nem mindegyik) osztályára nézve közös dolgot akarunk kifejezni. } // hiba A felülíró függvények típusa ugyanaz kell. Tanácsok [1] Ha tulajdonságok unióját akarjuk kifejezni. // . §15.2. §15. §15..558 Absztrakciós módszerek A Cond::new_expr() és a Cond::clone() visszatérési típusa Cond* és nem Expr* .2. §15. ezért egy Cond információvesztés nélkül lemásolható („klónozható”): void user2(Cond* pc. mint a felülírt virtuális függvényé. §15. §15. §15.2.4. Jegyezzük meg. hogy a paramétertípusok hasonló módosítása típushibához vezetne (lásd §15.4.4. [10] A konstruktor vagy destruktor futása alatt ne hívjunk virtuális függvényt. §15. Cond* p3 = pe->clone(). de a visszatérési érték típusa kevésbé szigorúan kötött.4. Expr* pe) { Cond* p2 = pc->clone().4.5. hogy legyen.. használjunk közönséges többszörös öröklõdést.1. [5] Ha az adott osztályhierarchia bejárása elkerülhetetlen. §15.3. Vagyis ha az eredeti visszatérési érték B* volt. feltéve. [3] Használjunk virtuális bázisosztályt.8[12]).1. [2] A tulajdonságoknak a megvalósító kódtól való elválasztására használjunk többszörös öröklõdést.2. használjuk a dynamic_cast-ot. akkor a felülíró függvény visszatérési értéke lehet D* is.5. [9] Ha egy osztály definiálja az operator delete()-et.5. [6] A typeid helyett részesítsük elõnyben a dynamic_cast-ot.1.1. §15. 15.6.1. [4] Kerüljük a típuskényszerítést (cast).1. [8] Adattagokat ne adjunk meg védettként.3. akkor legyen virtuális destruktora.

hogy ott milyen mûveletek vannak. amely egy objektum létrehozása közben az RTTIhez viszonyítva mutatja be a konstruktorhívások sorrendjét. 2.4. (*2) Írjunk szabványos C++-programot. amely úgy viselkedik.2-beli clone() mûvelet egy olyan változatát. Írjuk le a paramétertípusokra vonatkozó felülírási szabályok egy biztonságos enyhítését. 7. Elõször a program helyes mûködésére összpontosítsunk.5) Rajzoljuk le a §15. Nézzünk bele egy grafikus könyvtárba. hogy közös bázisosztálya az adatbázisban mezõk soraként tárolt objektumoknak. amennyit csak tudunk. Gondoskodjunk arról. (*2) Írjunk olyan programot. amely ugyanúgy mûködik. 9. Hozzunk létre egy adatbázis-objektum osztályt egy mûvelethalmazzal. Készítsünk egy dcast sablont. 11. ami konverzió nélkül elrontana egy Derived típusú objektumot. hogy a program annyira „okos” legyen. Minden játékos lehessen élõ személy vagy számítógép. 8.8[3]-beli játék felhasználói felületén. írjunk le annyi C++-kulcsszót. 6. Készítsünk egy „egyszerû Arena”-t mint az Arena-ból származó osztályt.5) Írjuk meg a Reversi/Othello társasjáték egy változatát. hogyan lehet a dynamic_cast-ot megvalósítani. (*3) Javítsunk a §15. operátorokkal vagy írásjelekkel elválasztva. hogy nincs azonosítókkal. Gyakorlatok 1. (*2. hogy belenéznénk a könyvbe. amely alapján grafikus objektumok egy könyvtára számára közös bázisosztály lehet. amelyben legalább tíz különbözõ kulcsszó szerepel egymás után. (*3) Gondoljuk meg. mint a dynamic_cast. Vizsgáljunk meg egy adatbázis-könyvtárat. de a 0-val való visszatérés helyett bad_cast kivételt vált ki.8. Hasonlóan mutassuk be az objektum lebontását. mint a dynamic_cast. úgy. amely alapján elhihetõ. hogyan lehetne egy virtuális függvényt meghívni.11) teszi a lemásolt objektumot. és csak azután arra. 12. Határozzunk meg egy grafikus adatbázis-objektumot többszörös öröklõdéssel és anélkül.2. (*1) Írjunk olyan ptr_cast sablont. 4.15. (*3. (*3) Készítsünk egy grafikus objektumosztályt egy mûvelethalmazzal. Osztályhierarchiák 559 15. tehát egy Derived* paraméterû függvény felülírhat egy Base*-ot. hogy a rendszert a dcast vagy az elõzõleg megadott osztályok megváltoztatása nélkül lehessen új osztályokkal bõvíteni. Magyarázzuk el. hogy érdemes legyen ellene játszani. és hasonlítsuk össze a két megoldás elõnyeit és hátrányait. 5. Írjunk egy programot. hogy a függvényparaméterek típus-ellenõrzési szabályai a visszatérési értékre vonatkozóakhoz hasonlóan enyhítettek. hogy ott milyen mûveletek vannak. 3.6. . amely a paraméterben megkapott Arena-ba (§10. (*2) Anélkül. de csak az általunk meghatározott függvényekre és adatokra támaszkodik. (*2) Írjuk meg a §15. (*2) Tegyük fel.4-beli Radio objektum memóriakiosztásának egy lehetséges változatát.

hogyan bõvíthetjük a könyvtárat. Megvizsgáljuk a könyvtár szerkezetét és azokat az alapvetõ módszereket. pontosabban a rendszer fejlesztõi hogyan képzelték el a továbbfejlesztést. Bejárók és memóriafoglalók 20. A könyvtár szerkezete és a tárolók 17. Szabványos tárolók 18. Fejezetek 16. illetve szemléltessük azokat az általános módszereket. hogy megértsük. Ezenkívül bemutatjuk azt is. Algoritmusok és függvényobjektumok 19. Adatfolyamok 22. amelyeket tervezéskor és programozáskor használhatunk. Számok .Harmadik rész A standard könyvtár Ebben a részben a C++ standard könyvtárát mutatjuk be. hogyan kell használni ezt a könyvtárat. Célunk az. Karakterláncok 21. amelyeket az egyes elemek megvalósításához használtak.

Marcus.. Túlontúl sokat vesszõdtél már Marcus Cocozával.. míg végül valósággal a rabszolgája lettél.„. vajon milyen hatással lesz Marcus Cocoza boldogságára és tekintélyére. Szinte semmit sem teszel anélkül.. vagy elunja magát. Dehát mit számít mindez valójában? Az emberek az egész világon ostobaságokat mûvelnek. ím én adok neked most egy jótanácsot. hanem több ember légy. hogy Marcus Cocoza esetleg valami ostobaságot követ el.s te.. Szeretném ha felszabadulnál. Mostantól fogva ne csak egy. ha szíved újra megtalálná békéjét.” (Karen Blixen: Álmodozók.. oly sok mindent adtál nekem. Szüntelen félelemben élsz. hogy mindig csak ugyanaz a Marcus Cocoza vagy. annyi. Kertész Judit fordítása) . amennyit csak el tudsz gondolni. Sok ember légy. Hagyd el a régi játékot.. hogy ne mérlegelnéd.

1. Egyedi volt. az utóbbi esetben azonban ez nem alkalmazható. Egyszerû volt. függvényt. ami érdekes. hanem az. A standard könyvtár Minek kell szerepelnie a C++ standard könyvtárában? A programozó szempontjából az tûnik ideálisnak. hogy egy könyvtárban mi legyen benne. hogy a standard könyvtár milyen elemeket tartalmazzon. Biztos. ha egy könyvtárban megtalál minden olyan osztályt. hogy sikeres lesz!” (H. Az elõbbi kérdés esetében ésszerû megközelítés. jelentõs és eléggé általános. és sablont. így minden programozó számíthat rá. . hogy minden elérhetõ legyen. A standard könyvtár olyan eszköz. A kérdés azonban most nem az.16 A könyvtár szerkezete és a tárolók „Újdonság volt. amelyet minden C++-változat tartalmaz. Nelson) Tervezési szempontok a standard könyvtárhoz • A könyvtár szerkezete • Szabványos fejállományok • Nyelvi támogatás • A tárolók szerkezete • Bejárók • Bázisosztállyal rendelkezõ tárolók • Az STL tárolói • vector • Bejárók • Elemek elérése • Konstruktorok • Módosítók • Listamûveletek • Méret és kapacitás • vector<bool> • Tanácsok • Gyakorlatok 16.

Azzal.1) és bemeneti/kimeneti adatfolyamokat (21. Ugyanakkor a mintaillesztõ és a grafikai lehetõségek nem szerepelnek itt. amelyeket nem lehet a nyelven belül optimálisan elkészíteni minden rendszer számára (például sqrt(). Az utolsó pont különösen fontos a tervezés szempontjából. hogy ugyanazt az ötletet más-más formában valósítsák meg. A karakterlánc.4) kezelését. akkor azt megvalósíthatjuk külön.7) – csak azért helyeztek a standard könyvtárba. Támogatja a nyelv lehetõségeinek használatát.3 vagy memmove(). A könyvtár tervezésekor leginkább az utolsó három szerepkört vették figyelembe. 5.6) és a futási idejû típusinformáció (RTTI. asszociatív tömböket (map) (§17. §22.és listakezelõ lehetõségek például a standard könyvtárban kaptak helyet. . mert – bár tagadhatatlanul széles körben használatosak – nem kimondottan a külön fejlesztett könyvtárak közötti együttmûködést szolgálják.4. mert ezzel korlátozható a standard könyvtár hatóköre és gátat szabhatunk a szolgáltatások özönének.6). azt a lehetõséget is nyitva hagyjuk a további könyvtárak számára. Néhány további eszközt – például a véletlenszám-elõállítókat (§22. hogy a felhasználó ugyanolyan bemeneti/kimeneti felületet biztosíthasson saját típusaihoz. mert így használatuk kényelmes és hagyományosan itt a helyük. fejezet). Elérhetõvé teszi azokat a függvényeket. 6. Ha egy lehetõség nem feltétlenül szükséges a fenti szerepkörök teljesítéséhez. hogy kihagyunk egy szolgáltatást a standard könyvtárból. a standard könyvtáron kívül is. akkor a külön fejlesztett könyvtárak csak a beépített típusok segítségével mûködhetnének együtt egymással. §19. mint a könyvtár a beépített típusokhoz. amelyekre a programozó más rendszerre átvihetõ (hordozható) programok készítésekor támaszkodhat.4. Információkat ad az adott nyelvi változat egyedi tulajdonságairól. például listákat (§17. 2. 3. A hordozhatóság például olyan általános fogalom.7. A közös tárolótípusok (például a listák vagy asszociatív tömbök) pedig igen jelentõsek a külön fejlesztett könyvtárak kényelmes együttmûködésének biztosításában. Keretet biztosít a könyvtár elemeinek továbbfejlesztésére. Olyan magas szintû szolgáltatásokat nyújt. 4.2). További könyvtárak közös alapjául szolgál. rendezõ függvényeket (§18.2).2. például szabályokkal és eszközökkel segíti. amely fontos tervezési szempont minden egyedi célú könyvtár esetében. Ezek a szerepek erõsen összefüggnek.2. például megadja a legnagyobb float értéket (§22. §15.564 A standard könyvtár A C++ standard könyvtára a következõ szolgáltatásokat nyújtja: 1.1). Ha ez nem így történt volna. például a memóriakezelést (§6.

10. hiszen ugyanazok az adatok más-más szempont szerint is rendezhetõk. Legyen kényelmes. Nyújtson teljeskörû szolgáltatásokat ahhoz. hogy a késõbbi könyvtárak elõállításában komoly vetélytársa legyen a saját kezûleg elõállított függvényeknek. hogy a felhasználó paraméterként határozza meg az eljárásmódot. de ha egy feladat teljesítését vállalja. Elég hatékony legyen ahhoz. 5. Rossz megoldás például. akkor elegendõ eszközt kell biztosítania ahhoz. 7. hogy az egyes felhasználóknak és fejlesztõknek ne k